Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
                // If AJAX request, generate a standard plugin file url.
150
                if (AJAX_SCRIPT) {
151
                    $fileurl = moodle_url::make_pluginfile_url(
152
                        $contextid,
153
                        $component,
154
                        $filearea,
155
                        $fileitemid,
156
                        $areafile->get_filepath(),
157
                        $areafile->get_filename()
158
                    );
159
                } else { // Otherwise, generate a webservice plugin file url.
160
                    $fileurl = moodle_url::make_webservice_pluginfile_url(
161
                        $contextid,
162
                        $component,
163
                        $filearea,
164
                        $fileitemid,
165
                        $areafile->get_filepath(),
166
                        $areafile->get_filename()
167
                    );
168
                }
169
                $file['fileurl'] = $fileurl->out(false);
170
                $file['icon'] = file_file_icon($areafile);
171
                $files[] = $file;
172
            }
173
        }
174
        return $files;
175
    }
176
 
177
 
178
    /**
179
     * Create and return a session linked token. Token to be used for html embedded client apps that want to communicate
180
     * with the Moodle server through web services. The token is linked to the current session for the current page request.
181
     * It is expected this will be called in the script generating the html page that is embedding the client app and that the
182
     * returned token will be somehow passed into the client app being embedded in the page.
183
     *
184
     * @param int $tokentype EXTERNAL_TOKEN_EMBEDDED|EXTERNAL_TOKEN_PERMANENT
185
     * @param stdClass $service service linked to the token
186
     * @param int $userid user linked to the token
187
     * @param context $context
188
     * @param int $validuntil date when the token expired
189
     * @param string $iprestriction allowed ip - if 0 or empty then all ips are allowed
190
     * @param string $name token name as a note or token identity at the table view.
191
     * @return string generated token
192
     */
193
    public static function generate_token(
194
        int $tokentype,
195
        stdClass $service,
196
        int $userid,
197
        context $context,
198
        int $validuntil = 0,
199
        string $iprestriction = '',
200
        string $name = ''
201
    ): string {
202
        global $DB, $USER, $SESSION;
203
 
204
        // Make sure the token doesn't exist (even if it should be almost impossible with the random generation).
205
        $numtries = 0;
206
        do {
207
            $numtries++;
208
            $generatedtoken = md5(uniqid((string) rand(), true));
209
            if ($numtries > 5) {
210
                throw new moodle_exception('tokengenerationfailed');
211
            }
212
        } while ($DB->record_exists('external_tokens', ['token' => $generatedtoken]));
213
        $newtoken = (object) [
214
            'token' => $generatedtoken,
215
        ];
216
 
217
        if (empty($service->requiredcapability) || has_capability($service->requiredcapability, $context, $userid)) {
218
            $newtoken->externalserviceid = $service->id;
219
        } else {
220
            throw new moodle_exception('nocapabilitytousethisservice');
221
        }
222
 
223
        $newtoken->tokentype = $tokentype;
224
        $newtoken->userid = $userid;
225
        if ($tokentype == EXTERNAL_TOKEN_EMBEDDED) {
226
            $newtoken->sid = session_id();
227
        }
228
 
229
        $newtoken->contextid = $context->id;
230
        $newtoken->creatorid = $USER->id;
231
        $newtoken->timecreated = time();
232
        $newtoken->validuntil = $validuntil;
233
        if (!empty($iprestriction)) {
234
            $newtoken->iprestriction = $iprestriction;
235
        }
236
 
237
        // Generate the private token, it must be transmitted only via https.
238
        $newtoken->privatetoken = random_string(64);
239
 
240
        if (!$name) {
241
            // Generate a token name.
242
            $name = self::generate_token_name();
243
        }
244
        $newtoken->name = $name;
245
 
246
        $tokenid = $DB->insert_record('external_tokens', $newtoken);
247
        // Create new session to hold newly created token ID.
248
        $SESSION->webservicenewlycreatedtoken = $tokenid;
249
 
250
        return $newtoken->token;
251
    }
