Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 402 and 403]

   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  namespace core_external;
  18  
  19  use context;
  20  use context_course;
  21  use context_helper;
  22  use context_system;
  23  use core_user;
  24  use moodle_exception;
  25  use moodle_url;
  26  use stdClass;
  27  
  28  /**
  29   * Utility functions for the external API.
  30   *
  31   * @package    core_webservice
  32   * @copyright  2015 Juan Leyva
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class util {
  36      /**
  37       * Validate a list of courses, returning the complete course objects for valid courses.
  38       *
  39       * Each course has an additional 'contextvalidated' field, this will be set to true unless
  40       * you set $keepfails, in which case it will be false if validation fails for a course.
  41       *
  42       * @param  array $courseids A list of course ids
  43       * @param  array $courses   An array of courses already pre-fetched, indexed by course id.
  44       * @param  bool $addcontext True if the returned course object should include the full context object.
  45       * @param  bool $keepfails  True to keep all the course objects even if validation fails
  46       * @return array            An array of courses and the validation warnings
  47       */
  48      public static function validate_courses(
  49          $courseids,
  50          $courses = [],
  51          $addcontext = false,
  52          $keepfails = false
  53      ) {
  54          global $DB;
  55  
  56          // Delete duplicates.
  57          $courseids = array_unique($courseids);
  58          $warnings = [];
  59  
  60          // Remove courses which are not even requested.
  61          $courses = array_intersect_key($courses, array_flip($courseids));
  62  
  63          // For any courses NOT loaded already, get them in a single query (and preload contexts)
  64          // for performance. Preserve ordering because some tests depend on it.
  65          $newcourseids = [];
  66          foreach ($courseids as $cid) {
  67              if (!array_key_exists($cid, $courses)) {
  68                  $newcourseids[] = $cid;
  69              }
  70          }
  71          if ($newcourseids) {
  72              [$listsql, $listparams] = $DB->get_in_or_equal($newcourseids);
  73  
  74              // Load list of courses, and preload associated contexts.
  75              $contextselect = context_helper::get_preload_record_columns_sql('x');
  76              $newcourses = $DB->get_records_sql(
  77                  "
  78                              SELECT c.*, $contextselect
  79                                FROM {course} c
  80                                JOIN {context} x ON x.instanceid = c.id
  81                               WHERE x.contextlevel = ? AND c.id $listsql",
  82                  array_merge([CONTEXT_COURSE], $listparams)
  83              );
  84              foreach ($newcourseids as $cid) {
  85                  if (array_key_exists($cid, $newcourses)) {
  86                      $course = $newcourses[$cid];
  87                      context_helper::preload_from_record($course);
  88                      $courses[$course->id] = $course;
  89                  }
  90              }
  91          }
  92  
  93          foreach ($courseids as $cid) {
  94              // Check the user can function in this context.
  95              try {
  96                  $context = context_course::instance($cid);
  97                  external_api::validate_context($context);
  98  
  99                  if ($addcontext) {
 100                      $courses[$cid]->context = $context;
 101                  }
 102                  $courses[$cid]->contextvalidated = true;
 103              } catch (\Exception $e) {
 104                  if ($keepfails) {
 105                      $courses[$cid]->contextvalidated = false;
 106                  } else {
 107                      unset($courses[$cid]);
 108                  }
 109                  $warnings[] = [
 110                      'item' => 'course',
 111                      'itemid' => $cid,
 112                      'warningcode' => '1',
 113                      'message' => 'No access rights in course context',
 114                  ];
 115              }
 116          }
 117  
 118          return [$courses, $warnings];
 119      }
 120  
 121      /**
 122       * Returns all area files (optionally limited by itemid).
 123       *
 124       * @param int $contextid context ID
 125       * @param string $component component
 126       * @param string $filearea file area
 127       * @param int $itemid item ID or all files if not specified
 128       * @param bool $useitemidinurl wether to use the item id in the file URL (modules intro don't use it)
 129       * @return array of files, compatible with the external_files structure.
 130       * @since Moodle 3.2
 131       */
 132      public static function get_area_files($contextid, $component, $filearea, $itemid = false, $useitemidinurl = true) {
 133          $files = [];
 134          $fs = get_file_storage();
 135  
 136          if ($areafiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'itemid, filepath, filename', false)) {
 137              foreach ($areafiles as $areafile) {
 138                  $file = [];
 139                  $file['filename'] = $areafile->get_filename();
 140                  $file['filepath'] = $areafile->get_filepath();
 141                  $file['mimetype'] = $areafile->get_mimetype();
 142                  $file['filesize'] = $areafile->get_filesize();
 143                  $file['timemodified'] = $areafile->get_timemodified();
 144                  $file['isexternalfile'] = $areafile->is_external_file();
 145                  if ($file['isexternalfile']) {
 146                      $file['repositorytype'] = $areafile->get_repository_type();
 147                  }
 148                  $fileitemid = $useitemidinurl ? $areafile->get_itemid() : null;
 149                  $file['fileurl'] = moodle_url::make_webservice_pluginfile_url(
 150                      $contextid,
 151                      $component,
 152                      $filearea,
 153                      $fileitemid,
 154                      $areafile->get_filepath(),
 155                      $areafile->get_filename()
 156                  )->out(false);
 157                  $files[] = $file;
 158              }
 159          }
 160          return $files;
 161      }
 162  
 163  
 164      /**
 165       * Create and return a session linked token. Token to be used for html embedded client apps that want to communicate
 166       * with the Moodle server through web services. The token is linked to the current session for the current page request.
 167       * It is expected this will be called in the script generating the html page that is embedding the client app and that the
 168       * returned token will be somehow passed into the client app being embedded in the page.
 169       *
 170       * @param int $tokentype EXTERNAL_TOKEN_EMBEDDED|EXTERNAL_TOKEN_PERMANENT
 171       * @param stdClass $service service linked to the token
 172       * @param int $userid user linked to the token
 173       * @param context $context
 174       * @param int $validuntil date when the token expired
 175       * @param string $iprestriction allowed ip - if 0 or empty then all ips are allowed
 176       * @param string $name token name as a note or token identity at the table view.
 177       * @return string generated token
 178       */
 179      public static function generate_token(
 180          int $tokentype,
 181          stdClass $service,
 182          int $userid,
 183          context $context,
 184          int $validuntil = 0,
 185          string $iprestriction = '',
 186          string $name = ''
 187      ): string {
 188          global $DB, $USER, $SESSION;
 189  
 190          // Make sure the token doesn't exist (even if it should be almost impossible with the random generation).
 191          $numtries = 0;
 192          do {
 193              $numtries++;
 194              $generatedtoken = md5(uniqid((string) rand(), true));
 195              if ($numtries > 5) {
 196                  throw new moodle_exception('tokengenerationfailed');
 197              }
 198          } while ($DB->record_exists('external_tokens', ['token' => $generatedtoken]));
 199          $newtoken = (object) [
 200              'token' => $generatedtoken,
 201          ];
 202  
 203          if (empty($service->requiredcapability) || has_capability($service->requiredcapability, $context, $userid)) {
 204              $newtoken->externalserviceid = $service->id;
 205          } else {
 206              throw new moodle_exception('nocapabilitytousethisservice');
 207          }
 208  
 209          $newtoken->tokentype = $tokentype;
 210          $newtoken->userid = $userid;
 211          if ($tokentype == EXTERNAL_TOKEN_EMBEDDED) {
 212              $newtoken->sid = session_id();
 213          }
 214  
 215          $newtoken->contextid = $context->id;
 216          $newtoken->creatorid = $USER->id;
 217          $newtoken->timecreated = time();
 218          $newtoken->validuntil = $validuntil;
 219          if (!empty($iprestriction)) {
 220              $newtoken->iprestriction = $iprestriction;
 221          }
 222  
 223          // Generate the private token, it must be transmitted only via https.
 224          $newtoken->privatetoken = random_string(64);
 225  
 226          if (!$name) {
 227              // Generate a token name.
 228              $name = self::generate_token_name();
 229          }
 230          $newtoken->name = $name;
 231  
 232          $tokenid = $DB->insert_record('external_tokens', $newtoken);
 233          // Create new session to hold newly created token ID.
 234          $SESSION->webservicenewlycreatedtoken = $tokenid;
 235  
 236          return $newtoken->token;
 237      }
 238  
 239      /**
 240       * Get a service by its id.
 241       *
 242       * @param int $serviceid
 243       * @return stdClass
 244       */
 245      public static function get_service_by_id(int $serviceid): stdClass {
 246          global $DB;
 247  
 248          return $DB->get_record('external_services', ['id' => $serviceid], '*', MUST_EXIST);
 249      }
 250  
 251      /**
 252       * Get a service by its name.
 253       *
 254       * @param string $name The service name.
 255       * @return stdClass
 256       */
 257      public static function get_service_by_name(string $name): stdClass {
 258          global $DB;
 259  
 260          return $DB->get_record('external_services', ['name' => $name], '*', MUST_EXIST);
 261      }
 262  
 263      /**
 264       * Set the last time a token was sent and trigger the \core\event\webservice_token_sent event.
 265       *
 266       * This function is used when a token is generated by the user via login/token.php or admin/tool/mobile/launch.php.
 267       * In order to protect the privatetoken, we remove it from the event params.
 268       *
 269       * @param  stdClass $token token object
 270       */
 271      public static function log_token_request(stdClass $token): void {
 272          global $DB, $USER;
 273  
 274          $token->privatetoken = null;
 275  
 276          // Log token access.
 277          $DB->set_field('external_tokens', 'lastaccess', time(), ['id' => $token->id]);
 278  
 279          $event = \core\event\webservice_token_sent::create([
 280              'objectid' => $token->id,
 281          ]);
 282          $event->add_record_snapshot('external_tokens', $token);
 283          $event->trigger();
 284  
 285          // Check if we need to notify the user about the new login via token.
 286          $loginip = getremoteaddr();
 287          if ($USER->lastip === $loginip) {
 288              return;
 289          }
 290  
 291          $shouldskip = WS_SERVER || CLI_SCRIPT || !NO_MOODLE_COOKIES;
 292          if ($shouldskip && !PHPUNIT_TEST) {
 293              return;
 294          }
 295  
 296          // Schedule adhoc task to sent a login notification to the user.
 297          $task = new \core\task\send_login_notifications();
 298          $task->set_userid($USER->id);
 299          $logintime = time();
 300          $task->set_custom_data([
 301              'useragent' => \core_useragent::get_user_agent_string(),
 302              'ismoodleapp' => \core_useragent::is_moodle_app(),
 303              'loginip' => $loginip,
 304              'logintime' => $logintime,
 305          ]);
 306          $task->set_component('core');
 307          // We need sometime so the mobile app will send to Moodle the device information after login.
 308          $task->set_next_run_time(time() + (2 * MINSECS));
 309          \core\task\manager::reschedule_or_queue_adhoc_task($task);
 310      }
 311  
 312      /**
 313       * Generate or return an existing token for the current authenticated user.
 314       * This function is used for creating a valid token for users authenticathing via places, including:
 315       * - login/token.php
 316       * - admin/tool/mobile/launch.php.
 317       *
 318       * @param stdClass $service external service object
 319       * @return stdClass token object
 320       * @throws moodle_exception
 321       */
 322      public static function generate_token_for_current_user(stdClass $service) {
 323          global $DB, $USER, $CFG;
 324  
 325          core_user::require_active_user($USER, true, true);
 326  
 327          // Check if there is any required system capability.
 328          if ($service->requiredcapability && !has_capability($service->requiredcapability, context_system::instance())) {
 329              throw new moodle_exception('missingrequiredcapability', 'webservice', '', $service->requiredcapability);
 330          }
 331  
 332          // Specific checks related to user restricted service.
 333          if ($service->restrictedusers) {
 334              $authoriseduser = $DB->get_record('external_services_users', [
 335                  'externalserviceid' => $service->id,
 336                  'userid' => $USER->id,
 337              ]);
 338  
 339              if (empty($authoriseduser)) {
 340                  throw new moodle_exception('usernotallowed', 'webservice', '', $service->shortname);
 341              }
 342  
 343              if (!empty($authoriseduser->validuntil) && $authoriseduser->validuntil < time()) {
 344                  throw new moodle_exception('invalidtimedtoken', 'webservice');
 345              }
 346  
 347              if (!empty($authoriseduser->iprestriction) && !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) {
 348                  throw new moodle_exception('invalidiptoken', 'webservice');
 349              }
 350          }
 351  
 352          // Check if a token has already been created for this user and this service.
 353          $conditions = [
 354              'userid' => $USER->id,
 355              'externalserviceid' => $service->id,
 356              'tokentype' => EXTERNAL_TOKEN_PERMANENT,
 357          ];
 358          $tokens = $DB->get_records('external_tokens', $conditions, 'timecreated ASC');
 359  
 360          // A bit of sanity checks.
 361          foreach ($tokens as $key => $token) {
 362              // Checks related to a specific token. (script execution continue).
 363              $unsettoken = false;
 364              // If sid is set then there must be a valid associated session no matter the token type.
 365              if (!empty($token->sid)) {
 366                  if (!\core\session\manager::session_exists($token->sid)) {
 367                      // This token will never be valid anymore, delete it.
 368                      $DB->delete_records('external_tokens', ['sid' => $token->sid]);
 369                      $unsettoken = true;
 370                  }
 371              }
 372  
 373              // Remove token is not valid anymore.
 374              if (!empty($token->validuntil) && $token->validuntil < time()) {
 375                  $DB->delete_records('external_tokens', ['token' => $token->token, 'tokentype' => EXTERNAL_TOKEN_PERMANENT]);
 376                  $unsettoken = true;
 377              }
 378  
 379              // Remove token if its IP is restricted.
 380              if (isset($token->iprestriction) && !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
 381                  $unsettoken = true;
 382              }
 383  
 384              if ($unsettoken) {
 385                  unset($tokens[$key]);
 386              }
 387          }
 388  
 389          // If some valid tokens exist then use the most recent.
 390          if (count($tokens) > 0) {
 391              $token = array_pop($tokens);
 392          } else {
 393              $context = context_system::instance();
 394              $isofficialservice = $service->shortname == MOODLE_OFFICIAL_MOBILE_SERVICE;
 395  
 396              if (
 397                  ($isofficialservice && has_capability('moodle/webservice:createmobiletoken', $context)) ||
 398                  (!is_siteadmin($USER) && has_capability('moodle/webservice:createtoken', $context))
 399              ) {
 400                  // Create a new token.
 401                  $token = new stdClass();
 402                  $token->token = md5(uniqid((string) rand(), true));
 403                  $token->userid = $USER->id;
 404                  $token->tokentype = EXTERNAL_TOKEN_PERMANENT;
 405                  $token->contextid = context_system::instance()->id;
 406                  $token->creatorid = $USER->id;
 407                  $token->timecreated = time();
 408                  $token->externalserviceid = $service->id;
 409                  // By default tokens are valid for 12 weeks.
 410                  $token->validuntil = $token->timecreated + $CFG->tokenduration;
 411                  $token->iprestriction = null;
 412                  $token->sid = null;
 413                  $token->lastaccess = null;
 414                  $token->name = self::generate_token_name();
 415                  // Generate the private token, it must be transmitted only via https.
 416                  $token->privatetoken = random_string(64);
 417                  $token->id = $DB->insert_record('external_tokens', $token);
 418  
 419                  $eventtoken = clone $token;
 420                  $eventtoken->privatetoken = null;
 421                  $params = [
 422                      'objectid' => $eventtoken->id,
 423                      'relateduserid' => $USER->id,
 424                      'other' => [
 425                          'auto' => true,
 426                      ],
 427                  ];
 428                  $event = \core\event\webservice_token_created::create($params);
 429                  $event->add_record_snapshot('external_tokens', $eventtoken);
 430                  $event->trigger();
 431              } else {
 432                  throw new moodle_exception('cannotcreatetoken', 'webservice', '', $service->shortname);
 433              }
 434          }
 435          return $token;
 436      }
 437  
 438      /**
 439       * Format the string to be returned properly as requested by the either the web service server,
 440       * either by an internally call.
 441       * The caller can change the format (raw) with the settings singleton
 442       * All web service servers must set this singleton when parsing the $_GET and $_POST.
 443       *
 444       * <pre>
 445       * Options are the same that in {@see format_string()} with some changes:
 446       *      filter      : Can be set to false to force filters off, else observes {@see settings}.
 447       * </pre>
 448       *
 449       * @param string|null $content The string to be filtered. Should be plain text, expect
 450       * possibly for multilang tags.
 451       * @param context $context The id of the context for the string or the context (affects filters).
 452       * @param boolean $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713
 453       * @param array $options options array/object or courseid
 454       * @return string text
 455       */
 456      public static function format_string(
 457          $content,
 458          $context,
 459          $striplinks = true,
 460          $options = []
 461      ) {
 462          if ($content === null || $content === '') {
 463              // Nothing to return.
 464              // Note: It's common for the DB to return null, so we allow format_string to take a null,
 465              // even though it is counter-intuitive.
 466              return '';
 467          }
 468  
 469          // Get settings (singleton).
 470          $settings = external_settings::get_instance();
 471  
 472          if (!$settings->get_raw()) {
 473              $options['context'] = $context;
 474              $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter();
 475              return format_string($content, $striplinks, $options);
 476          }
 477  
 478          return $content;
 479      }
 480  
 481      /**
 482       * Format the text to be returned properly as requested by the either the web service server,
 483       * either by an internally call.
 484       * The caller can change the format (raw, filter, file, fileurl) with the \core_external\settings singleton
 485       * All web service servers must set this singleton when parsing the $_GET and $_POST.
 486       *
 487       * <pre>
 488       * Options are the same that in {@see format_text()} with some changes in defaults to provide backwards compatibility:
 489       *      trusted     :   If true the string won't be cleaned. Default false.
 490       *      noclean     :   If true the string won't be cleaned only if trusted is also true. Default false.
 491       *      nocache     :   If true the string will not be cached and will be formatted every call. Default false.
 492       *      filter      :   Can be set to false to force filters off, else observes {@see \core_external\settings}.
 493       *      para        :   If true then the returned string will be wrapped in div tags.
 494       *                      Default (different from format_text) false.
 495       *                      Default changed because div tags are not commonly needed.
 496       *      newlines    :   If true then lines newline breaks will be converted to HTML newline breaks. Default true.
 497       *      context     :   Not used! Using contextid parameter instead.
 498       *      overflowdiv :   If set to true the formatted text will be encased in a div with the class no-overflow before being
 499       *                      returned. Default false.
 500       *      allowid     :   If true then id attributes will not be removed, even when using htmlpurifier. Default (different from
 501       *                      format_text) true. Default changed id attributes are commonly needed.
 502       *      blanktarget :   If true all <a> tags will have target="_blank" added unless target is explicitly specified.
 503       * </pre>
 504       *
 505       * @param string|null $text The content that may contain ULRs in need of rewriting.
 506       * @param string|int|null $textformat The text format.
 507       * @param context $context This parameter and the next two identify the file area to use.
 508       * @param string|null $component
 509       * @param string|null $filearea helps identify the file area.
 510       * @param int|string|null $itemid helps identify the file area.
 511       * @param array|stdClass|null $options text formatting options
 512       * @return array text + textformat
 513       */
 514      public static function format_text(
 515          $text,
 516          $textformat,
 517          $context,
 518          $component = null,
 519          $filearea = null,
 520          $itemid = null,
 521          $options = null
 522      ) {
 523          global $CFG;
 524  
 525          if ($text === null || $text === '') {
 526              // Nothing to return.
 527              // Note: It's common for the DB to return null, so we allow format_string to take nulls,
 528              // even though it is counter-intuitive.
 529              return ['', $textformat ?? FORMAT_MOODLE];
 530          }
 531  
 532          if (empty($itemid)) {
 533              $itemid = null;
 534          }
 535  
 536          // Get settings (singleton).
 537          $settings = external_settings::get_instance();
 538  
 539          if ($component && $filearea && $settings->get_fileurl()) {
 540              require_once($CFG->libdir . "/filelib.php");
 541              $text = file_rewrite_pluginfile_urls($text, $settings->get_file(), $context->id, $component, $filearea, $itemid);
 542          }
 543  
 544          // Note that $CFG->forceclean does not apply here if the client requests for the raw database content.
 545          // This is consistent with web clients that are still able to load non-cleaned text into editors, too.
 546  
 547          if (!$settings->get_raw()) {
 548              $options = (array) $options;
 549  
 550              // If context is passed in options, check that is the same to show a debug message.
 551              if (isset($options['context'])) {
 552                  if (is_int($options['context'])) {
 553                      if ($options['context'] != $context->id) {
 554                          debugging(
 555                              'Different contexts found in external_format_text parameters. $options[\'context\'] not allowed. ' .
 556                              'Using $contextid parameter...',
 557                              DEBUG_DEVELOPER
 558                          );
 559                      }
 560                  } else if ($options['context'] instanceof context) {
 561                      if ($options['context']->id != $context->id) {
 562                          debugging(
 563                              'Different contexts found in external_format_text parameters. $options[\'context\'] not allowed. ' .
 564                              'Using $contextid parameter...',
 565                              DEBUG_DEVELOPER
 566                          );
 567                      }
 568                  }
 569              }
 570  
 571              $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter();
 572              $options['para'] = isset($options['para']) ? $options['para'] : false;
 573              $options['context'] = $context;
 574              $options['allowid'] = isset($options['allowid']) ? $options['allowid'] : true;
 575  
 576              $text = format_text($text, $textformat, $options);
 577              // Once converted to html (from markdown, plain... lets inform consumer this is already HTML).
 578              $textformat = FORMAT_HTML;
 579          }
 580  
 581          // Note: The formats defined in weblib are strings.
 582          return [$text, $textformat];
 583      }
 584  
 585      /**
 586       * Validate text field format against known FORMAT_XXX
 587       *
 588       * @param string $format the format to validate
 589       * @return string the validated format
 590       * @throws \moodle_exception
 591       * @since Moodle 2.3
 592       */
 593      public static function validate_format($format) {
 594          $allowedformats = [FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN];
 595          if (!in_array($format, $allowedformats)) {
 596              throw new moodle_exception(
 597                  'formatnotsupported',
 598                  'webservice',
 599                  '',
 600                  null,
 601                  "The format with value={$format} is not supported by this Moodle site"
 602              );
 603          }
 604          return $format;
 605      }
 606  
 607      /**
 608       * Delete all pre-built services, related tokens, and external functions information defined for the specified component.
 609       *
 610       * @param string $component The frankenstyle component name
 611       */
 612      public static function delete_service_descriptions(string $component): void {
 613          global $DB;
 614  
 615          $params = [$component];
 616  
 617          $DB->delete_records_select(
 618              'external_tokens',
 619              "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)",
 620              $params
 621          );
 622          $DB->delete_records_select(
 623              'external_services_users',
 624              "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)",
 625              $params
 626          );
 627          $DB->delete_records_select(
 628              'external_services_functions',
 629              "functionname IN (SELECT name FROM {external_functions} WHERE component = ?)",
 630              $params
 631          );
 632          $DB->delete_records('external_services', ['component' => $component]);
 633          $DB->delete_records('external_functions', ['component' => $component]);
 634      }
 635  
 636      /**
 637       * Generate token name.
 638       *
 639       * @return string
 640       */
 641      public static function generate_token_name(): string {
 642          return get_string(
 643              'tokennameprefix',
 644              'webservice',
 645              random_string(5)
 646          );
 647      }
 648  }