Merge branch 'MDL-81031-main' of https://github.com/andrewnicols/moodle
This commit is contained in:
@@ -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
|
||||
@@ -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"));
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.';
|
||||
|
||||
Vendored
+10
@@ -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
Vendored
+1
-1
@@ -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
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -618,4 +618,11 @@ $definitions = array(
|
||||
'simpledata' => true,
|
||||
'staticacceleration' => true,
|
||||
],
|
||||
|
||||
'routes' => [
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'simplekeys' => true,
|
||||
'simpledata' => true,
|
||||
'canuselocalstore' => true,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user