/modules/Guzzle/Parser/UriTemplate/UriTemplate.php

https://gitlab.com/x33n/ampache · PHP · 254 lines · 169 code · 36 blank · 49 comment · 50 complexity · b8c59a215eb0bfabdbb42b1fb9e74542 MD5 · raw file

  1. <?php
  2. namespace Guzzle\Parser\UriTemplate;
  3. /**
  4. * Expands URI templates using an array of variables
  5. *
  6. * @link http://tools.ietf.org/html/draft-gregorio-uritemplate-08
  7. */
  8. class UriTemplate implements UriTemplateInterface
  9. {
  10. const DEFAULT_PATTERN = '/\{([^\}]+)\}/';
  11. /** @var string URI template */
  12. private $template;
  13. /** @var array Variables to use in the template expansion */
  14. private $variables;
  15. /** @var string Regex used to parse expressions */
  16. private $regex = self::DEFAULT_PATTERN;
  17. /** @var array Hash for quick operator lookups */
  18. private static $operatorHash = array(
  19. '+' => true, '#' => true, '.' => true, '/' => true, ';' => true, '?' => true, '&' => true
  20. );
  21. /** @var array Delimiters */
  22. private static $delims = array(
  23. ':', '/', '?', '#', '[', ']', '@', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '='
  24. );
  25. /** @var array Percent encoded delimiters */
  26. private static $delimsPct = array(
  27. '%3A', '%2F', '%3F', '%23', '%5B', '%5D', '%40', '%21', '%24', '%26', '%27', '%28', '%29', '%2A', '%2B', '%2C',
  28. '%3B', '%3D'
  29. );
  30. public function expand($template, array $variables)
  31. {
  32. if ($this->regex == self::DEFAULT_PATTERN && false === strpos($template, '{')) {
  33. return $template;
  34. }
  35. $this->template = $template;
  36. $this->variables = $variables;
  37. return preg_replace_callback($this->regex, array($this, 'expandMatch'), $this->template);
  38. }
  39. /**
  40. * Set the regex patten used to expand URI templates
  41. *
  42. * @param string $regexPattern
  43. */
  44. public function setRegex($regexPattern)
  45. {
  46. $this->regex = $regexPattern;
  47. }
  48. /**
  49. * Parse an expression into parts
  50. *
  51. * @param string $expression Expression to parse
  52. *
  53. * @return array Returns an associative array of parts
  54. */
  55. private function parseExpression($expression)
  56. {
  57. // Check for URI operators
  58. $operator = '';
  59. if (isset(self::$operatorHash[$expression[0]])) {
  60. $operator = $expression[0];
  61. $expression = substr($expression, 1);
  62. }
  63. $values = explode(',', $expression);
  64. foreach ($values as &$value) {
  65. $value = trim($value);
  66. $varspec = array();
  67. $substrPos = strpos($value, ':');
  68. if ($substrPos) {
  69. $varspec['value'] = substr($value, 0, $substrPos);
  70. $varspec['modifier'] = ':';
  71. $varspec['position'] = (int) substr($value, $substrPos + 1);
  72. } elseif (substr($value, -1) == '*') {
  73. $varspec['modifier'] = '*';
  74. $varspec['value'] = substr($value, 0, -1);
  75. } else {
  76. $varspec['value'] = (string) $value;
  77. $varspec['modifier'] = '';
  78. }
  79. $value = $varspec;
  80. }
  81. return array(
  82. 'operator' => $operator,
  83. 'values' => $values
  84. );
  85. }
  86. /**
  87. * Process an expansion
  88. *
  89. * @param array $matches Matches met in the preg_replace_callback
  90. *
  91. * @return string Returns the replacement string
  92. */
  93. private function expandMatch(array $matches)
  94. {
  95. static $rfc1738to3986 = array(
  96. '+' => '%20',
  97. '%7e' => '~'
  98. );
  99. $parsed = self::parseExpression($matches[1]);
  100. $replacements = array();
  101. $prefix = $parsed['operator'];
  102. $joiner = $parsed['operator'];
  103. $useQueryString = false;
  104. if ($parsed['operator'] == '?') {
  105. $joiner = '&';
  106. $useQueryString = true;
  107. } elseif ($parsed['operator'] == '&') {
  108. $useQueryString = true;
  109. } elseif ($parsed['operator'] == '#') {
  110. $joiner = ',';
  111. } elseif ($parsed['operator'] == ';') {
  112. $useQueryString = true;
  113. } elseif ($parsed['operator'] == '' || $parsed['operator'] == '+') {
  114. $joiner = ',';
  115. $prefix = '';
  116. }
  117. foreach ($parsed['values'] as $value) {
  118. if (!array_key_exists($value['value'], $this->variables) || $this->variables[$value['value']] === null) {
  119. continue;
  120. }
  121. $variable = $this->variables[$value['value']];
  122. $actuallyUseQueryString = $useQueryString;
  123. $expanded = '';
  124. if (is_array($variable)) {
  125. $isAssoc = $this->isAssoc($variable);
  126. $kvp = array();
  127. foreach ($variable as $key => $var) {
  128. if ($isAssoc) {
  129. $key = rawurlencode($key);
  130. $isNestedArray = is_array($var);
  131. } else {
  132. $isNestedArray = false;
  133. }
  134. if (!$isNestedArray) {
  135. $var = rawurlencode($var);
  136. if ($parsed['operator'] == '+' || $parsed['operator'] == '#') {
  137. $var = $this->decodeReserved($var);
  138. }
  139. }
  140. if ($value['modifier'] == '*') {
  141. if ($isAssoc) {
  142. if ($isNestedArray) {
  143. // Nested arrays must allow for deeply nested structures
  144. $var = strtr(http_build_query(array($key => $var)), $rfc1738to3986);
  145. } else {
  146. $var = $key . '=' . $var;
  147. }
  148. } elseif ($key > 0 && $actuallyUseQueryString) {
  149. $var = $value['value'] . '=' . $var;
  150. }
  151. }
  152. $kvp[$key] = $var;
  153. }
  154. if (empty($variable)) {
  155. $actuallyUseQueryString = false;
  156. } elseif ($value['modifier'] == '*') {
  157. $expanded = implode($joiner, $kvp);
  158. if ($isAssoc) {
  159. // Don't prepend the value name when using the explode modifier with an associative array
  160. $actuallyUseQueryString = false;
  161. }
  162. } else {
  163. if ($isAssoc) {
  164. // When an associative array is encountered and the explode modifier is not set, then the
  165. // result must be a comma separated list of keys followed by their respective values.
  166. foreach ($kvp as $k => &$v) {
  167. $v = $k . ',' . $v;
  168. }
  169. }
  170. $expanded = implode(',', $kvp);
  171. }
  172. } else {
  173. if ($value['modifier'] == ':') {
  174. $variable = substr($variable, 0, $value['position']);
  175. }
  176. $expanded = rawurlencode($variable);
  177. if ($parsed['operator'] == '+' || $parsed['operator'] == '#') {
  178. $expanded = $this->decodeReserved($expanded);
  179. }
  180. }
  181. if ($actuallyUseQueryString) {
  182. if (!$expanded && $joiner != '&') {
  183. $expanded = $value['value'];
  184. } else {
  185. $expanded = $value['value'] . '=' . $expanded;
  186. }
  187. }
  188. $replacements[] = $expanded;
  189. }
  190. $ret = implode($joiner, $replacements);
  191. if ($ret && $prefix) {
  192. return $prefix . $ret;
  193. }
  194. return $ret;
  195. }
  196. /**
  197. * Determines if an array is associative
  198. *
  199. * @param array $array Array to check
  200. *
  201. * @return bool
  202. */
  203. private function isAssoc(array $array)
  204. {
  205. return (bool) count(array_filter(array_keys($array), 'is_string'));
  206. }
  207. /**
  208. * Removes percent encoding on reserved characters (used with + and # modifiers)
  209. *
  210. * @param string $string String to fix
  211. *
  212. * @return string
  213. */
  214. private function decodeReserved($string)
  215. {
  216. return str_replace(self::$delimsPct, self::$delims, $string);
  217. }
  218. }