PageRenderTime 52ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/externallib.php

https://github.com/dongsheng/moodle
PHP | 1596 lines | 882 code | 160 blank | 554 comment | 177 complexity | 3bb4bb143ca1429b920aac11aa8a9aa1 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, GPL-3.0, Apache-2.0, LGPL-2.1

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Support for external API
  18. *
  19. * @package core_webservice
  20. * @copyright 2009 Petr Skodak
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. defined('MOODLE_INTERNAL') || die();
  24. /**
  25. * Exception indicating user is not allowed to use external function in the current context.
  26. *
  27. * @package core_webservice
  28. * @copyright 2009 Petr Skodak
  29. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30. * @since Moodle 2.0
  31. */
  32. class restricted_context_exception extends moodle_exception {
  33. /**
  34. * Constructor
  35. *
  36. * @since Moodle 2.0
  37. */
  38. function __construct() {
  39. parent::__construct('restrictedcontextexception', 'error');
  40. }
  41. }
  42. /**
  43. * Base class for external api methods.
  44. *
  45. * @package core_webservice
  46. * @copyright 2009 Petr Skodak
  47. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  48. * @since Moodle 2.0
  49. */
  50. class external_api {
  51. /** @var stdClass context where the function calls will be restricted */
  52. private static $contextrestriction;
  53. /**
  54. * Returns detailed function information
  55. *
  56. * @param string|object $function name of external function or record from external_function
  57. * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
  58. * MUST_EXIST means throw exception if no record or multiple records found
  59. * @return stdClass description or false if not found or exception thrown
  60. * @since Moodle 2.0
  61. */
  62. public static function external_function_info($function, $strictness=MUST_EXIST) {
  63. global $DB, $CFG;
  64. if (!is_object($function)) {
  65. if (!$function = $DB->get_record('external_functions', array('name' => $function), '*', $strictness)) {
  66. return false;
  67. }
  68. }
  69. // First try class autoloading.
  70. if (!class_exists($function->classname)) {
  71. // Fallback to explicit include of externallib.php.
  72. if (empty($function->classpath)) {
  73. $function->classpath = core_component::get_component_directory($function->component).'/externallib.php';
  74. } else {
  75. $function->classpath = $CFG->dirroot.'/'.$function->classpath;
  76. }
  77. if (!file_exists($function->classpath)) {
  78. throw new coding_exception('Cannot find file ' . $function->classpath .
  79. ' with external function implementation');
  80. }
  81. require_once($function->classpath);
  82. if (!class_exists($function->classname)) {
  83. throw new coding_exception('Cannot find external class ' . $function->classname);
  84. }
  85. }
  86. $function->ajax_method = $function->methodname.'_is_allowed_from_ajax';
  87. $function->parameters_method = $function->methodname.'_parameters';
  88. $function->returns_method = $function->methodname.'_returns';
  89. $function->deprecated_method = $function->methodname.'_is_deprecated';
  90. // Make sure the implementaion class is ok.
  91. if (!method_exists($function->classname, $function->methodname)) {
  92. throw new coding_exception('Missing implementation method ' .
  93. $function->classname . '::' . $function->methodname);
  94. }
  95. if (!method_exists($function->classname, $function->parameters_method)) {
  96. throw new coding_exception('Missing parameters description method ' .
  97. $function->classname . '::' . $function->parameters_method);
  98. }
  99. if (!method_exists($function->classname, $function->returns_method)) {
  100. throw new coding_exception('Missing returned values description method ' .
  101. $function->classname . '::' . $function->returns_method);
  102. }
  103. if (method_exists($function->classname, $function->deprecated_method)) {
  104. if (call_user_func(array($function->classname, $function->deprecated_method)) === true) {
  105. $function->deprecated = true;
  106. }
  107. }
  108. $function->allowed_from_ajax = false;
  109. // Fetch the parameters description.
  110. $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method));
  111. if (!($function->parameters_desc instanceof external_function_parameters)) {
  112. throw new coding_exception($function->classname . '::' . $function->parameters_method .
  113. ' did not return a valid external_function_parameters object.');
  114. }
  115. // Fetch the return values description.
  116. $function->returns_desc = call_user_func(array($function->classname, $function->returns_method));
  117. // Null means void result or result is ignored.
  118. if (!is_null($function->returns_desc) and !($function->returns_desc instanceof external_description)) {
  119. throw new coding_exception($function->classname . '::' . $function->returns_method .
  120. ' did not return a valid external_description object');
  121. }
  122. // Now get the function description.
  123. // TODO MDL-31115 use localised lang pack descriptions, it would be nice to have
  124. // easy to understand descriptions in admin UI,
  125. // on the other hand this is still a bit in a flux and we need to find some new naming
  126. // conventions for these descriptions in lang packs.
  127. $function->description = null;
  128. $servicesfile = core_component::get_component_directory($function->component).'/db/services.php';
  129. if (file_exists($servicesfile)) {
  130. $functions = null;
  131. include($servicesfile);
  132. if (isset($functions[$function->name]['description'])) {
  133. $function->description = $functions[$function->name]['description'];
  134. }
  135. if (isset($functions[$function->name]['testclientpath'])) {
  136. $function->testclientpath = $functions[$function->name]['testclientpath'];
  137. }
  138. if (isset($functions[$function->name]['type'])) {
  139. $function->type = $functions[$function->name]['type'];
  140. }
  141. if (isset($functions[$function->name]['ajax'])) {
  142. $function->allowed_from_ajax = $functions[$function->name]['ajax'];
  143. } else if (method_exists($function->classname, $function->ajax_method)) {
  144. if (call_user_func(array($function->classname, $function->ajax_method)) === true) {
  145. debugging('External function ' . $function->ajax_method . '() function is deprecated.' .
  146. 'Set ajax=>true in db/service.php instead.', DEBUG_DEVELOPER);
  147. $function->allowed_from_ajax = true;
  148. }
  149. }
  150. if (isset($functions[$function->name]['loginrequired'])) {
  151. $function->loginrequired = $functions[$function->name]['loginrequired'];
  152. } else {
  153. $function->loginrequired = true;
  154. }
  155. if (isset($functions[$function->name]['readonlysession'])) {
  156. $function->readonlysession = $functions[$function->name]['readonlysession'];
  157. } else {
  158. $function->readonlysession = false;
  159. }
  160. }
  161. return $function;
  162. }
  163. /**
  164. * Call an external function validating all params/returns correctly.
  165. *
  166. * Note that an external function may modify the state of the current page, so this wrapper
  167. * saves and restores tha PAGE and COURSE global variables before/after calling the external function.
  168. *
  169. * @param string $function A webservice function name.
  170. * @param array $args Params array (named params)
  171. * @param boolean $ajaxonly If true, an extra check will be peformed to see if ajax is required.
  172. * @return array containing keys for error (bool), exception and data.
  173. */
  174. public static function call_external_function($function, $args, $ajaxonly=false) {
  175. global $PAGE, $COURSE, $CFG, $SITE;
  176. require_once($CFG->libdir . "/pagelib.php");
  177. $externalfunctioninfo = static::external_function_info($function);
  178. // Eventually this should shift into the various handlers and not be handled via config.
  179. $readonlysession = $externalfunctioninfo->readonlysession ?? false;
  180. if (!$readonlysession || empty($CFG->enable_read_only_sessions)) {
  181. \core\session\manager::restart_with_write_lock($readonlysession);
  182. }
  183. $currentpage = $PAGE;
  184. $currentcourse = $COURSE;
  185. $response = array();
  186. try {
  187. // Taken straight from from setup.php.
  188. if (!empty($CFG->moodlepageclass)) {
  189. if (!empty($CFG->moodlepageclassfile)) {
  190. require_once($CFG->moodlepageclassfile);
  191. }
  192. $classname = $CFG->moodlepageclass;
  193. } else {
  194. $classname = 'moodle_page';
  195. }
  196. $PAGE = new $classname();
  197. $COURSE = clone($SITE);
  198. if ($ajaxonly && !$externalfunctioninfo->allowed_from_ajax) {
  199. throw new moodle_exception('servicenotavailable', 'webservice');
  200. }
  201. // Do not allow access to write or delete webservices as a public user.
  202. if ($externalfunctioninfo->loginrequired && !WS_SERVER) {
  203. if (defined('NO_MOODLE_COOKIES') && NO_MOODLE_COOKIES && !PHPUNIT_TEST) {
  204. throw new moodle_exception('servicerequireslogin', 'webservice');
  205. }
  206. if (!isloggedin()) {
  207. throw new moodle_exception('servicerequireslogin', 'webservice');
  208. } else {
  209. require_sesskey();
  210. }
  211. }
  212. // Validate params, this also sorts the params properly, we need the correct order in the next part.
  213. $callable = array($externalfunctioninfo->classname, 'validate_parameters');
  214. $params = call_user_func($callable,
  215. $externalfunctioninfo->parameters_desc,
  216. $args);
  217. $params = array_values($params);
  218. // Allow any Moodle plugin a chance to override this call. This is a convenient spot to
  219. // make arbitrary behaviour customisations. The overriding plugin could call the 'real'
  220. // function first and then modify the results, or it could do a completely separate
  221. // thing.
  222. $callbacks = get_plugins_with_function('override_webservice_execution');
  223. $result = false;
  224. foreach ($callbacks as $plugintype => $plugins) {
  225. foreach ($plugins as $plugin => $callback) {
  226. $result = $callback($externalfunctioninfo, $params);
  227. if ($result !== false) {
  228. break 2;
  229. }
  230. }
  231. }
  232. // If the function was not overridden, call the real one.
  233. if ($result === false) {
  234. $callable = array($externalfunctioninfo->classname, $externalfunctioninfo->methodname);
  235. $result = call_user_func_array($callable, $params);
  236. }
  237. // Validate the return parameters.
  238. if ($externalfunctioninfo->returns_desc !== null) {
  239. $callable = array($externalfunctioninfo->classname, 'clean_returnvalue');
  240. $result = call_user_func($callable, $externalfunctioninfo->returns_desc, $result);
  241. }
  242. $response['error'] = false;
  243. $response['data'] = $result;
  244. } catch (Throwable $e) {
  245. $exception = get_exception_info($e);
  246. unset($exception->a);
  247. $exception->backtrace = format_backtrace($exception->backtrace, true);
  248. if (!debugging('', DEBUG_DEVELOPER)) {
  249. unset($exception->debuginfo);
  250. unset($exception->backtrace);
  251. }
  252. $response['error'] = true;
  253. $response['exception'] = $exception;
  254. // Do not process the remaining requests.
  255. }
  256. $PAGE = $currentpage;
  257. $COURSE = $currentcourse;
  258. return $response;
  259. }
  260. /**
  261. * Set context restriction for all following subsequent function calls.
  262. *
  263. * @param stdClass $context the context restriction
  264. * @since Moodle 2.0
  265. */
  266. public static function set_context_restriction($context) {
  267. self::$contextrestriction = $context;
  268. }
  269. /**
  270. * This method has to be called before every operation
  271. * that takes a longer time to finish!
  272. *
  273. * @param int $seconds max expected time the next operation needs
  274. * @since Moodle 2.0
  275. */
  276. public static function set_timeout($seconds=360) {
  277. $seconds = ($seconds < 300) ? 300 : $seconds;
  278. core_php_time_limit::raise($seconds);
  279. }
  280. /**
  281. * Validates submitted function parameters, if anything is incorrect
  282. * invalid_parameter_exception is thrown.
  283. * This is a simple recursive method which is intended to be called from
  284. * each implementation method of external API.
  285. *
  286. * @param external_description $description description of parameters
  287. * @param mixed $params the actual parameters
  288. * @return mixed params with added defaults for optional items, invalid_parameters_exception thrown if any problem found
  289. * @since Moodle 2.0
  290. */
  291. public static function validate_parameters(external_description $description, $params) {
  292. if ($description instanceof external_value) {
  293. if (is_array($params) or is_object($params)) {
  294. throw new invalid_parameter_exception('Scalar type expected, array or object received.');
  295. }
  296. if ($description->type == PARAM_BOOL) {
  297. // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-)
  298. if (is_bool($params) or $params === 0 or $params === 1 or $params === '0' or $params === '1') {
  299. return (bool)$params;
  300. }
  301. }
  302. $debuginfo = 'Invalid external api parameter: the value is "' . $params .
  303. '", the server was expecting "' . $description->type . '" type';
  304. return validate_param($params, $description->type, $description->allownull, $debuginfo);
  305. } else if ($description instanceof external_single_structure) {
  306. if (!is_array($params)) {
  307. throw new invalid_parameter_exception('Only arrays accepted. The bad value is: \''
  308. . print_r($params, true) . '\'');
  309. }
  310. $result = array();
  311. foreach ($description->keys as $key=>$subdesc) {
  312. if (!array_key_exists($key, $params)) {
  313. if ($subdesc->required == VALUE_REQUIRED) {
  314. throw new invalid_parameter_exception('Missing required key in single structure: '. $key);
  315. }
  316. if ($subdesc->required == VALUE_DEFAULT) {
  317. try {
  318. $result[$key] = static::validate_parameters($subdesc, $subdesc->default);
  319. } catch (invalid_parameter_exception $e) {
  320. //we are only interested by exceptions returned by validate_param() and validate_parameters()
  321. //(in order to build the path to the faulty attribut)
  322. throw new invalid_parameter_exception($key." => ".$e->getMessage() . ': ' .$e->debuginfo);
  323. }
  324. }
  325. } else {
  326. try {
  327. $result[$key] = static::validate_parameters($subdesc, $params[$key]);
  328. } catch (invalid_parameter_exception $e) {
  329. //we are only interested by exceptions returned by validate_param() and validate_parameters()
  330. //(in order to build the path to the faulty attribut)
  331. throw new invalid_parameter_exception($key." => ".$e->getMessage() . ': ' .$e->debuginfo);
  332. }
  333. }
  334. unset($params[$key]);
  335. }
  336. if (!empty($params)) {
  337. throw new invalid_parameter_exception('Unexpected keys (' . implode(', ', array_keys($params)) . ') detected in parameter array.');
  338. }
  339. return $result;
  340. } else if ($description instanceof external_multiple_structure) {
  341. if (!is_array($params)) {
  342. throw new invalid_parameter_exception('Only arrays accepted. The bad value is: \''
  343. . print_r($params, true) . '\'');
  344. }
  345. $result = array();
  346. foreach ($params as $param) {
  347. $result[] = static::validate_parameters($description->content, $param);
  348. }
  349. return $result;
  350. } else {
  351. throw new invalid_parameter_exception('Invalid external api description');
  352. }
  353. }
  354. /**
  355. * Clean response
  356. * If a response attribute is unknown from the description, we just ignore the attribute.
  357. * If a response attribute is incorrect, invalid_response_exception is thrown.
  358. * Note: this function is similar to validate parameters, however it is distinct because
  359. * parameters validation must be distinct from cleaning return values.
  360. *
  361. * @param external_description $description description of the return values
  362. * @param mixed $response the actual response
  363. * @return mixed response with added defaults for optional items, invalid_response_exception thrown if any problem found
  364. * @author 2010 Jerome Mouneyrac
  365. * @since Moodle 2.0
  366. */
  367. public static function clean_returnvalue(external_description $description, $response) {
  368. if ($description instanceof external_value) {
  369. if (is_array($response) or is_object($response)) {
  370. throw new invalid_response_exception('Scalar type expected, array or object received.');
  371. }
  372. if ($description->type == PARAM_BOOL) {
  373. // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-)
  374. if (is_bool($response) or $response === 0 or $response === 1 or $response === '0' or $response === '1') {
  375. return (bool)$response;
  376. }
  377. }
  378. $responsetype = gettype($response);
  379. $debuginfo = 'Invalid external api response: the value is "' . $response .
  380. '" of PHP type "' . $responsetype . '", the server was expecting "' . $description->type . '" type';
  381. try {
  382. return validate_param($response, $description->type, $description->allownull, $debuginfo);
  383. } catch (invalid_parameter_exception $e) {
  384. //proper exception name, to be recursively catched to build the path to the faulty attribut
  385. throw new invalid_response_exception($e->debuginfo);
  386. }
  387. } else if ($description instanceof external_single_structure) {
  388. if (!is_array($response) && !is_object($response)) {
  389. throw new invalid_response_exception('Only arrays/objects accepted. The bad value is: \'' .
  390. print_r($response, true) . '\'');
  391. }
  392. // Cast objects into arrays.
  393. if (is_object($response)) {
  394. $response = (array) $response;
  395. }
  396. $result = array();
  397. foreach ($description->keys as $key=>$subdesc) {
  398. if (!array_key_exists($key, $response)) {
  399. if ($subdesc->required == VALUE_REQUIRED) {
  400. throw new invalid_response_exception('Error in response - Missing following required key in a single structure: ' . $key);
  401. }
  402. if ($subdesc instanceof external_value) {
  403. if ($subdesc->required == VALUE_DEFAULT) {
  404. try {
  405. $result[$key] = static::clean_returnvalue($subdesc, $subdesc->default);
  406. } catch (invalid_response_exception $e) {
  407. //build the path to the faulty attribut
  408. throw new invalid_response_exception($key." => ".$e->getMessage() . ': ' . $e->debuginfo);
  409. }
  410. }
  411. }
  412. } else {
  413. try {
  414. $result[$key] = static::clean_returnvalue($subdesc, $response[$key]);
  415. } catch (invalid_response_exception $e) {
  416. //build the path to the faulty attribut
  417. throw new invalid_response_exception($key." => ".$e->getMessage() . ': ' . $e->debuginfo);
  418. }
  419. }
  420. unset($response[$key]);
  421. }
  422. return $result;
  423. } else if ($description instanceof external_multiple_structure) {
  424. if (!is_array($response)) {
  425. throw new invalid_response_exception('Only arrays accepted. The bad value is: \'' .
  426. print_r($response, true) . '\'');
  427. }
  428. $result = array();
  429. foreach ($response as $param) {
  430. $result[] = static::clean_returnvalue($description->content, $param);
  431. }
  432. return $result;
  433. } else {
  434. throw new invalid_response_exception('Invalid external api response description');
  435. }
  436. }
  437. /**
  438. * Makes sure user may execute functions in this context.
  439. *
  440. * @param stdClass $context
  441. * @since Moodle 2.0
  442. */
  443. public static function validate_context($context) {
  444. global $CFG, $PAGE;
  445. if (empty($context)) {
  446. throw new invalid_parameter_exception('Context does not exist');
  447. }
  448. if (empty(self::$contextrestriction)) {
  449. self::$contextrestriction = context_system::instance();
  450. }
  451. $rcontext = self::$contextrestriction;
  452. if ($rcontext->contextlevel == $context->contextlevel) {
  453. if ($rcontext->id != $context->id) {
  454. throw new restricted_context_exception();
  455. }
  456. } else if ($rcontext->contextlevel > $context->contextlevel) {
  457. throw new restricted_context_exception();
  458. } else {
  459. $parents = $context->get_parent_context_ids();
  460. if (!in_array($rcontext->id, $parents)) {
  461. throw new restricted_context_exception();
  462. }
  463. }
  464. $PAGE->reset_theme_and_output();
  465. list($unused, $course, $cm) = get_context_info_array($context->id);
  466. require_login($course, false, $cm, false, true);
  467. $PAGE->set_context($context);
  468. }
  469. /**
  470. * Get context from passed parameters.
  471. * The passed array must either contain a contextid or a combination of context level and instance id to fetch the context.
  472. * For example, the context level can be "course" and instanceid can be courseid.
  473. *
  474. * See context_helper::get_all_levels() for a list of valid context levels.
  475. *
  476. * @param array $param
  477. * @since Moodle 2.6
  478. * @throws invalid_parameter_exception
  479. * @return context
  480. */
  481. protected static function get_context_from_params($param) {
  482. $levels = context_helper::get_all_levels();
  483. if (!empty($param['contextid'])) {
  484. return context::instance_by_id($param['contextid'], IGNORE_MISSING);
  485. } else if (!empty($param['contextlevel']) && isset($param['instanceid'])) {
  486. $contextlevel = "context_".$param['contextlevel'];
  487. if (!array_search($contextlevel, $levels)) {
  488. throw new invalid_parameter_exception('Invalid context level = '.$param['contextlevel']);
  489. }
  490. return $contextlevel::instance($param['instanceid'], IGNORE_MISSING);
  491. } else {
  492. // No valid context info was found.
  493. throw new invalid_parameter_exception('Missing parameters, please provide either context level with instance id or contextid');
  494. }
  495. }
  496. /**
  497. * Returns a prepared structure to use a context parameters.
  498. * @return external_single_structure
  499. */
  500. protected static function get_context_parameters() {
  501. $id = new external_value(
  502. PARAM_INT,
  503. 'Context ID. Either use this value, or level and instanceid.',
  504. VALUE_DEFAULT,
  505. 0
  506. );
  507. $level = new external_value(
  508. PARAM_ALPHA,
  509. 'Context level. To be used with instanceid.',
  510. VALUE_DEFAULT,
  511. ''
  512. );
  513. $instanceid = new external_value(
  514. PARAM_INT,
  515. 'Context instance ID. To be used with level',
  516. VALUE_DEFAULT,
  517. 0
  518. );
  519. return new external_single_structure(array(
  520. 'contextid' => $id,
  521. 'contextlevel' => $level,
  522. 'instanceid' => $instanceid,
  523. ));
  524. }
  525. }
  526. /**
  527. * Common ancestor of all parameter description classes
  528. *
  529. * @package core_webservice
  530. * @copyright 2009 Petr Skodak
  531. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  532. * @since Moodle 2.0
  533. */
  534. abstract class external_description {
  535. /** @var string Description of element */
  536. public $desc;
  537. /** @var bool Element value required, null not allowed */
  538. public $required;
  539. /** @var mixed Default value */
  540. public $default;
  541. /**
  542. * Contructor
  543. *
  544. * @param string $desc
  545. * @param bool $required
  546. * @param mixed $default
  547. * @since Moodle 2.0
  548. */
  549. public function __construct($desc, $required, $default) {
  550. $this->desc = $desc;
  551. $this->required = $required;
  552. $this->default = $default;
  553. }
  554. }
  555. /**
  556. * Scalar value description class
  557. *
  558. * @package core_webservice
  559. * @copyright 2009 Petr Skodak
  560. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  561. * @since Moodle 2.0
  562. */
  563. class external_value extends external_description {
  564. /** @var mixed Value type PARAM_XX */
  565. public $type;
  566. /** @var bool Allow null values */
  567. public $allownull;
  568. /**
  569. * Constructor
  570. *
  571. * @param mixed $type
  572. * @param string $desc
  573. * @param bool $required
  574. * @param mixed $default
  575. * @param bool $allownull
  576. * @since Moodle 2.0
  577. */
  578. public function __construct($type, $desc='', $required=VALUE_REQUIRED,
  579. $default=null, $allownull=NULL_ALLOWED) {
  580. parent::__construct($desc, $required, $default);
  581. $this->type = $type;
  582. $this->allownull = $allownull;
  583. }
  584. }
  585. /**
  586. * Associative array description class
  587. *
  588. * @package core_webservice
  589. * @copyright 2009 Petr Skodak
  590. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  591. * @since Moodle 2.0
  592. */
  593. class external_single_structure extends external_description {
  594. /** @var array Description of array keys key=>external_description */
  595. public $keys;
  596. /**
  597. * Constructor
  598. *
  599. * @param array $keys
  600. * @param string $desc
  601. * @param bool $required
  602. * @param array $default
  603. * @since Moodle 2.0
  604. */
  605. public function __construct(array $keys, $desc='',
  606. $required=VALUE_REQUIRED, $default=null) {
  607. parent::__construct($desc, $required, $default);
  608. $this->keys = $keys;
  609. }
  610. }
  611. /**
  612. * Bulk array description class.
  613. *
  614. * @package core_webservice
  615. * @copyright 2009 Petr Skodak
  616. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  617. * @since Moodle 2.0
  618. */
  619. class external_multiple_structure extends external_description {
  620. /** @var external_description content */
  621. public $content;
  622. /**
  623. * Constructor
  624. *
  625. * @param external_description $content
  626. * @param string $desc
  627. * @param bool $required
  628. * @param array $default
  629. * @since Moodle 2.0
  630. */
  631. public function __construct(external_description $content, $desc='',
  632. $required=VALUE_REQUIRED, $default=null) {
  633. parent::__construct($desc, $required, $default);
  634. $this->content = $content;
  635. }
  636. }
  637. /**
  638. * Description of top level - PHP function parameters.
  639. *
  640. * @package core_webservice
  641. * @copyright 2009 Petr Skodak
  642. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  643. * @since Moodle 2.0
  644. */
  645. class external_function_parameters extends external_single_structure {
  646. /**
  647. * Constructor - does extra checking to prevent top level optional parameters.
  648. *
  649. * @param array $keys
  650. * @param string $desc
  651. * @param bool $required
  652. * @param array $default
  653. */
  654. public function __construct(array $keys, $desc='', $required=VALUE_REQUIRED, $default=null) {
  655. global $CFG;
  656. if ($CFG->debugdeveloper) {
  657. foreach ($keys as $key => $value) {
  658. if ($value instanceof external_value) {
  659. if ($value->required == VALUE_OPTIONAL) {
  660. debugging('External function parameters: invalid OPTIONAL value specified.', DEBUG_DEVELOPER);
  661. break;
  662. }
  663. }
  664. }
  665. }
  666. parent::__construct($keys, $desc, $required, $default);
  667. }
  668. }
  669. /**
  670. * Generate a token
  671. *
  672. * @param string $tokentype EXTERNAL_TOKEN_EMBEDDED|EXTERNAL_TOKEN_PERMANENT
  673. * @param stdClass|int $serviceorid service linked to the token
  674. * @param int $userid user linked to the token
  675. * @param stdClass|int $contextorid
  676. * @param int $validuntil date when the token expired
  677. * @param string $iprestriction allowed ip - if 0 or empty then all ips are allowed
  678. * @return string generated token
  679. * @author 2010 Jamie Pratt
  680. * @since Moodle 2.0
  681. */
  682. function external_generate_token($tokentype, $serviceorid, $userid, $contextorid, $validuntil=0, $iprestriction=''){
  683. global $DB, $USER;
  684. // make sure the token doesn't exist (even if it should be almost impossible with the random generation)
  685. $numtries = 0;
  686. do {
  687. $numtries ++;
  688. $generatedtoken = md5(uniqid(rand(),1));
  689. if ($numtries > 5){
  690. throw new moodle_exception('tokengenerationfailed');
  691. }
  692. } while ($DB->record_exists('external_tokens', array('token'=>$generatedtoken)));
  693. $newtoken = new stdClass();
  694. $newtoken->token = $generatedtoken;
  695. if (!is_object($serviceorid)){
  696. $service = $DB->get_record('external_services', array('id' => $serviceorid));
  697. } else {
  698. $service = $serviceorid;
  699. }
  700. if (!is_object($contextorid)){
  701. $context = context::instance_by_id($contextorid, MUST_EXIST);
  702. } else {
  703. $context = $contextorid;
  704. }
  705. if (empty($service->requiredcapability) || has_capability($service->requiredcapability, $context, $userid)) {
  706. $newtoken->externalserviceid = $service->id;
  707. } else {
  708. throw new moodle_exception('nocapabilitytousethisservice');
  709. }
  710. $newtoken->tokentype = $tokentype;
  711. $newtoken->userid = $userid;
  712. if ($tokentype == EXTERNAL_TOKEN_EMBEDDED){
  713. $newtoken->sid = session_id();
  714. }
  715. $newtoken->contextid = $context->id;
  716. $newtoken->creatorid = $USER->id;
  717. $newtoken->timecreated = time();
  718. $newtoken->validuntil = $validuntil;
  719. if (!empty($iprestriction)) {
  720. $newtoken->iprestriction = $iprestriction;
  721. }
  722. // Generate the private token, it must be transmitted only via https.
  723. $newtoken->privatetoken = random_string(64);
  724. $DB->insert_record('external_tokens', $newtoken);
  725. return $newtoken->token;
  726. }
  727. /**
  728. * Create and return a session linked token. Token to be used for html embedded client apps that want to communicate
  729. * with the Moodle server through web services. The token is linked to the current session for the current page request.
  730. * It is expected this will be called in the script generating the html page that is embedding the client app and that the
  731. * returned token will be somehow passed into the client app being embedded in the page.
  732. *
  733. * @param string $servicename name of the web service. Service name as defined in db/services.php
  734. * @param int $context context within which the web service can operate.
  735. * @return int returns token id.
  736. * @since Moodle 2.0
  737. */
  738. function external_create_service_token($servicename, $context){
  739. global $USER, $DB;
  740. $service = $DB->get_record('external_services', array('name'=>$servicename), '*', MUST_EXIST);
  741. return external_generate_token(EXTERNAL_TOKEN_EMBEDDED, $service, $USER->id, $context, 0);
  742. }
  743. /**
  744. * Delete all pre-built services (+ related tokens) and external functions information defined in the specified component.
  745. *
  746. * @param string $component name of component (moodle, mod_assignment, etc.)
  747. */
  748. function external_delete_descriptions($component) {
  749. global $DB;
  750. $params = array($component);
  751. $DB->delete_records_select('external_tokens',
  752. "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params);
  753. $DB->delete_records_select('external_services_users',
  754. "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params);
  755. $DB->delete_records_select('external_services_functions',
  756. "functionname IN (SELECT name FROM {external_functions} WHERE component = ?)", $params);
  757. $DB->delete_records('external_services', array('component'=>$component));
  758. $DB->delete_records('external_functions', array('component'=>$component));
  759. }
  760. /**
  761. * Standard Moodle web service warnings
  762. *
  763. * @package core_webservice
  764. * @copyright 2012 Jerome Mouneyrac
  765. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  766. * @since Moodle 2.3
  767. */
  768. class external_warnings extends external_multiple_structure {
  769. /**
  770. * Constructor
  771. *
  772. * @since Moodle 2.3
  773. */
  774. public function __construct($itemdesc = 'item', $itemiddesc = 'item id',
  775. $warningcodedesc = 'the warning code can be used by the client app to implement specific behaviour') {
  776. parent::__construct(
  777. new external_single_structure(
  778. array(
  779. 'item' => new external_value(PARAM_TEXT, $itemdesc, VALUE_OPTIONAL),
  780. 'itemid' => new external_value(PARAM_INT, $itemiddesc, VALUE_OPTIONAL),
  781. 'warningcode' => new external_value(PARAM_ALPHANUM, $warningcodedesc),
  782. 'message' => new external_value(PARAM_TEXT,
  783. 'untranslated english message to explain the warning')
  784. ), 'warning'),
  785. 'list of warnings', VALUE_OPTIONAL);
  786. }
  787. }
  788. /**
  789. * A pre-filled external_value class for text format.
  790. *
  791. * Default is FORMAT_HTML
  792. * This should be used all the time in external xxx_params()/xxx_returns functions
  793. * as it is the standard way to implement text format param/return values.
  794. *
  795. * @package core_webservice
  796. * @copyright 2012 Jerome Mouneyrac
  797. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  798. * @since Moodle 2.3
  799. */
  800. class external_format_value extends external_value {
  801. /**
  802. * Constructor
  803. *
  804. * @param string $textfieldname Name of the text field
  805. * @param int $required if VALUE_REQUIRED then set standard default FORMAT_HTML
  806. * @param int $default Default value.
  807. * @since Moodle 2.3
  808. */
  809. public function __construct($textfieldname, $required = VALUE_REQUIRED, $default = null) {
  810. if ($default == null && $required == VALUE_DEFAULT) {
  811. $default = FORMAT_HTML;
  812. }
  813. $desc = $textfieldname . ' format (' . FORMAT_HTML . ' = HTML, '
  814. . FORMAT_MOODLE . ' = MOODLE, '
  815. . FORMAT_PLAIN . ' = PLAIN or '
  816. . FORMAT_MARKDOWN . ' = MARKDOWN)';
  817. parent::__construct(PARAM_INT, $desc, $required, $default);
  818. }
  819. }
  820. /**
  821. * Validate text field format against known FORMAT_XXX
  822. *
  823. * @param array $format the format to validate
  824. * @return the validated format
  825. * @throws coding_exception
  826. * @since Moodle 2.3
  827. */
  828. function external_validate_format($format) {
  829. $allowedformats = array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN);
  830. if (!in_array($format, $allowedformats)) {
  831. throw new moodle_exception('formatnotsupported', 'webservice', '' , null,
  832. 'The format with value=' . $format . ' is not supported by this Moodle site');
  833. }
  834. return $format;
  835. }
  836. /**
  837. * Format the string to be returned properly as requested by the either the web service server,
  838. * either by an internally call.
  839. * The caller can change the format (raw) with the external_settings singleton
  840. * All web service servers must set this singleton when parsing the $_GET and $_POST.
  841. *
  842. * <pre>
  843. * Options are the same that in {@link format_string()} with some changes:
  844. * filter : Can be set to false to force filters off, else observes {@link external_settings}.
  845. * </pre>
  846. *
  847. * @param string $str The string to be filtered. Should be plain text, expect
  848. * possibly for multilang tags.
  849. * @param boolean $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713
  850. * @param context|int $contextorid The id of the context for the string or the context (affects filters).
  851. * @param array $options options array/object or courseid
  852. * @return string text
  853. * @since Moodle 3.0
  854. */
  855. function external_format_string($str, $contextorid, $striplinks = true, $options = array()) {
  856. // Get settings (singleton).
  857. $settings = external_settings::get_instance();
  858. if (empty($contextorid)) {
  859. throw new coding_exception('contextid is required');
  860. }
  861. if (!$settings->get_raw()) {
  862. if (is_object($contextorid) && is_a($contextorid, 'context')) {
  863. $context = $contextorid;
  864. } else {
  865. $context = context::instance_by_id($contextorid);
  866. }
  867. $options['context'] = $context;
  868. $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter();
  869. $str = format_string($str, $striplinks, $options);
  870. }
  871. return $str;
  872. }
  873. /**
  874. * Format the text to be returned properly as requested by the either the web service server,
  875. * either by an internally call.
  876. * The caller can change the format (raw, filter, file, fileurl) with the external_settings singleton
  877. * All web service servers must set this singleton when parsing the $_GET and $_POST.
  878. *
  879. * <pre>
  880. * Options are the same that in {@link format_text()} with some changes in defaults to provide backwards compatibility:
  881. * trusted : If true the string won't be cleaned. Default false.
  882. * noclean : If true the string won't be cleaned only if trusted is also true. Default false.
  883. * nocache : If true the string will not be cached and will be formatted every call. Default false.
  884. * filter : Can be set to false to force filters off, else observes {@link external_settings}.
  885. * para : If true then the returned string will be wrapped in div tags. Default (different from format_text) false.
  886. * Default changed because div tags are not commonly needed.
  887. * newlines : If true then lines newline breaks will be converted to HTML newline breaks. Default true.
  888. * context : Not used! Using contextid parameter instead.
  889. * overflowdiv : If set to true the formatted text will be encased in a div with the class no-overflow before being
  890. * returned. Default false.
  891. * allowid : If true then id attributes will not be removed, even when using htmlpurifier. Default (different from
  892. * format_text) true. Default changed id attributes are commonly needed.
  893. * blanktarget : If true all <a> tags will have target="_blank" added unless target is explicitly specified.
  894. * </pre>
  895. *
  896. * @param string $text The content that may contain ULRs in need of rewriting.
  897. * @param int $textformat The text format.
  898. * @param context|int $contextorid This parameter and the next two identify the file area to use.
  899. * @param string $component
  900. * @param string $filearea helps identify the file area.
  901. * @param int $itemid helps identify the file area.
  902. * @param object/array $options text formatting options
  903. * @return array text + textformat
  904. * @since Moodle 2.3
  905. * @since Moodle 3.2 component, filearea and itemid are optional parameters
  906. */
  907. function external_format_text($text, $textformat, $contextorid, $component = null, $filearea = null, $itemid = null,
  908. $options = null) {
  909. global $CFG;
  910. // Get settings (singleton).
  911. $settings = external_settings::get_instance();
  912. if (is_object($contextorid) && is_a($contextorid, 'context')) {
  913. $context = $contextorid;
  914. $contextid = $context->id;
  915. } else {
  916. $context = null;
  917. $contextid = $contextorid;
  918. }
  919. if ($component and $filearea and $settings->get_fileurl()) {
  920. require_once($CFG->libdir . "/filelib.php");
  921. $text = file_rewrite_pluginfile_urls($text, $settings->get_file(), $contextid, $component, $filearea, $itemid);
  922. }
  923. // Note that $CFG->forceclean does not apply here if the client requests for the raw database content.
  924. // This is consistent with web clients that are still able to load non-cleaned text into editors, too.
  925. if (!$settings->get_raw()) {
  926. $options = (array)$options;
  927. // If context is passed in options, check that is the same to show a debug message.
  928. if (isset($options['context'])) {
  929. if ((is_object($options['context']) && $options['context']->id != $contextid)
  930. || (!is_object($options['context']) && $options['context'] != $contextid)) {
  931. debugging('Different contexts found in external_format_text parameters. $options[\'context\'] not allowed.
  932. Using $contextid parameter...', DEBUG_DEVELOPER);
  933. }
  934. }
  935. $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter();
  936. $options['para'] = isset($options['para']) ? $options['para'] : false;
  937. $options['context'] = !is_null($context) ? $context : context::instance_by_id($contextid);
  938. $options['allowid'] = isset($options['allowid']) ? $options['allowid'] : true;
  939. $text = format_text($text, $textformat, $options);
  940. $textformat = FORMAT_HTML; // Once converted to html (from markdown, plain... lets inform consumer this is already HTML).
  941. }
  942. return array($text, $textformat);
  943. }
  944. /**
  945. * Generate or return an existing token for the current authenticated user.
  946. * This function is used for creating a valid token for users authenticathing via login/token.php or admin/tool/mobile/launch.php.
  947. *
  948. * @param stdClass $service external service object
  949. * @return stdClass token object
  950. * @since Moodle 3.2
  951. * @throws moodle_exception
  952. */
  953. function external_generate_token_for_current_user($service) {
  954. global $DB, $USER, $CFG;
  955. core_user::require_active_user($USER, true, true);
  956. // Check if there is any required system capability.
  957. if ($service->requiredcapability and !has_capability($service->requiredcapability, context_system::instance())) {
  958. throw new moodle_exception('missingrequiredcapability', 'webservice', '', $service->requiredcapability);
  959. }
  960. // Specific checks related to user restricted service.
  961. if ($service->restrictedusers) {
  962. $authoriseduser = $DB->get_record('external_services_users',
  963. array('externalserviceid' => $service->id, 'userid' => $USER->id));
  964. if (empty($authoriseduser)) {
  965. throw new moodle_exception('usernotallowed', 'webservice', '', $service->shortname);
  966. }
  967. if (!empty($authoriseduser->validuntil) and $authoriseduser->validuntil < time()) {
  968. throw new moodle_exception('invalidtimedtoken', 'webservice');
  969. }
  970. if (!empty($authoriseduser->iprestriction) and !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) {
  971. throw new moodle_exception('invalidiptoken', 'webservice');
  972. }
  973. }
  974. // Check if a token has already been created for this user and this service.
  975. $conditions = array(
  976. 'userid' => $USER->id,
  977. 'externalserviceid' => $service->id,
  978. 'tokentype' => EXTERNAL_TOKEN_PERMANENT
  979. );
  980. $tokens = $DB->get_records('external_tokens', $conditions, 'timecreated ASC');
  981. // A bit of sanity checks.
  982. foreach ($tokens as $key => $token) {
  983. // Checks related to a specific token. (script execution continue).
  984. $unsettoken = false;
  985. // If sid is set then there must be a valid associated session no matter the token type.
  986. if (!empty($token->sid)) {
  987. if (!\core\session\manager::session_exists($token->sid)) {
  988. // This token will never be valid anymore, delete it.
  989. $DB->delete_records('external_tokens', array('sid' => $token->sid));
  990. $unsettoken = true;
  991. }
  992. }
  993. // Remove token is not valid anymore.
  994. if (!empty($token->validuntil) and $token->validuntil < time()) {
  995. $DB->delete_records('external_tokens', array('token' => $token->token, 'tokentype' => EXTERNAL_TOKEN_PERMANENT));
  996. $unsettoken = true;
  997. }
  998. // Remove token if its IP is restricted.
  999. if (isset($token->iprestriction) and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
  1000. $unsettoken = true;
  1001. }
  1002. if ($unsettoken) {
  1003. unset($tokens[$key]);
  1004. }
  1005. }
  1006. // If some valid tokens exist then use the most recent.
  1007. if (count($tokens) > 0) {
  1008. $token = array_pop($tokens);
  1009. } else {
  1010. $context = context_system::instance();
  1011. $isofficialservice = $service->shortname == MOODLE_OFFICIAL_MOBILE_SERVICE;
  1012. if (($isofficialservice and has_capability('moodle/webservice:createmobiletoken', $context)) or
  1013. (!is_siteadmin($USER) && has_capability('moodle/webservice:createtoken', $context))) {
  1014. // Create a new token.
  1015. $token = new stdClass;
  1016. $token->token = md5(uniqid(rand(), 1));
  1017. $token->userid = $USER->id;
  1018. $token->tokentype = EXTERNAL_TOKEN_PERMANENT;
  1019. $token->contextid = context_system::instance()->id;
  1020. $token->creatorid = $USER->id;
  1021. $token->timecreated = time();
  1022. $token->externalserviceid = $service->id;
  1023. // By default tokens are valid for 12 weeks.
  1024. $token->validuntil = $token->timecreated + $CFG->tokenduration;
  1025. $token->iprestriction = null;
  1026. $token->sid = null;
  1027. $token->lastaccess = null;
  1028. // Generate the private token, it must be transmitted only via https.
  1029. $token->privatetoken = random_string(64);
  1030. $token->id = $DB->insert_record('external_tokens', $token);
  1031. $eventtoken = clone $token;
  1032. $eventtoken->privatetoken = null;
  1033. $params = array(
  1034. 'objectid' => $eventtoken->id,
  1035. 'relateduserid' => $USER->id,
  1036. 'other' => array(
  1037. 'auto' => true
  1038. )
  1039. );
  1040. $event = \core\event\webservice_token_created::create($params);
  1041. $event->add_record_snapshot('external_tokens', $eventtoken);
  1042. $event->trigger();
  1043. } else {
  1044. throw new moodle_exception('cannotcreatetoken', 'webservice', '', $service->shortname);
  1045. }
  1046. }
  1047. return $token;
  1048. }
  1049. /**
  1050. * Set the last time a token was sent and trigger the \core\event\webservice_token_sent event.
  1051. *
  1052. * This function is used when a token is generated by the user via login/token.php or admin/tool/mobile/launch.php.
  1053. * In order to protect the privatetoken, we remove it from the event params.
  1054. *
  1055. * @param stdClass $token token object
  1056. * @since Moodle 3.2
  1057. */
  1058. function external_log_token_request($token) {
  1059. global $DB, $USER;
  1060. $token->privatetoken = null;
  1061. // Log token access.
  1062. $DB->set_field('external_tokens', 'lastaccess', time(), array('id' => $token->id));
  1063. $params = array(
  1064. 'objectid' => $token->id,
  1065. );
  1066. $event = \core\event\webservice_token_sent::create($params);
  1067. $event->add_record_snapshot('external_tokens', $token);
  1068. $event->trigger();
  1069. // Check if we need to notify the user about the new login via token.
  1070. $loginip = getremoteaddr();
  1071. if ($USER->lastip != $loginip &&
  1072. ((!WS_SERVER && !CLI_SCRIPT && NO_MOODLE_COOKIES) || PHPUNIT_TEST)) {
  1073. $logintime = time();
  1074. $useragent = \core_useragent::get_user_agent_string();
  1075. $ismoodleapp = \core_useragent::is_moodle_app();
  1076. // Schedule adhoc task to sent a login notification to the user.
  1077. $task = new \core\

Large files files are truncated, but you can click here to view the full file