diff --git a/bin/console b/bin/console index 65b3a681fd..9a3d2317e7 100755 --- a/bin/console +++ b/bin/console @@ -12,6 +12,8 @@ namespace KonsoleKommander; use Alchemy\Phrasea\Command\Plugin\ListPlugin; +use Alchemy\Phrasea\Command\Setup\H264ConfigurationDumper; +use Alchemy\Phrasea\Command\Setup\H264MappingGenerator; use Alchemy\Phrasea\Command\SearchEngine\IndexFull; use Alchemy\Phrasea\Command\WebsocketServer; use Alchemy\Phrasea\Core\Version; @@ -108,6 +110,8 @@ $cli->command(new AddPlugin()); $cli->command(new ListPlugin()); $cli->command(new RemovePlugin()); $cli->command(new Configuration()); +$cli->command(new H264ConfigurationDumper()); +$cli->command(new H264MappingGenerator()); $cli->command(new XSendFileConfigurationDumper()); $cli->command(new XSendFileMappingGenerator()); diff --git a/config/configuration.sample.yml b/config/configuration.sample.yml index 92f738ce90..1aa6447925 100644 --- a/config/configuration.sample.yml +++ b/config/configuration.sample.yml @@ -179,4 +179,8 @@ xsendfile: enabled: false type: nginx mapping: [] +h264-pseudo-streaming: + enabled: false + type: nginx + mapping: [] plugins: [] diff --git a/lib/Alchemy/Phrasea/Command/Setup/H264ConfigurationDumper.php b/lib/Alchemy/Phrasea/Command/Setup/H264ConfigurationDumper.php new file mode 100644 index 0000000000..103efb04f4 --- /dev/null +++ b/lib/Alchemy/Phrasea/Command/Setup/H264ConfigurationDumper.php @@ -0,0 +1,56 @@ +setDescription('Dump the virtual host configuration depending on Phraseanet configuration'); + } + + /** + * {@inheritdoc} + */ + protected function doExecute(InputInterface $input, OutputInterface $output) + { + $output->writeln(''); + + if (!$this->container['phraseanet.h264-factory']->isH264Enabled()) { + $output->writeln('H264 pseudo streaming support is disabled'); + $ret = 1; + } else { + $output->writeln('H264 pseudo streaming support is enabled'); + $ret = 0; + } + + try { + $configuration = $this->container['phraseanet.h264-factory']->createMode(true, true)->getVirtualHostConfiguration(); + $output->writeln('H264 pseudo streaming configuration seems OK'); + $output->writeln($configuration); + } catch (RuntimeException $e) { + $output->writeln('H264 pseudo streaming configuration seems invalid'); + $ret = 1; + } + + $output->writeln(''); + + return $ret; + } +} diff --git a/lib/Alchemy/Phrasea/Command/Setup/H264MappingGenerator.php b/lib/Alchemy/Phrasea/Command/Setup/H264MappingGenerator.php new file mode 100644 index 0000000000..a897bfa88a --- /dev/null +++ b/lib/Alchemy/Phrasea/Command/Setup/H264MappingGenerator.php @@ -0,0 +1,111 @@ +addOption('write', 'w', null, 'Writes the configuration') + ->addOption('enabled', 'e', null, 'Set the enable toggle to `true`') + ->addArgument('type', InputArgument::REQUIRED, 'The configuration type, either `nginx` or `apache`') + ->setDescription('Generates Phraseanet H264 pseudo streaming mapping configuration depending on databoxes configuration'); + } + + /** + * {@inheritdoc} + */ + protected function doExecute(InputInterface $input, OutputInterface $output) + { + $paths = $this->extractPath($this->container['phraseanet.appbox']); + foreach ($paths as $path) { + $this->container['filesystem']->mkdir($path); + } + + $type = strtolower($input->getArgument('type')); + $enabled = $input->getOption('enabled'); + + $factory = new H264Factory($this->container['monolog'], true, $type, $this->computeMapping($paths)); + $mode = $factory->createMode(true); + + $currentConf = isset($this->container['phraseanet.configuration']['h264-pseudo-streaming']) ? $this->container['phraseanet.configuration']['h264-pseudo-streaming'] : array(); + $currentMapping = (isset($currentConf['mapping']) && is_array($currentConf['mapping'])) ? $currentConf['mapping'] : array(); + + $conf = array( + 'enabled' => $enabled, + 'type' => $type, + 'mapping' => array_replace_recursive($mode->getMapping(), $currentMapping), + ); + + if ($input->getOption('write')) { + $output->write("Writing configuration ..."); + $this->container['phraseanet.configuration']['h264-pseudo-streaming'] = $conf; + $output->writeln(" OK"); + $output->writeln(""); + $output->write("It is now strongly recommended to use h264-pseudo-streaming:dump-configuration command to upgrade your virtual-host"); + } else { + $output->writeln("Configuration will not be written, use --write option to write it"); + $output->writeln(""); + $output->writeln(Yaml::dump(array('h264-pseudo-streaming' => $conf), 4)); + } + + return 0; + } + + private function computeMapping($paths) + { + $paths = array_unique($paths); + + $ret = array(); + + foreach ($paths as $path) { + $ret[$path] = $this->pathsToConf($path); + } + + return $ret; + } + + private function pathsToConf($path) + { + static $n = 0; + $n++; + + return array('mount-point' => 'mp4-videos-'.$n, 'directory' => $path, 'passphrase' => \random::generatePassword(32)); + } + + private function extractPath(\appbox $appbox) + { + $paths = array(); + + foreach ($appbox->get_databoxes() as $databox) { + foreach ($databox->get_subdef_structure() as $group => $subdefs) { + if ('video' !== $group) { + continue; + } + foreach ($subdefs as $subdef) { + $paths[] = $subdef->get_path(); + } + } + } + + return array_filter(array_unique($paths)); + } +} diff --git a/lib/Alchemy/Phrasea/Command/Setup/XSendFileMappingGenerator.php b/lib/Alchemy/Phrasea/Command/Setup/XSendFileMappingGenerator.php index e673827d18..8c61a92f24 100644 --- a/lib/Alchemy/Phrasea/Command/Setup/XSendFileMappingGenerator.php +++ b/lib/Alchemy/Phrasea/Command/Setup/XSendFileMappingGenerator.php @@ -12,6 +12,7 @@ namespace Alchemy\Phrasea\Command\Setup; use Alchemy\Phrasea\Command\Command; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Alchemy\Phrasea\Http\XSendFile\XSendFileFactory; @@ -25,7 +26,7 @@ class XSendFileMappingGenerator extends Command $this->addOption('write', 'w', null, 'Writes the configuration') ->addOption('enabled', 'e', null, 'Set the enable toggle to `true`') - ->addArgument('type', null, 'The configuration type, either `nginx` or `apache`') + ->addArgument('type', InputArgument::REQUIRED, 'The configuration type, either `nginx` or `apache`') ->setDescription('Generates Phraseanet xsendfile mapping configuration depending on databoxes configuration'); } diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1.php b/lib/Alchemy/Phrasea/Controller/Api/V1.php index 3991c9b4f0..7a691543df 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1.php @@ -231,6 +231,8 @@ class V1 implements ControllerProviderInterface $controllers->get('/stories/{any_id}/{anyother_id}/', 'controller.api.v1:getBadRequest'); + $controllers->get('/me/', 'controller.api.v1:get_current_user'); + return $controllers; } @@ -827,14 +829,16 @@ class V1 implements ControllerProviderInterface return $checker->getMessage($app['translator']); }, iterator_to_array($file->getChecks())); - $usr_id = null; + $usr_id = $user = null; if ($file->getSession()->getUser()) { - $usr_id = $file->getSession()->getUser()->getId(); + $user = $file->getSession()->getUser(); + $usr_id = $user->getId(); } $session = [ 'id' => $file->getSession()->getId(), 'usr_id' => $usr_id, + 'user' => $user ? $this->list_user($user) : null, ]; return [ @@ -1265,6 +1269,13 @@ class V1 implements ControllerProviderInterface return Result::create($request, $ret)->createResponse(); } + public function get_current_user(Application $app, Request $request) + { + $ret = ["user" => $this->list_user($app['authentication']->getUser())]; + + return Result::create($request, $ret)->createResponse(); + } + /** * Retrieve elements of one basket * @@ -1310,6 +1321,7 @@ class V1 implements ControllerProviderInterface 'can_agree' => $participant->getCanAgree(), 'can_see_others' => $participant->getCanSeeOthers(), 'readonly' => $user->getId() != $app['authentication']->getUser()->getId(), + 'user' => $this->list_user($user), ], 'agreement' => $validation_datas->getAgreement(), 'updated_on' => $validation_datas->getUpdated()->format(DATE_ATOM), @@ -1567,6 +1579,9 @@ class V1 implements ControllerProviderInterface */ private function list_permalink(\media_Permalink_Adapter $permalink) { + $downloadUrl = $permalink->get_url(); + $downloadUrl->getQuery()->set('download', '1'); + return [ 'created_on' => $permalink->get_created_on()->format(DATE_ATOM), 'id' => $permalink->get_id(), @@ -1575,8 +1590,8 @@ class V1 implements ControllerProviderInterface 'label' => $permalink->get_label(), 'updated_on' => $permalink->get_last_modified()->format(DATE_ATOM), 'page_url' => $permalink->get_page(), - 'download_url' => $permalink->get_url() . '&download', - 'url' => $permalink->get_url() + 'download_url' => (string) $downloadUrl, + 'url' => (string) $permalink->get_url() ]; } @@ -1649,10 +1664,12 @@ class V1 implements ControllerProviderInterface { $ret = [ 'basket_id' => $basket->getId(), + 'owner' => $this->list_user($basket->getOwner()), 'created_on' => $basket->getCreated()->format(DATE_ATOM), 'description' => (string) $basket->getDescription(), 'name' => $basket->getName(), 'pusher_usr_id' => $basket->getPusher() ? $basket->getPusher()->getId() : null, + 'pusher' => $basket->getPusher() ? $this->list_user($basket->getPusher()) : null, 'updated_on' => $basket->getUpdated()->format(DATE_ATOM), 'unread' => !$basket->getIsRead(), 'validation_basket' => !!$basket->getValidation() @@ -1669,6 +1686,7 @@ class V1 implements ControllerProviderInterface 'can_agree' => $participant->getCanAgree(), 'can_see_others' => $participant->getCanSeeOthers(), 'readonly' => $user->getId() != $app['authentication']->getUser()->getId(), + 'user' => $this->list_user($user), ]; }, iterator_to_array($basket->getValidation()->getParticipants())); @@ -1680,11 +1698,12 @@ class V1 implements ControllerProviderInterface $ret = array_merge( [ - 'validation_users' => $users, - 'expires_on' => $expires_on_atom, - 'validation_infos' => $basket->getValidation()->getValidationString($app, $app['authentication']->getUser()), - 'validation_confirmed' => $basket->getValidation()->getParticipant($app['authentication']->getUser())->getIsConfirmed(), - 'validation_initiator' => $basket->getValidation()->isInitiator($app['authentication']->getUser()), + 'validation_users' => $users, + 'expires_on' => $expires_on_atom, + 'validation_infos' => $basket->getValidation()->getValidationString($app, $app['authentication']->getUser()), + 'validation_confirmed' => $basket->getValidation()->getParticipant($app['authentication']->getUser())->getIsConfirmed(), + 'validation_initiator' => $basket->getValidation()->isInitiator($app['authentication']->getUser()), + 'validation_initiator_user' => $this->list_user($basket->getValidation()->getInitiator()), ], $ret ); } @@ -2064,6 +2083,46 @@ class V1 implements ControllerProviderInterface ]; } + private function list_user(User $user) + { + switch ($user->getGender()) { + case User::GENDER_MR; + $gender = 'Mr'; + break; + case User::GENDER_MRS; + $gender = 'Mrs'; + break; + case User::GENDER_MISS; + $gender = 'Miss'; + break; + } + + return array( + '@entity@' => self::OBJECT_TYPE_USER, + 'id' => $user->getId(), + 'email' => $user->getEmail() ?: null, + 'login' => $user->getLogin() ?: null, + 'first_name' => $user->getFirstName() ?: null, + 'last_name' => $user->getLastName() ?: null, + 'display_name' => $user->getDisplayName() ?: null, + 'gender' => $gender, + 'address' => $user->getAddress() ?: null, + 'zip_code' => $user->getZipCode() ?: null, + 'city' => $user->getCity() ?: null, + 'country' => $user->getCountry() ?: null, + 'phone' => $user->getPhone() ?: null, + 'fax' => $user->getFax() ?: null, + 'job' => $user->getJob() ?: null, + 'position' => $user->getActivity() ?: null, + 'company' => $user->getCompany() ?: null, + 'geoname_id' => $user->getGeonameId() ?: null, + 'last_connection' => $user->getLastConnection() ? $user->getLastConnection()->format(DATE_ATOM) : null, + 'created_on' => $user->getCreated() ? $user->getCreated()->format(DATE_ATOM) : null, + 'updated_on' => $user->getUpdated() ? $user->getUpdated()->format(DATE_ATOM) : null, + 'locale' => $user->getLocale() ?: null, + ); + } + private function logout(Application $app) { if (null !== $app['authentication']->getUser()) { diff --git a/lib/Alchemy/Phrasea/Controller/Prod/Edit.php b/lib/Alchemy/Phrasea/Controller/Prod/Edit.php index 36d32fe81b..7389611c28 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/Edit.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/Edit.php @@ -200,7 +200,7 @@ class Edit implements ControllerProviderInterface $thumbnail = $record->get_thumbnail(); $elements[$indice]['subdefs']['thumbnail'] = [ - 'url' => $thumbnail->get_url() + 'url' => (string) $thumbnail->get_url() , 'w' => $thumbnail->get_width() , 'h' => $thumbnail->get_height() ]; diff --git a/lib/Alchemy/Phrasea/Controller/Prod/Records.php b/lib/Alchemy/Phrasea/Controller/Prod/Records.php index 181edd1f7d..c65949772b 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/Records.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/Records.php @@ -216,7 +216,7 @@ class Records implements ControllerProviderInterface $renewed = []; foreach ($records as $record) { - $renewed[$record->get_serialize_key()] = $record->get_preview()->renew_url(); + $renewed[$record->get_serialize_key()] = (string) $record->get_preview()->renew_url(); }; return $app->json($renewed); diff --git a/lib/Alchemy/Phrasea/Core/Provider/FileServeServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/FileServeServiceProvider.php index 0697d1bba1..a22ab5d29d 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/FileServeServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/FileServeServiceProvider.php @@ -12,6 +12,7 @@ namespace Alchemy\Phrasea\Core\Provider; use Alchemy\Phrasea\Core\Event\Subscriber\XSendFileSubscriber; +use Alchemy\Phrasea\Http\H264PseudoStreaming\H264Factory; use Alchemy\Phrasea\Http\ServeFileResponseFactory; use Alchemy\Phrasea\Http\XSendFile\XSendFileFactory; use Silex\Application; @@ -28,6 +29,14 @@ class FileServeServiceProvider implements ServiceProviderInterface return XSendFileFactory::create($app); }); + $app['phraseanet.h264-factory'] = $app->share(function ($app) { + return H264Factory::create($app); + }); + + $app['phraseanet.h264'] = $app->share(function ($app) { + return $app['phraseanet.h264-factory']->createMode(false); + }); + $app['phraseanet.file-serve'] = $app->share(function (Application $app) { return ServeFileResponseFactory::create($app); }); diff --git a/lib/Alchemy/Phrasea/Feed/Formatter/CoolirisFormatter.php b/lib/Alchemy/Phrasea/Feed/Formatter/CoolirisFormatter.php index 459aec5f67..017680f848 100644 --- a/lib/Alchemy/Phrasea/Feed/Formatter/CoolirisFormatter.php +++ b/lib/Alchemy/Phrasea/Feed/Formatter/CoolirisFormatter.php @@ -229,7 +229,7 @@ class CoolirisFormatter extends FeedFormatterAbstract implements FeedFormatterIn if (null !== $preview_permalink) { $preview = $this->addTag($document, $item, 'media:content'); - $preview->setAttribute('url', $preview_permalink->get_url()); + $preview->setAttribute('url', (string) $preview_permalink->get_url()); $preview->setAttribute('fileSize', $preview_sd->get_size()); $preview->setAttribute('type', $preview_sd->get_mime()); $preview->setAttribute('medium', $medium); @@ -250,7 +250,7 @@ class CoolirisFormatter extends FeedFormatterAbstract implements FeedFormatterIn if (null !== $thumbnail_permalink) { $thumbnail = $this->addTag($document, $item, 'media:thumbnail'); - $thumbnail->setAttribute('url', $thumbnail_permalink->get_url()); + $thumbnail->setAttribute('url', (string) $thumbnail_permalink->get_url()); if (null !== $thumbnail_sd->get_width()) { $thumbnail->setAttribute('width', $thumbnail_sd->get_width()); @@ -261,7 +261,7 @@ class CoolirisFormatter extends FeedFormatterAbstract implements FeedFormatterIn $thumbnail = $this->addTag($document, $item, 'media:content'); - $thumbnail->setAttribute('url', $thumbnail_permalink->get_url()); + $thumbnail->setAttribute('url', (string) $thumbnail_permalink->get_url()); $thumbnail->setAttribute('fileSize', $thumbnail_sd->get_size()); $thumbnail->setAttribute('type', $thumbnail_sd->get_mime()); $thumbnail->setAttribute('medium', $medium); diff --git a/lib/Alchemy/Phrasea/Feed/Formatter/FeedFormatterAbstract.php b/lib/Alchemy/Phrasea/Feed/Formatter/FeedFormatterAbstract.php index 025cb52fa2..d0b1663163 100644 --- a/lib/Alchemy/Phrasea/Feed/Formatter/FeedFormatterAbstract.php +++ b/lib/Alchemy/Phrasea/Feed/Formatter/FeedFormatterAbstract.php @@ -121,7 +121,7 @@ abstract class FeedFormatterAbstract if (null !== $preview_permalink) { $preview = $this->addTag($document, $group, 'media:content'); - $preview->setAttribute('url', $preview_permalink->get_url()); + $preview->setAttribute('url', (string) $preview_permalink->get_url()); $preview->setAttribute('fileSize', $preview_sd->get_size()); $preview->setAttribute('type', $preview_sd->get_mime()); $preview->setAttribute('medium', $medium); @@ -142,7 +142,7 @@ abstract class FeedFormatterAbstract if (null !== $thumbnail_permalink) { $thumbnail = $this->addTag($document, $group, 'media:thumbnail'); - $thumbnail->setAttribute('url', $thumbnail_permalink->get_url()); + $thumbnail->setAttribute('url', (string) $thumbnail_permalink->get_url()); if (null !== $thumbnail_sd->get_width()) { $thumbnail->setAttribute('width', $thumbnail_sd->get_width()); @@ -153,7 +153,7 @@ abstract class FeedFormatterAbstract $thumbnail = $this->addTag($document, $group, 'media:content'); - $thumbnail->setAttribute('url', $thumbnail_permalink->get_url()); + $thumbnail->setAttribute('url', (string) $thumbnail_permalink->get_url()); $thumbnail->setAttribute('fileSize', $thumbnail_sd->get_size()); $thumbnail->setAttribute('type', $thumbnail_sd->get_mime()); $thumbnail->setAttribute('medium', $medium); diff --git a/lib/Alchemy/Phrasea/Http/XSendFile/AbstractXSendFileMode.php b/lib/Alchemy/Phrasea/Http/AbstractServerMode.php similarity index 94% rename from lib/Alchemy/Phrasea/Http/XSendFile/AbstractXSendFileMode.php rename to lib/Alchemy/Phrasea/Http/AbstractServerMode.php index 7ebe216a84..1e5a7ae381 100644 --- a/lib/Alchemy/Phrasea/Http/XSendFile/AbstractXSendFileMode.php +++ b/lib/Alchemy/Phrasea/Http/AbstractServerMode.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Alchemy\Phrasea\Http\XSendFile; +namespace Alchemy\Phrasea\Http; use Alchemy\Phrasea\Exception\InvalidArgumentException; -abstract class AbstractXSendFileMode +abstract class AbstractServerMode { protected $mapping = []; diff --git a/lib/Alchemy/Phrasea/Http/H264PseudoStreaming/Apache.php b/lib/Alchemy/Phrasea/Http/H264PseudoStreaming/Apache.php new file mode 100644 index 0000000000..4863d7e3da --- /dev/null +++ b/lib/Alchemy/Phrasea/Http/H264PseudoStreaming/Apache.php @@ -0,0 +1,109 @@ + $entry) { + if (!is_array($entry)) { + throw new InvalidArgumentException('H264 pseudo streaming mapping entry must be an array'); + } + + if (!isset($entry['directory'])) { + throw new InvalidArgumentException('H264 pseudo streaming mapping entry must contain at least a "directory" key'); + } + + if (!isset($entry['mount-point'])) { + throw new InvalidArgumentException('H264 pseudo streaming mapping entry must contain at least a "mount-point" key'); + } + + if (!isset($entry['passphrase'])) { + throw new InvalidArgumentException('H264 pseudo streaming mapping entry must contain at least a "passphrase" key'); + } + + if (false === is_dir(trim($entry['directory'])) || '' === trim($entry['mount-point']) || '' === trim($entry['passphrase'])) { + continue; + } + + $final[$key] = array( + 'directory' => $this->sanitizePath(realpath($entry['directory'])), + 'mount-point' => $this->sanitizeMountPoint($entry['mount-point']), + 'passphrase' => trim($entry['passphrase']), + ); + } + + $this->mapping = $final; + } + + /** + * {@inheritdoc} + */ + public function getUrl($pathfile) + { + if (!is_file($pathfile)) { + return null; + } + $pathfile = realpath($pathfile); + + foreach ($this->mapping as $entry) { + if (0 !== strpos($pathfile, $entry['directory'])) { + continue; + } + + return $this->generateUrl($pathfile, $entry); + } + } + + /** + * {@inheritdoc} + */ + public function getVirtualHostConfiguration() + { + $output = "\n"; + foreach ($this->mapping as $entry) { + $output .= " Alias ".$entry['mount-point']." \"".$entry['directory']."\"\n"; + $output .= "\n"; + $output .= " \n"; + $output .= " AuthTokenSecret \"".$entry['passphrase']."\"\n"; + $output .= " AuthTokenPrefix ".$entry['mount-point']."\n"; + $output .= " AuthTokenTimeout 3600\n"; + $output .= " AuthTokenLimitByIp off\n"; + $output .= " \n"; + $output .= "\n"; + } + + return $output; + } + + private function generateUrl($pathfile, array $entry) + { + $path = substr($pathfile, strlen($entry['directory'])); + + $hexTime = dechex(time() + 3600); + $token = md5($entry['passphrase'] . $path . $hexTime); + + return Url::factory($entry['mount-point'] .'/'. $token . "/" . $hexTime . $path); + } +} diff --git a/lib/Alchemy/Phrasea/Http/H264PseudoStreaming/H264Factory.php b/lib/Alchemy/Phrasea/Http/H264PseudoStreaming/H264Factory.php new file mode 100644 index 0000000000..87db6205b0 --- /dev/null +++ b/lib/Alchemy/Phrasea/Http/H264PseudoStreaming/H264Factory.php @@ -0,0 +1,97 @@ +logger = $logger; + $this->enabled = (Boolean) $enabled; + $this->type = strtolower($type); + $this->mapping = $mapping; + } + + /** + * Returns a new instance of H264Interface. + * + * @return H264Interface + * + * @throws InvalidArgumentException if mode type is unknown + */ + public function createMode($throwException = false, $forceMode = false) + { + if (false === $this->enabled && true !== $forceMode) { + return new NullMode(); + } + + switch ($this->type) { + case 'apache': + case 'apache2': + return new Apache($this->mapping); + case 'nginx': + return new Nginx($this->mapping); + default: + $this->logger->error('Invalid h264 pseudo streaming configuration.'); + if ($throwException) { + throw new InvalidArgumentException(sprintf('Invalid h264 pseudo streaming configuration width type "%s"', $this->type)); + } + + return new NullMode(); + } + } + + /** + * Creates a new instance of H264 Factory given a configuration. + * + * @param Application $app + * + * @return H264Factory + */ + public static function create(Application $app) + { + $conf = $app['phraseanet.configuration']['h264-pseudo-streaming']; + + $mapping = array(); + + if (isset($conf['mapping'])) { + $mapping = $conf['mapping']; + } + + return new self($app['monolog'], $conf['enabled'], $conf['type'], $mapping); + } + + /** + * @return Boolean + */ + public function isH264Enabled() + { + return $this->enabled; + } +} diff --git a/lib/Alchemy/Phrasea/Http/H264PseudoStreaming/H264Interface.php b/lib/Alchemy/Phrasea/Http/H264PseudoStreaming/H264Interface.php new file mode 100644 index 0000000000..a9f9ab793f --- /dev/null +++ b/lib/Alchemy/Phrasea/Http/H264PseudoStreaming/H264Interface.php @@ -0,0 +1,25 @@ + $entry) { + if (!is_array($entry)) { + throw new InvalidArgumentException('H264 pseudo streaming mapping entry must be an array'); + } + + if (!isset($entry['directory'])) { + throw new InvalidArgumentException('H264 pseudo streaming mapping entry must contain at least a "directory" key'); + } + + if (!isset($entry['mount-point'])) { + throw new InvalidArgumentException('H264 pseudo streaming mapping entry must contain at least a "mount-point" key'); + } + + if (!isset($entry['passphrase'])) { + throw new InvalidArgumentException('H264 pseudo streaming mapping entry must contain at least a "passphrase" key'); + } + + if (false === is_dir(trim($entry['directory'])) || '' === trim($entry['mount-point']) || '' === trim($entry['passphrase'])) { + continue; + } + + $final[$key] = array( + 'directory' => $this->sanitizePath(realpath($entry['directory'])), + 'mount-point' => $this->sanitizeMountPoint($entry['mount-point']), + 'passphrase' => trim($entry['passphrase']), + ); + } + + $this->mapping = $final; + } + + /** + * {@inheritdoc} + */ + public function getUrl($pathfile) + { + if (!is_file($pathfile)) { + return null; + } + $pathfile = realpath($pathfile); + + foreach ($this->mapping as $entry) { + if (0 !== strpos($pathfile, $entry['directory'])) { + continue; + } + + return $this->generateUrl($pathfile, $entry); + } + } + + /** + * {@inheritdoc} + */ + public function getVirtualHostConfiguration() + { + $output = "\n"; + foreach ($this->mapping as $entry) { + $output .= " location " . $entry['mount-point']. " {\n"; + $output .= " mp4;\n"; + $output .= " secure_link \$arg_hash,\$arg_expires;\n"; + $output .= " secure_link_md5 \"\$secure_link_expires\$uri ".$entry['passphrase']."\";\n"; + $output .= " \n"; + $output .= " if (\$secure_link = \"\") {\n"; + $output .= " return 403;\n"; + $output .= " }\n"; + $output .= " if (\$secure_link = \"0\") {\n"; + $output .= " return 410;\n"; + $output .= " }\n"; + $output .= " \n"; + $output .= " alias ".$entry['directory'].";\n"; + $output .= " }\n"; + } + + return $output; + } + + private function generateUrl($pathfile, array $entry) + { + $path = $entry['mount-point'].substr($pathfile, strlen($entry['directory'])); + $expire = time() + 3600; // At which point in time the file should expire. time() + x; would be the usual usage. + + $hash = str_replace(array('+', '/', '='), array('-', '_', ''), base64_encode(md5($expire.$path.' '.$entry['passphrase'], true))); + + return Url::factory($path.'?hash='.$hash.'&expires='.$expire); + } +} diff --git a/lib/Alchemy/Phrasea/Http/H264PseudoStreaming/NullMode.php b/lib/Alchemy/Phrasea/Http/H264PseudoStreaming/NullMode.php new file mode 100644 index 0000000000..32addaa8dd --- /dev/null +++ b/lib/Alchemy/Phrasea/Http/H264PseudoStreaming/NullMode.php @@ -0,0 +1,30 @@ +geonamesConnector->geoname($geonameid)->get('country'); + $data = $this->geonamesConnector->geoname($geonameid); + $country = $data->get('country'); $user->setGeonameId($geonameid); + $user->setCity($data->get('name')); if (isset($country['code'])) { $user->setCountry($country['code']); } } catch (GeonamesExceptionInterface $e) { $user->setCountry(null); + $user->setCity(null); } $this->manager->update($user); diff --git a/lib/classes/media/Permalink/Adapter.php b/lib/classes/media/Permalink/Adapter.php index 8657a8125f..f6d4c30168 100644 --- a/lib/classes/media/Permalink/Adapter.php +++ b/lib/classes/media/Permalink/Adapter.php @@ -11,6 +11,7 @@ use Alchemy\Phrasea\Application; use Alchemy\Phrasea\Exception\RuntimeException; +use Guzzle\Http\Url; use Doctrine\DBAL\DBALException; class media_Permalink_Adapter implements media_Permalink_Interface, cache_cacheableInterface @@ -137,20 +138,20 @@ class media_Permalink_Adapter implements media_Permalink_Interface, cache_cachea /** * - * @return string + * @return Url */ public function get_url() { $label = $this->get_label() . '.' . pathinfo($this->media_subdef->get_file(), PATHINFO_EXTENSION); - return $this->app->url('permalinks_permalink', [ + return Url::factory($this->app->url('permalinks_permalink', [ 'sbas_id' => $this->media_subdef->get_sbas_id(), 'record_id' => $this->media_subdef->get_record_id(), 'subdef' => $this->media_subdef->get_name(), /** @Ignore */ 'label' => $label, 'token' => $this->get_token(), - ]); + ])); } /** diff --git a/lib/classes/media/abstract.php b/lib/classes/media/abstract.php index b04d94d2c9..c807be9087 100644 --- a/lib/classes/media/abstract.php +++ b/lib/classes/media/abstract.php @@ -9,11 +9,13 @@ * file that was distributed with this source code. */ +use Guzzle\Http\Url; + abstract class media_abstract { /** * - * @var string + * @var Url */ protected $url; @@ -39,7 +41,7 @@ abstract class media_abstract * @param int $height * @return media */ - public function __construct($url, $width, $height) + public function __construct(Url $url, $width, $height) { $this->url = $url; $this->height = (int) $height; @@ -50,7 +52,7 @@ abstract class media_abstract /** * - * @return string + * @return Url */ public function get_url() { diff --git a/lib/classes/media/subdef.php b/lib/classes/media/subdef.php index b54a66c5ec..b10703e2b9 100644 --- a/lib/classes/media/subdef.php +++ b/lib/classes/media/subdef.php @@ -13,6 +13,7 @@ use Alchemy\Phrasea\Application; use MediaAlchemyst\Alchemyst; use MediaVorus\MediaVorus; use MediaVorus\Media\MediaInterface; +use Guzzle\Http\Url; class media_subdef extends media_abstract implements cache_cacheableInterface { @@ -231,7 +232,7 @@ class media_subdef extends media_abstract implements cache_cacheableInterface , 'height' => $this->height , 'etag' => $this->etag , 'path' => $this->path - , 'url' => $this->url . ($this->is_physically_present ? '?etag=' . $this->etag : '') + , 'url' => $this->url , 'file' => $this->file , 'physically_present' => $this->is_physically_present , 'is_substituted' => $this->is_substituted @@ -280,7 +281,7 @@ class media_subdef extends media_abstract implements cache_cacheableInterface $this->height = 256; $this->path = $this->app['root.path'] . '/www/skins/icons/substitution/'; $this->file = 'regroup_thumb.png'; - $this->url = '/skins/icons/substitution/regroup_thumb.png'; + $this->url = Url::factory('/skins/icons/substitution/regroup_thumb.png'); } else { $mime = $this->record->get_mime(); $mime = trim($mime) != '' ? str_replace('/', '_', $mime) : 'application_octet-stream'; @@ -290,7 +291,7 @@ class media_subdef extends media_abstract implements cache_cacheableInterface $this->height = 256; $this->path = $this->app['root.path'] . '/www/skins/icons/substitution/'; $this->file = str_replace('+', '%20', $mime) . '.png'; - $this->url = '/skins/icons/substitution/' . $this->file; + $this->url = Url::factory('/skins/icons/substitution/' . $this->file); } $this->is_physically_present = false; @@ -298,7 +299,7 @@ class media_subdef extends media_abstract implements cache_cacheableInterface if ( ! file_exists($this->path . $this->file)) { $this->path = $this->app['root.path'] . '/www/skins/icons/'; $this->file = 'substitution.png'; - $this->url = '/skins/icons/' . $this->file; + $this->url = Url::factory('/skins/icons/' . $this->file); } return $this; @@ -367,9 +368,11 @@ class media_subdef extends media_abstract implements cache_cacheableInterface public function get_url() { $url = parent::get_url(); - $etag = $this->getEtag(); + if (null !== $this->getEtag()) { + $url->getQuery()->set('etag', $this->getEtag()); + } - return $url . ($etag ? '?etag=' . $etag : ''); + return $url; } /** @@ -740,17 +743,16 @@ class media_subdef extends media_abstract implements cache_cacheableInterface } if (in_array($this->mime, ['video/mp4'])) { - $token = p4file::apache_tokenize($this->app['conf'], $this->get_pathfile()); - if ($token) { - $this->url = $token; + if (null !== $url = $this->app['phraseanet.h264']->getUrl($this->get_pathfile())) { + $this->url = $url; return; } } - $this->url = "/datafiles/" . $this->record->get_sbas_id() + $this->url = Url::factory("/datafiles/" . $this->record->get_sbas_id() . "/" . $this->record->get_record_id() . "/" - . $this->get_name() . "/"; + . $this->get_name() . "/"); return; } diff --git a/lib/classes/module/report/nav.php b/lib/classes/module/report/nav.php index 29a336529e..63b5b622f3 100644 --- a/lib/classes/module/report/nav.php +++ b/lib/classes/module/report/nav.php @@ -518,7 +518,7 @@ class module_report_nav extends module_report $this->result[] = [ 'photo' => "" + src='" . (string) $x->get_url() . "'>" , 'record_id' => $record->get_record_id() , 'date' => $this->app['date-formatter']->getPrettyString($document->get_creation_date()) , 'type' => $document->get_mime() diff --git a/lib/classes/p4file.php b/lib/classes/p4file.php deleted file mode 100644 index 9e60f15358..0000000000 --- a/lib/classes/p4file.php +++ /dev/null @@ -1,60 +0,0 @@ -get(['registry', 'executables', 'h264-streaming-enabled']) && is_file($file)) { - if (mb_strpos($file, $conf->get(['registry', 'executables', 'auth-token-directory-path'])) === false) { - return false; - } - - $server = new system_server(); - - if ($server->is_nginx()) { - $fileToProtect = mb_substr($file, mb_strlen($conf->get(['registry', 'executables', 'auth-token-directory-path']))); - - $secret = $conf->get(['registry', 'executables', 'auth-token-passphrase']); - $protectedPath = p4string::addFirstSlash(p4string::delEndSlash($conf->get(['registry', 'executables', 'auth-token-directory']))); - - $hexTime = strtoupper(dechex(time() + 3600)); - - $token = md5($protectedPath . $fileToProtect . '/' . $secret . '/' . $hexTime); - - $url = $protectedPath . $fileToProtect . '/' . $token . '/' . $hexTime; - - $ret = $url; - } elseif ($server->is_apache()) { - $fileToProtect = mb_substr($file, mb_strlen($conf->get(['registry', 'executables', 'auth-token-directory-path']))); - - $secret = $conf->get(['registry', 'executables', 'auth-token-passphrase']); // Same as AuthTokenSecret - $protectedPath = p4string::addEndSlash(p4string::delFirstSlash($conf->get(['registry', 'executables', 'auth-token-directory']))); // Same as AuthTokenPrefix - $hexTime = dechex(time()); // Time in Hexadecimal - - $token = md5($secret . $fileToProtect . $hexTime); - - // We build the url - $url = '/' . $protectedPath . $token . "/" . $hexTime . $fileToProtect; - - $ret = $url; - } - } - - return $ret; - } - -} diff --git a/lib/classes/patch/384alpha1a.php b/lib/classes/patch/384alpha1a.php new file mode 100644 index 0000000000..9c3a53975f --- /dev/null +++ b/lib/classes/patch/384alpha1a.php @@ -0,0 +1,63 @@ +release; + } + + /** + * {@inheritdoc} + */ + public function require_all_upgrades() + { + return false; + } + + /** + * {@inheritdoc} + */ + public function concern() + { + return $this->concern; + } + + /** + * {@inheritdoc} + */ + public function apply(base $appbox, Application $app) + { + $config = $app['phraseanet.configuration']->getConfig(); + + $config['h264-pseudo-streaming'] = array( + 'enabled' => false, + 'type' => null, + 'mapping' => array(), + ); + + $app['phraseanet.configuration']->setConfig($config); + + return true; + } +} diff --git a/lib/classes/record/preview.php b/lib/classes/record/preview.php index cfb0e22820..5eacf2e20e 100644 --- a/lib/classes/record/preview.php +++ b/lib/classes/record/preview.php @@ -14,6 +14,7 @@ use Alchemy\Phrasea\Model\Entities\Basket; use Alchemy\Phrasea\Model\Entities\BasketElement; use Alchemy\Phrasea\SearchEngine\SearchEngineInterface; use Alchemy\Phrasea\SearchEngine\SearchEngineOptions; +use Guzzle\Http\Url; class record_preview extends record_adapter { @@ -462,7 +463,7 @@ class record_preview extends record_adapter $width = 350; $height = 150; - $url = 'http://chart.apis.google.com/chart?' . + $url = Url::factory('http://chart.apis.google.com/chart?' . 'chs=' . $width . 'x' . $height . '&chd=t:' . implode(',', $views) . '&cht=lc' . @@ -475,7 +476,7 @@ class record_preview extends record_adapter . date_format(new DateTime(), 'd M') . '|1:|0|' . round($top / 2, 2) . '|' . $top . '|2:|min|average|max' . - '&chxp=2,' . $min . ',' . $average . ',' . $max; + '&chxp=2,' . $min . ',' . $average . ',' . $max); $this->view_popularity = new media_adapter($url, $width, $height); @@ -543,12 +544,12 @@ class record_preview extends record_adapter $width = 550; $height = 100; - $url = 'http://chart.apis.google.com/chart?' + $url = Url::factory('http://chart.apis.google.com/chart?' . 'cht=p3&chf=bg,s,00000000&chd=t:' . implode(',', $referrers) . '&chs=' . $width . 'x' . $height . '&chl=' - . urlencode(implode('|', array_keys($referrers))) . ''; + . urlencode(implode('|', array_keys($referrers)))); $this->refferer_popularity = new media_adapter($url, $width, $height); @@ -618,7 +619,7 @@ class record_preview extends record_adapter $width = 250; $height = 150; - $url = 'http://chart.apis.google.com/chart?' . + $url = Url::factory('http://chart.apis.google.com/chart?' . 'chs=' . $width . 'x' . $height . '&chd=t:' . implode(',', $dwnls) . '&cht=lc' . @@ -628,7 +629,7 @@ class record_preview extends record_adapter '&chxl=0:|' . date_format(new DateTime('-30 days'), 'd M') . '|' . date_format(new DateTime('-15 days'), 'd M') . '|' . date_format(new DateTime(), 'd M') . '|1:|0|' - . round($top / 2) . '|' . $top . ''; + . round($top / 2) . '|' . $top); $ret = new media_adapter($url, $width, $height); $this->download_popularity = $ret; diff --git a/lib/conf.d/configuration.yml b/lib/conf.d/configuration.yml index 104c4837e4..e1ea10930e 100644 --- a/lib/conf.d/configuration.yml +++ b/lib/conf.d/configuration.yml @@ -182,4 +182,8 @@ xsendfile: enabled: false type: nginx mapping: [] +h264-pseudo-streaming: + enabled: false + type: nginx + mapping: [] plugins: [] diff --git a/templates/web/common/thumbnail.html.twig b/templates/web/common/thumbnail.html.twig index 7950f4a5e1..b2d7a12946 100644 --- a/templates/web/common/thumbnail.html.twig +++ b/templates/web/common/thumbnail.html.twig @@ -66,7 +66,13 @@ {src:"/include/jslibs/flowplayer/flowplayer-3.2.18.swf", wmode: "transparent"}, {clip:{url:"{{url|url_encode}}",autoPlay: true,autoBuffering:true,provider: "h264streaming",scaling:"fit"}, onError:function(code,message){getNewVideoToken("{{thumbnail.get_sbas_id() ~'_'~thumbnail.get_record_id()}}", this);}, - plugins: {h264streaming: {url: "/include/jslibs/flowplayer/pseudostreaming/flowplayer.pseudostreaming-3.2.13.swf"}} + plugins: { + {% if app['phraseanet.h264-factory'].isH264Enabled() %} + h264streaming: { + url: "/include/jslibs/flowplayer/pseudostreaming/flowplayer.pseudostreaming-3.2.13.swf" + } + {% endif %} + } }); {% elseif record_type == 'FLEXPAPER' %} @@ -172,7 +178,15 @@ flowplayer("preview{{random}}", {src:"/include/jslibs/flowplayer/flowplayer-3.2.18.swf", wmode: "transparent"}, {clip:{url:"{{url}}",autoPlay: true,autoBuffering:true,provider: "h264streaming",scaling:"fit"}, - onError:function(code,message){getNewVideoToken({{thumbnail.get_sbas_id() ~'_'~thumbnail.get_record_id()}}, this);}, plugins: {h264streaming: {url: "/include/jslibs/flowplayer/pseudostreaming/flowplayer.pseudostreaming-3.2.13.swf"}}}); + onError:function(code,message){getNewVideoToken({{thumbnail.get_sbas_id() ~'_'~thumbnail.get_record_id()}}, this);}, + plugins: { + {% if app['phraseanet.h264-factory'].isH264Enabled() %} + h264streaming: { + url: "/include/jslibs/flowplayer/pseudostreaming/flowplayer.pseudostreaming-3.2.13.swf" + } + {% endif %} + } + }); {% elseif record_type == 'FLEXPAPER' %} {% set random = random(100000) %} diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiTestCase.php b/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiTestCase.php index b215690712..64b2e68b40 100644 --- a/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiTestCase.php +++ b/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiTestCase.php @@ -1015,7 +1015,7 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase $this->assertArrayHasKey("baskets", $content['response']); foreach ($content['response']['baskets'] as $basket) { - $this->evaluateGoodBasket($basket); + $this->evaluateGoodBasket($basket, self::$DI['user_notAdmin']); } $route = '/api/v1/records/24892534/51654651553/related/'; @@ -1186,7 +1186,7 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase $this->assertArrayHasKey("baskets", $content['response']); foreach ($content['response']['baskets'] as $basket) { - $this->evaluateGoodBasket($basket); + $this->evaluateGoodBasket($basket, self::$DI['user']); } } @@ -1206,7 +1206,7 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase $this->assertEquals(1, count($content['response'])); $this->assertArrayHasKey("basket", $content['response']); - $this->evaluateGoodBasket($content['response']['basket']); + $this->evaluateGoodBasket($content['response']['basket'], self::$DI['user_notAdmin']); $this->assertEquals('un Joli Nom', $content['response']['basket']['name']); } @@ -1231,7 +1231,7 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase $this->assertArrayHasKey("basket_elements", $content['response']); $this->assertArrayHasKey("basket", $content['response']); - $this->evaluateGoodBasket($content['response']['basket']); + $this->evaluateGoodBasket($content['response']['basket'], self::$DI['user']); foreach ($content['response']['basket_elements'] as $basket_element) { $this->assertArrayHasKey('basket_element_id', $basket_element); @@ -1263,7 +1263,7 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase $this->assertEquals(1, count((array) $content['response'])); $this->assertArrayHasKey("basket", $content['response']); - $this->evaluateGoodBasket($content['response']['basket']); + $this->evaluateGoodBasket($content['response']['basket'], self::$DI['user']); $this->assertEquals($content['response']['basket']['name'], 'un Joli Nom'); @@ -1277,7 +1277,7 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase $this->assertArrayHasKey("basket", $content['response']); - $this->evaluateGoodBasket($content['response']['basket']); + $this->evaluateGoodBasket($content['response']['basket'], self::$DI['user']); $this->assertEquals($content['response']['basket']['name'], 'un Joli Nom'); @@ -1289,7 +1289,7 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase $this->assertEquals(1, count((array) $content['response'])); $this->assertArrayHasKey("basket", $content['response']); - $this->evaluateGoodBasket($content['response']['basket']); + $this->evaluateGoodBasket($content['response']['basket'], self::$DI['user']); $this->assertEquals($content['response']['basket']['name'], 'aƩaa'); } @@ -1312,7 +1312,7 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase $this->assertEquals(1, count((array) $content['response'])); $this->assertArrayHasKey("basket", $content['response']); - $this->evaluateGoodBasket($content['response']['basket']); + $this->evaluateGoodBasket($content['response']['basket'], self::$DI['user']); $this->assertEquals($content['response']['basket']['description'], 'une belle desc'); } @@ -1332,7 +1332,7 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase $found = false; foreach ($content['response']['baskets'] as $basket) { - $this->evaluateGoodBasket($basket); + $this->evaluateGoodBasket($basket, self::$DI['user']); $found = true; break; } @@ -1746,6 +1746,15 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase { $this->assertArrayHasKey('id', $item); $this->assertArrayHasKey('quarantine_session', $item); + + $session = $item['quarantine_session']; + $this->assertArrayHasKey('id', $session); + $this->assertArrayHasKey('usr_id', $session); + $this->assertArrayHasKey('user', $session); + if ($session['user'] !== null) { + $this->evaluateGoodUserItem($session['user'], self::$DI['user_notAdmin']); + } + $this->assertArrayHasKey('base_id', $item); $this->assertArrayHasKey('original_name', $item); $this->assertArrayHasKey('sha256', $item); @@ -1760,6 +1769,54 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase $this->assertDateAtom($item['created_on']); } + public function testRouteMe() + { + $this->setToken(self::$token); + + $route = '/api/v1/me/'; + + $this->evaluateMethodNotAllowedRoute($route, array('POST', 'PUT', 'DELETE')); + + self::$DI['client']->request('GET', $route, $this->getParameters(), array(), array('HTTP_Accept' => $this->getAcceptMimeType())); + $content = $this->unserialize(self::$DI['client']->getResponse()->getContent()); + + $this->assertArrayHasKey('user', $content['response']); + + $this->evaluateGoodUserItem($content['response']['user'], self::$DI['user_notAdmin']); + } + + protected function evaluateGoodUserItem($data, \User_Adapter $user) + { + foreach (array( + '@entity@' => \API_V1_adapter::OBJECT_TYPE_USER, + 'id' => $user->get_id(), + 'email' => $user->get_email() ?: null, + 'login' => $user->get_login() ?: null, + 'first_name' => $user->get_firstname() ?: null, + 'last_name' => $user->get_lastname() ?: null, + 'display_name' => $user->get_display_name() ?: null, + 'address' => $user->get_address() ?: null, + 'zip_code' => $user->get_zipcode() ?: null, + 'city' => $user->get_city() ?: null, + 'country' => $user->get_country() ?: null, + 'phone' => $user->get_tel() ?: null, + 'fax' => $user->get_fax() ?: null, + 'job' => $user->get_job() ?: null, + 'position' => $user->get_position() ?: null, + 'company' => $user->get_company() ?: null, + 'geoname_id' => $user->get_geonameid() ?: null, + 'last_connection' => $user->get_last_connection() ? $user->get_last_connection()->format(DATE_ATOM) : null, + 'created_on' => $user->get_creation_date() ? $user->get_creation_date()->format(DATE_ATOM) : null, + 'updated_on' => $user->get_modification_date() ? $user->get_modification_date()->format(DATE_ATOM) : null, + 'locale' => $user->get_locale() ?: null, + ) as $key => $value) { + $this->assertArrayHasKey($key, $data, 'Assert key is present '.$key); + if ($value) { + $this->assertEquals($value, $data[$key], 'Check key '.$key); + } + } + } + protected function evaluateGoodFeed($feed) { $this->assertArrayHasKey('id', $feed); @@ -2129,9 +2186,13 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase $this->assertEquals(405, $response->getStatusCode(), 'Test status code 405 ' . $response->getContent()); } - protected function evaluateGoodBasket($basket) + protected function evaluateGoodBasket($basket, \User_Adapter $user) { $this->assertTrue(is_array($basket)); + $this->assertArrayHasKey('basket_id', $basket); + $this->assertArrayHasKey('owner', $basket); + $this->evaluateGoodUserItem($basket['owner'], $user); + $this->assertArrayHasKey('pusher', $basket); $this->assertArrayHasKey('created_on', $basket); $this->assertArrayHasKey('description', $basket); $this->assertArrayHasKey('name', $basket); @@ -2141,6 +2202,7 @@ abstract class ApiTestCase extends \PhraseanetWebTestCase if (!is_null($basket['pusher_usr_id'])) { $this->assertTrue(is_int($basket['pusher_usr_id'])); + $this->evaluateGoodUserItem($basket['pusher'], self::$DI['user_notAdmin']); } $this->assertTrue(is_string($basket['name'])); diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Root/RSSFeedTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Root/RSSFeedTest.php index e9e674bff9..fdb38c571e 100644 --- a/tests/Alchemy/Tests/Phrasea/Controller/Root/RSSFeedTest.php +++ b/tests/Alchemy/Tests/Phrasea/Controller/Root/RSSFeedTest.php @@ -372,7 +372,7 @@ class RSSFeedTest extends \PhraseanetWebTestCase foreach ($current_attributes as $attribute => $value) { switch ($attribute) { case "url": - $this->assertEquals($permalink->get_url(), $value); + $this->assertEquals((string) $permalink->get_url(), $value); break; case "fileSize": $this->assertEquals($ressource->get_size(), $value); diff --git a/tests/Alchemy/Tests/Phrasea/Core/Provider/FileServeServiceProviderTest.php b/tests/Alchemy/Tests/Phrasea/Core/Provider/FileServeServiceProviderTest.php index caebbcd83b..d3275caa7d 100644 --- a/tests/Alchemy/Tests/Phrasea/Core/Provider/FileServeServiceProviderTest.php +++ b/tests/Alchemy/Tests/Phrasea/Core/Provider/FileServeServiceProviderTest.php @@ -23,6 +23,16 @@ class FileServeServiceProviderTest extends ServiceProviderTestCase 'phraseanet.xsendfile-factory', 'Alchemy\Phrasea\Http\XSendFile\XSendFileFactory' ], + [ + 'Alchemy\Phrasea\Core\Provider\FileServeServiceProvider', + 'phraseanet.h264-factory', + 'Alchemy\Phrasea\Http\H264PseudoStreaming\H264Factory' + ], + [ + 'Alchemy\Phrasea\Core\Provider\FileServeServiceProvider', + 'phraseanet.h264', + 'Alchemy\Phrasea\Http\H264PseudoStreaming\H264Interface' + ], ]; } diff --git a/tests/Alchemy/Tests/Phrasea/Http/H264PseudoStreaming/ApacheTest.php b/tests/Alchemy/Tests/Phrasea/Http/H264PseudoStreaming/ApacheTest.php new file mode 100644 index 0000000000..da9c3702ae --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Http/H264PseudoStreaming/ApacheTest.php @@ -0,0 +1,46 @@ +assertNull($mode->getUrl($pathfile)); + } else { + $this->assertRegExp($expectedRegExp, (string) $mode->getUrl($pathfile)); + } + } + + public function provideMappingsAndFiles() + { + $dir = sys_get_temp_dir().'/to/subdef'; + $file = $dir . '/to/file'; + + if (!is_dir(dirname($file))) { + mkdir(dirname($file), 0777, true); + } + if (!is_file($file)) { + touch($file); + } + + $mapping = array(array( + 'directory' => $dir, + 'mount-point' => 'mp4-videos', + 'passphrase' => '123456', + )); + + return array( + array(array(), null, '/path/to/file'), + array($mapping, null, '/path/to/file'), + array($mapping, '/^\/mp4-videos\/[a-zA-Z0-9]+\/[0-9a-f]+\/to\/file$/', $file), + ); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Http/H264PseudoStreaming/H264FactoryTest.php b/tests/Alchemy/Tests/Phrasea/Http/H264PseudoStreaming/H264FactoryTest.php new file mode 100644 index 0000000000..7c27a74a4c --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Http/H264PseudoStreaming/H264FactoryTest.php @@ -0,0 +1,84 @@ +assertInstanceOf('Alchemy\Phrasea\Http\H264PseudoStreaming\H264Factory', $factory); + } + + public function testFactoryWithH264Enable() + { + $logger = $this->getMock('Psr\Log\LoggerInterface'); + + $factory = new H264Factory($logger, true, 'nginx', $this->getNginxMapping()); + $this->assertInstanceOf('Alchemy\Phrasea\Http\H264PseudoStreaming\H264Interface', $factory->createMode()); + $this->assertTrue($factory->isH264Enabled()); + } + + public function testFactoryWithH264Disabled() + { + $logger = $this->getMock('Psr\Log\LoggerInterface'); + + $factory = new H264Factory($logger, false, 'nginx',$this->getNginxMapping()); + $this->assertInstanceOf('Alchemy\Phrasea\Http\H264PseudoStreaming\NullMode', $factory->createMode()); + $this->assertFalse($factory->isH264Enabled()); + } + + /** + * @expectedException \Alchemy\Phrasea\Exception\InvalidArgumentException + */ + public function testFactoryWithWrongTypeThrowsAnExceptionIfRequired() + { + $logger = $this->getMock('Psr\Log\LoggerInterface'); + + $factory = new H264Factory($logger, true, 'wrong-type', $this->getNginxMapping()); + $factory->createMode(true); + } + + public function testFactoryWithWrongTypeDoesNotThrowsAnException() + { + $logger = $this->getMock('Psr\Log\LoggerInterface'); + + $logger->expects($this->once()) + ->method('error') + ->with($this->isType('string')); + + $factory = new H264Factory($logger, true, 'wrong-type', $this->getNginxMapping()); + $this->assertInstanceOf('Alchemy\Phrasea\Http\H264PseudoStreaming\NullMode', $factory->createMode(false)); + } + + /** + * @dataProvider provideTypes + */ + public function testFactoryType($type, $mapping, $classmode) + { + $logger = $this->getMock('Psr\Log\LoggerInterface'); + + $factory = new H264Factory($logger, true, $type, $mapping); + $this->assertInstanceOf($classmode, $factory->createMode()); + } + + public function provideTypes() + { + return array( + array('nginx', $this->getNginxMapping(), 'Alchemy\Phrasea\Http\H264PseudoStreaming\Nginx'), + array('apache', $this->getNginxMapping(), 'Alchemy\Phrasea\Http\H264PseudoStreaming\Apache'), + array('apache2', $this->getNginxMapping(), 'Alchemy\Phrasea\Http\H264PseudoStreaming\Apache'), + ); + } + + private function getNginxMapping() + { + return array(array( + 'directory' => __DIR__ . '/../../../../files/', + 'mount-point' => '/protected/', + 'passphrase' => 'dfdskqhfsfilddsmfmqsdmlfomqs', + )); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Http/H264PseudoStreaming/NginxTest.php b/tests/Alchemy/Tests/Phrasea/Http/H264PseudoStreaming/NginxTest.php new file mode 100644 index 0000000000..f41bfe3bbc --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Http/H264PseudoStreaming/NginxTest.php @@ -0,0 +1,46 @@ +assertNull($mode->getUrl($pathfile)); + } else { + $this->assertRegExp($expectedRegExp, (string) $mode->getUrl($pathfile)); + } + } + + public function provideMappingsAndFiles() + { + $dir = sys_get_temp_dir().'/to/subdef'; + $file = $dir . '/to/file'; + + if (!is_dir(dirname($file))) { + mkdir(dirname($file), 0777, true); + } + if (!is_file($file)) { + touch($file); + } + + $mapping = array(array( + 'directory' => $dir, + 'mount-point' => 'mp4-videos', + 'passphrase' => '123456', + )); + + return array( + array(array(), null, '/path/to/file'), + array($mapping, null, '/path/to/file'), + array($mapping, '/^\/mp4-videos\/to\/file\?hash=[a-zA-Z0-9-_+]+&expires=[0-9]+/', $file), + ); + } +} diff --git a/tests/classes/media/subdefTest.php b/tests/classes/media/subdefTest.php index 9011bc2e5c..ee62ad0c24 100644 --- a/tests/classes/media/subdefTest.php +++ b/tests/classes/media/subdefTest.php @@ -84,8 +84,10 @@ class media_subdefTest extends \PhraseanetTestCase */ public function testGet_url() { - $this->assertEquals('/skins/icons/substitution/image_jpeg.png', self::$objectNotPresent->get_url()); - $this->assertRegExp('#\/datafiles\/' . self::$objectPresent->get_sbas_id() . '\/' . self::$objectPresent->get_record_id() . '\/preview\/\?etag=[0-9a-f]{32}#', self::$objectPresent->get_url()); + $this->assertInstanceOf('Guzzle\Http\Url', self::$objectNotPresent->get_url()); + $this->assertInstanceOf('Guzzle\Http\Url', self::$objectPresent->get_url()); + $this->assertEquals('/skins/icons/substitution/image_jpeg.png', (string) self::$objectNotPresent->get_url()); + $this->assertRegExp('#\/datafiles\/' . self::$objectPresent->get_sbas_id() . '\/' . self::$objectPresent->get_record_id() . '\/preview\/\?etag=[0-9a-f]{32}#', (string) self::$objectPresent->get_url()); } /** @@ -246,8 +248,8 @@ class media_subdefTest extends \PhraseanetTestCase */ public function testRenew_url() { - $this->assertInternalType('string', self::$objectPresent->renew_url()); - $this->assertInternalType('string', self::$objectNotPresent->renew_url()); + $this->assertInstanceOf('Guzzle\Http\Url', self::$objectPresent->renew_url()); + $this->assertInstanceOf('Guzzle\Http\Url', self::$objectNotPresent->renew_url()); } /**