This commit is contained in:
Shamim Rezaie
2024-08-06 16:01:09 +08:00
committed by Jun Pataleta
861 changed files with 23655 additions and 8778 deletions
@@ -0,0 +1,13 @@
issueNumber: MDL-81031
notes:
core:
- message: |
The following classes have been renamed.
Existing classes are currently unaffected.
| Old class name | New class name |
| --- | --- |
| `\core_user` | `\core\user` |
type: improved
+10 -1
View File
@@ -109,7 +109,16 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
// Add the 'profiling' page to admin block.
$ADMIN->add('development', $temp);
// Web service test clients DO NOT COMMIT : THE EXTERNAL WEB PAGE IS NOT AN ADMIN PAGE !!!!!
$ADMIN->add(
parentname: 'development',
something: new admin_externalpage(
name: 'swaggerui',
visiblename: new lang_string('swaggerui', 'admin'),
url: "$CFG->wwwroot/admin/swaggerui.php",
),
);
// Web service test clients DO NOT COMMIT : THE EXTERNAL WEB PAGE IS NOT AN ADMIN PAGE !!!!!
$ADMIN->add('development', new admin_externalpage('testclient', new lang_string('testclient', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/testclient.php"));
+87
View File
@@ -0,0 +1,87 @@
<?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/>.
/**
* Swagger UI for Moodle
*
* @package core_admin
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require('../config.php');
require_once($CFG->libdir . '/adminlib.php');
$swaggerversion = '5.17.14';
$PAGE->set_url('/admin/swaggerui.php');
admin_externalpage_setup('swaggerui');
$PAGE->requires->css(new moodle_url("https://unpkg.com/swagger-ui-dist@{$swaggerversion}/swagger-ui.css"));
echo $OUTPUT->header();
// These have to be manually added for now because they must be made cross-origin. The `js` method does not yet support this.
echo html_writer::tag(
tagname: 'script',
contents: '',
attributes: [
'src' => new moodle_url("https://unpkg.com/swagger-ui-dist@{$swaggerversion}/swagger-ui-bundle.js"),
'crossorigin' => 'crossorigin',
],
);
echo html_writer::tag(
tagname: 'script',
contents: '',
attributes: [
'src' => new moodle_url("https://unpkg.com/swagger-ui-plugin-hierarchical-tags"),
'crossorigin' => 'crossorigin',
],
);
$openapipath = moodle_url::routed_path('/api/rest/v2/openapi.json')->out();
$swaggerinit = <<<JS
window.ui = SwaggerUIBundle({
url: "{$openapipath}",
dom_id: '#swagger-ui',
// Enable the "Try it out" button by default.
tryItOutEnabled: true,
// Show snippets different OS options.
requestSnippetsEnabled: true,
deepLinking: true,
plugins: [
HierarchicalTagsPlugin,
],
hierarchicalTagSeparator: /[_]/
});
JS;
$PAGE->requires->js_init_code(
jscode: $swaggerinit,
ondomready: true,
);
echo html_writer::div('', '', [
'id' => 'swagger-ui',
]);
echo $OUTPUT->footer();
+8
View File
@@ -180,6 +180,14 @@ $CFG->wwwroot = 'http://example.com/moodle';
$CFG->dataroot = '/home/example/moodledata';
// Whether the Moodle router is fully configured.
//
// From Moodle 4.5 this is set to false.
// The default value will change in a future release.
//
// When not configured on the web server it must be accessed via https://example.com/moodle/r.php
// When configured the on the web server the 'r.php' may be removed.
$CFG->routerconfigured = false;
//=========================================================================
// 4. DATA FILES PERMISSIONS
+1
View File
@@ -1354,6 +1354,7 @@ $string['supportemailsubject'] = 'Site support request - {$a}';
$string['supportavailability'] = 'Support availability';
$string['supportname'] = 'Support name';
$string['supportpage'] = 'Contact site support link';
$string['swaggerui'] = 'Moodle REST API UI (SwaggerUI)';
$string['suspenduser'] = 'Suspend user account';
$string['switchlang'] = 'Switch lang directory';
$string['systempaths'] = 'System paths';
+1
View File
@@ -63,6 +63,7 @@ $string['cachedef_eventinvalidation'] = 'Event invalidation';
$string['cachedef_externalbadges'] = 'External badges for particular user';
$string['cachedef_fontawesomeiconmapping'] = 'Mapping of icons for font awesome';
$string['cachedef_file_imageinfo'] = 'File image info e.g. dimensions';
$string['cachedef_routes'] = 'Route data';
$string['cachedef_suspended_userids'] = 'List of suspended users per course';
$string['cachedef_groupdata'] = 'Course group information';
$string['cachedef_h5p_content_type_translations'] = 'H5P content-type libraries translations';
+1
View File
@@ -390,6 +390,7 @@ $string['invalidxmlfile'] = '"{$a}" is not a valid XML file';
$string['iplookupfailed'] = 'Cannot find geo information about this IP address {$a}';
$string['iplookupprivate'] = 'Cannot display lookup of private IP address';
$string['ipmismatch'] = 'Client IP address mismatch';
$string['itemnotfound'] = 'No {$a->itemtype} was found with an identifier of \'{$a->identifier}\'';
$string['listcantmovedown'] = 'Failed to move item down, as it is the last of its peers.';
$string['listcantmoveleft'] = 'Failed to move item left, as it has no parent';
$string['listcantmoveright'] = 'Failed to move item right, as there is no peer to make it a child of. Move it below another peer and then you can move it right.';
+10
View File
@@ -0,0 +1,10 @@
define("core/fetch",["exports","core/config","./pending"],(function(_exports,_config,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* The core/fetch module allows you to make web service requests to the Moodle API.
*
* @module core/fetch
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.request=_exports.performPut=_exports.performPost=_exports.performHead=_exports.performGet=_exports.performDelete=void 0,_config=_interopRequireDefault(_config),_pending=_interopRequireDefault(_pending);const normaliseComponent=component=>component.replace(/^core_/,""),getRequest=(component,endpoint,_ref)=>{let{params:params={},body:body=null,method:method="GET"}=_ref;const url=new URL("".concat(_config.default.apibase,"rest/v2/").concat(component,"/").concat(endpoint)),options={method:method,headers:{Accept:"application/json","Content-Type":"application/json"}};return Object.entries(params).forEach((_ref2=>{let[key,value]=_ref2;url.searchParams.append(key,value)})),body&&(body instanceof FormData?options.body=body:options.body=body instanceof Object?JSON.stringify(body):body),new Request(url,options)},request=async function(component,action){let{params:params={},body:body=null,method:method="GET"}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const pending=new _pending.default("Requesting ".concat(component,"/").concat(action," with ").concat(method)),result=await fetch(getRequest(normaliseComponent(component),action,{params:params,method:method,body:body}));if(pending.resolve(),result.ok)return result.json();throw new Error(result.statusText)};_exports.request=request;_exports.performGet=function(component,action){let{params:params={}}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return request(component,action,{params:params,method:"GET"})};_exports.performHead=function(component,action){let{params:params={}}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return request(component,action,{params:params,method:"HEAD"})};_exports.performPost=function(component,action){let{body:body}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return request(component,action,{body:body,method:"POST"})};_exports.performPut=function(component,action){let{body:body}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return request(component,action,{body:body,method:"POST"})};_exports.performDelete=function(component,action){let{params:params={},body:body=null}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return request(component,action,{body:body,params:params,method:"DELETE"})}}));
//# sourceMappingURL=fetch.min.js.map
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
define("core/utils",["exports","core/pending"],(function(_exports,_pending){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.throttle=_exports.getNormalisedComponent=_exports.debounce=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};_exports.throttle=(func,wait)=>{let onCooldown=!1,runAgain=null;const run=function(){for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++)args[_key]=arguments[_key];runAgain=null!==runAgain,onCooldown||(func.apply(this,args),onCooldown=!0,setTimeout((()=>{const recurse=runAgain;onCooldown=!1,runAgain=null,recurse&&run(args)}),wait))};return run};const debounceMap=new Map;_exports.debounce=function(func,wait){let{pending:pending=!1,cancel:cancel=!1}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},timeout=null;const returnedFunction=function(){for(var _len2=arguments.length,args=new Array(_len2),_key2=0;_key2<_len2;_key2++)args[_key2]=arguments[_key2];pending&&!debounceMap.has(returnedFunction)&&debounceMap.set(returnedFunction,new _pending.default("core/utils:debounce")),clearTimeout(timeout),timeout=setTimeout((async()=>{const pendingPromise=debounceMap.get(returnedFunction);debounceMap.delete(returnedFunction),await func.apply(undefined,args),null==pendingPromise||pendingPromise.resolve()}),wait)};return cancel&&(returnedFunction.cancel=()=>{const pendingPromise=debounceMap.get(returnedFunction);null==pendingPromise||pendingPromise.resolve(),clearTimeout(timeout)}),returnedFunction};_exports.getNormalisedComponent=component=>component&&"moodle"!==component&&"core"!==component?component:"core"}));
define("core/utils",["exports","core/pending","jquery"],(function(_exports,_pending,_jquery){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.wrapPromiseInWhenable=_exports.throttle=_exports.getNormalisedComponent=_exports.debounce=void 0,_pending=_interopRequireDefault(_pending),_jquery=_interopRequireDefault(_jquery);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}_exports.throttle=(func,wait)=>{let onCooldown=!1,runAgain=null;const run=function(){for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++)args[_key]=arguments[_key];runAgain=null!==runAgain,onCooldown||(func.apply(this,args),onCooldown=!0,setTimeout((()=>{const recurse=runAgain;onCooldown=!1,runAgain=null,recurse&&run(args)}),wait))};return run};const debounceMap=new Map;_exports.debounce=function(func,wait){let{pending:pending=!1,cancel:cancel=!1}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},timeout=null;const returnedFunction=function(){for(var _len2=arguments.length,args=new Array(_len2),_key2=0;_key2<_len2;_key2++)args[_key2]=arguments[_key2];pending&&!debounceMap.has(returnedFunction)&&debounceMap.set(returnedFunction,new _pending.default("core/utils:debounce")),clearTimeout(timeout),timeout=setTimeout((async()=>{const pendingPromise=debounceMap.get(returnedFunction);debounceMap.delete(returnedFunction),await func.apply(undefined,args),null==pendingPromise||pendingPromise.resolve()}),wait)};return cancel&&(returnedFunction.cancel=()=>{const pendingPromise=debounceMap.get(returnedFunction);null==pendingPromise||pendingPromise.resolve(),clearTimeout(timeout)}),returnedFunction};_exports.getNormalisedComponent=component=>component&&"moodle"!==component&&"core"!==component?component:"core";_exports.wrapPromiseInWhenable=promise=>_jquery.default.when(promise)}));
//# sourceMappingURL=utils.min.js.map
File diff suppressed because one or more lines are too long
+237
View File
@@ -0,0 +1,237 @@
// 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/>.
/**
* The core/fetch module allows you to make web service requests to the Moodle API.
*
* @module core/fetch
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Cfg from 'core/config';
import PendingPromise from './pending';
/**
* Normalise the component name to remove the core_ prefix.
*
* @param {string} component
* @returns {string}
*/
const normaliseComponent = (component) => component.replace(/^core_/, '');
/**
* Get the Request object for a given API request.
*
* @param {string} component The frankenstyle component name
* @param {string} endpoint The endpoint within the componet to call
* @param {object} params
* @param {object} [params.params = {}] The parameters to pass to the API
* @param {string|Object|FormData} [params.body = null] The HTTP method to use
* @param {string} [params.method = "GET"] The HTTP method to use
* @returns {Request}
*/
const getRequest = (
component,
endpoint,
{
params = {},
body = null,
method = 'GET',
}
) => {
const url = new URL(`${Cfg.apibase}rest/v2/${component}/${endpoint}`);
const options = {
method,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
};
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
if (body) {
if (body instanceof FormData) {
options.body = body;
} else if (body instanceof Object) {
options.body = JSON.stringify(body);
} else {
options.body = body;
}
}
return new Request(url, options);
};
/**
* Make a request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {object} [params.params = {}] The parameters to pass to the API
* @param {string|Object|FormData} [params.body = null] The HTTP method to use
* @param {string} [params.method = "GET"] The HTTP method to use
* @returns {Promise<object>}
*/
const request = async(
component,
action,
{
params = {},
body = null,
method = 'GET',
} = {},
) => {
const pending = new PendingPromise(`Requesting ${component}/${action} with ${method}`);
const result = await fetch(
getRequest(
normaliseComponent(component),
action,
{params, method, body},
),
);
pending.resolve();
if (result.ok) {
return result.json();
}
throw new Error(result.statusText);
};
/**
* Make a request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {object} [params.params = {}] The parameters to pass to the API
* @returns {Promise<object>}
*/
const performGet = (
component,
action,
{
params = {},
} = {},
) => request(
component,
action,
{params, method: 'GET'},
);
/**
* Make a request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {object} [params.params = {}] The parameters to pass to the API
* @returns {Promise<object>}
*/
const performHead = (
component,
action,
{
params = {},
} = {},
) => request(
component,
action,
{params, method: 'HEAD'},
);
/**
* Make a request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {string|Object|FormData} params.body The HTTP method to use
* @returns {Promise<object>}
*/
const performPost = (
component,
action,
{
body,
} = {},
) => request(
component,
action,
{body, method: 'POST'},
);
/**
* Make a request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {string|Object|FormData} params.body The HTTP method to use
* @returns {Promise<object>}
*/
const performPut = (
component,
action,
{
body,
} = {},
) => request(
component,
action,
{body, method: 'POST'},
);
/**
* Make a request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {object} [params.params = {}] The parameters to pass to the API
* @param {string|Object|FormData} [params.body = null] The HTTP method to use
* @returns {Promise<object>}
*/
const performDelete = (
component,
action,
{
params = {},
body = null,
} = {},
) => request(
component,
action,
{
body,
params,
method: 'DELETE',
},
);
export {
request,
performGet,
performHead,
performPost,
performPut,
performDelete,
};
+9
View File
@@ -22,6 +22,7 @@
*/
import Pending from 'core/pending';
import jQuery from 'jquery';
/**
* Create a wrapper function to throttle the execution of the given
@@ -147,3 +148,11 @@ export const getNormalisedComponent = (component) => {
return 'core';
};
/**
* Wrap a Native Promise in a jQuery Whenable for b/c.
*
* @param {*} promise
* @returns {jQuery}
*/
export const wrapPromiseInWhenable = (promise) => jQuery.when(promise);
+10
View File
@@ -234,6 +234,16 @@
"allowedlevel2": true,
"allowedspread": true
},
"route": {
"component": "core",
"allowedlevel2": true,
"allowedspread": true
},
"router": {
"component": "core",
"allowedlevel2": true,
"allowedspread": true
},
"rss": {
"component": "core_rss",
"allowedlevel2": false,
+43 -32
View File
@@ -102,40 +102,51 @@ class component {
];
/** @var array<string|array<string>> associative array of PRS-4 namespaces and corresponding paths. */
protected static $psr4namespaces = [
'MaxMind' => 'lib/maxmind/MaxMind',
'GeoIp2' => 'lib/maxmind/GeoIp2',
'Sabberworm\\CSS' => 'lib/php-css-parser',
'MoodleHQ\\RTLCSS' => 'lib/rtlcss',
'ScssPhp\\ScssPhp' => 'lib/scssphp',
'OpenSpout' => 'lib/openspout/src',
'MatthiasMullie\\Minify' => 'lib/minify/matthiasmullie-minify/src/',
'MatthiasMullie\\PathConverter' => 'lib/minify/matthiasmullie-pathconverter/src/',
'IMSGlobal\LTI' => 'lib/ltiprovider/src',
'Packback\\Lti1p3' => 'lib/lti1p3/src',
'Phpml' => 'lib/mlbackend/php/phpml/src/Phpml',
'PHPMailer\\PHPMailer' => 'lib/phpmailer/src',
'RedeyeVentures\\GeoPattern' => 'lib/geopattern-php/GeoPattern',
'Firebase\\JWT' => 'lib/php-jwt/src',
'ZipStream' => 'lib/zipstream/src/',
'MyCLabs\\Enum' => 'lib/php-enum/src',
'PhpXmlRpc' => 'lib/phpxmlrpc',
'Psr\\Http\\Client' => 'lib/psr/http-client/src',
'Psr\\Http\\Message' => [
\MaxMind::class => 'lib/maxmind/MaxMind',
\GeoIp2::class => 'lib/maxmind/GeoIp2',
\Sabberworm\CSS::class => 'lib/php-css-parser',
\MoodleHQ\RTLCSS::class => 'lib/rtlcss',
\ScssPhp\ScssPhp::class => 'lib/scssphp',
\OpenSpout::class => 'lib/openspout/src',
\MatthiasMullie\Minify::class => 'lib/minify/matthiasmullie-minify/src/',
\MatthiasMullie\PathConverter::class => 'lib/minify/matthiasmullie-pathconverter/src/',
\IMSGlobal\LTI::class => 'lib/ltiprovider/src',
\Packback\Lti1p3::class => 'lib/lti1p3/src',
\Phpml::class => 'lib/mlbackend/php/phpml/src/Phpml',
\PHPMailer\PHPMailer::class => 'lib/phpmailer/src',
\RedeyeVentures\GeoPattern::class => 'lib/geopattern-php/GeoPattern',
\Firebase\JWT::class => 'lib/php-jwt/src',
\ZipStream::class => 'lib/zipstream/src/',
\MyCLabs\Enum::class => 'lib/php-enum/src',
\PhpXmlRpc::class => 'lib/phpxmlrpc',
\Psr\Http\Client::class => 'lib/psr/http-client/src',
\Psr\Http\Message::class => [
'lib/psr/http-message/src',
'lib/psr/http-factory/src',
],
'Psr\\EventDispatcher' => 'lib/psr/event-dispatcher/src',
'Psr\\Clock' => 'lib/psr/clock/src',
'Psr\\Container' => 'lib/psr/container/src',
'GuzzleHttp\\Psr7' => 'lib/guzzlehttp/psr7/src',
'GuzzleHttp\\Promise' => 'lib/guzzlehttp/promises/src',
'GuzzleHttp' => 'lib/guzzlehttp/guzzle/src',
'Kevinrob\\GuzzleCache' => 'lib/guzzlehttp/kevinrob/guzzlecache/src',
'Aws' => 'lib/aws-sdk/src',
'JmesPath' => 'lib/jmespath/src',
'Laravel\\SerializableClosure' => 'lib/laravel/serializable-closure/src',
'DI' => 'lib/php-di/php-di/src',
'Invoker' => 'lib/php-di/invoker/src',
\Psr\Http\Server::class => [
"lib/psr/http-server-handler/src",
"lib/psr/http-server-middleware/src",
],
\Psr\EventDispatcher::class => 'lib/psr/event-dispatcher/src',
\Psr\Clock::class => 'lib/psr/clock/src',
\Psr\Container::class => 'lib/psr/container/src',
\Psr\Log::class => "lib/psr/log/src",
\Psr\SimpleCache::class => 'lib/psr/simple-cache/src',
\GuzzleHttp\Psr7::class => 'lib/guzzlehttp/psr7/src',
\GuzzleHttp\Promise::class => 'lib/guzzlehttp/promises/src',
\GuzzleHttp::class => 'lib/guzzlehttp/guzzle/src',
\Kevinrob\GuzzleCache::class => 'lib/guzzlehttp/kevinrob/guzzlecache/src',
\Aws::class => 'lib/aws-sdk/src',
\JmesPath::class => 'lib/jmespath/src',
\Laravel\SerializableClosure::class => 'lib/laravel/serializable-closure/src',
\DI::class => 'lib/php-di/php-di/src',
\Complex::class => 'lib/phpspreadsheet/markbaker/classes/src',
\Matrix::class => 'lib/phpspreadsheet/markbaker/classes/src',
\PhpOffice\PhpSpreadsheet::class => 'lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet',
\Invoker::class => 'lib/php-di/invoker/src',
\FastRoute::class => 'lib/nikic/fast-route/src',
\Slim::class => 'lib/slim/slim/Slim',
];
/**
@@ -441,7 +452,7 @@ class component {
$keyclasses = [
\core\exception\moodle_exception::class,
\core\output\bootstrap_renderer::class,
\core_filters\filter_manager::class,
\core\router::class,
];
foreach ($keyclasses as $classname) {
if (!array_key_exists($classname, $cache['classmap'])) {
@@ -0,0 +1,33 @@
<?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\exception;
use core\router\response\access_denied_response;
/**
* An exception to describe the case where access has been denied to a resource.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class access_denied_exception extends moodle_exception implements response_aware_exception {
#[\Override]
public function get_response_classname(): string {
return access_denied_response::class;
}
}
@@ -0,0 +1,52 @@
<?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\exception;
use core\router\response\not_found_response;
/**
* An exception to describe the case where a requested item was not found.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class not_found_exception extends \moodle_exception implements response_aware_exception {
/**
* Constructor for a new not found exception.
*
* @param string $itemtype The type of item that was not found.
* @param string $identifier The identifier of the item that was not found.
*/
public function __construct(
string $itemtype,
string $identifier,
) {
parent::__construct(
errorcode: 'itemnotfound',
a: [
'itemtype' => $itemtype,
'identifier' => $identifier,
],
);
}
#[\Override]
public function get_response_classname(): string {
return not_found_response::class;
}
}
@@ -0,0 +1,33 @@
<?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\exception;
/**
* An exception which is aware of the response class that should be used to handle it.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface response_aware_exception {
/**
* Get the class name of the response that should be used to handle this exception.
*
* @return string
*/
public function get_response_classname(): string;
}
+4 -2
View File
@@ -17,6 +17,7 @@
namespace core\hook;
use DI\ContainerBuilder;
use DI\Definition;
use core\attribute\label;
/**
@@ -67,13 +68,14 @@ class di_configuration {
* </code>
*
* @param string $id The identifier of the container entry
* @param callable $definition The definition of the container entry
* @param callable|Definition\Definition|Definition\SelfResolvingDefinition|Definition\Helper\DefinitionHelper $definition
* The definition of the container entry
* @return self
* @example
*/
public function add_definition(
string $id,
callable $definition,
callable|Definition\Definition|Definition\SelfResolvingDefinition|Definition\Helper\DefinitionHelper $definition,
): self {
$this->builder->addDefinitions([
$id => $definition,
@@ -298,7 +298,7 @@ class page_requirements_manager {
* @return array List of safe config values that are available to javascript.
*/
public function get_config_for_javascript(moodle_page $page, renderer_base $renderer) {
global $CFG;
global $CFG, $USER;
if (empty($this->M_cfg)) {
$iconsystem = \core\output\icon_system::instance();
@@ -315,6 +315,7 @@ class page_requirements_manager {
$this->M_cfg = [
'wwwroot' => $CFG->wwwroot,
'apibase' => $this->get_api_base(),
'homeurl' => $page->navigation->action,
'sesskey' => sesskey(),
'sessiontimeout' => $CFG->sessiontimeout,
@@ -335,6 +336,7 @@ class page_requirements_manager {
'langrev' => get_string_manager()->get_revision(),
'templaterev' => $this->get_templaterev(),
'siteId' => (int) SITEID,
'userId' => (int) $USER->id,
];
if ($CFG->debugdeveloper) {
$this->M_cfg['developerdebug'] = true;
@@ -346,6 +348,29 @@ class page_requirements_manager {
return $this->M_cfg;
}
/**
* Return the base URL for the API.
*
* If the router has been fully configured on the web server then we can use the shortened route, otherwise the r.php.
*
* @return string
*/
protected function get_api_base(): string {
global $CFG;
if (!empty($CFG->router_configured)) {
return sprintf(
"%s/api/",
$CFG->wwwroot,
);
}
return sprintf(
"%s/r.php/api/",
$CFG->wwwroot,
);
}
/**
* Initialise with the bits of JavaScript that every Moodle page should have.
*
@@ -0,0 +1,50 @@
<?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\output;
use Slim\Interfaces\ErrorRendererInterface;
use Throwable;
// phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
/**
* Class routed_error_handler
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class routed_error_handler implements ErrorRendererInterface {
#[\Override]
public function __invoke(Throwable $exception, bool $displayErrorDetails): string {
// @codeCoverageIgnoreStart
if (defined('ABORT_AFTER_CONFIG') && !defined('ABORT_AFTER_CONFIG_CANCEL')) {
define('ABORT_AFTER_CONFIG_CANCEL', true);
require(__DIR__ . '/../../setup.php');
}
// @codeCoverageIgnoreEnd
if ($whoops = get_whoops()) {
$whoops->sendHttpCode($exception->getCode());
$whoops->handleException($exception);
} else {
default_exception_handler($exception);
}
return '';
}
}
+26
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.
*
+40
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 <[email protected]>
* @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,
) {
}
}
+157
View File
@@ -0,0 +1,157 @@
<?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\route\api;
use core\exception;
use core\param;
use core\router\route;
use core\output\mustache_template_source_loader;
use core\router\schema\response\payload_response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Template Controller.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class templates {
use \core\router\route_controller;
/**
* Fetch a single template for a component in a theme.
*
* @param ResponseInterface $response
* @param string $themename
* @param string $component
* @param null|string $identifier
* @return payload_response
*/
#[route(
path: '/templates/{themename}/{component}/{identifier}',
method: ['GET'],
title: 'Fetch a single template',
description: 'Fetch a single template for a component in a theme',
security: [],
pathtypes: [
new \core\router\parameters\path_themename(),
new \core\router\parameters\path_component(),
new \core\router\schema\parameters\path_parameter(
name: 'identifier',
type: param::SAFEPATH,
),
],
queryparams: [
new \core\router\schema\parameters\query_parameter(
name: 'includecomments',
type: param::BOOL,
description: 'Include comments in the template',
default: false,
),
],
headerparams: [
new \core\router\parameters\header_language(),
],
responses: [
new \core\router\schema\response\response(
statuscode: 200,
description: 'OK',
content: [
new \core\router\schema\response\content\json_media_type(
schema: new \core\router\schema\objects\schema_object(
content: [
'templates' => new \core\router\schema\objects\array_of_strings(
keyparamtype: param::TEXT,
valueparamtype: param::RAW,
),
'strings' => new \core\router\schema\objects\array_of_strings(
keyparamtype: param::TEXT,
valueparamtype: param::RAW,
),
],
),
examples: [
new \core\router\schema\example(
name: 'Single template value',
summary: 'A json response containing the template for a single template',
value: [
'templates' => [
"mod_example/template_identifier" => "<div class=\"example\">Hello World</div>",
"mod_example/other_template" => "<div class=\"example\">Hello World</div>",
],
'strings' => [
'core/loading' => 'Loading',
],
],
),
]
),
],
),
],
)]
public function get_templates(
ServerRequestInterface $request,
ResponseInterface $response,
mustache_template_source_loader $loader,
string $themename,
string $component,
string $identifier,
): payload_response {
global $PAGE;
$PAGE->set_context(\core\context\system::instance());
$params = $request->getQueryParams();
$comments = $params['includecomments'];
try {
$dependencies = $loader->load_with_dependencies(
templatecomponent: $component,
templatename: $identifier,
themename: $themename,
includecomments: $comments,
lang: $request->getHeaderLine('language'),
);
} catch (\moodle_exception $e) {
throw new exception\not_found_exception('template', "{$component}/{$identifier}");
}
$result = [
'templates' => [],
'strings' => [],
];
foreach ($dependencies['templates'] as $component => $templates) {
foreach ($templates as $template => $value) {
$result['templates']["{$component}/{$template}"] = $value;
}
}
foreach ($dependencies['strings'] as $component => $templates) {
foreach ($templates as $template => $value) {
$result['strings']["{$component}/{$template}"] = $value;
}
}
return new payload_response(
payload: $result,
request: $request,
);
}
}
+259
View File
@@ -0,0 +1,259 @@
<?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 core\output\routed_error_handler;
use core\router\middleware\cors_middleware;
use core\router\middleware\error_handling_middleware;
use core\router\middleware\moodle_bootstrap_middleware;
use core\router\middleware\moodle_route_attribute_middleware;
use core\router\middleware\uri_normalisation_middleware;
use core\router\middleware\validation_middleware;
use core\router\request_validator_interface;
use core\router\response_handler;
use core\router\response_validator_interface;
use core\router\route_loader_interface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App;
use Slim\Interfaces\RouteGroupInterface;
/**
* Moodle Router.
*
* This class represents the Moodle Router, which handles all aspects of Routing in Moodle.
*
* It should not normally be accessed or used outside of its own unit tests, the route_testcase, and the `r.php` handler.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class router {
/** @var string The base path to use for all requests */
public readonly string $basepath;
/** @var App The SlimPHP App */
protected readonly App $app;
/**
* Create a new Router.
*
* @param response_handler $responsehandler
* @param route_loader_interface $routeloader
* @param request_validator_interface $requestvalidator
* @param response_validator_interface $responsevalidator
* @param null|string $basepath
*/
public function __construct(
/** @var response_handler */
protected response_handler $responsehandler,
/** @var route_loader_interface The router loader to use */
protected readonly route_loader_interface $routeloader,
/** @var request_validator_interface */
protected request_validator_interface $requestvalidator,
/** @var response_validator_interface */
protected response_validator_interface $responsevalidator,
?string $basepath = null,
) {
if ($basepath === null) {
$basepath = $this->guess_basepath();
}
$this->basepath = $basepath;
}
/**
* Guess the basepath for the Router.
*
* @return string
*/
protected function guess_basepath(): string {
global $CFG;
// Moodle is not guaranteed to exist at the domain root.
// Strip out the current script.
$scriptroot = parse_url($CFG->wwwroot, PHP_URL_PATH);
$relativeroot = sprintf(
'%s%s',
$scriptroot,
str_replace(
realpath($CFG->dirroot),
'',
realpath($_SERVER['SCRIPT_FILENAME']),
),
);
// The server is not configured to rewrite unknown requests to automatically use the router.
if ($_SERVER && array_key_exists('REQUEST_URI', $_SERVER)) {
if (str_starts_with($_SERVER['REQUEST_URI'], $relativeroot)) {
$scriptroot .= '/r.php';
}
}
return $scriptroot;
}
/**
* Get the configured SlimPHP Application.
*
* @return App
*/
public function get_app(): App {
if (!isset($this->app)) {
$this->create_app($this->basepath);
}
return $this->app;
}
/**
* Get the Response Factory for the Router.
*
* @return ResponseFactoryInterface
*/
public function get_response_factory(): ResponseFactoryInterface {
return $this->get_app()->getResponseFactory();
}
/**
* Create the configured SlimPHP Application.
*
* @param string $basepath The base path of the Moodle instance
*/
protected function create_app(
string $basepath = '',
): void {
global $CFG;
// PHP Does not support autoloading functions.
require_once("{$CFG->libdir}/nikic/fast-route/src/functions.php");
// Create an App using the DI Bridge.
$this->app = router\bridge::create();
// Add Middleware to the App.
// Note: App Middleware is called before any Group or Route middleware.
$this->add_middleware();
$this->configure_caching();
$this->configure_routes();
// Configure the basepath for Moodle.
$this->app->setBasePath($basepath);
}
/**
* Add Middleware to the App.
*/
protected function add_middleware(): void {
// Middleware is added like an onion.
// For a Response, the outer-most middleware is executed first, and the inner-most middleware is executed last.
// For a Request, the inner-most middleware is executed first, and the outer-most middleware is executed last.
// Add the body parsing middleware from Slim.
// See https://www.slimframework.com/docs/v4/middleware/body-parsing.html for further information.
$this->app->addBodyParsingMiddleware();
// Add Middleware to Bootstrap Moodle from a request.
$this->app->add(di::get(moodle_bootstrap_middleware::class));
// Add the Moodle route attribute to the request.
// This must be processed after the Routing Middleware has been processed on the request.
$this->app->add(di::get(moodle_route_attribute_middleware::class));
// Add the Routing Middleware as one of the outer-most middleware.
// This allows the Route to be accessed before it is handled.
// See https://www.slimframework.com/docs/v4/cookbook/retrieving-current-route.html for further information.
$this->app->addRoutingMiddleware();
// Add request normalisation middleware to standardise the URI.
// This must be done before the Routing Middleware to ensure that the route is matched correctly.
$this->app->add(di::get(uri_normalisation_middleware::class));
// Add the Error Handling Middleware and configure it to show Moodle Errors for HTML pages.
$errormiddleware = $this->app->addErrorMiddleware(true, true, true);
$errorhandler = $errormiddleware->getDefaultErrorHandler();
$errorhandler->registerErrorRenderer('text/html', routed_error_handler::class);
}
/**
* Configure the API routes.
*/
protected function configure_routes(): void {
$routegroups = $this->routeloader->configure_routes($this->app);
foreach ($routegroups as $name => $collection) {
match ($name) {
route_loader_interface::ROUTE_GROUP_API => $this->configure_api_route($collection),
default => null,
};
}
}
/**
* Configure the API Route Middleware.
*
* @param RouteGroupInterface $group
*/
protected function configure_api_route(RouteGroupInterface $group): void {
$group
->add(di::get(error_handling_middleware::class))
// Add a Middleware to set the CORS headers for all REST Responses.
->add(di::get(cors_middleware::class))
->add(di::get(validation_middleware::class));
}
/**
* Configure caching for the routes.
*/
protected function configure_caching(): void {
global $CFG;
// Note: Slim uses a file cache and is not compatible with MUC.
$this->app->getRouteCollector()->setCacheFile(
sprintf(
"%s/routes.%s.cache",
$CFG->cachedir,
sha1($this->basepath),
),
);
}
/**
* Handle the specified Request.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function handle_request(
ServerRequestInterface $request,
): ResponseInterface {
return $this->get_app()->handle($request);
}
/**
* Serve the current request using global variables.
*
* @codeCoverageIgnore
*/
public function serve(): void {
$this->get_app()->run();
}
}
@@ -0,0 +1,209 @@
<?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\router;
use Slim\Interfaces\RouteInterface;
/**
* A base Route Loader
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class abstract_route_loader {
/**
* Get all routes in the namespace.
*
* @param string $namespace The namespace to get the routes for
* @param callable $componentpathcallback A callback to get the component path for a class
* @return array[]
*/
protected function get_all_routes_in_namespace(
string $namespace,
callable $componentpathcallback,
): array {
$routes = [];
// Get all classes in the namespace.
$classes = \core_component::get_component_classes_in_namespace(namespace: $namespace);
foreach (array_keys($classes) as $classname) {
$classinfo = new \ReflectionClass($classname);
$component = \core_component::get_component_from_classname($classname);
$componentpath = $componentpathcallback($component);
// Add all public methods with a #[route] attribute in this class.
array_push($routes, ...$this->get_all_routes_in_class(
componentpath: $componentpath,
classinfo: $classinfo,
));
}
return $routes;
}
/**
* Get all routes in a class.
*
* @param string $componentpath The path to the component that the class belongs to
* @param \ReflectionClass $classinfo The class to get the routes for
* @return array[]
*/
protected function get_all_routes_in_class(
string $componentpath,
\ReflectionClass $classinfo,
): array {
// Filter out any methods which are public but do not have any route attached.
return array_filter(
array_map(
fn ($methodinfo) => $this->get_route_data_for_method(
componentpath: $componentpath,
classinfo: $classinfo,
methodinfo: $methodinfo,
),
$classinfo->getMethods(\ReflectionMethod::IS_PUBLIC),
)
);
}
/**
* Get route data for a single method in a class.
*
* @param string $componentpath The path to the component that the class belongs to
* @param \ReflectionClass $classinfo The class to get the route data for
* @param \ReflectionMethod $methodinfo The method to get the route data for
* @return null|array[]
*/
protected function get_route_data_for_method(
string $componentpath,
\ReflectionClass $classinfo,
\ReflectionMethod $methodinfo,
): ?array {
$routeattribute = $this->get_route_attribute_for_method(
$classinfo,
$methodinfo,
);
if ($routeattribute === null) {
// No route on this method.
return null;
}
// Build the pattern for this route.
$path = $routeattribute->get_path();
$pattern = "/{$componentpath}{$path}";
// Remove duplicate slashes.
$pattern = preg_replace('@/+@', '/', $pattern);
// Get the HTTP methods for this route.
$httpmethods = $routeattribute->get_methods(['GET']);
return [
'methods' => $httpmethods,
'pattern' => $pattern,
'callable' => [$classinfo->getName(), $methodinfo->getName()],
];
}
/**
* Get the route attribute for the specified method.
*
* Note: If a parent has a route, but the method does not, no route will be returned.
*
* @param \ReflectionClass $classinfo The class to get the route attribute for
* @param \ReflectionMethod $methodinfo The method to get the route attribute for
* @return null|route
*/
protected function get_route_attribute_for_method(
\ReflectionClass $classinfo,
\ReflectionMethod $methodinfo,
): ?route {
// Fetch the route attribute from the method.
// Each method can only have a single route attribute.
$routeattributes = $methodinfo->getAttributes(route::class);
if (empty($routeattributes)) {
return null;
}
// Get the instance.
$methodroute = $routeattributes[0]->newInstance();
// Set the parent route if the class has one.
$classattributes = $classinfo->getAttributes(route::class);
if ($classattributes) {
// The class has a #route attribute.
$methodroute->set_parent($classattributes[0]->newInstance());
}
return $methodroute;
}
/**
* Normalise the component for use as part of the path.
*
* If the component is a subsystem, the `core_` prefix will be removed.
* If the component is 'core', it will be kept.
* All other components will use their frankenstyle name.
*
* @param string $component
* @return string
*/
protected function normalise_component_path(
string $component,
): string {
if ($component === 'core') {
return $component;
}
[$type, $subsystem] = \core_component::normalize_component($component);
if ($type === 'core') {
$component = $subsystem;
}
if ($component === null) {
$component = '';
}
return $component;
}
/**
* Set a route name for the specified callable.
*
* @param RouteInterface $slimroute
* @param string|array|callable $callable
* @return string|null The name of the route if it was set, otherwise null
*/
protected function set_route_name_for_callable(
RouteInterface $slimroute,
string|array|callable $callable,
): ?string {
if (is_string($callable)) {
$slimroute->setName($callable);
return $callable;
}
if (is_array($callable)) {
$name = implode('::', $callable);
$slimroute->setName($name);
return $name;
}
// Unable to set a name. Return null.
return null;
}
}
+159
View File
@@ -0,0 +1,159 @@
<?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\router;
use core\component;
use core\router\schema\specification;
use Psr\Http\Message\ResponseInterface;
use ReflectionClass;
use Throwable;
/**
* Moodle Router.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class apidocs {
/**
* Generate the API docs for the API.
*
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function openapi_docs(
ResponseInterface $response,
): ResponseInterface {
global $PAGE;
$PAGE->set_context(\core\context\system::instance());
$api = new specification();
$classes = $this->get_openapi_classes_names();
foreach (array_keys($classes) as $classname) {
$classinfo = new ReflectionClass($classname);
[$component] = explode('\\', $classinfo->getNamespaceName());
$classroutes = $classinfo->getAttributes(route::class);
if ($classroutes) {
foreach ($classroutes as $classroute) {
$parentroute = $classroute->newInstance();
$this->get_api_docs_for_route(
component: $component,
classinfo: $classinfo,
api: $api,
parentcontexts: [$parentroute],
);
}
} else {
$this->get_api_docs_for_route(
component: $component,
classinfo: $classinfo,
api: $api,
);
}
}
// At the moment only json is supported. This could be extended to support other formats in future.
return $response
->withHeader('Content-Type', 'application/json')
->withBody(\GuzzleHttp\Psr7\Utils::streamFor(
json_encode(
$api,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES,
),
));
}
/**
* Get the list of OpenAPI Class Names.
*
* @return string[]
*/
protected function get_openapi_classes_names(): array {
global $CFG;
$classes = [];
foreach (component::get_component_names(true) as $component) {
try {
$classes = array_merge(
$classes,
component::get_component_classes_in_namespace(
component: $component,
namespace: 'route\api',
),
);
// @codeCoverageIgnoreStart
} catch (Throwable $error) {
// Some kind of error occurred whilst loading routes in this component.
// When debugging, this is useful to know.
// When not, log to error_log.
if (!$CFG->debugdisplay) {
debugging('Error loading route data: ' . $error->getMessage());
} else {
default_exception_handler($error);
}
}
// @codeCoverageIgnoreEnd
}
return $classes;
}
/**
* Get the API Docs for the specified Route.
*
* @param string $component The component that the route relates to
* @param ReflectionClass $classinfo
* @param specification $api
* @param array $parentcontexts
* @return self
*/
protected function get_api_docs_for_route(
string $component,
ReflectionClass $classinfo,
specification $api,
array $parentcontexts = [],
): self {
$methods = $classinfo->getMethods();
foreach ($methods as $method) {
if (!$method->isPublic()) {
continue;
}
// Get the route attribute for this method.
$routeattribute = util::get_route_instance_for_method(
[$classinfo->getName(), $method->getName()],
);
if ($routeattribute === null) {
// This method has no route attribute. Maybe just a helper method.
continue;
}
// Add this path to the OpenAPI specification.
$api->add_path(
component: $component,
route: $routeattribute,
);
}
return $this;
}
}
+95
View File
@@ -0,0 +1,95 @@
<?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/>.
declare(strict_types=1);
namespace core\router;
use core\di;
use Invoker\Invoker;
use Invoker\ParameterResolver\AssociativeArrayResolver;
use Invoker\ParameterResolver\Container\TypeHintContainerResolver;
use Invoker\ParameterResolver\DefaultValueResolver;
use Invoker\ParameterResolver\ResolverChain;
use Psr\Container\ContainerInterface;
use Slim\App;
use Slim\Factory\AppFactory;
use Slim\Interfaces\CallableResolverInterface;
use Slim\Interfaces\InvocationStrategyInterface;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* This factory creates a Slim application correctly configured with PHP-DI.
*
* To use this, replace `Slim\Factory\AppFactory::create()`
* with `DI\Bridge\Slim\Bridge::create()`.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class bridge {
/**
* Create a new Slim application with PHP-DI.
*
* @param ContainerInterface|null $container
* @return App
*/
public static function create(?ContainerInterface $container = null): App {
$container = $container ?: di::get_container();
di::set(
CallableResolverInterface::class,
new callable_resolver(new \Invoker\CallableResolver($container)),
);
$app = AppFactory::createFromContainer($container);
di::set(App::class, $app);
$controllerinvoker = static::create_controller_invoker($container);
$app->getRouteCollector()->setDefaultInvocationStrategy($controllerinvoker);
return $app;
}
/**
* Create a controller invoker
*
* @param ContainerInterface $container
* @return InvocationStrategyInterface
*/
protected static function create_controller_invoker(ContainerInterface $container): InvocationStrategyInterface {
$resolvers = [
// Inject parameters by name first.
new AssociativeArrayResolver(),
// Then inject services by type-hints for those that weren't resolved.
new TypeHintContainerResolver($container),
// Then fall back on parameters default values for optional route parameters.
new DefaultValueResolver(),
];
$invoker = new Invoker(new ResolverChain($resolvers), $container);
return new controller_invoker(
container: $container,
invoker: $invoker,
);
}
}
+106
View File
@@ -0,0 +1,106 @@
<?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\router;
use Invoker\Exception\NotCallableException;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Interfaces\AdvancedCallableResolverInterface;
// phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Resolve middleware and route callables using PHP-DI.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class callable_resolver implements AdvancedCallableResolverInterface {
/**
* Create a new instance of the Callable Resolver.
*
* @param \Invoker\CallableResolver $callableresolver The DI Callable Resolver instance
*/
public function __construct(
/** @var \Invoker\CallableResolver The DI Callable Resolver instance */
protected \Invoker\CallableResolver $callableresolver,
) {
}
#[\Override]
public function resolve($toResolve): callable {
return $this->callableresolver->resolve($this->translate_notation($toResolve));
}
#[\Override]
public function resolveRoute($toResolve): callable {
return $this->resolve_possible_signature($toResolve, 'handle', RequestHandlerInterface::class);
}
#[\Override]
public function resolveMiddleware($toResolve): callable {
return $this->resolve_possible_signature($toResolve, 'process', MiddlewareInterface::class);
}
/**
* Translate Slim string callable notation ('nameOrKey:method') to PHP-DI notation ('nameOrKey::method').
*
* For a full list of supported callables, see the Slim Docs at
* https://www.slimframework.com/docs/v4/objects/routing.html#container-resolution.
*
* @param mixed $toresolve
* @return mixed
*/
private function translate_notation(mixed $toresolve): mixed {
if (is_string($toresolve) && preg_match(\Slim\CallableResolver::$callablePattern, $toresolve)) {
$toresolve = str_replace(':', '::', $toresolve);
}
return $toresolve;
}
/**
* Resolve a possible signature for a callable.
*
* @param mixed $toresolve The callable to resolve
* @param string $method The method to resolve
* @param string $typename The type name to resolve
*/
private function resolve_possible_signature(
mixed $toresolve,
string $method,
string $typename,
): callable {
if (is_string($toresolve)) {
$toresolve = $this->translate_notation($toresolve);
try {
$callable = $this->callableresolver->resolve([$toresolve, $method]);
if (is_array($callable) && $callable[0] instanceof $typename) {
return $callable;
}
} catch (NotCallableException $e) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
// Fall back to looking for a generic callable.
}
}
return $this->callableresolver->resolve($toresolve);
}
}
+98
View File
@@ -0,0 +1,98 @@
<?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\router;
use Invoker\InvokerInterface;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Controller Invoker for the Moodle Router.
*
* This class handles invocation of the route callable, and the conversion of the response into an appropriate format.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class controller_invoker implements \Slim\Interfaces\InvocationStrategyInterface {
/**
* Create a new controller invoker.
*
* @param ContainerInterface $container
* @param InvokerInterface $invoker
*/
public function __construct(
/** @var ContainerInterface The DI container */
protected ContainerInterface $container,
/** @var InvokerInterface The invoker */
protected InvokerInterface $invoker,
) {
}
#[\Override]
public function __invoke(
callable $callable,
ServerRequestInterface $request,
ResponseInterface $response,
array $routeArguments, // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
): ResponseInterface {
// Inject the request and response by parameter name.
$parameters = [
'request' => self::inject_route_arguments(
$request,
$routeArguments, // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
),
'response' => $response,
];
// Inject the route arguments by name.
$parameters += $routeArguments; // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
// Inject the attributes defined on the request.
$parameters += $request->getAttributes();
$result = $this->invoker->call($callable, $parameters);
return $this->container->get(response_handler::class)->standardise_response($result);
}
/**
* Helper to inject route arguments.
*
* This is based on the ControllerInvoker.
*
* @param ServerRequestInterface $request
* @param array $routeargs
* @return ServerRequestInterface
*/
private static function inject_route_arguments(
ServerRequestInterface $request,
array $routeargs,
): ServerRequestInterface {
$args = $request;
foreach ($routeargs as $key => $value) {
// Note: This differs to upstream where route args always override attributes.
// We apply mapped parameters via route attributes and must therefore override the route args.
if (!$args->getAttribute($key)) {
$args = $args->withAttribute($key, $value);
}
}
return $args;
}
}
+49
View File
@@ -0,0 +1,49 @@
<?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\router;
/**
* Class hook_callbacks
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class hook_callbacks {
/**
* Provide DI Configuration for the Router system.
*
* @param \core\hook\di_configuration $hook
* @codeCoverageIgnore
*/
public static function provide_di_configuration(
\core\hook\di_configuration $hook,
): void {
$hook->add_definition(
request_validator_interface::class,
\DI\get(request_validator::class),
);
$hook->add_definition(
response_validator_interface::class,
\DI\get(response_validator::class),
);
$hook->add_definition(
route_loader_interface::class,
\DI\get(route_loader::class),
);
}
}
@@ -0,0 +1,47 @@
<?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\router\middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Routing\RouteContext;
/**
* Middleware to add CORS headers to the response.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cors_middleware implements MiddlewareInterface {
#[\Override]
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
$routecontext = RouteContext::fromRequest($request);
$routingresults = $routecontext->getRoutingResults();
$methods = $routingresults->getAllowedMethods();
$response = $handler->handle($request);
return $response
->withHeader('Content-Type', 'application/json')
->withHeader('Content-Disposition', 'inline')
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', implode(',', $methods))
->withHeader('Access-Control-Allow-Headers', 'Content-Type, api_key, Authorization');
}
}
@@ -0,0 +1,61 @@
<?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\router\middleware;
use core\router\response_handler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Middleware to handle errors in a route callable.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class error_handling_middleware implements MiddlewareInterface {
/**
* Create a new instance of the error handling middleware.
*
* @param response_handler $responsehandler A handler to standardise a response
*/
public function __construct(
/** @var response_handler A handler to standardise a response */
protected response_handler $responsehandler,
) {
}
#[\Override]
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
try {
$response = $handler->handle($request);
} catch (\Exception $e) {
// @codeCoverageIgnoreStart
if (defined('ABORT_AFTER_CONFIG') && !defined('ABORT_AFTER_CONFIG_CANCEL')) {
define('ABORT_AFTER_CONFIG_CANCEL', true);
require(__DIR__ . '/../../../setup.php');
}
// @codeCoverageIgnoreEnd
$response = $this->responsehandler->get_response_from_exception($request, $e);
}
return $response;
}
}
@@ -0,0 +1,101 @@
<?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\router\middleware;
use core\router\util;
use core\router\route_loader_interface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Middleware to set flags and define setup.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodle_bootstrap_middleware implements MiddlewareInterface {
#[\Override]
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
global $PAGE;
if (str_contains($request->getUri(), route_loader_interface::ROUTE_GROUP_API)) {
// @codeCoverageIgnoreStart
if (!defined('AJAX_SCRIPT')) {
define('AJAX_SCRIPT', true);
}
// @codeCoverageIgnoreEnd
}
$routeattribute = util::get_route_instance_for_request($request);
if ($routeattribute && !$routeattribute->cookies) {
// @codeCoverageIgnoreStart
// This request should not access Moodle cookies.
if (!defined('NO_MOODLE_COOKIES')) {
define('NO_MOODLE_COOKIES', true);
}
// @codeCoverageIgnoreEnd
}
if (!$routeattribute || !$routeattribute->abortafterconfig) {
// Do not load the full Moodle stack. This is a lightweight request.
$this->load_full_moodle();
}
$PAGE->set_url((string) $request->getUri());
return $handler->handle($request);
}
/**
* Check whether Moodle is fully loaded.
*
* @return bool
* @codeCoverageIgnore
*/
public function is_full_moodle_loaded(): bool {
if (defined('ABORT_AFTER_CONFIG')) {
return defined('ABORT_AFTER_CONFIG_CANCEL');
}
return true;
}
/**
* Load the full Moodle Framework.
*
* @codeCoverageIgnore
*/
protected function load_full_moodle(): void {
// Note: These globals should be defined even if they are not used as they are used in the require.
global $CFG, $DB, $SESSION, $OUTPUT, $PAGE;
if ($this->is_full_moodle_loaded()) {
return;
}
// Ok, now we need to start normal moodle script, we need to load all libs and $DB.
if (defined('ABORT_AFTER_CONFIG_CANCEL') && ABORT_AFTER_CONFIG_CANCEL) {
return;
}
define('ABORT_AFTER_CONFIG_CANCEL', true);
require("{$CFG->dirroot}/lib/setup.php");
}
}
@@ -0,0 +1,39 @@
<?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\router\middleware;
use core\router\route;
use core\router\util;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Middleware to add the Moodle route attribute.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodle_route_attribute_middleware implements MiddlewareInterface {
#[\Override]
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
$routeattribute = util::get_route_instance_for_request($request);
return $handler->handle($request->withAttribute(route::class, $routeattribute));
}
}
@@ -0,0 +1,61 @@
<?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\router\middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Middleware to normalise the URI path.
*
* This middleware will:
* - remove duplicate /
* - remove any trailing /
* - ensure that there is a leading /
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class uri_normalisation_middleware implements MiddlewareInterface {
#[\Override]
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
$uri = $request->getUri();
$path = $uri->getPath();
// Remove duplicate slashes.
$path = preg_replace('@/+@', '/', $path);
// Remove trailing slashes.
$path = rtrim($path, '/');
// Ensure that there is always a path.
// Note: This must be performed after handling removal of duplicate and trailing slashes.
if ($path === '') {
$path = '/';
}
if ($uri->getPath() !== $path) {
// Path has changed. Update it.
$request = $request->withUri($uri->withPath($path));
}
return $handler->handle($request);
}
}
@@ -0,0 +1,72 @@
<?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\router\middleware;
use core\router\request_validator_interface;
use core\router\response_handler;
use core\router\response_validator_interface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Middleware to handle validation of request and response based on the route data.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class validation_middleware implements MiddlewareInterface {
/**
* Create a new instance of the validation middleware.
*
* @param response_handler $responsehandler A handler to standardise a response
* @param request_validator_interface $requestvalidator A request validator
* @param response_validator_interface $responsevalidator A response validator
*/
public function __construct(
/** @var response_handler A handler to standardise a response */
protected response_handler $responsehandler,
/** @var request_validator_interface The request validator used to validate incoming data */
protected request_validator_interface $requestvalidator,
/** @var response_validator_interface The response validator used to validate incoming data */
protected response_validator_interface $responsevalidator,
) {
}
#[\Override]
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
try {
$request = $this->requestvalidator->validate_request($request);
} catch (\Exception $e) {
return $this->responsehandler->get_response_from_exception($request, $e);
}
$response = $handler->handle($request);
try {
$this->responsevalidator->validate_response($request, $response);
} catch (\Exception $e) {
return $this->responsehandler->get_response_from_exception($request, $e);
}
return $response;
}
}
@@ -0,0 +1,69 @@
<?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\router\parameters;
use core\param;
use core\router\schema\example;
use core\router\schema\referenced_object;
/**
* A header to accept an optional language for the requested content.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class header_language extends \core\router\schema\parameters\header_object implements referenced_object {
/**
* Create a new path_component parameter.
*
* @param string $name The name of the parameter to use for the component name
* @param mixed ...$extra Additional arguments
*/
public function __construct(
string $name = 'language',
...$extra,
) {
global $CFG;
$extra['name'] = $name;
$extra['type'] = param::LANG;
$extra['description'] = 'The language of the requested response.';
// Generally speaking, the default language should be the site default.
// This is a value which is usually stored in DB, so we have a fallback for when the full
// Moodle configuration has not been loaded.
$extra['default'] = $CFG->lang ?? 'en';
$extra['examples'] = [
new example(
name: 'Site default',
value: null,
),
new example(
name: 'English',
value: 'en',
),
new example(
name: 'Deutsch (kids)',
value: 'de_kids',
),
];
parent::__construct(...$extra);
}
}
@@ -0,0 +1,65 @@
<?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\router\parameters;
use core\param;
use core\router\schema\example;
use core\router\schema\referenced_object;
/**
* A component path parameter.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class path_component extends \core\router\schema\parameters\path_parameter implements referenced_object {
/**
* Create a new path_component parameter.
*
* @param string $name The name of the parameter to use for the component name
* @param mixed ...$extra Additional arguments
*/
public function __construct(
string $name = 'component',
...$extra,
) {
$extra['name'] = $name;
$extra['type'] = param::COMPONENT;
$extra['description'] = 'The name of a Moodle component, in frankenstyle format.';
$extra['examples'] = [
new example(
name: 'The core subsystem',
value: 'core',
),
new example(
name: 'The Course subsystem',
value: 'core_course',
),
new example(
name: 'An activity module',
value: 'mod_assign',
),
new example(
name: 'An assignment subplugin',
value: 'assignsubmission_file',
),
];
parent::__construct(...$extra);
}
}
@@ -0,0 +1,135 @@
<?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\router\parameters;
use core\exception\not_found_exception;
use core\param;
use core\router\schema\example;
use core\router\schema\parameters\mapped_property_parameter;
use core\router\schema\referenced_object;
use Psr\Http\Message\ServerRequestInterface;
/**
* A Moodle parameter referenced in the path.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class path_course extends \core\router\schema\parameters\path_parameter implements
mapped_property_parameter,
referenced_object
{
/**
* Create a new path_course parameter.
*
* @param string $name The name of the parameter to use for the course identifier
* @param mixed ...$extra Additional arguments
*/
public function __construct(
string $name = 'course',
...$extra,
) {
$extra['name'] = $name;
$extra['type'] = param::RAW;
$extra['description'] = <<<EOF
The course identifier.
This can be the id of the course, the idnumber of the course, or the shortname of the course.
If specifying a course idnumber, the value should be in the format `idnumber:[idnumber]`.
If specifying a course shortname, the value should be in the format `name:[shortname]`.
EOF;
$extra['examples'] = [
new example(
name: 'A course id',
value: 54,
),
new example(
name: 'A course specified by its idnumber',
value: 'idnumber:000117-physics-101-1',
),
new example(
name: 'A course specified by its shortname',
value: 'name:000117-phys101-0',
),
];
parent::__construct(...$extra);
}
/**
* Get the course object for the given identifier.
*
* @param string $value A course id, idnumber, or shortname
* @return object
* @throws not_found_exception If the course cannot be found
*/
protected function get_course_for_value(string $value): mixed {
global $DB;
$data = false;
if (is_numeric($value)) {
$data = $DB->get_record('course', [
'id' => $value,
]);
} else if (str_starts_with($value, 'idnumber:')) {
$data = $DB->get_record('course', [
'idnumber' => substr($value, strlen('idnumber:')),
]);
} else if (str_starts_with($value, 'name:')) {
$data = $DB->get_record('course', [
'shortname' => substr($value, strlen('name:')),
]);
}
if ($data) {
return $data;
}
throw new not_found_exception('course', $value);
}
#[\Override]
public function add_attributes_for_parameter_value(
ServerRequestInterface $request,
string $value,
): ServerRequestInterface {
$course = $this->get_course_for_value($value);
return $request
->withAttribute($this->name, $course)
->withAttribute("{$this->name}context", \core\context\course::instance($course->id));
}
#[\Override]
public function get_schema_from_type(param $type): \stdClass {
$schema = parent::get_schema_from_type($type);
$schema->pattern = "^(";
$schema->pattern .= implode("|", [
'\d+',
'idnumber:.+',
'name:.+',
]);
$schema->pattern .= ")$";
return $schema;
}
}
@@ -0,0 +1,58 @@
<?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\router\parameters;
use core\param;
use core\router\schema\referenced_object;
use core\router\schema\example;
/**
* Routing parameter for validation.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class path_themename extends \core\router\schema\parameters\path_parameter implements referenced_object {
/**
* Create a new path_themename parameter.
*
* @param string $name The name of the parameter to use for the theme name
* @param mixed ...$args Additional arguments
*/
public function __construct(
string $name = 'themename',
...$args,
) {
$args['name'] = $name;
$args['type'] = param::ALPHANUMEXT;
$args['description'] = 'The name of a Moodle theme.';
$args['examples'] = [
new example(
name: 'The Boost theme',
value: 'boost',
),
new example(
name: 'The Classic theme',
value: 'classic',
),
];
parent::__construct(...$args);
}
}
+143
View File
@@ -0,0 +1,143 @@
<?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\router\parameters;
use core\exception\not_found_exception;
use core\param;
use core\user;
use core\router\schema\example;
use core\router\schema\parameters\mapped_property_parameter;
use core\router\schema\referenced_object;
use Psr\Http\Message\ServerRequestInterface;
/**
* A parameter representing a user.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class path_user extends \core\router\schema\parameters\path_parameter implements
mapped_property_parameter,
referenced_object
{
/**
* Create a new instance of the path_user.
*
* @param string $name The name of the parameter to use for the identifier
* @param mixed ...$extra Additional arguments
*/
public function __construct(
string $name = 'user',
...$extra,
) {
$extra['name'] = $name;
$extra['type'] = param::RAW;
$extra['description'] = <<<EOF
The user identifier.
This can be the magic string 'current', or the user's id, idnumber, or username.
If specifying an id, the value should be in the format `id:[id]`.
If specifying an idnumber, the value should be in the format `idnumber:[idnumber]`.
If specifying a username, the value should be in the format `username:[username]`.
EOF;
$extra['examples'] = [
new example(
name: 'The current user',
value: 'current',
),
new example(
name: 'A user specified by their user id',
value: '94853',
),
new example(
name: 'A user specified by their idnumber',
value: 'idnumber:some-student-idnumber ',
),
new example(
name: 'A user specified by their username',
value: 'username:lyona1',
),
];
parent::__construct(...$extra);
}
/**
* Get the user object for the given identifier.
*
* @param string $value A user id, idnumber, or username
* @return object
* @throws not_found_exception If the user cannot be found
*/
protected function get_user_for_value(string $value): mixed {
global $USER;
if ($value === 'current') {
return $USER;
}
$data = null;
if (is_numeric($value)) {
$data = user::get_user($value);
} else if (str_starts_with($value, 'idnumber:')) {
$data = user::get_user_by_idnumber(substr($value, strlen('idnumber:')));
} else if (str_starts_with($value, 'username:')) {
$data = user::get_user_by_username(substr($value, strlen('username:')));
}
if ($data) {
return $data;
}
throw new not_found_exception('user', $value);
}
#[\Override]
public function add_attributes_for_parameter_value(
ServerRequestInterface $request,
string $value,
): ServerRequestInterface {
$user = $this->get_user_for_value($value);
$request = $request->withAttribute($this->name, $user);
if ($user->id) {
$request = $request->withAttribute("{$this->name}context", \core\context\user::instance($user->id));
}
return $request;
}
#[\Override]
public function get_schema_from_type(param $type): \stdClass {
$schema = parent::get_schema_from_type($type);
$schema->pattern = "^(";
$schema->pattern .= implode("|", [
'current',
'\d+',
'idnumber:.+',
'username:.+',
]);
$schema->pattern .= ")$";
return $schema;
}
}
+197
View File
@@ -0,0 +1,197 @@
<?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\router;
use invalid_parameter_exception;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Exception\HttpNotFoundException;
use Slim\Interfaces\RouteInterface;
use Slim\Routing\RouteContext;
/**
* Routing attribute.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class request_validator implements request_validator_interface {
/**
* Validate the request content.
*
* @param ServerRequestInterface $request
* @return ServerRequestInterface
*/
public function validate_request(
ServerRequestInterface $request,
): ServerRequestInterface {
$moodleroute = $request->getAttribute(route::class);
if (!$moodleroute) {
return $request;
}
// Add a Route middleware to validate the path, and parameters.
$slimroute = RouteContext::fromRequest($request)->getRoute();
// Validate that the path arguments are valid.
// If they are not, then an Exception should be thrown.
$request = $this->validate_path($request, $moodleroute, $slimroute);
// Validate query parameters.
$request = $this->validate_query($request, $moodleroute);
// Validate request headers.
$request = $this->validate_request_header($request, $moodleroute);
// Validate request body parameters.
// Found in POST, PUT, DELETE, etc.
$request = $this->validate_request_body($request, $moodleroute);
return $request;
}
/**
* Validate that the path arguments match those supplied in the route.
*
* @param ServerRequestInterface $request
* @param route $moodleroute
* @param RouteInterface $slimroute The route to validate.
* @return ServerRequestInterface
* @throws \coding_exception
*/
protected function validate_path(
ServerRequestInterface $request,
route $moodleroute,
RouteInterface $slimroute,
): ServerRequestInterface {
$requiredparams = count(array_filter(
$moodleroute->get_path_parameters(),
fn ($pathtype) => $pathtype->is_required($moodleroute),
));
if ($requiredparams > count($slimroute->getArguments())) {
throw new \coding_exception(sprintf(
"Route %s has %d arguments, but %d pathtypes were specified.",
$slimroute->getPattern(),
count($slimroute->getArguments()),
count($moodleroute->get_path_parameters()),
));
}
foreach ($moodleroute->get_path_parameters() as $pathtype) {
try {
$request = $pathtype->validate($request, $slimroute);
} catch (invalid_parameter_exception $e) {
throw new HttpNotFoundException($request, $e->getMessage());
}
}
return $request;
}
/**
* Validate that the query parameters match those supplied in the route.
*
* @param ServerRequestInterface $request
* @param route $moodleroute
* @return ServerRequestInterface
*/
protected function validate_query(
ServerRequestInterface $request,
route $moodleroute,
): ServerRequestInterface {
$requestparams = $request->getQueryParams();
$paramnames = array_map(
fn ($param) => $param->get_name($this),
$moodleroute->get_query_parameters(),
);
// Check for any undeclared parameters.
$unknownparams = array_diff(
array_keys($requestparams),
$paramnames,
);
// Remove these from the URL.
// They will still be accessible via optional_param.
$request = $request->withQueryParams(
array_diff_key(
$requestparams,
array_flip($unknownparams),
),
);
foreach ($moodleroute->get_query_parameters() as $queryparam) {
$request = $queryparam->validate($request, $request->getQueryParams());
}
return $request;
}
/**
* Validate that the request headers match the schema.
*
* @param ServerRequestInterface $request
* @param route $moodleroute
* @return ServerRequestInterface
*/
protected function validate_request_header(
ServerRequestInterface $request,
route $moodleroute,
): ServerRequestInterface {
$headerparams = $moodleroute->get_header_parameters();
foreach ($headerparams as $headerparam) {
$request = $headerparam->validate($request);
}
return $request;
}
/**
* Validate that the request body matches the schema.
*
* @param ServerRequestInterface $request
* @param route $moodleroute
* @return ServerRequestInterface
*/
protected function validate_request_body(
ServerRequestInterface $request,
route $moodleroute,
): ServerRequestInterface {
if ($moodleroute->get_request_body() === null) {
// Clear the parsed body if there should not be one.
return $request->withParsedBody([]);
}
$bodyconfig = $moodleroute->get_request_body()->get_body_for_request($request);
$bodyschema = $bodyconfig->get_schema();
$parsedbody = $request->getParsedBody();
if (empty($parsedbody)) {
if ($moodleroute->get_request_body()->is_required()) {
throw new invalid_parameter_exception('Missing request body.');
}
// No body to validate.
return $request;
}
return $request->withParsedBody(
$bodyschema->validate_data($request->getParsedBody()),
);
}
}
@@ -0,0 +1,38 @@
<?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\router;
use Psr\Http\Message\ServerRequestInterface;
/**
* Routing attribute.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface request_validator_interface {
/**
* Validate the request content.
*
* @param ServerRequestInterface $request
* @return ServerRequestInterface
*/
public function validate_request(
ServerRequestInterface $request,
): ServerRequestInterface;
}
@@ -0,0 +1,36 @@
<?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\router\response;
/**
* A response for when access is denied to a resource.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class access_denied_response extends exception_response {
#[\Override]
public static function get_exception_status_code(): int {
return 403;
}
#[\Override]
protected static function get_response_description(): string {
return 'Access was denied to the resource.';
}
}
@@ -0,0 +1,37 @@
<?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\router\response;
use core\router\schema\referenced_object;
/**
* A standard empty 204 response.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class empty_response extends \core\router\schema\response\response implements
referenced_object
{
public function __construct(...$args) {
parent::__construct(
statuscode: 204,
description: 'A successful response with no content.',
);
}
}
@@ -0,0 +1,135 @@
<?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\router\response;
use core\param;
use core\router\schema\objects\scalar_type;
use core\router\schema\objects\schema_object;
use core\router\schema\objects\stacktrace;
use core\router\schema\referenced_object;
use core\router\schema\response\content\payload_response_type;
use core\router\schema\response\payload_response;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ServerRequestInterface;
/**
* A standard response for user preferences.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class exception_response extends \core\router\schema\response\response implements
referenced_object
{
/**
* Constructor for a new exception-related response.
*/
public function __construct() {
parent::__construct(
statuscode: static::get_exception_status_code(),
description: static::get_response_description(),
content: new payload_response_type(
schema: static::get_response_schema(),
),
);
}
/**
* Get the response for the exception.
*
* @param ServerRequestInterface $request
* @param \Exception $exception
* @param mixed[] ...$extra
* @return payload_response
*/
public static function get_response(
ServerRequestInterface $request,
\Exception $exception,
...$extra,
): payload_response {
return new payload_response(
payload: static::get_payload_data($exception, ...$extra),
request: $request,
response: new Response(
status: static::get_exception_status_code(),
body: $exception->getMessage(),
reason: explode("\n", $exception->getMessage())[0],
),
);
}
/**
* Get the schema for the response.
*
* @return schema_object
*/
protected static function get_response_schema(): schema_object {
return new schema_object(
content: [
'message' => new scalar_type(
type: param::ALPHANUMEXT,
description: 'The message of the exception.',
),
'errorcode' => new scalar_type(
type: param::ALPHANUMEXT,
description: 'The error code of the exception.',
),
'stacktrace' => new stacktrace(),
],
);
}
/**
* The status code that this exception should return.
*
* @return int
*/
protected static function get_exception_status_code(): int {
return 500;
}
/**
* Get the description of this response.
*
* @return string
*/
abstract protected static function get_response_description(): string;
/**
* Get the response payload data.
*
* @param \Exception $exception
* @param mixed ...$extra
* @return array
*/
protected static function get_payload_data(
\Exception $exception,
...$extra,
): array {
$data = [
'message' => $exception->getMessage(),
'stacktrace' => $exception->getTrace(),
];
if (is_a($exception, \moodle_exception::class)) {
$data['errorcode'] = $exception->errorcode;
}
return $data;
}
}
@@ -0,0 +1,36 @@
<?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\router\response;
/**
* A standard response for user preferences.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class invalid_parameter_response extends exception_response {
#[\Override]
public static function get_exception_status_code(): int {
return 400;
}
#[\Override]
protected static function get_response_description(): string {
return 'The parameter provided was invalid in some way.';
}
}
@@ -0,0 +1,36 @@
<?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\router\response;
/**
* A standard response for user preferences.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class not_found_response extends exception_response {
#[\Override]
public static function get_exception_status_code(): int {
return 404;
}
#[\Override]
protected static function get_response_description(): string {
return 'The item was not found.';
}
}
+103
View File
@@ -0,0 +1,103 @@
<?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\router;
use core\exception\invalid_parameter_exception;
use core\exception\response_aware_exception;
use core\router;
use core\router\response\exception_response;
use core\router\response\invalid_parameter_response;
use core\router\schema\response\response_type;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Controller Invoker for the Moodle Router.
*
* This class handles invocation of the route callable, and the conversion of the response into an appropriate format.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class response_handler {
/**
* Create a new response handler.
*
* @param ContainerInterface $container
*/
public function __construct(
/** @var ContainerInterface */
private readonly ContainerInterface $container,
) {
}
/**
* Invoke a route callable.
*
* Note: Much of this is copied from the parent class, but we need to handle the response differently.
*
* @param ResponseInterface|response_type $response The response object.
* @return ResponseInterface The response from the callable.
*/
public function standardise_response(
ResponseInterface | response_type $response,
): ResponseInterface {
if ($response instanceof ResponseInterface) {
// An object implementing ResponseInterface is returned, so we can just return it.
return $response;
}
$responsefactory = $this->container->get(router::class)->get_response_factory();
// This must be a response\response_type.
return $response->get_response($responsefactory);
}
/**
* Get the response from an exception.
*
* @param ServerRequestInterface $request
* @param \Exception $exception
*
* @return ResponseInterface
*/
public function get_response_from_exception(
ServerRequestInterface $request,
\Exception $exception,
): ResponseInterface {
$response = match (true) {
// Newer exceptions may be response-aware, so we can use the response class they specify.
(is_a($exception, response_aware_exception::class)) => $exception->get_response_classname()::get_response(
$request,
$exception,
),
// Some legacy expressions are here for the moment.
is_a($exception, invalid_parameter_exception::class) => invalid_parameter_response::get_response(
$request,
$exception,
),
// Otherwise use the default.
default => exception_response::get_response($request, $exception),
};
return $this->standardise_response($response);
}
}
+50
View File
@@ -0,0 +1,50 @@
<?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\router;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Response Validator.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class response_validator implements response_validator_interface {
#[\Override]
public function validate_response(
ServerRequestInterface $request,
ResponseInterface $response,
): void {
$moodleroute = $request->getAttribute(route::class);
if (!$moodleroute) {
return;
}
$expectedresponse = $moodleroute->get_response_with_status_code($response->getStatusCode());
if (!$expectedresponse) {
// Decide what we should do here.
// Probably just throw heaps of debugging information.
// Maybe Except with debugging enabled.
return;
} else {
$expectedresponse->validate($response);
}
}
}
@@ -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\router;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Response Validator interface.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface response_validator_interface {
/**
* Validate the request content.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
*/
public function validate_response(
ServerRequestInterface $request,
ResponseInterface $response,
): void;
}
+318
View File
@@ -0,0 +1,318 @@
<?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\router;
use core\exception\coding_exception;
use core\router\schema\parameter;
use core\router\schema\response\response;
use core\router\schema\request_body;
use Attribute;
/**
* Routing attribute.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class route {
/** @var string[] The list of HTTP Methods */
protected null|array $method = null;
/**
* The parent route, if relevant.
*
* A method-level route may have a class-level route as a parent. The two are combined to provide
* a fully-qualified path.
*
* @var route|null
*/
protected readonly ?route $parentroute;
/**
* Constructor for a new Moodle route.
*
* @param string $title A title to briefly describe the route (not translated)
* @param string $description A verbose explanation of the operation behavior (not translated)
* @param string $summary A short summary of what the operation does (not translated)
* @param null|string[] $security A list of security mechanisms
* @param null|string $path The path to match
* @param null|array|string $method The method, or methods, supported
* @param parameter[] $pathtypes Validators for the path arguments
* @param parameter[] $queryparams Validators for the path arguments
* @param parameter[] $headerparams Validators for the path arguments
* @param request_body|null $requestbody Validators for the path arguments
* @param response[] $responses A list of possible response types
* @param bool $deprecated Whether this endpoint is deprecated
* @param string[] $tags A list of tags
* @param bool $cookies Whether this request requires cookies
* @param bool $abortafterconfig Whether to abort after configuration
* @param mixed[] ...$extra Any additional arguments not yet supported in this version of Moodle
* @throws coding_exception
*/
public function __construct(
/** @var string A title to briefly describe the route (not translated) */
public readonly string $title = '',
/** @var string A verbose explanation of the operation behavior (not translated) */
public readonly string $description = '',
/** @var string A short summary of what the operation does (not translated) */
public readonly string $summary = '',
/** @var array<string> A list of security mechanisms */
public readonly ?array $security = null,
/**
* The path to the route.
*
* This is relative to the parent route, if one exists.
* A route must be set on one, or both, of the class and method level routes.
*
* @var string|null
*/
public ?string $path = null,
null|array|string $method = null,
/** @var parameter[] A list of param types for path arguments */
protected readonly array $pathtypes = [],
/** @var parameter[] A list of query parameters with matching types */
protected readonly array $queryparams = [],
/** @var parameter[] A list of header parameters */
protected readonly array $headerparams = [],
/** @var null|request_body A list of parameters found in the body */
public readonly ?request_body $requestbody = null,
/** @var response[] A list of possible response types */
protected readonly array $responses = [],
/** @var bool Whether this endpoint is deprecated */
public readonly bool $deprecated = false,
/** @var string[] A list of tags */
public readonly array $tags = [],
/** @var bool Whether this request may use cookies */
public readonly bool $cookies = true,
/** @var bool Whether to abort after configuration */
public readonly bool $abortafterconfig = false,
// Note. We do not make use of these extras.
// These allow us to add additional arguments in future versions, whilst allowing plugins to use this version.
...$extra,
) {
// Normalise the method.
if (is_string($method)) {
$method = [$method];
}
$this->method = $method;
// Validate the query parameters.
if (count(array_filter($this->queryparams, fn($pathtype) => !is_a($pathtype, parameter::class)))) {
throw new coding_exception('All query parameters must be an instance of \core\router\parameter.');
}
if (count(array_filter($this->queryparams, fn($pathtype) => $pathtype->get_in() !== 'query'))) {
throw new coding_exception('All query parameters must be in the query.');
}
// Validate the path parameters.
if (count(array_filter($this->pathtypes, fn($pathtype) => !is_a($pathtype, parameter::class)))) {
throw new coding_exception('All path parameters must be an instance of \core\router\parameter.');
}
if (count(array_filter($this->pathtypes, fn($pathtype) => $pathtype->get_in() !== 'path'))) {
throw new coding_exception('All path properties must be in the path.');
}
// Validate the header parameters.
if (count(array_filter($this->headerparams, fn($pathtype) => !is_a($pathtype, parameter::class)))) {
throw new coding_exception('All path parameters must be an instance of \core\router\parameter.');
}
if (count(array_filter($this->headerparams, fn($pathtype) => $pathtype->get_in() !== 'header'))) {
throw new coding_exception('All header properties must be in the path.');
}
}
/**
* Set the parent route, usually a Class-level route.
*
* @param route $parent
* @return self
*/
public function set_parent(route $parent): self {
$this->parentroute = $parent;
return $this;
}
/**
* Get the fully-qualified path for this route relative to root.
*
* This includes the path of any parent route.
*
* @return string
*/
public function get_path(): string {
$path = $this->path ?? '';
if (isset($this->parentroute)) {
$path = $this->parentroute->get_path() . $path;
}
return $path;
}
/**
* Get the list of HTTP methods associated with this route.
*
* @param null|string[] $default The default methods to use if none are set
* @return null|string[]
*/
public function get_methods(?array $default = null): ?array {
$methods = $this->method;
if (isset($this->parentroute)) {
$parentmethods = $this->parentroute->get_methods();
if ($methods) {
$methods = array_unique(
array_merge($parentmethods ?? [], $methods),
);
} else {
$methods = $parentmethods;
}
}
// If there are no methods from either this attribute or any parent, use the default.
$methods = $methods ?? $default;
if ($methods) {
sort($methods);
}
return $methods;
}
/**
* Get the list of path parameters, including any from the parent.
*
* @return array
*/
public function get_path_parameters(): array {
$parameters = [];
if (isset($this->parentroute)) {
$parameters = $this->parentroute->get_path_parameters();
}
foreach ($this->pathtypes as $parameter) {
$parameters[$parameter->get_name()] = $parameter;
}
return $parameters;
}
/**
* Get the list of path parameters, including any from the parent.
*
* @return array
*/
public function get_header_parameters(): array {
$parameters = [];
if (isset($this->parentroute)) {
$parameters = $this->parentroute->get_header_parameters();
}
foreach ($this->headerparams as $parameter) {
$parameters[$parameter->get_name()] = $parameter;
}
return $parameters;
}
/**
* Get the list of path parameters, including any from the parent.
*
* @return array
*/
public function get_query_parameters(): array {
$parameters = [];
if (isset($this->parentroute)) {
$parameters = $this->parentroute->get_query_parameters();
}
foreach ($this->queryparams as $parameter) {
$parameters[$parameter->get_name()] = $parameter;
}
return $parameters;
}
/**
* Get the request body for this route.
*
* @return request_body|null
*/
public function get_request_body(): ?request_body {
return $this->requestbody;
}
/**
* Whether this route expects a request body.
*
* @return bool
*/
public function has_request_body(): bool {
return $this->requestbody !== null;
}
/**
* Get all responses.
*
* @return response[]
*/
public function get_responses(): array {
return $this->responses;
}
/**
* Get the response with the specified response code.
*
* @param int $statuscode
* @return response|null
*/
public function get_response_with_status_code(int $statuscode): ?response {
foreach ($this->get_responses() as $response) {
if ($response->get_status_code() === $statuscode) {
return $response;
}
}
return null;
}
/**
* Whether this route expects any validatable parameters.
* That is, any parameter in the path, query params, or the request body.
*
* @return bool
*/
public function has_any_validatable_parameter(): bool {
return count($this->get_path_parameters()) || count($this->get_query_parameters()) || $this->has_request_body();
}
}
+139
View File
@@ -0,0 +1,139 @@
<?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\router;
use moodle_url;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* A controller to make it easier to implement a route.
*
* This controller adds the Container to the constructor which allows controllers to support DI.
*
* This trait is entirely optional.
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait route_controller {
/**
* Constructor for Route Controllers.
*
* @param ContainerInterface $container
*/
public function __construct(
/** @var ContainerInterface The DI Container */
protected ContainerInterface $container,
) {
}
/**
* Generate a Page Not Found result.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
* @throws \Slim\Exception\HttpNotFoundException
*/
protected function page_not_found(
ServerRequestInterface $request,
ResponseInterface $response,
): ResponseInterface {
throw new \Slim\Exception\HttpNotFoundException($request);
}
/**
* Redirect to a URL.
*
* @param ResponseInterface $response
* @param string|moodle_url $url
* @return ResponseInterface
*/
protected function redirect(
ResponseInterface $response,
string|moodle_url $url,
): ResponseInterface {
return $response
->withStatus(302)
->withHeader('Location', (string) $url);
}
/**
* Redirect to the requested callable.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param array|callable|string $callable
* @param null|array $pathparams
* @param null|array $queryparams
* @param null|array $excludeparams A list of any parameters to remove the URI during the redirect
* @return ResponseInterface
*/
protected function redirect_to_callable(
ServerRequestInterface $request,
ResponseInterface $response,
array|callable|string $callable,
?array $pathparams = null,
?array $queryparams = null,
?array $excludeparams = null,
): ResponseInterface {
// Provide defaults for the path and query params if not specified.
if ($pathparams === null) {
$pathparams = $request->getQueryParams();
}
if ($queryparams === null) {
$queryparams = $request->getQueryParams();
}
// Generate a URI from the callable and the parameters.
$url = util::get_path_for_callable(
$callable,
$pathparams ?? [],
$queryparams ?? [],
);
// Remove any params.
$url->remove_params($excludeparams);
return $this->redirect($response, $url);
}
/**
* Get a parameter from the query params after validation.
*
* @param ServerRequestInterface $request
* @param string $key
* @param mixed $default
* @return mixed
*/
protected function get_param(
ServerRequestInterface $request,
string $key,
mixed $default = null,
): mixed {
$params = $request->getQueryParams();
if (array_key_exists($key, $params)) {
return $params[$key];
} else {
debugging("Missing parameter: $key");
}
return $default;
}
}
+83
View File
@@ -0,0 +1,83 @@
<?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\router;
use Slim\App;
use Slim\Interfaces\RouteGroupInterface;
use Slim\Routing\RouteCollectorProxy;
/**
* Route Loader and Discovery agent.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class route_loader extends abstract_route_loader implements route_loader_interface {
#[\Override]
public function configure_routes(App $app): array {
return [
route_loader_interface::ROUTE_GROUP_API => $this->configure_api_routes($app, route_loader_interface::ROUTE_GROUP_API),
];
}
/**
* Configure all API routes.
*
* @param App $app
* @param string $path
* @return RouteGroupInterface
*/
protected function configure_api_routes(App $app, string $path): RouteGroupInterface {
return $app->group($path, function (
RouteCollectorProxy $group,
): void {
// Add all API routes located in the route\api L2\L3 namespace.
foreach ($this->get_all_api_routes() as $apiroute) {
$slimroute = $group->map(...$apiroute);
$this->set_route_name_for_callable($slimroute, $apiroute['callable']);
}
// Add the OpenAPI docs route.
$callable = [apidocs::class, 'openapi_docs'];
$slimroute = $group->get('/openapi.json', $callable);
$this->set_route_name_for_callable($slimroute, $callable);
});
}
/**
* Fetch all API routes.
*
* Note: This method caches results in MUC.
*
* @return array[]
*/
protected function get_all_api_routes(): array {
$cache = \cache::make('core', 'routes');
if (!($routes = $cache->get('api_routes'))) {
$routes = $this->get_all_routes_in_namespace(
namespace: 'route\api',
componentpathcallback: $this->normalise_component_path(...),
);
$cache->set('api_routes', $routes);
}
return $routes;
}
}
@@ -0,0 +1,43 @@
<?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\router;
use Slim\App;
use Slim\Interfaces\RouteGroupInterface;
use Slim\Interfaces\RouteInterface;
/**
* A route loader.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface route_loader_interface {
/** @var string The route path prefix to use for API calls */
public const ROUTE_GROUP_API = '/api/rest/v2';
/**
* Configure all routes for the Application.
*
* This method returns a set of RouteGroupInterface instances for each route prefix.
*
* @param App $app The application to configure routes for
* @return RouteInterface[]|RouteGroupInterface
*/
public function configure_routes(App $app): array;
}
+109
View File
@@ -0,0 +1,109 @@
<?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\router\schema;
use core\exception\coding_exception;
/**
* A Response Example Object.
*
* https://spec.openapis.org/oas/v3.1.0#example-object
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class example extends openapi_base {
/**
* Create a new example.
*
* @param string $name The name of the example.
* @param string|null $summary A summary of the example.
* @param string|null $description A long description fo the example. CommonMark syntax may be used.
* @param mixed $value Embedded literal example.
* @param string|null $externalvalue A URI that points to the literal example.
* @param mixed ...$extra
* @throws coding_exception if both the value and externalvalue are null
*/
public function __construct(
/** @var string The name of the example */
protected string $name,
/** @var string|null A summary of the example */
protected ?string $summary = null,
/** @var string|null A long description fo the example. CommonMark syntax may be used */
protected ?string $description = null,
/**
* Embedded literal example.
*
* The value field and externalValue field are mutually exclusive.
* To represent examples of media types that cannot naturally represented in JSON or YAML,
* use a string value to contain the example, escaping where necessary.
*
* @var mixed
*/
protected mixed $value = null,
/**
* A URI that points to the literal example.
*
* This provides the capability to reference examples that cannot easily be included in JSON or YAML documents.
* The value field and externalValue field are mutually exclusive. See the rules for resolving Relative References.
*
* @var string|null
*/
protected ?string $externalvalue = null,
...$extra,
) {
if (!($value === null || $externalvalue === null)) {
throw new coding_exception('Only one of value or externalvalue can be specified.');
}
parent::__construct(...$extra);
}
/**
* Get the name of this example.
*
* @return string
*/
public function get_name(): string {
return $this->name;
}
#[\Override]
public function get_openapi_description(
specification $api,
?string $path = null,
): ?\stdClass {
$data = (object) [];
if ($this->summary !== null) {
$data->summary = $this->summary;
}
if ($this->description !== null) {
$data->description = $this->description;
}
if ($this->value !== null) {
$data->value = $this->value;
} else if ($this->externalvalue !== null) {
$data->externalValue = $this->externalvalue;
}
return $data;
}
}
@@ -0,0 +1,46 @@
<?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\router\schema;
/**
* A Header Object.
*
* https://spec.openapis.org/oas/v3.1.0#headerObject
*
* The Header Object follows the structure of the Parameter Object with the following changes:
*
* - name MUST NOT be specified, it is given in the corresponding headers map.
* - in MUST NOT be specified, it is implicitly in header.
* - All traits that are affected by the location MUST be applicable to a location of header (for example, style).
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class header_object extends parameters\header_object {
#[\Override]
final public function get_openapi_description(
specification $api,
?string $path = null,
): ?\stdClass {
$data = parent::get_openapi_description($api);
unset($data->in);
unset($data->name);
return $data;
}
}
@@ -0,0 +1,78 @@
<?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\router\schema\objects;
use core\param;
use core\router\schema\referenced_object;
/**
* A schema to describe an array of strings.
*
* TODO: This should really take a param:: type for validation of both name and value.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class array_of_strings extends array_of_things implements referenced_object {
/**
* Create a new array_of_strings schema.
*
* @param param $keyparamtype The type of the key parameter
* @param param $valueparamtype The type of the value parameter
* @param mixed ...$extra Additional arguments
*/
public function __construct(
/** @var param The type param type for the key */
protected param $keyparamtype = param::RAW,
/** @var param The type param type for the value */
protected param $valueparamtype = param::RAW,
...$extra,
) {
$extra['thingtype'] = 'string';
parent::__construct(...$extra);
}
#[\Override]
public function validate_data(mixed $data) {
foreach ($data as $name => $value) {
$this->keyparamtype->validate_param(
param: $name,
debuginfo: $this->get_debug_info_for_validation_failure($this->keyparamtype, $name),
);
$this->valueparamtype->validate_param(
param: $value,
debuginfo: $this->get_debug_info_for_validation_failure($this->valueparamtype, $value),
);
}
return $data;
}
/**
* Get the debug info for a validation failure.
*
* @param param $type
* @param string $value
* @return string
*/
protected function get_debug_info_for_validation_failure(
param $type,
string $value,
): string {
return "The value '{$value}' was not of type {$type->value}.";
}
}
@@ -0,0 +1,93 @@
<?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\router\schema\objects;
use core\param;
use core\router\schema\specification;
/**
* A schema to describe an array of things. These could be any type, including other schema definitions.
*
* See https://spec.openapis.org/oas/v3.0.0#model-with-map-dictionary-properties for relevant documentation.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class array_of_things extends type_base {
/**
* An array of things.
*
* @param string|type_base|param|null $thingtype The OpenAPI type, or null if any type is allowed.
* @param mixed[] ...$extra
*/
public function __construct(
/** @var string|type_base|param|null The child item type */
protected string|type_base|param|null $thingtype = null,
...$extra,
) {
parent::__construct(...$extra);
}
#[\Override]
public function get_openapi_description(
specification $api,
?string $path = null,
): ?\stdClass {
return $this->get_schema();
}
/**
* Get the OpenAPI schema for this object.
*
* @return \stdClass
*/
public function get_schema(): \stdClass {
return (object) [
'type' => 'object',
'additionalProperties' => $this->get_additional_properties($this->thingtype),
];
}
#[\Override]
public function validate_data(mixed $data) {
if (!is_array($data)) {
throw new \invalid_parameter_exception('Invalid data type, expected array.');
}
if ($this->thingtype === null) {
return $data;
}
if (is_a($this->thingtype, type_base::class)) {
$validator = fn ($value) => $this->thingtype->validate_data($value);
} else {
if (is_string($this->thingtype)) {
$param = param::from($this->thingtype);
} else {
$param = $this->thingtype;
}
$validator = fn ($value) => $param->validate_param($value);
}
foreach ($data as $value) {
$validator($value);
}
return $data;
}
}
@@ -0,0 +1,65 @@
<?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\router\schema\objects;
use core\param;
use core\router\schema\specification;
/**
* A scalar type.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class scalar_type extends type_base {
/**
* Instantiate a new Scalar Type
*
* @param param $type The Moodle PARAM_ type
* @param bool $required Whether the value is required or not
* @param mixed $default The value used if none was supplied (request bodies only)
* @param mixed[] ...$extra
*/
public function __construct(
/** @var param The type of the parameter content */
protected param $type,
/** @var bool Whether the value is required or not */
protected bool $required = false,
/** @var mixed $default The value used if none was supplied (request bodies only) */
protected mixed $default = null,
...$extra,
) {
parent::__construct(...$extra);
}
#[\Override]
public function get_openapi_description(
specification $api,
?string $path = null,
): ?\stdClass {
return $this->get_schema_from_type($this->type);
}
#[\Override]
public function validate_data(mixed $data) {
return $this->type->validate_param(
param: $data,
allownull: $this->required ? NULL_NOT_ALLOWED : NULL_ALLOWED,
);
}
}
@@ -0,0 +1,113 @@
<?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\router\schema\objects;
use core\exception\coding_exception;
use core\router\schema\specification;
/**
* A schema to describe an array of things. These could be any type, including other schema definitions.
*
* See https://spec.openapis.org/oas/v3.1.0#model-with-map-dictionary-properties for relevant documentation.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class schema_object extends type_base {
/**
* An array of things.
*
* @param type_base[] $content The child content
* @param mixed[] ...$extra
* @throws coding_exception
*/
public function __construct(
/** @var type_base[] The child content */
protected array $content,
...$extra,
) {
foreach ($content as $child) {
if (!$child instanceof type_base) {
throw new coding_exception('Content must be an array of type_base objects');
}
}
parent::__construct(...$extra);
}
/**
* Whether this schema object has this key as a type.
*
* @param string $key
* @return bool
*/
public function has(string $key): bool {
return isset($this->content[$key]);
}
/**
* Get the schema object for this key.
*
* @param string $key
* @return type_base
*/
public function get(string $key): type_base {
return $this->content[$key];
}
#[\Override]
public function validate_data(mixed $data) {
foreach ($data as $key => $values) {
if (!$this->has($key)) {
// We do not know about this one.
// Remove it from the params array.
$data = array_diff_key(
$data,
[$key => $values],
);
continue;
}
// Validate this parameter.
$child = $this->content[$key];
$data[$key] = $child->validate_data($values);
}
return $data;
}
#[\Override]
public function get_openapi_description(
specification $api,
?string $path = null,
): ?\stdClass {
$data = (object) [
'type' => 'object',
'properties' => (object) [],
];
foreach ($this->content as $name => $content) {
$data->properties->{$name} = $content->get_openapi_schema(
$api,
);
}
return $data;
}
}
@@ -0,0 +1,128 @@
<?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\router\schema\objects;
use core\param;
use core\router\schema\example;
use core\router\schema\referenced_object;
use core\router\schema\specification;
/**
* A standard response for user preferences.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class stacktrace extends type_base implements referenced_object {
/** @var array The stacks in the trace */
protected array $content;
/**
* Constructor for a new stacktrace object.
*/
public function __construct() {
$this->content = [
'file' => new scalar_type(param::PATH),
'line' => new scalar_type(param::INT),
'function' => new scalar_type(param::RAW),
'args' => new array_of_things(),
'class' => new scalar_type(param::RAW),
'type' => new scalar_type(param::RAW),
];
$pathroot = '/Users/example/Sites/moodle';
parent::__construct(
examples: [
new example(
name: 'A sample stacktrace',
value: [
[
"file" => "{$pathroot}/lib/classes/router/schema/objects/array_of_strings.php",
"line" => 48,
"function" => "validate_param",
"args" => [
"string",
"int",
false,
"The value 'string' was not of type string.",
],
],
[
"file" => "{$pathroot}/lib/classes/router/schema/objects/schema_object.php",
"line" => 85,
"function" => "validate_data",
"class" => "core\\router\\schema\\objects\\array_of_strings",
"type" => "->",
"args" => [
[
"additionalProp1" => "string",
"additionalProp2" => "string",
"additionalProp3" => "string",
],
],
],
[
"file" => "{$pathroot}/lib/classes/router/route.php",
"line" => 264,
"function" => "validate_data",
"class" => "core\\router\\schema\\objects\\schema_object",
"type" => "->",
"args" => [
[
"preferences" => [
"additionalProp1" => "string",
"additionalProp2" => "string",
"additionalProp3" => "string",
],
],
],
],
],
),
],
);
}
#[\Override]
public function get_openapi_description(
specification $api,
?string $path = null,
): ?\stdClass {
$additionalproperties = new \stdClass();
foreach ($this->content as $name => $content) {
$additionalproperties->{$name} = $content->get_openapi_description($api, $path);
}
$data = parent::get_openapi_description($api, $path);
$data->type = 'array';
$data->items = (object) [
'type' => 'object',
'properties' => $additionalproperties,
];
return $data;
}
#[\Override]
public function validate_data($data) {
// Do not validate the data at all.
// Stacktraces tend to be used with exceptions and we want whatever was passed through to come out.
return $data;
}
}
@@ -0,0 +1,95 @@
<?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\router\schema\objects;
use core\router\schema\openapi_base;
use core\router\schema\specification;
/**
* Part of the OpenAPI Schema.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class type_base extends openapi_base {
/**
* Note: We do not implement the $example, because it has been deprecated in OpenApi 3.0.
*
* @param array $examples
* @param mixed[] ...$extra
*/
public function __construct(
/** @var array Any examples that may be present for the type */
protected array $examples = [],
...$extra,
) {
parent::__construct(...$extra);
}
#[\Override]
public function get_openapi_description(
specification $api,
?string $path = null,
): ?\stdClass {
$data = (object) [];
if (count($this->examples)) {
$data->examples = [];
foreach ($this->examples as $example) {
$data->examples[] = $example->get_openapi_schema(
api: $api,
);
}
}
return $data;
}
/**
* Get the additional OpenAPI properties if relevant.
*
* @param string|null|type_base $type
* @return bool|array
*/
protected function get_additional_properties(string|null|type_base $type): bool|array {
// The additionalProperties are described here:
// https://spec.openapis.org/oas/v3.1.0#schema-object-examples.
if ($type === null) {
return true;
}
if (is_a($type, self::class)) {
// This type is a reference to another schema object.
return [
'$ref' => $type->get_reference(),
];
}
// TODO MDL-82243: Validate against supported OpenAPI types.
return [
'type' => $type,
];
}
/**
* Validate the data against the type.
*
* @param mixed $data
*/
abstract public function validate_data(mixed $data);
}
+243
View File
@@ -0,0 +1,243 @@
<?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\router\schema;
use coding_exception;
use core\param;
use core\router\schema\objects\type_base;
use core\router\schema\response\response;
use stdClass;
/**
* A generic part of the OpenAPI Schema object.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class openapi_base {
/**
* Base constructor which does nothing.
*
* We keep an $extra parameter here for future-proofing.
* This allows named parameters to be used and allows contrib plugins to
* make use of parameters in newer versions even if they don't exist in older versions.
*
* @param mixed ...$extra Extra arguments to allow for future versions of Moodle to add options without breaking plugins
*/
public function __construct(
mixed ...$extra,
) {
}
/**
* Get the $ref for this class.
*
* @param bool $qualify Whether to qualify the reference with the #/components/ part.
* @return string
*/
public function get_reference(
bool $qualify = true,
): string {
return static::get_reference_for_class(
classname: get_class($this),
qualify: $qualify,
);
}
/**
* Get the OpenAPI data to include in the OpenAPI specification.
*
* @param specification $api
* @param null|string $path
* @return null|stdClass
* @throws coding_exception
*/
final public function get_openapi_schema(
specification $api,
?string $path = null,
): ?stdClass {
if (is_a($this, referenced_object::class)) {
// This class is a referenced object, so we need to add it to the specification.
if (!$api->is_reference_defined($this->get_reference())) {
$api->add_component($this);
}
return (object) [
'$ref' => $this->get_reference(),
];
}
return $this->get_openapi_description(
api: $api,
path: $path,
);
}
/**
* Get the OpenAPI data to include in the OpenAPI specification.
*
* @param specification $api
* @param null|string $path
* @return null|stdClass
*/
abstract public function get_openapi_description(
specification $api,
?string $path = null,
): ?stdClass;
/**
* Get the $ref a class name.
*
* https://swagger.io/docs/specification/using-ref/
*
* @param string $classname The class to get a reference for
* @param bool $qualify Whether to qualify the reference with the #/components/ part
* @return string The reference
* @throws coding_exception
*/
public static function get_reference_for_class(
string $classname,
bool $qualify = true,
): string {
$reference = static::escape_reference($classname);
if (!$qualify) {
return $reference;
}
// Note: The following list must be kept in-sync with specification::add_component().
return match (true) {
is_a($classname, header_object::class, true) => static::get_reference_for_header($reference),
is_a($classname, parameter::class, true) => static::get_reference_for_parameter($reference),
is_a($classname, response::class, true) => static::get_reference_for_response($reference),
is_a($classname, example::class, true) => static::get_reference_for_example($reference),
is_a($classname, request_body::class, true) => static::get_reference_for_request_body($reference),
is_a($classname, type_base::class, true) => static::get_reference_for_schema($reference),
default => throw new coding_exception("Class {$classname} is not a schema."),
};
}
/**
* Get the qualified $ref for a parameter.
*
* @param string $reference
* @return string
*/
public static function get_reference_for_header(string $reference): string {
return "#/components/headers/{$reference}";
}
/**
* Get the qualified $ref for a parameter.
*
* @param string $reference
* @return string
*/
public static function get_reference_for_parameter(string $reference): string {
return "#/components/parameters/{$reference}";
}
/**
* Get the qualified $ref for a response.
*
* @param string $reference
* @return string
*/
public static function get_reference_for_response(string $reference): string {
return "#/components/responses/{$reference}";
}
/**
* Get the qualified $ref for an example.
*
* @param string $reference
* @return string
*/
public static function get_reference_for_example(string $reference): string {
return "#/components/examples/{$reference}";
}
/**
* Get the qualified $ref for a request body.
*
* @param string $reference
* @return string
*/
public static function get_reference_for_request_body(string $reference): string {
return "#/components/requestBodies/{$reference}";
}
/**
* Get the qualified $ref for a schema.
*
* @param string $reference
* @return string
*/
public static function get_reference_for_schema(string $reference): string {
return "#/components/schemas/{$reference}";
}
/**
* Escape a reference following rules defined at https://swagger.io/docs/specification/using-ref/.
*
* @param string $reference
* @return string
*/
public static function escape_reference(string $reference): string {
// Note https://swagger.io/docs/specification/using-ref/ defines the following replacements:
// ~ => ~0
// / => ~1
// We also add some other replacements:
// \ => --
// These must be used in all reference names.
// See also https://spec.openapis.org/oas/v3.1.0#components-object
// And the following regular expression:
// ^[a-zA-Z0-9\.\-_]+$.
return str_replace(
['~', '/', '\\'],
['~0', '~1', '--'],
$reference,
);
}
/**
* Get the schema for a given type.
*
* @param param $type
* @return stdClass
*/
public function get_schema_from_type(param $type): stdClass {
$data = new stdClass();
$data->type = match ($type) {
// OpenAPI uses an extension of the JSON Schema to define both integers and numbers (float).
param::INT => 'integer',
param::FLOAT => 'number',
param::BOOL => 'boolean',
// All other types are string types and most have a pattern.
default => 'string',
};
if ($pattern = $type->get_clientside_expression()) {
$data->pattern = $pattern;
}
return $data;
}
}
+176
View File
@@ -0,0 +1,176 @@
<?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\router\schema;
use core\exception\coding_exception;
use core\param;
use core\router\route;
use core\router\schema\objects\type_base;
use stdClass;
/**
* OpenAPI parameter.
*
* https://spec.openapis.org/oas/v3.1.0#parameter-object
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class parameter extends openapi_base {
/** @var string A query parameter */
public const IN_QUERY = 'query';
/** @var string A header parameter */
public const IN_HEADER = 'header';
/** @var string A URI path parameter */
public const IN_PATH = 'path';
/** @var string A cookie parameter */
public const IN_COOKIE = 'cookie';
/**
* Constructor for a Parameter Object.
*
* @param string $name The name of the parameter. Parameter names are case sensitive.
* - If in is "path", the name field MUST correspond to a template expression occurring within the
* path field in the Paths Object.
* See Path Templating for further information.
* - If in is "header" and the name field is "Accept", "Content-Type" or "Authorization",
* the parameter definition SHALL be ignored.
* - For all other cases, the name corresponds to the parameter name used by the in property.
* @param string $in The location of the parameter. Possible values are "query", "header", "path" or "cookie".
* @param null|string $description
* @param null|bool $required
* @param null|bool $deprecated Specifies that a parameter is deprecated and SHOULD be transitioned out of usage.
* @param null|param $type A Moodle parameter type, which can be used instead of a schema.
* @param mixed $default The default value
* @param null|type_base $schema
* @param null|example $example
* @param example[] $examples
* @param mixed[] ...$extra
* @throws coding_exception
*/
public function __construct(
/** @var string The name of the parameter. Parameter names are case sensitive */
protected string $name,
/** @var string The location of the parameter */
protected string $in,
/** @var string|null A description of the parameter */
protected ?string $description = null,
/** @var bool|null Whether the parameter is required */
protected ?bool $required = null,
/** @var bool|null Whether the parameter is deprecated */
protected ?bool $deprecated = false,
/** @var param|null A Moodle parameter type */
protected ?param $type = null,
/** @var mixed|null The default value of the parameter */
protected mixed $default = null,
/** @var type_base|null The schema */
protected ?type_base $schema = null,
/** @var example|null An example */
protected ?example $example = null,
/** @var example[] An array of examples */
protected array $examples = [],
...$extra,
) {
if ($example) {
if (count($examples)) {
throw new coding_exception('Only one of example or examples can be specified.');
}
$this->examples[$example->get_name()] = $example;
}
if ($required === true && $default !== null) {
throw new coding_exception('A parameter cannot be required and have a default value.');
}
parent::__construct(...$extra);
}
#[\Override]
public function get_openapi_description(
specification $api,
?string $path = null,
): ?stdClass {
$data = (object) [
// The `name`, and `in` values are required.
'name' => $this->name,
'in' => $this->in,
];
if ($this->description !== null) {
$data->description = $this->description;
}
// Allow another schema to be passed.
if ($this->schema !== null) {
$data->schema = $this->schema->get_openapi_schema($api, $path);
} else {
$data->schema = $this->get_schema_from_type($this->type);
}
if (count($this->examples) > 0) {
$data->examples = [];
foreach ($this->examples as $example) {
$data->examples[$example->get_name()] = $example->get_openapi_schema(
api: $api,
);
}
}
return $data;
}
/**
* Get the OpenAPI 'in' property.
*
* @return string
*/
public function get_in(): string {
return $this->in;
}
/**
* Fetch the underlying param.
*
* @return param
*/
public function get_type(): param {
return $this->type;
}
/**
* Whether this property is required.
*
* @param route $route
* @return bool
*/
public function is_required(route $route): bool {
return $this->required ?? false;
}
/**
* Get the name of the parameter.
*
* @return string
*/
public function get_name(): string {
return $this->name;
}
}
@@ -0,0 +1,106 @@
<?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\router\schema\parameters;
use core\exception\invalid_parameter_exception;
use core\param;
use core\router\schema\parameter;
use Psr\Http\Message\ServerRequestInterface;
/**
* A Header Object.
*
* https://spec.openapis.org/oas/v3.1.0#headerObject
*
* The Header Object follows the structure of the Parameter Object with the following changes:
*
* - name MUST NOT be specified, it is given in the corresponding headers map.
* - in MUST NOT be specified, it is implicitly in header.
* - All traits that are affected by the location MUST be applicable to a location of header (for example, style).
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class header_object extends parameter {
/**
* Create a new header object.
*
* @param bool $multiple Whether this parameter can be specified multiple times.
* @param mixed ...$extra Header arguments to pass to the parameter constructor.
*/
public function __construct(
/** @var bool Whether multiple instances of this header are supported */
protected bool $multiple = false,
...$extra,
) {
$extra['in'] = parameter::IN_HEADER;
parent::__construct(...$extra);
}
/**
* Validate the parameter.
*
* @param ServerRequestInterface $request The request to validate.
* @return ServerRequestInterface The request with the validated parameter.
* @throws invalid_parameter_exception If the parameter is invalid.
*/
public function validate(
ServerRequestInterface $request,
): ServerRequestInterface {
if ($request->hasHeader($this->name)) {
$headervalues = $request->getHeader($this->name);
if (!$this->multiple && count($headervalues) > 1) {
throw new invalid_parameter_exception(
"The parameter {$this->name} was specified multiple times, but it can only be specified once",
);
}
// This parameter was specified. Validate it.
if ($this->get_type() === param::BOOL) {
$headervalues = array_map(fn ($headervalue) => match ($headervalue) {
'true' => 1,
'false' => 0,
default => throw new \ValueError('Invalid boolean value.'),
}, $headervalues);
return $request->withHeader($this->name, $headervalues);
}
foreach ($headervalues as $headervalue) {
$this->type->validate_param($headervalue);
}
return $request;
}
if ($this->required) {
throw new invalid_parameter_exception(
"A required parameter {$this->name} was not provided and must be specified",
);
}
if ($this->default !== null) {
// This parameter is optional. Fill the default.
return $request->withHeader($this->name, $this->default);
}
// This parameter is optional and there is no default.
// Fill a null value.
return $request->withHeader($this->name, null);
}
}
@@ -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\router\schema\parameters;
use Psr\Http\Message\ServerRequestInterface;
/**
* An OpenAPI Parameter which supports validation.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface mapped_property_parameter {
/**
* Add attribute for the current parameter to the request.
*
* @param ServerRequestInterface $request
* @param string $value
* @return ServerRequestInterface
*/
public function add_attributes_for_parameter_value(
ServerRequestInterface $request,
string $value,
): ServerRequestInterface;
}
@@ -0,0 +1,113 @@
<?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\router\schema\parameters;
use core\router\route;
use core\router\schema\parameter;
use core\router\schema\specification;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Routing\Route as RoutingRoute;
use stdClass;
/**
* Routing parameter for validation.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class path_parameter extends parameter {
/**
* Create an instance of a new path parameter.
*
* @param mixed ...$extra Additional parameters for the parameter
*/
public function __construct(
...$extra,
) {
$extra['in'] = parameter::IN_PATH;
parent::__construct(
...$extra,
);
}
/**
* Validate the path parameter.
*
* @param ServerRequestInterface $request
* @param RoutingRoute $route
* @return ServerRequestInterface The modified request with parameters updated
*/
public function validate(
ServerRequestInterface $request,
RoutingRoute $route,
): ServerRequestInterface {
$args = $route->getArguments();
$value = $route->getArgument($this->name);
$this->type->validate_param(
param: $value,
allownull: NULL_ALLOWED,
);
if (is_a($this, mapped_property_parameter::class)) {
// Unfortunately args must be a string, but mapped properties can be an object.
// Remove the argument, and instead provide the mapped property as an attribute.
unset($args[$this->name]);
$route->setArguments($args);
$request = $this->add_attributes_for_parameter_value($request, $value);
}
return $request;
}
#[\Override]
final public function get_openapi_description(
specification $api,
?string $path = null,
): ?stdClass {
if ($path && !str_contains($path, "{{$this->name}}")) {
// In OpenAPI, Path parameters can never be optional.
return null;
}
$data = parent::get_openapi_description(
api: $api,
path: $path,
);
$data->required = true;
return $data;
}
/**
* Check whether this parameter is required for the given route.
*
* @param route $route
* @return bool
*/
public function is_required(route $route): bool {
$path = $route->get_path();
// Find the position of the parameter in the path.
$paramposition = strpos($path, '{' . $this->name . '}');
// If _any_ part of the path before the parameter contains a '[' character, then this _must_ be optional.
// A required parameter cannot follow an optional parameter.
return !str_contains(substr($path, 0, $paramposition), '[');
}
}
@@ -0,0 +1,147 @@
<?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\router\schema\parameters;
use core\exception\coding_exception;
use core\param;
use core\router\schema\parameter;
use core\router\schema\specification;
use Psr\Http\Message\ServerRequestInterface;
/**
* Routing query parameter for validation.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class query_parameter extends parameter {
/**
* Query parameter constructor to override the location of the parameter.
*
* @param bool|null $allowreserved Determines whether the parameter value SHOULD allow reserved characters.
* @param array ...$extra
*/
public function __construct(
/**
* Determines whether the parameter value SHOULD allow reserved characters.
*
* As defined by [RFC3986], these characters are :/?#[]@!$&'()*+,;= to be included without percent-encoding.
* This property only applies to parameters with an in value of query. The default value is false.
*
* @var bool|null
*/
protected ?bool $allowreserved = null,
...$extra,
) {
$extra['in'] = parameter::IN_QUERY;
parent::__construct(...$extra);
}
/**
* Validate query parameters.
*
* @param ServerRequestInterface $request
* @param array $params
* @return ServerRequestInterface
* @throws coding_exception
* @throws \ValueError
*/
public function validate(
ServerRequestInterface $request,
array $params,
): ServerRequestInterface {
if (array_key_exists($this->name, $params)) {
// This parameter was specified. Validate it.
if ($this->get_type() === param::BOOL) {
match ($params[$this->name]) {
'true' => $params[$this->name] = 1,
'false' => $params[$this->name] = 0,
default => throw new \ValueError('Invalid boolean value.'),
};
}
$this->type->validate_param($params[$this->name]);
return $this->update_request_params(
$request,
array_merge(
$params,
[$this->name => $params[$this->name]],
),
);
}
if ($this->required) {
throw new coding_exception(
"A required parameter {$this->name} was not provided and must be specified",
);
}
if ($this->default !== null) {
// This parameter is optional. Fill the default.
return $this->update_request_params(
$request,
array_merge(
$params,
[$this->name => $this->default],
),
);
}
// This parameter is optional and there is no default.
// Fill a null value.
return $this->update_request_params(
$request,
array_merge(
$params,
[$this->name => null],
),
);
}
/**
* Update the request parameters.
*
* @param ServerRequestInterface $request
* @param array $params
* @return ServerRequestInterface
*/
protected function update_request_params(
ServerRequestInterface $request,
array $params,
): ServerRequestInterface {
return $request->withQueryParams($params);
}
#[\Override]
final public function get_openapi_description(
specification $api,
?string $path = null,
): ?\stdClass {
$data = parent::get_openapi_description($api, $path);
if ($this->allowreserved) {
// Determines whether the parameter value SHOULD allow reserved characters, as defined by [RFC3986]
// :/?#[]@!$&'()*+,;=
// to be included without percent-encoding.
// This property only applies to parameters with an in value of query. The default value is false.
$data->allowReserved = $this->allowreserved;
}
return $data;
}
}
@@ -0,0 +1,29 @@
<?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\router\schema;
/**
* An OpenAPI Schema Object which is referenced rather than inserted directly.
*
* This mean that it is inserted into the /components/ part of the schema rather than duplicated.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface referenced_object {
}
+141
View File
@@ -0,0 +1,141 @@
<?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\router\schema;
use core\router\schema\response\content\media_type;
use core\router\schema\response\content\payload_response_type;
use Psr\Http\Message\ServerRequestInterface;
/**
* Routing request body for validation.
*
* https://spec.openapis.org/oas/v3.1.0#request-body-object
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class request_body extends openapi_base {
/**
* Create a new request body.
*
* @param string $description A brief description of the request body.
* @param payload_response_type|payload_response_type[] $content The content of the request body.
* @param bool $required Whether the request body is required
* @param mixed ...$args Extra args for future compatibility.
* @throws \coding_exception if the content is not an instance of media_type.
*/
public function __construct(
/**
* A brief description of the request body.
*
* This could contain examples of use. CommonMark syntax MAY be used for rich text representation.
* @var string
*/
protected string $description = '',
/**
* The content of the request body.
*
* @var payload_response_type|media_type[]
*/
protected array|payload_response_type $content = [],
/** @var bool Whether the request body is required */
protected bool $required = false,
...$args,
) {
if (!empty($content)) {
if (is_array($content)) {
foreach ($content as $contentitem) {
if (!($contentitem instanceof media_type)) {
throw new \coding_exception('Content must be an instance of media_type.');
}
}
}
}
parent::__construct(...$args);
}
#[\Override]
public function get_openapi_description(
specification $api,
?string $path = null,
): ?\stdClass {
$data = (object) [
'description' => $this->description,
'required' => $this->required,
'content' => [],
];
if ($this->content instanceof response\content\payload_response_type) {
$data->content = $this->content->get_openapi_schema(
api: $api,
);
return $data;
}
foreach ($this->content as $content) {
$data->content[$content->get_encoding()] = $content->get_openapi_schema(
api: $api,
);
}
return $data;
}
/**
* Get the relevant body for the specified request.
*
* Request bodies can be different for different content-types, as noted in the request.
*
* @param ServerRequestInterface $request
* @return media_type
* @throws \invalid_parameter_exception
*/
public function get_body_for_request(
ServerRequestInterface $request,
): media_type {
if ($this->content instanceof payload_response_type) {
$content = $this->content->get_media_type_instance(
mimetype: $request->getHeaderLine('Content-Type'),
required: $this->is_required(),
);
if ($content) {
return $content;
}
} else {
foreach ($this->content as $content) {
if ($content::get_encoding() === $request->getHeaderLine('Content-Type')) {
return $content;
}
}
}
throw new \invalid_parameter_exception('No matching content type found.');
}
/**
* Whether this query parameter is required.
*
* @return bool
*/
public function is_required(): bool {
return $this->required;
}
}
@@ -0,0 +1,59 @@
<?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\router\schema\response;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* An abstract response to a request.
*
* This approach is inspired and based upon slim-routing https://github.com/juliangut/slim-routing
* We only need a fraction of this functionality.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class abstract_response implements response_type {
/**
* Create a new abstract response.
*
* @param ServerRequestInterface $request The request
* @param ResponseInterface|null $response The response
*/
public function __construct(
/** @var ServerRequestInterface The Request */
public readonly ServerRequestInterface $request,
/** @var ResponseInterface|null The Response */
public readonly ?ResponseInterface $response = null,
) {
}
#[\Override]
public function get_request(): ServerRequestInterface {
return $this->request;
}
#[\Override]
public function get_response(
ResponseFactoryInterface $responsefactory,
): ?ResponseInterface {
return $this->response ?? $responsefactory->createResponse();
}
}
@@ -0,0 +1,31 @@
<?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\router\schema\response\content;
/**
* A JSON Message body.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class json_media_type extends media_type {
#[\Override]
public static function get_encoding(): string {
return 'application/json';
}
}
@@ -0,0 +1,128 @@
<?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\router\schema\response\content;
use core\exception\coding_exception;
use core\router\schema\example;
use core\router\schema\openapi_base;
use core\router\schema\objects\type_base;
use core\router\schema\specification;
/**
* An OpenAPI MediaType.
* https://swagger.io/specification/#media-type-object
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class media_type extends openapi_base {
/**
* Create a new instance of a media_type definition.
*
* @param type_base|null $schema The OpenAPI Schema to use
* @param example|null $example An example of the media type
* @param example[] $examples An array of examples of the media type
* @param bool $required Whether the media_type is required
* @param mixed[] ...$extra
* @throws coding_exception
*/
public function __construct(
/** @var type_base|null The OpenAPI Schema to use */
protected ?type_base $schema = null,
/** @var example|null An example of the media type */
protected ?example $example = null,
/** @var example[] An array of examples of the media type */
protected array $examples = [],
/** @var bool Whether the media_type is required */
protected bool $required = false,
...$extra,
) {
if ($example) {
if (count($examples)) {
throw new coding_exception('Only one of example or examples can be specified.');
}
$this->examples[$example->get_name()] = $example;
}
parent::__construct(...$extra);
}
#[\Override]
public function get_openapi_description(
specification $api,
?string $path = null,
): ?\stdClass {
$data = (object) [];
if ($this->schema) {
$data->schema = $this->schema->get_openapi_schema(
api: $api,
);
}
if (count($this->examples)) {
$data->examples = [];
foreach ($this->examples as $example) {
$data->examples[$example->get_name()] = $example->get_openapi_schema($api);
}
}
if ($this->required) {
$data->required = true;
}
return $data;
}
/**
* Get the schema for this media type.
*
* @return type_base
*/
public function get_schema(): type_base {
return $this->schema;
}
/**
* Get the mimetype for this media type.
*
* @return string
*/
public function get_mimetype(): string {
return static::get_encoding();
}
/**
* Get the encoding for this media type.
*
* @return string
*/
abstract public static function get_encoding(): string;
/**
* Whether this query parameter is required.
*
* @return bool
*/
public function is_required(): bool {
return $this->required;
}
}
@@ -0,0 +1,112 @@
<?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\router\schema\response\content;
use core\router\schema\openapi_base;
use core\router\schema\specification;
/**
* A standard Moodle response for all supported payload types.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class payload_response_type extends openapi_base {
/** @var array Arguments to pass the media instantiator */
protected array $args;
/**
* Crate a new payload response type.
*
* @param bool $required Whether this query parameter is required.
* @param array ...$args Extra args for future compatibility.
*/
public function __construct(
/** @var bool Whether a payload response is required */
protected bool $required = false,
...$args,
) {
parent::__construct();
$this->args = $args;
}
/**
* Get the supported content types.
*
* @return \class-string<media_type>[]
*/
public function get_supported_content_types(): array {
return [
json_media_type::class,
];
}
/**
* Get a media type instance for the given mimetype.
*
* @param string|null $mimetype The mimetype to get the instance for.
* @param string|null $classname The classname to get the instance for.
* @param bool $required Whether the media type is required.
* @return media_type|null
*/
public function get_media_type_instance(
?string $mimetype = null,
?string $classname = null,
bool $required = false,
): ?media_type {
if ($classname) {
return new $classname(...$this->args);
}
foreach ($this->get_supported_content_types() as $contenttypeclass) {
if (empty($mimetype) || $contenttypeclass::get_encoding() === $mimetype) {
$args = $this->args;
$args['required'] = $required;
return new $contenttypeclass(...$args);
}
}
return null; // @codeCoverageIgnore
}
#[\Override]
public function get_openapi_description(
specification $api,
?string $path = null,
): ?\stdClass {
$content = (object) [];
foreach ($this->get_supported_content_types() as $contenttypeclass) {
$contenttype = new $contenttypeclass(...$this->args);
$content->{$contenttype->get_mimetype()} = $contenttype->get_openapi_schema(
api: $api,
);
}
return $content;
}
/**
* Whether this query parameter is required.
*
* @return bool
*/
public function is_required(): bool {
return $this->required;
}
}
@@ -0,0 +1,83 @@
<?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\router\schema\response;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* A Payload Response for a Routed request.
*
* This response is a container for a response which contains a set of data.
* It is used to pass data from a controller to the routing engine, where it will be formatted into the
* response type requested by the client.
*
* This approach is inspired and based upon slim-routing https://github.com/juliangut/slim-routing
* We only need a fraction of this functionality.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class payload_response extends abstract_response {
/**
* Create a new payload response.
*
* @param array $payload The payload
* @param ServerRequestInterface $request The request
* @param ResponseInterface|null $response The response
*/
public function __construct(
/** @var array The payload */
public readonly array $payload,
ServerRequestInterface $request,
?ResponseInterface $response = null,
) {
parent::__construct($request, $response);
}
#[\Override]
public function get_response(
ResponseFactoryInterface $responsefactory,
): ?ResponseInterface {
$response = parent::get_response($responsefactory);
$response->getBody()->write((string) json_encode(
$this->payload,
$this->get_json_flags(),
));
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
}
/**
* Get the flags to use when encoding JSON.
*
* @return int
*/
private function get_json_flags(): int {
global $CFG;
$flags = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_PRESERVE_ZERO_FRACTION;
if ($CFG->debugdeveloper) {
$flags |= \JSON_PRETTY_PRINT;
}
return $flags;
}
}
@@ -0,0 +1,135 @@
<?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\router\schema\response;
use core\exception\coding_exception;
use core\router\schema\openapi_base;
use core\router\schema\response\content\media_type;
use core\router\schema\specification;
use core\router\schema\response\content\payload_response_type;
use Psr\Http\Message\ResponseInterface;
/**
* An OpenAPI Response.
*
* https://spec.openapis.org/oas/v3.1.0#response-object
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class response extends openapi_base {
/**
* Create a new described response.
*
* @param int $statuscode The status code for this response
* @param string $description A description of this response
* @param array $headers The headers associated with this response
* @param array|payload_response_type $content The content of this response
* @param mixed ...$extra Any extra data to store
* @throws coding_exception
*/
public function __construct(
/** @var int The status code for this response */
public readonly int $statuscode = 200,
/** @var string A description of this response */
public readonly string $description = '',
/** @var array The headers associated with this response */
private readonly array $headers = [],
/** @var array|payload_response_type The content of this response */
public readonly array|payload_response_type $content = [],
...$extra,
) {
if (is_array($content)) {
foreach ($content as $contentitem) {
if (!$contentitem instanceof media_type) {
throw new coding_exception('Content must be an array of payload response types');
}
}
}
parent::__construct(...$extra);
}
/**
* Validate the response.
*
* @param ResponseInterface $response The response to validate
*/
public function validate(
ResponseInterface $response,
): void {
$response;
}
/**
* Get the description for this response.
*
* @return string
*/
protected function get_description(): string {
if ($this->description !== '') {
return $this->description;
}
return match ($this->statuscode) {
200 => 'OK',
default => '',
};
}
#[\Override]
public function get_openapi_description(
specification $api,
?string $path = null,
): ?\stdClass {
$data = (object) [
'description' => $this->get_description(),
];
if (count($this->headers)) {
foreach ($this->headers as $header) {
$data->headers[$header->get_name()] = $header->get_openapi_schema(
api: $api,
);
}
}
if ($this->content instanceof content\payload_response_type) {
$data->content = $this->content->get_openapi_schema(
api: $api,
);
} else if (count($this->content)) {
foreach ($this->content as $body) {
$data->content[$body->get_mimetype()] = $body->get_openapi_schema(
api: $api,
);
}
}
return $data;
}
/**
* Get the status code for this response.
*
* @return int
*/
public function get_status_code(): int {
return $this->statuscode;
}
}
@@ -0,0 +1,47 @@
<?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\router\schema\response;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* An abstract response to a request.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface response_type {
/**
* Get the Request object for this response.
*
* @return ServerRequestInterface
*/
public function get_request(): ServerRequestInterface;
/**
* Get the Response object for this response.
*
* @param ResponseFactoryInterface $responsefactory The response factory
* @return null|ResponseInterface
*/
public function get_response(
ResponseFactoryInterface $responsefactory,
): ?ResponseInterface;
}
@@ -0,0 +1,88 @@
<?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\router\schema\response;
use GuzzleHttp\Psr7\Utils;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* A response which will render the specified template.
*
* This approach is inspired and based upon slim-routing https://github.com/juliangut/slim-routing
* We only need a fraction of this functionality.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class view_response extends abstract_response {
/**
* Create a new view response.
*
* @param string $template The template name
* @param array $parameters The parameters to pass
* @param ServerRequestInterface $request The request
* @param ResponseInterface|null $response The response
*/
public function __construct(
/** @var string The template name */
private readonly string $template,
/** @var array The parameters to pass */
private readonly array $parameters,
ServerRequestInterface $request,
?ResponseInterface $response = null,
) {
parent::__construct($request, $response);
}
/**
* Get the template name.
*
* @return string
*/
public function get_template_name(): string {
return $this->template;
}
/**
* Get the parameters.
*
* @return array
*/
public function get_parameters(): array {
return $this->parameters;
}
#[\Override]
public function get_response(
ResponseFactoryInterface $responsefactory,
): ?ResponseInterface {
global $OUTPUT;
$response = parent::get_response($responsefactory);
return $response
->withHeader('Content-Type', 'text/html; charset=utf-8')
->withBody(Utils::streamFor(
$OUTPUT->render_from_template(
$this->get_template_name(),
$this->get_parameters(),
),
));
}
}
+521
View File
@@ -0,0 +1,521 @@
<?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\router\schema;
use coding_exception;
use core\router\response\invalid_parameter_response;
use core\router\response\not_found_response;
use core\router\route;
use core\router\route_loader_interface;
use core\router\schema\objects\type_base;
use core\router\schema\response\response;
use core\url;
use stdClass;
/**
* Moodle OpenApi Specification class.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class specification implements
\JsonSerializable
{
/** @var string The OpenAPI version represented in this specification */
public const OPENAPI_VERSION = '3.1.0';
/** @var stdClass The data which forms the specification */
protected stdClass $data;
/** @var bool Whether the data has been finalised for output yet */
protected bool $finalised = false;
/** @var callable[] A list of common responses that are frequently found in paths */
protected array $commonresponses = [];
/**
* Constructor to configure base information.
*/
public function __construct() {
$this->data = (object) [
'openapi' => self::OPENAPI_VERSION,
'info' => (object) [
'title' => 'Moodle LMS',
'description' => 'REST API for Moodle LMS',
'summary' => 'Moodle LMS REST API',
'license' => (object) [
'name' => 'GNU GPL v3 or later',
'url' => 'https://www.gnu.org/licenses/gpl-3.0.html',
],
],
// Servers are added during output.
'servers' => [],
// Paths are added after initialisation.
'paths' => (object) [],
'components' => (object) [
// Note: This list must be kept in-sync with add_component.
'schemas' => (object) [],
'responses' => (object) [],
'parameters' => (object) [],
'examples' => (object) [],
'requestBodies' => (object) [],
'headers' => (object) [],
// The add_component method does not support securitySchemes because we hard-code these.
'securitySchemes' => (object) [
'api_key' => (object) [
'type' => 'apiKey',
'name' => 'api_key',
'in' => parameter::IN_HEADER,
],
'cookie' => (object) [
'type' => 'apiKey',
'name' => 'MoodleSession',
'in' => parameter::IN_COOKIE,
],
// TODO MDL-82242: Add support for OAuth2.
],
],
// TODO MDL-82242: Add support for OAuth2.
'security' => [
(object) [
'api_key' => [],
'cookie' => [],
],
],
'externalDocs' => (object) [
'description' => 'Moodle Developer Docs',
'url' => 'https://moodledev.io',
],
];
$this->generate_common_responses();
}
/**
* Generate the callables for common responses that are frequently found in paths.
*
* @return specification
*/
protected function generate_common_responses(): self {
$invalidresponse = new invalid_parameter_response();
$notfoundresponse = new not_found_response();
$this->commonresponses[] = function (
route $route,
stdClass $data
) use (
$invalidresponse,
$notfoundresponse,
): stdClass {
if ($route->has_any_validatable_parameter()) {
if (!array_key_exists($invalidresponse::get_exception_status_code(), $data->responses)) {
$data->responses[$invalidresponse::get_exception_status_code()] = $invalidresponse->get_openapi_schema($this);
}
if (!array_key_exists($notfoundresponse::get_exception_status_code(), $data->responses)) {
$data->responses[$notfoundresponse::get_exception_status_code()] = $notfoundresponse->get_openapi_schema($this);
}
}
return $data;
};
return $this;
}
/**
* Get the common request responses.
*
* @return callable[]
*/
public function get_common_request_responses(): array {
if (empty($this->commonresponses)) {
$this->generate_common_responses(); // @codeCoverageIgnore
}
return $this->commonresponses;
}
/**
* Finalise the data and prepare it for consumption.
*/
protected function finalise(): self {
global $CFG;
if ($this->finalised) {
return $this;
}
// Add the Moodle site version here.
$this->data->info->version = $CFG->version;
// Add the server configuration.
$serverdescription = str_replace("'", "\'", format_string(get_site()->fullname));
$this->add_server(
url::routed_path(route_loader_interface::ROUTE_GROUP_API)->out(),
$serverdescription,
);
$this->finalised = true;
return $this;
}
/**
* Implement the json serialisation interface.
*
* @return mixed
*/
public function jsonSerialize(): mixed {
return $this->get_schema();
}
/**
* Get the OpenAPI schema.
*
* @return stdClass
*/
final public function get_schema(): stdClass {
return $this
->finalise()
->data;
}
/**
* Add a component to the components object.
*
* https://spec.openapis.org/oas/v3.1.0#components-object
*
* Note: The following component types are supported:
*
* - schemas
* - responses
* - parameters
* - examples
* - requestBodies
* - headers
*
* At this time, other component types are not supported.
*
* @param openapi_base $object
* @return specification
* @throws coding_exception If the component type is unknown.
*/
public function add_component(openapi_base $object): self {
match (true) {
is_a($object, header_object::class) => $this->add_header($object),
is_a($object, parameter::class) => $this->add_parameter($object),
is_a($object, response::class) => $this->add_response($object),
is_a($object, example::class) => $this->add_example($object),
is_a($object, request_body::class) => $this->add_request_body($object),
is_a($object, type_base::class) => $this->add_schema($object),
default => throw new coding_exception("Unknown object type."),
};
return $this;
}
/**
* Add a server to the specification.
*
* @param string $url The URL of the API base
* @param string $description
* @return specification
*/
public function add_server(
string $url,
string $description,
): self {
$this->data->servers[] = (object) [
'url' => $url,
'description' => $description,
];
return $this;
}
/**
* Add an API Path.
*
* @param string $component The Moodle component
* @param route $route The route which handles this request
* @return specification
*/
public function add_path(
string $component,
route $route,
): self {
// Compile the final path, complete with component prefix.
[$type, $subsystem] = \core_component::normalize_component($component);
if ($type === 'core') {
if ($subsystem) {
$path = "/{$subsystem}";
} else {
$path = "/core";
}
} else {
$path = "/{$component}";
}
$path .= $route->get_path();
// Helper to add the path to the specification.
// Note: We use this helper because OpenAPI does not support optional parameters.
// Therefore we must handle that in Moodle, adding path variants with and without each optional parameter.
$addpath = function (string $path) use ($route, $component) {
// Remove the optional parameters delimiters from the path.
$path = str_replace(
['[', ']'],
'',
$path,
);
// Get the OpenAPI description for this path with the updated path.
$pathdocs = $this->get_openapi_schema_for_route(
route: $route,
component: $component,
path: $path,
);
if (!property_exists($this->data->paths, $path)) {
$this->data->paths->$path = (object) [];
}
foreach ((array) $pathdocs as $method => $methoddata) {
// Copy each of the pathdocs into place.
$this->data->paths->{$path}->{$method} = $methoddata;
}
};
// First add the entire path complete with all optional parameters.
// The optional parameter delimiters are `[` and `]`, and are removed in `$addpath`.
$addpath($path);
// Check for any optional parameters.
// OpenAPI does not support optional parameters so we have to duplicate routes instead.
// We can determine if this is optional if there is any `[` character before it in the path.
// There can be no required parameter after any optional parameter.
$optionalparameters = array_filter(
array: $route->get_path_parameters(),
callback: fn ($parameter) => !$parameter->is_required($route),
);
if (!empty($optionalparameters)) {
// Go through the path from end to start removing optional parameters and adding them to the path list.
while (strrpos($path, '[') !== false) {
$path = substr($path, 0, strrpos($path, '['));
$addpath($path);
}
}
return $this;
}
/**
* Add a schema to the shared components section of the specification.
*
* @param type_base $schema
* @return specification
*/
protected function add_schema(
type_base $schema,
): self {
$name = $schema->get_reference(qualify: false);
if (!property_exists($this->data->components->schemas, $name)) {
$this->data->components->schemas->$name = $schema->get_openapi_description($this);
}
return $this;
}
/**
* Add a schema to the shared components section of the specification.
*
* @param parameter $parameter
* @return specification
*/
protected function add_parameter(
parameter $parameter,
): self {
$name = $parameter->get_reference(qualify: false);
$this->data->components->parameters->$name = $parameter->get_openapi_description($this);
return $this;
}
/**
* Add a header to the shared components section of the specification.
*
* @param header_object $header
* @return self
*/
protected function add_header(
header_object $header,
): self {
$name = $header->get_reference(qualify: false);
$this->data->components->headers->$name = $header->get_openapi_description($this);
return $this;
}
/**
* Add a response to the shared components section of the specification.
*
* @param response $response
* @return specification
*/
protected function add_response(
response $response,
): self {
$name = $response->get_reference(qualify: false);
$this->data->components->responses->$name = $response->get_openapi_description($this);
return $this;
}
/**
* Add an example to the shared components section of the specification.
*
* @param example $example
* @return specification
*/
protected function add_example(
example $example,
): self {
$name = $example->get_reference(qualify: false);
$this->data->components->examples->$name = $example->get_openapi_description($this);
return $this;
}
/**
* Add a request body to the shared components section of the specification.
*
* @param request_body $body
* @return specification
*/
protected function add_request_body(
request_body $body,
): self {
$name = $body->get_reference(qualify: false);
$this->data->components->requestBodies->$name = $body->get_openapi_description($this);
return $this;
}
/**
* Check whether a reference is defined
*
* @param string $ref
* @return bool
*/
public function is_reference_defined(
string $ref,
): bool {
if (!str_starts_with($ref, '#/components/')) {
return false;
}
// Remove the leading #/components/ part.
$ref = substr($ref, strlen('#/components/'));
// Split the path and name.
[$path, $name] = explode('/', $ref, 2);
if (!property_exists($this->data->components, $path)) {
return false;
}
return property_exists($this->data->components->$path, $name);
}
/**
* Get the OpenAPI description for this route.
*
* @param route $route
* @param string $component
* @param string $path
* @return stdClass
*/
public function get_openapi_schema_for_route(
route $route,
string $component,
string $path,
): stdClass {
$data = (object) [
'description' => $route->description,
'summary' => $route->title,
'tags' => [$component, ...$route->tags],
'parameters' => [],
'responses' => [],
];
if ($route->get_request_body()) {
$data->requestBody = $route->get_request_body()->get_openapi_schema(
api: $this,
path: $path,
);
}
if ($route->security !== null) {
$data->security = $route->security;
}
if ($route->deprecated) {
$data->deprecated = true;
}
foreach ($route->get_responses() as $response) {
$data->responses[$response->get_status_code()] = $response->get_openapi_schema(
api: $this,
path: $path,
);
}
$data->parameters = array_values(array_filter(
array_map(
fn($param) => $param->get_openapi_schema(
api: $this,
path: $path,
),
array_merge(
$route->get_path_parameters(),
$route->get_query_parameters(),
$route->get_header_parameters(),
),
),
fn($param) => $param !== null,
));
foreach ($this->get_common_request_responses() as $callable) {
$data = $callable($route, $data);
}
$methoddata = [];
$methods = $route->get_methods(['GET']);
foreach ($methods as $method) {
$methoddata[strtolower($method)] = $data;
}
return (object) $methoddata;
}
}
+230
View File
@@ -0,0 +1,230 @@
<?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\router;
use moodle_url;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Routing\RouteContext;
/**
* Routing Helper Utilities.
*
* This class includes a variety of helpers for working with routes, including:
* - redirectors
* - callable to route name converters
* - callable to path converters
* - helpers to fetch the \core\router\route instance
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class util {
/**
* Redirect to the specified URL, carrying all parameters across too.
*
* @param string|moodle_url $path
* @param array $excludeparams Any parameters to exclude from the query params
* @codeCoverageIgnore
*/
public static function redirect_with_params(
string|moodle_url $path,
array $excludeparams = [],
): never {
$params = $_GET;
$url = new moodle_url(
$path,
$params,
);
$url->remove_params($excludeparams);
redirect($url);
}
/**
* Redirect to the route at the callable supplied.
*
* @param callable|array|string $callable
* @param array $params Any parameters to include in the path
* @codeCoverageIgnore
*/
public static function redirect_to_callable(
callable|array|string $callable,
array $params = [],
): never {
$params = array_merge(
$_GET,
$params,
);
$url = self::get_path_for_callable($callable, $params, $params);
redirect($url);
}
/**
* Get the route name for the specified callable.
*
* @param callable|array|string $callable
* @return string
* @throws \coding_exception If the callable could not be resolved into an Array format
*/
public static function get_route_name_for_callable(
callable|array|string $callable,
): string {
$resolver = \core\di::get(\Invoker\CallableResolver::class);
$callable = $resolver->resolve($callable);
if (!is_array($callable)) {
throw new \coding_exception('Resolved callable must be in array form');
}
return get_class($callable[0]) . '::' . $callable[1];
}
/**
* Get the URI path for the specified callable.
*
* @param string|array|callable $callable the Callable to get the URI for
* @param array $params Any parameters to include in the path
* @param array $queryparams Any parameters to include in the query string
* @return moodle_url
*/
public static function get_path_for_callable(
string|array|callable $callable,
array $params,
array $queryparams,
): moodle_url {
global $CFG;
$router = \core\di::get(\core\router::class);
$app = $router->get_app();
$parser = $app->getRouteCollector()->getRouteParser();
$routename = self::get_route_name_for_callable($callable);
return new moodle_url(
url: $parser->fullUrlFor(
new Uri($CFG->wwwroot),
$routename,
$params,
$queryparams,
),
);
}
/**
* Get the route attribute for the specified request.
*
* @param ServerRequestInterface $request
* @return null|route
*/
public static function get_route_instance_for_request(ServerRequestInterface $request): ?route {
if ($route = $request->getAttribute(route::class)) {
return $route;
}
$context = RouteContext::fromRequest($request);
if ($slimroute = $context->getRoute()) {
return self::get_route_instance_for_method($slimroute->getCallable());
}
// This should not be encountered - the route should always be set.
return null; // @codeCoverageIgnore
}
/**
* Get the instance of the \route\router\route attribute for the specified callable if one is available.
*
* @param callable|array|string $callable
* @return null|route The route if one was found.
*/
public static function get_route_instance_for_method(callable|array|string $callable): ?route {
// Normalise the callable using the resolver.
// This happens in the same way that Slim does so.
$resolver = \core\di::get(\Invoker\CallableResolver::class);
$callable = $resolver->resolve($callable);
if (!is_array($callable)) {
// The callable could not be resolved into an array.
return null;
}
// Locate the Class for this callable.
$classinfo = new \ReflectionClass($callable[0]);
// Locate the method for this callable.
$methodinfo = $classinfo->getMethod($callable[1]);
if (!$methodinfo) {
// The method does not exist. This shouldn't be possible because the resolver will throw an exception.
return null; // @codeCoverageIgnore
}
return self::attempt_get_route_instance_for_method($classinfo, $methodinfo);
}
/**
* Attempt to get the route instance for the specified method, handling any errors in the code along the way.
*
* @param \ReflectionClass $classinfo
* @param \ReflectionMethod $methodinfo
* @return null|route
*/
private static function attempt_get_route_instance_for_method(
\ReflectionClass $classinfo,
\ReflectionMethod $methodinfo,
): ?route {
$instantiator = function (array $attributes) {
global $CFG;
try {
return $attributes ? $attributes[0]->newInstance() : null;
// @codeCoverageIgnoreStart
} catch (\Throwable $e) {
// The route attribute could not be instantiated.
// When debugging, this is useful to know.
// When not, log to error_log.
if (!$CFG->debugdisplay) {
debugging('Could not instantiate route attribute: ' . $e->getMessage());
return null;
}
default_exception_handler($e);
}
// @codeCoverageIgnoreEnd
};
$methodattributes = $methodinfo->getAttributes(route::class);
$methodroute = $instantiator($methodattributes);
if (!$methodroute) {
// No route found.
return null;
}
$classattributes = $classinfo->getAttributes(route::class);
if ($classattributes) {
$classinstance = $instantiator($classattributes);
if ($classinstance) {
// The class has a #route attribute.
$methodroute->set_parent($classinstance);
}
}
return $methodroute;
}
}
+549
View File
@@ -0,0 +1,549 @@
<?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\tests;
use core\router;
use core\router\bridge;
use core\router\mocking_route_loader;
use core\router\route_loader_interface;
use core\router\schema\openapi_base;
use core\router\schema\referenced_object;
use core\router\schema\specification;
use stdClass;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\ServerRequest;
use GuzzleHttp\Psr7\Uri;
use PHPUnit\Framework\ExpectationFailedException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Slim\App;
use Slim\Middleware\RoutingMiddleware;
use Slim\Routing\Route;
use Slim\Routing\RouteContext;
/**
* Tests for user preference API handler.
*
* @package core
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class route_testcase extends \advanced_testcase {
/**
* Update the test route loader using the supplied callback.
*
* @param callable $modifier
*/
protected function update_test_route_loader(
callable $modifier,
): void {
self::load_fixture('core', 'router/mocking_route_loader.php');
$routeloader = \core\di::get(mocking_route_loader::class);
$modifier($routeloader);
\core\di::set(route_loader_interface::class, $routeloader);
}
/**
* Add a route from a class method.
*
* @param string $classname The class to add the route from
* @param string $methodname The method name to add
* @param null|string $grouppath The path to the route group
*/
protected function add_route_to_route_loader(
string $classname,
string $methodname,
?string $grouppath = null,
) {
$grouppath = $grouppath ?? $this->guess_group_path_from_classname($classname);
$this->update_test_route_loader(fn (mocking_route_loader $routeloader) => $routeloader->mock_route_from_class_method(
$grouppath,
new \ReflectionMethod($classname, $methodname),
));
}
/**
* Add all routes from the specified class to the test loader.
*
* Only methods within the class with a #[route] attribute will be added.
*
* @param string $classname The class to add routes from
* @param null|string $grouppath The path of the route group
*/
protected function add_class_routes_to_route_loader(
string $classname,
?string $grouppath = null,
): void {
$this->update_test_route_loader(
fn (mocking_route_loader $routeloader) => $routeloader->add_all_routes_in_class(
grouppath: $grouppath ?? $this->guess_group_path_from_classname($classname),
class: $classname,
),
);
}
/**
* Guess the group path from a class name.
*
* @param string $classname
* @return string
*/
protected function guess_group_path_from_classname(
string $classname,
): string {
[, , $l3] = explode('\\', $classname, 4);
if ($l3 === 'api') {
return route_loader_interface::ROUTE_GROUP_API;
}
throw new \coding_exception("Unable to determine route path for '{$classname}'");
}
/**
* Mock a route from a route attribute.
*
* @param string $grouppath
* @param \core\router\route $route
* @param string $name
* @param callable|null $callable
*/
protected function mock_route_from_route_attribute(
string $grouppath,
\core\router\route $route,
string $name = 'route',
?callable $callable = null,
): void {
if ($callable === null) {
$callable = fn ($request, $response) => $response->withStatus(200);
}
$this->update_test_route_loader(fn (mocking_route_loader $routeloader) => $routeloader->mock_route_from_callable(
grouppath: $grouppath,
methods: $route->get_methods(['GET']),
pattern: $route->get_path(),
callable: $callable,
name: $name,
));
}
/**
* Get a fully-configured instance of the Moodle Routing Application.
*
* @return App
*/
protected function get_app(): App {
$router = $this->get_router();
return $router->get_app();
}
/**
* Get a fully-configured instance of the Moodle Routing Application.
*
* @param string $basepath The basepath for the router
* @return router
*/
protected function get_router(string $basepath = ''): router {
\core\di::set(
router::class,
\DI\autowire(router::class)->constructorParameter('basepath', $basepath),
);
return \core\di::get(router::class);
}
/**
* Get an unconfigured instance of the Slim Application.
*
* @return App
*/
protected function get_simple_app(): App {
global $CFG;
require_once("{$CFG->libdir}/nikic/fast-route/src/functions.php");
$app = bridge::create(
container: \core\di::get_container(),
);
return $app;
}
/**
* Get the request for a route which is known to the router.
*
* @param \core\router\route $route
* @param string $path
* @return ServerRequestInterface
*/
protected function get_request_for_routed_route(
\core\router\route $route,
string $path,
): ServerRequestInterface {
$this->mock_route_from_route_attribute('', $route);
// Grab just one method.
$methods = $route->get_methods();
$method = $methods ? reset($methods) : 'GET';
$request = $this->create_request(
method: $method,
path: $path,
prefix: '',
route: $route,
);
$request = $this->route_request(
$this->get_app(),
$request,
);
return $request;
}
/**
* Create a Request object.
*
* @param string $method
* @param string $path
* @param string $prefix
* @param array $headers
* @param array $cookies
* @param array $serverparams
* @param null|\core\router\route $route
* @return ServerRequestInterface
*/
protected function create_request(
string $method,
string $path,
string $prefix = route_loader_interface::ROUTE_GROUP_API,
array $headers = ['Content-Type' => 'application/json'],
array $cookies = [],
array $serverparams = [],
?\core\router\route $route = null,
): ServerRequestInterface {
$uri = new Uri($prefix . $path);
$request = new ServerRequest(
method: $method,
headers: $headers,
uri: $uri,
serverParams: $serverparams,
);
// Sadly Guzzle's Uri only deals with query strings, not query params.
$query = $uri->getQuery();
if ($query) {
$queryparams = [];
foreach (explode('&', $query) as $queryparam) {
[$key, $value] = explode('=', $queryparam, 2);
$queryparams[$key] = $value;
}
$request = $request->withQueryParams($queryparams);
}
if ($route) {
$request = $request->withAttribute(\core\router\route::class, $route);
}
return $request
->withCookieParams($cookies);
}
/**
* Process a request with the app.
*
* @param string $method
* @param string $path
* @param string $prefix
* @param array $headers
* @param null|StreamInterface $body
* @param null|string $contenttype
* @param array $cookies
* @param array $serverparams
* @return ResponseInterface
*/
protected function process_request(
string $method,
string $path,
string $prefix = '',
array $headers = ['HTTP_ACCEPT' => 'application/json'],
?StreamInterface $body = null,
?string $contenttype = 'application/json',
array $cookies = [],
array $serverparams = [],
): ResponseInterface {
$app = $this->get_app();
if ($contenttype !== null) {
$headers['Content-Type'] = $contenttype;
}
$request = $this->create_request(
$method,
$path,
$prefix,
$headers,
$cookies,
$serverparams,
);
if ($body) {
$request = $request->withBody($body);
}
return $app->handle($request);
}
/**
* Process a request with the app.
*
* @param string $method
* @param string $path
* @param array $headers
* @param null|StreamInterface $body
* @param array $cookies
* @param array $serverparams
* @return ResponseInterface
*/
protected function process_api_request(
string $method,
string $path,
array $headers = ['HTTP_ACCEPT' => 'application/json'],
?StreamInterface $body = null,
array $cookies = [],
array $serverparams = [],
): ResponseInterface {
return $this->process_request(
method: $method,
path: $path,
prefix: route_loader_interface::ROUTE_GROUP_API,
headers: $headers,
body: $body,
cookies: $cookies,
serverparams: $serverparams,
);
}
/**
* Route a request within the app.
*
* @param App $app
* @param ServerRequestInterface $request
* @return ServerRequestInterface
*/
protected function route_request(
App $app,
ServerRequestInterface $request,
): ServerRequestInterface {
$routingmiddleware = new RoutingMiddleware(
$app->getRouteResolver(),
$app->getRouteCollector()->getRouteParser(),
);
return $routingmiddleware->performRouting($request);
}
/**
* Create a route and route it to create a request.
*
* @param string $routepath
* @param string $requestpath
* @return ServerRequestInterface
*/
protected function create_route(
string $routepath,
string $requestpath,
): ServerRequestInterface {
$app = $this->get_simple_app();
$app->get($routepath, fn () => new Response());
$request = $this->route_request($app, new ServerRequest('GET', $requestpath));
return $request;
}
/**
* Get the Slim Route object from a Request object.
*
* @param ServerRequestInterface $request
* @return Route
*/
protected function get_slim_route_from_request(
ServerRequestInterface $request,
): Route {
return $request->getAttribute(RouteContext::ROUTE);
}
/**
* Assert that a Response object was valid.
*
* @param ResponseInterface $response
* @param null|int $statuscode The expected status code
* @throws ExpectationFailedException
*/
protected function assert_valid_response(
ResponseInterface $response,
?int $statuscode = 200,
): void {
$this->assertInstanceOf(Response::class, $response);
$this->assertEquals(
$statuscode,
$response->getStatusCode(),
"Response status code is not $statuscode",
);
}
/**
* Assert that the supplied response related to an exception.
*
* @param ResponseInterface $response
* @param null|int $responsecode The expected response code
*/
protected function assert_exception_response(
ResponseInterface $response,
?int $responsecode = null,
): void {
$this->assertInstanceOf(Response::class, $response);
$this->assertNotEquals(
200,
$response->getStatusCode(),
);
if ($responsecode !== null) {
$this->assertEquals(
$responsecode,
$response->getStatusCode(),
);
}
$payload = $this->decode_response($response);
$this->assertObjectHasProperty('message', $payload);
$this->assertObjectHasProperty('stacktrace', $payload);
}
/**
* Assert that the supplied response was an invalid_parameter_exception response.
*
* @param ResponseInterface $response
*/
protected function assert_invalid_parameter_response(
ResponseInterface $response,
): void {
$this->assert_exception_response($response, 400);
$payload = $this->decode_response($response);
$this->assertObjectHasProperty('errorcode', $payload);
$this->assertEquals('invalidparameter', $payload->errorcode);
}
/**
* Assert that the supplied response was an access_denied exception response.
*
* @param ResponseInterface $response
*/
protected function assert_access_denied_response(
ResponseInterface $response,
): void {
$this->assert_exception_response($response, 403);
$payload = $this->decode_response($response);
$this->assertObjectHasProperty('errorcode', $payload);
}
/**
* Assert that the supplied response was a not_found exception response.
*
* @param \Psr\Http\Message\ResponseInterface $response
*/
protected function assert_not_found_response(
ResponseInterface $response,
): void {
$this->assert_exception_response($response, 404);
$payload = $this->decode_response($response);
$this->assertObjectHasProperty('errorcode', $payload);
}
/**
* Decode the JSON response for a Response object.
*
* @param ResponseInterface $response
* @param bool $forcearray Force the contents to Array instead of Object
* @return stdClass|array
*/
protected function decode_response(
ResponseInterface $response,
bool $forcearray = false,
): stdClass|array {
if ($forcearray) {
return json_decode(
json: (string) $response->getBody(),
associative: true,
);
} else {
return (object) json_decode(
json: (string) $response->getBody(),
associative: false,
flags: JSON_FORCE_OBJECT,
);
}
}
/**
* Get the schema for an OpenAPI Component.
*
* Components include headers, parameters, responses, examples, requestBodies, and schemas.
*
* All components are subclasses of the openapi_base class and may be referenced.
*
* Any component which implements the referenced_object interface will return a reference
* to the stored internal object.
*
* @param specification $api
* @param openapi_base $component
* @return stdClass|null
*/
protected function get_api_component_schema(
specification $api,
openapi_base $component,
): ?stdClass {
$this->assertInstanceOf(referenced_object::class, $component);
if (is_a($component, \core\router\schema\header_object::class)) {
$type = 'headers';
} else if (is_a($component, \core\router\schema\parameter::class)) {
$type = 'parameters';
} else if (is_a($component, \core\router\schema\response\response::class)) {
$type = 'responses';
} else if (is_a($component, \core\router\schema\example::class)) {
$type = 'examples';
} else if (is_a($component, \core\router\schema\request_body::class)) {
$type = 'requestBodies';
} else if (is_a($component, \core\router\schema\objects\type_base::class)) {
$type = 'schemas';
} else {
$this->fail('Component is not a recognised type');
}
$ref = $component->get_reference(false);
$schema = $api->get_schema();
$components = $schema->components;
$component = $components->{$type}->{$ref} ?? null;
return $component;
}
}
+22
View File
@@ -622,6 +622,28 @@ class url {
return $url;
}
/**
* Create a new moodle_url instance from routed path.
*
* @param string $path The routed path
* @param null|array $params The path parameters
* @param null|string $anchor The anchor
* @return self
*/
public static function routed_path(
string $path,
?array $params = null,
?string $anchor = null,
): self {
global $CFG;
if (!$CFG->routerconfigured) {
$path = '/r.php/' . ltrim($path, '/');
}
$url = new self($path, $params, $anchor);
return $url;
}
/**
* General moodle file url.
*
+252 -137
View File
@@ -14,27 +14,30 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* User class
*
* @package core
* @copyright 2013 Rajesh Taneja <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
use core\context\user as context_user;
use core\context\course as context_course;
use core\context\system as context_system;
use core_user\fields;
defined('MOODLE_INTERNAL') || die();
use core\exception\invalid_parameter_exception;
use core\exception\moodle_exception;
use core\exception\coding_exception;
use core\output\theme_config;
use core\output\user_picture;
use core_date;
use dml_exception;
use stdClass;
/**
* User class to access user details.
*
* @todo move api's from user/lib.php and deprecate old ones.
* @todo MDL-82650 Move api's from user/lib.php and deprecate old ones.
* @package core
* @copyright 2013 Rajesh Taneja <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_user {
class user {
/**
* No reply user id.
*/
@@ -80,7 +83,7 @@ class core_user {
'firstnamephonetic',
'lastnamephonetic',
'middlename',
'alternatename'
'alternatename',
];
/** @var int Indicates that user profile view should be prevented */
@@ -104,7 +107,7 @@ class core_user {
/**
* Return user object from db or create noreply or support user,
* if userid matches corse_user::NOREPLY_USER or corse_user::SUPPORT_USER
* if userid matches \core\user::NOREPLY_USER or \core\user::SUPPORT_USER
* respectively. If userid is not found, then return false.
*
* @param int $userid user id
@@ -128,7 +131,7 @@ class core_user {
return self::get_support_user();
break;
default:
return $DB->get_record('user', array('id' => $userid), $fields, $strictness);
return $DB->get_record('user', ['id' => $userid], $fields, $strictness);
}
}
@@ -153,7 +156,7 @@ class core_user {
$mnethostid = $CFG->mnet_localhost_id;
}
return $DB->get_record('user', array('email' => $email, 'mnethostid' => $mnethostid), $fields, $strictness);
return $DB->get_record('user', ['email' => $email, 'mnethostid' => $mnethostid], $fields, $strictness);
}
/**
@@ -177,7 +180,38 @@ class core_user {
$mnethostid = $CFG->mnet_localhost_id;
}
return $DB->get_record('user', array('username' => $username, 'mnethostid' => $mnethostid), $fields, $strictness);
return $DB->get_record('user', ['username' => $username, 'mnethostid' => $mnethostid], $fields, $strictness);
}
/**
* Return User object based on their idnumber.
*
* @param string $idnumber The idnumber of the user searched.
* @param string $fields A comma separated list of user fields to be returned, support and noreply user.
* @param null|int $mnethostid The id of the remote host.
* @param int $strictness IGNORE_MISSING means compatible mode, false returned if user not found, debug message if more found;
* IGNORE_MULTIPLE means return first user, ignore multiple user records found(not recommended);
* MUST_EXIST means throw an exception if no user record or multiple records found.
* @return stdClass|bool user record if found, else false.
*/
public static function get_user_by_idnumber(
string $idnumber,
string $fields = '*',
?int $mnethostid = null,
int $strictness = IGNORE_MISSING,
): stdClass|bool {
global $DB, $CFG;
// Because we use the username as the search criteria, we must also restrict our search based on mnet host.
if (empty($mnethostid)) {
// If empty, we restrict to local users.
$mnethostid = $CFG->mnet_localhost_id;
}
return $DB->get_record('user', [
'idnumber' => $idnumber,
'mnethostid' => $mnethostid,
], $fields, $strictness);
}
/**
@@ -210,13 +244,17 @@ class core_user {
* user identity fields.
*
* @param string $query Search query text
* @param \context_course|null $coursecontext Course context or null if system-wide
* @param context_course|null $coursecontext Course context or null if system-wide
* @param int $max Max number of users to return, default 30 (zero = no limit)
* @param int $querylimit Max number of database queries, default 5 (zero = no limit)
* @return array Array of user objects with limited fields
*/
public static function search($query, \context_course $coursecontext = null,
$max = 30, $querylimit = 5) {
public static function search(
$query,
?context_course $coursecontext = null,
$max = 30,
$querylimit = 5
) {
global $CFG, $DB;
require_once($CFG->dirroot . '/user/lib.php');
@@ -229,15 +267,16 @@ class core_user {
}
// Check permission to view profiles at each context.
$systemcontext = \context_system::instance();
$systemcontext = context_system::instance();
$viewsystem = has_capability('moodle/user:viewdetails', $systemcontext);
if ($viewsystem) {
$userquery = 'SELECT id FROM {user}';
$userparams = [];
}
if (!$viewsystem) {
list($userquery, $userparams) = self::get_enrolled_sql_on_courses_with_capability(
'moodle/user:viewdetails');
[$userquery, $userparams] = self::get_enrolled_sql_on_courses_with_capability(
'moodle/user:viewdetails'
);
if (!$userquery) {
// No permissions anywhere, return nothing.
return [];
@@ -245,7 +284,7 @@ class core_user {
}
// Start building the WHERE clause based on name.
list ($where, $whereparams) = users_search_sql($query, 'u');
[$where, $whereparams] = users_search_sql($query, 'u');
// We allow users to search with extra identity fields (as well as name) but only if they
// have the permission to display those identity fields.
@@ -278,8 +317,9 @@ class core_user {
$whereparams = array_merge($whereparams, $extraparams);
} else {
// Get all courses where user can view full user identity.
list($sql, $params) = self::get_enrolled_sql_on_courses_with_capability(
'moodle/site:viewuseridentity');
[$sql, $params] = self::get_enrolled_sql_on_courses_with_capability(
'moodle/site:viewuseridentity'
);
if ($sql) {
// Join that with the user query to get an extra field indicating if we can.
$userquery = "
@@ -299,9 +339,9 @@ class core_user {
// Default order is just name order. But if searching within a course then we show users
// within the course first.
list ($order, $orderparams) = users_order_by_sql('u', $query, $systemcontext);
[$order, $orderparams] = users_order_by_sql('u', $query, $systemcontext);
if ($coursecontext) {
list ($sql, $params) = get_enrolled_sql($coursecontext);
[$sql, $params] = get_enrolled_sql($coursecontext);
$mainfield = 'innerusers2.id';
if ($usingshowidentity) {
$mainfield .= ', innerusers2.showidentity';
@@ -323,13 +363,17 @@ class core_user {
$pos = 0;
$readcount = $max + 2;
for ($i = 0; $i < $querylimit; $i++) {
$rawresult = $DB->get_records_sql("
$rawresult = $DB->get_records_sql(
"
SELECT $selectfields
FROM ($userquery) users
JOIN {user} u ON u.id = users.id
WHERE $where
ORDER BY $order", array_merge($userparams, $whereparams, $orderparams),
$pos, $readcount);
ORDER BY $order",
array_merge($userparams, $whereparams, $orderparams),
$pos,
$readcount
);
foreach ($rawresult as $user) {
// Skip guest.
if ($user->username === 'guest') {
@@ -371,8 +415,12 @@ class core_user {
*/
protected static function get_enrolled_sql_on_courses_with_capability($capability) {
// Get all courses where user have the capability.
$courses = get_user_capability_course($capability, null, true,
implode(',', array_values(context_helper::get_preload_record_columns('ctx'))));
$courses = get_user_capability_course(
$capability,
null,
true,
implode(',', array_values(context_helper::get_preload_record_columns('ctx')))
);
if (!$courses) {
return [null, null];
}
@@ -384,8 +432,8 @@ class core_user {
$unionparams = [];
foreach ($courses as $course) {
// Get SQL to list user ids enrolled in this course.
\context_helper::preload_from_record($course);
list ($sql, $params) = get_enrolled_sql(\context_course::instance($course->id));
context_helper::preload_from_record($course);
[$sql, $params] = get_enrolled_sql(context_course::instance($course->id));
// Combine to a big union query.
if ($unionsql) {
@@ -528,7 +576,7 @@ class core_user {
return false;
}
if ($checkdb) {
return $DB->record_exists('user', array('id' => $userid));
return $DB->record_exists('user', ['id' => $userid]);
} else {
return true;
}
@@ -573,11 +621,11 @@ class core_user {
throw new moodle_exception('guestsarenotallowed', 'error');
}
if ($checksuspended and $user->suspended) {
if ($checksuspended && $user->suspended) {
throw new moodle_exception('suspended', 'auth');
}
if ($checknologin and $user->auth == 'nologin') {
if ($checknologin && $user->auth == 'nologin') {
throw new moodle_exception('suspended', 'auth');
}
}
@@ -589,12 +637,12 @@ class core_user {
* @param array $filemanageroptions
* @return bool True if the user was updated, false if it stayed the same.
*/
public static function update_picture(stdClass $usernew, $filemanageroptions = array()) {
public static function update_picture(stdClass $usernew, $filemanageroptions = []) {
global $CFG, $DB;
require_once("$CFG->libdir/gdlib.php");
$context = context_user::instance($usernew->id, MUST_EXIST);
$user = core_user::get_user($usernew->id, 'id, picture', MUST_EXIST);
$user = self::get_user($usernew->id, 'id, picture', MUST_EXIST);
$newpicture = $user->picture;
// Get file_storage to process files.
@@ -636,7 +684,7 @@ class core_user {
}
if ($newpicture != $user->picture) {
$DB->set_field('user', 'picture', $newpicture, array('id' => $user->id));
$DB->set_field('user', 'picture', $newpicture, ['id' => $user->id]);
return true;
} else {
return false;
@@ -669,66 +717,112 @@ class core_user {
// Array of user fields properties and expected parameters.
// Every new field on the user table should be added here otherwise it won't be validated.
$fields = array();
$fields['id'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
$fields['auth'] = array('type' => PARAM_AUTH, 'null' => NULL_NOT_ALLOWED);
$fields['confirmed'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
$fields['policyagreed'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
$fields['deleted'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
$fields['suspended'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
$fields['mnethostid'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
$fields['username'] = array('type' => PARAM_USERNAME, 'null' => NULL_NOT_ALLOWED);
$fields['password'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
$fields['idnumber'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
$fields['firstname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
$fields['lastname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
$fields['surname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
$fields['email'] = array('type' => PARAM_RAW_TRIMMED, 'null' => NULL_NOT_ALLOWED);
$fields['emailstop'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0);
$fields['phone1'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
$fields['phone2'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
$fields['institution'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
$fields['department'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
$fields['address'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
$fields['city'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->defaultcity);
$fields['country'] = array('type' => PARAM_ALPHA, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->country,
'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_countries(true, true)));
$fields['lang'] = array('type' => PARAM_LANG, 'null' => NULL_NOT_ALLOWED,
'default' => (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang,
'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_translations(false)));
$fields['calendartype'] = array('type' => PARAM_PLUGIN, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->calendartype,
'choices' => array_merge(array('' => ''), \core_calendar\type_factory::get_list_of_calendar_types()));
$fields['theme'] = array('type' => PARAM_THEME, 'null' => NULL_NOT_ALLOWED,
'default' => theme_config::DEFAULT_THEME, 'choices' => array_merge(array('' => ''), get_list_of_themes()));
$fields['timezone'] = array('type' => PARAM_TIMEZONE, 'null' => NULL_NOT_ALLOWED,
'default' => core_date::get_server_timezone()); // Must not use choices here: timezones can come and go.
$fields['firstaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
$fields['lastaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
$fields['lastlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
$fields['currentlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
$fields['lastip'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
$fields['secret'] = array('type' => PARAM_ALPHANUM, 'null' => NULL_NOT_ALLOWED);
$fields['picture'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
$fields['description'] = array('type' => PARAM_RAW, 'null' => NULL_ALLOWED);
$fields['descriptionformat'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
$fields['mailformat'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
'default' => $CFG->defaultpreference_mailformat);
$fields['maildigest'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
'default' => $CFG->defaultpreference_maildigest);
$fields['maildisplay'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
'default' => $CFG->defaultpreference_maildisplay);
$fields['autosubscribe'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
'default' => $CFG->defaultpreference_autosubscribe);
$fields['trackforums'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
'default' => $CFG->defaultpreference_trackforums);
$fields['timecreated'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
$fields['timemodified'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
$fields['trustbitmask'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
$fields['imagealt'] = array('type' => PARAM_TEXT, 'null' => NULL_ALLOWED);
$fields['lastnamephonetic'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
$fields['firstnamephonetic'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
$fields['middlename'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
$fields['alternatename'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
$fields = [];
$fields['id'] = ['type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED];
$fields['auth'] = ['type' => PARAM_AUTH, 'null' => NULL_NOT_ALLOWED];
$fields['confirmed'] = ['type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED];
$fields['policyagreed'] = ['type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED];
$fields['deleted'] = ['type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED];
$fields['suspended'] = ['type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED];
$fields['mnethostid'] = ['type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED];
$fields['username'] = ['type' => PARAM_USERNAME, 'null' => NULL_NOT_ALLOWED];
$fields['password'] = ['type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED];
$fields['idnumber'] = ['type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED];
$fields['firstname'] = ['type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED];
$fields['lastname'] = ['type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED];
$fields['surname'] = ['type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED];
$fields['email'] = ['type' => PARAM_RAW_TRIMMED, 'null' => NULL_NOT_ALLOWED];
$fields['emailstop'] = ['type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0];
$fields['phone1'] = ['type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED];
$fields['phone2'] = ['type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED];
$fields['institution'] = ['type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED];
$fields['department'] = ['type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED];
$fields['address'] = ['type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED];
$fields['city'] = ['type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->defaultcity];
$fields['country'] = [
'type' => PARAM_ALPHA,
'null' => NULL_NOT_ALLOWED,
'default' => $CFG->country,
'choices' => array_merge(
['' => ''],
get_string_manager()->get_list_of_countries(true, true)
),
];
$fields['lang'] = [
'type' => PARAM_LANG,
'null' => NULL_NOT_ALLOWED,
'default' => (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang,
'choices' => array_merge(
['' => ''],
get_string_manager()->get_list_of_translations(false)
),
];
$fields['calendartype'] = [
'type' => PARAM_PLUGIN,
'null' => NULL_NOT_ALLOWED,
'default' => $CFG->calendartype,
'choices' => array_merge(
['' => ''],
\core_calendar\type_factory::get_list_of_calendar_types()
),
];
$fields['theme'] = [
'type' => PARAM_THEME,
'null' => NULL_NOT_ALLOWED,
'default' => theme_config::DEFAULT_THEME,
'choices' => array_merge(
['' => ''],
get_list_of_themes()
),
];
$fields['timezone'] = [
// Must not use choices here: timezones can come and go.
'type' => PARAM_TIMEZONE,
'null' => NULL_NOT_ALLOWED,
'default' => core_date::get_server_timezone(),
];
$fields['firstaccess'] = ['type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED];
$fields['lastaccess'] = ['type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED];
$fields['lastlogin'] = ['type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED];
$fields['currentlogin'] = ['type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED];
$fields['lastip'] = ['type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED];
$fields['secret'] = ['type' => PARAM_ALPHANUM, 'null' => NULL_NOT_ALLOWED];
$fields['picture'] = ['type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED];
$fields['description'] = ['type' => PARAM_RAW, 'null' => NULL_ALLOWED];
$fields['descriptionformat'] = ['type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED];
$fields['mailformat'] = [
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED,
'default' => $CFG->defaultpreference_mailformat,
];
$fields['maildigest'] = [
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED,
'default' => $CFG->defaultpreference_maildigest,
];
$fields['maildisplay'] = [
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED,
'default' => $CFG->defaultpreference_maildisplay,
];
$fields['autosubscribe'] = [
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED,
'default' => $CFG->defaultpreference_autosubscribe,
];
$fields['trackforums'] = [
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED,
'default' => $CFG->defaultpreference_trackforums,
];
$fields['timecreated'] = ['type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED];
$fields['timemodified'] = ['type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED];
$fields['trustbitmask'] = ['type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED];
$fields['imagealt'] = ['type' => PARAM_TEXT, 'null' => NULL_ALLOWED];
$fields['lastnamephonetic'] = ['type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED];
$fields['firstnamephonetic'] = ['type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED];
$fields['middlename'] = ['type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED];
$fields['alternatename'] = ['type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED];
self::$propertiescache = $fields;
}
@@ -770,8 +864,10 @@ class core_user {
validate_param($value, self::$propertiescache[$property]['type'], self::$propertiescache[$property]['null']);
}
// Check that the value is part of a list of allowed values.
if (!empty(self::$propertiescache[$property]['choices']) &&
!isset(self::$propertiescache[$property]['choices'][$value])) {
if (
!empty(self::$propertiescache[$property]['choices']) &&
!isset(self::$propertiescache[$property]['choices'][$value])
) {
throw new invalid_parameter_exception($value);
}
} catch (invalid_parameter_exception $e) {
@@ -806,7 +902,7 @@ class core_user {
foreach ($user as $field => $value) {
// Get the property parameter type and do the cleaning.
try {
$user->$field = core_user::clean_field($value, $field);
$user->$field = self::clean_field($value, $field);
} catch (coding_exception $e) {
debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
}
@@ -828,7 +924,7 @@ class core_user {
}
try {
$type = core_user::get_property_type($field);
$type = self::get_property_type($field);
if (isset(self::$propertiescache[$field]['choices'])) {
if (!array_key_exists($data, self::$propertiescache[$field]['choices'])) {
@@ -900,9 +996,10 @@ class core_user {
self::fill_properties_cache();
if (!array_key_exists($property, self::$propertiescache) && !array_key_exists('choices',
self::$propertiescache[$property])) {
if (
!array_key_exists($property, self::$propertiescache) &&
!array_key_exists('choices', self::$propertiescache[$property])
) {
throw new coding_exception('Invalid property requested, or the property does not has a list of choices.');
}
@@ -939,11 +1036,12 @@ class core_user {
* 'choices' => array(1, 2..) // An array of accepted values of the user field - optional
* 'default' => $CFG->setting // An default value for the field - optional
* 'isregex' => false/true // Whether the name of the preference is a regular expression (default false).
* 'permissioncallback' => callable // Function accepting arguments ($user, $preferencename) that checks if current user
* 'permissioncallback' => callable // Function accepting arguments ($user, $preferencename) that checks if current
* // user
* // is allowed to modify this preference for given user.
* // If not specified core_user::default_preference_permission_check() will be assumed.
* 'cleancallback' => callable // Custom callback for cleaning value if something more difficult than just type/choices is needed
* // accepts arguments ($value, $preferencename)
* // If not specified \core\user::default_preference_permission_check() will be assumed.
* 'cleancallback' => callable // Custom callback for cleaning value if something more difficult than just type/choices
* // is needed accepts arguments ($value, $preferencename)
* )
* )
*
@@ -958,32 +1056,44 @@ class core_user {
// Array of user preferences and expected types/values.
// Every preference that can be updated directly by user should be added here.
$preferences = array();
$preferences['auth_forcepasswordchange'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'choices' => array(0, 1),
'permissioncallback' => function($user, $preferencename) {
$preferences = [];
$preferences['auth_forcepasswordchange'] = [
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED,
'choices' => [0, 1],
'permissioncallback' => function ($user, $preferencename) {
global $USER;
$systemcontext = context_system::instance();
return ($USER->id != $user->id && (has_capability('moodle/user:update', $systemcontext) ||
($user->timecreated > time() - 10 && has_capability('moodle/user:create', $systemcontext))));
});
$preferences['forum_markasreadonnotification'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
'choices' => array(0, 1));
$preferences['htmleditor'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED,
'cleancallback' => function($value, $preferencename) {
if (empty($value) || !array_key_exists($value, core_component::get_plugin_list('editor'))) {
},
];
$preferences['forum_markasreadonnotification'] = [
'type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
'choices' => [0, 1],
];
$preferences['htmleditor'] = [
'type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED,
'cleancallback' => function ($value, $preferencename) {
if (empty($value) || !array_key_exists($value, component::get_plugin_list('editor'))) {
return null;
}
return $value;
});
$preferences['badgeprivacysetting'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
'choices' => array(0, 1), 'permissioncallback' => function($user, $preferencename) {
},
];
$preferences['badgeprivacysetting'] = [
'type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
'choices' => [0, 1], 'permissioncallback' => function ($user, $preferencename) {
global $CFG;
return !empty($CFG->enablebadges) && self::is_current_user($user);
});
$preferences['blogpagesize'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 10,
'permissioncallback' => function($user, $preferencename) {
},
];
$preferences['blogpagesize'] = [
'type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 10,
'permissioncallback' => function ($user, $preferencename) {
return self::is_current_user($user) && has_capability('moodle/blog:view', context_system::instance());
});
},
];
$preferences['filemanager_recentviewmode'] = [
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED,
@@ -1058,7 +1168,7 @@ class core_user {
global $CFG;
return self::is_current_user($user) &&
(!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_USER));
}
},
];
// Core components that may want to define their preferences.
@@ -1091,7 +1201,7 @@ class core_user {
* @param string $preferencename
* @return array
*/
protected static function get_preference_definition($preferencename) {
public static function get_preference_definition($preferencename) {
self::fill_preferences_cache();
foreach (self::$preferencescache as $key => $preference) {
@@ -1127,13 +1237,13 @@ class core_user {
// Editing own profile.
$systemcontext = context_system::instance();
return has_capability('moodle/user:editownprofile', $systemcontext);
} else {
} else {
// Teachers, parents, etc.
$personalcontext = context_user::instance($user->id);
if (!has_capability('moodle/user:editprofile', $personalcontext)) {
return false;
}
if (is_siteadmin($user->id) and !is_siteadmin($USER)) {
if (is_siteadmin($user->id) && !is_siteadmin($USER)) {
// Only admins may edit other admins.
return false;
}
@@ -1361,7 +1471,7 @@ class core_user {
}
}
$requirednames = array();
$requirednames = [];
// With each name, see if it is in the display name template, and add it to the required names array if it is.
foreach ($allnames as $allname) {
if (strpos($template, $allname) !== false) {
@@ -1386,7 +1496,7 @@ class core_user {
// Tidy up any misc. characters (Not perfect, but gets most characters).
// Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or
// katakana and parenthesis.
$patterns = array();
$patterns = [];
// This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been
// filled in by a user.
// The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:).
@@ -1412,9 +1522,9 @@ class core_user {
*
* @param stdClass $user the person to get details of.
* @param context|null $context The context will be used to determine the visibility of the user's profile url.
* @return moodle_url Profile url of the user
* @return url Profile url of the user
*/
public static function get_profile_url(stdClass $user, context $context = null): moodle_url {
public static function get_profile_url(stdClass $user, ?context $context = null): url {
if (empty($user->id)) {
throw new coding_exception('User id is required when displaying profile url.');
}
@@ -1429,9 +1539,9 @@ class core_user {
// If courseid is not set or is set to site id, then return profile page, otherwise return view page.
if (!isset($params['courseid']) || $params['courseid'] == SITEID) {
return new moodle_url('/user/profile.php', $params);
return new url('/user/profile.php', $params);
} else {
return new moodle_url('/user/view.php', $params);
return new url('/user/view.php', $params);
}
}
@@ -1606,3 +1716,8 @@ class core_user {
return $namefields;
}
}
// Alias this class to the old name.
// This file will be autoloaded by the legacyclasses autoload system.
// In future all uses of this class will be corrected and the legacy references will be removed.
class_alias(user::class, \core_user::class);
+7
View File
@@ -618,4 +618,11 @@ $definitions = array(
'simpledata' => true,
'staticacceleration' => true,
],
'routes' => [
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'simpledata' => true,
'canuselocalstore' => true,
],
);
+4
View File
@@ -106,4 +106,8 @@ $callbacks = [
'hook' => \core\hook\task\after_failed_task_max_delay::class,
'callback' => core\task\failed_task_callbacks::class . '::send_failed_task_max_delay_message',
],
[
'hook' => \core\hook\di_configuration::class,
'callback' => [\core\router\hook_callbacks::class, 'provide_di_configuration'],
],
];
+8 -1
View File
@@ -15,6 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
use core\exception\response_aware_exception;
use core\router\response\not_found_response;
/**
* This library contains all the Data Manipulation Language (DML) functions
@@ -176,7 +178,7 @@ class dml_multiple_records_exception extends dml_exception {
* @copyright 2008 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dml_missing_record_exception extends dml_exception {
class dml_missing_record_exception extends dml_exception implements response_aware_exception {
/** @var string A table's name.*/
public $tablename;
/** @var string An SQL query.*/
@@ -218,6 +220,11 @@ class dml_missing_record_exception extends dml_exception {
$errorinfo = $sql."\n[".var_export($params, true).']';
parent::__construct($errcode, $tablename, $errorinfo);
}
#[\Override]
public function get_response_classname(): string {
return not_found_response::class;
}
}
/**
+12 -14
View File
@@ -24,20 +24,18 @@
defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/phpspreadsheet/vendor/autoload.php");
use \PhpOffice\PhpSpreadsheet\Spreadsheet;
use \PhpOffice\PhpSpreadsheet\IOFactory;
use \PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use \PhpOffice\PhpSpreadsheet\Cell\DataType;
use \PhpOffice\PhpSpreadsheet\Shared\Date;
use \PhpOffice\PhpSpreadsheet\Style\Alignment;
use \PhpOffice\PhpSpreadsheet\Style\Border;
use \PhpOffice\PhpSpreadsheet\Style\Fill;
use \PhpOffice\PhpSpreadsheet\Style\Font;
use \PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use \PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
use \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Font;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/**
* Define and operate over one Moodle Workbook.
+31
View File
@@ -0,0 +1,31 @@
Copyright (c) 2013 by Nikita Popov.
Some rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+313
View File
@@ -0,0 +1,313 @@
FastRoute - Fast request router for PHP
=======================================
This library provides a fast implementation of a regular expression based router. [Blog post explaining how the
implementation works and why it is fast.][blog_post]
Install
-------
To install with composer:
```sh
composer require nikic/fast-route
```
Requires PHP 5.4 or newer.
Usage
-----
Here's a basic usage example:
```php
<?php
require '/path/to/vendor/autoload.php';
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/users', 'get_all_users_handler');
// {id} must be a number (\d+)
$r->addRoute('GET', '/user/{id:\d+}', 'get_user_handler');
// The /{title} suffix is optional
$r->addRoute('GET', '/articles/{id:\d+}[/{title}]', 'get_article_handler');
});
// Fetch method and URI from somewhere
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
// Strip query string (?foo=bar) and decode URI
if (false !== $pos = strpos($uri, '?')) {
$uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);
$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
// ... 404 Not Found
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$allowedMethods = $routeInfo[1];
// ... 405 Method Not Allowed
break;
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
// ... call $handler with $vars
break;
}
```
### Defining routes
The routes are defined by calling the `FastRoute\simpleDispatcher()` function, which accepts
a callable taking a `FastRoute\RouteCollector` instance. The routes are added by calling
`addRoute()` on the collector instance:
```php
$r->addRoute($method, $routePattern, $handler);
```
The `$method` is an uppercase HTTP method string for which a certain route should match. It
is possible to specify multiple valid methods using an array:
```php
// These two calls
$r->addRoute('GET', '/test', 'handler');
$r->addRoute('POST', '/test', 'handler');
// Are equivalent to this one call
$r->addRoute(['GET', 'POST'], '/test', 'handler');
```
By default the `$routePattern` uses a syntax where `{foo}` specifies a placeholder with name `foo`
and matching the regex `[^/]+`. To adjust the pattern the placeholder matches, you can specify
a custom pattern by writing `{bar:[0-9]+}`. Some examples:
```php
// Matches /user/42, but not /user/xyz
$r->addRoute('GET', '/user/{id:\d+}', 'handler');
// Matches /user/foobar, but not /user/foo/bar
$r->addRoute('GET', '/user/{name}', 'handler');
// Matches /user/foo/bar as well
$r->addRoute('GET', '/user/{name:.+}', 'handler');
```
Custom patterns for route placeholders cannot use capturing groups. For example `{lang:(en|de)}`
is not a valid placeholder, because `()` is a capturing group. Instead you can use either
`{lang:en|de}` or `{lang:(?:en|de)}`.
Furthermore parts of the route enclosed in `[...]` are considered optional, so that `/foo[bar]`
will match both `/foo` and `/foobar`. Optional parts are only supported in a trailing position,
not in the middle of a route.
```php
// This route
$r->addRoute('GET', '/user/{id:\d+}[/{name}]', 'handler');
// Is equivalent to these two routes
$r->addRoute('GET', '/user/{id:\d+}', 'handler');
$r->addRoute('GET', '/user/{id:\d+}/{name}', 'handler');
// Multiple nested optional parts are possible as well
$r->addRoute('GET', '/user[/{id:\d+}[/{name}]]', 'handler');
// This route is NOT valid, because optional parts can only occur at the end
$r->addRoute('GET', '/user[/{id:\d+}]/{name}', 'handler');
```
The `$handler` parameter does not necessarily have to be a callback, it could also be a controller
class name or any other kind of data you wish to associate with the route. FastRoute only tells you
which handler corresponds to your URI, how you interpret it is up to you.
#### Shorcut methods for common request methods
For the `GET`, `POST`, `PUT`, `PATCH`, `DELETE` and `HEAD` request methods shortcut methods are available. For example:
```php
$r->get('/get-route', 'get_handler');
$r->post('/post-route', 'post_handler');
```
Is equivalent to:
```php
$r->addRoute('GET', '/get-route', 'get_handler');
$r->addRoute('POST', '/post-route', 'post_handler');
```
#### Route Groups
Additionally, you can specify routes inside of a group. All routes defined inside a group will have a common prefix.
For example, defining your routes as:
```php
$r->addGroup('/admin', function (RouteCollector $r) {
$r->addRoute('GET', '/do-something', 'handler');
$r->addRoute('GET', '/do-another-thing', 'handler');
$r->addRoute('GET', '/do-something-else', 'handler');
});
```
Will have the same result as:
```php
$r->addRoute('GET', '/admin/do-something', 'handler');
$r->addRoute('GET', '/admin/do-another-thing', 'handler');
$r->addRoute('GET', '/admin/do-something-else', 'handler');
```
Nested groups are also supported, in which case the prefixes of all the nested groups are combined.
### Caching
The reason `simpleDispatcher` accepts a callback for defining the routes is to allow seamless
caching. By using `cachedDispatcher` instead of `simpleDispatcher` you can cache the generated
routing data and construct the dispatcher from the cached information:
```php
<?php
$dispatcher = FastRoute\cachedDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
$r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');
$r->addRoute('GET', '/user/{name}', 'handler2');
}, [
'cacheFile' => __DIR__ . '/route.cache', /* required */
'cacheDisabled' => IS_DEBUG_ENABLED, /* optional, enabled by default */
]);
```
The second parameter to the function is an options array, which can be used to specify the cache
file location, among other things.
### Dispatching a URI
A URI is dispatched by calling the `dispatch()` method of the created dispatcher. This method
accepts the HTTP method and a URI. Getting those two bits of information (and normalizing them
appropriately) is your job - this library is not bound to the PHP web SAPIs.
The `dispatch()` method returns an array whose first element contains a status code. It is one
of `Dispatcher::NOT_FOUND`, `Dispatcher::METHOD_NOT_ALLOWED` and `Dispatcher::FOUND`. For the
method not allowed status the second array element contains a list of HTTP methods allowed for
the supplied URI. For example:
[FastRoute\Dispatcher::METHOD_NOT_ALLOWED, ['GET', 'POST']]
> **NOTE:** The HTTP specification requires that a `405 Method Not Allowed` response include the
`Allow:` header to detail available methods for the requested resource. Applications using FastRoute
should use the second array element to add this header when relaying a 405 response.
For the found status the second array element is the handler that was associated with the route
and the third array element is a dictionary of placeholder names to their values. For example:
/* Routing against GET /user/nikic/42 */
[FastRoute\Dispatcher::FOUND, 'handler0', ['name' => 'nikic', 'id' => '42']]
### Overriding the route parser and dispatcher
The routing process makes use of three components: A route parser, a data generator and a
dispatcher. The three components adhere to the following interfaces:
```php
<?php
namespace FastRoute;
interface RouteParser {
public function parse($route);
}
interface DataGenerator {
public function addRoute($httpMethod, $routeData, $handler);
public function getData();
}
interface Dispatcher {
const NOT_FOUND = 0, FOUND = 1, METHOD_NOT_ALLOWED = 2;
public function dispatch($httpMethod, $uri);
}
```
The route parser takes a route pattern string and converts it into an array of route infos, where
each route info is again an array of it's parts. The structure is best understood using an example:
/* The route /user/{id:\d+}[/{name}] converts to the following array: */
[
[
'/user/',
['id', '\d+'],
],
[
'/user/',
['id', '\d+'],
'/',
['name', '[^/]+'],
],
]
This array can then be passed to the `addRoute()` method of a data generator. After all routes have
been added the `getData()` of the generator is invoked, which returns all the routing data required
by the dispatcher. The format of this data is not further specified - it is tightly coupled to
the corresponding dispatcher.
The dispatcher accepts the routing data via a constructor and provides a `dispatch()` method, which
you're already familiar with.
The route parser can be overwritten individually (to make use of some different pattern syntax),
however the data generator and dispatcher should always be changed as a pair, as the output from
the former is tightly coupled to the input of the latter. The reason the generator and the
dispatcher are separate is that only the latter is needed when using caching (as the output of
the former is what is being cached.)
When using the `simpleDispatcher` / `cachedDispatcher` functions from above the override happens
through the options array:
```php
<?php
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
/* ... */
}, [
'routeParser' => 'FastRoute\\RouteParser\\Std',
'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased',
'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased',
]);
```
The above options array corresponds to the defaults. By replacing `GroupCountBased` by
`GroupPosBased` you could switch to a different dispatching strategy.
### A Note on HEAD Requests
The HTTP spec requires servers to [support both GET and HEAD methods][2616-511]:
> The methods GET and HEAD MUST be supported by all general-purpose servers
To avoid forcing users to manually register HEAD routes for each resource we fallback to matching an
available GET route for a given resource. The PHP web SAPI transparently removes the entity body
from HEAD responses so this behavior has no effect on the vast majority of users.
However, implementers using FastRoute outside the web SAPI environment (e.g. a custom server) MUST
NOT send entity bodies generated in response to HEAD requests. If you are a non-SAPI user this is
*your responsibility*; FastRoute has no purview to prevent you from breaking HTTP in such cases.
Finally, note that applications MAY always specify their own HEAD method route for a given
resource to bypass this behavior entirely.
### Credits
This library is based on a router that [Levi Morrison][levi] implemented for the Aerys server.
A large number of tests, as well as HTTP compliance considerations, were provided by [Daniel Lowrey][rdlowrey].
[2616-511]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.1 "RFC 2616 Section 5.1.1"
[blog_post]: http://nikic.github.io/2014/02/18/Fast-request-routing-using-regular-expressions.html
[levi]: https://github.com/morrisonlevi
[rdlowrey]: https://github.com/rdlowrey
+24
View File
@@ -0,0 +1,24 @@
{
"name": "nikic/fast-route",
"description": "Fast request router for PHP",
"keywords": ["routing", "router"],
"license": "BSD-3-Clause",
"authors": [
{
"name": "Nikita Popov",
"email": "[email protected]"
}
],
"autoload": {
"psr-4": {
"FastRoute\\": "src/"
},
"files": ["src/functions.php"]
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35|~5.7"
}
}
@@ -0,0 +1,7 @@
<?php
namespace FastRoute;
class BadRouteException extends \LogicException
{
}
@@ -0,0 +1,26 @@
<?php
namespace FastRoute;
interface DataGenerator
{
/**
* Adds a route to the data generator. The route data uses the
* same format that is returned by RouterParser::parser().
*
* The handler doesn't necessarily need to be a callable, it
* can be arbitrary data that will be returned when the route
* matches.
*
* @param string $httpMethod
* @param array $routeData
* @param mixed $handler
*/
public function addRoute($httpMethod, $routeData, $handler);
/**
* Returns dispatcher data in some unspecified format, which
* depends on the used method of dispatch.
*/
public function getData();
}
@@ -0,0 +1,31 @@
<?php
namespace FastRoute\DataGenerator;
class CharCountBased extends RegexBasedAbstract
{
protected function getApproxChunkSize()
{
return 30;
}
protected function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$suffixLen = 0;
$suffix = '';
$count = count($regexToRoutesMap);
foreach ($regexToRoutesMap as $regex => $route) {
$suffixLen++;
$suffix .= "\t";
$regexes[] = '(?:' . $regex . '/(\t{' . $suffixLen . '})\t{' . ($count - $suffixLen) . '})';
$routeMap[$suffix] = [$route->handler, $route->variables];
}
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'suffix' => '/' . $suffix, 'routeMap' => $routeMap];
}
}
@@ -0,0 +1,30 @@
<?php
namespace FastRoute\DataGenerator;
class GroupCountBased extends RegexBasedAbstract
{
protected function getApproxChunkSize()
{
return 10;
}
protected function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$numGroups = 0;
foreach ($regexToRoutesMap as $regex => $route) {
$numVariables = count($route->variables);
$numGroups = max($numGroups, $numVariables);
$regexes[] = $regex . str_repeat('()', $numGroups - $numVariables);
$routeMap[$numGroups + 1] = [$route->handler, $route->variables];
++$numGroups;
}
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'routeMap' => $routeMap];
}
}
@@ -0,0 +1,27 @@
<?php
namespace FastRoute\DataGenerator;
class GroupPosBased extends RegexBasedAbstract
{
protected function getApproxChunkSize()
{
return 10;
}
protected function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$offset = 1;
foreach ($regexToRoutesMap as $regex => $route) {
$regexes[] = $regex;
$routeMap[$offset] = [$route->handler, $route->variables];
$offset += count($route->variables);
}
$regex = '~^(?:' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'routeMap' => $routeMap];
}
}
@@ -0,0 +1,27 @@
<?php
namespace FastRoute\DataGenerator;
class MarkBased extends RegexBasedAbstract
{
protected function getApproxChunkSize()
{
return 30;
}
protected function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$markName = 'a';
foreach ($regexToRoutesMap as $regex => $route) {
$regexes[] = $regex . '(*MARK:' . $markName . ')';
$routeMap[$markName] = [$route->handler, $route->variables];
++$markName;
}
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'routeMap' => $routeMap];
}
}
@@ -0,0 +1,186 @@
<?php
namespace FastRoute\DataGenerator;
use FastRoute\BadRouteException;
use FastRoute\DataGenerator;
use FastRoute\Route;
abstract class RegexBasedAbstract implements DataGenerator
{
/** @var mixed[][] */
protected $staticRoutes = [];
/** @var Route[][] */
protected $methodToRegexToRoutesMap = [];
/**
* @return int
*/
abstract protected function getApproxChunkSize();
/**
* @return mixed[]
*/
abstract protected function processChunk($regexToRoutesMap);
public function addRoute($httpMethod, $routeData, $handler)
{
if ($this->isStaticRoute($routeData)) {
$this->addStaticRoute($httpMethod, $routeData, $handler);
} else {
$this->addVariableRoute($httpMethod, $routeData, $handler);
}
}
/**
* @return mixed[]
*/
public function getData()
{
if (empty($this->methodToRegexToRoutesMap)) {
return [$this->staticRoutes, []];
}
return [$this->staticRoutes, $this->generateVariableRouteData()];
}
/**
* @return mixed[]
*/
private function generateVariableRouteData()
{
$data = [];
foreach ($this->methodToRegexToRoutesMap as $method => $regexToRoutesMap) {
$chunkSize = $this->computeChunkSize(count($regexToRoutesMap));
$chunks = array_chunk($regexToRoutesMap, $chunkSize, true);
$data[$method] = array_map([$this, 'processChunk'], $chunks);
}
return $data;
}
/**
* @param int
* @return int
*/
private function computeChunkSize($count)
{
$numParts = max(1, round($count / $this->getApproxChunkSize()));
return (int) ceil($count / $numParts);
}
/**
* @param mixed[]
* @return bool
*/
private function isStaticRoute($routeData)
{
return count($routeData) === 1 && is_string($routeData[0]);
}
private function addStaticRoute($httpMethod, $routeData, $handler)
{
$routeStr = $routeData[0];
if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
throw new BadRouteException(sprintf(
'Cannot register two routes matching "%s" for method "%s"',
$routeStr, $httpMethod
));
}
if (isset($this->methodToRegexToRoutesMap[$httpMethod])) {
foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) {
if ($route->matches($routeStr)) {
throw new BadRouteException(sprintf(
'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"',
$routeStr, $route->regex, $httpMethod
));
}
}
}
$this->staticRoutes[$httpMethod][$routeStr] = $handler;
}
private function addVariableRoute($httpMethod, $routeData, $handler)
{
list($regex, $variables) = $this->buildRegexForRoute($routeData);
if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
throw new BadRouteException(sprintf(
'Cannot register two routes matching "%s" for method "%s"',
$regex, $httpMethod
));
}
$this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(
$httpMethod, $handler, $regex, $variables
);
}
/**
* @param mixed[]
* @return mixed[]
*/
private function buildRegexForRoute($routeData)
{
$regex = '';
$variables = [];
foreach ($routeData as $part) {
if (is_string($part)) {
$regex .= preg_quote($part, '~');
continue;
}
list($varName, $regexPart) = $part;
if (isset($variables[$varName])) {
throw new BadRouteException(sprintf(
'Cannot use the same placeholder "%s" twice', $varName
));
}
if ($this->regexHasCapturingGroups($regexPart)) {
throw new BadRouteException(sprintf(
'Regex "%s" for parameter "%s" contains a capturing group',
$regexPart, $varName
));
}
$variables[$varName] = $varName;
$regex .= '(' . $regexPart . ')';
}
return [$regex, $variables];
}
/**
* @param string
* @return bool
*/
private function regexHasCapturingGroups($regex)
{
if (false === strpos($regex, '(')) {
// Needs to have at least a ( to contain a capturing group
return false;
}
// Semi-accurate detection for capturing groups
return (bool) preg_match(
'~
(?:
\(\?\(
| \[ [^\]\\\\]* (?: \\\\ . [^\]\\\\]* )* \]
| \\\\ .
) (*SKIP)(*FAIL) |
\(
(?!
\? (?! <(?![!=]) | P< | \' )
| \*
)
~x',
$regex
);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace FastRoute;
interface Dispatcher
{
const NOT_FOUND = 0;
const FOUND = 1;
const METHOD_NOT_ALLOWED = 2;
/**
* Dispatches against the provided HTTP method verb and URI.
*
* Returns array with one of the following formats:
*
* [self::NOT_FOUND]
* [self::METHOD_NOT_ALLOWED, ['GET', 'OTHER_ALLOWED_METHODS']]
* [self::FOUND, $handler, ['varName' => 'value', ...]]
*
* @param string $httpMethod
* @param string $uri
*
* @return array
*/
public function dispatch($httpMethod, $uri);
}

Some files were not shown because too many files have changed in this diff Show More