252
 
253
    /**
254
     * Get a service by its id.
255
     *
256
     * @param int $serviceid
257
     * @return stdClass
258
     */
259
    public static function get_service_by_id(int $serviceid): stdClass {
260
        global $DB;
261
 
262
        return $DB->get_record('external_services', ['id' => $serviceid], '*', MUST_EXIST);
263
    }
264
 
265
    /**
266
     * Get a service by its name.
267
     *
268
     * @param string $name The service name.
269
     * @return stdClass
270
     */
271
    public static function get_service_by_name(string $name): stdClass {
272
        global $DB;
273
 
274
        return $DB->get_record('external_services', ['name' => $name], '*', MUST_EXIST);
275
    }
276
 
277
    /**
278
     * Set the last time a token was sent and trigger the \core\event\webservice_token_sent event.
279
     *
280
     * This function is used when a token is generated by the user via login/token.php or admin/tool/mobile/launch.php.
281
     * In order to protect the privatetoken, we remove it from the event params.
282
     *
283
     * @param  stdClass $token token object
284
     */
285
    public static function log_token_request(stdClass $token): void {
286
        global $DB, $USER;
287
 
288
        $token->privatetoken = null;
289
 
290
        // Log token access.
291
        $DB->set_field('external_tokens', 'lastaccess', time(), ['id' => $token->id]);
292
 
293
        $event = \core\event\webservice_token_sent::create([
294
            'objectid' => $token->id,
295
        ]);
296
        $event->add_record_snapshot('external_tokens', $token);
297
        $event->trigger();
298
 
299
        // Check if we need to notify the user about the new login via token.
300
        $loginip = getremoteaddr();
301
        if ($USER->lastip === $loginip) {
302
            return;
303
        }
304
 
305
        $shouldskip = WS_SERVER || CLI_SCRIPT || !NO_MOODLE_COOKIES;
306
        if ($shouldskip && !PHPUNIT_TEST) {
307
            return;
308
        }
309
 
310
        // Schedule adhoc task to sent a login notification to the user.
311
        $task = new \core\task\send_login_notifications();
312
        $task->set_userid($USER->id);
313
        $logintime = time();
314
        $task->set_custom_data([
315
            'useragent' => \core_useragent::get_user_agent_string(),
316
            'ismoodleapp' => \core_useragent::is_moodle_app(),
317
            'loginip' => $loginip,
318
            'logintime' => $logintime,
319
        ]);
320
        $task->set_component('core');
321
        // We need sometime so the mobile app will send to Moodle the device information after login.
322
        $task->set_next_run_time(time() + (2 * MINSECS));
323
        \core\task\manager::reschedule_or_queue_adhoc_task($task);
324
    }
325
 
326
    /**
327
     * Generate or return an existing token for the current authenticated user.
328
     * This function is used for creating a valid token for users authenticathing via places, including:
329
     * - login/token.php
330
     * - admin/tool/mobile/launch.php.
331
     *
332
     * @param stdClass $service external service object
333
     * @return stdClass token object
334
     * @throws moodle_exception
335
     */
