HttpAcceptNegotiator.php 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. <?php
  2. namespace Wikimedia\Http;
  3. /**
  4. * Utility for negotiating a value from a set of supported values using a preference list.
  5. * This is intended for use with HTTP headers like Accept, Accept-Language, Accept-Encoding, etc.
  6. * See RFC 2616 section 14 for details.
  7. *
  8. * To use this with a request header, first parse the header value into an array of weights
  9. * using HttpAcceptParser, then call getBestSupportedKey.
  10. *
  11. * @license GPL-2.0-or-later
  12. * @author Daniel Kinzler
  13. * @author Thiemo Kreuz
  14. */
  15. class HttpAcceptNegotiator {
  16. /**
  17. * @var string[]
  18. */
  19. private $supportedValues;
  20. /**
  21. * @var string
  22. */
  23. private $defaultValue;
  24. /**
  25. * @param string[] $supported A list of supported values.
  26. */
  27. public function __construct( array $supported ) {
  28. $this->supportedValues = $supported;
  29. $this->defaultValue = reset( $supported );
  30. }
  31. /**
  32. * Returns the best supported key from the given weight map. Of the keys from the
  33. * $weights parameter that are also in the list of supported values supplied to
  34. * the constructor, this returns the key that has the highest weight associated
  35. * with it. If two keys have the same weight, the more specific key is preferred,
  36. * as required by RFC2616 section 14. Keys that map to 0 or false are ignored.
  37. * If no matching key is found, $default is returned.
  38. *
  39. * @param float[] $weights An associative array mapping accepted values to their
  40. * respective weights.
  41. *
  42. * @param null|string $default The value to return if non of the keys in $weights
  43. * is supported (null per default).
  44. *
  45. * @return null|string The best supported key from the $weights parameter.
  46. */
  47. public function getBestSupportedKey( array $weights, $default = null ) {
  48. // Make sure we correctly bias against wildcards and ranges, see RFC2616, section 14.
  49. foreach ( $weights as $name => &$weight ) {
  50. if ( $name === '*' || $name === '*/*' ) {
  51. $weight -= 0.000002;
  52. } elseif ( substr( $name, -2 ) === '/*' ) {
  53. $weight -= 0.000001;
  54. }
  55. }
  56. // Sort $weights by value and...
  57. asort( $weights );
  58. // remove any keys with values equal to 0 or false (HTTP/1.1 section 3.9)
  59. $weights = array_filter( $weights );
  60. // ...use the ordered list of keys
  61. $preferences = array_reverse( array_keys( $weights ) );
  62. $value = $this->getFirstSupportedValue( $preferences, $default );
  63. return $value;
  64. }
  65. /**
  66. * Returns the first supported value from the given preference list. Of the values from
  67. * the $preferences parameter that are also in the list of supported values supplied
  68. * to the constructor, this returns the value that has the lowest index in the list.
  69. * If no such value is found, $default is returned.
  70. *
  71. * @param string[] $preferences A list of acceptable values, in order of preference.
  72. *
  73. * @param null|string $default The value to return if non of the keys in $weights
  74. * is supported (null per default).
  75. *
  76. * @return null|string The best supported key from the $weights parameter.
  77. */
  78. public function getFirstSupportedValue( array $preferences, $default = null ) {
  79. foreach ( $preferences as $value ) {
  80. foreach ( $this->supportedValues as $supported ) {
  81. if ( $this->valueMatches( $value, $supported ) ) {
  82. return $supported;
  83. }
  84. }
  85. }
  86. return $default;
  87. }
  88. /**
  89. * Returns true if the given acceptable value matches the given supported value,
  90. * according to the HTTP specification. The following rules are used:
  91. *
  92. * - comparison is case-insensitive
  93. * - if $accepted and $supported are equal, they match
  94. * - if $accepted is `*` or `*` followed by `/*`, it matches any $supported value.
  95. * - if both $accepted and $supported contain a `/`, and $accepted ends with `/*`,
  96. * they match if the part before the first `/` is equal.
  97. *
  98. * @param string $accepted An accepted value (may contain wildcards)
  99. * @param string $supported A supported value.
  100. *
  101. * @return bool Whether the given supported value matches the given accepted value.
  102. */
  103. private function valueMatches( $accepted, $supported ) {
  104. // RDF 2045: MIME types are case insensitive.
  105. // full match
  106. if ( strcasecmp( $accepted, $supported ) === 0 ) {
  107. return true;
  108. }
  109. // wildcard match (HTTP/1.1 section 14.1, 14.2, 14.3)
  110. if ( $accepted === '*' || $accepted === '*/*' ) {
  111. return true;
  112. }
  113. // wildcard match (HTTP/1.1 section 14.1)
  114. if ( substr( $accepted, -2 ) === '/*'
  115. && strncasecmp( $accepted, $supported, strlen( $accepted ) - 2 ) === 0
  116. ) {
  117. return true;
  118. }
  119. return false;
  120. }
  121. }