feature #29896 [Mime] Add the component (fabpot)

This PR was squashed before being merged into the 4.3-dev branch (closes #29896).

Discussion
----------

[Mime] Add the component

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #28832 #21985 makes #15460 trivial
| License       | MIT
| Doc PR        | symfony/symfony-docs#10886

This has been on my todo-list for X years :)

Commits
-------

bdca5d999b tweaked code
5268389191 [Mime] added freedesktop as a source for mime types
74ca91deaa [Mime] added the component
d7ee0ecc49 [HttpFoundation] updated File code
This commit is contained in:
Fabien Potencier 2019-01-17 07:58:40 +01:00
commit db6784bb09
42 changed files with 3945 additions and 68 deletions

View File

@ -60,6 +60,7 @@
"symfony/ldap": "self.version",
"symfony/lock": "self.version",
"symfony/messenger": "self.version",
"symfony/mime": "self.version",
"symfony/monolog-bridge": "self.version",
"symfony/options-resolver": "self.version",
"symfony/process": "self.version",

View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Registers custom mime types guessers.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class AddMimeTypeGuesserPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if ($container->has('mime_types')) {
$definition = $container->findDefinition('mime_types');
foreach ($container->findTaggedServiceIds('mime.mime_type_guesser', true) as $id => $attributes) {
$definition->addMethodCall('registerGuesser', [new Reference($id)]);
}
}
}
}

View File

@ -42,6 +42,7 @@ class UnusedTagsPass implements CompilerPassInterface
'messenger.bus',
'messenger.receiver',
'messenger.message_handler',
'mime.mime_type_guesser',
'monolog.logger',
'proxy',
'routing.expression_language_provider',

View File

@ -73,6 +73,8 @@ use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
use Symfony\Component\Messenger\Transport\TransportInterface;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypes;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
@ -310,6 +312,10 @@ class FrameworkExtension extends Extension
'Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController',
]);
if (class_exists(MimeTypes::class)) {
$loader->load('mime_type.xml');
}
$container->registerForAutoconfiguration(Command::class)
->addTag('console.command');
$container->registerForAutoconfiguration(ResourceCheckerInterface::class)
@ -374,6 +380,8 @@ class FrameworkExtension extends Extension
->addTag('messenger.message_handler');
$container->registerForAutoconfiguration(TransportFactoryInterface::class)
->addTag('messenger.transport_factory');
$container->registerForAutoconfiguration(MimeTypeGuesserInterface::class)
->addTag('mime.mime_type_guesser');
$container->registerForAutoconfiguration(LoggerAwareInterface::class)
->addMethodCall('setLogger', [new Reference('logger')]);

View File