336
    public static function generate_token_for_current_user(stdClass $service) {
337
        global $DB, $USER, $CFG;
338
 
339
        core_user::require_active_user($USER, true, true);
340
 
341
        // Check if there is any required system capability.
342
        if ($service->requiredcapability && !has_capability($service->requiredcapability, context_system::instance())) {
343
            throw new moodle_exception('missingrequiredcapability', 'webservice', '', $service->requiredcapability);
344
        }
345
 
346
        // Specific checks related to user restricted service.
347
        if ($service->restrictedusers) {
348
            $authoriseduser = $DB->get_record('external_services_users', [
349
                'externalserviceid' => $service->id,
350
                'userid' => $USER->id,
351
            ]);
352
 
353
            if (empty($authoriseduser)) {
354
                throw new moodle_exception('usernotallowed', 'webservice', '', $service->shortname);
355
            }
356
 
357
            if (!empty($authoriseduser->validuntil) && $authoriseduser->validuntil < time()) {
358
                throw new moodle_exception('invalidtimedtoken', 'webservice');
359
            }
360
 
361
            if (!empty($authoriseduser->iprestriction) && !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) {
362
                throw new moodle_exception('invalidiptoken', 'webservice');
363
            }
364
        }
365
 
366
        // Check if a token has already been created for this user and this service.
367
        $conditions = [
368
            'userid' => $USER->id,
369
            'externalserviceid' => $service->id,
370
            'tokentype' => EXTERNAL_TOKEN_PERMANENT,
371
        ];
372
        $tokens = $DB->get_records('external_tokens', $conditions, 'timecreated ASC');
373
 
374
        // A bit of sanity checks.
375
        foreach ($tokens as $key => $token) {
376
            // Checks related to a specific token. (script execution continue).
377
            $unsettoken = false;
378
            // If sid is set then there must be a valid associated session no matter the token type.
379
            if (!empty($token->sid)) {
380
                if (!\core\session\manager::session_exists($token->sid)) {
381
                    // This token will never be valid anymore, delete it.
382
                    $DB->delete_records('external_tokens', ['sid' => $token->sid]);
383
                    $unsettoken = true;
384
                }
385
            }
386
 
387
            // Remove token is not valid anymore.
388
            if (!empty($token->validuntil) && $token->validuntil < time()) {
389
                $DB->delete_records('external_tokens', ['token' => $token->token, 'tokentype' => EXTERNAL_TOKEN_PERMANENT]);
390
                $unsettoken = true;
391
            }
392
 
393
            // Remove token if its IP is restricted.
394
            if (isset($token->iprestriction) && !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
395
                $unsettoken = true;
396
            }
397
 
398
            if ($unsettoken) {
399
                unset($tokens[$key]);
400
            }
401
        }
402
 
403
        // If some valid tokens exist then use the most recent.
404
        if (count($tokens) > 0) {
405
            $token = array_pop($tokens);
406
        } else {
407
            $context = context_system::instance();
408
            $isofficialservice = $service->shortname == MOODLE_OFFICIAL_MOBILE_SERVICE;
409
 
410
            if (
411
                ($isofficialservice && has_capability('moodle/webservice:createmobiletoken', $context)) ||
412
                (!is_siteadmin($USER) && has_capability('moodle/webservice:createtoken', $context))
413
            ) {
414
                // Create a new token.
415
                $token = new stdClass();
416
                $token->token = md5(uniqid((string) rand(), true));
417
                $token->userid = $USER->id;
418
                $token->tokentype = EXTERNAL_TOKEN_PERMANENT;
419
                $token->contextid = context_system::instance()->id;
420
                $token->creatorid = $USER->id;
421
                $token->timecreated = time();
422
                $token->externalserviceid = $service->id;
423
                // By default tokens are valid for 12 weeks.
424
                $token->validuntil = $token->timecreated + $CFG->tokenduration;
425
                $token->iprestriction = null;
426
                $token->sid = null;
427
                $token->lastaccess = null;
428
                $token->name = self::generate_token_name();
429
                // Generate the private token, it must be transmitted only via https.
430
                $token->privatetoken = random_string(64);
431
                $token->id = $DB->insert_record('external_tokens', $token);
432
 
433
                $eventtoken = clone $token;
434
                $eventtoken->privatetoken = null;
435
                $params = [
436
                    'objectid' => $eventtoken->id,
437
                    'relateduserid' => $USER->id,
438
                    'other' => [
439
                        'auto' => true,
440
                    ],
441
                ];
442
                $event = \core\event\webservice_token_created::create($params);
443
                $event->add_record_snapshot('external_tokens', $eventtoken);
444
                $event->trigger();
445
            } else {
446
                throw new moodle_exception('cannotcreatetoken', 'webservice', '', $service->shortname);
447
            }
448
        }
449
        return $token;
450
    }
