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 |
|
---|
18 | namespace Google\Utils;
|
---|
19 |
|
---|
20 | /**
|
---|
21 | * Implementation of levels 1-3 of the URI Template spec.
|
---|
22 | * @see http://tools.ietf.org/html/rfc6570
|
---|
23 | */
|
---|
24 | class UriTemplate
|
---|
25 | {
|
---|
26 | const TYPE_MAP = "1";
|
---|
27 | const TYPE_LIST = "2";
|
---|
28 | const TYPE_SCALAR = "4";
|
---|
29 |
|
---|
30 | /**
|
---|
31 | * @var array $operators
|
---|
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 = [
|
---|
37 | "+" => "reserved",
|
---|
38 | "/" => "segments",
|
---|
39 | "." => "dotprefix",
|
---|
40 | "#" => "fragment",
|
---|
41 | ";" => "semicolon",
|
---|
42 | "?" => "form",
|
---|
43 | "&" => "continuation"
|
---|
44 | ];
|
---|
45 |
|
---|
46 | /**
|
---|
47 | * @var array<string>
|
---|
48 | * These are the characters which should not be URL encoded in reserved
|
---|
49 | * strings.
|
---|
50 | */
|
---|
51 | private $reserved = [
|
---|
52 | "=", ",", "!", "@", "|", ":", "/", "?", "#",
|
---|
53 | "[", "]", '$', "&", "'", "(", ")", "*", "+", ";"
|
---|
54 | ];
|
---|
55 | private $reservedEncoded = [
|
---|
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 | } else {
|
---|
142 | // If no operator we replace with the defaults.
|
---|
143 | $data = $this->replaceVars($data, $parameters);
|
---|
144 | }
|
---|
145 | // This is chops out the {...} and replaces with the new section.
|
---|
146 | return substr($string, 0, $start) . $data . substr($string, $end + 1);
|
---|
147 | }
|
---|
148 |
|
---|
149 | private function replaceVars(
|
---|
150 | $section,
|
---|
151 | $parameters,
|
---|
152 | $sep = ",",
|
---|
153 | $combine = null,
|
---|
154 | $reserved = false,
|
---|
155 | $tag_empty = false,
|
---|
156 | $combine_on_empty = true
|
---|
157 | ) {
|
---|
158 | if (strpos($section, ",") === false) {
|
---|
159 | // If we only have a single value, we can immediately process.
|
---|
160 | return $this->combine(
|
---|
161 | $section,
|
---|
162 | $parameters,
|
---|
163 | $sep,
|
---|
164 | $combine,
|
---|
165 | $reserved,
|
---|
166 | $tag_empty,
|
---|
167 | $combine_on_empty
|
---|
168 | );
|
---|
169 | } else {
|
---|
170 | // If we have multiple values, we need to split and loop over them.
|
---|
171 | // Each is treated individually, then glued together with the
|
---|
172 | // separator character.
|
---|
173 | $vars = explode(",", $section);
|
---|
174 | return $this->combineList(
|
---|
175 | $vars,
|
---|
176 | $sep,
|
---|
177 | $parameters,
|
---|
178 | $combine,
|
---|
179 | $reserved,
|
---|
180 | false, // Never emit empty strings in multi-param replacements
|
---|
181 | $combine_on_empty
|
---|
182 | );
|
---|
183 | }
|
---|
184 | }
|
---|
185 |
|
---|
186 | public function combine(
|
---|
187 | $key,
|
---|
188 | $parameters,
|
---|
189 | $sep,
|
---|
190 | $combine,
|
---|
191 | $reserved,
|
---|
192 | $tag_empty,
|
---|
193 | $combine_on_empty
|
---|
194 | ) {
|
---|
195 | $length = false;
|
---|
196 | $explode = false;
|
---|
197 | $skip_final_combine = false;
|
---|
198 | $value = false;
|
---|
199 |
|
---|
200 | // Check for length restriction.
|
---|
201 | if (strpos($key, ":") !== false) {
|
---|
202 | list($key, $length) = explode(":", $key);
|
---|
203 | }
|
---|
204 |
|
---|
205 | // Check for explode parameter.
|
---|
206 | if ($key[strlen($key) - 1] == "*") {
|
---|
207 | $explode = true;
|
---|
208 | $key = substr($key, 0, -1);
|
---|
209 | $skip_final_combine = true;
|
---|
210 | }
|
---|
211 |
|
---|
212 | // Define the list separator.
|
---|
213 | $list_sep = $explode ? $sep : ",";
|
---|
214 |
|
---|
215 | if (isset($parameters[$key])) {
|
---|
216 | $data_type = $this->getDataType($parameters[$key]);
|
---|
217 | switch ($data_type) {
|
---|
218 | case self::TYPE_SCALAR:
|
---|
219 | $value = $this->getValue($parameters[$key], $length);
|
---|
220 | break;
|
---|
221 | case self::TYPE_LIST:
|
---|
222 | $values = [];
|
---|
223 | foreach ($parameters[$key] as $pkey => $pvalue) {
|
---|
224 | $pvalue = $this->getValue($pvalue, $length);
|
---|
225 | if ($combine && $explode) {
|
---|
226 | $values[$pkey] = $key . $combine . $pvalue;
|
---|
227 | } else {
|
---|
228 | $values[$pkey] = $pvalue;
|
---|
229 | }
|
---|
230 | }
|
---|
231 | $value = implode($list_sep, $values);
|
---|
232 | if ($value == '') {
|
---|
233 | return '';
|
---|
234 | }
|
---|
235 | break;
|
---|
236 | case self::TYPE_MAP:
|
---|
237 | $values = [];
|
---|
238 | foreach ($parameters[$key] as $pkey => $pvalue) {
|
---|
239 | $pvalue = $this->getValue($pvalue, $length);
|
---|
240 | if ($explode) {
|
---|
241 | $pkey = $this->getValue($pkey, $length);
|
---|
242 | $values[] = $pkey . "=" . $pvalue; // Explode triggers = combine.
|
---|
243 | } else {
|
---|
244 | $values[] = $pkey;
|
---|
245 | $values[] = $pvalue;
|
---|
246 | }
|
---|
247 | }
|
---|
248 | $value = implode($list_sep, $values);
|
---|
249 | if ($value == '') {
|
---|
250 | return false;
|
---|
251 | }
|
---|
252 | break;
|
---|
253 | }
|
---|
254 | } elseif ($tag_empty) {
|
---|
255 | // If we are just indicating empty values with their key name, return that.
|
---|
256 | return $key;
|
---|
257 | } else {
|
---|
258 | // Otherwise we can skip this variable due to not being defined.
|
---|
259 | return false;
|
---|
260 | }
|
---|
261 |
|
---|
262 | if ($reserved) {
|
---|
263 | $value = str_replace($this->reservedEncoded, $this->reserved, $value);
|
---|
264 | }
|
---|
265 |
|
---|
266 | // If we do not need to include the key name, we just return the raw
|
---|
267 | // value.
|
---|
268 | if (!$combine || $skip_final_combine) {
|
---|
269 | return $value;
|
---|
270 | }
|
---|
271 |
|
---|
272 | // Else we combine the key name: foo=bar, if value is not the empty string.
|
---|
273 | return $key . ($value != '' || $combine_on_empty ? $combine . $value : '');
|
---|
274 | }
|
---|
275 |
|
---|
276 | /**
|
---|
277 | * Return the type of a passed in value
|
---|
278 | */
|
---|
279 | private function getDataType($data)
|
---|
280 | {
|
---|
281 | if (is_array($data)) {
|
---|
282 | reset($data);
|
---|
283 | if (key($data) !== 0) {
|
---|
284 | return self::TYPE_MAP;
|
---|
285 | }
|
---|
286 | return self::TYPE_LIST;
|
---|
287 | }
|
---|
288 | return self::TYPE_SCALAR;
|
---|
289 | }
|
---|
290 |
|
---|
291 | /**
|
---|
292 | * Utility function that merges multiple combine calls
|
---|
293 | * for multi-key templates.
|
---|
294 | */
|
---|
295 | private function combineList(
|
---|
296 | $vars,
|
---|
297 | $sep,
|
---|
298 | $parameters,
|
---|
299 | $combine,
|
---|
300 | $reserved,
|
---|
301 | $tag_empty,
|
---|
302 | $combine_on_empty
|
---|
303 | ) {
|
---|
304 | $ret = [];
|
---|
305 | foreach ($vars as $var) {
|
---|
306 | $response = $this->combine(
|
---|
307 | $var,
|
---|
308 | $parameters,
|
---|
309 | $sep,
|
---|
310 | $combine,
|
---|
311 | $reserved,
|
---|
312 | $tag_empty,
|
---|
313 | $combine_on_empty
|
---|
314 | );
|
---|
315 | if ($response === false) {
|
---|
316 | continue;
|
---|
317 | }
|
---|
318 | $ret[] = $response;
|
---|
319 | }
|
---|
320 | return implode($sep, $ret);
|
---|
321 | }
|
---|
322 |
|
---|
323 | /**
|
---|
324 | * Utility function to encode and trim values
|
---|
325 | */
|
---|
326 | private function getValue($value, $length)
|
---|
327 | {
|
---|
328 | if ($length) {
|
---|
329 | $value = substr($value, 0, $length);
|
---|
330 | }
|
---|
331 | $value = rawurlencode($value);
|
---|
332 | return $value;
|
---|
333 | }
|
---|
334 | }
|
---|