Merge branch 'MDL-72096-master' of https://github.com/mickhawkins/moodle

This commit is contained in:
Eloy Lafuente (stronk7) 2021-11-15 19:02:13 +01:00
commit d0fc23cd06
3 changed files with 299 additions and 0 deletions

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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.