├── .github ├── release-drafter.yml └── workflows │ ├── code-docs.yml │ ├── code-quality.yml │ └── php-unit.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── bin └── fileeye-mimemap ├── composer.json ├── examples └── readme_examples.php ├── phpcs.xml ├── phpstan.neon.dist ├── phpunit.xml ├── resources └── default_map_build.yml ├── src ├── Command │ └── UpdateCommand.php ├── Extension.php ├── ExtensionInterface.php ├── MalformedTypeException.php ├── Map │ ├── AbstractMap.php │ ├── BaseMap.php │ ├── DefaultMap.php │ ├── EmptyMap.php │ ├── MapInterface.php │ └── MimeMapInterface.php ├── MapHandler.php ├── MapUpdater.php ├── MappingException.php ├── SourceUpdateException.php ├── Type.php ├── TypeInterface.php ├── TypeParameter.php ├── TypeParser.php └── UndefinedException.php └── tests ├── fixtures ├── MiniMap.php.test ├── invalid.freedesktop.xml ├── min.freedesktop.xml ├── min.mime-types.txt ├── some.mime-types.txt ├── zero.freedesktop.xml └── zero.mime-types.txt └── src ├── ExtensionTest.php ├── MapHandlerTest.php ├── MapUpdaterTest.php ├── MimeMapTestBase.php ├── TypeParameterTest.php └── TypeTest.php /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What’s Changed 3 | 4 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/code-docs.yml: -------------------------------------------------------------------------------- 1 | name: Code documentation 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | # Allow running the workflow manually from the Actions tab 8 | #workflow_dispatch: 9 | 10 | # Allow GITHUB_TOKEN to deploy to GitHub Pages 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | # Allow one concurrent deployment 17 | concurrency: 18 | group: pages 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | - name: Configure GitHub Pages 28 | uses: actions/configure-pages@v5 29 | - name: Cache phpDocumentor build files 30 | id: phpdocumentor-cache 31 | uses: actions/cache@v3 32 | with: 33 | path: .phpdoc/cache 34 | key: ${{ runner.os }}-phpdocumentor-${{ github.sha }} 35 | restore-keys: | 36 | ${{ runner.os }}-phpdocumentor- 37 | - name: Build with phpDocumentor 38 | run: docker run --rm --volume "$(pwd):/data" phpdoc/phpdoc:3 -vv -d src --target docs --cache-folder .phpdoc/cache --template default --title MimeMap 39 | - name: Upload artifact to GitHub Pages 40 | uses: actions/upload-pages-artifact@v3 41 | with: 42 | path: docs 43 | 44 | deploy: 45 | needs: build 46 | environment: 47 | name: github-pages 48 | url: ${{ steps.deployment.outputs.page_url }} 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.github/workflows/code-quality.yml: -------------------------------------------------------------------------------- 1 | name: Code quality 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | static-analysis: 12 | name: "Code quality checks" 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | php-version: 18 | - "8.1" 19 | 20 | steps: 21 | - name: "Checkout code" 22 | uses: "actions/checkout@v4" 23 | 24 | - name: "Install PHP" 25 | uses: "shivammathur/setup-php@v2" 26 | with: 27 | coverage: "none" 28 | php-version: "${{ matrix.php-version }}" 29 | tools: "cs2pr" 30 | 31 | - name: "Install dependencies with Composer" 32 | uses: "ramsey/composer-install@v2" 33 | 34 | - name: "Require tools" 35 | continue-on-error: true 36 | run: | 37 | composer config --no-plugins allow-plugins.phpstan/extension-installer true 38 | composer require --ansi --dev "phpstan/phpstan:>=2" "phpstan/extension-installer:>=1.4" "phpstan/phpstan-phpunit:>=1.4" "squizlabs/php_codesniffer:>=3.7" "phpunit/phpunit:>=10" 39 | 40 | - name: "Run static analysis with phpstan/phpstan" 41 | run: "vendor/bin/phpstan" 42 | 43 | - name: "Run code style check with squizlabs/php_codesniffer" 44 | run: ./vendor/bin/phpcs --runtime-set ignore_warnings_on_exit 1 45 | -------------------------------------------------------------------------------- /.github/workflows/php-unit.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | schedule: 7 | - cron: "0 6 * * 3" 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | phpunit: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - php-version: "8.1" 21 | php-unit-args: "-c phpunit.xml" 22 | - php-version: "8.2" 23 | php-unit-args: "-c phpunit.xml" 24 | - php-version: "8.3" 25 | php-unit-args: "-c phpunit.xml" 26 | - php-version: "8.4" 27 | php-unit-args: "-c phpunit.xml" 28 | - php-version: "8.5" 29 | php-unit-args: "-c phpunit.xml --fail-on-deprecation --fail-on-phpunit-deprecation" 30 | 31 | steps: 32 | 33 | - uses: actions/checkout@v4 34 | 35 | - name: Install PHP 36 | uses: "shivammathur/setup-php@v2" 37 | with: 38 | php-version: "${{ matrix.php-version }}" 39 | coverage: "pcov" 40 | ini-values: "zend.assertions=1" 41 | 42 | - name: Install Composer dependencies 43 | run: composer install --no-progress --ansi 44 | 45 | - name: "Require tools" 46 | continue-on-error: true 47 | run: composer require --ansi --dev "phpunit/phpunit:>=10" 48 | 49 | - name: Run tests 50 | run: ./vendor/bin/phpunit --color=always --testdox --coverage-clover=coverage.xml ${{ matrix.php-unit-args }} 51 | 52 | - name: Send code coverage report to Codecov.io 53 | uses: codecov/codecov-action@v4 54 | with: 55 | token: ${{ secrets.CODECOV_TOKEN }} 56 | files: coverage.xml 57 | 58 | - name: Mapping test 59 | id: mapTest 60 | continue-on-error: true 61 | if: ${{ matrix.php-version == 8.1 }} 62 | run: | 63 | php ./bin/fileeye-mimemap --version 64 | php ./bin/fileeye-mimemap update --diff --fail-on-diff --ansi 65 | 66 | - name: Map update 67 | id: mapUpdate 68 | if: ${{ steps.mapTest.outcome == 'failure' }} 69 | run: php ./bin/fileeye-mimemap update --ansi 70 | 71 | - name: Map artifact 72 | if: ${{ steps.mapTest.outcome == 'failure' && steps.mapUpdate.outcome == 'success' }} 73 | uses: actions/upload-artifact@v4 74 | with: 75 | name: map 76 | path: src/Map/DefaultMap.php 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | .phpunit.cache -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MimeMap 2 | 3 | [![Tests](https://github.com/FileEye/MimeMap/actions/workflows/php-unit.yml/badge.svg)](https://github.com/FileEye/MimeMap/actions/workflows/php-unit.yml) 4 | [![PHPStan level](https://img.shields.io/badge/PHPStan%20level-max-brightgreen.svg?style=flat)](https://github.com/FileEye/MimeMap/actions/workflows/code-quality.yml) 5 | [![codecov](https://codecov.io/gh/FileEye/MimeMap/branch/master/graph/badge.svg?token=SUAMNKZLEW)](https://codecov.io/gh/FileEye/MimeMap) 6 | [![Latest Stable Version](https://poser.pugx.org/fileeye/mimemap/v/stable)](https://packagist.org/packages/fileeye/mimemap) 7 | [![Total Downloads](https://poser.pugx.org/fileeye/mimemap/downloads)](https://packagist.org/packages/fileeye/mimemap) 8 | [![License](https://poser.pugx.org/fileeye/mimemap/license)](https://packagist.org/packages/fileeye/mimemap) 9 | 10 | A PHP library to handle MIME Content-Type fields and their related file 11 | extensions. 12 | 13 | 14 | ## Features 15 | 16 | - Parses MIME Content-Type fields 17 | - Supports the [RFC 2045](https://www.ietf.org/rfc/rfc2045.txt) specification 18 | - Provides utility functions for working with and determining info about MIME 19 | types 20 | - Map file extensions to MIME types and vice-versa 21 | - Automatically update the mapping between MIME types and file extensions from 22 | the most authoritative sources available, [Apache's documentation](http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types?view=log) 23 | and the [freedesktop.org project](http://freedesktop.org). 24 | - PHPUnit tested, 100% test coverage 25 | - PHPStan tested, level 10 26 | 27 | 28 | ## Credits 29 | 30 | MimeMap is a fork of PEAR's [MIME_Type](https://github.com/pear/MIME_Type) package. 31 | See all the [original contributors](https://github.com/pear/MIME_Type/graphs/contributors). 32 | 33 | Note that in comparison with PEAR's MIME_Type, this library has a different 34 | scope, mainly focused on finding the mapping between each MIME type and its 35 | generally accepted file extensions. 36 | Features to detect the MIME type of a file have been removed. The [symfony/http-foundation](https://github.com/symfony/http-foundation) 37 | library and its [MimeTypeGuesser](https://api.symfony.com/master/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesser.html) 38 | API are the suggested components to cover that use case. 39 | 40 | 41 | ### Alternative packages 42 | 43 | MimeMap's main difference from similar packages is that it provides 44 | functionalities to use multiple type-to-extension maps and to change the 45 | mapping either at runtime or statically in PHP classes. 46 | See [wgenial/php-mimetyper](https://github.com/wgenial/php-mimetyper#other-php-libraries-for-mime-types) 47 | for a nice list of alternative PHP libraries for MIME type handling. 48 | 49 | 50 | ## Installation 51 | 52 | ``` 53 | $ composer require fileeye/mimemap 54 | ``` 55 | 56 | 57 | ## Usage 58 | 59 | See latest documentation [here](https://fileeye.github.io/MimeMap/), automated with phpDocumentor. 60 | 61 | ### Basic 62 | 63 | The package comes with a default map that describes MIME types and the file 64 | extensions normally associated to each MIME type. 65 | The map also stores information about MIME type _aliases_, (alternative 66 | _media/subtype_ combinations that describe the same MIME type), and the 67 | descriptions of most MIME types and of the acronyms used. 68 | 69 | For example: the MIME type _'application/pdf'_ 70 | * is described as _'PDF document'_ 71 | * the PDF acronym is described as _'PDF: Portable Document Format'_ 72 | * is normally using a file extension _'pdf'_ 73 | * has aliases such as _'application/x-pdf'_, _'image/pdf'_ 74 | 75 | The API the package implements is pretty straightforward: 76 | 77 | 78 | 1. You have a MIME type, and want to get the file extensions normally associated 79 | to it: 80 | 81 | ```php 82 | use FileEye\MimeMap\Type; 83 | ... 84 | $type = new Type('image/jpeg'); 85 | 86 | print_r($type->getExtensions()); 87 | // will print ['jpeg', 'jpg', 'jpe'] 88 | 89 | print_r($type->getDefaultExtension()); 90 | // will print 'jpeg' 91 | 92 | // When passing an alias to a MIME type, the API will 93 | // return the extensions to the parent type: 94 | $type = new Type('image/pdf'); 95 | 96 | print_r($type->getDefaultExtension()); 97 | // will print 'pdf' which is the default extension for 'application/pdf' 98 | ``` 99 | 100 | 2. Viceversa, you have a file extensions, and want to get the MIME type normally 101 | associated to it: 102 | 103 | ```php 104 | use FileEye\MimeMap\Extension; 105 | ... 106 | $ext = new Extension('xar'); 107 | 108 | print_r($ext->getTypes()); 109 | // will return ['application/vnd.xara', 'application/x-xar'] 110 | 111 | print_r($ext->getDefaultType()); 112 | // will return 'application/vnd.xara' 113 | ``` 114 | 115 | 3. You have a raw MIME Content-Type string and want to add a parameter: 116 | 117 | ```php 118 | use FileEye\MimeMap\Type; 119 | ... 120 | $type = new Type('text / (Unstructured text) plain ; charset = (UTF8, not ASCII) utf-8'); 121 | $type->addParameter('lang', 'it', 'Italian'); 122 | 123 | echo $type->toString(Type::SHORT_TEXT); 124 | // will print 'text/plain' 125 | 126 | echo $type->toString(Type::FULL_TEXT); 127 | // will print 'text/plain; charset="utf-8"; lang="it"' 128 | 129 | echo $type->toString(Type::FULL_TEXT_WITH_COMMENTS); 130 | // will print 'text/plain (Unstructured text); charset="utf-8" (UTF8, not ASCII), lang="it" (Italian)' 131 | ``` 132 | 133 | 4. You have a MIME Content-Type string and want to add the type's description as a comment: 134 | 135 | ```php 136 | use FileEye\MimeMap\Type; 137 | ... 138 | $type = new Type('text/html'); 139 | 140 | $type_desc = $type->getDescription(); 141 | $type->setSubTypeComment($type_desc); 142 | echo $type->toString(Type::FULL_TEXT_WITH_COMMENTS); 143 | // will print 'text/html (HTML document)' 144 | 145 | // Setting the $include_acronym parameter of getDescription to true 146 | // will extend the description to include the meaning of the acronym 147 | $type_desc = $type->getDescription(true); 148 | $type->setSubTypeComment($type_desc); 149 | echo $type->toString(Type::FULL_TEXT_WITH_COMMENTS); 150 | // will print 'text/html (HTML document, HTML: HyperText Markup Language)' 151 | ``` 152 | 153 | 154 | ### Specify alternative MIME type mapping 155 | 156 | 157 | You can also alter the default map at runtime, either by adding/removing 158 | mappings, or indicating to MimeMap to use a totally different map. The 159 | alternative map must be stored in a PHP class that extends from 160 | `\FileEye\MimeMap\Map\AbstractMap`. 161 | 162 | 1. You want to add an additional MIME type to extension mapping to the 163 | default class: 164 | 165 | ```php 166 | use FileEye\MimeMap\Extension; 167 | use FileEye\MimeMap\MapHandler; 168 | use FileEye\MimeMap\Type; 169 | ... 170 | $map = MapHandler::map(); 171 | $map->addTypeExtensionMapping('foo/bar', 'baz'); 172 | 173 | $type = new Type('foo/bar'); 174 | $default_extension = $type->getDefaultExtension(); 175 | // will return 'baz' 176 | 177 | $ext = new Extension('baz'); 178 | $default_type = $ext->getDefaultExtension(); 179 | // will return 'foo/bar' 180 | ``` 181 | 182 | 2. You want to set an alternative map class as default: 183 | 184 | ```php 185 | use FileEye\MimeMap\Extension; 186 | use FileEye\MimeMap\MapHandler; 187 | use FileEye\MimeMap\Type; 188 | ... 189 | MapHandler::setDefaultMapClass('MyProject\MyMap'); 190 | ... 191 | ``` 192 | 193 | 3. You can also use the alternative map just for a single Type or Extension 194 | object: 195 | 196 | ```php 197 | use FileEye\MimeMap\Extension; 198 | use FileEye\MimeMap\Type; 199 | ... 200 | $type = new Type('foo/bar', 'MyProject\MyMap'); 201 | $ext = new Extension('baz', 'MyProject\MyMap'); 202 | ``` 203 | 204 | 205 | ## Development 206 | 207 | 208 | ### Updating the extension mapping code 209 | 210 | The default extension-to-type mapping class can be updated from the sources' 211 | code repositories, using the `fileeye-mimemap` utility: 212 | 213 | ``` 214 | $ cd [project_directory]/vendor/bin 215 | $ fileeye-mimemap update 216 | ``` 217 | 218 | By default, the utility fetches a mapping source available from the [Apache's documentation](http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types?view=co) 219 | website, merges it with another mapping source from the [freedesktop.org project](https://gitlab.freedesktop.org/xdg/shared-mime-info/-/blob/master/data/freedesktop.org.xml.in), 220 | then integrates the result with any overrides specified in the 221 | `resources/default_map_build.yml` file, and finally updates the PHP file where 222 | the `\FileEye\MimeMap\Map\DefaultMap` class is stored. 223 | 224 | The `--script` and `--class` options allow specifying a different update logic 225 | and a different class file to update. Type 226 | ``` 227 | $ fileeye-mimemap update --help 228 | ``` 229 | to get more information. 230 | -------------------------------------------------------------------------------- /bin/fileeye-mimemap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new UpdateCommand()); 25 | $application->run(); 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fileeye/mimemap", 3 | "type": "library", 4 | "description": "A PHP library to handle MIME Content-Type fields and their related file extensions.", 5 | "keywords": ["mime", "mime-type", "mime-database", "mime-parser"], 6 | "homepage": "https://github.com/FileEye/MimeMap", 7 | "license": "LGPL-3.0-or-later", 8 | "require": { 9 | "php": ">=8.1" 10 | }, 11 | "require-dev": { 12 | "composer-runtime-api": "^2.0.0", 13 | "sebastian/comparator": ">=5", 14 | "sebastian/diff": ">=5", 15 | "symfony/filesystem": ">=6.4", 16 | "symfony/console": ">=6.4", 17 | "symfony/var-dumper": ">=6.4", 18 | "symfony/yaml": ">=6.4" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "FileEye\\MimeMap\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "FileEye\\MimeMap\\Test\\": "tests/src/" 28 | } 29 | }, 30 | "bin": ["bin/fileeye-mimemap"], 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "2.x-dev" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/readme_examples.php: -------------------------------------------------------------------------------- 1 | getExtensions()); 16 | // will print ['jpeg', 'jpg', 'jpe'] 17 | 18 | print_r($type->getDefaultExtension()); 19 | // will print 'jpeg' 20 | 21 | // When passing an alias to a MIME type, the API will 22 | // return the extensions to the parent type: 23 | $type = new Type('image/pdf'); 24 | 25 | print_r($type->getDefaultExtension()); 26 | // will print 'pdf' which is the default extension for 'application/pdf' 27 | 28 | // ------------------- 29 | 30 | $ext = new Extension('xar'); 31 | 32 | print_r($ext->getTypes()); 33 | // will return ['application/vnd.xara', 'application/x-xar'] 34 | 35 | print_r($ext->getDefaultType()); 36 | // will return 'application/vnd.xara' 37 | 38 | // ------------------- 39 | 40 | $type = new Type('text / (Unstructured text) plain ; charset = (UTF8, not ASCII) utf-8'); 41 | $type->addParameter('lang', 'it', 'Italian'); 42 | 43 | echo $type->toString(Type::SHORT_TEXT); 44 | // will print 'text/plain' 45 | 46 | echo $type->toString(Type::FULL_TEXT); 47 | // will print 'text/plain; charset="utf-8"; lang="it"' 48 | 49 | echo $type->toString(Type::FULL_TEXT_WITH_COMMENTS); 50 | // will print 'text/plain (Unstructured text); charset="utf-8" (UTF8, not ASCII), lang="it" (Italian)' 51 | 52 | // ------------------- 53 | 54 | $type = new Type('text/html'); 55 | 56 | $type_desc = $type->getDescription(); 57 | $type->setSubTypeComment($type_desc); 58 | echo $type->toString(Type::FULL_TEXT_WITH_COMMENTS); 59 | // will print 'text/html (HTML document)' 60 | 61 | // Setting the $include_acronym parameter of getDescription to true 62 | // will extend the description to include the meaning of the acronym 63 | $type_desc = $type->getDescription(true); 64 | $type->setSubTypeComment($type_desc); 65 | echo $type->toString(Type::FULL_TEXT_WITH_COMMENTS); 66 | // will print 'text/html (HTML document, HTML: HyperText Markup Language)' 67 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | src 13 | tests 14 | 15 | 16 | 17 | 18 | 19 | 0 20 | 21 | 22 | 23 | error 24 | 25 | 26 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | 4 | paths: 5 | - src 6 | - tests 7 | 8 | typeAliases: 9 | GenericMap: """ 10 | array,string>>>> 11 | """ 12 | MimeMap: """ 13 | array{ 14 | 't': array<'e'|'desc'|'a',array,string>>>, 15 | 'e': array<'t',array,string>>>, 16 | 'a': array<'t',array,string>>> 17 | } 18 | """ 19 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | 16 | ./src/ 17 | 18 | 19 | ./src/Command/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /resources/default_map_build.yml: -------------------------------------------------------------------------------- 1 | # MimeMap 2 | # 3 | # This file provides the default script to build the MIME-type to file extension 4 | # map. It is used as input to the 'fileeye-mimemap' utility. 5 | # 6 | # The script fetches a mapping source available from the Apache's documentation 7 | # website, merges it with another mapping source from the freedesktop.org 8 | # project, integrates the result with any overrides specified by 9 | # 'applyOverrides', and finally updates the PHP file where the 10 | # '\FileEye\MimeMap\Map\DefaultMap' class is stored. 11 | # 12 | # The entries are executed sequentially; each entry indicates a MapUpdater 13 | # method to be invoked and the arguments to be passed in. 14 | 15 | 16 | # The Apache httpd project contains the most complete list of file extension to 17 | # mime type mapping on the planet. We use it to update our own list. 18 | # Alternative URLs: 19 | # http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types?view=co 20 | # (no longer working, ViewC subversion was disabled Feb 2025, status @ https://status.apache.org/) 21 | # https://raw.githubusercontent.com/apache/httpd/refs/heads/trunk/docs/conf/mime.types 22 | # (GitHub mirror) 23 | - 24 | - 'Loading MIME type information from svn.apache.org' 25 | - loadMapFromApacheFile 26 | - [https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types] 27 | 28 | # Extend Apache table with the Freedesktop.org database. 29 | - 30 | - 'Removing redundant svn.apache.org types' 31 | - applyOverrides 32 | - 33 | - 34 | - [removeType, [application/x-msaccess]] 35 | - [removeType, [application/vnd.ms-xpsdocument]] 36 | - [removeType, [application/vnd.stardivision.writer-global]] 37 | - [removeType, [application/x-bzip2]] 38 | - [removeType, [application/x-cbr]] 39 | - [removeType, [application/x-iso9660-image]] 40 | - [removeType, [application/x-chess-pgn]] 41 | - [removeType, [application/x-debian-package]] 42 | - [removeType, [application/java-archive]] 43 | - [removeType, [application/java-vm]] 44 | - [removeType, [application/x-lzh-compressed]] 45 | - [removeType, [application/x-pkcs12]] 46 | - [removeType, [application/x-rar-compressed]] 47 | - [removeType, [application/x-shockwave-flash]] 48 | - [removeType, [application/vnd.smaf]] 49 | - [removeType, [application/x-gtar]] 50 | - [removeType, [audio/x-flac]] 51 | - [removeType, [audio/x-aac]] 52 | - [removeType, [video/x-m4v]] 53 | - [removeType, [video/x-ms-wvx]] 54 | - [removeType, [video/x-ms-wmx]] 55 | - [removeType, [audio/x-pn-realaudio]] 56 | - [removeType, [application/vnd.rn-realmedia-vbr]] 57 | - [removeType, [application/docbook+xml]] 58 | - [removeType, [image/g3fax]] 59 | - [removeType, [image/x-icon]] 60 | - [removeType, [image/x-pcx]] 61 | - [removeType, [application/x-msmetafile]] 62 | - [removeType, [text/x-vcalendar]] 63 | - [removeType, [text/x-vcard]] 64 | - [removeType, [text/x-opml]] 65 | - [removeType, [text/x-c]] 66 | - [removeType, [application/x-tex]] 67 | - [removeType, [video/x-fli]] 68 | - [removeType, [video/x-ms-wm]] 69 | - [removeType, [video/x-ms-asf]] 70 | - [removeType, [audio/x-wav]] 71 | - [removeType, [video/x-msvideo]] 72 | - [removeType, [image/vnd.ms-photo]] 73 | - [removeType, [application/x-bzip]] 74 | - [removeType, [application/vnd.oasis.opendocument.database]] 75 | 76 | - 77 | - 'Updating with information from freedesktop.org' 78 | - loadMapFromFreedesktopFile 79 | - [https://gitlab.freedesktop.org/xdg/shared-mime-info/raw/master/data/freedesktop.org.xml.in] 80 | 81 | - 82 | - 'Cleanup video/x-anim' 83 | - applyOverrides 84 | - 85 | - 86 | - [removeTypeExtensionMapping, [application/x-sharedlib, "so.[0-9]*"]] 87 | - [removeTypeExtensionMapping, [application/x-troff-man, "[1-9]"]] 88 | - [removeTypeExtensionMapping, [video/x-anim, "anim[1-9j]"]] 89 | - [addTypeExtensionMapping, [video/x-anim, anim1]] 90 | - [addTypeExtensionMapping, [video/x-anim, anim2]] 91 | - [addTypeExtensionMapping, [video/x-anim, anim3]] 92 | - [addTypeExtensionMapping, [video/x-anim, anim4]] 93 | - [addTypeExtensionMapping, [video/x-anim, anim5]] 94 | - [addTypeExtensionMapping, [video/x-anim, anim6]] 95 | - [addTypeExtensionMapping, [video/x-anim, anim7]] 96 | - [addTypeExtensionMapping, [video/x-anim, anim8]] 97 | - [addTypeExtensionMapping, [video/x-anim, anim9]] 98 | - [addTypeExtensionMapping, [video/x-anim, animj]] 99 | 100 | - 101 | - 'Adding back selected svn.apache.org mappings' 102 | - applyOverrides 103 | - 104 | - 105 | - [addTypeExtensionMapping, [application/x-bzip2, boz]] 106 | - [addTypeExtensionMapping, [application/vnd.comicbook-rar, cba]] 107 | - [addTypeExtensionMapping, [text/x-csrc, dic]] 108 | - [addTypeExtensionMapping, [image/wmf, emz]] 109 | - [addTypeExtensionMapping, [application/vnd.ms-asf, wm]] 110 | 111 | # MimeMap overrides. 112 | - 113 | - 'Applying MimeMap overrides' 114 | - applyOverrides 115 | - 116 | - 117 | - [setExtensionDefaultType, [sub, text/vnd.dvb.subtitle]] 118 | - [setExtensionDefaultType, [md, text/markdown]] 119 | -------------------------------------------------------------------------------- /src/Command/UpdateCommand.php: -------------------------------------------------------------------------------- 1 | setName('update') 31 | ->setDescription('Updates the MIME-type-to-extension map. Executes the commands in the script file specified by --script, then writes the map to the PHP file where the PHP --class is defined.') 32 | ->addOption( 33 | 'script', 34 | null, 35 | InputOption::VALUE_REQUIRED, 36 | 'File name of the script containing the sequence of commands to execute to build the default map.', 37 | MapUpdater::getDefaultMapBuildFile(), 38 | ) 39 | ->addOption( 40 | 'class', 41 | null, 42 | InputOption::VALUE_REQUIRED, 43 | 'The fully qualified class name of the PHP class storing the map.', 44 | MapHandler::DEFAULT_MAP_CLASS, 45 | ) 46 | ->addOption( 47 | 'diff', 48 | null, 49 | InputOption::VALUE_NONE, 50 | 'Report updates.', 51 | ) 52 | ->addOption( 53 | 'fail-on-diff', 54 | null, 55 | InputOption::VALUE_NONE, 56 | 'Exit with an error when a difference is found. Map will not be updated.', 57 | ) 58 | ; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | protected function execute(InputInterface $input, OutputInterface $output): int 65 | { 66 | $io = new SymfonyStyle($input, $output); 67 | 68 | $updater = new MapUpdater(); 69 | $updater->selectBaseMap(MapUpdater::DEFAULT_BASE_MAP_CLASS); 70 | 71 | $scriptFile = $input->getOption('script'); 72 | if (!is_string($scriptFile)) { 73 | $io->error('Invalid value for --script option.'); 74 | return (2); 75 | } 76 | 77 | $mapClass = $input->getOption('class'); 78 | if (!is_string($mapClass)) { 79 | $io->error('Invalid value for --class option.'); 80 | return (2); 81 | } 82 | 83 | $diff = $input->getOption('diff'); 84 | assert(is_bool($diff)); 85 | $failOnDiff = $input->getOption('fail-on-diff'); 86 | assert(is_bool($failOnDiff)); 87 | 88 | // Executes on the base map the script commands. 89 | $contents = file_get_contents($scriptFile); 90 | if ($contents === false) { 91 | $io->error('Failed loading update script file ' . $scriptFile); 92 | return (2); 93 | } 94 | 95 | $commands = Yaml::parse($contents); 96 | if (!is_array($commands)) { 97 | $io->error('Invalid update script file ' . $scriptFile); 98 | return (2); 99 | } 100 | 101 | /** @var list}> $commands */ 102 | foreach ($commands as $command) { 103 | $output->writeln("{$command[0]} ..."); 104 | try { 105 | $callable = [$updater, $command[1]]; 106 | assert(is_callable($callable)); 107 | $errors = call_user_func_array($callable, $command[2]); 108 | if (is_array($errors) && !empty($errors)) { 109 | /** @var list $errors */ 110 | foreach ($errors as $error) { 111 | $output->writeln("$error."); 112 | } 113 | } 114 | } catch (\Exception $e) { 115 | $io->error($e->getMessage()); 116 | return(1); 117 | } 118 | } 119 | 120 | // Load the map to be changed. 121 | /** @var class-string $mapClass */ 122 | MapHandler::setDefaultMapClass($mapClass); 123 | $current_map = MapHandler::map(); 124 | 125 | // Check if anything got changed. 126 | $write = true; 127 | if ($diff) { 128 | $write = false; 129 | foreach ([ 130 | 't' => 'MIME types', 131 | 'a' => 'MIME type aliases', 132 | 'e' => 'extensions', 133 | ] as $key => $desc) { 134 | try { 135 | $output->writeln("Checking changes to {$desc} ..."); 136 | $this->compareMaps($current_map, $updater->getMap(), $key); 137 | } catch (\RuntimeException $e) { 138 | $output->writeln("Changes to {$desc} mapping:"); 139 | $output->writeln($e->getMessage()); 140 | $write = true; 141 | } 142 | } 143 | } 144 | 145 | // Fail on diff if required. 146 | if ($write && $diff && $failOnDiff) { 147 | $io->error('Changes to mapping detected and --fail-on-diff requested, aborting.'); 148 | return(2); 149 | } 150 | 151 | // If changed, save the new map to the PHP file. 152 | if ($write) { 153 | try { 154 | $updater->writeMapToPhpClassFile($current_map->getFileName()); 155 | $output->writeln('Code updated.'); 156 | } catch (\RuntimeException $e) { 157 | $io->error($e->getMessage() . '.'); 158 | return(2); 159 | } 160 | } else { 161 | $output->writeln('No changes to mapping.'); 162 | } 163 | 164 | // Reset the new map's map array. 165 | $updater->getMap()->reset(); 166 | 167 | return(0); 168 | } 169 | 170 | /** 171 | * Compares two type-to-extension maps by section. 172 | * 173 | * @param MimeMapInterface $old_map 174 | * The first map to compare. 175 | * @param MimeMapInterface $new_map 176 | * The second map to compare. 177 | * @param string $section 178 | * The first-level array key to compare: 't' or 'e' or 'a'. 179 | * 180 | * @throws \RuntimeException with diff details if the maps differ. 181 | * 182 | * @return bool 183 | * True if the maps are equal. 184 | */ 185 | protected function compareMaps(MimeMapInterface $old_map, MimeMapInterface $new_map, string $section): bool 186 | { 187 | $old_map->sort(); 188 | $new_map->sort(); 189 | $old = $old_map->getMapArray(); 190 | $new = $new_map->getMapArray(); 191 | 192 | $factory = new Factory; 193 | $comparator = $factory->getComparatorFor($old[$section], $new[$section]); 194 | try { 195 | $comparator->assertEquals($old[$section], $new[$section]); 196 | return true; 197 | } catch (ComparisonFailure $failure) { 198 | $old_string = var_export($old[$section], true); 199 | $new_string = var_export($new[$section], true); 200 | if (class_exists('\SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder')) { 201 | $differ = new Differ(new UnifiedDiffOutputBuilder("--- Removed\n+++ Added\n")); 202 | throw new \RuntimeException($differ->diff($old_string, $new_string)); 203 | } else { 204 | throw new \RuntimeException(' '); 205 | } 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Extension.php: -------------------------------------------------------------------------------- 1 | extension = strtolower($extension); 25 | $this->map = MapHandler::map($mapClass); 26 | } 27 | 28 | public function getDefaultType(): string 29 | { 30 | return $this->getTypes()[0]; 31 | } 32 | 33 | public function getTypes(): array 34 | { 35 | $types = $this->map->getExtensionTypes($this->extension); 36 | if (!empty($types)) { 37 | return $types; 38 | } 39 | throw new MappingException('No MIME type mapped to extension ' . $this->extension); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ExtensionInterface.php: -------------------------------------------------------------------------------- 1 | |null $mapClass 18 | * (Optional) The FQCN of the map class to use. 19 | * 20 | * @api 21 | */ 22 | public function __construct(string $extension, ?string $mapClass = null); 23 | 24 | /** 25 | * Returns the file extension's preferred MIME type. 26 | * 27 | * @throws MappingException if no mapping found. 28 | * 29 | * @api 30 | */ 31 | public function getDefaultType(): string; 32 | 33 | /** 34 | * Returns all the MIME types related to the file extension. 35 | * 36 | * @throws MappingException if no mapping found. 37 | * 38 | * @return list 39 | * 40 | * @api 41 | */ 42 | public function getTypes(): array; 43 | } 44 | -------------------------------------------------------------------------------- /src/MalformedTypeException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | abstract class AbstractMap extends BaseMap implements MimeMapInterface 17 | { 18 | public static function getInstance(): MimeMapInterface 19 | { 20 | $instance = parent::getInstance(); 21 | assert($instance instanceof MimeMapInterface); 22 | return $instance; 23 | } 24 | 25 | /** 26 | * Normalizes a mime-type string to Media/Subtype. 27 | * 28 | * @param string $typeString 29 | * MIME type string to parse. 30 | * 31 | * @throws MalformedTypeException when $typeString is malformed. 32 | * 33 | * @return string 34 | * A MIME type string in the 'Media/Subtype' format. 35 | */ 36 | protected function normalizeType(string $typeString): string 37 | { 38 | // Media and SubType are separated by a slash '/'. 39 | $media = TypeParser::parseStringPart($typeString, 0, '/'); 40 | 41 | if (!$media['string']) { 42 | throw new MalformedTypeException('Media type not found'); 43 | } 44 | if (!$media['delimiter_matched']) { 45 | throw new MalformedTypeException('Slash \'/\' to separate media type and subtype not found'); 46 | } 47 | 48 | // SubType and Parameters are separated by semicolons ';'. 49 | $sub = TypeParser::parseStringPart($typeString, $media['end_offset'] + 1, ';'); 50 | 51 | if (!$sub['string']) { 52 | throw new MalformedTypeException('Media subtype not found'); 53 | } 54 | 55 | return strtolower($media['string']) . '/' . strtolower($sub['string']); 56 | } 57 | 58 | public function hasType(string $type): bool 59 | { 60 | $type = $this->normalizeType($type); 61 | return (bool) $this->getMapEntry('t', $type); 62 | } 63 | 64 | public function hasAlias(string $alias): bool 65 | { 66 | $alias = $this->normalizeType($alias); 67 | return (bool) $this->getMapEntry('a', $alias); 68 | } 69 | 70 | public function hasExtension(string $extension): bool 71 | { 72 | $extension = strtolower($extension); 73 | return (bool) $this->getMapEntry('e', $extension); 74 | } 75 | 76 | public function listTypes(?string $match = null): array 77 | { 78 | return array_map('strval', $this->listEntries('t', $match)); 79 | } 80 | 81 | public function listAliases(?string $match = null): array 82 | { 83 | return array_map('strval', $this->listEntries('a', $match)); 84 | } 85 | 86 | public function listExtensions(?string $match = null): array 87 | { 88 | return array_map('strval', $this->listEntries('e', $match)); 89 | } 90 | 91 | public function addTypeDescription(string $type, string $description): MimeMapInterface 92 | { 93 | $type = $this->normalizeType($type); 94 | 95 | // Consistency checks. 96 | if ($this->hasAlias($type)) { 97 | throw new MappingException("Cannot add description for '{$type}', '{$type}' is an alias"); 98 | } 99 | 100 | $this->addMapSubEntry('t', $type, 'desc', $description); 101 | return $this; 102 | } 103 | 104 | public function addTypeAlias(string $type, string $alias): MimeMapInterface 105 | { 106 | $type = $this->normalizeType($type); 107 | $alias = $this->normalizeType($alias); 108 | 109 | // Consistency checks. 110 | if (!$this->hasType($type)) { 111 | throw new MappingException("Cannot set '{$alias}' as alias for '{$type}', '{$type}' not defined"); 112 | } 113 | if ($this->hasType($alias)) { 114 | throw new MappingException("Cannot set '{$alias}' as alias for '{$type}', '{$alias}' is already defined as a type"); 115 | } 116 | if ($this->hasAlias($alias)) { 117 | $unaliased_types = $this->getAliasTypes($alias); 118 | if (!empty($unaliased_types) && $unaliased_types[0] !== $type) { 119 | throw new MappingException("Cannot set '{$alias}' as alias for '{$type}', it is an alias of '{$unaliased_types[0]}' already"); 120 | } 121 | return $this; 122 | } 123 | 124 | $this->addMapSubEntry('t', $type, 'a', $alias); 125 | $this->addMapSubEntry('a', $alias, 't', $type); 126 | return $this; 127 | } 128 | 129 | public function addTypeExtensionMapping(string $type, string $extension): MimeMapInterface 130 | { 131 | $type = $this->normalizeType($type); 132 | $extension = strtolower($extension); 133 | 134 | // Consistency checks. 135 | if ($this->hasAlias($type)) { 136 | throw new MappingException("Cannot map '{$extension}' to '{$type}', '{$type}' is an alias"); 137 | } 138 | 139 | // Add entry to 't'. 140 | $this->addMapSubEntry('t', $type, 'e', $extension); 141 | 142 | // Add entry to 'e'. 143 | $this->addMapSubEntry('e', $extension, 't', $type); 144 | 145 | return $this; 146 | } 147 | 148 | public function getTypeDescriptions(string $type): array 149 | { 150 | $descriptions = $this->getMapSubEntry('t', $this->normalizeType($type), 'desc') ?: []; 151 | return array_values($descriptions); 152 | } 153 | 154 | public function getTypeAliases(string $type): array 155 | { 156 | $aliases = $this->getMapSubEntry('t', $this->normalizeType($type), 'a') ?: []; 157 | return array_values($aliases); 158 | } 159 | 160 | public function getTypeExtensions(string $type): array 161 | { 162 | $extensions = $this->getMapSubEntry('t', $this->normalizeType($type), 'e') ?: []; 163 | return array_values($extensions); 164 | } 165 | 166 | public function setTypeDefaultExtension(string $type, string $extension): MimeMapInterface 167 | { 168 | $type = $this->normalizeType($type); 169 | $extension = strtolower($extension); 170 | return $this->setValueAsDefault('t', $type, 'e', $extension); 171 | } 172 | 173 | public function removeType(string $type): bool 174 | { 175 | $type = $this->normalizeType($type); 176 | 177 | // Return false if type is not found. 178 | if (!$this->hasType($type)) { 179 | return false; 180 | } 181 | 182 | // Loop through all the associated extensions and remove them. 183 | foreach ($this->getTypeExtensions($type) as $extension) { 184 | $this->removeTypeExtensionMapping($type, $extension); 185 | } 186 | 187 | // Loop through all the associated aliases and remove them. 188 | foreach ($this->getTypeAliases($type) as $alias) { 189 | $this->removeTypeAlias($type, $alias); 190 | } 191 | 192 | unset(static::$map['t'][$type]); 193 | 194 | return true; 195 | } 196 | 197 | public function removeTypeAlias(string $type, string $alias): bool 198 | { 199 | $type = $this->normalizeType($type); 200 | $alias = $this->normalizeType($alias); 201 | 202 | // Remove any extension mapped to the alias. 203 | if ($extensions = $this->getMapSubEntry('a', $alias, 'e')) { 204 | foreach ($extensions as $extension) { 205 | $this->removeMapSubEntry('a', $alias, 'e', $extension); 206 | $this->removeMapSubEntry('e', $extension, 't', $alias); 207 | } 208 | } 209 | 210 | $type_ret = $this->removeMapSubEntry('t', $type, 'a', $alias); 211 | $alias_ret = $this->removeMapSubEntry('a', $alias, 't', $type); 212 | 213 | return $type_ret && $alias_ret; 214 | } 215 | 216 | public function removeTypeExtensionMapping(string $type, string $extension): bool 217 | { 218 | $type = $this->normalizeType($type); 219 | $extension = strtolower($extension); 220 | 221 | if ($this->hasAlias($type)) { 222 | $alias = $type; 223 | $type_ret = $this->removeMapSubEntry('a', $alias, 'e', $extension); 224 | $extension_ret = $this->removeMapSubEntry('e', $extension, 't', $alias); 225 | } else { 226 | $this->removeAliasedTypesExtensionMapping($type, $extension); 227 | $type_ret = $this->removeMapSubEntry('t', $type, 'e', $extension); 228 | $extension_ret = $this->removeMapSubEntry('e', $extension, 't', $type); 229 | } 230 | 231 | return $type_ret && $extension_ret; 232 | } 233 | 234 | /** 235 | * Removes aliased types extension mapping. 236 | * 237 | * @param string $type 238 | * A MIME type. 239 | * @param string $extension 240 | * The file extension to be removed. 241 | */ 242 | protected function removeAliasedTypesExtensionMapping(string $type, string $extension): void 243 | { 244 | $type = $this->normalizeType($type); 245 | $extension = strtolower($extension); 246 | foreach ($this->getExtensionTypes($extension) as $associated_type) { 247 | if ($this->hasAlias($associated_type) && $type === $this->getAliasTypes($associated_type)[0]) { 248 | $this->removeMapSubEntry('a', $associated_type, 'e', $extension); 249 | $this->removeMapSubEntry('e', $extension, 't', $associated_type); 250 | } 251 | } 252 | } 253 | 254 | public function getAliasTypes(string $alias): array 255 | { 256 | $types = $this->getMapSubEntry('a', $this->normalizeType($alias), 't') ?: []; 257 | return array_values($types); 258 | } 259 | 260 | public function getExtensionTypes(string $extension): array 261 | { 262 | $types = $this->getMapSubEntry('e', strtolower($extension), 't') ?: []; 263 | return array_values($types); 264 | } 265 | 266 | public function setExtensionDefaultType(string $extension, string $type): MimeMapInterface 267 | { 268 | $type = $this->normalizeType($type); 269 | $extension = strtolower($extension); 270 | 271 | if ($this->hasAlias($type)) { 272 | $alias = $type; 273 | $type = $this->getAliasTypes($alias)[0]; 274 | // Check that the alias is referring to a type associated with that 275 | // extension. 276 | $associated_types = $this->getMapSubEntry('e', $extension, 't') ?: []; 277 | if (!in_array($type, $associated_types)) { 278 | throw new MappingException("Cannot set '{$alias}' as default type for extension '{$extension}', its unaliased type '{$type}' is not associated to '{$extension}'"); 279 | } 280 | $this->addMapSubEntry('a', $alias, 'e', $extension); 281 | $this->addMapSubEntry('e', $extension, 't', $alias); 282 | return $this->setValueAsDefault('e', $extension, 't', $alias); 283 | } else { 284 | return $this->setValueAsDefault('e', $extension, 't', $type); 285 | } 286 | } 287 | 288 | protected function setValueAsDefault(string $entry, string $entryKey, string $subEntry, string $value): MimeMapInterface 289 | { 290 | parent::setValueAsDefault($entry, $entryKey, $subEntry, $value); 291 | return $this; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/Map/BaseMap.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | abstract class BaseMap implements MapInterface 16 | { 17 | /** 18 | * Singleton instance. 19 | * 20 | * @var MapInterface|null 21 | */ 22 | protected static $instance; 23 | 24 | /** 25 | * Mapping between file extensions and MIME types. 26 | * 27 | * @var TMap 28 | */ 29 | protected static $map = []; 30 | 31 | /** 32 | * A backup of the mapping between file extensions and MIME types. 33 | * 34 | * Used during the map update process. 35 | * 36 | * @var TMap|null 37 | */ 38 | protected static ?array $backupMap; 39 | 40 | public function __construct() 41 | { 42 | } 43 | 44 | public function backup(): void 45 | { 46 | static::$backupMap = static::$map; 47 | } 48 | 49 | public function reset(): void 50 | { 51 | if (isset(static::$backupMap)) { 52 | static::$map = static::$backupMap; 53 | } 54 | static::$backupMap = null; 55 | } 56 | 57 | public static function getInstance(): MapInterface 58 | { 59 | if (!isset(static::$instance)) { 60 | static::$instance = new static(); 61 | } 62 | return static::$instance; 63 | } 64 | 65 | public function getFileName(): string 66 | { 67 | throw new \LogicException(__METHOD__ . ' is not implemented in ' . get_called_class()); 68 | } 69 | 70 | public function getMapArray(): array 71 | { 72 | return static::$map; 73 | } 74 | 75 | public function sort(): MapInterface 76 | { 77 | foreach (array_keys(static::$map) as $k) { 78 | ksort(static::$map[$k]); 79 | foreach (static::$map[$k] as &$sub) { 80 | ksort($sub); 81 | } 82 | } 83 | return $this; 84 | } 85 | 86 | /** 87 | * Gets a list of entries of the map. 88 | * 89 | * @param string $entry 90 | * The main array entry. 91 | * @param string|null $match 92 | * (Optional) a match wildcard to limit the list. 93 | * 94 | * @return list 95 | * The list of the entries. 96 | */ 97 | protected function listEntries(string $entry, ?string $match = null): array 98 | { 99 | if (!isset(static::$map[$entry])) { 100 | return []; 101 | } 102 | 103 | $list = array_keys(static::$map[$entry]); 104 | 105 | if (is_null($match)) { 106 | return $list; 107 | } else { 108 | $re = strtr($match, ['/' => '\\/', '*' => '.*']); 109 | return array_values(array_filter($list, function (int|string $v) use ($re): bool { 110 | return preg_match("/$re/", (string) $v) === 1; 111 | })); 112 | } 113 | } 114 | 115 | /** 116 | * Gets the content of an entry of the map. 117 | * 118 | * @param string $entry 119 | * The main array entry. 120 | * @param string $entryKey 121 | * The main entry value. 122 | * 123 | * @return array> 124 | * The values of the entry, or empty array if missing. 125 | */ 126 | protected function getMapEntry(string $entry, string $entryKey): array 127 | { 128 | return static::$map[$entry][$entryKey] ?? []; 129 | } 130 | 131 | /** 132 | * Gets the content of a subentry of the map. 133 | * 134 | * @param string $entry 135 | * The main array entry. 136 | * @param string $entryKey 137 | * The main entry value. 138 | * @param string $subEntry 139 | * The sub entry. 140 | * 141 | * @return array,string> 142 | * The values of the subentry, or empty array if missing. 143 | */ 144 | protected function getMapSubEntry(string $entry, string $entryKey, string $subEntry): array 145 | { 146 | return static::$map[$entry][$entryKey][$subEntry] ?? []; 147 | } 148 | 149 | /** 150 | * Adds an entry to the map. 151 | * 152 | * Checks that no duplicate entries are made. 153 | * 154 | * @param string $entry 155 | * The main array entry. 156 | * @param string $entryKey 157 | * The main entry value. 158 | * @param string $subEntry 159 | * The sub entry. 160 | * @param string $value 161 | * The value to add. 162 | * 163 | * @return MapInterface 164 | */ 165 | protected function addMapSubEntry(string $entry, string $entryKey, string $subEntry, string $value): MapInterface 166 | { 167 | if (!isset(static::$map[$entry][$entryKey][$subEntry])) { 168 | // @phpstan-ignore assign.propertyType 169 | static::$map[$entry][$entryKey][$subEntry] = [$value]; 170 | } else { 171 | if (array_search($value, static::$map[$entry][$entryKey][$subEntry]) === false) { 172 | // @phpstan-ignore assign.propertyType 173 | static::$map[$entry][$entryKey][$subEntry][] = $value; 174 | } 175 | } 176 | return $this; 177 | } 178 | 179 | /** 180 | * Removes an entry from the map. 181 | * 182 | * @param string $entry 183 | * The main array entry. 184 | * @param string $entryKey 185 | * The main entry value. 186 | * @param string $subEntry 187 | * The sub entry. 188 | * @param string $value 189 | * The value to remove. 190 | * 191 | * @return bool 192 | * true if the entry was removed, false if the entry was not present. 193 | */ 194 | protected function removeMapSubEntry(string $entry, string $entryKey, string $subEntry, string $value): bool 195 | { 196 | // Return false if no entry. 197 | if (!isset(static::$map[$entry][$entryKey][$subEntry])) { 198 | return false; 199 | } 200 | 201 | // Return false if no value. 202 | $k = array_search($value, static::$map[$entry][$entryKey][$subEntry]); 203 | if ($k === false) { 204 | return false; 205 | } 206 | 207 | // Remove the map sub entry key. 208 | // @phpstan-ignore assign.propertyType 209 | unset(static::$map[$entry][$entryKey][$subEntry][$k]); 210 | 211 | // Remove the sub entry if no more values. 212 | if (empty(static::$map[$entry][$entryKey][$subEntry])) { 213 | // @phpstan-ignore assign.propertyType 214 | unset(static::$map[$entry][$entryKey][$subEntry]); 215 | } else { 216 | // Resequence the remaining values. 217 | $tmp = []; 218 | foreach (static::$map[$entry][$entryKey][$subEntry] as $v) { 219 | $tmp[] = $v; 220 | } 221 | // @phpstan-ignore assign.propertyType 222 | static::$map[$entry][$entryKey][$subEntry] = $tmp; 223 | } 224 | 225 | // Remove the entry if no more values. 226 | if (empty(static::$map[$entry][$entryKey])) { 227 | // @phpstan-ignore assign.propertyType 228 | unset(static::$map[$entry][$entryKey]); 229 | } 230 | 231 | return true; 232 | } 233 | 234 | /** 235 | * Sets a value as the default for an entry. 236 | * 237 | * @param string $entry 238 | * The main array entry. 239 | * @param string $entryKey 240 | * The main entry value. 241 | * @param string $subEntry 242 | * The sub entry. 243 | * @param string $value 244 | * The value to add. 245 | * 246 | * @throws MappingException if no mapping found. 247 | * 248 | * @return MapInterface 249 | */ 250 | protected function setValueAsDefault(string $entry, string $entryKey, string $subEntry, string $value): MapInterface 251 | { 252 | // Throw exception if no entry. 253 | if (!isset(static::$map[$entry][$entryKey][$subEntry])) { 254 | throw new MappingException("Cannot set '{$value}' as default for '{$entryKey}', '{$entryKey}' not defined"); 255 | } 256 | 257 | // Throw exception if no entry-value pair. 258 | $k = array_search($value, static::$map[$entry][$entryKey][$subEntry]); 259 | if ($k === false) { 260 | throw new MappingException("Cannot set '{$value}' as default for '{$entryKey}', '{$value}' not associated to '{$entryKey}'"); 261 | } 262 | 263 | // Move value to top of array and resequence the rest. 264 | $tmp = [$value]; 265 | foreach (static::$map[$entry][$entryKey][$subEntry] as $kk => $v) { 266 | if ($kk === $k) { 267 | continue; 268 | } 269 | $tmp[] = $v; 270 | } 271 | // @phpstan-ignore assign.propertyType 272 | static::$map[$entry][$entryKey][$subEntry] = $tmp; 273 | 274 | return $this; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/Map/EmptyMap.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public static function getInstance(): MapInterface; 23 | 24 | /** 25 | * Returns the map's class fully qualified filename. 26 | */ 27 | public function getFileName(): string; 28 | 29 | /** 30 | * Gets the map array. 31 | * 32 | * @return TMap 33 | */ 34 | public function getMapArray(): array; 35 | 36 | /** 37 | * Sorts the map. 38 | * 39 | * @return MapInterface 40 | */ 41 | public function sort(): MapInterface; 42 | 43 | /** 44 | * Backs up the map array. 45 | */ 46 | public function backup(): void; 47 | 48 | /** 49 | * Resets the map array to the backup. 50 | */ 51 | public function reset(): void; 52 | } 53 | -------------------------------------------------------------------------------- /src/Map/MimeMapInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface MimeMapInterface extends MapInterface 13 | { 14 | /** 15 | * Determines if a MIME type exists. 16 | * 17 | * @param string $type The type to be found. 18 | */ 19 | public function hasType(string $type): bool; 20 | 21 | /** 22 | * Determines if a MIME type alias exists. 23 | * 24 | * @param string $alias The alias to be found. 25 | */ 26 | public function hasAlias(string $alias): bool; 27 | 28 | /** 29 | * Determines if an entry exists from the 'extensions' array. 30 | * 31 | * @param string $extension The extension to be found. 32 | */ 33 | public function hasExtension(string $extension): bool; 34 | 35 | /** 36 | * Lists all the MIME types defined in the map. 37 | * 38 | * @param string $match (Optional) a match wildcard to limit the list. 39 | * 40 | * @return list 41 | */ 42 | public function listTypes(?string $match = null): array; 43 | 44 | /** 45 | * Lists all the MIME types aliases defined in the map. 46 | * 47 | * @param string $match (Optional) a match wildcard to limit the list. 48 | * 49 | * @return list 50 | */ 51 | public function listAliases(?string $match = null): array; 52 | 53 | /** 54 | * Lists all the extensions defined in the map. 55 | * 56 | * @param string $match (Optional) a match wildcard to limit the list. 57 | * 58 | * @return list 59 | */ 60 | public function listExtensions(?string $match = null): array; 61 | 62 | /** 63 | * Adds a description of a MIME type. 64 | * 65 | * @param string $type 66 | * A MIME type. 67 | * @param string $description 68 | * The description of the MIME type. 69 | * 70 | * @throws MappingException if $type is an alias. 71 | */ 72 | public function addTypeDescription(string $type, string $description): MimeMapInterface; 73 | 74 | /** 75 | * Adds an alias of a MIME type. 76 | * 77 | * @param string $type 78 | * A MIME type. 79 | * @param string $alias 80 | * An alias of $type. 81 | * 82 | * @throws MappingException if no $type is found. 83 | */ 84 | public function addTypeAlias(string $type, string $alias): MimeMapInterface; 85 | 86 | /** 87 | * Adds a type-to-extension mapping. 88 | * 89 | * @param string $type 90 | * A MIME type. 91 | * @param string $extension 92 | * A file extension. 93 | * 94 | * @throws MappingException if $type is an alias. 95 | */ 96 | public function addTypeExtensionMapping(string $type, string $extension): MimeMapInterface; 97 | 98 | /** 99 | * Gets the descriptions of a MIME type. 100 | * 101 | * @param string $type The type to be found. 102 | * 103 | * @return list The mapped descriptions. 104 | */ 105 | public function getTypeDescriptions(string $type): array; 106 | 107 | /** 108 | * Gets the aliases of a MIME type. 109 | * 110 | * @param string $type The type to be found. 111 | * 112 | * @return list The mapped aliases. 113 | */ 114 | public function getTypeAliases(string $type): array; 115 | 116 | /** 117 | * Gets the content of an entry from the 't' array. 118 | * 119 | * @param string $type The type to be found. 120 | * 121 | * @return list The mapped file extensions. 122 | */ 123 | public function getTypeExtensions(string $type): array; 124 | 125 | /** 126 | * Changes the default extension for a MIME type. 127 | * 128 | * @param string $type 129 | * A MIME type. 130 | * @param string $extension 131 | * A file extension. 132 | * 133 | * @throws MappingException if no mapping found. 134 | */ 135 | public function setTypeDefaultExtension(string $type, string $extension): MimeMapInterface; 136 | 137 | /** 138 | * Removes the entire mapping of a type. 139 | * 140 | * @param string $type 141 | * A MIME type. 142 | * 143 | * @return bool 144 | * true if the mapping was removed, false if the type was not present. 145 | */ 146 | public function removeType(string $type): bool; 147 | 148 | /** 149 | * Removes a MIME type alias. 150 | * 151 | * @param string $type 152 | * A MIME type. 153 | * @param string $alias 154 | * The alias to be removed. 155 | * 156 | * @return bool 157 | * true if the alias was removed, false if the alias was not present. 158 | */ 159 | public function removeTypeAlias(string $type, string $alias): bool; 160 | 161 | /** 162 | * Removes a type-to-extension mapping. 163 | * 164 | * @param string $type 165 | * A MIME type. 166 | * @param string $extension 167 | * The file extension to be removed. 168 | * 169 | * @return bool 170 | * true if the mapping was removed, false if the mapping was not present. 171 | */ 172 | public function removeTypeExtensionMapping(string $type, string $extension): bool; 173 | 174 | /** 175 | * Gets the parent types of an alias. 176 | * 177 | * There should not be multiple types for an alias. 178 | * 179 | * @param string $alias The alias to be found. 180 | * 181 | * @return list 182 | */ 183 | public function getAliasTypes(string $alias): array; 184 | 185 | /** 186 | * Gets the content of an entry from the 'extensions' array. 187 | * 188 | * @param string $extension The extension to be found. 189 | * 190 | * @return list The mapped MIME types. 191 | */ 192 | public function getExtensionTypes(string $extension): array; 193 | 194 | /** 195 | * Changes the default MIME type for a file extension. 196 | * 197 | * Allows a MIME type alias to be set as default for the extension. 198 | * 199 | * @param string $extension 200 | * A file extension. 201 | * @param string $type 202 | * A MIME type. 203 | * 204 | * @throws MappingException if no mapping found. 205 | */ 206 | public function setExtensionDefaultType(string $extension, string $type): MimeMapInterface; 207 | } 208 | -------------------------------------------------------------------------------- /src/MapHandler.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | protected static string $defaultMapClass = self::DEFAULT_MAP_CLASS; 26 | 27 | /** 28 | * Sets a map class as default for new instances. 29 | * 30 | * @param class-string $mapClass A FQCN. 31 | */ 32 | public static function setDefaultMapClass(string $mapClass): void 33 | { 34 | static::$defaultMapClass = $mapClass; 35 | } 36 | 37 | /** 38 | * Returns the map instance. 39 | * 40 | * @param class-string|null $mapClass 41 | * (Optional) The map FQCN to be used. If null, the default map will be 42 | * used. 43 | */ 44 | public static function map(?string $mapClass = null): MimeMapInterface 45 | { 46 | if ($mapClass === null) { 47 | $mapClass = static::$defaultMapClass; 48 | } 49 | $instance = $mapClass::getInstance(); 50 | assert($instance instanceof MimeMapInterface); 51 | return $instance; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MapUpdater.php: -------------------------------------------------------------------------------- 1 | map; 43 | } 44 | 45 | /** 46 | * Sets the map object to update. 47 | * 48 | * @param class-string $mapClass 49 | * The FQCN of the map to be updated. 50 | */ 51 | public function selectBaseMap(string $mapClass): MapUpdater 52 | { 53 | $this->map = MapHandler::map($mapClass); 54 | $this->map->backup(); 55 | return $this; 56 | } 57 | 58 | /** 59 | * Loads a new type-to-extension map reading from a file in Apache format. 60 | * 61 | * @param string $source_file 62 | * The source file. The file must conform to the format in the Apache 63 | * source code repository file where MIME types and file extensions are 64 | * associated. 65 | * 66 | * @return list 67 | * A list of error messages. 68 | * 69 | * @throws SourceUpdateException 70 | * If it was not possible to access the source file. 71 | */ 72 | public function loadMapFromApacheFile(string $source_file): array 73 | { 74 | $errors = []; 75 | 76 | $lines = @file($source_file); 77 | if ($lines == false) { 78 | throw new SourceUpdateException("Failed accessing {$source_file}"); 79 | } 80 | $i = 1; 81 | foreach ($lines as $line) { 82 | if ($line[0] == '#') { 83 | $i++; 84 | continue; 85 | } 86 | $line = preg_replace("#\\s+#", ' ', trim($line)); 87 | if (is_string($line)) { 88 | $parts = explode(' ', $line); 89 | $type = array_shift($parts); 90 | foreach ($parts as $extension) { 91 | $this->map->addTypeExtensionMapping($type, $extension); 92 | } 93 | } else { 94 | $errors[] = "Error processing line $i"; 95 | } 96 | $i++; 97 | } 98 | $this->map->sort(); 99 | 100 | return $errors; 101 | } 102 | 103 | /** 104 | * Loads a new type-to-extension map reading from a Freedesktop.org file. 105 | * 106 | * @param string $source_file 107 | * The source file. The file must conform to the format in the 108 | * Freedesktop.org database. 109 | * 110 | * @return list 111 | * A list of error messages. 112 | * 113 | * @throws SourceUpdateException 114 | * If it was not possible to access the source file. 115 | */ 116 | public function loadMapFromFreedesktopFile(string $source_file): array 117 | { 118 | $errors = []; 119 | 120 | $contents = @file_get_contents($source_file); 121 | if ($contents == false) { 122 | throw new SourceUpdateException('Failed loading file ' . $source_file); 123 | } 124 | 125 | $xml = @simplexml_load_string($contents); 126 | if ($xml == false) { 127 | $errors[] = 'Malformed XML in file ' . $source_file; 128 | return $errors; 129 | } 130 | 131 | $aliases = []; 132 | 133 | foreach ($xml as $node) { 134 | $exts = []; 135 | foreach ($node->glob as $glob) { 136 | $pattern = (string) $glob['pattern']; 137 | if ('*' != $pattern[0] || '.' != $pattern[1]) { 138 | continue; 139 | } 140 | $exts[] = substr($pattern, 2); 141 | } 142 | if (empty($exts)) { 143 | continue; 144 | } 145 | 146 | $type = (string) $node['type']; 147 | 148 | // Add description. 149 | if (isset($node->comment)) { 150 | $this->map->addTypeDescription($type, (string) $node->comment[0]); 151 | } 152 | if (isset($node->acronym)) { 153 | $acronym = (string) $node->acronym; 154 | /** @var ?string $expandedAcronym */ 155 | $expandedAcronym = $node->{'expanded-acronym'} ?? null; 156 | if (isset($expandedAcronym)) { 157 | $acronym .= ': ' . $expandedAcronym; 158 | } 159 | $this->map->addTypeDescription($type, $acronym); 160 | } 161 | 162 | // Add extensions. 163 | foreach ($exts as $ext) { 164 | $this->map->addTypeExtensionMapping($type, $ext); 165 | } 166 | 167 | // All aliases are accumulated and processed at the end of the 168 | // cycle to allow proper consistency checking on the completely 169 | // developed list of types. 170 | foreach ($node->alias as $alias) { 171 | $aliases[$type][] = (string) $alias['type']; 172 | } 173 | } 174 | 175 | // Add all the aliases, provide logging of errors. 176 | foreach ($aliases as $type => $a) { 177 | foreach ($a as $alias) { 178 | try { 179 | $this->map->addTypeAlias($type, $alias); 180 | } catch (MappingException $e) { 181 | $errors[] = $e->getMessage(); 182 | } 183 | } 184 | } 185 | $this->map->sort(); 186 | 187 | return $errors; 188 | } 189 | 190 | /** 191 | * Applies to the map an array of overrides. 192 | * 193 | * @param array}> $overrides 194 | * The overrides to be applied. 195 | * 196 | * @return list 197 | * A list of error messages. 198 | */ 199 | public function applyOverrides(array $overrides): array 200 | { 201 | $errors = []; 202 | 203 | foreach ($overrides as $command) { 204 | try { 205 | $callable = [$this->map, $command[0]]; 206 | assert(is_callable($callable)); 207 | call_user_func_array($callable, $command[1]); 208 | } catch (MappingException $e) { 209 | $errors[] = $e->getMessage(); 210 | } 211 | } 212 | $this->map->sort(); 213 | 214 | return $errors; 215 | } 216 | 217 | /** 218 | * Updates the map at a destination PHP file. 219 | */ 220 | public function writeMapToPhpClassFile(string $destinationFile): MapUpdater 221 | { 222 | $content = @file_get_contents($destinationFile); 223 | if ($content == false) { 224 | throw new \RuntimeException('Failed loading file ' . $destinationFile); 225 | } 226 | 227 | $newContent = preg_replace( 228 | '#protected static \$map = (.+?);#s', 229 | "protected static \$map = " . preg_replace('/\s+$/m', '', var_export($this->map->getMapArray(), true)) . ";", 230 | $content 231 | ); 232 | file_put_contents($destinationFile, $newContent); 233 | 234 | return $this; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/MappingException.php: -------------------------------------------------------------------------------- 1 | 57 | */ 58 | protected array $descriptions = []; 59 | 60 | /** 61 | * Optional MIME parameters. 62 | * 63 | * @var array 64 | */ 65 | protected array $parameters = []; 66 | 67 | /** 68 | * The MIME types map. 69 | * 70 | * @var MimeMapInterface 71 | */ 72 | protected readonly MimeMapInterface $map; 73 | 74 | public function __construct(string $typeString, ?string $mapClass = null) 75 | { 76 | TypeParser::parse($typeString, $this); 77 | $this->map = MapHandler::map($mapClass); 78 | } 79 | 80 | public function getMedia(): string 81 | { 82 | return $this->media; 83 | } 84 | 85 | public function setMedia(string $media): TypeInterface 86 | { 87 | $this->media = $media; 88 | return $this; 89 | } 90 | 91 | public function hasMediaComment(): bool 92 | { 93 | return $this->mediaComment !== null; 94 | } 95 | 96 | public function getMediaComment(): string 97 | { 98 | if ($this->hasMediaComment()) { 99 | assert(is_string($this->mediaComment)); 100 | return $this->mediaComment; 101 | } 102 | throw new UndefinedException('Media comment is not defined'); 103 | } 104 | 105 | public function setMediaComment(?string $comment = null): TypeInterface 106 | { 107 | $this->mediaComment = $comment; 108 | return $this; 109 | } 110 | 111 | public function getSubType(): string 112 | { 113 | return $this->subType; 114 | } 115 | 116 | public function setSubType(string $subType): TypeInterface 117 | { 118 | $this->subType = $subType; 119 | return $this; 120 | } 121 | 122 | public function hasSubTypeComment(): bool 123 | { 124 | return $this->subTypeComment !== null; 125 | } 126 | 127 | public function getSubTypeComment(): string 128 | { 129 | if ($this->hasSubTypeComment()) { 130 | assert(is_string($this->subTypeComment)); 131 | return $this->subTypeComment; 132 | } 133 | throw new UndefinedException('Subtype comment is not defined'); 134 | } 135 | 136 | public function setSubTypeComment(?string $comment = null): TypeInterface 137 | { 138 | $this->subTypeComment = $comment; 139 | return $this; 140 | } 141 | 142 | public function hasParameters(): bool 143 | { 144 | return (bool) $this->parameters; 145 | } 146 | 147 | public function getParameters(): array 148 | { 149 | if ($this->hasParameters()) { 150 | return $this->parameters; 151 | } 152 | throw new UndefinedException("No parameters defined"); 153 | } 154 | 155 | public function hasParameter(string $name): bool 156 | { 157 | return isset($this->parameters[$name]); 158 | } 159 | 160 | public function getParameter(string $name): TypeParameter 161 | { 162 | if ($this->hasParameter($name)) { 163 | return $this->parameters[$name]; 164 | } 165 | throw new UndefinedException("Parameter $name is not defined"); 166 | } 167 | 168 | public function addParameter(string $name, string $value, ?string $comment = null): void 169 | { 170 | $this->parameters[$name] = new TypeParameter($name, $value, $comment); 171 | } 172 | 173 | public function removeParameter(string $name): void 174 | { 175 | unset($this->parameters[$name]); 176 | } 177 | 178 | public function toString(int $format = Type::FULL_TEXT): string 179 | { 180 | $type = strtolower($this->media); 181 | if ($format > Type::FULL_TEXT && $this->hasMediaComment()) { 182 | $type .= ' (' . $this->getMediaComment() . ')'; 183 | } 184 | $type .= '/' . strtolower($this->subType); 185 | if ($format > Type::FULL_TEXT && $this->hasSubTypeComment()) { 186 | $type .= ' (' . $this->getSubTypeComment() . ')'; 187 | } 188 | if ($format > Type::SHORT_TEXT && count($this->parameters)) { 189 | foreach ($this->parameters as $parameter) { 190 | $type .= '; ' . $parameter->toString($format); 191 | } 192 | } 193 | return $type; 194 | } 195 | 196 | public function isExperimental(): bool 197 | { 198 | return substr($this->getMedia(), 0, 2) == 'x-' || substr($this->getSubType(), 0, 2) == 'x-'; 199 | } 200 | 201 | public function isVendor(): bool 202 | { 203 | return substr($this->getSubType(), 0, 4) == 'vnd.'; 204 | } 205 | 206 | public function isWildcard(): bool 207 | { 208 | return ($this->getMedia() === '*' && $this->getSubtype() === '*') || strpos($this->getSubtype(), '*') !== false; 209 | } 210 | 211 | public function isAlias(): bool 212 | { 213 | return $this->map->hasAlias($this->toString(static::SHORT_TEXT)); 214 | } 215 | 216 | public function wildcardMatch(string $wildcard): bool 217 | { 218 | $wildcardType = new static($wildcard); 219 | 220 | if (!$wildcardType->isWildcard()) { 221 | return false; 222 | } 223 | 224 | $wildcardRe = strtr($wildcardType->toString(static::SHORT_TEXT), [ 225 | '/' => '\\/', 226 | '*' => '.*', 227 | ]); 228 | $subject = $this->toString(static::SHORT_TEXT); 229 | 230 | return preg_match("/{$wildcardRe}/", $subject) === 1; 231 | } 232 | 233 | public function buildTypesList(): array 234 | { 235 | $subject = $this->toString(static::SHORT_TEXT); 236 | 237 | // Find all types. 238 | $types = []; 239 | if (!$this->isWildcard()) { 240 | if ($this->map->hasType($subject)) { 241 | $types[] = $subject; 242 | } 243 | } else { 244 | foreach ($this->map->listTypes($subject) as $t) { 245 | $types[] = $t; 246 | } 247 | } 248 | 249 | if (!empty($types)) { 250 | return $types; 251 | } 252 | 253 | throw new MappingException('No MIME type found for ' . $subject . ' in map'); 254 | } 255 | 256 | /** 257 | * Returns the unaliased MIME type. 258 | * 259 | * @return TypeInterface 260 | * $this if the current type is not an alias, the parent type if the 261 | * current type is an alias. 262 | */ 263 | protected function getUnaliasedType(): TypeInterface 264 | { 265 | return $this->isAlias() ? new static($this->map->getAliasTypes($this->toString(static::SHORT_TEXT))[0]) : $this; 266 | } 267 | 268 | public function hasDescription(): bool 269 | { 270 | if ($this->descriptions === []) { 271 | $this->descriptions = $this->map->getTypeDescriptions($this->getUnaliasedType()->toString(static::SHORT_TEXT)); 272 | } 273 | return isset($this->descriptions[0]); 274 | } 275 | 276 | public function getDescription(bool $includeAcronym = false): string 277 | { 278 | if (!$this->hasDescription()) { 279 | throw new MappingException('No description available for type: ' . $this->toString(static::SHORT_TEXT)); 280 | } 281 | 282 | $res = $this->descriptions[0]; 283 | if ($includeAcronym && isset($this->descriptions[1])) { 284 | $res .= ', ' . $this->descriptions[1]; 285 | } 286 | 287 | return $res; 288 | } 289 | 290 | public function getAliases(): array 291 | { 292 | // Fail if the current type is an alias already. 293 | if ($this->isAlias()) { 294 | $subject = $this->toString(static::SHORT_TEXT); 295 | throw new MappingException("Cannot get aliases for '{$subject}', it is an alias itself"); 296 | } 297 | 298 | // Build the array of aliases. 299 | $aliases = []; 300 | foreach ($this->buildTypesList() as $t) { 301 | foreach ($this->map->getTypeAliases((string) $t) as $a) { 302 | $aliases[$a] = $a; 303 | } 304 | } 305 | 306 | return array_keys($aliases); 307 | } 308 | 309 | public function getDefaultExtension(): string 310 | { 311 | $unaliasedType = $this->getUnaliasedType(); 312 | $subject = $unaliasedType->toString(static::SHORT_TEXT); 313 | 314 | if (!$unaliasedType->isWildcard()) { 315 | $proceed = $this->map->hasType($subject); 316 | } else { 317 | $proceed = count($this->map->listTypes($subject)) === 1; 318 | } 319 | 320 | if ($proceed) { 321 | return $unaliasedType->getExtensions()[0]; 322 | } 323 | 324 | throw new MappingException('Cannot determine default extension for type: ' . $unaliasedType->toString(static::SHORT_TEXT)); 325 | } 326 | 327 | public function getExtensions(): array 328 | { 329 | // Build the array of extensions. 330 | $extensions = []; 331 | foreach ($this->getUnaliasedType()->buildTypesList() as $t) { 332 | foreach ($this->map->getTypeExtensions((string) $t) as $e) { 333 | $extensions[$e] = $e; 334 | } 335 | } 336 | return array_values($extensions); 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/TypeInterface.php: -------------------------------------------------------------------------------- 1 | |null $mapClass 20 | * (Optional) The FQCN of the map class to use. 21 | * 22 | * @api 23 | */ 24 | public function __construct(string $typeString, ?string $mapClass = null); 25 | 26 | /** 27 | * Gets a MIME type's media. 28 | * 29 | * Note: 'media' refers to the portion before the first slash. 30 | * 31 | * @api 32 | */ 33 | public function getMedia(): string; 34 | 35 | /** 36 | * Sets a MIME type's media. 37 | * 38 | * @api 39 | */ 40 | public function setMedia(string $media): TypeInterface; 41 | 42 | /** 43 | * Checks if the MIME type has media comment. 44 | * 45 | * @api 46 | */ 47 | public function hasMediaComment(): bool; 48 | 49 | /** 50 | * Gets the MIME type's media comment. 51 | * 52 | * @throws UndefinedException 53 | * 54 | * @api 55 | */ 56 | public function getMediaComment(): string; 57 | 58 | /** 59 | * Sets the MIME type's media comment. 60 | * 61 | * @param string $comment (optional) a comment; when missing any existing comment is removed. 62 | * 63 | * @api 64 | */ 65 | public function setMediaComment(?string $comment = null): TypeInterface; 66 | 67 | /** 68 | * Gets a MIME type's subtype. 69 | * 70 | * @api 71 | */ 72 | public function getSubType(): string; 73 | 74 | /** 75 | * Sets a MIME type's subtype. 76 | * 77 | * @api 78 | */ 79 | public function setSubType(string $subType): TypeInterface; 80 | 81 | /** 82 | * Checks if the MIME type has subtype comment. 83 | * 84 | * @api 85 | */ 86 | public function hasSubTypeComment(): bool; 87 | 88 | /** 89 | * Gets the MIME type's subtype comment. 90 | * 91 | * @throws UndefinedException 92 | * 93 | * @api 94 | */ 95 | public function getSubTypeComment(): string; 96 | 97 | /** 98 | * Sets the MIME type's subtype comment. 99 | * 100 | * @param string|null $comment (optional) a comment; when missing any existing comment is removed. 101 | * 102 | * @api 103 | */ 104 | public function setSubTypeComment(?string $comment = null): TypeInterface; 105 | 106 | /** 107 | * Checks if the MIME type has any parameter. 108 | * 109 | * @api 110 | */ 111 | public function hasParameters(): bool; 112 | 113 | /** 114 | * Get the MIME type's parameters. 115 | * 116 | * @return TypeParameter[] 117 | * 118 | * @throws UndefinedException 119 | * 120 | * @api 121 | */ 122 | public function getParameters(): array; 123 | 124 | /** 125 | * Checks if the MIME type has a parameter. 126 | * 127 | * @throws UndefinedException 128 | * 129 | * @api 130 | */ 131 | public function hasParameter(string $name): bool; 132 | 133 | /** 134 | * Get a MIME type's parameter. 135 | * 136 | * @throws UndefinedException 137 | * 138 | * @api 139 | */ 140 | public function getParameter(string $name): TypeParameter; 141 | 142 | /** 143 | * Add a parameter to this type 144 | * 145 | * @api 146 | */ 147 | public function addParameter(string $name, string $value, ?string $comment = null): void; 148 | 149 | /** 150 | * Remove a parameter from this type. 151 | * 152 | * @api 153 | */ 154 | public function removeParameter(string $name): void; 155 | 156 | /** 157 | * Create a textual MIME type from object values. 158 | * 159 | * This function performs the opposite function of parse(). 160 | * 161 | * @param int $format 162 | * The format of the output string. 163 | * 164 | * @api 165 | */ 166 | public function toString(int $format = Type::FULL_TEXT): string; 167 | 168 | /** 169 | * Is this type experimental? 170 | * 171 | * Note: Experimental types are denoted by a leading 'x-' in the media or 172 | * subtype, e.g. text/x-vcard or x-world/x-vrml. 173 | * 174 | * @api 175 | */ 176 | public function isExperimental(): bool; 177 | 178 | /** 179 | * Is this a vendor MIME type? 180 | * 181 | * Note: Vendor types are denoted with a leading 'vnd. in the subtype. 182 | * 183 | * @api 184 | */ 185 | public function isVendor(): bool; 186 | 187 | /** 188 | * Is this a wildcard type? 189 | * 190 | * @api 191 | */ 192 | public function isWildcard(): bool; 193 | 194 | /** 195 | * Is this an alias? 196 | * 197 | * @api 198 | */ 199 | public function isAlias(): bool; 200 | 201 | /** 202 | * Perform a wildcard match on a MIME type 203 | * 204 | * Example: 205 | * $type = new Type('image/png'); 206 | * $type->wildcardMatch('image/*'); 207 | * 208 | * @param string $wildcard 209 | * Wildcard to check against. 210 | * 211 | * @return bool 212 | * True if there was a match, false otherwise. 213 | * 214 | * @api 215 | */ 216 | public function wildcardMatch(string $wildcard): bool; 217 | 218 | /** 219 | * Builds a list of MIME types existing in the map. 220 | * 221 | * If the current type is a wildcard, than all the types matching the 222 | * wildcard will be returned. 223 | * 224 | * @throws MappingException if no mapping found. 225 | * 226 | * @return array 227 | * 228 | * @api 229 | */ 230 | public function buildTypesList(): array; 231 | 232 | /** 233 | * Checks if a description for the MIME type exists. 234 | * 235 | * @api 236 | */ 237 | public function hasDescription(): bool; 238 | 239 | /** 240 | * Returns a description for the MIME type, if existing in the map. 241 | * 242 | * @param bool $includeAcronym 243 | * (Optional) if true and an acronym description exists for the type, 244 | * the returned description will contain the acronym and its description, 245 | * appended with a comma. Defaults to false. 246 | * 247 | * @throws MappingException if no description found. 248 | * 249 | * @api 250 | */ 251 | public function getDescription(bool $includeAcronym = false): string; 252 | 253 | /** 254 | * Returns all the aliases related to the MIME type(s). 255 | * 256 | * If the current type is a wildcard, than all aliases of all the 257 | * types matching the wildcard will be returned. 258 | * 259 | * @throws MappingException on error. 260 | * 261 | * @return list 262 | * 263 | * @api 264 | */ 265 | public function getAliases(): array; 266 | 267 | /** 268 | * Returns the MIME type's preferred file extension. 269 | * 270 | * @throws MappingException if no mapping found. 271 | * 272 | * @api 273 | */ 274 | public function getDefaultExtension(): string; 275 | 276 | /** 277 | * Returns all the file extensions related to the MIME type(s). 278 | * 279 | * If the current type is a wildcard, than all extensions of all the types matching the wildcard will be returned. 280 | * 281 | * @throws MappingException if no mapping found. 282 | * 283 | * @return list 284 | * 285 | * @api 286 | */ 287 | public function getExtensions(): array; 288 | } 289 | -------------------------------------------------------------------------------- /src/TypeParameter.php: -------------------------------------------------------------------------------- 1 | name; 32 | } 33 | 34 | /** 35 | * Gets the parameter value. 36 | * 37 | * @api 38 | */ 39 | public function getValue(): string 40 | { 41 | return $this->value; 42 | } 43 | 44 | /** 45 | * Does this parameter have a comment? 46 | * 47 | * @api 48 | */ 49 | public function hasComment(): bool 50 | { 51 | return (bool) $this->comment; 52 | } 53 | 54 | /** 55 | * Gets the parameter comment. 56 | * 57 | * @throws UndefinedException 58 | * 59 | * @api 60 | */ 61 | public function getComment(): string 62 | { 63 | if ($this->hasComment()) { 64 | assert(is_string($this->comment)); 65 | return $this->comment; 66 | } 67 | throw new UndefinedException('Parameter comment is not defined'); 68 | } 69 | 70 | /** 71 | * Gets a string representation of this parameter. 72 | * 73 | * @param int $format The format of the output string. 74 | * 75 | * @api 76 | */ 77 | public function toString(int $format = Type::FULL_TEXT): string 78 | { 79 | $val = $this->name . '="' . str_replace('"', '\\"', $this->value) . '"'; 80 | if ($format > Type::FULL_TEXT && $this->hasComment()) { 81 | $val .= ' (' . $this->getComment() . ')'; 82 | } 83 | return $val; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/TypeParser.php: -------------------------------------------------------------------------------- 1 | setMedia(strtolower($media['string'])); 31 | if ($media['comment'] !== null) { 32 | $type->setMediaComment($media['comment']); 33 | } 34 | 35 | // SubType and Parameters are separated by semicolons ';'. 36 | $sub = static::parseStringPart($typeString, $media['end_offset'] + 1, ';'); 37 | if (!$sub['string']) { 38 | throw new MalformedTypeException('Media subtype not found'); 39 | } 40 | $type->setSubType(strtolower($sub['string'])); 41 | if ($sub['comment'] !== null) { 42 | $type->setSubTypeComment($sub['comment']); 43 | } 44 | 45 | // Loops through the parameter. 46 | while ($sub['delimiter_matched']) { 47 | $sub = static::parseStringPart($typeString, $sub['end_offset'] + 1, ';'); 48 | $tmp = explode('=', $sub['string'], 2); 49 | $p_name = trim($tmp[0]); 50 | $p_val = str_replace('\\"', '"', trim($tmp[1] ?? '')); 51 | $type->addParameter($p_name, $p_val, $sub['comment']); 52 | } 53 | } 54 | 55 | /** 56 | * Parses a part of the content MIME type string. 57 | * 58 | * Splits string and comment until a delimiter is found. 59 | * 60 | * @param string $string 61 | * Input string. 62 | * @param int $offset 63 | * Offset to start parsing from. 64 | * @param string $delimiter 65 | * Stop parsing when delimiter found. 66 | * 67 | * @return array{'string': string, 'comment': string|null, 'delimiter_matched': bool, 'end_offset': int} 68 | * An array with the following keys: 69 | * 'string' - the uncommented part of $string 70 | * 'comment' - the comment part of $string 71 | * 'delimiter_matched' - true if a $delimiter stopped the parsing, false 72 | * otherwise 73 | * 'end_offset' - the last position parsed in $string. 74 | */ 75 | public static function parseStringPart(string $string, int $offset, string $delimiter): array 76 | { 77 | $inquote = false; 78 | $escaped = false; 79 | $incomment = 0; 80 | $newstring = ''; 81 | $comment = ''; 82 | 83 | for ($n = $offset; $n < strlen($string); $n++) { 84 | if ($string[$n] === $delimiter && !$escaped && !$inquote && $incomment === 0) { 85 | break; 86 | } 87 | 88 | if ($escaped) { 89 | if ($incomment == 0) { 90 | $newstring .= $string[$n]; 91 | } else { 92 | $comment .= $string[$n]; 93 | } 94 | $escaped = false; 95 | continue; 96 | } 97 | 98 | if ($string[$n] == '\\') { 99 | if ($incomment > 0) { 100 | $comment .= $string[$n]; 101 | } 102 | $escaped = true; 103 | continue; 104 | } 105 | 106 | if (!$inquote && $incomment > 0 && $string[$n] == ')') { 107 | $incomment--; 108 | if ($incomment == 0) { 109 | $comment .= ' '; 110 | } 111 | continue; 112 | } 113 | 114 | if (!$inquote && $string[$n] == '(') { 115 | $incomment++; 116 | continue; 117 | } 118 | 119 | if ($string[$n] == '"') { 120 | if ($incomment > 0) { 121 | $comment .= $string[$n]; 122 | } else { 123 | if ($inquote) { 124 | $inquote = false; 125 | } else { 126 | $inquote = true; 127 | } 128 | } 129 | continue; 130 | } 131 | 132 | if ($incomment == 0) { 133 | $newstring .= $string[$n]; 134 | continue; 135 | } 136 | 137 | $comment .= $string[$n]; 138 | } 139 | 140 | if ($incomment > 0) { 141 | throw new MalformedTypeException('Comment closing bracket missing: ' . $comment); 142 | } 143 | 144 | return [ 145 | 'string' => trim($newstring), 146 | 'comment' => empty($comment) ? null : trim($comment), 147 | 'delimiter_matched' => isset($string[$n]) ? ($string[$n] === $delimiter) : false, 148 | 'end_offset' => $n, 149 | ]; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/UndefinedException.php: -------------------------------------------------------------------------------- 1 | >>> 37 | */ 38 | // phpcs:disable 39 | protected static $map = array ( 40 | 't' => 41 | array ( 42 | 'application/andrew-inset' => 43 | array ( 44 | 'desc' => 45 | array ( 46 | 0 => 'ATK inset', 47 | 1 => 'ATK: Andrew Toolkit', 48 | ), 49 | 'e' => 50 | array ( 51 | 0 => 'ez', 52 | ), 53 | ), 54 | ), 55 | 'e' => 56 | array ( 57 | 'ez' => 58 | array ( 59 | 't' => 60 | array ( 61 | 0 => 'application/andrew-inset', 62 | ), 63 | ), 64 | ), 65 | ); 66 | // phpcs:enable 67 | } 68 | -------------------------------------------------------------------------------- /tests/fixtures/invalid.freedesktop.xml: -------------------------------------------------------------------------------- 1 | 2 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ]> 64 | 65 | 66 | Atari 2600 67 | Atari 2600 68 | Atari 2600 69 | Atari 2600 70 | Atari 2600 71 | Atari 2600 72 | Atari 2600 73 | Atari 2600 74 | Atari 2600 75 | Atari 2600 76 | Atari 2600 77 | Atari 2600 78 | אטארי 2600 79 | Atari 2600 80 | Atari 2600 81 | Atari 2600 82 | Atari 2600 83 | Atari 2600 84 | Atari 2600 85 | Atari 2600 86 | Atari 2600 87 | Atari 2600 88 | Atari 2600 89 | Atari 2600 90 | Атари 2600 91 | Atari 2600 92 | Atari 2600 93 | Atari 2600 94 | 雅达利 2600 95 | Atari 2600 96 | 97 | 98 | 99 | 100 | help page 101 | صفحة المساعدة 102 | yardım səhifəsi 103 | staronka dapamohi 104 | Страница от помощта 105 | pàgina d'ajuda 106 | stránka nápovědy 107 | tudalen gymorth 108 | hjælpeside 109 | Hilfeseite 110 | Σελίδα βοήθειας 111 | help page 112 | help-paĝo 113 | página de ayuda 114 | laguntzako orria 115 | ohjesivu 116 | hjálparsíða 117 | page d'aide 118 | leathanach cabhrach 119 | páxina de axuda 120 | דף עזרה 121 | Stranica pomoći 122 | súgóoldal 123 | Pagina de adjuta 124 | halaman bantuan 125 | Pagina di aiuto 126 | ヘルプページ 127 | анықтама парағы 128 | 도움말 페이지 129 | žinyno puslapis 130 | palīdzības lapa 131 | Halaman bantuan 132 | hjelpside 133 | hulppagina 134 | hjelpeside 135 | pagina d'ajuda 136 | Strona pomocy 137 | página de ajuda 138 | Página de ajuda 139 | pagină de ajutor 140 | Страница справки 141 | Stránka Pomocníka 142 | stran pomoči 143 | Faqe ndihme 144 | страница помоћи 145 | hjälpsida 146 | yardım sayfası 147 | сторінка довідки 148 | trang trợ giúp 149 | 帮助页面 150 | 求助頁面 151 | 152 | 153 | 154 | plain text document 155 | مستند نصي مجرد 156 | documentu de testu planu 157 | prosty tekstavy dakument 158 | Документ с неформатиран текст 159 | document de text pla 160 | prostý textový dokument 161 | rent tekstdokument 162 | Einfaches Textdokument 163 | Έγγραφο απλού κειμένου 164 | plain text document 165 | plata teksta dokumento 166 | documento de texto sencillo 167 | testu soileko dokumentua 168 | perustekstiasiakirja 169 | document texte brut 170 | cáipéis ghnáth-théacs 171 | documento de texto sinxelo 172 | מסמך טקסט פשוט 173 | Običan tekstovni dokument 174 | egyszerű szöveg 175 | Documento de texto simple 176 | dokumen teks polos 177 | Documento in testo semplice 178 | 平文テキストドキュメント 179 | мәтіндік құжаты 180 | 일반 텍스트 문서 181 | paprastas tekstinis dokumentas 182 | vienkāršs teksta dokuments 183 | Dokumen teks jernih 184 | vanlig tekstdokument 185 | plattetekst-document 186 | vanleg tekstdokument 187 | document tèxte brut 188 | Zwykły dokument tekstowy 189 | documento em texto simples 190 | Documento de Texto 191 | document text simplu 192 | Текстовый документ 193 | Obyčajný textový dokument 194 | običajna besedilna datoteka 195 | Dokument në tekst të thjeshtë 196 | обичан текстуални документ 197 | vanligt textdokument 198 | düz metin belgesi 199 | звичайний текстовий документ 200 | tài liệu nhập thô 201 | 纯文本文档 202 | 純文字文件 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | PDF document 213 | مستند PDF 214 | Documentu PDF 215 | Dakument PDF 216 | Документ — PDF 217 | document PDF 218 | dokument PDF 219 | Dogfen PDF 220 | PDF-dokument 221 | PDF-Dokument 222 | Έγγραφο PDF 223 | PDF document 224 | PDF-dokumento 225 | documento PDF 226 | PDF dokumentua 227 | PDF-asiakirja 228 | PDF skjal 229 | document PDF 230 | cáipéis PDF 231 | documento PDF 232 | מסמך PDF 233 | PDF dokument 234 | PDF-dokumentum 235 | Documento PDF 236 | Dokumen PDF 237 | Documento PDF 238 | PDF ドキュメント 239 | PDF құжаты 240 | PDF 문서 241 | PDF dokumentas 242 | PDF dokuments 243 | Dokumen PDF 244 | PDF-dokument 245 | PDF-document 246 | PDF-dokument 247 | document PDF 248 | Dokument PDF 249 | documento PDF 250 | Documento PDF 251 | Document PDF 252 | Документ PDF 253 | Dokument PDF 254 | Dokument PDF 255 | Dokument PDF 256 | ПДФ документ 257 | PDF-dokument 258 | PDF belgesi 259 | документ PDF 260 | Tài liệu PDF 261 | PDF 文档 262 | PDF 文件 263 | PDF 264 | Portable Document Format 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /tests/fixtures/min.mime-types.txt: -------------------------------------------------------------------------------- 1 | # This file maps Internet media types to unique file extension(s). 2 | # 3 | # This is a subset version for testing purposes. 4 | # 5 | # MIME type (lowercased) Extensions 6 | # ============================================ ========== 7 | image/jpeg jpeg jpg jpe 8 | text/plain txt 9 | -------------------------------------------------------------------------------- /tests/fixtures/some.mime-types.txt: -------------------------------------------------------------------------------- 1 | # This file maps Internet media types to unique file extension(s). 2 | # 3 | # This is a subset version for testing purposes. 4 | 5 | # MIME type (lowercased) Extensions 6 | # ============================================ ========== 7 | # application/edi-consent 8 | # application/edi-x12 9 | # application/edifact 10 | application/octet-stream bin dms lrf mar so dist distz pkg bpk dump elc deploy 11 | application/pls+xml pls 12 | audio/midi mid midi kar rmi 13 | # audio/mobile-xmf 14 | audio/mp4 m4a mp4a 15 | image/jpeg jpeg jpg jpe 16 | image/sgi sgi 17 | image/svg+xml svg svgz 18 | # image/t38 19 | image/tiff tiff tif 20 | text/css css 21 | text/csv csv 22 | text/plain txt text conf def list log in 23 | -------------------------------------------------------------------------------- /tests/fixtures/zero.freedesktop.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ]> 64 | 65 | 66 | -------------------------------------------------------------------------------- /tests/fixtures/zero.mime-types.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FileEye/MimeMap/bab84c46fef4dc2d079450eb116b6119783a7595/tests/fixtures/zero.mime-types.txt -------------------------------------------------------------------------------- /tests/src/ExtensionTest.php: -------------------------------------------------------------------------------- 1 | assertSame('text/plain', (new Extension('txt'))->getDefaultType()); 13 | $this->assertSame('text/plain', (new Extension('TXT'))->getDefaultType()); 14 | $this->assertSame('image/png', (new Extension('png'))->getDefaultType()); 15 | $this->assertSame('application/vnd.oasis.opendocument.text', (new Extension('odt'))->getDefaultType()); 16 | } 17 | 18 | public function testGetStrictDefaultTypeUnknownExtension(): void 19 | { 20 | $this->expectException(MappingException::class); 21 | $this->expectExceptionMessage("No MIME type mapped to extension ohmygodthatisnoextension"); 22 | $this->assertSame('application/octet-stream', (new Extension('ohmygodthatisnoextension'))->getDefaultType()); 23 | } 24 | 25 | public function testGetTypes(): void 26 | { 27 | $this->assertSame(['text/vnd.dvb.subtitle', 'image/vnd.dvb.subtitle', 'text/x-microdvd', 'text/x-mpsub', 'text/x-subviewer'], (new Extension('sub'))->getTypes()); 28 | $this->assertSame(['text/vnd.dvb.subtitle', 'image/vnd.dvb.subtitle', 'text/x-microdvd', 'text/x-mpsub', 'text/x-subviewer'], (new Extension('sUb'))->getTypes()); 29 | } 30 | 31 | public function testGetStrictTypesUnknownExtension(): void 32 | { 33 | $this->expectException(MappingException::class); 34 | $this->expectExceptionMessage("No MIME type mapped to extension ohmygodthatisnoextension"); 35 | $this->assertSame(['application/octet-stream'], (new Extension('ohmygodthatisnoextension'))->getTypes()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/src/MapHandlerTest.php: -------------------------------------------------------------------------------- 1 | map = MapHandler::map(); 24 | } 25 | 26 | public function testSetDefaultMapClass(): void 27 | { 28 | MapHandler::setDefaultMapClass(EmptyMap::class); 29 | $this->assertInstanceOf(EmptyMap::class, MapHandler::map()); 30 | MapHandler::setDefaultMapClass(DefaultMap::class); 31 | // @phpstan-ignore method.impossibleType 32 | $this->assertInstanceOf(DefaultMap::class, MapHandler::map()); 33 | } 34 | 35 | public function testMap(): void 36 | { 37 | $this->assertStringContainsString('DefaultMap.php', $this->map->getFileName()); 38 | } 39 | 40 | public function testSort(): void 41 | { 42 | $this->map->addTypeExtensionMapping('aaa/aaa', '000a')->sort(); 43 | $this->assertSame('aaa/aaa', $this->map->listTypes()[0]); 44 | $this->assertSame('000a', $this->map->listExtensions()[0]); 45 | } 46 | 47 | public function testAdd(): void 48 | { 49 | // Adding a new type with a new extension. 50 | $this->map->addTypeExtensionMapping('bingo/bongo', 'bngbng'); 51 | $this->map->addTypeDescription('bingo/bongo', 'Bingo, Bongo!'); 52 | $this->assertSame(['bngbng'], (new Type('bingo/bongo'))->getExtensions()); 53 | $this->assertSame('bngbng', (new Type('bingo/bongo'))->getDefaultExtension()); 54 | $this->assertSame('Bingo, Bongo!', (new Type('bingo/bongo'))->getDescription()); 55 | $this->assertSame(['bingo/bongo'], (new Extension('bngbng'))->getTypes()); 56 | $this->assertSame('bingo/bongo', (new Extension('bngbng'))->getDefaultType()); 57 | 58 | // Adding an already existing mapping should not duplicate entries. 59 | $this->map->addTypeExtensionMapping('bingo/bongo', 'bngbng'); 60 | $this->assertSame(['bngbng'], (new Type('bingo/bongo'))->getExtensions()); 61 | $this->assertSame(['bingo/bongo'], (new Extension('bngbng'))->getTypes()); 62 | 63 | // Adding another extension to existing type. 64 | $this->map->addTypeExtensionMapping('bingo/bongo', 'bigbog'); 65 | $this->assertSame(['bngbng', 'bigbog'], (new Type('bingo/bongo'))->getExtensions()); 66 | $this->assertSame(['bingo/bongo'], (new Extension('bigbog'))->getTypes()); 67 | 68 | // Adding another type to existing extension. 69 | $this->map->addTypeExtensionMapping('boing/being', 'bngbng'); 70 | $this->assertSame(['bngbng'], (new Type('boing/being'))->getExtensions()); 71 | $this->assertSame(['bingo/bongo', 'boing/being'], (new Extension('bngbng'))->getTypes()); 72 | } 73 | 74 | public function testRemove(): void 75 | { 76 | $this->assertSame(['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'asc'], (new Type('text/plain'))->getExtensions()); 77 | $this->assertSame('txt', (new Type('text/plain'))->getDefaultExtension()); 78 | $this->assertSame(['text/plain'], (new Extension('txt'))->getTypes()); 79 | $this->assertSame('text/plain', (new Extension('txt'))->getDefaultType()); 80 | 81 | // Remove an existing type-extension pair. 82 | $this->assertTrue($this->map->removeTypeExtensionMapping('text/plain', 'txt')); 83 | $this->assertSame(['text', 'conf', 'def', 'list', 'log', 'in', 'asc'], (new Type('text/plain'))->getExtensions()); 84 | 85 | // Try removing a non-existing extension. 86 | $this->assertFalse($this->map->removeTypeExtensionMapping('text/plain', 'axx')); 87 | 88 | // Remove an existing alias. 89 | $this->assertSame(['application/x-pdf', 'image/pdf', 'application/acrobat', 'application/nappdf'], (new Type('application/pdf'))->getAliases()); 90 | $this->assertTrue($this->map->removeTypeAlias('application/pdf', 'application/x-pdf')); 91 | $this->assertSame(['image/pdf', 'application/acrobat', 'application/nappdf'], (new Type('application/pdf'))->getAliases()); 92 | 93 | // Try removing a non-existing alias. 94 | $this->assertFalse($this->map->removeTypeAlias('application/pdf', 'foo/bar')); 95 | $this->assertSame(['image/pdf', 'application/acrobat', 'application/nappdf'], (new Type('application/pdf'))->getAliases()); 96 | 97 | // Try removing a non-existing type. 98 | $this->assertFalse($this->map->removeType('axx/axx')); 99 | } 100 | 101 | public function testGetExtensionTypesAfterTypeExtensionMappingRemoval(): void 102 | { 103 | $this->expectException(MappingException::class); 104 | $this->expectExceptionMessage("No MIME type mapped to extension txt"); 105 | $this->assertTrue($this->map->removeTypeExtensionMapping('text/plain', 'txt')); 106 | $types = (new Extension('txt'))->getTypes(); 107 | } 108 | 109 | public function testGetExtensionDefaultTypeAfterTypeExtensionMappingRemoval(): void 110 | { 111 | $this->expectException(MappingException::class); 112 | $this->expectExceptionMessage("No MIME type mapped to extension txt"); 113 | $this->assertTrue($this->map->removeTypeExtensionMapping('text/plain', 'txt')); 114 | $defaultType = (new Extension('txt'))->getDefaultType(); 115 | } 116 | 117 | public function testGetTypeExtensionsAfterTypeRemoval(): void 118 | { 119 | $this->expectException(MappingException::class); 120 | $this->expectExceptionMessage("No MIME type found for text/plain in map"); 121 | $this->assertTrue($this->map->removeType('text/plain')); 122 | $extensions = (new Type('text/plain'))->getExtensions(); 123 | } 124 | 125 | public function testGetExtensionTypesAfterTypeRemoval(): void 126 | { 127 | $this->expectException(MappingException::class); 128 | $this->expectExceptionMessage('No MIME type mapped to extension def'); 129 | $this->assertTrue($this->map->removeType('text/plain')); 130 | $types = (new Extension('DEf'))->getTypes(); 131 | } 132 | 133 | public function testGetExtensionDefaultTypeAfterTypeRemoval(): void 134 | { 135 | $this->expectException(MappingException::class); 136 | $this->expectExceptionMessage('No MIME type mapped to extension def'); 137 | $this->assertTrue($this->map->removeType('text/plain')); 138 | $defaultType = (new Extension('DeF'))->getDefaultType(); 139 | } 140 | 141 | public function testGetTypeExtensionsAfterTypeWithAliasRemoval(): void 142 | { 143 | $this->expectException(MappingException::class); 144 | $this->expectExceptionMessage('No MIME type found for text/markdown in map'); 145 | $this->assertTrue($this->map->hasAlias('text/x-markdown')); 146 | $this->assertTrue($this->map->removeType('text/markdown')); 147 | $this->assertFalse($this->map->hasAlias('text/x-markdown')); 148 | $extensions = (new Type('text/markdown'))->getExtensions(); 149 | } 150 | 151 | public function testGetAliasExtensionsAfterTypeWithAliasRemoval(): void 152 | { 153 | $this->expectException(MappingException::class); 154 | $this->expectExceptionMessage("No MIME type found for text/x-markdown in map"); 155 | $this->assertTrue($this->map->hasAlias('text/x-markdown')); 156 | $this->assertTrue($this->map->removeType('text/markdown')); 157 | $this->assertFalse($this->map->hasAlias('text/x-markdown')); 158 | $extensions = (new Type('text/x-markdown'))->getExtensions(); 159 | } 160 | 161 | public function testGetExtensionTypesAfterTypeWithAliasRemoval(): void 162 | { 163 | $this->expectException(MappingException::class); 164 | $this->expectExceptionMessage("No MIME type mapped to extension lyx"); 165 | $this->assertTrue($this->map->hasAlias('text/x-lyx')); 166 | $this->assertTrue($this->map->removeType('application/x-lyx')); 167 | $this->assertFalse($this->map->hasAlias('text/x-lyx')); 168 | $types = (new Extension('LYX'))->getTypes(); 169 | } 170 | 171 | public function testGetExtensionDefaultTypeAfterTypeWithAliasRemoval(): void 172 | { 173 | $this->expectException(MappingException::class); 174 | $this->expectExceptionMessage("No MIME type mapped to extension lyx"); 175 | $this->assertTrue($this->map->hasAlias('text/x-lyx')); 176 | $this->assertTrue($this->map->removeType('application/x-lyx')); 177 | $this->assertFalse($this->map->hasAlias('text/x-lyx')); 178 | $defaultType = (new Extension('lyx'))->getDefaultType(); 179 | } 180 | 181 | public function testHasType(): void 182 | { 183 | $this->assertTrue($this->map->hasType('text/plain')); 184 | $this->assertFalse($this->map->hasType('foo/bar')); 185 | } 186 | 187 | public function testHasAlias(): void 188 | { 189 | $this->assertTrue($this->map->hasAlias('application/acrobat')); 190 | $this->assertFalse($this->map->hasAlias('foo/bar')); 191 | } 192 | 193 | public function testHasExtension(): void 194 | { 195 | $this->assertTrue($this->map->hasExtension('jpg')); 196 | $this->assertFalse($this->map->hasExtension('jpgjpg')); 197 | } 198 | 199 | public function testSetExtensionDefaultType(): void 200 | { 201 | $this->assertSame(['text/vnd.dvb.subtitle', 'image/vnd.dvb.subtitle', 'text/x-microdvd', 'text/x-mpsub', 'text/x-subviewer'], (new Extension('sub'))->getTypes()); 202 | $this->map->setExtensionDefaultType('SUB', 'image/vnd.dvb.subtitle'); 203 | $this->assertSame(['image/vnd.dvb.subtitle', 'text/vnd.dvb.subtitle', 'text/x-microdvd', 'text/x-mpsub', 'text/x-subviewer'], (new Extension('SUB'))->getTypes()); 204 | } 205 | 206 | public function testAddAliasToType(): void 207 | { 208 | $this->assertSame(['image/psd', 'image/x-psd', 'image/photoshop', 'image/x-photoshop', 'application/photoshop', 'application/x-photoshop',], (new Type('image/vnd.adobe.photoshop'))->getAliases()); 209 | $this->map->addTypeAlias('image/vnd.adobe.photoshop', 'application/x-foo-bar'); 210 | $this->assertSame(['image/psd', 'image/x-psd', 'image/photoshop', 'image/x-photoshop', 'application/photoshop', 'application/x-photoshop', 'application/x-foo-bar',], (new Type('image/vnd.adobe.photoshop'))->getAliases()); 211 | $this->assertContains('application/x-foo-bar', $this->map->listAliases()); 212 | } 213 | 214 | public function testReAddAliasToType(): void 215 | { 216 | $this->assertSame(['image/psd', 'image/x-psd', 'image/photoshop', 'image/x-photoshop', 'application/photoshop', 'application/x-photoshop',], (new Type('image/vnd.adobe.photoshop'))->getAliases()); 217 | $this->map->addTypeAlias('image/vnd.adobe.photoshop', 'application/x-photoshop'); 218 | $this->assertSame(['image/psd', 'image/x-psd', 'image/photoshop', 'image/x-photoshop', 'application/photoshop', 'application/x-photoshop',], (new Type('image/vnd.adobe.photoshop'))->getAliases()); 219 | } 220 | 221 | public function testAddAliasToMultipleTypes(): void 222 | { 223 | $this->assertSame([], (new Type('text/plain'))->getAliases()); 224 | $this->expectException(MappingException::class); 225 | $this->expectExceptionMessage("Cannot set 'application/x-photoshop' as alias for 'text/plain', it is an alias of 'image/vnd.adobe.photoshop' already"); 226 | $this->map->addTypeAlias('text/plain', 'application/x-photoshop'); 227 | $this->assertSame([], (new Type('text/plain'))->getAliases()); 228 | } 229 | 230 | public function testAddAliasToMissingType(): void 231 | { 232 | $this->expectException(MappingException::class); 233 | $this->expectExceptionMessage("Cannot set 'baz/qoo' as alias for 'bar/foo', 'bar/foo' not defined"); 234 | $this->map->addTypeAlias('bar/foo', 'baz/qoo'); 235 | } 236 | 237 | public function testAddAliasIsATypeAlready(): void 238 | { 239 | $this->expectException(MappingException::class); 240 | $this->expectExceptionMessage("Cannot set 'text/plain' as alias for 'text/richtext', 'text/plain' is already defined as a type"); 241 | $this->map->addTypeAlias('text/richtext', 'text/plain'); 242 | } 243 | 244 | public function testAddDescriptionToAlias(): void 245 | { 246 | $this->expectException(MappingException::class); 247 | $this->expectExceptionMessage("Cannot add description for 'application/acrobat', 'application/acrobat' is an alias"); 248 | $this->map->addTypeDescription('application/acrobat', 'description of alias'); 249 | } 250 | 251 | public function testSetExtensionDefaultTypeNoExtension(): void 252 | { 253 | $this->expectException(MappingException::class); 254 | $this->map->setExtensionDefaultType('zxzx', 'image/vnd.dvb.subtitle'); 255 | } 256 | 257 | public function testSetExtensionDefaultTypeNoType(): void 258 | { 259 | $this->expectException(MappingException::class); 260 | $this->map->setExtensionDefaultType('sub', 'image/bingo'); 261 | } 262 | 263 | /** 264 | * Check that a type alias can be set as extension default. 265 | */ 266 | public function testSetExtensionDefaultTypeToAlias(): void 267 | { 268 | $this->assertSame(['application/pdf'], (new Extension('pdf'))->getTypes()); 269 | 270 | $this->map->setExtensionDefaultType('pdf', 'application/x-pdf'); 271 | $this->assertSame(['application/x-pdf', 'application/pdf'], (new Extension('pdf'))->getTypes()); 272 | $this->assertSame('application/x-pdf', (new Extension('pdf'))->getDefaultType()); 273 | 274 | $this->map->setExtensionDefaultType('pdf', 'image/pdf'); 275 | $this->assertSame(['image/pdf', 'application/x-pdf', 'application/pdf'], (new Extension('pdf'))->getTypes()); 276 | $this->assertSame('image/pdf', (new Extension('pdf'))->getDefaultType()); 277 | 278 | // Remove the alias, should be removed from extension types. 279 | $this->assertTrue($this->map->removeTypeAlias('application/pdf', 'application/x-pdf')); 280 | $this->assertSame(['image/pdf', 'application/pdf'], (new Extension('pdf'))->getTypes()); 281 | $this->assertSame('image/pdf', (new Extension('pdf'))->getDefaultType()); 282 | 283 | // Add a fake MIME type to 'psd', an alias to that, then remove 284 | // 'image/vnd.adobe.photoshop'. 285 | $this->assertSame(['image/vnd.adobe.photoshop'], (new Extension('psd'))->getTypes()); 286 | $this->assertSame('image/vnd.adobe.photoshop', (new Extension('psd'))->getDefaultType()); 287 | $this->map->setExtensionDefaultType('psd', 'image/psd'); 288 | $this->assertSame(['image/psd', 'image/vnd.adobe.photoshop'], (new Extension('psd'))->getTypes()); 289 | $this->assertSame('image/psd', (new Extension('psd'))->getDefaultType()); 290 | $this->map->addTypeExtensionMapping('bingo/bongo', 'psd'); 291 | $this->assertSame(['image/psd', 'image/vnd.adobe.photoshop', 'bingo/bongo'], (new Extension('psd'))->getTypes()); 292 | $this->map->addTypeAlias('bingo/bongo', 'bar/foo'); 293 | $this->assertSame(['image/psd', 'image/vnd.adobe.photoshop', 'bingo/bongo'], (new Extension('psd'))->getTypes()); 294 | $this->map->setExtensionDefaultType('psd', 'bar/foo'); 295 | $this->assertSame(['bar/foo', 'image/psd', 'image/vnd.adobe.photoshop', 'bingo/bongo'], (new Extension('psd'))->getTypes()); 296 | $this->assertTrue($this->map->removeType('image/vnd.adobe.photoshop')); 297 | $this->assertSame(['bar/foo', 'bingo/bongo'], (new Extension('psd'))->getTypes()); 298 | } 299 | 300 | /** 301 | * Check removing an aliased type mapping. 302 | */ 303 | public function testRemoveAliasedTypeMapping(): void 304 | { 305 | $this->map->addTypeExtensionMapping('bingo/bongo', 'psd'); 306 | $this->assertSame(['image/vnd.adobe.photoshop', 'bingo/bongo'], (new Extension('psd'))->getTypes()); 307 | $this->map->addTypeAlias('bingo/bongo', 'bar/foo'); 308 | $this->assertSame(['image/vnd.adobe.photoshop', 'bingo/bongo'], (new Extension('psd'))->getTypes()); 309 | $this->map->setExtensionDefaultType('psd', 'bar/foo'); 310 | $this->assertSame(['bar/foo', 'image/vnd.adobe.photoshop', 'bingo/bongo'], (new Extension('psd'))->getTypes()); 311 | $this->map->removeTypeExtensionMapping('bar/foo', 'psd'); 312 | $this->assertSame(['image/vnd.adobe.photoshop', 'bingo/bongo'], (new Extension('psd'))->getTypes()); 313 | } 314 | 315 | /** 316 | * Check that a removing a type mapping also remove its aliases. 317 | */ 318 | public function testRemoveUnaliasedTypeMapping(): void 319 | { 320 | // Add a fake MIME type to 'psd', an alias to that, then remove 321 | // 'image/vnd.adobe.photoshop'. 322 | $this->map->addTypeExtensionMapping('bingo/bongo', 'psd'); 323 | $this->assertSame(['image/vnd.adobe.photoshop', 'bingo/bongo'], (new Extension('psd'))->getTypes()); 324 | $this->map->addTypeAlias('bingo/bongo', 'bar/foo'); 325 | $this->assertSame(['image/vnd.adobe.photoshop', 'bingo/bongo'], (new Extension('psd'))->getTypes()); 326 | $this->map->setExtensionDefaultType('psd', 'bar/foo'); 327 | $this->assertSame(['bar/foo', 'image/vnd.adobe.photoshop', 'bingo/bongo'], (new Extension('psd'))->getTypes()); 328 | $this->map->removeTypeExtensionMapping('bingo/bongo', 'psd'); 329 | $this->assertSame(['image/vnd.adobe.photoshop'], (new Extension('psd'))->getTypes()); 330 | } 331 | 332 | public function testSetExtensionDefaultTypeToInvalidAlias(): void 333 | { 334 | $this->expectException(MappingException::class); 335 | $this->expectExceptionMessage("Cannot set 'image/psd' as default type for extension 'pdf', its unaliased type 'image/vnd.adobe.photoshop' is not associated to 'pdf'"); 336 | $this->map->setExtensionDefaultType('pdf', 'image/psd'); 337 | } 338 | 339 | public function testSetTypeDefaultExtension(): void 340 | { 341 | $this->assertSame(['jpeg', 'jpg', 'jpe', 'jfif'], (new Type('image/jpeg'))->getExtensions()); 342 | $this->map->setTypeDefaultExtension('image/jpeg', 'jpg'); 343 | $this->assertSame(['jpg', 'jpeg', 'jpe', 'jfif'], (new Type('image/JPEG'))->getExtensions()); 344 | } 345 | 346 | public function testSetTypeDefaultExtensionNoExtension(): void 347 | { 348 | $this->expectException(MappingException::class); 349 | $this->map->setTypeDefaultExtension('image/jpeg', 'zxzx'); 350 | } 351 | 352 | public function testSetTypeDefaultExtensionNoType(): void 353 | { 354 | $this->expectException(MappingException::class); 355 | $this->map->setTypeDefaultExtension('image/bingo', 'jpg'); 356 | } 357 | 358 | public function testAddExtensionToAlias(): void 359 | { 360 | $this->expectException(MappingException::class); 361 | $this->expectExceptionMessage("Cannot map 'pdf' to 'application/acrobat', 'application/acrobat' is an alias"); 362 | $this->map->addTypeExtensionMapping('application/acrobat', 'pdf'); 363 | } 364 | 365 | /** 366 | * Data provider for testAddMalformedTypeExtensionMapping. 367 | * 368 | * @return array> 369 | */ 370 | public static function malformedTypeProvider(): array 371 | { 372 | return [ 373 | 'empty string' => [''], 374 | 'n' => ['n'], 375 | 'no media' => ['/n'], 376 | 'no sub type' => ['n/'], 377 | 'no comment closing bracket a' => ['image (open ()/*'], 378 | 'no comment closing bracket b' => ['image / * (open (())'], 379 | ]; 380 | } 381 | 382 | #[DataProvider('malformedTypeProvider')] 383 | public function testAddMalformedTypeExtensionMapping(string $type): void 384 | { 385 | $this->expectException(MalformedTypeException::class); 386 | $this->map->addTypeExtensionMapping($type, 'xxx'); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /tests/src/MapUpdaterTest.php: -------------------------------------------------------------------------------- 1 | updater = new MapUpdater(); 25 | $this->updater->selectBaseMap(MapUpdater::DEFAULT_BASE_MAP_CLASS); 26 | $this->newMap = $this->updater->getMap(); 27 | $this->assertInstanceOf(MapUpdater::DEFAULT_BASE_MAP_CLASS, $this->newMap); 28 | $this->fileSystem = new Filesystem(); 29 | } 30 | 31 | public function tearDown(): void 32 | { 33 | $this->assertInstanceOf(MapUpdater::DEFAULT_BASE_MAP_CLASS, $this->newMap); 34 | $this->newMap->reset(); 35 | } 36 | 37 | public function testLoadMapFromApacheFile(): void 38 | { 39 | $this->updater->loadMapFromApacheFile(dirname(__FILE__) . '/../fixtures/min.mime-types.txt'); 40 | $expected = [ 41 | 't' => [ 42 | 'image/jpeg' => ['e' => ['jpeg', 'jpg', 'jpe']], 43 | 'text/plain' => ['e' => ['txt']], 44 | ], 45 | 'e' => [ 46 | 'jpe' => ['t' => ['image/jpeg']], 47 | 'jpeg' => ['t' => ['image/jpeg']], 48 | 'jpg' => ['t' => ['image/jpeg']], 49 | 'txt' => ['t' => ['text/plain']], 50 | ], 51 | ]; 52 | // @phpstan-ignore method.impossibleType 53 | $this->assertSame($expected, $this->newMap->getMapArray()); 54 | $this->assertSame(['image/jpeg', 'text/plain'], $this->newMap->listTypes()); 55 | $this->assertSame(['jpe', 'jpeg', 'jpg', 'txt'], $this->newMap->listExtensions()); 56 | $this->assertSame([], $this->newMap->listAliases()); 57 | } 58 | 59 | public function testLoadMapFromApacheFileZeroLines(): void 60 | { 61 | $this->expectException(SourceUpdateException::class); 62 | $this->updater->loadMapFromApacheFile(dirname(__FILE__) . '/../fixtures/zero.mime-types.txt'); 63 | } 64 | 65 | public function testLoadMapFromApacheMissingFile(): void 66 | { 67 | $this->expectException(SourceUpdateException::class); 68 | $this->updater->loadMapFromApacheFile('certainly_missing.txt'); 69 | } 70 | 71 | public function testApplyOverridesFailure(): void 72 | { 73 | $this->updater->loadMapFromFreedesktopFile(dirname(__FILE__) . '/../fixtures/min.freedesktop.xml'); 74 | $errors = $this->updater->applyOverrides([['addTypeExtensionMapping', ['application/x-pdf', 'pdf']]]); 75 | $this->assertSame(["Cannot map 'pdf' to 'application/x-pdf', 'application/x-pdf' is an alias"], $errors); 76 | } 77 | 78 | public function testLoadMapFromFreedesktopFile(): void 79 | { 80 | $this->updater->applyOverrides([['addTypeExtensionMapping', ['application/x-pdf', 'pdf']]]); 81 | $errors = $this->updater->loadMapFromFreedesktopFile(dirname(__FILE__) . '/../fixtures/min.freedesktop.xml'); 82 | $this->assertSame(["Cannot set 'application/x-pdf' as alias for 'application/pdf', 'application/x-pdf' is already defined as a type"], $errors); 83 | $expected = [ 84 | 't' => [ 85 | 'application/pdf' => [ 86 | 'a' => ['image/pdf', 'application/acrobat', 'application/nappdf'], 87 | 'desc' => ['PDF document', 'PDF: Portable Document Format'], 88 | 'e' => ['pdf'] 89 | ], 90 | 'application/x-atari-2600-rom' => [ 91 | 'desc' => ['Atari 2600'], 92 | 'e' => ['a26'] 93 | ], 94 | 'application/x-pdf' => [ 95 | 'e' => ['pdf'] 96 | ], 97 | 'text/plain' => [ 98 | 'desc' => ['plain text document'], 99 | 'e' => ['txt', 'asc'] 100 | ], 101 | ], 102 | 'e' => [ 103 | 'a26' => ['t' => ['application/x-atari-2600-rom']], 104 | 'asc' => ['t' => ['text/plain']], 105 | 'pdf' => ['t' => ['application/x-pdf', 'application/pdf']], 106 | 'txt' => ['t' => ['text/plain']], 107 | ], 108 | 'a' => [ 109 | 'application/acrobat' => ['t' => ['application/pdf']], 110 | 'application/nappdf' => ['t' => ['application/pdf']], 111 | 'image/pdf' => ['t' => ['application/pdf']], 112 | ], 113 | ]; 114 | // @phpstan-ignore method.impossibleType 115 | $this->assertSame($expected, $this->newMap->getMapArray()); 116 | $this->assertSame(['application/pdf', 'application/x-atari-2600-rom', 'application/x-pdf', 'text/plain'], $this->newMap->listTypes()); 117 | $this->assertSame(['a26', 'asc', 'pdf', 'txt'], $this->newMap->listExtensions()); 118 | $this->assertSame(['application/acrobat', 'application/nappdf', 'image/pdf'], $this->newMap->listAliases()); 119 | } 120 | 121 | public function testLoadMapFromFreedesktopFileZeroLines(): void 122 | { 123 | $this->updater->loadMapFromFreedesktopFile(dirname(__FILE__) . '/../fixtures/zero.freedesktop.xml'); 124 | // @phpstan-ignore method.impossibleType 125 | $this->assertSame([], $this->newMap->getMapArray()); 126 | } 127 | 128 | public function testLoadMapFromFreedesktopMissingFile(): void 129 | { 130 | $this->expectException(SourceUpdateException::class); 131 | $this->updater->loadMapFromFreedesktopFile('certainly_missing.xml'); 132 | } 133 | 134 | public function testLoadMapFromFreedesktopInvalidFile(): void 135 | { 136 | $this->assertSame( 137 | ['Malformed XML in file ' . dirname(__FILE__) . '/../fixtures/invalid.freedesktop.xml'], 138 | $this->updater->loadMapFromFreedesktopFile(dirname(__FILE__) . '/../fixtures/invalid.freedesktop.xml') 139 | ); 140 | } 141 | 142 | public function testEmptyMapNotWriteable(): void 143 | { 144 | $this->expectException('LogicException'); 145 | $this->assertSame('', $this->newMap->getFileName()); 146 | } 147 | 148 | public function testWriteMapToPhpClassFile(): void 149 | { 150 | $this->fileSystem->copy(__DIR__ . '/../fixtures/MiniMap.php.test', __DIR__ . '/../fixtures/MiniMap.php'); 151 | include_once(__DIR__ . '/../fixtures/MiniMap.php'); 152 | // @phpstan-ignore class.notFound, argument.type 153 | MapHandler::setDefaultMapClass(MiniMap::class); 154 | $map_a = MapHandler::map(); 155 | $this->assertStringContainsString('fixtures/MiniMap.php', $map_a->getFileName()); 156 | $content = file_get_contents($map_a->getFileName()); 157 | assert(is_string($content)); 158 | $this->assertStringNotContainsString('text/plain', $content); 159 | $this->updater->loadMapFromApacheFile(dirname(__FILE__) . '/../fixtures/min.mime-types.txt'); 160 | $this->updater->applyOverrides([['addTypeExtensionMapping', ['bing/bong', 'binbon']]]); 161 | $this->updater->writeMapToPhpClassFile($map_a->getFileName()); 162 | $content = file_get_contents($map_a->getFileName()); 163 | assert(is_string($content)); 164 | $this->assertStringContainsString('text/plain', $content); 165 | $this->assertStringContainsString('bing/bong', $content); 166 | $this->assertStringContainsString('binbon', $content); 167 | $this->fileSystem->remove(__DIR__ . '/../fixtures/MiniMap.php'); 168 | } 169 | 170 | public function testWriteMapToPhpClassFileFailure(): void 171 | { 172 | $this->fileSystem->copy(__DIR__ . '/../fixtures/MiniMap.php.test', __DIR__ . '/../fixtures/MiniMap.php'); 173 | include_once(__DIR__ . '/../fixtures/MiniMap.php'); 174 | // @phpstan-ignore class.notFound, argument.type 175 | MapHandler::setDefaultMapClass(MiniMap::class); 176 | $map_a = MapHandler::map(); 177 | $this->assertStringContainsString('fixtures/MiniMap.php', $map_a->getFileName()); 178 | $content = file_get_contents($map_a->getFileName()); 179 | assert(is_string($content)); 180 | $this->assertStringNotContainsString('text/plain', $content); 181 | $this->expectException(\RuntimeException::class); 182 | $this->expectExceptionMessage("Failed loading file foo://bar.stub"); 183 | $this->updater->writeMapToPhpClassFile("foo://bar.stub"); 184 | } 185 | 186 | public function testGetDefaultMapBuildFile(): void 187 | { 188 | $this->assertStringContainsString('default_map_build.yml', MapUpdater::getDefaultMapBuildFile()); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /tests/src/MimeMapTestBase.php: -------------------------------------------------------------------------------- 1 | assertSame('a', $mt->getParameter('a')->getName()); 15 | $this->assertSame('parameter', $mt->getParameter('a')->getValue()); 16 | $this->assertTrue($mt->getParameter('a')->hasComment()); 17 | $this->assertSame('with a comment', $mt->getParameter('a')->getComment()); 18 | $mt = new Type('image/png;param=foo(with a comment)'); 19 | $this->assertSame('param', $mt->getParameter('param')->getName()); 20 | $this->assertSame('foo', $mt->getParameter('param')->getValue()); 21 | $this->assertTrue($mt->getParameter('param')->hasComment()); 22 | $this->assertSame('with a comment', $mt->getParameter('param')->getComment()); 23 | } 24 | 25 | public function testHasCommentNegative(): void 26 | { 27 | $mt = new Type('image/png; a="parameter"'); 28 | $this->assertSame('a', $mt->getParameter('a')->getName()); 29 | $this->assertSame('parameter', $mt->getParameter('a')->getValue()); 30 | $this->assertFalse($mt->getParameter('a')->hasComment()); 31 | $mt = new Type('image/png;foo=bar'); 32 | $this->assertSame('foo', $mt->getParameter('foo')->getName()); 33 | $this->assertSame('bar', $mt->getParameter('foo')->getValue()); 34 | $this->assertFalse($mt->getParameter('foo')->hasComment()); 35 | $this->expectException(UndefinedException::class); 36 | $this->expectExceptionMessage('Parameter comment is not defined'); 37 | $comment = $mt->getParameter('foo')->getComment(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/src/TypeTest.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public static function parseProvider(): array 20 | { 21 | return [ 22 | 'application/ogg;description=Hello there!;asd=fgh' => [ 23 | 'application/ogg;description=Hello there!;asd=fgh', 24 | [ 25 | 'application/ogg', 26 | 'application/ogg; description="Hello there!"; asd="fgh"', 27 | 'application/ogg; description="Hello there!"; asd="fgh"', 28 | ], 29 | ['application'], 30 | ['ogg'], 31 | true, 32 | [ 33 | 'description' => ['Hello there!'], 34 | 'asd' => ['fgh'], 35 | ], 36 | ], 37 | 'text/plain' => [ 38 | 'text/plain', 39 | [ 40 | 'text/plain', 41 | 'text/plain', 42 | 'text/plain', 43 | ], 44 | ['text'], 45 | ['plain'], 46 | false, 47 | [], 48 | ], 49 | 'text/plain;a=b' => [ 50 | 'text/plain;a=b', 51 | [ 52 | 'text/plain', 53 | 'text/plain; a="b"', 54 | 'text/plain; a="b"', 55 | ], 56 | ['text'], 57 | ['plain'], 58 | true, 59 | [ 60 | 'a' => ['b'], 61 | ], 62 | ], 63 | 'application/ogg' => [ 64 | 'application/ogg', 65 | [ 66 | 'application/ogg', 67 | 'application/ogg', 68 | 'application/ogg', 69 | ], 70 | ['application'], 71 | ['ogg'], 72 | false, 73 | [], 74 | ], 75 | '*/*' => [ 76 | '*/*', 77 | [ 78 | '*/*', 79 | '*/*', 80 | '*/*', 81 | ], 82 | ['*'], 83 | ['*'], 84 | false, 85 | [], 86 | ], 87 | 'n/n' => [ 88 | 'n/n', 89 | [ 90 | 'n/n', 91 | 'n/n', 92 | 'n/n', 93 | ], 94 | ['n'], 95 | ['n'], 96 | false, 97 | [], 98 | ], 99 | '(UTF-8 Plain Text) text / plain ; charset = utf-8' => [ 100 | '(UTF-8 Plain Text) text / plain ; charset = utf-8', 101 | [ 102 | 'text/plain', 103 | 'text/plain; charset="utf-8"', 104 | 'text (UTF-8 Plain Text)/plain; charset="utf-8"', 105 | ], 106 | ['text', 'UTF-8 Plain Text'], 107 | ['plain'], 108 | true, 109 | [ 110 | 'charset' => ['utf-8'], 111 | ], 112 | ], 113 | 'text (Text) / plain ; charset = utf-8' => [ 114 | 'text (Text) / plain ; charset = utf-8', 115 | [ 116 | 'text/plain', 117 | 'text/plain; charset="utf-8"', 118 | 'text (Text)/plain; charset="utf-8"', 119 | ], 120 | ['text', 'Text'], 121 | ['plain'], 122 | true, 123 | [ 124 | 'charset' => ['utf-8'], 125 | ], 126 | ], 127 | 'text / (Plain) plain ; charset = utf-8' => [ 128 | 'text / (Plain) plain ; charset = utf-8', 129 | [ 130 | 'text/plain', 131 | 'text/plain; charset="utf-8"', 132 | 'text/plain (Plain); charset="utf-8"', 133 | ], 134 | ['text'], 135 | ['plain', 'Plain'], 136 | true, 137 | [ 138 | 'charset' => ['utf-8'], 139 | ], 140 | ], 141 | 'text / plain (Plain Text) ; charset = utf-8' => [ 142 | 'text / plain (Plain Text) ; charset = utf-8', 143 | [ 144 | 'text/plain', 145 | 'text/plain; charset="utf-8"', 146 | 'text/plain (Plain Text); charset="utf-8"', 147 | ], 148 | ['text'], 149 | ['plain', 'Plain Text'], 150 | true, 151 | [ 152 | 'charset' => ['utf-8'], 153 | ], 154 | ], 155 | 'text / plain ; (Charset=utf-8) charset = utf-8' => [ 156 | 'text / plain ; (Charset=utf-8) charset = utf-8', 157 | [ 158 | 'text/plain', 159 | 'text/plain; charset="utf-8"', 160 | 'text/plain; charset="utf-8" (Charset=utf-8)', 161 | ], 162 | ['text'], 163 | ['plain'], 164 | true, 165 | [ 166 | 'charset' => ['utf-8', 'Charset=utf-8'], 167 | ], 168 | ], 169 | 'text / plain ; charset (Charset) = utf-8' => [ 170 | 'text / plain ; charset (Charset) = utf-8', 171 | [ 172 | 'text/plain', 173 | 'text/plain; charset="utf-8"', 174 | 'text/plain; charset="utf-8" (Charset)', 175 | ], 176 | ['text'], 177 | ['plain'], 178 | true, 179 | [ 180 | 'charset' => ['utf-8', 'Charset'], 181 | ], 182 | ], 183 | 'text / plain ; charset = (UTF8) utf-8' => [ 184 | 'text / plain ; charset = (UTF8) utf-8', 185 | [ 186 | 'text/plain', 187 | 'text/plain; charset="utf-8"', 188 | 'text/plain; charset="utf-8" (UTF8)', 189 | ], 190 | ['text'], 191 | ['plain'], 192 | true, 193 | [ 194 | 'charset' => ['utf-8', 'UTF8'], 195 | ], 196 | ], 197 | 'text / plain ; charset = utf-8 (UTF-8 Plain Text)' => [ 198 | 'text / plain ; charset = utf-8 (UTF-8 Plain Text)', 199 | [ 200 | 'text/plain', 201 | 'text/plain; charset="utf-8"', 202 | 'text/plain; charset="utf-8" (UTF-8 Plain Text)', 203 | ], 204 | ['text'], 205 | ['plain'], 206 | true, 207 | [ 208 | 'charset' => ['utf-8', 'UTF-8 Plain Text'], 209 | ], 210 | ], 211 | 'application/x-foobar;description="bbgh(kdur"' => [ 212 | 'application/x-foobar;description="bbgh(kdur"', 213 | [ 214 | 'application/x-foobar', 215 | 'application/x-foobar; description="bbgh(kdur"', 216 | 'application/x-foobar; description="bbgh(kdur"', 217 | ], 218 | ['application'], 219 | ['x-foobar'], 220 | true, 221 | [ 222 | 'description' => ['bbgh(kdur'], 223 | ], 224 | ], 225 | 'application/x-foobar;description="a \"quoted string\""' => [ 226 | 'application/x-foobar;description="a \"quoted string\""', 227 | [ 228 | 'application/x-foobar', 229 | 'application/x-foobar; description="a \"quoted string\""', 230 | 'application/x-foobar; description="a \"quoted string\""', 231 | ], 232 | ['application'], 233 | ['x-foobar'], 234 | true, 235 | [ 236 | 'description' => ['a "quoted string"'], 237 | ], 238 | ], 239 | 'text/xml;description=test' => [ 240 | 'text/xml;description=test', 241 | [ 242 | 'text/xml', 243 | 'text/xml; description="test"', 244 | 'text/xml; description="test"', 245 | ], 246 | ['text'], 247 | ['xml'], 248 | true, 249 | [ 250 | 'description' => ['test'], 251 | ], 252 | ], 253 | 'text/xml;one=test;two=three' => [ 254 | 'text/xml;one=test;two=three', 255 | [ 256 | 'text/xml', 257 | 'text/xml; one="test"; two="three"', 258 | 'text/xml; one="test"; two="three"', 259 | ], 260 | ['text'], 261 | ['xml'], 262 | true, 263 | [ 264 | 'one' => ['test'], 265 | 'two' => ['three'], 266 | ], 267 | ], 268 | 'text/xml;one="test";two="three"' => [ 269 | 'text/xml;one="test";two="three"', 270 | [ 271 | 'text/xml', 272 | 'text/xml; one="test"; two="three"', 273 | 'text/xml; one="test"; two="three"', 274 | ], 275 | ['text'], 276 | ['xml'], 277 | true, 278 | [ 279 | 'one' => ['test'], 280 | 'two' => ['three'], 281 | ], 282 | ], 283 | 'text/xml; this="is"; a="parameter" (with a comment)' => [ 284 | 'text/xml; this="is"; a="parameter" (with a comment)', 285 | [ 286 | 'text/xml', 287 | 'text/xml; this="is"; a="parameter"', 288 | 'text/xml; this="is"; a="parameter" (with a comment)', 289 | ], 290 | ['text'], 291 | ['xml'], 292 | true, 293 | [ 294 | 'this' => ['is'], 295 | 'a' => ['parameter', 'with a comment'], 296 | ], 297 | ], 298 | // Various edge cases. 299 | 'text/plain; charset="utf-8" (UTF/8)' => [ 300 | 'text/plain; charset="utf-8" (UTF/8)', 301 | [ 302 | 'text/plain', 303 | 'text/plain; charset="utf-8"', 304 | 'text/plain; charset="utf-8" (UTF/8)', 305 | ], 306 | ['text'], 307 | ['plain'], 308 | true, 309 | [ 310 | 'charset' => ['utf-8', 'UTF/8'], 311 | ], 312 | ], 313 | 'appf/xml; a=b; b="parameter" (with; a comment) ;c=d; e=f (;) ; g=h ' => [ 314 | 'appf/xml; a=b; b="parameter" (with; a comment) ;c=d; e=f (;) ; g=h ', 315 | [ 316 | 'appf/xml', 317 | 'appf/xml; a="b"; b="parameter"; c="d"; e="f"; g="h"', 318 | 'appf/xml; a="b"; b="parameter" (with; a comment); c="d"; e="f" (;); g="h"', 319 | ], 320 | ['appf'], 321 | ['xml'], 322 | true, 323 | [ 324 | 'a' => ['b'], 325 | 'b' => ['parameter', 'with; a comment'], 326 | 'c' => ['d'], 327 | 'e' => ['f', ';'], 328 | 'g' => ['h'], 329 | ], 330 | ], 331 | 'text/(abc)def(ghi)' => [ 332 | 'text/(abc)def(ghi)', 333 | [ 334 | 'text/def', 335 | 'text/def', 336 | 'text/def (abc ghi)', 337 | ], 338 | ['text'], 339 | ['def', 'abc ghi'], 340 | false, 341 | [], 342 | ], 343 | 'text/(abc)def' => [ 344 | 'text/(abc)def', 345 | [ 346 | 'text/def', 347 | 'text/def', 348 | 'text/def (abc)', 349 | ], 350 | ['text'], 351 | ['def', 'abc'], 352 | false, 353 | [], 354 | ], 355 | 'text/def(ghi)' => [ 356 | 'text/def(ghi)', 357 | [ 358 | 'text/def', 359 | 'text/def', 360 | 'text/def (ghi)', 361 | ], 362 | ['text'], 363 | ['def', 'ghi'], 364 | false, 365 | [], 366 | ], 367 | 'text/plain;a=(\)abc)def(\()' => [ 368 | 'text/plain;a=(\)abc)def(\()', 369 | [ 370 | 'text/plain', 371 | 'text/plain; a="def"', 372 | 'text/plain; a="def" (\)abc \()', 373 | ], 374 | ['text'], 375 | ['plain'], 376 | true, 377 | [ 378 | 'a' => ['def', '\)abc \('], 379 | ], 380 | ], 381 | 'text/plain;a=\\foo(abc)' => [ 382 | 'text/plain;a=\\foo(abc)', 383 | [ 384 | 'text/plain', 385 | 'text/plain; a="foo"', 386 | 'text/plain; a="foo" (abc)', 387 | ], 388 | ['text'], 389 | ['plain'], 390 | true, 391 | [ 392 | 'a' => ['foo', 'abc'], 393 | ], 394 | ], 395 | 'text/plain;a=(a"bc\)def")def' => [ 396 | 'text/plain;a=(a"bc\)def")def', 397 | [ 398 | 'text/plain', 399 | 'text/plain; a="def"', 400 | 'text/plain; a="def" (a"bc\)def")', 401 | ], 402 | ['text'], 403 | ['plain'], 404 | true, 405 | [ 406 | 'a' => ['def', 'a"bc\)def"'], 407 | ], 408 | ], 409 | 'text/plain;a="(abc)def"' => [ 410 | 'text/plain;a="(abc)def"', 411 | [ 412 | 'text/plain', 413 | 'text/plain; a="(abc)def"', 414 | 'text/plain; a="(abc)def"', 415 | ], 416 | ['text'], 417 | ['plain'], 418 | true, 419 | [ 420 | 'a' => ['(abc)def'], 421 | ], 422 | ], 423 | ]; 424 | } 425 | 426 | /** 427 | * @param string $type 428 | * @param string[] $expectedToString 429 | * @param string[] $expectedMedia 430 | * @param string[] $expectedSubType 431 | * @param bool $expectedHasParameters 432 | * @param string[] $expectedParameters 433 | */ 434 | #[DataProvider('parseProvider')] 435 | public function testParse(string $type, array $expectedToString, array $expectedMedia, array $expectedSubType, bool $expectedHasParameters, array $expectedParameters): void 436 | { 437 | $mt = new Type($type); 438 | $this->assertSame($expectedMedia[0], $mt->getMedia()); 439 | if (isset($expectedMedia[1])) { 440 | $this->assertTrue($mt->hasMediaComment()); 441 | $this->assertSame($expectedMedia[1], $mt->getMediaComment()); 442 | } else { 443 | $this->assertFalse($mt->hasMediaComment()); 444 | } 445 | $this->assertSame($expectedSubType[0], $mt->getSubType()); 446 | if (isset($expectedSubType[1])) { 447 | $this->assertTrue($mt->hasSubTypeComment()); 448 | $this->assertSame($expectedSubType[1], $mt->getSubTypeComment()); 449 | } else { 450 | $this->assertFalse($mt->hasSubTypeComment()); 451 | } 452 | $this->assertSame($expectedHasParameters, $mt->hasParameters()); 453 | if ($expectedHasParameters) { 454 | $this->assertSameSize($expectedParameters, $mt->getParameters()); 455 | } 456 | foreach ($expectedParameters as $name => $param) { 457 | $this->assertTrue(isset($mt->getParameters()[$name])); 458 | $this->assertSame($name, $mt->getParameter($name)->getName()); 459 | $this->assertSame($param[0], $mt->getParameter($name)->getValue()); 460 | if (isset($param[1])) { 461 | $this->assertTrue($mt->getParameter($name)->hasComment()); 462 | $this->assertSame($param[1], $mt->getParameter($name)->getComment()); 463 | } else { 464 | $this->assertFalse($mt->getParameter($name)->hasComment()); 465 | } 466 | } 467 | $this->assertSame($expectedToString[0], $mt->toString(Type::SHORT_TEXT)); 468 | $this->assertSame($expectedToString[1], $mt->toString(Type::FULL_TEXT)); 469 | $this->assertSame($expectedToString[2], $mt->toString(Type::FULL_TEXT_WITH_COMMENTS)); 470 | } 471 | 472 | /** 473 | * Data provider for testParseMalformed. 474 | * 475 | * @return array> 476 | */ 477 | public static function parseMalformedProvider(): array 478 | { 479 | return [ 480 | 'empty string' => [''], 481 | 'n' => ['n'], 482 | 'no media' => ['/n'], 483 | 'no sub type' => ['n/'], 484 | 'no comment closing bracket a' => ['image (open ()/*'], 485 | 'no comment closing bracket b' => ['image / * (open (())'], 486 | ]; 487 | } 488 | 489 | #[DataProvider('parseMalformedProvider')] 490 | public function testParseMalformed(string $type): void 491 | { 492 | $this->expectException(MalformedTypeException::class); 493 | new Type($type); 494 | } 495 | 496 | public function testParseAgain(): void 497 | { 498 | $mt = new Type('application/ogg;description=Hello there!;asd=fgh'); 499 | $this->assertCount(2, $mt->getParameters()); 500 | 501 | $mt = new Type('text/plain;hello=there!'); 502 | $this->assertCount(1, $mt->getParameters()); 503 | } 504 | 505 | public function testGetDescription(): void 506 | { 507 | $this->assertSame('HTML document', (new Type('text/html'))->getDescription()); 508 | $this->assertSame('HTML document, HTML: HyperText Markup Language', (new Type('text/html'))->getDescription(true)); 509 | 510 | $this->assertSame('GPX geographic data', (new Type('application/gpx+xml'))->getDescription()); 511 | $this->assertSame('GPX geographic data, GPX: GPS Exchange Format', (new Type('application/gpx+xml'))->getDescription(true)); 512 | $this->assertSame('GPX geographic data', (new Type('application/gpx'))->getDescription()); 513 | $this->assertSame('GPX geographic data, GPX: GPS Exchange Format', (new Type('application/gpx'))->getDescription(true)); 514 | $this->assertSame('GPX geographic data', (new Type('application/x-gpx'))->getDescription()); 515 | $this->assertSame('GPX geographic data, GPX: GPS Exchange Format', (new Type('application/x-gpx'))->getDescription(true)); 516 | } 517 | 518 | /** 519 | * Data provider for testMissingDescription. 520 | * 521 | * @return array> 522 | */ 523 | public static function missingDescriptionProvider(): array 524 | { 525 | return [ 526 | ['*/*'], 527 | ['image/*'], 528 | ['application/java*'], 529 | ['application/x-t3vm-image'], 530 | ]; 531 | } 532 | 533 | #[DataProvider('missingDescriptionProvider')] 534 | public function testMissingDescription(string $type): void 535 | { 536 | $t = new Type($type); 537 | $this->assertFalse($t->hasDescription()); 538 | $this->expectException(MappingException::class); 539 | $this->expectExceptionMessage('No description available for type: ' . $type); 540 | $desc = $t->getDescription(); 541 | } 542 | 543 | public function testMissingMediaComment(): void 544 | { 545 | $t = new Type('text/plain'); 546 | $this->assertFalse($t->hasMediaComment()); 547 | $this->expectException(UndefinedException::class); 548 | $this->expectExceptionMessage('Media comment is not defined'); 549 | $comment = $t->getMediaComment(); 550 | } 551 | 552 | public function testMissingSubTypeComment(): void 553 | { 554 | $t = new Type('text/plain'); 555 | $this->assertFalse($t->hasSubTypeComment()); 556 | $this->expectException(UndefinedException::class); 557 | $this->expectExceptionMessage('Subtype comment is not defined'); 558 | $comment = $t->getSubTypeComment(); 559 | } 560 | 561 | public function testMissingParameters(): void 562 | { 563 | $t = new Type('text/plain'); 564 | $this->assertFalse($t->hasParameters()); 565 | $this->expectException(UndefinedException::class); 566 | $this->expectExceptionMessage('No parameters defined'); 567 | $parameters = $t->getParameters(); 568 | } 569 | 570 | public function testMissingParameter(): void 571 | { 572 | $t = new Type('text/plain'); 573 | $this->assertFalse($t->hasParameter('foo')); 574 | $this->expectException(UndefinedException::class); 575 | $this->expectExceptionMessage('Parameter foo is not defined'); 576 | $parameters = $t->getParameter('foo'); 577 | } 578 | 579 | public function testSetComment(): void 580 | { 581 | $type = new Type('text/x-test'); 582 | $type->setMediaComment('media comment'); 583 | $this->assertSame('text (media comment)/x-test', $type->toString(Type::FULL_TEXT_WITH_COMMENTS)); 584 | $type->setSubTypeComment('subtype comment'); 585 | $this->assertSame('text (media comment)/x-test (subtype comment)', $type->toString(Type::FULL_TEXT_WITH_COMMENTS)); 586 | $type->setMediaComment(); 587 | $this->assertSame('text/x-test (subtype comment)', $type->toString(Type::FULL_TEXT_WITH_COMMENTS)); 588 | $type->setSubTypeComment(); 589 | $this->assertSame('text/x-test', $type->toString(Type::FULL_TEXT_WITH_COMMENTS)); 590 | } 591 | 592 | public function testIsExperimental(): void 593 | { 594 | $this->assertTrue((new Type('text/x-test'))->isExperimental()); 595 | $this->assertTrue((new Type('image/X-test'))->isExperimental()); 596 | $this->assertFalse((new Type('text/plain'))->isExperimental()); 597 | } 598 | 599 | public function testIsVendor(): void 600 | { 601 | $this->assertTrue((new Type('application/vnd.openoffice'))->isVendor()); 602 | $this->assertFalse((new Type('application/vendor.openoffice'))->isVendor()); 603 | $this->assertFalse((new Type('vnd/fsck'))->isVendor()); 604 | } 605 | 606 | public function testIsWildcard(): void 607 | { 608 | $this->assertTrue((new Type('*/*'))->isWildcard()); 609 | $this->assertTrue((new Type('image/*'))->isWildcard()); 610 | $this->assertFalse((new Type('text/plain'))->isWildcard()); 611 | 612 | $this->assertTrue((new Type('application/java*'))->isWildcard()); 613 | $this->assertTrue((new Type('application/java-*'))->isWildcard()); 614 | } 615 | 616 | public function testIsAlias(): void 617 | { 618 | $this->assertFalse((new Type('*/*'))->isAlias()); 619 | $this->assertFalse((new Type('image/*'))->isAlias()); 620 | $this->assertFalse((new Type('text/plain'))->isAlias()); 621 | $this->assertFalse((new Type('application/java*'))->isAlias()); 622 | $this->assertTrue((new Type('text/x-markdown'))->isAlias()); 623 | } 624 | 625 | public function testWildcardMatch(): void 626 | { 627 | $this->assertTrue((new Type('image/png'))->wildcardMatch('*/*')); 628 | $this->assertTrue((new Type('image/png'))->wildcardMatch('image/*')); 629 | $this->assertFalse((new Type('text/plain'))->wildcardMatch('image/*')); 630 | $this->assertFalse((new Type('image/png'))->wildcardMatch('image/foo')); 631 | 632 | $this->assertTrue((new Type('application/javascript'))->wildcardMatch('application/java*')); 633 | $this->assertTrue((new Type('application/java-serialized-object'))->wildcardMatch('application/java-*')); 634 | $this->assertFalse((new Type('application/javascript'))->wildcardMatch('application/java-*')); 635 | } 636 | 637 | public function testAddParameter(): void 638 | { 639 | $mt = new Type('image/png; foo=bar'); 640 | $mt->addParameter('baz', 'val', 'this is a comment'); 641 | $res = $mt->toString(Type::FULL_TEXT_WITH_COMMENTS); 642 | $this->assertStringContainsString('foo=', $res); 643 | $this->assertStringContainsString('bar', $res); 644 | $this->assertStringContainsString('baz=', $res); 645 | $this->assertStringContainsString('val', $res); 646 | $this->assertStringContainsString('(this is a comment)', $res); 647 | $this->assertSame('image/png; foo="bar"; baz="val" (this is a comment)', $res); 648 | } 649 | 650 | public function testRemoveParameter(): void 651 | { 652 | $mt = new Type('image/png; foo=bar;baz=val(this is a comment)'); 653 | $mt->removeParameter('foo'); 654 | $res = $mt->toString(Type::FULL_TEXT_WITH_COMMENTS); 655 | $this->assertStringNotContainsString('foo=', $res); 656 | $this->assertStringNotContainsString('bar', $res); 657 | $this->assertStringContainsString('baz=', $res); 658 | $this->assertSame('image/png; baz="val" (this is a comment)', $res); 659 | } 660 | 661 | public function testGetAliases(): void 662 | { 663 | $this->assertSame(['image/x-wmf', 'image/x-win-metafile', 'application/x-wmf', 'application/wmf', 'application/x-msmetafile'], (new Type('image/wmf'))->getAliases()); 664 | } 665 | 666 | public function testGetAliasesOnAlias(): void 667 | { 668 | $this->expectException(MappingException::class); 669 | $this->expectExceptionMessage("Cannot get aliases for 'image/x-wmf', it is an alias itself"); 670 | $this->assertSame([], (new Type('image/x-wmf'))->getAliases()); 671 | } 672 | 673 | public function testGetAliasesOnMissingType(): void 674 | { 675 | $this->expectException(MappingException::class); 676 | $this->expectExceptionMessage("No MIME type found for foo/bar in map"); 677 | $this->assertSame([], (new Type('foo/bar'))->getAliases()); 678 | } 679 | 680 | public function testGetExtensions(): void 681 | { 682 | $this->assertEquals(['atom'], (new Type('application/atom+xml'))->getExtensions()); 683 | $this->assertEquals(['pgp', 'gpg', 'asc', 'skr', 'pkr', 'key', 'sig'], (new Type('application/pgp*'))->getExtensions()); 684 | $this->assertEquals(['asc', 'sig', 'pgp', 'gpg'], (new Type('application/pgp-s*'))->getExtensions()); 685 | 686 | $this->assertEquals(['123', 'wk1', 'wk3', 'wk4', 'wks'], (new Type('application/vnd.lotus-1-2-3'))->getExtensions()); 687 | $this->assertEquals(['602'], (new Type('application/x-t602'))->getExtensions()); 688 | 689 | $this->assertSame(['smi', 'smil', 'sml', 'kino'], (new Type('application/smil+xml'))->getExtensions()); 690 | $this->assertSame(['smi', 'smil', 'sml', 'kino'], (new Type('application/smil'))->getExtensions()); 691 | } 692 | 693 | public function testGetExtensionsFail(): void 694 | { 695 | $this->expectException(MappingException::class); 696 | $this->expectExceptionMessage("No MIME type found for application/a000 in map"); 697 | $extensions = (new Type('application/a000'))->getExtensions(); 698 | } 699 | 700 | public function testGetDefaultExtension(): void 701 | { 702 | $this->assertEquals('atom', (new Type('application/atom+xml'))->getDefaultExtension()); 703 | $this->assertEquals('csv', (new Type('text/csv'))->getDefaultExtension()); 704 | 705 | $this->assertSame('smi', (new Type('application/smil+xml'))->getDefaultExtension()); 706 | $this->assertSame('smi', (new Type('application/smil'))->getDefaultExtension()); 707 | } 708 | 709 | /** 710 | * Data provider for testGetDefaultExtensionFail. 711 | * 712 | * @return array> 713 | */ 714 | public static function getDefaultExtensionFailProvider() 715 | { 716 | return [ 717 | ['*/*'], 718 | ['n/n'], 719 | ['image/*'], 720 | ['application/pgp*'], 721 | ]; 722 | } 723 | 724 | #[DataProvider('getDefaultExtensionFailProvider')] 725 | public function testGetDefaultExtensionFail(string $type): void 726 | { 727 | $this->expectException(MappingException::class); 728 | $this->assertSame('', (new Type($type))->getDefaultExtension()); 729 | } 730 | } 731 | --------------------------------------------------------------------------------