@ -14,6 +14,7 @@ namespace Symfony\Bundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddAnnotationsCachedReaderPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddDebugLogProcessorPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddMimeTypeGuesserPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\DataCollectorTranslatorPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\LoggingTranslatorPass;
@ -72,6 +73,11 @@ class FrameworkBundle extends Bundle
if ($trustedHosts = $this->container->getParameter('kernel.trusted_hosts')) {
Request::setTrustedHosts($trustedHosts);
}
if ($this->container->has('mime_types')) {
$mt = $this->container->get('mime_types');
$mt->setDefault($mt);
}
}
public function build(ContainerBuilder $container)
@ -118,6 +124,7 @@ class FrameworkBundle extends Bundle
$container->addCompilerPass(new ResettableServicePass());
$container->addCompilerPass(new TestServiceContainerWeakRefPass(), PassConfig::TYPE_BEFORE_REMOVING, -32);
$container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING);
$container->addCompilerPass(new AddMimeTypeGuesserPass());
$this->addCompilerPassIfExists($container, MessengerPass::class);
if ($container->getParameter('kernel.debug')) {

View File

@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults public="false" />
<service id="mime_types" class="Symfony\Component\Mime\MimeTypes" public="true" />
</services>
</container>

View File

@ -42,6 +42,7 @@
</service>
<service id="serializer.normalizer.data_uri" class="Symfony\Component\Serializer\Normalizer\DataUriNormalizer">
<argument type="service" id="mime_types" on-invalid="null" />
<!-- Run before serializer.normalizer.object -->
<tag name="serializer.normalizer" priority="-920" />
</service>

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddMimeTypeGuesserPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Mime\FileinfoMimeTypeGuesser;
use Symfony\Component\Mime\MimeTypes;
class AddMimeTypeGuesserPassTest extends TestCase
{
public function testTags()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new AddMimeTypeGuesserPass());
$definition = new Definition(FileinfoMimeTypeGuesser::class);
$definition->addArgument('/path/to/magic/file');
$definition->addTag('mime.mime_type_guesser');
$container->setDefinition('some_mime_type_guesser', $definition->setPublic(true));
$container->register('mime_types', MimeTypes::class)->setPublic(true);
$container->compile();
$router = $container->getDefinition('mime_types');
$calls = $router->getMethodCalls();
$this->assertCount(1, $calls);
$this->assertEquals('registerGuesser', $calls[0][0]);
$this->assertEquals(new Reference('some_mime_type_guesser'), $calls[0][1][0]);
}
}

View File

@ -23,7 +23,7 @@
"symfony/contracts": "^1.0.2",
"symfony/dependency-injection": "^4.2",
"symfony/event-dispatcher": "^4.1",
"symfony/http-foundation": "^4.1.2",
"symfony/http-foundation": "^4.3",
"symfony/http-kernel": "^4.2",
"symfony/polyfill-mbstring": "~1.0",
"symfony/filesystem": "~3.4|~4.0",
@ -43,10 +43,11 @@
"symfony/form": "^4.2",
"symfony/expression-language": "~3.4|~4.0",
"symfony/messenger": "^4.2",
"symfony/mime": "^4.3",
"symfony/process": "~3.4|~4.0",
"symfony/security-core": "~3.4|~4.0",
"symfony/security-csrf": "~3.4|~4.0",
"symfony/serializer": "^4.2",
"symfony/serializer": "^4.3",
"symfony/stopwatch": "~3.4|~4.0",
"symfony/translation": "~4.2",
"symfony/templating": "~3.4|~4.0",

View File

