Skip to content

Commit 563b9cd

Browse files
committed
Merge pull request #4 from mkantor/master
Simplify terminology and behavior.
2 parents 8c53c4f + d7e6ac6 commit 563b9cd

2 files changed

Lines changed: 103 additions & 136 deletions

File tree

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)