├── .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 | [](https://github.com/FileEye/MimeMap/actions/workflows/php-unit.yml)
4 | [](https://github.com/FileEye/MimeMap/actions/workflows/code-quality.yml)
5 | [](https://codecov.io/gh/FileEye/MimeMap)
6 | [](https://packagist.org/packages/fileeye/mimemap)
7 | [](https://packagist.org/packages/fileeye/mimemap)
8 | [](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 |
--------------------------------------------------------------------------------