451
 
452
    /**
453
     * Format the string to be returned properly as requested by the either the web service server,
454
     * either by an internally call.
455
     * The caller can change the format (raw) with the settings singleton
456
     * All web service servers must set this singleton when parsing the $_GET and $_POST.
457
     *
458
     * <pre>
459
     * Options are the same that in {@see format_string()} with some changes:
460
     *      filter      : Can be set to false to force filters off, else observes {@see settings}.
461
     * </pre>
462
     *
463
     * @param string|null $content The string to be filtered. Should be plain text, expect
464
     * possibly for multilang tags.
465
     * @param int|context $context The id of the context for the string or the context (affects filters).
466
     * @param boolean $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713
467
     * @param array $options options array/object or courseid
468
     * @return string text
469
     */
470
    public static function format_string(
471
        $content,
472
        $context,
473
        $striplinks = true,
474
        $options = []
475
    ) {
476
        if ($content === null || $content === '') {
477
            // Nothing to return.
478
            // Note: It's common for the DB to return null, so we allow format_string to take a null,
479
            // even though it is counter-intuitive.
480
            return '';
481
        }
482
 
483
        // Get settings (singleton).
484
        $settings = external_settings::get_instance();
485
 
486
        if (!$settings->get_raw()) {
487
            $options['context'] = $context;
488
            $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter();
489
            return format_string($content, $striplinks, $options);
490
        }
491
 
492
        return $content;
493
    }
494
 
495
    /**
496
     * Format the text to be returned properly as requested by the either the web service server,
497
     * either by an internally call.
498
     * The caller can change the format (raw, filter, file, fileurl) with the \core_external\settings singleton
499
     * All web service servers must set this singleton when parsing the $_GET and $_POST.
500
     *
501
     * <pre>
502
     * Options are the same that in {@see format_text()} with some changes in defaults to provide backwards compatibility:
503
     *      trusted     :   If true the string won't be cleaned. Default false.
504
     *      noclean     :   If true the string won't be cleaned only if trusted is also true. Default false.
505
     *      nocache     :   If true the string will not be cached and will be formatted every call. Default false.
506
     *      filter      :   Can be set to false to force filters off, else observes {@see \core_external\settings}.
507
     *      para        :   If true then the returned string will be wrapped in div tags.
508
     *                      Default (different from format_text) false.
509
     *                      Default changed because div tags are not commonly needed.
510
     *      newlines    :   If true then lines newline breaks will be converted to HTML newline breaks. Default true.
511
     *      context     :   Not used! Using contextid parameter instead.
512
     *      overflowdiv :   If set to true the formatted text will be encased in a div with the class no-overflow before being
513
     *                      returned. Default false.
514
     *      allowid     :   If true then id attributes will not be removed, even when using htmlpurifier. Default (different from
515
     *                      format_text) true. Default changed id attributes are commonly needed.
516
     *      blanktarget :   If true all <a> tags will have target="_blank" added unless target is explicitly specified.
517
     * </pre>
518
     *
519
     * @param string|null $text The content that may contain ULRs in need of rewriting.
520
     * @param string|int|null $textformat The text format.
521
     * @param context $context This parameter and the next two identify the file area to use.
522
     * @param string|null $component
523
     * @param string|null $filearea helps identify the file area.
524
     * @param int|string|null $itemid helps identify the file area.
525
     * @param array|stdClass|null $options text formatting options
526
     * @return array text + textformat
527
     */