@ -13,8 +13,7 @@ namespace Symfony\Component\HttpFoundation\File;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser;
use Symfony\Component\Mime\MimeTypes;
/**
* A file in the file system.
@ -50,33 +49,28 @@ class File extends \SplFileInfo
*
* @return string|null The guessed extension or null if it cannot be guessed
*
* @see ExtensionGuesser
* @see MimeTypes
* @see getMimeType()
*/
public function guessExtension()
{
$type = $this->getMimeType();
$guesser = ExtensionGuesser::getInstance();
return $guesser->guess($type);
return MimeTypes::getDefault()->getExtensions($this->getMimeType())[0] ?? null;
}
/**
* Returns the mime type of the file.
*
* The mime type is guessed using a MimeTypeGuesser instance, which uses finfo(),
* mime_content_type() and the system binary "file" (in this order), depending on
* which of those are available.
* The mime type is guessed using a MimeTypeGuesserInterface instance,
* which uses finfo_file() then the "file" system binary,
* depending on which of those are available.
*
* @return string|null The guessed mime type (e.g. "application/pdf")
*
* @see MimeTypeGuesser
* @see MimeTypes
*/
public function getMimeType()
{
$guesser = MimeTypeGuesser::getInstance();
return $guesser->guess($this->getPathname());
return MimeTypes::getDefault()->guessMimeType($this->getPathname());
}
/**

View File

@ -11,6 +11,10 @@
namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\Mime\MimeTypes;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', ExtensionGuesser::class, MimeTypes::class), E_USER_DEPRECATED);
/**
* A singleton mime type to file extension guesser.
*
@ -22,6 +26,8 @@ namespace Symfony\Component\HttpFoundation\File\MimeType;
* $guesser->register(new MyCustomExtensionGuesser());
*
* The last registered guesser is preferred over previously registered ones.
*
* @deprecated since Symfony 4.3, use {@link MimeTypes} instead
*/
class ExtensionGuesser implements ExtensionGuesserInterface
{

View File

@ -11,8 +11,12 @@
namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\Mime\MimeTypes;
/**
* Guesses the file extension corresponding to a given mime type.
*
* @deprecated since Symfony 4.3, use {@link MimeTypes} instead
*/
interface ExtensionGuesserInterface
{

View File

@ -13,11 +13,16 @@ namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\Mime\FileBinaryMimeTypeGuesser as NewFileBinaryMimeTypeGuesser;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', FileBinaryMimeTypeGuesser::class, NewFileBinaryMimeTypeGuesser::class), E_USER_DEPRECATED);
/**
* Guesses the mime type with the binary "file" (only available on *nix).
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since Symfony 4.3, use {@link NewFileBinaryMimeTypeGuesser} instead
*/
class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
{

View File

@ -13,11 +13,16 @@ namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\Mime\FileinfoMimeTypeGuesser as NewFileinfoMimeTypeGuesser;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', FileinfoMimeTypeGuesser::class, NewFileinfoMimeTypeGuesser::class), E_USER_DEPRECATED);
/**
* Guesses the mime type using the PECL extension FileInfo.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since Symfony 4.3, use {@link NewFileinfoMimeTypeGuesser} instead
*/
class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface
{

View File

@ -11,8 +11,14 @@
namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\Mime\MimeTypes;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', MimeTypeExtensionGuesser::class, MimeTypes::class), E_USER_DEPRECATED);
/**
* Provides a best-guess mapping of mime type to file extension.
*
* @deprecated since Symfony 4.3, use {@link MimeTypes} instead
*/
class MimeTypeExtensionGuesser implements ExtensionGuesserInterface
{

View File

@ -13,6 +13,9 @@ namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\Mime\MimeTypes;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', MimeTypeGuesser::class, MimeTypes::class), E_USER_DEPRECATED);
/**
* A singleton mime type guesser.

View File

@ -13,11 +13,14 @@ namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\Mime\MimeTypes;
/**
* Guesses the mime type of a file.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since Symfony 4.3, use {@link MimeTypes} instead
*/
interface MimeTypeGuesserInterface
{

View File

@ -20,7 +20,7 @@ use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException;
use Symfony\Component\HttpFoundation\File\Exception\NoFileException;
use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException;
use Symfony\Component\HttpFoundation\File\Exception\PartialFileException;
use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;
use Symfony\Component\Mime\MimeTypes;
/**
* A file uploaded through a form.
@ -140,10 +140,7 @@ class UploadedFile extends File
*/
public function guessClientExtension()
{
$type = $this->getClientMimeType();
$guesser = ExtensionGuesser::getInstance();
return $guesser->guess($type);
return MimeTypes::getDefault()->getExtensions($this->getClientMimeType())[0] ?? null;
}
/**

View File

@ -13,8 +13,10 @@ namespace Symfony\Component\HttpFoundation\Tests\File;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser;
/**
* @requires extension fileinfo
*/
class FileTest extends TestCase
{
protected $file;
@ -22,50 +24,24 @@ class FileTest extends TestCase
public function testGetMimeTypeUsesMimeTypeGuessers()
{
$file = new File(__DIR__.'/Fixtures/test.gif');
$guesser = $this->createMockGuesser($file->getPathname(), 'image/gif');
MimeTypeGuesser::getInstance()->register($guesser);
$this->assertEquals('image/gif', $file->getMimeType());
}
public function testGuessExtensionWithoutGuesser()
{
$file = new File(__DIR__.'/Fixtures/directory/.empty');
$this->assertNull($file->guessExtension());
}
public function testGuessExtensionIsBasedOnMimeType()
{
$file = new File(__DIR__.'/Fixtures/test');
$guesser = $this->createMockGuesser($file->getPathname(), 'image/gif');
MimeTypeGuesser::getInstance()->register($guesser);
$this->assertEquals('gif', $file->guessExtension());
}
/**
* @requires extension fileinfo
*/
public function testGuessExtensionWithReset()
{
$file = new File(__DIR__.'/Fixtures/other-file.example');
$guesser = $this->createMockGuesser($file->getPathname(), 'image/gif');
MimeTypeGuesser::getInstance()->register($guesser);
$this->assertEquals('gif', $file->guessExtension());
MimeTypeGuesser::reset();
$this->assertNull($file->guessExtension());
}
public function testConstructWhenFileNotExists()
{
$this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException');
new File(__DIR__.'/Fixtures/not_here');
}
@ -164,17 +140,4 @@ class FileTest extends TestCase
@unlink($targetPath);
@rmdir($targetDir);
}
protected function createMockGuesser($path, $mimeType)
{
$guesser = $this->getMockBuilder('Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface')->getMock();
$guesser
->expects($this->once())
->method('guess')
->with($this->equalTo($path))
->will($this->returnValue($mimeType))
;
return $guesser;
}
}

View File

@ -17,11 +17,10 @@ use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser;
/**
* @requires extension fileinfo
* @group legacy
*/
class MimeTypeTest extends TestCase
{
protected $path;
public function testGuessImageWithoutExtension()
{
$this->assertEquals('image/gif', MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/test'));

View File

@ -17,6 +17,7 @@
],
"require": {
"php": "^7.1.3",
"symfony/mime": "^4.3",
"symfony/polyfill-mbstring": "~1.1"
},
"require-dev": {

3
src/Symfony/Component/Mime/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
composer.lock
phpunit.xml
vendor/

View File

@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
/**
* Guesses the MIME type with the binary "file" (only available on *nix).
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
{
private $cmd;
/**
* The $cmd pattern must contain a "%s" string that will be replaced
* with the file name to guess.
*
* The command output must start with the MIME type of the file.
*
* @param string $cmd The command to run to get the MIME type of a file
*/
public function __construct(string $cmd = 'file -b --mime %s 2>/dev/null')
{
$this->cmd = $cmd;
}
/**
* {@inheritdoc}
*/
public function isGuesserSupported(): bool
{
static $supported = null;
if (null !== $supported) {
return $supported;
}
if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('passthru') || !\function_exists('escapeshellarg')) {
return $supported = false;
}
ob_start();
passthru('command -v file', $exitStatus);
$binPath = trim(ob_get_clean());
return $supported = 0 === $exitStatus && '' !== $binPath;
}
/**
* {@inheritdoc}
*/
public function guessMimeType(string $path): ?string
{
if (!is_file($path) || !is_readable($path)) {
throw new \InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path));
}
if (!$this->isGuesserSupported()) {
throw new \LogicException(sprintf('The "%s" guesser is not supported.', __CLASS__));
}
ob_start();
// need to use --mime instead of -i. see #6641
passthru(sprintf($this->cmd, escapeshellarg($path)), $return);
if ($return > 0) {
ob_end_clean();
return null;
}
$type = trim(ob_get_clean());
if (!preg_match('#^([a-z0-9\-]+/[a-z0-9\-\.]+)#i', $type, $match)) {
// it's not a type, but an error message
return null;
}
return $match[1];
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
/**
* Guesses the MIME type using the PECL extension FileInfo.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface
{
private $magicFile;
/**
* @param string $magicFile A magic file to use with the finfo instance
*
* @see http://www.php.net/manual/en/function.finfo-open.php
*/
public function __construct(string $magicFile = null)
{
$this->magicFile = $magicFile;
}
/**
* {@inheritdoc}
*/
public function isGuesserSupported(): bool
{
return \function_exists('finfo_open');
}
/**
* {@inheritdoc}
*/
public function guessMimeType(string $path): ?string
{
if (!is_file($path) || !is_readable($path)) {
throw new \InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path));
}
if (!$this->isGuesserSupported()) {
throw new \LogicException(sprintf('The "%s" guesser is not supported.', __CLASS__));
}
if (false === $finfo = new \finfo(FILEINFO_MIME_TYPE, $this->magicFile)) {
return null;
}
return $finfo->file($path);
}
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2010-2019 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
/**
* Guesses the MIME type of a file.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface MimeTypeGuesserInterface
{
/**
* Returns true if this guesser is supported.
*/
public function isGuesserSupported(): bool;
/**
* Guesses the MIME type of the file with the given path.
*
* @param string $path The path to the file
*
* @return string The MIME type or null, if none could be guessed
*
* @throws \LogicException If the guesser is not supported
* @throws \InvalidArgumentException If the file does not exist or is not readable
*/
public function guessMimeType(string $path): ?string;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface MimeTypesInterface extends MimeTypeGuesserInterface
{
/**
* Gets the extensions for the given MIME type.
*
* @return array an array of extensions (first one is the preferred one)
*/
public function getExtensions(string $mimeType): array;
/**
* Gets the MIME types for the given extension.
*
* @return array an array of MIME types (first one is the preferred one)
*/
public function getMimeTypes(string $ext): array;
}

View File

@ -0,0 +1,14 @@
MIME Component
==============
The MIME component allows manipulating MIME types.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/mime.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@ -0,0 +1,104 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
// load new map
$data = file_get_contents('https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types');
$new = [];
foreach (explode("\n", $data) as $line) {
if (!$line || '#' == $line[0]) {
continue;
}
$mimeType = substr($line, 0, strpos($line, "\t"));
$extensions = explode(' ', substr($line, strrpos($line, "\t") + 1));
$new[$mimeType] = $extensions;
}
$xml = simplexml_load_string(file_get_contents('https://raw.github.com/minad/mimemagic/master/script/freedesktop.org.xml'));
foreach ($xml as $node) {
$exts = [];
foreach ($node->glob as $glob) {
$pattern = (string) $glob['pattern'];
if ('*' != $pattern[0] || '.' != $pattern[1]) {
continue;
}
$exts[] = substr($pattern, 2);
}
if (!$exts) {
continue;
}
$mt = strtolower((string) $node['type']);
$new[$mt] = array_merge($new[$mt] ?? [], $exts);
foreach ($node->alias as $alias) {
$mt = strtolower((string) $alias['type']);
$new[$mt] = array_merge($new[$mt] ?? [], $exts);
}
}
// load current map
$data = file_get_contents($output = __DIR__.'/../../MimeTypes.php');
$current = [];
$pre = '';
$post = '';
foreach (explode("\n", $data) as $line) {
if (!preg_match("{^ '([^']+/[^']+)' => \['(.+)'\],$}", $line, $matches)) {
if (!$current) {
$pre .= $line."\n";
} else {
$post .= $line."\n";
}
continue;
}
$current[$matches[1]] = explode("', '", $matches[2]);
}
// we merge the 2 maps (we never remove old mime types)
$map = array_replace_recursive($current, $new);
ksort($map);
$data = $pre;
foreach ($map as $mimeType => $exts) {
$data .= sprintf(" '%s' => ['%s'],\n", $mimeType, implode("', '", array_unique($exts)));
}
$data .= $post;
// reverse map
$exts = [];
foreach ($map as $mimeType => $extensions) {
foreach ($extensions as $extension) {
$exts[$extension][] = $mimeType;
}
}
ksort($exts);
$updated = '';
$state = 0;
foreach (explode("\n", $data) as $line) {
if (!preg_match("{^ '([^'/]+)' => \['(.+)'\],$}", $line, $matches)) {
if (1 === $state) {
$state = 2;
foreach ($exts as $ext => $mimeTypes) {
$updated .= sprintf(" '%s' => ['%s'],\n", $ext, implode("', '", array_unique($mimeTypes)));
}
}
$updated .= $line."\n";
continue;
}
$state = 1;
}
$updated = preg_replace('{Updated from upstream on .+?\.}', 'Updated from upstream on '.date('Y-m-d'), $updated, -1);
file_put_contents($output, rtrim($updated, "\n")."\n");
echo "Done.\n";

View File

@ -0,0 +1,102 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
abstract class AbstractMimeTypeGuesserTest extends TestCase
{
public static function tearDownAfterClass()
{
$path = __DIR__.'/Fixtures/to_delete';
if (file_exists($path)) {
@chmod($path, 0666);
@unlink($path);
}
}
abstract protected function getGuesser(): MimeTypeGuesserInterface;
public function testGuessImageWithoutExtension()
{
if (!$this->getGuesser()->isGuesserSupported()) {
$this->markTestSkipped('Guesser is not supported');
}
$this->assertEquals('image/gif', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/test'));
}
public function testGuessImageWithDirectory()
{
if (!$this->getGuesser()->isGuesserSupported()) {
$this->markTestSkipped('Guesser is not supported');
}
$this->expectException('\InvalidArgumentException');
$this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/directory');
}
public function testGuessImageWithKnownExtension()
{
if (!$this->getGuesser()->isGuesserSupported()) {
$this->markTestSkipped('Guesser is not supported');
}
$this->assertEquals('image/gif', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/test.gif'));
}
public function testGuessFileWithUnknownExtension()
{
if (!$this->getGuesser()->isGuesserSupported()) {
$this->markTestSkipped('Guesser is not supported');
}
$this->assertEquals('application/octet-stream', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/.unknownextension'));
}
public function testGuessWithIncorrectPath()
{
if (!$this->getGuesser()->isGuesserSupported()) {
$this->markTestSkipped('Guesser is not supported');
}
$this->expectException('\InvalidArgumentException');
$this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/not_here');
}
public function testGuessWithNonReadablePath()
{
if (!$this->getGuesser()->isGuesserSupported()) {
$this->markTestSkipped('Guesser is not supported');
}
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Can not verify chmod operations on Windows');
}
if (!getenv('USER') || 'root' === getenv('USER')) {
$this->markTestSkipped('This test will fail if run under superuser');
}
$path = __DIR__.'/Fixtures/to_delete';
touch($path);
@chmod($path, 0333);
if ('0333' == substr(sprintf('%o', fileperms($path)), -4)) {
$this->expectException('\InvalidArgumentException');
$this->getGuesser()->guessMimeType($path);
} else {
$this->markTestSkipped('Can not verify chmod operations, change of file permissions failed');
}
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests;
use Symfony\Component\Mime\FileBinaryMimeTypeGuesser;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
class FileBinaryMimeTypeGuesserTest extends AbstractMimeTypeGuesserTest
{
protected function getGuesser(): MimeTypeGuesserInterface
{
return new FileBinaryMimeTypeGuesser();
}
}

View File

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests;
use Symfony\Component\Mime\FileinfoMimeTypeGuesser;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
/**
* @requires extension fileinfo
*/
class FileinfoMimeTypeGuesserTest extends AbstractMimeTypeGuesserTest
{
protected function getGuesser(): MimeTypeGuesserInterface
{
return new FileinfoMimeTypeGuesser();
}
}

View File

@ -0,0 +1 @@
f

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

View File

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypes;
/**
* @requires extension fileinfo
*/
class MimeTypesTest extends AbstractMimeTypeGuesserTest
{
protected function getGuesser(): MimeTypeGuesserInterface
{
return new MimeTypes();
}
public function testUnsupportedGuesser()
{
$guesser = $this->getGuesser();
$guesser->registerGuesser(new class() implements MimeTypeGuesserInterface {
public function isGuesserSupported(): bool
{
return false;
}
public function guessMimeType(string $mimeType): ?string
{
throw new \RuntimeException('Should never be called.');
}
});
$this->assertEquals('image/gif', $guesser->guessMimeType(__DIR__.'/Fixtures/test'));
}
public function testGetExtensions()
{
$mt = new MimeTypes();
$this->assertSame(['mbox'], $mt->getExtensions('application/mbox'));
$this->assertSame(['ai', 'eps', 'ps'], $mt->getExtensions('application/postscript'));
$this->assertSame([], $mt->getExtensions('application/whatever-symfony'));
}
public function testGetMimeTypes()
{
$mt = new MimeTypes();
$this->assertSame(['application/mbox'], $mt->getMimeTypes('mbox'));
$this->assertContains('application/postscript', $mt->getMimeTypes('ai'));
$this->assertContains('application/postscript', $mt->getMimeTypes('ps'));
$this->assertSame([], $mt->getMimeTypes('symfony'));
}
}

View File

@ -0,0 +1,33 @@
{
"name": "symfony/mime",
"type": "library",
"description": "A ",
"keywords": ["mime", "mime-type"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": "^7.1.3"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mime\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "4.3-dev"
}
}
}

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony MIME Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@ -13,7 +13,9 @@ namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface as DeprecatedMimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypes;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
@ -36,10 +38,22 @@ class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface, C
*/
private $mimeTypeGuesser;
public function __construct(MimeTypeGuesserInterface $mimeTypeGuesser = null)
/**
* @param MimeTypeGuesserInterface
*/
public function __construct($mimeTypeGuesser = null)
{
if (null === $mimeTypeGuesser && class_exists('Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser')) {
$mimeTypeGuesser = MimeTypeGuesser::getInstance();
if ($mimeTypeGuesser instanceof DeprecatedMimeTypeGuesserInterface) {
@trigger_error(sprintf('Passing a %s to "%s()" is deprecated since Symfony 4.3, pass a "%s" instead.', DeprecatedMimeTypeGuesserInterface::class, __METHOD__, MimeTypeGuesserInterface::class), E_USER_DEPRECATED);
} elseif (null === $mimeTypeGuesser) {
if (class_exists(MimeTypes::class)) {
$mimeTypeGuesser = MimeTypes::getDefault();
} else {
@trigger_error(sprintf('Passing null to "%s()" without symfony/mime installed is deprecated since Symfony 4.3, install symfony/mime.', __METHOD__), E_USER_DEPRECATED);
$mimeTypeGuesser = MimeTypeGuesser::getInstance();
}
} elseif (!$mimeTypeGuesser instanceof MimeTypes) {
throw new \InvalidArgumentException(sprintf('Argument 1 passed to "%s()" must be an instance of "%s" or null, %s given.', __METHOD__, MimeTypes::class, \is_object($mimeTypeGuesser) ? \get_class($mimeTypeGuesser) : \gettype($mimeTypeGuesser)));
}
$this->mimeTypeGuesser = $mimeTypeGuesser;
@ -140,7 +154,9 @@ class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface, C
return $object->getMimeType();
}
if ($this->mimeTypeGuesser && $mimeType = $this->mimeTypeGuesser->guess($object->getPathname())) {
if ($this->mimeTypeGuesser instanceof DeprecatedMimeTypeGuesserInterface && $mimeType = $this->mimeTypeGuesser->guess($object->getPathname())) {
return $mimeType;
} elseif ($this->mimeTypeGuesser && $mimeType = $this->mimeTypeGuesser->guessMimeType($object->getPathname())) {
return $mimeType;
}