1<?php 2/* 3 * Copyright 2013 Google Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18namespace Google\Utils; 19 20/** 21 * Implementation of levels 1-3 of the URI Template spec. 22 * @see http://tools.ietf.org/html/rfc6570 23 */ 24class UriTemplate 25{ 26 const TYPE_MAP = "1"; 27 const TYPE_LIST = "2"; 28 const TYPE_SCALAR = "4"; 29 30 /** 31 * @var $operators array 32 * These are valid at the start of a template block to 33 * modify the way in which the variables inside are 34 * processed. 35 */ 36 private $operators = array( 37 "+" => "reserved", 38 "/" => "segments", 39 "." => "dotprefix", 40 "#" => "fragment", 41 ";" => "semicolon", 42 "?" => "form", 43 "&" => "continuation" 44 ); 45 46 /** 47 * @var reserved array 48 * These are the characters which should not be URL encoded in reserved 49 * strings. 50 */ 51 private $reserved = array( 52 "=", ",", "!", "@", "|", ":", "/", "?", "#", 53 "[", "]",'$', "&", "'", "(", ")", "*", "+", ";" 54 ); 55 private $reservedEncoded = array( 56 "%3D", "%2C", "%21", "%40", "%7C", "%3A", "%2F", "%3F", 57 "%23", "%5B", "%5D", "%24", "%26", "%27", "%28", "%29", 58 "%2A", "%2B", "%3B" 59 ); 60 61 public function parse($string, array $parameters) 62 { 63 return $this->resolveNextSection($string, $parameters); 64 } 65 66 /** 67 * This function finds the first matching {...} block and 68 * executes the replacement. It then calls itself to find 69 * subsequent blocks, if any. 70 */ 71 private function resolveNextSection($string, $parameters) 72 { 73 $start = strpos($string, "{"); 74 if ($start === false) { 75 return $string; 76 } 77 $end = strpos($string, "}"); 78 if ($end === false) { 79 return $string; 80 } 81 $string = $this->replace($string, $start, $end, $parameters); 82 return $this->resolveNextSection($string, $parameters); 83 } 84 85 private function replace($string, $start, $end, $parameters) 86 { 87 // We know a data block will have {} round it, so we can strip that. 88 $data = substr($string, $start + 1, $end - $start - 1); 89 90 // If the first character is one of the reserved operators, it effects 91 // the processing of the stream. 92 if (isset($this->operators[$data[0]])) { 93 $op = $this->operators[$data[0]]; 94 $data = substr($data, 1); 95 $prefix = ""; 96 $prefix_on_missing = false; 97 98 switch ($op) { 99 case "reserved": 100 // Reserved means certain characters should not be URL encoded 101 $data = $this->replaceVars($data, $parameters, ",", null, true); 102 break; 103 case "fragment": 104 // Comma separated with fragment prefix. Bare values only. 105 $prefix = "#"; 106 $prefix_on_missing = true; 107 $data = $this->replaceVars($data, $parameters, ",", null, true); 108 break; 109 case "segments": 110 // Slash separated data. Bare values only. 111 $prefix = "/"; 112 $data =$this->replaceVars($data, $parameters, "/"); 113 break; 114 case "dotprefix": 115 // Dot separated data. Bare values only. 116 $prefix = "."; 117 $prefix_on_missing = true; 118 $data = $this->replaceVars($data, $parameters, "."); 119 break; 120 case "semicolon": 121 // Semicolon prefixed and separated. Uses the key name 122 $prefix = ";"; 123 $data = $this->replaceVars($data, $parameters, ";", "=", false, true, false); 124 break; 125 case "form": 126 // Standard URL format. Uses the key name 127 $prefix = "?"; 128 $data = $this->replaceVars($data, $parameters, "&", "="); 129 break; 130 case "continuation": 131 // Standard URL, but with leading ampersand. Uses key name. 132 $prefix = "&"; 133 $data = $this->replaceVars($data, $parameters, "&", "="); 134 break; 135 } 136 137 // Add the initial prefix character if data is valid. 138 if ($data || ($data !== false && $prefix_on_missing)) { 139 $data = $prefix . $data; 140 } 141 142 } else { 143 // If no operator we replace with the defaults. 144 $data = $this->replaceVars($data, $parameters); 145 } 146 // This is chops out the {...} and replaces with the new section. 147 return substr($string, 0, $start) . $data . substr($string, $end + 1); 148 } 149 150 private function replaceVars( 151 $section, 152 $parameters, 153 $sep = ",", 154 $combine = null, 155 $reserved = false, 156 $tag_empty = false, 157 $combine_on_empty = true 158 ) { 159 if (strpos($section, ",") === false) { 160 // If we only have a single value, we can immediately process. 161 return $this->combine( 162 $section, 163 $parameters, 164 $sep, 165 $combine, 166 $reserved, 167 $tag_empty, 168 $combine_on_empty 169 ); 170 } else { 171 // If we have multiple values, we need to split and loop over them. 172 // Each is treated individually, then glued together with the 173 // separator character. 174 $vars = explode(",", $section); 175 return $this->combineList( 176 $vars, 177 $sep, 178 $parameters, 179 $combine, 180 $reserved, 181 false, // Never emit empty strings in multi-param replacements 182 $combine_on_empty 183 ); 184 } 185 } 186 187 public function combine( 188 $key, 189 $parameters, 190 $sep, 191 $combine, 192 $reserved, 193 $tag_empty, 194 $combine_on_empty 195 ) { 196 $length = false; 197 $explode = false; 198 $skip_final_combine = false; 199 $value = false; 200 201 // Check for length restriction. 202 if (strpos($key, ":") !== false) { 203 list($key, $length) = explode(":", $key); 204 } 205 206 // Check for explode parameter. 207 if ($key[strlen($key) - 1] == "*") { 208 $explode = true; 209 $key = substr($key, 0, -1); 210 $skip_final_combine = true; 211 } 212 213 // Define the list separator. 214 $list_sep = $explode ? $sep : ","; 215 216 if (isset($parameters[$key])) { 217 $data_type = $this->getDataType($parameters[$key]); 218 switch ($data_type) { 219 case self::TYPE_SCALAR: 220 $value = $this->getValue($parameters[$key], $length); 221 break; 222 case self::TYPE_LIST: 223 $values = array(); 224 foreach ($parameters[$key] as $pkey => $pvalue) { 225 $pvalue = $this->getValue($pvalue, $length); 226 if ($combine && $explode) { 227 $values[$pkey] = $key . $combine . $pvalue; 228 } else { 229 $values[$pkey] = $pvalue; 230 } 231 } 232 $value = implode($list_sep, $values); 233 if ($value == '') { 234 return ''; 235 } 236 break; 237 case self::TYPE_MAP: 238 $values = array(); 239 foreach ($parameters[$key] as $pkey => $pvalue) { 240 $pvalue = $this->getValue($pvalue, $length); 241 if ($explode) { 242 $pkey = $this->getValue($pkey, $length); 243 $values[] = $pkey . "=" . $pvalue; // Explode triggers = combine. 244 } else { 245 $values[] = $pkey; 246 $values[] = $pvalue; 247 } 248 } 249 $value = implode($list_sep, $values); 250 if ($value == '') { 251 return false; 252 } 253 break; 254 } 255 } else if ($tag_empty) { 256 // If we are just indicating empty values with their key name, return that. 257 return $key; 258 } else { 259 // Otherwise we can skip this variable due to not being defined. 260 return false; 261 } 262 263 if ($reserved) { 264 $value = str_replace($this->reservedEncoded, $this->reserved, $value); 265 } 266 267 // If we do not need to include the key name, we just return the raw 268 // value. 269 if (!$combine || $skip_final_combine) { 270 return $value; 271 } 272 273 // Else we combine the key name: foo=bar, if value is not the empty string. 274 return $key . ($value != '' || $combine_on_empty ? $combine . $value : ''); 275 } 276 277 /** 278 * Return the type of a passed in value 279 */ 280 private function getDataType($data) 281 { 282 if (is_array($data)) { 283 reset($data); 284 if (key($data) !== 0) { 285 return self::TYPE_MAP; 286 } 287 return self::TYPE_LIST; 288 } 289 return self::TYPE_SCALAR; 290 } 291 292 /** 293 * Utility function that merges multiple combine calls 294 * for multi-key templates. 295 */ 296 private function combineList( 297 $vars, 298 $sep, 299 $parameters, 300 $combine, 301 $reserved, 302 $tag_empty, 303 $combine_on_empty 304 ) { 305 $ret = array(); 306 foreach ($vars as $var) { 307 $response = $this->combine( 308 $var, 309 $parameters, 310 $sep, 311 $combine, 312 $reserved, 313 $tag_empty, 314 $combine_on_empty 315 ); 316 if ($response === false) { 317 continue; 318 } 319 $ret[] = $response; 320 } 321 return implode($sep, $ret); 322 } 323 324 /** 325 * Utility function to encode and trim values 326 */ 327 private function getValue($value, $length) 328 { 329 if ($length) { 330 $value = substr($value, 0, $length); 331 } 332 $value = rawurlencode($value); 333 return $value; 334 } 335} 336