528
    public static function format_text(
529
        $text,
530
        $textformat,
531
        $context,
532
        $component = null,
533
        $filearea = null,
534
        $itemid = null,
535
        $options = null
536
    ) {
537
        global $CFG;
538
 
539
        if ($text === null || $text === '') {
540
            // Nothing to return.
541
            // Note: It's common for the DB to return null, so we allow format_string to take nulls,
542
            // even though it is counter-intuitive.
543
            return ['', $textformat ?? FORMAT_MOODLE];
544
        }
545
 
546
        if (empty($itemid)) {
547
            $itemid = null;
548
        }
549
 
550
        // Get settings (singleton).
551
        $settings = external_settings::get_instance();
552
 
553
        if ($component && $filearea && $settings->get_fileurl()) {
554
            require_once($CFG->libdir . "/filelib.php");
555
            $text = file_rewrite_pluginfile_urls($text, $settings->get_file(), $context->id, $component, $filearea, $itemid);
556
        }
557
 
558
        // Note that $CFG->forceclean does not apply here if the client requests for the raw database content.
559
        // This is consistent with web clients that are still able to load non-cleaned text into editors, too.
560
 
561
        if (!$settings->get_raw()) {
562
            $options = (array) $options;
563
 
564
            // If context is passed in options, check that is the same to show a debug message.
565
            if (isset($options['context'])) {
566
                if (is_int($options['context'])) {
567
                    if ($options['context'] != $context->id) {
568
                        debugging(
569
                            'Different contexts found in external_format_text parameters. $options[\'context\'] not allowed. ' .
570
                            'Using $contextid parameter...',
571
                            DEBUG_DEVELOPER
572
                        );
573
                    }
574
                } else if ($options['context'] instanceof context) {
575
                    if ($options['context']->id != $context->id) {
576
                        debugging(
577
                            'Different contexts found in external_format_text parameters. $options[\'context\'] not allowed. ' .
578
                            'Using $contextid parameter...',
579
                            DEBUG_DEVELOPER
580
                        );
581
                    }
582
                }
583
            }
584
 
585
            $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter();
586
            $options['para'] = isset($options['para']) ? $options['para'] : false;
587
            $options['context'] = $context;
588
            $options['allowid'] = isset($options['allowid']) ? $options['allowid'] : true;
589
 
590
            $text = format_text($text, $textformat, $options);
591
            // Once converted to html (from markdown, plain... lets inform consumer this is already HTML).
592
            $textformat = FORMAT_HTML;
593
        }
594
 
595
        // Note: The formats defined in weblib are strings.
596
        return [$text, $textformat];
597
    }
598
 
599
    /**
600
     * Validate text field format against known FORMAT_XXX
601
     *
602
     * @param string $format the format to validate
603
     * @return string the validated format
604
     * @throws \moodle_exception
605
     * @since Moodle 2.3
606
     */
607
    public static function validate_format($format) {
608
        $allowedformats = [FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN];
609
        if (!in_array($format, $allowedformats)) {
610
            throw new moodle_exception(
611
                'formatnotsupported',
612
                'webservice',
613
                '',
614
                null,
615
                "The format with value={$format} is not supported by this Moodle site"
616
            );
617
        }
618
        return $format;
619
    }
620
 
621
    /**
622
     * Delete all pre-built services, related tokens, and external functions information defined for the specified component.
623
     *
624
     * @param string $component The frankenstyle component name
625
     */
626
    public static function delete_service_descriptions(string $component): void {
627
        global $DB;
628
 
629
        $params = [$component];
630
 
631
        $DB->delete_records_select(
632
            'external_tokens',
633
            "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)",
634
            $params
635
        );
636
        $DB->delete_records_select(
637
            'external_services_users',
638
            "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)",
639
            $params
640
        );
641
        $DB->delete_records_select(
642
            'external_services_functions',
643
            "functionname IN (SELECT name FROM {external_functions} WHERE component = ?)",
644
            $params
645
        );
646
        $DB->delete_records('external_services', ['component' => $component]);
647
        $DB->delete_records('external_functions', ['component' => $component]);
648
    }
649
 
650
    /**
651
     * Generate token name.
652
     *
653
     * @return string
654
     */
655
    public static function generate_token_name(): string {
656
        return get_string(
657
            'tokennameprefix',
658
            'webservice',
659
            random_string(5)
660
        );
661
    }
662
}