Use power of symfony binary file response

Add commands

Fix typo

Remove extra line
This commit is contained in:
Nicolas Le Goff
2013-06-26 15:37:51 +02:00
committed by Romain Neutron
parent 7fc9eb3010
commit 01a36ee9f7
16 changed files with 423 additions and 118 deletions

View File

@@ -21,6 +21,8 @@ use Alchemy\Phrasea\Command\UpgradeDBDatas;
use Alchemy\Phrasea\Command\Setup\Install;
use Alchemy\Phrasea\Command\Setup\LessCompiler;
use Alchemy\Phrasea\Command\Setup\JavascriptBuilder;
use Alchemy\Phrasea\Command\Setup\XSendFileMappingNginxDumper;
use Alchemy\Phrasea\Command\Setup\XSendFileMappingApacheDumper;
use Alchemy\Phrasea\CLI;
use Alchemy\Phrasea\Command\Setup\CheckEnvironment;
@@ -66,6 +68,8 @@ try {
$app->command(new Install('system:install'));
$app->command(new LessCompiler());
$app->command(new JavascriptBuilder());
$app->command(new XSendFileMappingNginxDumper());
$app->command(new XSendFileMappingApacheDumper());
$result_code = is_int($app->run()) ? : 1;
} catch (\Exception $e) {

View File

@@ -130,3 +130,9 @@ registration-fields:
-
name: geonameid
required: true
xsendfile:
enable: false
mapping:
-
directory: ''
mount-point: ''

View File

@@ -26,9 +26,6 @@ define('GV_dailymotion_client_secret', '');
define('GV_client_navigator', false);
define('GV_base_datapath_noweb', '/tmp/');
define('GV_phrasea_sort', '');
define('GV_modxsendfile', false);
define('GV_X_Accel_Redirect', '');
define('GV_X_Accel_Redirect_mount_point', 'noweb');
define('GV_h264_streaming', false);
define('GV_mod_auth_token_directory', '');
define('GV_mod_auth_token_directory_path', '');

View File

@@ -99,6 +99,7 @@ use Alchemy\Phrasea\Core\Provider\TaskManagerServiceProvider;
use Alchemy\Phrasea\Core\Provider\TemporaryFilesystemServiceProvider;
use Alchemy\Phrasea\Core\Provider\TokensServiceProvider;
use Alchemy\Phrasea\Core\Provider\UnicodeServiceProvider;
use Alchemy\Phrasea\Core\Provider\XSendFileMappingServiceProvider;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
use Alchemy\Phrasea\Twig\JSUniqueID;
use Alchemy\Phrasea\Twig\Camelize;
@@ -306,6 +307,12 @@ class Application extends SilexApplication
$this->register(new ValidatorServiceProvider());
$this->register(new XPDFServiceProvider());
$this->register(new XSendFileMappingServiceProvider(), array(
'xsendfile.mapping' => array(
$this['root.path'] . '/tmp/download/' => '/download/',
$this['root.path'] . '/tmp/lazaret/' => '/lazaret/'
)
));
$this->register(new FileServeServiceProvider());
$this['phraseanet.exception_handler'] = $this->share(function ($app) {

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Setup;
use Alchemy\Phrasea\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* This command dumps XsendFile Apache condifuration
*/
class XSendFileMappingApacheDumper extends Command
{
public function __construct()
{
parent::__construct('xsendfile:dump-apache');
$this->setDescription('Dump XSendFile mapping for Apache web server');
}
/**
* {@inheritdoc}
*/
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$mapper = $this->container['phraseanet.xsendfile-mapping'];
$output->writeln('<info>Apache XSendfile configuration</info>');
$output->writeln('');
$output->writeln('<IfModule mod_xsendfile.c>');
$output->writeln(' <Files *>');
$output->writeln(' XSendFile on');
foreach ($this->container['phraseanet.xsendfile-mapping']->getMapping() as $entry) {
$output->writeln(' XSendFilePath ' . $mapper->sanitizePath($entry['directory']));
}
$output->writeln(' </Files>');
$output->writeln('</IfModule>');
$output->writeln('');
return 1;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Setup;
use Alchemy\Phrasea\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* This command dumps XSendFile Nginx configuration
*/
class XSendFileMappingNginxDumper extends Command
{
public function __construct()
{
parent::__construct('xsendfile:dump-nginx');
$this->setDescription('Dump xsendfile mapping for Nginx and Apache web server');
}
/**
* {@inheritdoc}
*/
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$mapper = $this->container['phraseanet.xsendfile-mapping'];
$output->writeln('<info>Nginx XSendfile configuration</info>');
$output->writeln('');
foreach ($this->container['phraseanet.xsendfile-mapping']->getMapping() as $entry) {
$output->writeln(' location ' . $mapper->sanitizeMountPoint($entry['mount-point']) . ' {');
$output->writeln(' internal;');
$output->writeln(' alias ' . $mapper->sanitizePath($entry['directory']));
$output->writeln(' }');
$output->writeln('');
}
return 1;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Core\Event\Subscriber;
use Silex\Application;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class XSendFileSubscriber implements EventSubscriberInterface
{
private $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => array('applyHeaders', 16),
);
}
public function applyHeaders(GetResponseEvent $event)
{
if ($this->app['phraseanet.configuration']['xsendfile']['enable']) {
$request = $event->getRequest();
$request->headers->set('X-Sendfile-Type', 'X-Accel-Redirect');
$request->headers->set('X-Accel-Mapping', (string) $this->app['phraseanet.xsendfile-mapping']);
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Core\Provider;
use Alchemy\Phrasea\XSendFile\Mapping;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
use Silex\Application;
use Silex\ServiceProviderInterface;
class XSendFileMappingServiceProvider implements ServiceProviderInterface
{
public function register(Application $app)
{
if (!isset($app['xsendfile.mapping'])) {
$app['xsendfile.mapping'] = array();
}
if (!is_array($app['xsendfile.mapping'])) {
throw new InvalidArgumentException('XSendFile mapping must be an array');
}
$app['phraseanet.xsendfile-mapping'] = $app->share(function($app) {
$mapping = array();
foreach($app['xsendfile.mapping'] as $path => $mountPoint) {
$mapping[] = array(
'directory' => $path,
'mount-point' => $mountPoint,
);
}
return Mapping::create($app, $mapping);
});
}
public function boot(Application $app)
{
}
}

View File

@@ -13,32 +13,17 @@ namespace Alchemy\Phrasea\Response;
use Alchemy\Phrasea\Response\DeliverDataInterface;
use Alchemy\Phrasea\Application;
use Psr\Log\LoggerInterface,
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class ServeFileResponseFactory implements DeliverDataInterface
{
private $xSendFileEnable = false;
private $mappings;
private $unicode;
private $logger;
public function __construct($enableXSendFile, $xAccelMappings, \unicode $unicode, LoggerInterface $logger = null)
public function __construct($enableXSendFile, \unicode $unicode)
{
$this->logger = $logger;
$this->xSendFileEnable = $enableXSendFile;
$mappings = array();
foreach ($xAccelMappings as $path => $mountPoint) {
if (is_dir($path) && '' !== $mountPoint) {
$mappings[$this->sanitizeXAccelPath($path)] = $this->sanitizeXAccelMountPoint($mountPoint);
}
}
$this->mappings = $mappings;
$this->unicode = $unicode;
}
@@ -49,12 +34,9 @@ class ServeFileResponseFactory implements DeliverDataInterface
public static function create(Application $app)
{
return new self(
$app['phraseanet.registry']->get('GV_modxsendfile'),
array(
$app['phraseanet.registry']->get('GV_X_Accel_Redirect') => $app['phraseanet.registry']->get('GV_X_Accel_Redirect_mount_point'),
$app['root.path'] . '/tmp/download/' => '/download/',
$app['root.path'] . '/tmp/lazaret/' => '/lazaret/'
), new \unicode(), $app['logger']);
$app['phraseanet.configuration']['xsendfile']['enable'],
$app['unicode']
);
}
/**
@@ -66,11 +48,7 @@ class ServeFileResponseFactory implements DeliverDataInterface
$response->setContentDisposition($disposition, $this->sanitizeFilename($filename), $this->sanitizeFilenameFallback($filename));
if ($this->isXSendFileEnable()) {
if ($this->isMappedFile($file)) {
$response->headers->set('X-Accel-Redirect', $this->xAccelRedirectMapping($file));
} else if (null !== $this->logger) {
$this->logger->warning(sprintf('%s is not located under a nginx xAccelPath'));
}
BinaryFileResponse::trustXSendfileTypeHeader();
}
if (null !== $mimeType) {
@@ -104,16 +82,6 @@ class ServeFileResponseFactory implements DeliverDataInterface
return $this->xSendFileEnable;
}
private function sanitizeXAccelPath($path)
{
return sprintf('%s/', rtrim($path, '/'));
}
private function sanitizeXAccelMountPoint($mountPoint)
{
return sprintf('/%s/', rtrim(ltrim($mountPoint, '/'), '/'));
}
private function sanitizeFilename($filename)
{
return str_replace(array('/', '\\'), '', $filename);
@@ -123,20 +91,4 @@ class ServeFileResponseFactory implements DeliverDataInterface
{
return $this->unicode->remove_nonazAZ09($filename, true, true, true);
}
private function xAccelRedirectMapping($file)
{
return str_replace(array_keys($this->mappings), array_values($this->mappings), $file);
}
private function isMapped($file)
{
foreach (array_keys($this->mappings) as $path) {
if (false !== strpos($file, $path)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\XSendFile;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
class Mapping
{
private $mapping;
/**
* @param array $mapping
* @throws \InvalidArgumentException
*/
public function __construct(array $mapping)
{
$this->validate($mapping);
$this->mapping = $mapping;
}
public function __toString()
{
$final = array();
foreach($this->mapping as $entry) {
if (!is_dir($entry['directory']) || '' === $entry['mount-point']) {
continue;
}
$final[] = sprintf('%s=%s', $this->sanitizeMountPoint($entry['mount-point']), $this->sanitizePath(realpath($entry['directory'])));
}
return implode(',', $final);
}
public function getMapping()
{
return $this->mapping;
}
public static function create(Application $app, array $mapping = array())
{
if (isset($app['phraseanet.configuration']['xsendfile']['mapping'])) {
$confMapping = $app['phraseanet.configuration']['xsendfile']['mapping'];
if (!is_array($confMapping)) {
throw new InvalidArgumentException('XSendFile mapping configuration must be an array');
}
foreach($confMapping as $entry) {
$mapping[] = $entry;
}
}
return new Mapping($mapping);
}
public function sanitizePath($path)
{
return sprintf('/%s', rtrim(ltrim($path, '/'),'/'));
}
public function sanitizeMountPoint($mountPoint)
{
return sprintf('/%s', rtrim(ltrim($mountPoint, '/'), '/'));
}
private function validate(array $mapping)
{
foreach($mapping as $entry) {
if (!is_array($entry)) {
throw new \InvalidArgumentException('XSendFile mapping entry must be an array');
}
if (!isset($entry['directory']) && !isset($entry['mount-point'])) {
throw new \InvalidArgumentException('XSendFile mapping entry must contain at least two keys "directory" and "mounbt-point"');
}
}
}
}

View File

@@ -351,9 +351,8 @@ class API_V1_adapter extends API_V1_Abstract
'defaultLanguage' => $app['phraseanet.registry']->get('id_GV_default_lng'),
'allowIndexing' => $app['phraseanet.registry']->get('GV_allow_search_engine'),
'modes' => array(
'XsendFile' => $app['phraseanet.registry']->get('GV_modxsendfile'),
'nginxXAccelRedirect' => $app['phraseanet.registry']->get('GV_X_Accel_Redirect'),
'nginxXAccelRedirectMountPoint' => $app['phraseanet.registry']->get('GV_X_Accel_Redirect_mount_point'),
'XsendFile' => $app['phraseanet.configuration']['xsendfile']['enable'],
'XsendFileMapping' => $app['phraseanet.configuration']['xsendfile']['mapping'],
'h264Streaming' => $app['phraseanet.registry']->get('GV_h264_streaming'),
'authTokenDirectory' => $app['phraseanet.registry']->get('GV_mod_auth_token_directory'),
'authTokenDirectoryPath' => $app['phraseanet.registry']->get('GV_mod_auth_token_directory_path'),

View File

@@ -196,26 +196,6 @@ return call_user_func_array(function(Application $app) {
), array(
'section' => _('GV::section:: Executables externes'),
'vars' => array(
array(
'type' => \registry::TYPE_BOOLEAN,
'name' => 'GV_modxsendfile',
'comment' => _('reglages:: mod_xsendfileapache active'),
'default' => false
),
array(
'type' => \registry::TYPE_STRING,
'name' => 'GV_X_Accel_Redirect',
'comment' => _('reglages:: Path en acces pour X-Accel-Redirect (NginX Uniquement)'),
'default' => '',
'end_slash' => true
),
array(
'type' => \registry::TYPE_STRING,
'name' => 'GV_X_Accel_Redirect_mount_point',
'comment' => _('reglages:: Point de montage pour X-Accel-Redirect (NginX Uniquement)'),
'default' => 'noweb',
'end_slash' => false
),
array(
'type' => \registry::TYPE_BOOLEAN,
'name' => 'GV_h264_streaming',

View File

@@ -133,3 +133,9 @@ registration-fields:
-
name: geonameid
required: true
xsendfile:
enable: false
mapping:
-
directory: ''
mount-point: ''

View File

@@ -0,0 +1,16 @@
<?php
namespace Alchemy\Tests\Phrasea\Core\Provider;
/**
* @covers Alchemy\Phrasea\Core\Provider\FileServeServiceProvider
*/
class XSendFileMappingServiceProviderTest extends ServiceProviderTestCase
{
public function provideServiceDescription()
{
return array(
array('Alchemy\Phrasea\Core\Provider\XSendFileMappingServiceProvider', 'phraseanet.xsendfile-mapping', 'Alchemy\\Phrasea\\XSendFile\\Mapping'),
);
}
}

View File

@@ -3,6 +3,8 @@
namespace Alchemy\Tests\Phrasea\Response;
use Alchemy\Phrasea\Response\ServeFileResponseFactory;
use Alchemy\Phrasea\XSendFile\Mapping;
use Symfony\Component\HttpFoundation\Request;
class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
{
@@ -10,11 +12,7 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
public function testDeliverFile()
{
$this->factory = new ServeFileResponseFactory(
false,
array(
__DIR__ . '/../../../../files/' => '/protected/'
), new \unicode());
$this->factory = new ServeFileResponseFactory(false, new \unicode());
$response = $this->factory->deliverFile(__DIR__ . '/../../../../files/cestlafete.jpg');
@@ -24,11 +22,7 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
public function testDeliverFileWithFilename()
{
$this->factory = new ServeFileResponseFactory(
false,
array(
__DIR__ . '/../../../../files/' => '/protected/'
), new \unicode());
$this->factory = new ServeFileResponseFactory(false, new \unicode());
$response = $this->factory->deliverFile(__DIR__ . '/../../../../files/cestlafete.jpg', 'toto.jpg');
@@ -38,11 +32,7 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
public function testDeliverFileWithFilenameAndDisposition()
{
$this->factory = new ServeFileResponseFactory(
false,
array(
__DIR__ . '/../../../../files/' => '/protected/'
), new \unicode());
$this->factory = new ServeFileResponseFactory(false, new \unicode());
$response = $this->factory->deliverFile(__DIR__ . '/../../../../files/cestlafete.jpg', 'toto.jpg', 'attachment');
@@ -52,13 +42,18 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
public function testDeliverFileWithFilenameAndDispositionAndXSendFile()
{
$this->factory = new ServeFileResponseFactory(
true,
$this->factory = new ServeFileResponseFactory(true, new \unicode());
$request = Request::create('/');
$request->headers->set('X-SendFile-Type', 'X-Accel-Redirect');
$request->headers->set('X-Accel-Mapping', (string) new Mapping(array(
array(
__DIR__ . '/../../../../files/' => '/protected/'
), new \unicode());
'directory' => __DIR__ . '/../../../../files/',
'mount-point' => '/protected/'
)
)));
$response = $this->factory->deliverFile(__DIR__ . '/../../../../files/cestlafete.jpg', 'toto.jpg', 'attachment');
$response->prepare($request);
$this->assertInstanceOf("Symfony\Component\HttpFoundation\Response", $response);
$this->assertEquals('attachment; filename="toto.jpg"', $response->headers->get('content-disposition'));
@@ -67,13 +62,18 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
public function testDeliverFileWithFilenameAndDispositionAndXSendFileAndNoTrailingSlashes()
{
$this->factory = new ServeFileResponseFactory(
true,
$this->factory = new ServeFileResponseFactory(true, new \unicode());
$request = Request::create('/');
$request->headers->set('X-SendFile-Type', 'X-Accel-Redirect');
$request->headers->set('X-Accel-Mapping', (string) new Mapping(array(
array(
__DIR__ . '/../../../../files' => 'protected'
), new \unicode());
'directory' => __DIR__ . '/../../../../files/',
'mount-point' => '/protected/'
)
)));
$response = $this->factory->deliverFile(__DIR__ . '/../../../../files/cestlafete.jpg', 'toto.jpg', 'attachment');
$response->prepare($request);
$this->assertInstanceOf("Symfony\Component\HttpFoundation\Response", $response);
$this->assertEquals('attachment; filename="toto.jpg"', $response->headers->get('content-disposition'));
@@ -85,37 +85,36 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
*/
public function testDeliverUnexistingFile()
{
$this->factory = new ServeFileResponseFactory(
true,
array(
__DIR__ . '/../../../../files' => 'protected'
), new \unicode());
$this->factory = new ServeFileResponseFactory(true, new \unicode());
$this->factory->deliverFile(__DIR__ . '/../../../../files/does_not_exists.jpg', 'toto.jpg', 'attachment');
}
public function testDeliverFileWithFilenameAndDispositionAndXSendFileButFileNotInXAccelMapping()
{
$this->factory = new ServeFileResponseFactory(
true,
$this->factory = new ServeFileResponseFactory(true, new \unicode());
$request = Request::create('/');
$request->headers->set('X-SendFile-Type', 'X-Accel-Redirect');
$request->headers->set('X-Accel-Mapping', (string) new Mapping(array(
array(
__DIR__ . '/../../../../files' => 'protected'
), new \unicode());
'directory' => __DIR__ . '/../../../../files/',
'mount-point' => '/protected/'
)
)));
$response = $this->factory->deliverFile(__DIR__ . '/../../../../classes/PhraseanetPHPUnitAbstract.php', 'PhraseanetPHPUnitAbstract.php', 'attachment');
$file = __DIR__ . '/../../../../classes/PhraseanetPHPUnitAbstract.php';
$response = $this->factory->deliverFile($file, 'PhraseanetPHPUnitAbstract.php', 'attachment');
$response->prepare($request);
$this->assertInstanceOf("Symfony\Component\HttpFoundation\Response", $response);
$this->assertEquals('attachment; filename="PhraseanetPHPUnitAbstract.php"', $response->headers->get('content-disposition'));
$this->assertEquals(null, $response->headers->get('x-accel-redirect'));
$this->assertEquals(realpath($file), $response->headers->get('x-accel-redirect'));
}
public function testDeliverDatas()
{
$this->factory = new ServeFileResponseFactory(
false,
array(
__DIR__ . '/../../../../files/' => '/protected/'
), new \unicode());
$this->factory = new ServeFileResponseFactory(false, new \unicode());
$data = 'Sex,Name,Birthday
M,Alphonse,1932

View File

@@ -0,0 +1,59 @@
<?php
namespace Alchemy\Tests\Phrasea\XSendFile;
use Alchemy\Phrasea\XSendFile\Mapping;
class MappingTest extends \PhraseanetWebTestCaseAbstract
{
public function testOneMapping()
{
$mapping = new Mapping(array(
array(
'directory' => __DIR__ . '/../../../../files/',
'mount-point' => '/protected/'
)
));
$this->assertEquals('/protected/=/home/nlegoff/workspace/Phraseanet/tests/files/', (string) $mapping);
}
public function testMultiMapping()
{
$mapping = new Mapping(array(
array(
'directory' => __DIR__ . '/../../../../files/',
'mount-point' => '/protected/'
),
array(
'directory' => __DIR__ . '/../../../../',
'mount-point' => '/uploads/'
),
));
$this->assertEquals('/protected/=/home/nlegoff/workspace/Phraseanet/tests/files/,/uploads/=/home/nlegoff/workspace/Phraseanet/tests/', (string) $mapping);
}
public function testMultiMappingWithANotExsistingDir()
{
$mapping = new Mapping(array(
array(
'directory' => __DIR__ . '/../../../../files/',
'mount-point' => '/protected/'
),
array(
'directory' => __DIR__ . '/../../../../do_not_exists',
'mount-point' => '/test/'
),
));
$this->assertEquals('/protected/=/home/nlegoff/workspace/Phraseanet/tests/files/', (string) $mapping);
}
public function testEmptyMapping()
{
$mapping = new Mapping(array());
$this->assertEquals('', (string) $mapping);
}
}