Skip to content

Commit

Permalink
MDL-81031 core: Add JS client-side validation
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewnicols authored and junpataleta committed Aug 6, 2024
1 parent 091ae55 commit f64ce7b
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 0 deletions.
26 changes: 26 additions & 0 deletions lib/classes/param.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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';

/**
Expand All @@ -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';

/**
Expand Down Expand Up @@ -316,20 +324,23 @@ 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';

/**
* PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
* 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';

/**
* PARAM_PLUGIN is used for plugin names such as 'forum = 'glossary', 'ldap', 'paypal', 'completionstatus'.
* 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';

/**
Expand Down Expand Up @@ -409,6 +420,21 @@ public function clean(mixed $value): mixed {
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.
*
Expand Down
40 changes: 40 additions & 0 deletions lib/classes/param_clientside_regex.php
Original file line number Diff line number Diff line change
@@ -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,
) {
}
}
4 changes: 4 additions & 0 deletions lib/classes/router/schema/openapi_base.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ public function get_schema_from_type(param $type): stdClass {
default => 'string',
};

if ($pattern = $type->get_clientside_expression()) {
$data->pattern = $pattern;
}

return $data;
}
}
10 changes: 10 additions & 0 deletions lib/tests/router/request_validator_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,12 @@ public function test_validate_request_missing_path_component(): void {
* 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}',
Expand All @@ -145,7 +150,12 @@ public function test_validate_request_invalid_path_component(): void {
* 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}',
Expand Down
1 change: 1 addition & 0 deletions lib/tests/router/schema/parameter_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,5 +187,6 @@ public function test_schema_includes_clientside_pattern(): void {
$description = $param->get_openapi_description(new specification());
$this->assertNotNull($description->schema);
$this->assertEquals('string', $description->schema->type);
$this->assertIsString($description->schema->pattern);
}
}

0 comments on commit f64ce7b

Please sign in to comment.