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 *
1718class 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