Backport of #1383.

Add logic on MediaAccessorController
This commit is contained in:
Benoît Burnichon
2015-08-12 00:20:41 +02:00
parent 657efc40e6
commit 0e8e462ca7
9 changed files with 414 additions and 45 deletions

View File

@@ -69,6 +69,7 @@ use Alchemy\Phrasea\Controller\Thesaurus\Thesaurus;
use Alchemy\Phrasea\Controller\Thesaurus\Xmlhttp as ThesaurusXMLHttp;
use Alchemy\Phrasea\Controller\User\Notifications;
use Alchemy\Phrasea\Controller\User\Preferences;
use Alchemy\Phrasea\ControllerProvider\MediaAccessor;
use Alchemy\Phrasea\Core\PhraseaExceptionHandler;
use Alchemy\Phrasea\Core\Provider\AccountServiceProvider;
use Alchemy\Phrasea\Core\Provider\AuthenticationManagerServiceProvider;
@@ -411,10 +412,12 @@ class Application extends SilexApplication
$this['log.channels'] = array('monolog', 'task-manager.logger');
$this->register(new LocaleServiceProvider());
$this->register(new MediaAccessor());
$this->mount('/include/minify/', new Minifier());
$this->mount('/permalink/', new Permalink());
$this->mount('/lightbox/', new Lightbox());
$this->mount($this['controller.media_accessor.route_prefix'], new MediaAccessor());
$app['plugins.directory'] = $app->share(function () {
$dir = __DIR__ . '/../../../plugins';

View File

@@ -0,0 +1,120 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2015 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Controller;
use Alchemy\Phrasea\Application;
use Firebase\JWT\JWT;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class MediaAccessorController
{
/** @var Application */
private $app;
/** @var array|\ArrayAccess */
private $keyStorage = [];
/** @var array */
private $allowedAlgorithms = [];
public function __construct(Application $app)
{
$this->app = $app;
}
/**
* @return \appbox
*/
public function getApplicationBox()
{
return $this->app['phraseanet.appbox'];
}
/**
* @param int $id
* @return \databox
*/
public function findDataboxById($id)
{
$appbox = $this->getApplicationBox();
return $appbox->get_databox($id);
}
/**
* @param array|\ArrayAccess $keyStorage
* @return $this
*/
public function setKeyStorage($keyStorage)
{
if (!is_array($keyStorage) && !$keyStorage instanceof \ArrayAccess) {
throw new \InvalidArgumentException(sprintf(
'expects $keyStorage to be an array or an instance of ArrayAccess, got %s',
is_object($keyStorage) ? get_class($keyStorage) : gettype($keyStorage)
));
}
$this->keyStorage = $keyStorage;
return $this;
}
/**
* @param array $allowedAlgorithms
* @return $this
*/
public function setAllowedAlgorithms(array $allowedAlgorithms)
{
$this->allowedAlgorithms = $allowedAlgorithms;
return $this;
}
public function showAction(Request $request, $token)
{
try {
$token = JWT::decode($token, $this->keyStorage, $this->allowedAlgorithms);
} catch (\Exception $exception) {
throw new BadRequestHttpException('invalid token', $exception);
}
if (! isset($token->sdef) || !is_array($token->sdef) || count($token->sdef) !== 3) {
throw new BadRequestHttpException('sdef should be a sub-definition identifier.');
}
list ($sbas_id, $record_id, $subdef) = $token->sdef;
try {
$databox = $this->findDataboxById($sbas_id);
$record = $databox->get_record($record_id);
$subDefinition = $record->get_subdef($subdef);
$permalink = $subDefinition->get_permalink();
} catch (\Exception $exception) {
throw new NotFoundHttpException('Media was not found', $exception);
}
$subRequest = Request::create(
(string) $permalink->get_url(),
'GET',
[],
$request->cookies->all(),
[],
$request->server->all()
);
if ($request->query->has('download')) {
$subRequest->query->set('download', $request->query->get('download'));
}
$response = $this->app->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false);
// Remove Caption link header as it contains permalink token.
$response->headers->remove('link');
return $response;
}
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2015 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\ControllerProvider;
use Alchemy\Phrasea\Controller\MediaAccessorController;
use Alchemy\Phrasea\Model\Provider\DefaultSecretProvider;
use Doctrine\ORM\EntityManager;
use Silex\Application;
use Silex\ControllerCollection;
use Silex\ControllerProviderInterface;
use Silex\ServiceProviderInterface;
class MediaAccessor implements ServiceProviderInterface, ControllerProviderInterface
{
public function register(Application $app)
{
$app['repo.secrets'] = $app->share(function (Application $app) {
/** @var EntityManager $manager */
$manager = $app['EM'];
return $manager->getRepository('Entities\Secret');
});
$app['provider.secrets'] = $app->share(function (Application $app) {
return new DefaultSecretProvider($app['repo.secrets'], $app['random.medium']);
});
$app['controller.media_accessor'] = $app->share(function (Application $app) {
$controller = new MediaAccessorController($app);
$controller
->setAllowedAlgorithms(['HS256'])
->setKeyStorage($app['provider.secrets']);
return $controller;
});
$app['controller.media_accessor.route_prefix'] = '/medias';
}
public function boot(Application $app)
{
// Intentionally left empty
}
public function connect(Application $app)
{
/** @var ControllerCollection $controllers */
$controllers = $app['controllers_factory'];
$controllers->get('/{token}', 'controller.media_accessor:showAction')
->bind('media_accessor');
return $controllers;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2015 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Model\Provider;
use Alchemy\Phrasea\Model\Repositories\SecretRepository;
use Entities\Secret;
use RandomLib\Generator;
/**
* This provider implements ArrayAccess to be used with php-jwt library
*/
class DefaultSecretProvider implements SecretProvider, \ArrayAccess
{
/** @var SecretRepository */
private $repository;
/** @var Generator */
private $generator;
public function __construct(SecretRepository $repository, Generator $generator)
{
$this->repository = $repository;
$this->generator = $generator;
}
public function getSecretForUser($userId)
{
$secret = $this->repository->findOneBy(['creatorId' => $userId], ['created' => 'DESC']);
if ($secret) {
return $secret;
}
$token = $this->generator->generateString(64, Generator::CHAR_ALNUM | Generator::CHAR_SYMBOLS);
$secret = new Secret($userId, $token);
$this->repository->save($secret);
return $secret;
}
public function offsetExists($offset)
{
return null !== $this->repository->find($offset);
}
public function offsetGet($offset)
{
$secret = $this->repository->find($offset);
if (!$secret instanceof Secret) {
throw new \RuntimeException('Undefined index: ' . $offset);
}
return $secret->getToken();
}
public function offsetSet($offset, $value)
{
throw new \LogicException('This ArrayAccess is non mutable.');
}
public function offsetUnset($offset)
{
throw new \LogicException('This ArrayAccess is non mutable.');
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2015 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Model\Provider;
use Entities\Secret;
interface SecretProvider
{
/**
* Get a secret for a user
* @param int $userId
* @return Secret
*/
public function getSecretForUser($userId);
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2015 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Model\Repositories;
use Doctrine\ORM\EntityRepository;
use Entities\Secret;
class SecretRepository extends EntityRepository
{
public function save(Secret $secret)
{
$this->_em->persist($secret);
$this->_em->flush($secret);
}
}

View File

@@ -15,12 +15,14 @@ use Alchemy\Phrasea\Account\CollectionRequestMapper;
use Alchemy\Phrasea\Account\Command\UpdateAccountCommand;
use Alchemy\Phrasea\Account\Command\UpdatePasswordCommand;
use Alchemy\Phrasea\Form\Login\PhraseaRenewPasswordForm;
use Alchemy\Phrasea\Model\Provider\SecretProvider;
use Alchemy\Phrasea\SearchEngine\SearchEngineOptions;
use Alchemy\Phrasea\SearchEngine\SearchEngineSuggestion;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Border\File;
use Alchemy\Phrasea\Border\Attribute\Status;
use Alchemy\Phrasea\Border\Manager as BorderManager;
use Firebase\JWT\JWT;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Alchemy\Phrasea\Core\PhraseaEvents;
@@ -583,7 +585,7 @@ class API_V1_adapter extends API_V1_Abstract
$record->substitute_subdef($request->get('name'), $media, $app, $adapt);
foreach ($record->get_embedable_medias() as $name => $media) {
if ($name == $request->get('name') &&
null !== ($subdef = $this->list_embedable_media($record, $media, $this->app['phraseanet.registry']))) {
null !== ($subdef = $this->listEmbeddableMedia($request, $record, $media, $this->app['phraseanet.registry']))) {
$ret[] = $subdef;
}
}
@@ -682,9 +684,9 @@ class API_V1_adapter extends API_V1_Abstract
foreach ($search_result->getResults() as $record) {
if ($record->is_grouping()) {
$ret['results']['stories'][] = $this->list_story($record, $request->get('_extended'));
$ret['results']['stories'][] = $this->list_story($request, $record, $request->get('_extended'));
} else {
$ret['results']['records'][] = $this->list_record($record, $request->get('_extended'));
$ret['results']['records'][] = $this->list_record($request, $record, $request->get('_extended'));
}
}
@@ -712,7 +714,7 @@ class API_V1_adapter extends API_V1_Abstract
list($ret, $search_result) = $this->prepare_search_request($request);
foreach ($search_result->getResults() as $record) {
$ret['results'][] = $this->list_record($record, $request->get('_extended'));
$ret['results'][] = $this->list_record($request, $record, $request->get('_extended'));
}
/**
@@ -748,7 +750,7 @@ class API_V1_adapter extends API_V1_Abstract
$record = $this->app['phraseanet.appbox']->get_databox($databox_id)->get_record($record_id);
$stories = array_map(function ($story) use ($that, $request) {
return $that->list_story($story, $request->get('_extended'));
return $that->list_story($request, $story, $request->get('_extended'));
}, array_values($record->get_grouping_parents()->get_elements()));
$result->set_datas(array(
@@ -823,7 +825,7 @@ class API_V1_adapter extends API_V1_Abstract
$mimes = $request->get('mimes', array());
foreach ($record->get_embedable_medias($devices, $mimes) as $name => $media) {
if (null !== $subdef = $this->list_embedable_media($record, $media, $this->app['phraseanet.registry'])) {
if (null !== $subdef = $this->listEmbeddableMedia($request, $record, $media, $this->app['phraseanet.registry'])) {
$ret[] = $subdef;
}
}
@@ -856,7 +858,7 @@ class API_V1_adapter extends API_V1_Abstract
$mimes = $request->get('mimes', array());
foreach ($record->get_embedable_medias($devices, $mimes) as $name => $media) {
if (null !== $subdef = $this->list_embedable_media($record, $media, $this->app['phraseanet.registry'])) {
if (null !== $subdef = $this->listEmbeddableMedia($request, $record, $media, $this->app['phraseanet.registry'])) {
$ret[] = $subdef;
}
}
@@ -971,7 +973,7 @@ class API_V1_adapter extends API_V1_Abstract
try {
$collection = collection::get_from_base_id($this->app, $request->get('base_id'));
$record->move_to_collection($collection, $this->app['phraseanet.appbox']);
$result->set_datas(array("record" => $this->list_record($record, $request->get('_extended'))));
$result->set_datas(array("record" => $this->list_record($request, $record, $request->get('_extended'))));
} catch (\Exception $e) {
$result->set_error_message(API_V1_result::ERROR_BAD_REQUEST, $e->getMessage());
}
@@ -993,7 +995,7 @@ class API_V1_adapter extends API_V1_Abstract
$databox = $this->app['phraseanet.appbox']->get_databox($databox_id);
try {
$record = $databox->get_record($record_id);
$result->set_datas(array('record' => $this->list_record($record, $request->get('_extended'))));
$result->set_datas(array('record' => $this->list_record($request, $record, $request->get('_extended'))));
} catch (NotFoundHttpException $e) {
$result->set_error_message(API_V1_result::ERROR_BAD_REQUEST, _('Record Not Found'));
} catch (\Exception $e) {
@@ -1017,7 +1019,7 @@ class API_V1_adapter extends API_V1_Abstract
$databox = $this->app['phraseanet.appbox']->get_databox($databox_id);
try {
$story = $databox->get_record($story_id);
$result->set_datas(array('story' => $this->list_story($story, $request->get('_extended'))));
$result->set_datas(array('story' => $this->list_story($request, $story, $request->get('_extended'))));
} catch (NotFoundHttpException $e) {
$result->set_error_message(API_V1_result::ERROR_BAD_REQUEST, _('Story Not Found'));
} catch (\Exception $e) {
@@ -1135,7 +1137,7 @@ class API_V1_adapter extends API_V1_Abstract
$result->set_datas(
array(
"basket" => $this->list_basket($Basket),
"basket_elements" => $this->list_basket_content($Basket, $request->get('_extended'))
"basket_elements" => $this->list_basket_content($request, $Basket, $request->get('_extended'))
)
);
@@ -1145,17 +1147,17 @@ class API_V1_adapter extends API_V1_Abstract
/**
* Retrieve elements of one basket
*
* @param Request $request
* @param \Entities\Basket $Basket
* @param bool $extended
*
* @return array
*/
protected function list_basket_content(\Entities\Basket $Basket, $extended = false)
protected function list_basket_content(Request $request, \Entities\Basket $Basket, $extended = false)
{
$ret = array();
foreach ($Basket->getElements() as $basket_element) {
$ret[] = $this->list_basket_element($basket_element, $extended);
$ret[] = $this->list_basket_element($request, $basket_element, $extended);
}
return $ret;
@@ -1164,17 +1166,17 @@ class API_V1_adapter extends API_V1_Abstract
/**
* Retrieve detailed information about a basket element
*
* @param Request $request
* @param \Entities\BasketElement $basket_element
* @param bool $extended
*
* @return array
*/
protected function list_basket_element(\Entities\BasketElement $basket_element, $extended = false)
protected function list_basket_element(Request $request, \Entities\BasketElement $basket_element, $extended = false)
{
$ret = array(
'basket_element_id' => $basket_element->getId(),
'order' => $basket_element->getOrd(),
'record' => $this->list_record($basket_element->getRecord($this->app), $extended),
'record' => $this->list_record($request, $basket_element->getRecord($this->app), $extended),
'validation_item' => null != $basket_element->getBasket()->getValidation(),
);
@@ -1330,7 +1332,13 @@ class API_V1_adapter extends API_V1_Abstract
'feed' => $this->list_publication($feed, $user),
'offset_start' => $offset_start,
'per_page' => $per_page,
'entries' => $this->list_publications_entries($feed, $request->get('_extended'), $offset_start, $per_page),
'entries' => $this->list_publications_entries(
$request,
$feed,
$request->get('_extended'),
$offset_start,
$per_page
),
);
$result->set_datas($data);
@@ -1363,7 +1371,13 @@ class API_V1_adapter extends API_V1_Abstract
'total_entries' => $feed->get_count_total_entries(),
'offset_start' => $offset_start,
'per_page' => $per_page,
'entries' => $this->list_publications_entries($feed, $request->get('_extended'), $offset_start, $per_page),
'entries' => $this->list_publications_entries(
$request,
$feed,
$request->get('_extended'),
$offset_start,
$per_page
),
));
return $result;
@@ -1391,7 +1405,7 @@ class API_V1_adapter extends API_V1_Abstract
throw new \API_V1_exception_forbidden('You have not access to the parent feed');
}
$data = array('entry' => $this->list_publication_entry($entry),);
$data = array('entry' => $this->list_publication_entry($request, $entry),);
$result->set_datas($data);
@@ -1424,20 +1438,26 @@ class API_V1_adapter extends API_V1_Abstract
/**
* Retrieve all entries of one feed
*
* @param Request $request
* @param Feed_Abstract $feed
* @param bool $extended
* @param int $offset_start
* @param int $how_many
*
* @param bool $extended
* @param int $offset_start
* @param int $how_many
* @return array
*/
protected function list_publications_entries(Feed_Abstract $feed, $extended = false, $offset_start = 0, $how_many = 5)
protected function list_publications_entries(
Request $request,
Feed_Abstract $feed,
$extended = false,
$offset_start = 0,
$how_many = 5
)
{
$entries = $feed->get_entries($offset_start, $how_many)->get_entries();
$out = array();
foreach ($entries as $entry) {
$out[] = $this->list_publication_entry($entry, $extended);
$out[] = $this->list_publication_entry($request, $entry, $extended);
}
return $out;
@@ -1446,16 +1466,16 @@ class API_V1_adapter extends API_V1_Abstract
/**
* Retrieve detailed information about one feed entry
*
* @param Request $request
* @param Feed_Entry_Adapter $entry
* @param bool $extended
*
* @return array
*/
protected function list_publication_entry(Feed_Entry_Adapter $entry, $extended = false)
protected function list_publication_entry(Request $request, Feed_Entry_Adapter $entry, $extended = false)
{
$items = array();
foreach ($entry->get_content() as $item) {
$items[] = $this->list_publication_entry_item($item, $extended);
$items[] = $this->list_publication_entry_item($request, $item, $extended);
}
return array(
@@ -1477,16 +1497,16 @@ class API_V1_adapter extends API_V1_Abstract
/**
* Retrieve detailed information about one feed entry item
*
* @param Request $request
* @param Feed_Entry_Item $item
* @param bool $extended
*
* @return array
*/
protected function list_publication_entry_item(Feed_Entry_Item $item, $extended = false)
protected function list_publication_entry_item(Request $request, Feed_Entry_Item $item, $extended = false)
{
$data = array(
'item_id' => $item->get_id(),
'record' => $this->list_record($item->get_record(), $extended)
'record' => $this->list_record($request, $item->get_record(), $extended)
);
return $data;
@@ -1523,13 +1543,18 @@ class API_V1_adapter extends API_V1_Abstract
/**
* Retrieve detailed information about one sub definition
*
* @param Request $request
* @param record_adapter $record
* @param media_subdef $media
* @param registryInterface $registry
*
* @return array|null
*/
protected function list_embedable_media(\record_adapter $record, media_subdef $media, registryInterface $registry)
protected function listEmbeddableMedia(
Request $request,
\record_adapter $record,
media_subdef $media,
registryInterface $registry
)
{
if (!$media->is_physically_present()) {
return null;
@@ -1551,6 +1576,15 @@ class API_V1_adapter extends API_V1_Abstract
$permalink = null;
}
$urlTTL = (int)$request->get(
'subdef_url_ttl',
$registry->get('GV_default_subdef_url_ttl')
);
if ($urlTTL < 0) {
$urlTTL = -1;
}
$issuer = $this->app['authentication']->getUser();
return array(
'name' => $media->get_name(),
'substituted' => $media->is_substituted(),
@@ -1563,9 +1597,31 @@ class API_V1_adapter extends API_V1_Abstract
'devices' => $media->getDevices(),
'player_type' => $media->get_type(),
'mime_type' => $media->get_mime(),
'url' => $this->generateSubDefinitionUrl($issuer, $media, $urlTTL),
'url_ttl' => $urlTTL,
);
}
private function generateSubDefinitionUrl(User_Adapter $issuer, media_subdef $subdef, $url_ttl)
{
$payload = [
'iat' => time(),
'iss' => $issuer->get_id(),
'sdef' => [$subdef->get_sbas_id(), $subdef->get_record_id(), $subdef->get_name()],
];
if ($url_ttl >= 0) {
$payload['exp'] = $payload['iat'] + $url_ttl;
}
/** @var SecretProvider $provider */
$provider = $this->app['provider.secrets'];
$secret = $provider->getSecretForUser($issuer->get_id());
return $this->app->url('media_accessor', [
'token' => JWT::encode($payload, $secret->getToken(), 'HS256', $secret->getId()),
]);
}
/**
* Retrieve detailed information about one permalink
*
@@ -1713,12 +1769,12 @@ class API_V1_adapter extends API_V1_Abstract
/**
* Retrieve detailed information about one record
*
* @param Request $request
* @param record_adapter $record
* @param bool $extended
*
* @return array
*/
public function list_record(record_adapter $record, $extended = false)
public function list_record(Request $request, record_adapter $record, $extended = false)
{
$technicalInformation = array();
foreach ($record->get_technical_infos() as $name => $value) {
@@ -1739,7 +1795,12 @@ class API_V1_adapter extends API_V1_Abstract
'collection_id' => $record->get_collection_id(),
'base_id' => $record->get_base_id(),
'sha256' => $record->get_sha256(),
'thumbnail' => $this->list_embedable_media($record, $record->get_thumbnail(), $this->app['phraseanet.registry']),
'thumbnail' => $this->listEmbeddableMedia(
$request,
$record,
$record->get_thumbnail(),
$this->app['phraseanet.registry']
),
'technical_informations' => $technicalInformation,
'phrasea_type' => $record->get_type(),
'uuid' => $record->get_uuid(),
@@ -1749,7 +1810,7 @@ class API_V1_adapter extends API_V1_Abstract
$subdefs = $caption = array();
foreach ($record->get_embedable_medias(array(), array()) as $name => $media) {
if (null !== $subdef = $this->list_embedable_media($record, $media, $this->app['phraseanet.registry'])) {
if (null !== $subdef = $this->listEmbeddableMedia($request, $record, $media, $this->app['phraseanet.registry'])) {
$subdefs[] = $subdef;
}
}
@@ -1778,21 +1839,22 @@ class API_V1_adapter extends API_V1_Abstract
/**
* Retrieve detailed information about one story
*
* @param Request $request
* @param record_adapter $story
* @param bool $extended
*
* @return array
* @throws API_V1_exception_notfound
* @throws Exception
*/
public function list_story(record_adapter $story, $extended = false)
public function list_story(Request $request, record_adapter $story, $extended = false)
{
if (!$story->is_grouping()) {
throw new \API_V1_exception_notfound('Story not found');
}
$that = $this;
$records = array_map(function (\record_adapter $record) use ($that, $extended) {
return $that->list_record($record, $extended);
$records = array_map(function (\record_adapter $record) use ($that, $extended, $request) {
return $that->list_record($request, $record, $extended);
}, array_values($story->get_children()->get_elements()));
$caption = $story->get_caption();
@@ -1815,7 +1877,12 @@ class API_V1_adapter extends API_V1_Abstract
'updated_on' => $story->get_modification_date()->format(DATE_ATOM),
'created_on' => $story->get_creation_date()->format(DATE_ATOM),
'collection_id' => phrasea::collFromBas($this->app, $story->get_base_id()),
'thumbnail' => $this->list_embedable_media($story, $story->get_thumbnail(), $this->app['phraseanet.registry']),
'thumbnail' => $this->listEmbeddableMedia(
$request,
$story,
$story->get_thumbnail(),
$this->app['phraseanet.registry']
),
'uuid' => $story->get_uuid(),
'metadatas' => array(
'@entity@' => self::OBJECT_TYPE_STORY_METADATA_BAG,

View File

@@ -1,5 +1,6 @@
Entities\Secret:
type: entity
repositoryClass: Alchemy\Phrasea\Model\Repositories\SecretRepository
table: Secrets
id:
id:

View File

@@ -1041,7 +1041,7 @@ abstract class ApiAbstract extends \PhraseanetWebTestCaseAbstract
/**
* @covers \API_V1_adapter::get_record_embed
* @covers \API_V1_adapter::list_embedable_media
* @covers \API_V1_adapter::listEmbeddableMedia
* @covers \API_V1_adapter::list_permalink
*/
public function testRecordsEmbedRoute()
@@ -1129,7 +1129,7 @@ abstract class ApiAbstract extends \PhraseanetWebTestCaseAbstract
/**
* @covers \API_V1_adapter::get_record_embed
* @covers \API_V1_adapter::list_embedable_media
* @covers \API_V1_adapter::listEmbeddableMedia
* @covers \API_V1_adapter::list_permalink
*/
public function testStoriesEmbedRoute()