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