Skip to content

Commit 4de509f

Browse files
author
Ben Ramsey
committed
Merge branch 'master' of github.com:ramsey/mimeparse
* 'master' of github.com:ramsey/mimeparse: More accurately differentiate between media-ranges and mime-types. Minor comment improvements. Wrap comments at 80 characters wherever possible. Make difference between parseMediaRange and parseMimeType more clear. Clarify exception message. Improve and correct some documentation. Use order of $supported array instead of $tieBreaker to break ties. Fix return value documentation for qualityAndFitnessParsed. Simplify process of rejecting unacceptable types. Rename fitnessAndQualityParsed to represent order of returned array. Use the term "generic subtype" instead of "format". Make some comments more PHP-centric.
2 parents 8c53c4f + 563b9cd commit 4de509f

File tree

2 files changed

+103
-136
lines changed

2 files changed

+103
-136
lines changed

src/Bitworking/Mimeparse.php

Lines changed: 80 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<?php
22
/**
3-
* Mimeparse class. This class provides basic functions for handling mime-types. It can
4-
* handle matching mime-types against a list of media-ranges. See section
5-
* 14.1 of the HTTP specification [RFC 2616] for a complete explanation.
3+
* Mimeparse class. Provides basic functions for handling mime-types. It can
4+
* match mime-types against a list of media-ranges. See section 14.1 of the
5+
* HTTP specification [RFC 2616] for a complete explanation.
66
*
7-
* It's just a port to php from original Python code (http://code.google.com/p/mimeparse/).
7+
* It's a PHP port of the original Python code
8+
* (http://code.google.com/p/mimeparse/).
89
*
910
* Ported from version 0.1.2. Comments are mostly excerpted from the original.
1011
*
@@ -17,27 +18,30 @@
1718
class Mimeparse
1819
{
1920
/**
20-
* Parses a mime-type and returns an array with its components.
21+
* Parses a media-range and returns an array with its components.
2122
*
22-
* The array returned contains:
23+
* The returned array contains:
2324
*
2425
* 1. type: The type categorization.
2526
* 2. subtype: The subtype categorization.
26-
* 3. params: A hash of all the parameters for the media range.
27-
* 4. format: The content format.
27+
* 3. params: An associative array of all the parameters for the
28+
* media-range.
29+
* 4. generic subtype: A more generic subtype, if one is present. See
30+
* http://tools.ietf.org/html/rfc3023#appendix-A.12
2831
*
29-
* For example, the media range "application/xhtml+xml;q=0.5" would
30-
* get parsed into:
32+
* For example, the media-range "application/xhtml+xml;q=0.5" would get
33+
* parsed into:
3134
*
32-
* array("application", "xhtml", array( "q" => "0.5" ), "xml")
35+
* array("application", "xhtml+xml", array( "q" => "0.5" ), "xml")
3336
*
34-
* @param string $mimeType
35-
* @return array ($type, $subtype, $params)
36-
* @throws UnexpectedValueException when $mimeType does not include a valid subtype
37+
* @param string $mediaRange
38+
* @return array ($type, $subtype, $params, $genericSubtype)
39+
* @throws UnexpectedValueException when $mediaRange does not include a
40+
* valid subtype
3741
*/
38-
public static function parseMimeType($mimeType)
42+
public static function parseMediaRange($mediaRange)
3943
{
40-
$parts = explode(';', $mimeType);
44+
$parts = explode(';', $mediaRange);
4145

4246
$params = array();
4347
foreach ($parts as $i => $param) {
@@ -49,78 +53,72 @@ public static function parseMimeType($mimeType)
4953

5054
$fullType = trim($parts[0]);
5155

52-
// Java URLConnection class sends an Accept header that includes a single "*"
53-
// Turn it into a legal wildcard.
56+
// Java URLConnection class sends an Accept header that includes a
57+
// single "*". Turn it into a legal wildcard.
5458
if ($fullType == '*') {
5559
$fullType = '*/*';
5660
}
5761

5862
list($type, $subtype) = explode('/', $fullType);
5963

6064
if (!$subtype) {
61-
throw new \UnexpectedValueException('malformed mime type');
65+
throw new \UnexpectedValueException('Malformed media-range: '.$mediaRange);
6266
}
6367

64-
if (false !== strpos($subtype, '+')) {
65-
// don't rewrite subtype to prevent compatibility issues
66-
list(/*$subtype*/, $format) = explode('+', $subtype, 2);
68+
$plusPos = strpos($subtype, '+');
69+
if (false !== $plusPos) {
70+
$genericSubtype = substr($subtype, $plusPos + 1);
6771
} else {
68-
$format = $subtype;
72+
$genericSubtype = $subtype;
6973
}
7074

71-
return array(trim($type), trim($subtype), $params, $format);
75+
return array(trim($type), trim($subtype), $params, $genericSubtype);
7276
}
7377

7478

7579
/**
76-
* Carves up a media range and returns an Array of the
77-
* [type, subtype, params] where "params" is a Hash of all
78-
* the parameters for the media range.
79-
*
80-
* For example, the media range "application/*;q=0.5" would
81-
* get parsed into:
82-
*
83-
* array("application", "*", ( "q", "0.5" ))
80+
* Parses a media-range via Mimeparse::parseMediaRange() and guarantees that
81+
* there is a value for the "q" param, filling it in with a proper default
82+
* if necessary.
8483
*
85-
* In addition this function also guarantees that there
86-
* is a value for "q" in the params dictionary, filling it
87-
* in with a proper default if necessary.
88-
*
89-
* @param string $range
90-
* @return array ($type, $subtype, $params)
84+
* @param string $mediaRange
85+
* @return array ($type, $subtype, $params, $genericSubtype)
9186
*/
92-
protected static function parseMediaRange($range)
87+
protected static function parseAndNormalizeMediaRange($mediaRange)
9388
{
94-
list($type, $subtype, $params) = self::parseMimeType($range);
89+
$parsedMediaRange = self::parseMediaRange($mediaRange);
90+
$params = $parsedMediaRange[2];
9591

9692
if (!isset($params['q'])
9793
|| !is_numeric($params['q'])
9894
|| floatval($params['q']) > 1
9995
|| floatval($params['q']) < 0
10096
) {
101-
$params['q'] = '1';
97+
$parsedMediaRange[2]['q'] = '1';
10298
}
10399

104-
return array($type, $subtype, $params);
100+
return $parsedMediaRange;
105101
}
106102

107103
/**
108104
* Find the best match for a given mime-type against a list of
109-
* media-ranges that have already been parsed by Mimeparse::parseMediaRange()
105+
* media-ranges that have already been parsed by
106+
* Mimeparse::parseAndNormalizeMediaRange()
110107
*
111-
* Returns the fitness and the "q" quality parameter of the best match, or an
112-
* array [-1, 0] if no match was found. Just as for Mimeparse::quality(),
113-
* $parsedRanges must be an Enumerable of parsed media-ranges.
108+
* Returns the fitness and the "q" quality parameter of the best match, or
109+
* an array [-1, 0] if no match was found. Just as for
110+
* Mimeparse::quality(), $parsedRanges must be an array of parsed
111+
* media-ranges.
114112
*
115113
* @param string $mimeType
116114
* @param array $parsedRanges
117-
* @return array ($bestFitness, $bestFitQuality)
115+
* @return array ($bestFitQuality, $bestFitness)
118116
*/
119-
protected static function fitnessAndQualityParsed($mimeType, $parsedRanges)
117+
protected static function qualityAndFitnessParsed($mimeType, $parsedRanges)
120118
{
121119
$bestFitness = -1;
122120
$bestFitQuality = 0;
123-
list($targetType, $targetSubtype, $targetParams) = self::parseMediaRange($mimeType);
121+
list($targetType, $targetSubtype, $targetParams) = self::parseAndNormalizeMediaRange($mimeType);
124122

125123
foreach ($parsedRanges as $item) {
126124
list($type, $subtype, $params) = $item;
@@ -151,25 +149,26 @@ protected static function fitnessAndQualityParsed($mimeType, $parsedRanges)
151149

152150
/**
153151
* Find the best match for a given mime-type against a list of
154-
* media-ranges that have already been parsed by Mimeparse::parseMediaRange()
152+
* media-ranges that have already been parsed by
153+
* Mimeparse::parseAndNormalizeMediaRange()
155154
*
156-
* Returns the "q" quality parameter of the best match, 0 if no match
157-
* was found. This function behaves the same as Mimeparse::quality() except that
158-
* $parsedRanges must be an Enumerable of parsed media-ranges.
155+
* Returns the "q" quality parameter of the best match, 0 if no match was
156+
* found. This function behaves the same as Mimeparse::quality() except
157+
* that $parsedRanges must be an array of parsed media-ranges.
159158
*
160159
* @param string $mimeType
161160
* @param array $parsedRanges
162161
* @return float $q
163162
*/
164163
protected static function qualityParsed($mimeType, $parsedRanges)
165164
{
166-
list($q, $fitness) = self::fitnessAndQualityParsed($mimeType, $parsedRanges);
165+
list($q, $fitness) = self::qualityAndFitnessParsed($mimeType, $parsedRanges);
167166
return $q;
168167
}
169168

170169
/**
171-
* Returns the quality "q" of a mime-type when compared against
172-
* the media-ranges in ranges. For example:
170+
* Returns the quality "q" of a mime-type when compared against the
171+
* media-ranges in ranges. For example:
173172
*
174173
* Mimeparse::quality("text/html", "text/*;q=0.3, text/html;q=0.7,
175174
* text/html;level=1, text/html;level=2;q=0.4, *\/*;q=0.5")
@@ -184,71 +183,57 @@ public static function quality($mimeType, $ranges)
184183
$parsedRanges = explode(',', $ranges);
185184

186185
foreach ($parsedRanges as $i => $r) {
187-
$parsedRanges[$i] = self::parseMediaRange($r);
186+
$parsedRanges[$i] = self::parseAndNormalizeMediaRange($r);
188187
}
189188

190189
return self::qualityParsed($mimeType, $parsedRanges);
191190
}
192191

193192
/**
194-
* Takes a list of supported mime-types and finds the best match
195-
* for all the media-ranges listed in header. The value of header
196-
* must be a string that conforms to the format of the HTTP Accept:
197-
* header. The value of supported is an Enumerable of mime-types
193+
* Takes a list of supported mime-types and finds the best match for all
194+
* the media-ranges listed in header. The value of $header must be a
195+
* string that conforms to the format of the HTTP Accept: header. The
196+
* value of $supported is an array of mime-types.
197+
*
198+
* In case of ties the mime-type with the lowest index in $supported will
199+
* be used.
198200
*
199201
* Mimeparse::bestMatch(array("application/xbel+xml", "text/xml"), "text/*;q=0.5,*\/*; q=0.1")
200202
* => "text/xml"
201203
*
202204
* @param array $supported
203205
* @param string $header
204-
* @param string $tieBreaker In case of a tie, this mime-type is preferred
205206
* @return mixed $mimeType or NULL
206207
*/
207-
public static function bestMatch($supported, $header, $tieBreaker = null)
208+
public static function bestMatch($supported, $header)
208209
{
209210
$parsedHeader = explode(',', $header);
210211

211212
foreach ($parsedHeader as $i => $r) {
212-
$parsedHeader[$i] = self::parseMediaRange($r);
213+
$parsedHeader[$i] = self::parseAndNormalizeMediaRange($r);
213214
}
214215

215216
$weightedMatches = array();
216-
foreach ($supported as $mimeType) {
217-
$weightedMatches[] = array(
218-
self::fitnessAndQualityParsed($mimeType, $parsedHeader),
219-
$mimeType
220-
);
221-
}
222-
223-
// If the best fit quality is 0 for anything, then it is
224-
// not acceptable for the client; remove it from the list
225-
// of weighted matches.
226-
$unacceptableTypes = array();
227-
foreach ($weightedMatches as $k => $v) {
228-
if (empty($v[0][0])) {
229-
$unacceptableTypes[] = $k;
217+
foreach ($supported as $index => $mimeType) {
218+
list($quality, $fitness) = self::qualityAndFitnessParsed($mimeType, $parsedHeader);
219+
if (!empty($quality)) {
220+
// Mime-types closer to the beginning of the array are
221+
// preferred. This preference score is used to break ties.
222+
$preference = 0 - $index;
223+
$weightedMatches[] = array(
224+
array($quality, $fitness, $preference),
225+
$mimeType
226+
);
230227
}
231228
}
232-
foreach ($unacceptableTypes as $weightedMatchKey) {
233-
unset($weightedMatches[$weightedMatchKey]);
234-
}
235229

230+
// Note that since fitness and preference are present in
231+
// $weightedMatches they will also be used when sorting (after quality
232+
// level).
236233
array_multisort($weightedMatches);
237-
$a = array_pop($weightedMatches);
238-
239-
// If there's a tie breaker specified, see if we have any ties
240-
// and then break them with the $tieBreaker
241-
if ($tieBreaker) {
242-
array_push($weightedMatches, $a);
243-
$ties = array_filter($weightedMatches, function ($val) use ($a) {
244-
return ($val[0] == $a[0]);
245-
});
246-
if (count($ties) > 1 && in_array(array($a[0], $tieBreaker), $ties)) {
247-
return $tieBreaker;
248-
}
249-
}
234+
$firstChoice = array_pop($weightedMatches);
250235

251-
return (empty($a[0][0]) ? null : $a[1]);
236+
return (empty($firstChoice[0][0]) ? null : $firstChoice[1]);
252237
}
253238

254239
/**

0 commit comments

Comments
 (0)