Enhance XSendFileMapping to handle apache

Update xsendfile
This commit is contained in:
Nicolas Le Goff
2013-07-01 12:29:41 +02:00
parent 6c62fe7d16
commit 8e31a692cb
31 changed files with 803 additions and 394 deletions

View File

@@ -28,8 +28,7 @@ use Alchemy\Phrasea\CLI;
use Alchemy\Phrasea\Command\Plugin\AddPlugin;
use Alchemy\Phrasea\Command\Plugin\RemovePlugin;
use Alchemy\Phrasea\Command\CheckConfig;
use Alchemy\Phrasea\Command\Setup\XSendFileMappingNginxDumper;
use Alchemy\Phrasea\Command\Setup\XSendFileMappingApacheDumper;
use Alchemy\Phrasea\Command\Setup\XSendFileMappingDumper;
require_once __DIR__ . '/../lib/autoload.php';
@@ -98,8 +97,7 @@ try {
$app->command(new AddPlugin());
$app->command(new RemovePlugin());
$app->command(new Configuration());
$app->command(new XSendFileMappingNginxDumper());
$app->command(new XSendFileMappingApacheDumper());
$app->command(new XSendFileMappingDumper());
$result_code = is_int($app->run()) ? : 1;
} catch (\Exception $e) {

View File

@@ -1,50 +0,0 @@
<?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 Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* This command dumps XsendFile Apache condifuration
*/
class XSendFileMappingApacheDumper extends AbstractXSendFileMappingDumper
{
public function __construct()
{
parent::__construct('xsendfile:dump-apache');
$this->setDescription('Dump XSendFile mapping for Apache web server');
}
/**
* {@inheritdoc}
*/
protected function doDump(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 0;
}
}

View File

@@ -12,35 +12,45 @@
namespace Alchemy\Phrasea\Command\Setup;
use Alchemy\Phrasea\Command\Command;
use Alchemy\Phrasea\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
abstract class AbstractXSendFileMappingDumper extends Command
class XSendFileMappingDumper extends Command
{
public function __construct($name = null) {
parent::__construct('xsendfile:configuration-dumper');
}
/**
* {@inheritdoc}
*/
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$configuration = 0;
$output->writeln('');
if ($this->container['phraseanet.file-serve']->isXSendFileEnable()) {
$output->writeln('XSendFile support is <info>enabled</info>');
} else {
if (!$this->container['phraseanet.xsendfile-factory']->isXSendFileModeEnabled()) {
$output->writeln('XSendFile support is <error>disabled</error>');
$configuration++;
return 1;
}
if (2 < count($this->container['phraseanet.xsendfile-mapping']->getMapping())) {
$output->writeln('XSendFile support is <info>enabled</info>');
try {
$configuration = $this->container['phraseanet.xsendfile-factory']->getMode(true)->getVirtualHostConfiguration();
$output->writeln('XSendFile configuration seems <info>OK</info>');
} else {
$output->writeln($configuration);
return 0;
} catch (RuntimeException $e) {
$output->writeln('XSendFile configuration seems <error>invalid</error>');
$configuration++;
return 1;
}
$output->writeln('');
return $configuration + $this->doDump($input, $output);
return 0;
}
abstract protected function doDump(InputInterface $input, OutputInterface $output);
}

View File

@@ -1,49 +0,0 @@
<?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 Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* This command dumps XSendFile Nginx configuration
*/
class XSendFileMappingNginxDumper extends AbstractXSendFileMappingDumper
{
public function __construct()
{
parent::__construct('xsendfile:dump-nginx');
$this->setDescription('Dump xsendfile mapping for Nginx web server');
}
/**
* {@inheritdoc}
*/
protected function doDump(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(' add_header Etag $upstream_http_etag;');
$output->writeln(' add_header Link $upstream_http_link;');
$output->writeln(' alias ' . $mapper->sanitizePath($entry['directory']) . ';');
$output->writeln(' }');
$output->writeln('');
}
return 0;
}
}

View File

@@ -15,6 +15,7 @@ use Silex\Application;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class XSendFileSubscriber implements EventSubscriberInterface
{
@@ -34,10 +35,9 @@ class XSendFileSubscriber implements EventSubscriberInterface
public function applyHeaders(GetResponseEvent $event)
{
if ($this->app['phraseanet.configuration']['xsendfile']['enabled']) {
$request = $event->getRequest();
$request->headers->set('X-Sendfile-Type', 'X-Accel-Redirect');
$request->headers->set('X-Accel-Mapping', (string) $this->app['phraseanet.xsendfile-mapping']);
if ($this->app['phraseanet.xsendfile-factory']->isXSendFileModeEnabled()) {
BinaryFileResponse::trustXSendfileTypeHeader();
$this->app['phraseanet.xsendfile-factory']->getMode()->setHeaders($event->getRequest());
}
}
}

View File

@@ -11,11 +11,11 @@
namespace Alchemy\Phrasea\Core\Provider;
use Alchemy\Phrasea\Core\Event\Subscriber\XSendFileSubscriber;
use Alchemy\Phrasea\Http\ServeFileResponseFactory;
use Alchemy\Phrasea\Http\XSendFile\XSendFileFactory;
use Silex\Application;
use Silex\ServiceProviderInterface;
use Alchemy\Phrasea\Http\ServeFileResponseFactory;
use Alchemy\Phrasea\Http\XsendfileMapping;
use Alchemy\Phrasea\Core\Event\Subscriber\XSendFileSubscriber;
class FileServeServiceProvider implements ServiceProviderInterface
{
@@ -24,27 +24,8 @@ class FileServeServiceProvider implements ServiceProviderInterface
*/
public function register(Application $app)
{
$app['xsendfile.mapping'] = $app->share(function(Application $app) {
$mapping = array();
if (isset($app['phraseanet.configuration']['xsendfile']['mapping'])) {
$mapping = $app['phraseanet.configuration']['xsendfile']['mapping'];
}
$mapping[] = array(
'directory' => $app['root.path'] . '/tmp/download/',
'mount-point' => '/download/',
);
$mapping[] = array(
'directory' => $app['root.path'] . '/tmp/lazaret/',
'mount-point' => '/lazaret/',
);
return $mapping;
});
$app['phraseanet.xsendfile-mapping'] = $app->share(function($app) {
return new XsendfileMapping($app['xsendfile.mapping']);
$app['phraseanet.xsendfile-factory'] = $app->share(function($app) {
return XSendFileFactory::create($app);
});
$app['phraseanet.file-serve'] = $app->share(function (Application $app) {

View File

@@ -24,7 +24,7 @@ interface DeliverDataInterface
* @param string $file
* @param string $filename
* @param string $disposition
* @param string|null $mimetype
* @param string|null $mimeType
* @param integer $cacheDuration
*/
public function deliverFile($file, $filename = null, $disposition = self::DISPOSITION_INLINE, $mimeType = null, $cacheDuration = 3600);

View File

@@ -18,17 +18,11 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse;
class ServeFileResponseFactory implements DeliverDataInterface
{
private $xSendFileEnable = false;
private $unicode;
public function __construct($enableXSendFile, \unicode $unicode)
public function __construct(\unicode $unicode)
{
$this->xSendFileEnable = (Boolean) $enableXSendFile;
$this->unicode = $unicode;
if ($this->xSendFileEnable) {
BinaryFileResponse::trustXSendfileTypeHeader();
}
}
/**
@@ -38,7 +32,6 @@ class ServeFileResponseFactory implements DeliverDataInterface
public static function create(Application $app)
{
return new self(
$app['phraseanet.configuration']['xsendfile']['enabled'],
$app['unicode']
);
}
@@ -77,11 +70,6 @@ class ServeFileResponseFactory implements DeliverDataInterface
return $response;
}
public function isXSendFileEnable()
{
return $this->xSendFileEnable;
}
private function sanitizeFilename($filename)
{
return str_replace(array('/', '\\'), '', $filename);

View File

@@ -0,0 +1,68 @@
<?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\Http\XSendFile;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
abstract class AbstractXSendFileMode
{
protected $mapping = array();
/**
* @params array $mapping
*
* @throws InvalidArgumentException if mapping is invalid;
*/
public function __construct(array $mapping)
{
$this->setMapping($mapping);
}
/**
* @return array
*/
public function getMapping()
{
return $this->mapping;
}
/**
* @params array $mapping
*
* @throws InvalidArgumentException if mapping is invalid;
*/
abstract public function setMapping(array $mapping);
/**
* Sanitizes path directory.
*
* @param string $path
*
* @return string
*/
protected function sanitizePath($path)
{
return sprintf('/%s', trim($path, '/'));
}
/**
* Sanitizes a mount point.
*
* @param string $mountPoint
*
* @return string
*/
protected function sanitizeMountPoint($mountPoint)
{
return sprintf('/%s', trim($mountPoint, '/'));
}
}

View File

@@ -0,0 +1,74 @@
<?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\Http\XSendFile;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
use Symfony\Component\HttpFoundation\Request;
class ApacheMode extends AbstractXSendFileMode implements ModeInterface
{
/**
* {@inheritdoc}
*/
public function setHeaders(Request $request)
{
$request->headers->add(array(
'X-Sendfile-Type' => 'X-SendFile',
));
}
/**
* {@inheritdoc}
*/
public function setMapping(array $mapping)
{
$final = array();
foreach ($mapping as $entry) {
if (!is_array($entry)) {
throw new InvalidArgumentException('XSendFile mapping entry must be an array');
}
if (!isset($entry['directory'])) {
throw new InvalidArgumentException('XSendFile mapping entry must contain at least a "directory" key');
}
if (false === is_dir(trim($entry['directory']))) {
continue;
}
$final[] = array(
'directory' => $this->sanitizePath(realpath($entry['directory']))
);
}
$this->mapping = $final;
}
/**
* {@inheritdoc}
*/
public function getVirtualHostConfiguration()
{
$output = "\n";
$output .= "<IfModule mod_xsendfile.c>\n";
$output .= " <Files *>\n";
$output .= " XSendFile on\n";
foreach ($this->mapping as $entry) {
$output .= ' XSendFilePath ' . $entry['directory'] . "\n";
}
$output .= " </Files>\n";
$output .= "</IfModule>\n";
return $output;
}
}

View File

@@ -0,0 +1,31 @@
<?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\Http\XSendFile;
use Symfony\Component\HttpFoundation\Request;
interface ModeInterface
{
/**
* Sets XSendFile headers.
*
* @params Request $request
*/
public function setHeaders(Request $request);
/**
* Prints virtualhost configuration for current XSendFile mode.
*
* @return string
*/
public function getVirtualHostConfiguration();
}

View File

@@ -0,0 +1,88 @@
<?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\Http\XSendFile;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
use Symfony\Component\HttpFoundation\Request;
class NginxMode extends AbstractXSendFileMode implements ModeInterface
{
/**
* {@inheritdoc}
*/
public function setHeaders(Request $request)
{
$xAccelMapping = array();
foreach ($this->mapping as $entry) {
$xAccelMapping[] = sprintf('%s=%s', $entry['mount-point'], $entry['directory']);
}
if (count($xAccelMapping) > 0 ) {
$request->headers->add(array(
'X-Sendfile-Type' => 'X-Accel-Redirect',
'X-Accel-Mapping' => implode(',', $xAccelMapping),
));
}
}
/**
* {@inheritdoc}
*/
public function setMapping(array $mapping)
{
$final = array();
foreach ($mapping as $entry) {
if (!is_array($entry)) {
throw new InvalidArgumentException('XSendFile mapping entry must be an array');
}
if (!isset($entry['directory'])) {
throw new InvalidArgumentException('XSendFile mapping entry must contain at least a "directory" key');
}
if (!isset($entry['mount-point'])) {
throw new InvalidArgumentException('XSendFile mapping entry must contain at least a "mount-point" key');
}
if (false === is_dir(trim($entry['directory'])) || '' === trim($entry['mount-point'])) {
continue;
}
$final[] = array(
'directory' => $this->sanitizePath(realpath($entry['directory'])),
'mount-point' => $this->sanitizeMountPoint($entry['mount-point'])
);
}
$this->mapping = $final;
}
/**
* {@inheritdoc}
*/
public function getVirtualHostConfiguration()
{
$output = "\n";
foreach ($this->mapping as $entry) {
$output .= " location " . $entry['mount-point']. " {\n";
$output .= " internal;\n";
$output .= " add_header Etag \$upstream_http_etag;\n";
$output .= " add_header Link \$upstream_http_link;\n";
$output .= " alias " . $entry['directory'] . ";\n";
$output .= " }\n";
}
return $output;
}
}

View File

@@ -0,0 +1,32 @@
<?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\Http\XSendFile;
use Symfony\Component\HttpFoundation\Request;
class NullMode implements ModeInterface
{
/**
* {@inheritdoc}
*/
public function setHeaders(Request $request)
{
}
/**
* {@inheritdoc}
*/
public function getVirtualHostConfiguration()
{
return "\n";
}
}

View File

@@ -0,0 +1,124 @@
<?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\Http\XSendFile;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
use Alchemy\Phrasea\Exception\RuntimeException;
use Psr\Log\LoggerInterface;
class XSendFileFactory
{
private $enabled;
private $logger;
private $type;
private $mapping;
/**
* Constructor
*
* @param LoggerInterface $logger
* @param boolean $enabled
* @param string $type
* @param array $mapping
*/
public function __construct(LoggerInterface $logger, $enabled, $type, array $mapping)
{
$this->logger = $logger;
$this->enabled = (Boolean) $enabled;
$this->type = strtolower($type);
$this->mapping = $mapping;
}
/**
* Creates a new instance of XSendFile Factory according to the application
* configuration.
*
* @param Application $app
* @return XSendFileFactory
*/
public static function create(Application $app)
{
$conf = $app['phraseanet.configuration']['xsendfile'];
$mapping = array();
if (isset($conf['mapping'])) {
$mapping = $conf['mapping'];
}
$mapping[] = array(
'directory' => $app['root.path'] . '/tmp/download/',
'mount-point' => '/download/',
);
$mapping[] = array(
'directory' => $app['root.path'] . '/tmp/lazaret/',
'mount-point' => '/lazaret/',
);
return new self($app['monolog'], $conf['enabled'], $conf['type'], $mapping);
}
/**
* Returns a new instance of XSendFileModeInterface.
*
* @return null|ModeInterface
*
* @throws InvalidArgumentException if mode type is unknown
*/
public function getMode($throwException = false)
{
if (false === $this->enabled) {
return new NullMode();
}
switch ($this->type) {
case 'nginx':
case 'sendfile':
case 'xaccel':
case 'xaccelredirect':
case 'x-accel':
case 'x-accel-redirect':
if (2 >= count($this->mapping)) {
$this->logger->error('Invalid xsendfile mapping configuration.');
if ($throwException) {
throw new RuntimeException('Mapping is not set up.');
}
}
return new NginxMode($this->mapping);
case 'apache':
case 'apache2':
case 'xsendfile':
return new ApacheMode($this->mapping);
default:
$this->logger->error('Invalid xsendfile type configuration.');
if ($throwException) {
throw new InvalidArgumentException(sprintf(
'Invalid xsendfile type value "%s"',
$this->type
));
}
return new NullMode();
}
}
/**
* @return Boolean
*/
public function isXSendFileModeEnabled()
{
return $this->enabled;
}
}

View File

@@ -1,85 +0,0 @@
<?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\Http;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
class XsendfileMapping
{
private $mapping;
/**
* @param array $mapping
*
* @throws InvalidArgumentException
*/
public function __construct(array $mapping)
{
$this->validate($mapping);
$final = array();
foreach($mapping as $entry) {
if (!is_dir(trim($entry['directory'])) || '' === trim($entry['mount-point'])) {
continue;
}
$entry = array(
'directory' => $this->sanitizePath(realpath($entry['directory'])),
'mount-point' => $this->sanitizeMountPoint($entry['mount-point']),
);
$final[] = $entry;
}
$this->mapping = $final;
}
public function __toString()
{
$final = array();
foreach($this->mapping as $entry) {
$final[] = sprintf('%s=%s', $entry['mount-point'], $entry['directory']);
}
return implode(',', $final);
}
public function getMapping()
{
return $this->mapping;
}
public function sanitizePath($path)
{
return sprintf('/%s', trim($path, '/'));
}
public function sanitizeMountPoint($mountPoint)
{
return sprintf('/%s', trim($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 "mount-point"');
}
}
}
}

View File

@@ -56,6 +56,7 @@ class patch_3813 implements patchInterface
->getConfig();
$config['xsendfile']['enabled'] = (Boolean) $app['phraseanet.registry']->get('GV_modxsendfile', false);
$config['xsendfile']['type'] = $config['xsendfile']['enabled'] ? 'nginx' : '';
if (null !== $xsendfilePath && null !== $xsendfileMountPoint) {
$config['xsendfile']['mapping'] = array(array(

View File

@@ -135,6 +135,7 @@ registration-fields:
required: true
xsendfile:
enabled: false
type: nginx|apache2
mapping:
-
directory: ''

View File

@@ -145,7 +145,7 @@
<option value="{{ template.get_id() }}">{{ template.get_display_name() }}</option>
{% endfor %}
</select>
<button type='button' id='reset_rights'>{% trans 'Delete all users rights' %}</button>
<button class="btn" type='button' id='reset_rights'>{% trans 'Delete all users rights' %}</button>
</td>
</tr>
</table>
@@ -565,7 +565,7 @@
{% endif %}
</div>
<div class="PNB10" style="top:auto;height:20px;">
<button class="users_rights_valid">{% trans 'boutton::valider' %}</button>
<button class="users_rights_valid btn">{% trans 'boutton::valider' %}</button>
<a href="{{ path('admin_users_search') }}" class="users_rights_cancel">{% trans 'boutton::retour' %}</a>
</div>
</div>

View File

@@ -20,7 +20,7 @@
<input type="checkbox" name="date_fields[]" value="{{ field }}" {% if field in configuration['date_fields'] %}checked="checked"{% endif %} > {{ field }}
{% endfor %}
<button type="submit">{% trans 'boutton::valider' %}</button>
<button type="submit" class="btn btn-warning" >{% trans 'boutton::valider' %}</button>
</form>
<textarea style="font-family: monospace;width:90%;height:70%">{{ configfile }}</textarea>

View File

@@ -116,6 +116,7 @@ registration-fields:
required: true
xsendfile:
enabled: false
type: ''
mapping:
-
directory: ''

View File

@@ -116,6 +116,7 @@ registration-fields:
required: true
xsendfile:
enabled: false
type: ''
mapping:
-
directory: ''

View File

@@ -141,6 +141,7 @@ registration-fields:
required: true
xsendfile:
enabled: false
type: ''
mapping:
-
directory: ''

View File

@@ -141,6 +141,7 @@ registration-fields:
required: true
xsendfile:
enabled: false
type: ''
mapping:
-
directory: ''

View File

@@ -21,32 +21,24 @@ class FileServeServiceProviderTest extends ServiceProviderTestCase
),
array(
'Alchemy\Phrasea\Core\Provider\FileServeServiceProvider',
'phraseanet.xsendfile-mapping',
'Alchemy\Phrasea\Http\XsendfileMapping'
'phraseanet.xsendfile-factory',
'Alchemy\Phrasea\Http\XSendFile\XSendFileFactory'
),
);
}
public function testMapping()
{
$app = new Application();
$app = clone self::$DI['app'];
$app['root.path'] = __DIR__ . '/../../../../../..';
$app->register(new ConfigurationServiceProvider());
$app->register(new FileServeServiceProvider());
$app['phraseanet.configuration.config-path'] = __DIR__ . '/fixtures/config-mapping.yml';
$app['phraseanet.configuration.config-compiled-path'] = __DIR__ . '/fixtures/config-mapping.php';
$this->assertEquals(array(array(
'directory' => '/tmp',
'mount-point' => 'mount',
),array(
'directory' => __DIR__ . '/../../../../../../tmp/download/',
'mount-point' => '/download/',
),array(
'directory' => __DIR__ . '/../../../../../../tmp/lazaret/',
'mount-point' => '/lazaret/',
)), $app['xsendfile.mapping']);
$this->assertInstanceOf('Alchemy\Phrasea\Http\XSendFile\NginxMode', $app['phraseanet.xsendfile-factory']->getMode());
$this->assertEquals(3, count($app['phraseanet.xsendfile-factory']->getMode()->getMapping()));
unlink($app['phraseanet.configuration.config-compiled-path']);
unset($app);
}
}

View File

@@ -1,5 +1,6 @@
xsendfile:
enabled: false
enabled: true
type: 'nginx'
mapping:
-
directory: '/tmp'

View File

@@ -3,16 +3,23 @@
namespace Alchemy\Tests\Phrasea\Http;
use Alchemy\Phrasea\Http\ServeFileResponseFactory;
use Alchemy\Phrasea\Http\XsendfileMapping;
use Alchemy\Phrasea\Http\XSendFile\NginxMode;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
{
protected $factory;
public function testDeliverFileFactoryCreation()
{
$factory = ServeFileResponseFactory::create(self::$DI['app']);
$this->assertInstanceOf('Alchemy\Phrasea\Http\ServeFileResponseFactory', $factory);
}
public function testDeliverFile()
{
$this->factory = new ServeFileResponseFactory(false, new \unicode());
$this->factory = new ServeFileResponseFactory(new \unicode());
$response = $this->factory->deliverFile(__DIR__ . '/../../../../files/cestlafete.jpg');
@@ -25,7 +32,7 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
public function testDeliverFileWithDuration()
{
$this->factory = new ServeFileResponseFactory(false, new \unicode());
$this->factory = new ServeFileResponseFactory(new \unicode());
$response = $this->factory->deliverFile(__DIR__ . '/../../../../files/cestlafete.jpg', 'hello', 'attachment', 'application/json', 23456);
@@ -35,7 +42,7 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
public function testDeliverFileWithFilename()
{
$this->factory = new ServeFileResponseFactory(false, new \unicode());
$this->factory = new ServeFileResponseFactory(new \unicode());
$response = $this->factory->deliverFile(__DIR__ . '/../../../../files/cestlafete.jpg', 'toto.jpg');
@@ -45,7 +52,7 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
public function testDeliverFileWithFilenameAndDisposition()
{
$this->factory = new ServeFileResponseFactory(false, new \unicode());
$this->factory = new ServeFileResponseFactory(new \unicode());
$response = $this->factory->deliverFile(__DIR__ . '/../../../../files/cestlafete.jpg', 'toto.jpg', 'attachment');
@@ -55,15 +62,18 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
public function testDeliverFileWithFilenameAndDispositionAndXSendFile()
{
$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 XsendfileMapping(array(
BinaryFileResponse::trustXSendfileTypeHeader();
$this->factory = new ServeFileResponseFactory(new \unicode());
$mode = new NginxMode(
array(
'directory' => __DIR__ . '/../../../../files/',
'mount-point' => '/protected/'
array(
'directory' => __DIR__ . '/../../../../files/',
'mount-point' => '/protected/'
)
)
)));
);
$request = Request::create('/');
$mode->setHeaders($request);
$response = $this->factory->deliverFile(__DIR__ . '/../../../../files/cestlafete.jpg', 'toto.jpg', 'attachment');
$response->prepare($request);
@@ -75,15 +85,18 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
public function testDeliverFileWithFilenameAndDispositionAndXSendFileAndNoTrailingSlashes()
{
$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 XsendfileMapping(array(
BinaryFileResponse::trustXSendfileTypeHeader();
$this->factory = new ServeFileResponseFactory(new \unicode());
$mode = new NginxMode(
array(
'directory' => __DIR__ . '/../../../../files/',
'mount-point' => '/protected/'
array(
'directory' => __DIR__ . '/../../../../files',
'mount-point' => '/protected'
)
)
)));
);
$request = Request::create('/');
$mode->setHeaders($request);
$response = $this->factory->deliverFile(__DIR__ . '/../../../../files/cestlafete.jpg', 'toto.jpg', 'attachment');
$response->prepare($request);
@@ -98,22 +111,26 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
*/
public function testDeliverUnexistingFile()
{
$this->factory = new ServeFileResponseFactory(true, new \unicode());
BinaryFileResponse::trustXSendfileTypeHeader();
$this->factory = new ServeFileResponseFactory(new \unicode());
$this->factory->deliverFile(__DIR__ . '/../../../../files/does_not_exists.jpg', 'toto.jpg', 'attachment');
}
public function testDeliverFileWithFilenameAndDispositionAndXSendFileButFileNotInXAccelMapping()
{
$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 XsendfileMapping(array(
BinaryFileResponse::trustXSendfileTypeHeader();
$this->factory = new ServeFileResponseFactory(new \unicode());
$mode = new NginxMode(
array(
'directory' => __DIR__ . '/../../../../files/',
'mount-point' => '/protected/'
array(
'directory' => __DIR__ . '/../../../../files/',
'mount-point' => '/protected/'
)
)
)));
);
$request = Request::create('/');
$mode->setHeaders($request);
$file = __DIR__ . '/../../../../classes/PhraseanetPHPUnitAbstract.php';
@@ -127,7 +144,7 @@ class ServeFileResponseFactoryTest extends \PhraseanetWebTestCaseAbstract
public function testDeliverDatas()
{
$this->factory = new ServeFileResponseFactory(false, new \unicode());
$this->factory = new ServeFileResponseFactory(new \unicode());
$data = 'Sex,Name,Birthday
M,Alphonse,1932

View File

@@ -0,0 +1,47 @@
<?php
namespace Alchemy\Tests\Phrasea\Http\XSendFile;
use Alchemy\Phrasea\Http\XSendFile\ApacheMode;
use Symfony\Component\HttpFoundation\Request;
class ApacheModeTest extends \PhraseanetPHPUnitAbstract
{
public function testGetVirtualHost()
{
$mode = new ApacheMode(array(array('directory' => __DIR__ )));
$conf = $mode->getVirtualHostConfiguration();
$this->assertRegExp('#'.__DIR__ . '#', $conf);
}
public function testSetValidHeaders()
{
$request = Request::create('/');
$mode = new ApacheMode(array(array('directory' => __DIR__ )));
$mode->setHeaders($request);
$this->assertArrayHasKey('x-sendfile-type', $request->headers->all());
}
public function testUnextingDirectoryMapping()
{
$mode = new ApacheMode(array(array('directory' => __DIR__ . '/Unknown/Dir')));
$this->assertEquals(array(), $mode->getMapping());
}
/**
* @dataProvider provideMappings
* @expectedException Alchemy\Phrasea\Exception\InvalidArgumentException
*/
public function testInvalidMapping($mapping)
{
new ApacheMode($mapping);
}
public function provideMappings()
{
return array(
array(array(array('wrong-key' => __DIR__))),
array(array('not-an-array')),
);
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Alchemy\Tests\Phrasea\Http\XSendFile;
use Alchemy\Phrasea\Http\XSendFile\NginxMode;
use Symfony\Component\HttpFoundation\Request;
class XSendFileModeNginxTest extends \PhraseanetPHPUnitAbstract
{
public function testGetVirtualHost()
{
$mode = new NginxMode(array(array(
'directory' => __DIR__, 'mount-point' => '/download')));
$conf = $mode->getVirtualHostConfiguration();
$this->assertRegExp('#'.__DIR__ . '#', $conf);
}
public function testSetValidHeaders()
{
$request = Request::create('/');
$mode = new NginxMode(array(array('directory' => __DIR__, 'mount-point' => '/download')));
$mode->setHeaders($request);
$this->assertArrayHasKey('x-sendfile-type', $request->headers->all());
$this->assertArrayHasKey('x-accel-mapping', $request->headers->all());
}
public function testSetValidMultiHeaders()
{
$protected = __DIR__ . '/../../../../../files/';
$upload = __DIR__ . '/../../../../../';
$request = Request::create('/');
$mode = new NginxMode(array(
array(
'directory' => $protected,
'mount-point' => '/protected/'
),
array(
'directory' => $upload,
'mount-point' => '/uploads/'
),
));
$mode->setHeaders($request);
$this->assertArrayHasKey('x-sendfile-type', $request->headers->all());
$this->assertArrayHasKey('x-accel-mapping', $request->headers->all());
$this->assertEquals('/protected='.realpath($protected).',/uploads='.realpath($upload), $request->headers->get('X-Accel-Mapping'));
}
public function testSetInvalidHeaders()
{
$request = Request::create('/');
$mode = new NginxMode(array(array('directory' => __DIR__ . '/Unknown/Dir', 'mount-point' => '/download')));
$mode->setHeaders($request);
$this->assertArrayNotHasKey('x-sendfile-type', $request->headers->all());
$this->assertArrayNotHasKey('x-accel-mapping', $request->headers->all());
}
public function testUnextingDirectoryMapping()
{
$mode = new NginxMode(array(array('directory' => __DIR__ . '/Unknown/Dir', 'mount-point' => '/download')));
$this->assertEquals(array(), $mode->getMapping());
}
/**
* @dataProvider provideMappings
* @expectedException Alchemy\Phrasea\Exception\InvalidArgumentException
*/
public function testInvalidMapping($mapping)
{
new NginxMode($mapping);
}
public function provideMappings()
{
return array(
array(array(array('Directory' => __DIR__))),
array(array(array('wrong-key' => __DIR__, 'mount-point' => '/'))),
array(array(array('directory' => __DIR__, 'wrong-key' => '/'))),
array(array('not-an-array')),
);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Alchemy\Tests\Phrasea\Http\XSendFile;
use Alchemy\Phrasea\Http\XSendFile\NullMode;
use Symfony\Component\HttpFoundation\Request;
class NullModeTest extends \PhraseanetPHPUnitAbstract
{
public function testGetVirtualHost()
{
$mode = new NullMode();
$conf = $mode->getVirtualHostConfiguration();
$this->assertSame("\n", $conf);
}
public function testSetHeaders()
{
$mode = new NullMode();
$request = Request::create('/');
$before = (string) $request->headers;
$mode->setHeaders($request);
$this->assertSame($before, (string) $request->headers);
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace Alchemy\Tests\Phrasea\Http\XSendFile;
use Alchemy\Phrasea\Http\XSendFile\XSendFileFactory;
class XSendFileFactoryTest extends \PhraseanetPHPUnitAbstract
{
public function testFactoryCreation()
{
$factory = XSendFileFactory::create(self::$DI['app']);
$this->assertInstanceOf('Alchemy\Phrasea\Http\XSendFile\XSendFileFactory', $factory);
}
public function testFactoryWithXsendFileEnable()
{
$logger = $this->getMock('Psr\Log\LoggerInterface');
$factory = new XSendFileFactory($logger, true, 'nginx', $this->getNginxMapping());
$this->assertInstanceOf('Alchemy\Phrasea\Http\XSendFile\ModeInterface', $factory->getMode());
}
public function testFactoryWithXsendFileDisabled()
{
$logger = $this->getMock('Psr\Log\LoggerInterface');
$factory = new XSendFileFactory($logger, false, 'nginx',$this->getNginxMapping());
$this->assertInstanceOf('Alchemy\Phrasea\Http\XSendFile\NullMode', $factory->getMode());
$this->assertFalse($factory->isXSendFileModeEnabled());
}
/**
* @expectedException Alchemy\Phrasea\Exception\InvalidArgumentException
*/
public function testFactoryWithWrongTypeThrowsAnExceptionIfRequired()
{
$logger = $this->getMock('Psr\Log\LoggerInterface');
$factory = new XSendFileFactory($logger, true, 'wrong-type', $this->getNginxMapping());
$factory->getMode(true);
}
public function testFactoryWithWrongTypeDoesNotThrowsAnExceptio()
{
$logger = $this->getMock('Psr\Log\LoggerInterface');
$logger->expects($this->once())
->method('error')
->with($this->isType('string'));
$factory = new XSendFileFactory($logger, true, 'wrong-type', $this->getNginxMapping());
$this->assertInstanceOf('Alchemy\Phrasea\Http\XSendFile\NullMode', $factory->getMode(false));
}
/**
* @dataProvider provideTypes
*/
public function testFactoryType($type, $mapping, $classmode)
{
$logger = $this->getMock('Psr\Log\LoggerInterface');
$factory = new XSendFileFactory($logger, true, $type, $mapping);
$this->assertInstanceOf($classmode, $factory->getMode());
}
public function provideTypes()
{
return array(
array('apache', $this->getApacheMapping(), 'Alchemy\Phrasea\Http\XSendFile\ApacheMode'),
array('apache2', $this->getApacheMapping(), 'Alchemy\Phrasea\Http\XSendFile\ApacheMode'),
array('xsendfile', $this->getApacheMapping(), 'Alchemy\Phrasea\Http\XSendFile\ApacheMode'),
array('nginx',$this->getNginxMapping(), 'Alchemy\Phrasea\Http\XSendFile\NginxMode'),
array('sendfile',$this->getNginxMapping(), 'Alchemy\Phrasea\Http\XSendFile\NginxMode'),
array('xaccel',$this->getNginxMapping(), 'Alchemy\Phrasea\Http\XSendFile\NginxMode'),
array('xaccelredirect',$this->getNginxMapping(), 'Alchemy\Phrasea\Http\XSendFile\NginxMode'),
array('x-accel',$this->getNginxMapping(), 'Alchemy\Phrasea\Http\XSendFile\NginxMode'),
array('x-accel-redirect',$this->getNginxMapping(), 'Alchemy\Phrasea\Http\XSendFile\NginxMode'),
);
}
public function testInvalidMappingThrowsAnExceptionIfRequired()
{
$logger = $this->getMock('Psr\Log\LoggerInterface');
$logger->expects($this->once())
->method('error')
->with($this->isType('string'));
$factory = new XSendFileFactory($logger, true, 'nginx', array());
$this->setExpectedException('Alchemy\Phrasea\Exception\RuntimeException');
$factory->getMode(true);
}
public function testInvalidMappingDoesNotThrowsAnException()
{
$logger = $this->getMock('Psr\Log\LoggerInterface');
$logger->expects($this->once())
->method('error')
->with($this->isType('string'));
$factory = new XSendFileFactory($logger, true, 'nginx', array());
$this->assertInstanceOf('Alchemy\Phrasea\Http\XSendFile\NginxMode', $factory->getMode(false));
}
public function testInvalidMappingDoesNotThrowsAnExceptionByDefault()
{
$logger = $this->getMock('Psr\Log\LoggerInterface');
$logger->expects($this->once())
->method('error')
->with($this->isType('string'));
$factory = new XSendFileFactory($logger, true, 'nginx', array());
$this->assertInstanceOf('Alchemy\Phrasea\Http\XSendFile\NginxMode', $factory->getMode());
}
private function getNginxMapping()
{
return array(array(
'directory' => __DIR__ . '/../../../../files/',
'mount-point' => '/protected/'
));
}
private function getApacheMapping()
{
return array(array(
'directory' => __DIR__ . '/../../../../files/',
));
}
}

View File

@@ -1,103 +0,0 @@
<?php
namespace Alchemy\Tests\Phrasea\Http;
use Alchemy\Phrasea\Http\XsendfileMapping;
class XsendfileMappingTest extends \PhraseanetWebTestCaseAbstract
{
public function testOneMapping()
{
$dir = __DIR__ . '/../../../../files/';
$mapping = new XsendfileMapping(array(
array(
'directory' => $dir,
'mount-point' => '/protected/'
)
));
$this->assertEquals('/protected='.realpath($dir), (string) $mapping);
}
public function testMultiMapping()
{
$protected = __DIR__ . '/../../../../files/';
$upload = __DIR__ . '/../../../../';
$mapping = new XsendfileMapping(array(
array(
'directory' => $protected,
'mount-point' => '/protected/'
),
array(
'directory' => $upload,
'mount-point' => '/uploads/'
),
));
$this->assertEquals('/protected='.realpath($protected).',/uploads='.realpath($upload), (string) $mapping);
}
public function testMultiMappingWithANotExsistingDir()
{
$protected = __DIR__ . '/../../../../files/';
$mapping = new XsendfileMapping(array(
array(
'directory' => $protected,
'mount-point' => '/protected/'
),
array(
'directory' => '/path/to/nonexistent/directory',
'mount-point' => '/test/'
),
));
$this->assertEquals('/protected='.realpath($protected), (string) $mapping);
}
public function testEmptyMapping()
{
$mapping = new XsendfileMapping(array());
$this->assertEquals('', (string) $mapping);
}
/**
* @dataProvider provideVariousMappings
*/
public function testGetMapping($map, $expected)
{
$mapping = new XsendfileMapping($map);
$this->assertEquals($expected, $mapping->getMapping());
}
public function provideVariousMappings()
{
return array(
array(array(), array()),
array(array(array('mount-point' => false, 'directory' => false)), array()),
array(array(array('mount-point' => 'mount', 'directory' => false)), array()),
array(array(array('mount-point' => false, 'directory' => __DIR__)), array()),
array(array(array('mount-point' => 'mount', 'directory' => __DIR__)), array(array('mount-point' => '/mount', 'directory' => __DIR__))),
array(array(array('mount-point' => '/mount/', 'directory' => __DIR__ . '/../..')), array(array('mount-point' => '/mount', 'directory' => realpath(__DIR__.'/../..')))),
);
}
/**
* @dataProvider provideInvalidMappings
* @expectedException Alchemy\Phrasea\Exception\InvalidArgumentException
*/
public function testInvalidMapping($map)
{
new XsendfileMapping($map);
}
public function provideInvalidMappings()
{
return array(
array(array('mount-point' => '/mount', 'directory' => __DIR__)),
array(array(array('mount-point' => '/mount'))),
array(array(array('directory' => __DIR__))),
);
}
}