PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.41
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.41
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
fluent-boards / vendor / wpfluent / framework / src / WPFluent / Support / UriTemplate.php

UriTemplate.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 1.41, at vendor/wpfluent/framework/src/WPFluent/Support/UriTemplate.php

296 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\Framework\Support;
4
5 /**
6 * Expands URI templates. Userland implementation of PECL uri_template.
7 *
8 * @see https://datatracker.ietf.org/doc/html/rfc6570
9 */
10 final class UriTemplate
11 {
12 /**
13 * @var array<string, array{prefix:string, joiner:string, query:bool}> Hash for quick operator lookups
14 */
15 private static $operatorHash = [
16 '' => ['prefix' => '', 'joiner' => ',', 'query' => false],
17 '+' => ['prefix' => '', 'joiner' => ',', 'query' => false],
18 '#' => ['prefix' => '#', 'joiner' => ',', 'query' => false],
19 '.' => ['prefix' => '.', 'joiner' => '.', 'query' => false],
20 '/' => ['prefix' => '/', 'joiner' => '/', 'query' => false],
21 ';' => ['prefix' => ';', 'joiner' => ';', 'query' => true],
22 '?' => ['prefix' => '?', 'joiner' => '&', 'query' => true],
23 '&' => ['prefix' => '&', 'joiner' => '&', 'query' => true],
24 ];
25
26 /**
27 * @var string[] Delimiters
28 */
29 private static $delims = [
30 ':',
31 '/',
32 '?',
33 '#',
34 '[',
35 ']',
36 '@',
37 '!',
38 '$',
39 '&',
40 '\'',
41 '(',
42 ')',
43 '*',
44 '+',
45 ',',
46 ';',
47 '=',
48 ];
49
50 /**
51 * @var string[] Percent encoded delimiters
52 */
53 private static $delimsPct = [
54 '%3A',
55 '%2F',
56 '%3F',
57 '%23',
58 '%5B',
59 '%5D',
60 '%40',
61 '%21',
62 '%24',
63 '%26',
64 '%27',
65 '%28',
66 '%29',
67 '%2A',
68 '%2B',
69 '%2C',
70 '%3B',
71 '%3D',
72 ];
73
74 /**
75 * @param array<string,mixed> $variables Variables to use in the template expansion
76 *
77 * @throws \RuntimeException
78 */
79 public static function expand(string $template, array $variables): string
80 {
81 if (false === \strpos($template, '{')) {
82 return $template;
83 }
84
85 /** @var string|null */
86 $result = \preg_replace_callback(
87 '/\{([^\}]+)\}/',
88 self::expandMatchCallback($variables),
89 $template
90 );
91
92 if (null === $result) {
93 throw new \RuntimeException(\sprintf(
94 'Unable to process template: %s', \preg_last_error_msg()
95 ));
96 }
97
98 return $result;
99 }
100
101 /**
102 * @param array<string,mixed> $variables Variables to use in the template expansion
103 *
104 * @return callable(string[]): string
105 */
106 private static function expandMatchCallback(array $variables): callable
107 {
108 return static function (array $matches) use ($variables): string {
109 return self::expandMatch($matches, $variables);
110 };
111 }
112
113 /**
114 * Process an expansion
115 *
116 * @param array<string,mixed> $variables Variables to use in the template expansion
117 * @param string[] $matches Matches met in the preg_replace_callback
118 *
119 * @return string Returns the replacement string
120 */
121 private static function expandMatch(array $matches, array $variables): string
122 {
123 $replacements = [];
124 $parsed = self::parseExpression($matches[1]);
125 $prefix = self::$operatorHash[$parsed['operator']]['prefix'];
126 $joiner = self::$operatorHash[$parsed['operator']]['joiner'];
127 $useQuery = self::$operatorHash[$parsed['operator']]['query'];
128 $allUndefined = true;
129
130 foreach ($parsed['values'] as $value) {
131 if (!isset($variables[$value['value']])) {
132 continue;
133 }
134
135 $variable = $variables[$value['value']];
136 $actuallyUseQuery = $useQuery;
137 $expanded = '';
138
139 if (\is_array($variable)) {
140 $isAssoc = self::isAssoc($variable);
141 $kvp = [];
142 /** @var mixed $var */
143 foreach ($variable as $key => $var) {
144 if ($isAssoc) {
145 $key = \rawurlencode((string) $key);
146 $isNestedArray = \is_array($var);
147 } else {
148 $isNestedArray = false;
149 }
150
151 if (!$isNestedArray) {
152 $var = \rawurlencode((string) $var);
153 if ($parsed['operator'] === '+' || $parsed['operator'] === '#') {
154 $var = self::decodeReserved($var);
155 }
156 }
157
158 if ($value['modifier'] === '*') {
159 if ($isAssoc) {
160 if ($isNestedArray) {
161 // Nested arrays must allow for deeply nested structures.
162 $var = \http_build_query([$key => $var], '', '&', \PHP_QUERY_RFC3986);
163 } else {
164 $var = \sprintf('%s=%s', (string) $key, (string) $var);
165 }
166 } elseif ($key > 0 && $actuallyUseQuery) {
167 $var = \sprintf('%s=%s', $value['value'], (string) $var);
168 }
169 }
170
171 /** @var string $var */
172 $kvp[$key] = $var;
173 }
174
175 if (0 === \count($variable)) {
176 $actuallyUseQuery = false;
177 } elseif ($value['modifier'] === '*') {
178 $expanded = \implode($joiner, $kvp);
179 if ($isAssoc) {
180 // Don't prepend the value name when using the explode
181 // modifier with an associative array.
182 $actuallyUseQuery = false;
183 }
184 } else {
185 if ($isAssoc) {
186 // When an associative array is encountered and the
187 // explode modifier is not set, then the result must be
188 // a comma separated list of keys followed by their
189 // respective values.
190 foreach ($kvp as $k => &$v) {
191 $v = \sprintf('%s,%s', $k, $v);
192 }
193 }
194 $expanded = \implode(',', $kvp);
195 }
196 } else {
197 $allUndefined = false;
198 if ($value['modifier'] === ':' && isset($value['position'])) {
199 $variable = \substr((string) $variable, 0, $value['position']);
200 }
201 $expanded = \rawurlencode((string) $variable);
202 if ($parsed['operator'] === '+' || $parsed['operator'] === '#') {
203 $expanded = self::decodeReserved($expanded);
204 }
205 }
206
207 if ($actuallyUseQuery) {
208 if ($expanded === '' && $joiner !== '&') {
209 $expanded = $value['value'];
210 } else {
211 $expanded = \sprintf('%s=%s', $value['value'], $expanded);
212 }
213 }
214
215 $replacements[] = $expanded;
216 }
217
218 $ret = \implode($joiner, $replacements);
219
220 if ('' === $ret) {
221 // Spec section 3.2.4 and 3.2.5
222 if (false === $allUndefined && ('#' === $prefix || '.' === $prefix)) {
223 return $prefix;
224 }
225 } else {
226 if ('' !== $prefix) {
227 return \sprintf('%s%s', $prefix, $ret);
228 }
229 }
230
231 return $ret;
232 }
233
234 /**
235 * Parse an expression into parts
236 *
237 * @param string $expression Expression to parse
238 *
239 * @return array{operator:string, values:array<array{value:string, modifier:(''|'*'|':'), position?:int}>}
240 */
241 private static function parseExpression(string $expression): array
242 {
243 $result = [];
244
245 if (isset(self::$operatorHash[$expression[0]])) {
246 $result['operator'] = $expression[0];
247 /** @var string */
248 $expression = \substr($expression, 1);
249 } else {
250 $result['operator'] = '';
251 }
252
253 $result['values'] = [];
254 foreach (\explode(',', $expression) as $value) {
255 $value = \trim($value);
256 $varspec = [];
257 if ($colonPos = \strpos($value, ':')) {
258 $varspec['value'] = (string) \substr($value, 0, $colonPos);
259 $varspec['modifier'] = ':';
260 $varspec['position'] = (int) \substr($value, $colonPos + 1);
261 } elseif (\substr($value, -1) === '*') {
262 $varspec['modifier'] = '*';
263 $varspec['value'] = (string) \substr($value, 0, -1);
264 } else {
265 $varspec['value'] = $value;
266 $varspec['modifier'] = '';
267 }
268 $result['values'][] = $varspec;
269 }
270
271 return $result;
272 }
273
274 /**
275 * Determines if an array is associative.
276 *
277 * This makes the assumption that input arrays are sequences or hashes.
278 * This assumption is a tradeoff for accuracy in favor of speed, but it
279 * should work in almost every case where input is supplied for a URI
280 * template.
281 */
282 private static function isAssoc(array $array): bool
283 {
284 return $array && \array_keys($array)[0] !== 0;
285 }
286
287 /**
288 * Removes percent encoding on reserved characters (used with + and #
289 * modifiers).
290 */
291 private static function decodeReserved(string $string): string
292 {
293 return \str_replace(self::$delimsPct, self::$delims, $string);
294 }
295 }
296