Repository navigation
Conversation
| private function isExcluded(array $acceptable, string $supported): bool | ||
| { | ||
| foreach ($acceptable as $accept) { | ||
| if ($accept['q'] === 0.0 && $this->match($accept, $supported)) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } |
There was a problem hiding this comment.
This calls match() with its default flags, so text/*;q=0 does not exclude text/html. Also, exact parameter-set matching lets text/html;charset=utf-8 escape text/html;q=0. Both still select excluded representations through a positive wildcard. Add regression coverage and apply media-range matching with specificity preserved.
There was a problem hiding this comment.
isExcluded() no longer goes through match(). It finds the most specific header value that matches the supported value and excludes the value when that one has q=0 (RFC 9110, section 12.5.1). A media range matches by type and subtype, and its own parameters must be present in the supported value, so text/*;q=0 now excludes text/html and text/html;q=0 excludes text/html;charset=utf-8, while text/html, text/*;q=0 still selects text/html and text/html;level=1;q=0.5 beats text/html;q=0. testMediaRangesExcludeMatchingValues and testMoreSpecificRangesTakePrecedence cover these, with positive controls (78e6fb1).
04f1211 to
78e6fb1
Compare
|
Rebased onto develop. The description is updated for the wider scope. |
| [$aType, $aSubType] = explode('/', $accept['value'], 2) + ['', '']; | ||
| [$sType, $sSubType] = explode('/', $supported['value'], 2) + ['', '']; | ||
|
|
||
| if ($aType !== $sType || ($aSubType !== '*' && $aSubType !== $sSubType)) { |
There was a problem hiding this comment.
Media types are case-insensitive. With Accept: text/*;q=0, TEXT/HTML;q=0.5, */*;q=0.1, media(['text/html'], true) now returns an empty string instead of text/html. Please use case-insensitive comparisons and add a regression test.
There was a problem hiding this comment.
The exclusion check now compares media types, subtypes and parameter names ignoring case, and charset values too; other parameter values stay case-sensitive (RFC 9110, section 8.3.1). Accept-Charset and Accept-Encoding values are also compared ignoring case there. With Accept: text/*;q=0, TEXT/HTML;q=0.5, */*;q=0.1, media(['text/html'], true) returns text/html again. testRangesExcludeValuesIgnoringCase covers it, along with TEXT/*;q=0, CHARSET="UTF-8";q=0, Accept-Charset: UTF-8;q=0, * and Accept-Encoding: GZIP;q=0, * (ab58f75).
| } | ||
| } | ||
|
|
||
| return $aSubType === '*' ? 1 : 2 + count($accept['params']); |
There was a problem hiding this comment.
With Accept: text/*;charset=utf-8;q=0, text/* and supported values media(['text/html;charset=utf-8', 'text/plain;charset=iso-8859-1'], true), negotiation still selects the excluded UTF-8 HTML instead of the ISO-8859-1 plain text.
Both ranges receive specificity 1, so the broader positive range wins. The range with the charset parameter should be more specific. Could you include parameters in wildcard specificity and add a regression test?
There was a problem hiding this comment.
Parameters now count in the specificity of every media range. A range is more specific than one of the same kind with fewer parameters, and a type/subtype range stays more specific than any type/* range, which in turn stays more specific than any */* range: the specificity is level * (count(supported params) + 1) + count(range params), and a matching range can't have more parameters than the supported value. With Accept: text/*;charset=utf-8;q=0, text/*, your example now selects text/plain;charset=iso-8859-1, and */*;charset=utf-8;q=0, */* excludes UTF-8 values the same way. testMediaRangeParametersAddSpecificity covers both, and checks that text/html;q=0 still takes precedence over text/*;charset=utf-8;level=1 (ab58f75).
78e6fb1 to
d2dba59
Compare
A wildcard or a broader range could select a supported value even when the header excluded that value with q=0. For example, `Accept-Encoding: gzip;q=0, *` selected gzip, `Accept: text/*;q=0, */*` selected text/html, `Accept: text/html;q=0, */*` selected text/html;charset=utf-8, and `Accept-Language: en;q=0, *` selected en-US, with and without strict locale negotiation. A supported value is now excluded when the most specific value in the header that matches it has q=0 (RFC 9110, Section 12.5.1), so `text/html, text/*;q=0` still selects text/html. A media range matches the media types of its type and subtype that have its parameters, and a language range matches the tags that equal it or start with it followed by "-" (RFC 4647, Section 3.3.1). A type/subtype range is more specific than a type/* range, which is more specific than */*, and a range with more parameters is more specific than one of the same kind with fewer, so `text/*;charset=utf-8;q=0, text/*` excludes text/html;charset=utf-8. These comparisons ignore case, except for parameter values other than charset (RFC 9110, Section 8.3). Wildcards, broader matches and the strict locale fallbacks skip excluded values. Without a match, the fallback to the first supported value when `$strictMatch` is false is unchanged.
d2dba59 to
ab58f75
Compare
|
Rebased onto develop again. |
Description
When the header holds a wildcard,
Negotiatereturns the first supported value, even if the same header excludes that value withq=0. In RFC 9110, a weight of 0 means "not acceptable" (section 12.4.2), and a more specific entry overrides a wildcard (sections 12.5.1 and 12.5.3). Examples:Accept-Encoding: gzip;q=0, *withencoding(['gzip', 'br'])returnsgzip.Accept: application/json;q=0, */*withmedia(['application/json', 'text/html'], true)returnsapplication/json.Accept: text/*, text/html;q=0withmedia(['text/html', 'text/plain'], true)returnstext/html.charset()behaves the same way, and so doeslanguage(), with and without$strictLocaleNegotiation. For example,Accept-Language: en;q=0, *givesen.getBestMatch()andgetBestLocaleMatch()now skip values that the header excludes. A privateisExcluded()applies the most specific value in the header that matches a supported value (RFC 9110, section 12.5.1): media ranges match by type, subtype and their own parameters, sotext/*;q=0excludestext/htmlandtext/html;q=0excludestext/html;charset=utf-8, and language ranges use RFC 4647 basic filtering, soen;q=0excludesen-US. A more specific range with a positive weight still wins:text/html, text/*;q=0selectstext/html, anden;q=0, en-GB;q=0.5, *selectsen-GB. Wildcards, broader matches and the strict locale fallbacks all skip excluded values. When nothing matches,$strictMatch = falsestill falls back to the first supported value, as documented and astestBestMatchOnlyLowQualityexpects.testWildcardsSkipExcludedValues,testMediaRangesExcludeMatchingValues,testMoreSpecificRangesTakePrecedenceandtestLanguageRangesExcludeRegionalLocalesintests/system/HTTP/NegotiateTest.phpcovermedia(),charset(),encoding()andlanguage()in both locale modes, with positive controls. They fail without the change.tests/system/HTTP/passes. php-cs-fixer, PHPStan and Rector (dry run) report nothing for the changed files.Checklist: