mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 00:12:56 +02:00
Merge branch 'MDL-72096-master' of https://github.com/mickhawkins/moodle
This commit is contained in:
commit
d0fc23cd06
@ -1852,3 +1852,97 @@ function get_max_courses_in_category() {
|
||||
return $CFG->maxcoursesincategory;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a safe ORDER BY statement from user interactable requests.
|
||||
*
|
||||
* This allows safe user specified sorting (ORDER BY), by abstracting the SQL from the value being requested by the user.
|
||||
* A standard string (and optional direction) can be specified, which will be mapped to a predefined allow list of SQL ordering.
|
||||
* The mapping can optionally include a 'default', which will be used if the key provided is invalid.
|
||||
*
|
||||
* Example usage:
|
||||
* -If $orderbymap = [
|
||||
* 'courseid' => 'c.id',
|
||||
* 'somecustomvalue'=> 'c.startdate, c.shortname',
|
||||
* 'default' => 'c.fullname',
|
||||
* ]
|
||||
* -A value from the map array's keys can be passed in by a user interaction (eg web service) along with an optional direction.
|
||||
* -get_safe_orderby($orderbymap, 'courseid', 'DESC') would return: ORDER BY c.id DESC
|
||||
* -get_safe_orderby($orderbymap, 'somecustomvalue') would return: ORDER BY c.startdate, c.shortname
|
||||
* -get_safe_orderby($orderbymap, 'invalidblah', 'DESC') would return: ORDER BY c.fullname DESC
|
||||
* -If no default key was specified in $orderbymap, the invalidblah example above would return empty string.
|
||||
*
|
||||
* @param array $orderbymap An array in the format [keystring => sqlstring]. A default fallback can be set with the key 'default'.
|
||||
* @param string $orderbykey A string to be mapped to a key in $orderbymap.
|
||||
* @param string $direction Optional ORDER BY direction (ASC/DESC, case insensitive).
|
||||
* @param bool $useprefix Whether ORDER BY is prefixed to the output (true by default). This should not be modified in most cases.
|
||||
* It is included to enable get_safe_orderby_multiple() to use this function multiple times.
|
||||
* @return string The ORDER BY statement, or empty string if $orderbykey is invalid and no default is mapped.
|
||||
*/
|
||||
function get_safe_orderby(array $orderbymap, string $orderbykey, string $direction = '', bool $useprefix = true): string {
|
||||
$orderby = $useprefix ? ' ORDER BY ' : '';
|
||||
$output = '';
|
||||
|
||||
// Only include an order direction if ASC/DESC is explicitly specified (case insensitive).
|
||||
$direction = strtoupper($direction);
|
||||
if (!in_array($direction, ['ASC', 'DESC'], true)) {
|
||||
$direction = '';
|
||||
} else {
|
||||
$direction = " {$direction}";
|
||||
}
|
||||
|
||||
// Prepare the statement if the key maps to a defined sort parameter.
|
||||
if (isset($orderbymap[$orderbykey])) {
|
||||
$output = "{$orderby}{$orderbymap[$orderbykey]}{$direction}";
|
||||
} else if (array_key_exists('default', $orderbymap)) {
|
||||
// Fall back to use the default if one is specified.
|
||||
$output = "{$orderby}{$orderbymap['default']}{$direction}";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a safe ORDER BY statement from user interactable requests using multiple values.
|
||||
*
|
||||
* This allows safe user specified sorting (ORDER BY) similar to get_safe_orderby(), but supports multiple keys and directions.
|
||||
* This is useful in cases where combinations of columns are needed and/or each item requires a specified direction (ASC/DESC).
|
||||
* The mapping can optionally include a 'default', which will be used if the key provided is invalid.
|
||||
*
|
||||
* Example usage:
|
||||
* -If $orderbymap = [
|
||||
* 'courseid' => 'c.id',
|
||||
* 'fullname'=> 'c.fullname',
|
||||
* 'default' => 'c.startdate',
|
||||
* ]
|
||||
* -An array of values from the map's keys can be passed in by a user interaction (eg web service), with optional directions.
|
||||
* -get_safe_orderby($orderbymap, ['courseid', 'fullname'], ['DESC', 'ASC']) would return: ORDER BY c.id DESC, c.fullname ASC
|
||||
* -get_safe_orderby($orderbymap, ['courseid', 'invalidblah'], ['aaa', 'DESC']) would return: ORDER BY c.id, c.startdate DESC
|
||||
* -If no default key was specified in $orderbymap, the invalidblah example above would return: ORDER BY c.id
|
||||
*
|
||||
* @param array $orderbymap An array in the format [keystring => sqlstring]. A default fallback can be set with the key 'default'.
|
||||
* @param array $orderbykeys An array of strings to be mapped to keys in $orderbymap.
|
||||
* @param array $directions Optional array of ORDER BY direction (ASC/DESC, case insensitive).
|
||||
* The array keys should match array keys in $orderbykeys.
|
||||
* @return string The ORDER BY statement, or empty string if $orderbykeys contains no valid items and no default is mapped.
|
||||
*/
|
||||
function get_safe_orderby_multiple(array $orderbymap, array $orderbykeys, array $directions = []): string {
|
||||
$output = '';
|
||||
|
||||
// Check each key for a valid mapping and add to the ORDER BY statement (invalid entries will be empty strings).
|
||||
foreach ($orderbykeys as $index => $orderbykey) {
|
||||
$direction = $directions[$index] ?? '';
|
||||
$safeorderby = get_safe_orderby($orderbymap, $orderbykey, $direction, false);
|
||||
|
||||
if (!empty($safeorderby)) {
|
||||
$output .= ", {$safeorderby}";
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix with ORDER BY if any valid ordering is specified (and remove comma from the start).
|
||||
if (!empty($output)) {
|
||||
$output = ' ORDER BY' . ltrim($output, ',');
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
@ -894,4 +894,205 @@ class core_datalib_testcase extends advanced_testcase {
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals([$userids[1], $userids[3]], array_keys($results));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_get_safe_orderby().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_safe_orderby_provider(): array {
|
||||
$orderbymap = [
|
||||
'courseid' => 'c.id',
|
||||
'somecustomvalue' => 'c.startdate, c.shortname',
|
||||
'default' => 'c.fullname',
|
||||
];
|
||||
$orderbymapnodefault = [
|
||||
'courseid' => 'c.id',
|
||||
'somecustomvalue' => 'c.startdate, c.shortname',
|
||||
];
|
||||
|
||||
return [
|
||||
'Valid option, no direction specified' => [
|
||||
$orderbymap,
|
||||
'somecustomvalue',
|
||||
'',
|
||||
' ORDER BY c.startdate, c.shortname',
|
||||
],
|
||||
'Valid option, valid direction specified' => [
|
||||
$orderbymap,
|
||||
'courseid',
|
||||
'DESC',
|
||||
' ORDER BY c.id DESC',
|
||||
],
|
||||
'Valid option, valid lowercase direction specified' => [
|
||||
$orderbymap,
|
||||
'courseid',
|
||||
'asc',
|
||||
' ORDER BY c.id ASC',
|
||||
],
|
||||
'Valid option, invalid direction specified' => [
|
||||
$orderbymap,
|
||||
'courseid',
|
||||
'BOOP',
|
||||
' ORDER BY c.id',
|
||||
],
|
||||
'Valid option, invalid lowercase direction specified' => [
|
||||
$orderbymap,
|
||||
'courseid',
|
||||
'boop',
|
||||
' ORDER BY c.id',
|
||||
],
|
||||
'Invalid option default fallback, with valid direction' => [
|
||||
$orderbymap,
|
||||
'thisdoesnotexist',
|
||||
'ASC',
|
||||
' ORDER BY c.fullname ASC',
|
||||
],
|
||||
'Invalid option default fallback, with invalid direction' => [
|
||||
$orderbymap,
|
||||
'thisdoesnotexist',
|
||||
'BOOP',
|
||||
' ORDER BY c.fullname',
|
||||
],
|
||||
'Invalid option without default, with valid direction' => [
|
||||
$orderbymapnodefault,
|
||||
'thisdoesnotexist',
|
||||
'ASC',
|
||||
'',
|
||||
],
|
||||
'Invalid option without default, with invalid direction' => [
|
||||
$orderbymapnodefault,
|
||||
'thisdoesnotexist',
|
||||
'NOPE',
|
||||
'',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the get_safe_orderby function.
|
||||
*
|
||||
* @dataProvider get_safe_orderby_provider
|
||||
* @param array $orderbymap The ORDER BY parameter mapping array.
|
||||
* @param string $orderbykey The string key being provided, to check against the map.
|
||||
* @param string $direction The optional direction to order by.
|
||||
* @param string $expected The expected string output of the method.
|
||||
*/
|
||||
public function test_get_safe_orderby(array $orderbymap, string $orderbykey, string $direction, string $expected): void {
|
||||
$actual = get_safe_orderby($orderbymap, $orderbykey, $direction);
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_get_safe_orderby_multiple().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_safe_orderby_multiple_provider(): array {
|
||||
$orderbymap = [
|
||||
'courseid' => 'c.id',
|
||||
'firstname' => 'u.firstname',
|
||||
'default' => 'c.startdate',
|
||||
];
|
||||
$orderbymapnodefault = [
|
||||
'courseid' => 'c.id',
|
||||
'firstname' => 'u.firstname',
|
||||
];
|
||||
|
||||
return [
|
||||
'Valid options, no directions specified' => [
|
||||
$orderbymap,
|
||||
['courseid', 'firstname'],
|
||||
[],
|
||||
' ORDER BY c.id, u.firstname',
|
||||
],
|
||||
'Valid options, some direction specified' => [
|
||||
$orderbymap,
|
||||
['courseid', 'firstname'],
|
||||
['DESC'],
|
||||
' ORDER BY c.id DESC, u.firstname',
|
||||
],
|
||||
'Valid options, all directions specified' => [
|
||||
$orderbymap,
|
||||
['courseid', 'firstname'],
|
||||
['ASC', 'desc'],
|
||||
' ORDER BY c.id ASC, u.firstname DESC',
|
||||
],
|
||||
'Valid options, valid and invalid directions specified' => [
|
||||
$orderbymap,
|
||||
['courseid', 'firstname'],
|
||||
['BOOP', 'DESC'],
|
||||
' ORDER BY c.id, u.firstname DESC',
|
||||
],
|
||||
'Valid options, all invalid directions specified' => [
|
||||
$orderbymap,
|
||||
['courseid', 'firstname'],
|
||||
['BOOP', 'SNOOT'],
|
||||
' ORDER BY c.id, u.firstname',
|
||||
],
|
||||
'Valid and invalid option default fallback, with valid directions' => [
|
||||
$orderbymap,
|
||||
['thisdoesnotexist', 'courseid'],
|
||||
['asc', 'DESC'],
|
||||
' ORDER BY c.startdate ASC, c.id DESC',
|
||||
],
|
||||
'Valid and invalid option default fallback, with invalid direction' => [
|
||||
$orderbymap,
|
||||
['courseid', 'thisdoesnotexist'],
|
||||
['BOOP', 'SNOOT'],
|
||||
' ORDER BY c.id, c.startdate',
|
||||
],
|
||||
'Valid and invalid option without default, with valid direction' => [
|
||||
$orderbymapnodefault,
|
||||
['thisdoesnotexist', 'courseid'],
|
||||
['ASC', 'DESC'],
|
||||
' ORDER BY c.id DESC',
|
||||
],
|
||||
'Valid and invalid option without default, with invalid direction' => [
|
||||
$orderbymapnodefault,
|
||||
['thisdoesnotexist', 'courseid'],
|
||||
['BOOP', 'SNOOT'],
|
||||
' ORDER BY c.id',
|
||||
],
|
||||
'Invalid option only without default, with valid direction' => [
|
||||
$orderbymapnodefault,
|
||||
['thisdoesnotexist'],
|
||||
['ASC'],
|
||||
'',
|
||||
],
|
||||
'Invalid option only without default, with invalid direction' => [
|
||||
$orderbymapnodefault,
|
||||
['thisdoesnotexist'],
|
||||
['BOOP'],
|
||||
'',
|
||||
],
|
||||
'Single valid option, direction specified' => [
|
||||
$orderbymap,
|
||||
['firstname'],
|
||||
['ASC'],
|
||||
' ORDER BY u.firstname ASC',
|
||||
],
|
||||
'Single valid option, direction not specified' => [
|
||||
$orderbymap,
|
||||
['firstname'],
|
||||
[],
|
||||
' ORDER BY u.firstname',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the get_safe_orderby_multiple function.
|
||||
*
|
||||
* @dataProvider get_safe_orderby_multiple_provider
|
||||
* @param array $orderbymap The ORDER BY parameter mapping array.
|
||||
* @param array $orderbykeys The array of string keys being provided, to check against the map.
|
||||
* @param array $directions The optional directions to order by.
|
||||
* @param string $expected The expected string output of the method.
|
||||
*/
|
||||
public function test_get_safe_orderby_multiple(array $orderbymap, array $orderbykeys, array $directions,
|
||||
string $expected): void {
|
||||
$actual = get_safe_orderby_multiple($orderbymap, $orderbykeys, $directions);
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +123,10 @@ completely removed from Moodle core too.
|
||||
* New html_table attribute "$responsive" which defaults to true. When set to true, tables created via html_writer::table() will be enclosed
|
||||
in a .table-responsive div container which will allow the table to be scrolled horizontally with ease, especially when the table is rendered in smaller viewports.
|
||||
Set to false to prevent the table from being enclosed in the responsive container.
|
||||
* Two new helper functions have been added to lib/datalib.php, for safely preparing SQL ORDER BY statements where user
|
||||
interactions define sort parameters (see the respective docblocks for full details and examples):
|
||||
-get_safe_orderby() - where a single sort parameter is required.
|
||||
-get_safe_orderby_multiple() - where multiple sort parameters are required.
|
||||
|
||||
=== 3.11.4 ===
|
||||
* A new option dontforcesvgdownload has been added to the $options parameter of the send_file() function.
|
||||
|
Loading…
x
Reference in New Issue
Block a user