@@ -153,7 +153,9 @@ public function __construct(
153
153
}
154
154
if (isset ($ parts ['query ' ])) {
155
155
// Note: the values may not be correctly decoded, url parameters should be always passed as array.
156
- parse_str (str_replace ('& ' , '& ' , $ parts ['query ' ]), $ this ->params );
156
+ $ out = [];
157
+ parse_str (str_replace ('& ' , '& ' , $ parts ['query ' ]), $ out );
158
+ $ this ->params ($ out );
157
159
}
158
160
unset($ parts ['query ' ]);
159
161
foreach ($ parts as $ key => $ value ) {
@@ -179,28 +181,36 @@ public function __construct(
179
181
*
180
182
* The added params override existing ones if they have the same name.
181
183
*
182
- * @param null|array $params Defaults to null. If null then returns all params .
184
+ * @param null|array $params Array of parameters to add. Note all values that are not arrays are cast to strings .
183
185
* @return array Array of Params for url.
184
186
* @throws coding_exception
185
187
*/
186
188
public function params (?array $ params = null ) {
187
189
$ params = (array )$ params ;
190
+ $ params = $ this ->clean_url_params ($ params );
191
+ $ this ->params = array_merge ($ this ->params , $ params );
192
+ return $ this ->params ;
193
+ }
188
194
189
- foreach ($ params as $ key => $ value ) {
190
- if (is_int ($ key )) {
191
- throw new coding_exception ('Url parameters can not have numeric keys! ' );
192
- }
193
- if (!is_string ($ value )) {
194
- if (is_array ($ value )) {
195
- throw new coding_exception ('Url parameters values can not be arrays! ' );
196
- }
197
- if (is_object ($ value ) && !method_exists ($ value , '__toString ' )) {
198
- throw new coding_exception ('Url parameters values can not be objects, unless __toString() is defined! ' );
199
- }
195
+ /**
196
+ * Converts given URL parameter values that are not arrays into strings.
197
+ *
198
+ * This is to maintain the same behaviour as the original params() function.
199
+ *
200
+ * @param array $params
201
+ * @return array
202
+ */
203
+ private function clean_url_params (array $ params ): array {
204
+ // Convert all values to strings.
205
+ // This was the original implementation of the params function,
206
+ // which we have kept for backwards compatibility.
207
+ array_walk_recursive ($ params , function (&$ value ) {
208
+ if (is_object ($ value ) && !method_exists ($ value , '__toString ' )) {
209
+ throw new coding_exception ('Url parameters values can not be objects, unless __toString() is defined! ' );
200
210
}
201
- $ this -> params [ $ key ] = (string )$ value ;
202
- }
203
- return $ this -> params ;
211
+ $ value = (string ) $ value ;
212
+ });
213
+ return $ params ;
204
214
}
205
215
206
216
/**
@@ -246,7 +256,7 @@ public function remove_all_params($unused = null) {
246
256
*
247
257
* @param string $paramname name
248
258
* @param string $newvalue Param value. If new value specified current value is overriden or parameter is added
249
- * @return mixed string parameter value, null if parameter does not exist
259
+ * @return array| string|null parameter value, null if parameter does not exist.
250
260
*/
251
261
public function param ($ paramname , $ newvalue = '' ) {
252
262
if (func_num_args () > 1 ) {
@@ -261,28 +271,65 @@ public function param($paramname, $newvalue = '') {
261
271
}
262
272
263
273
/**
264
- * Merges parameters and validates them
274
+ * Merges parameters.
265
275
*
266
276
* @param null|array $overrideparams
267
277
* @return array merged parameters
268
- * @throws coding_exception
269
278
*/
270
279
protected function merge_overrideparams (?array $ overrideparams = null ) {
271
- $ overrideparams = (array )$ overrideparams ;
272
- $ params = $ this ->params ;
273
- foreach ($ overrideparams as $ key => $ value ) {
274
- if (is_int ($ key )) {
275
- throw new coding_exception ('Overridden parameters can not have numeric keys! ' );
276
- }
277
- if (is_array ($ value )) {
278
- throw new coding_exception ('Overridden parameters values can not be arrays! ' );
279
- }
280
- if (is_object ($ value ) && !method_exists ($ value , '__toString ' )) {
281
- throw new coding_exception ('Overridden parameters values can not be objects, unless __toString() is defined! ' );
280
+ $ overrideparams = (array ) $ overrideparams ;
281
+ $ overrideparams = $ this ->clean_url_params ($ overrideparams );
282
+ return array_merge ($ this ->params , $ overrideparams );
283
+ }
284
+
285
+ /**
286
+ * Recursively transforms the given array of values to query string parts.
287
+ *
288
+ * Example query string parts: a=2, a[0]=2
289
+ *
290
+ * @param array $data Data to encode into query string parts. Can be a multi level array. All end values must be strings.
291
+ * @return array array of query string parts. All parts are rawurlencoded.
292
+ */
293
+ private function recursively_transform_params_to_query_string_parts (array $ data ): array {
294
+ $ stringparams = [];
295
+
296
+ // Define a recursive function to encode the array into a set of string params.
297
+ // We need to do this recursively, so that multi level array parameters are properly supported.
298
+ $ parsestringparams = function (array $ data ) use (&$ stringparams , &$ parsestringparams ) {
299
+ foreach ($ data as $ key => $ value ) {
300
+ // If this is an array, rewrite the $value keys to track the position in the array.
301
+ // and pass back to this function recursively until the values are no longer arrays.
302
+ // E.g. if $key is 'a' and $value was [0 => true, 1 => false]
303
+ // the new array becomes ['a[0]' => true, 'a[1]' => false].
304
+ if (is_array ($ value )) {
305
+ $ newvalue = [];
306
+ foreach ($ value as $ innerkey => $ innervalue ) {
307
+ $ newkey = $ key . '[ ' . $ innerkey . '] ' ;
308
+ $ newvalue [$ newkey ] = $ innervalue ;
309
+ }
310
+ $ parsestringparams ($ newvalue );
311
+ } else {
312
+ // Else no more arrays to traverse - build the final query string part.
313
+ // We enforce that all end values are strings for consistency.
314
+ // When params() is used, it will convert all params given to strings.
315
+ // This will catch out anyone setting the params property directly.
316
+ if (!is_string ($ value )) {
317
+ throw new coding_exception ('Unexpected query string value type.
318
+ All values that are not arrays should be a string. ' );
319
+ }
320
+
321
+ if (isset ($ value ) && $ value !== '' ) {
322
+ $ stringparams [] = rawurlencode ($ key ) . '= ' . rawurlencode ($ value );
323
+ } else {
324
+ $ stringparams [] = rawurlencode ($ key );
325
+ }
326
+ }
282
327
}
283
- $ params [$ key ] = (string )$ value ;
284
- }
285
- return $ params ;
328
+ };
329
+
330
+ $ parsestringparams ($ data );
331
+
332
+ return $ stringparams ;
286
333
}
287
334
288
335
/**
@@ -296,29 +343,18 @@ protected function merge_overrideparams(?array $overrideparams = null) {
296
343
* @return string query string that can be added to a url.
297
344
*/
298
345
public function get_query_string ($ escaped = true , ?array $ overrideparams = null ) {
299
- $ arr = [];
300
346
if ($ overrideparams !== null ) {
301
347
$ params = $ this ->merge_overrideparams ($ overrideparams );
302
348
} else {
303
349
$ params = $ this ->params ;
304
350
}
305
- foreach ($ params as $ key => $ val ) {
306
- if (is_array ($ val )) {
307
- foreach ($ val as $ index => $ value ) {
308
- $ arr [] = rawurlencode ($ key . '[ ' . $ index . '] ' ) . "= " . rawurlencode ($ value );
309
- }
310
- } else {
311
- if (isset ($ val ) && $ val !== '' ) {
312
- $ arr [] = rawurlencode ($ key ) . "= " . rawurlencode ($ val );
313
- } else {
314
- $ arr [] = rawurlencode ($ key );
315
- }
316
- }
317
- }
351
+
352
+ $ stringparams = $ this ->recursively_transform_params_to_query_string_parts ($ params );
353
+
318
354
if ($ escaped ) {
319
- return implode ('& ' , $ arr );
355
+ return implode ('& ' , $ stringparams );
320
356
} else {
321
- return implode ('& ' , $ arr );
357
+ return implode ('& ' , $ stringparams );
322
358
}
323
359
}
324
360
@@ -330,17 +366,28 @@ public function get_query_string($escaped = true, ?array $overrideparams = null)
330
366
* @return array params array for templates.
331
367
*/
332
368
public function export_params_for_template (): array {
333
- $ data = [];
334
- foreach ($ this ->params as $ key => $ val ) {
335
- if (is_array ($ val )) {
336
- foreach ($ val as $ index => $ value ) {
337
- $ data [] = ['name ' => $ key . '[ ' . $ index . '] ' , 'value ' => $ value ];
338
- }
339
- } else {
340
- $ data [] = ['name ' => $ key , 'value ' => $ val ];
369
+ $ querystringparts = $ this ->recursively_transform_params_to_query_string_parts ($ this ->params );
370
+
371
+ return array_map (function ($ value ) {
372
+ // First urldecode it, they are encoded by default.
373
+ $ value = rawurldecode ($ value );
374
+
375
+ // Now seperate the parts into name and value.
376
+ // splitting on the first '=' sign.
377
+ $ parts = explode ('= ' , $ value , 2 );
378
+
379
+ // Parts must be of length 1 or 2, anything else is an invalid.
380
+ if (count ($ parts ) !== 1 && count ($ parts ) !== 2 ) {
381
+ throw new coding_exception ('Invalid query string construction, unexpected number of parts ' );
341
382
}
342
- }
343
- return $ data ;
383
+
384
+ // There might not always be a '=' e.g. when the value is an empty string.
385
+ // in this case, just fallback to an empty string.
386
+ $ name = $ parts [0 ];
387
+ $ value = $ parts [1 ] ?? '' ;
388
+
389
+ return ['name ' => $ name , 'value ' => $ value ];
390
+ }, $ querystringparts );
344
391
}
345
392
346
393
/**
@@ -847,7 +894,7 @@ public function get_path($includeslashargument = true) {
847
894
* Returns a given parameter value from the URL.
848
895
*
849
896
* @param string $name Name of parameter
850
- * @return string Value of parameter or null if not set
897
+ * @return array| string|null Value of parameter or null if not set.
851
898
*/
852
899
public function get_param ($ name ) {
853
900
if (array_key_exists ($ name , $ this ->params )) {
0 commit comments