MDL-81031 core: Add JS client-side validation

This commit is contained in:
Andrew Nicols 2023-11-08 15:28:05 +08:00 committed by Jun Pataleta
parent 091ae55c20
commit f64ce7b46e
No known key found for this signature in database
GPG Key ID: F83510526D99E2C7
5 changed files with 81 additions and 0 deletions

View File

@ -36,22 +36,26 @@ enum param: string {
/**
* PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
*/
#[param_clientside_regex('^[a-zA-Z]+$')]
case ALPHA = 'alpha';
/**
* PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed
* NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
*/
#[param_clientside_regex('^[a-zA-Z_\-]*$')]
case ALPHAEXT = 'alphaext';
/**
* PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
*/
#[param_clientside_regex('^[a-zA-Z0-9]*$')]
case ALPHANUM = 'alphanum';
/**
* PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
*/
#[param_clientside_regex('^[a-zA-Z0-9_\-]*$')]
case ALPHANUMEXT = 'alphanumext';
/**
@ -108,6 +112,7 @@ enum param: string {
* This is preferred over PARAM_FLOAT for numbers typed in by the user.
* Cleans localised numbers to computer readable numbers; false for invalid numbers.
*/
#[param_clientside_regex('^\d*([\.,])\d+$')]
case LOCALISEDFLOAT = 'localisedfloat';
/**
@ -165,6 +170,7 @@ enum param: string {
/**
* PARAM_SAFEDIR - safe directory name, suitable for include() and require()
*/
#[param_clientside_regex('^[a-zA-Z0-9_\-]*$')]
case SAFEDIR = 'safedir';
/**
@ -173,11 +179,13 @@ enum param: string {
*
* This is NOT intended to be used for absolute paths or any user uploaded files.
*/
#[param_clientside_regex('^[a-zA-Z0-9\/_\-]*$')]
case SAFEPATH = 'safepath';
/**
* PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9. Numbers and comma only.
*/
#[param_clientside_regex('^[0-9,]*$')]
case SEQUENCE = 'sequence';
/**
@ -316,6 +324,7 @@ enum param: string {
* Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
* NOTE: numbers and underscores are strongly discouraged in plugin names!
*/
#[param_clientside_regex('^[a-z][a-z0-9]*(_(?:[a-z][a-z0-9_](?!__))*)?[a-z0-9]+$')]
case COMPONENT = 'component';
/**
@ -323,6 +332,7 @@ enum param: string {
* It is usually used together with context id and component.
* Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
*/
#[param_clientside_regex('^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$')]
case AREA = 'area';
/**
@ -330,6 +340,7 @@ enum param: string {
* Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
* NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
*/
#[param_clientside_regex('^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$')]
case PLUGIN = 'plugin';
/**
@ -409,6 +420,21 @@ enum param: string {
return $this->{$methodname}($value);
}
/**
* Get the clientside regular expression for this parameter.
*
* @return null|string
*/
public function get_clientside_expression(): ?string {
$ref = new \ReflectionClassConstant(self::class, $this->name);
$attributes = $ref->getAttributes(param_clientside_regex::class);
if (count($attributes) === 0) {
return null;
}
return $attributes[0]->newInstance()->regex;
}
/**
* Returns a value for the named variable, taken from request arguments.
*

View File

@ -0,0 +1,40 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core;
use Attribute;
/**
* A JS-compatible regular expression to validate the format of a param.
*
* @package core
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[Attribute(Attribute::TARGET_CLASS_CONSTANT)]
class param_clientside_regex {
/**
* Create a clientside regular expression for use with a \core\param enum case.
*
* @param string $regex The Regular Expression that validates the param case
*/
public function __construct(
/** @var string The Regular Expression that validates the param case */
public readonly string $regex,
) {
}
}

View File

@ -234,6 +234,10 @@ abstract class openapi_base {
default => 'string',
};
if ($pattern = $type->get_clientside_expression()) {
$data->pattern = $pattern;
}
return $data;
}
}

View File

@ -122,7 +122,12 @@ final class request_validator_test extends route_testcase {
* When a pathtype fails to validate, it will result in an HttpNotFoundException.
*/
public function test_validate_request_invalid_path_component(): void {
// Most of the path types are converted to regexes and will lead to a 404 before they get this far.
$type = param::INT;
$this->assertEmpty(
$type->get_clientside_expression(),
'This test requires a type with no clientside expression. Please update the test.',
);
$route = new route(
path: '/example/{required}',
@ -145,7 +150,12 @@ final class request_validator_test extends route_testcase {
* When a pathtype fails to validate, it will result in an HttpNotFoundException.
*/
public function test_validate_request_invalid_path_component_native(): void {
// Most of the path types are converted to regexes and will lead to a 404 before they get this far.
$type = param::ALPHA;
$this->assertNotEmpty(
$type->get_clientside_expression(),
'This test requires a type with clientside expression. Please update the test.',
);
$route = new route(
path: '/example/{required}',

View File

@ -187,5 +187,6 @@ final class parameter_test extends route_testcase {
$description = $param->get_openapi_description(new specification());
$this->assertNotNull($description->schema);
$this->assertEquals('string', $description->schema->type);
$this->assertIsString($description->schema->pattern);
}
}