Proyectos de Subversion Moodle

Rev

Rev 11 | Rev 1327 | Ir a la última revisión | | Comparar con el anterior | 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
/**
18
 * moodlelib.php - Moodle main library
19
 *
20
 * Main library file of miscellaneous general-purpose Moodle functions.
21
 * Other main libraries:
22
 *  - weblib.php      - functions that produce web output
23
 *  - datalib.php     - functions that access the database
24
 *
25
 * @package    core
26
 * @subpackage lib
27
 * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
28
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29
 */
30
 
31
use core\di;
32
use core\hook;
33
 
34
defined('MOODLE_INTERNAL') || die();
35
 
36
// CONSTANTS (Encased in phpdoc proper comments).
37
 
38
// Date and time constants.
39
/**
40
 * Time constant - the number of seconds in a year
41
 */
42
define('YEARSECS', 31536000);
43
 
44
/**
45
 * Time constant - the number of seconds in a week
46
 */
47
define('WEEKSECS', 604800);
48
 
49
/**
50
 * Time constant - the number of seconds in a day
51
 */
52
define('DAYSECS', 86400);
53
 
54
/**
55
 * Time constant - the number of seconds in an hour
56
 */
57
define('HOURSECS', 3600);
58
 
59
/**
60
 * Time constant - the number of seconds in a minute
61
 */
62
define('MINSECS', 60);
63
 
64
/**
65
 * Time constant - the number of minutes in a day
66
 */
67
define('DAYMINS', 1440);
68
 
69
/**
70
 * Time constant - the number of minutes in an hour
71
 */
72
define('HOURMINS', 60);
73
 
74
// Parameter constants - every call to optional_param(), required_param()
75
// or clean_param() should have a specified type of parameter.
76
 
77
// We currently include \core\param manually here to avoid broken upgrades.
78
// This may change after the next LTS release as LTS releases require the previous LTS release.
79
require_once(__DIR__ . '/classes/deprecation.php');
80
require_once(__DIR__ . '/classes/param.php');
81
 
82
/**
83
 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
84
 */
85
define('PARAM_ALPHA', \core\param::ALPHA->value);
86
 
87
/**
88
 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed
89
 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
90
 */
91
define('PARAM_ALPHAEXT', \core\param::ALPHAEXT->value);
92
 
93
/**
94
 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
95
 */
96
define('PARAM_ALPHANUM', \core\param::ALPHANUM->value);
97
 
98
/**
99
 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
100
 */
101
define('PARAM_ALPHANUMEXT', \core\param::ALPHANUMEXT->value);
102
 
103
/**
104
 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
105
 */
106
define('PARAM_AUTH', \core\param::AUTH->value);
107
 
108
/**
109
 * PARAM_BASE64 - Base 64 encoded format
110
 */
111
define('PARAM_BASE64', \core\param::BASE64->value);
112
 
113
/**
114
 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
115
 */
116
define('PARAM_BOOL', \core\param::BOOL->value);
117
 
118
/**
119
 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
120
 * checked against the list of capabilities in the database.
121
 */
122
define('PARAM_CAPABILITY', \core\param::CAPABILITY->value);
123
 
124
/**
125
 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
126
 * to use this. The normal mode of operation is to use PARAM_RAW when receiving
127
 * the input (required/optional_param or formslib) and then sanitise the HTML
128
 * using format_text on output. This is for the rare cases when you want to
129
 * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
130
 */
131
define('PARAM_CLEANHTML', \core\param::CLEANHTML->value);
132
 
133
/**
134
 * PARAM_EMAIL - an email address following the RFC
135
 */
136
define('PARAM_EMAIL', \core\param::EMAIL->value);
137
 
138
/**
139
 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
140
 */
141
define('PARAM_FILE', \core\param::FILE->value);
142
 
143
/**
144
 * PARAM_FLOAT - a real/floating point number.
145
 *
146
 * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
147
 * It does not work for languages that use , as a decimal separator.
148
 * Use PARAM_LOCALISEDFLOAT instead.
149
 */
150
define('PARAM_FLOAT', \core\param::FLOAT->value);
151
 
152
/**
153
 * PARAM_LOCALISEDFLOAT - a localised real/floating point number.
154
 * This is preferred over PARAM_FLOAT for numbers typed in by the user.
155
 * Cleans localised numbers to computer readable numbers; false for invalid numbers.
156
 */
157
define('PARAM_LOCALISEDFLOAT', \core\param::LOCALISEDFLOAT->value);
158
 
159
/**
160
 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
161
 */
162
define('PARAM_HOST', \core\param::HOST->value);
163
 
164
/**
165
 * PARAM_INT - integers only, use when expecting only numbers.
166
 */
167
define('PARAM_INT', \core\param::INT->value);
168
 
169
/**
170
 * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
171
 */
172
define('PARAM_LANG', \core\param::LANG->value);
173
 
174
/**
175
 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
176
 * others! Implies PARAM_URL!)
177
 */
178
define('PARAM_LOCALURL', \core\param::LOCALURL->value);
179
 
180
/**
181
 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
182
 */
183
define('PARAM_NOTAGS', \core\param::NOTAGS->value);
184
 
185
/**
186
 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
187
 * traversals note: the leading slash is not removed, window drive letter is not allowed
188
 */
189
define('PARAM_PATH', \core\param::PATH->value);
190
 
191
/**
192
 * PARAM_PEM - Privacy Enhanced Mail format
193
 */
194
define('PARAM_PEM', \core\param::PEM->value);
195
 
196
/**
197
 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
198
 */
199
define('PARAM_PERMISSION', \core\param::PERMISSION->value);
200
 
201
/**
202
 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
203
 */
204
define('PARAM_RAW', \core\param::RAW->value);
205
 
206
/**
207
 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
208
 */
209
define('PARAM_RAW_TRIMMED', \core\param::RAW_TRIMMED->value);
210
 
211
/**
212
 * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
213
 */
214
define('PARAM_SAFEDIR', \core\param::SAFEDIR->value);
215
 
216
/**
217
 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
218
 * and other references to Moodle code files.
219
 *
220
 * This is NOT intended to be used for absolute paths or any user uploaded files.
221
 */
222
define('PARAM_SAFEPATH', \core\param::SAFEPATH->value);
223
 
224
/**
225
 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9.  Numbers and comma only.
226
 */
227
define('PARAM_SEQUENCE', \core\param::SEQUENCE->value);
228
 
229
/**
230
 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
231
 */
232
define('PARAM_TAG', \core\param::TAG->value);
233
 
234
/**
235
 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
236
 */
237
define('PARAM_TAGLIST', \core\param::TAGLIST->value);
238
 
239
/**
240
 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
241
 */
242
define('PARAM_TEXT', \core\param::TEXT->value);
243
 
244
/**
245
 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
246
 */
247
define('PARAM_THEME', \core\param::THEME->value);
248
 
249
/**
250
 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
251
 * http://localhost.localdomain/ is ok.
252
 */
253
define('PARAM_URL', \core\param::URL->value);
254
 
255
/**
256
 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
257
 * accounts, do NOT use when syncing with external systems!!
258
 */
259
define('PARAM_USERNAME', \core\param::USERNAME->value);
260
 
261
/**
262
 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
263
 */
264
define('PARAM_STRINGID', \core\param::STRINGID->value);
265
 
266
// DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
267
/**
268
 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
269
 * It was one of the first types, that is why it is abused so much ;-)
270
 * @deprecated since 2.0
271
 */
272
define('PARAM_CLEAN', \core\param::CLEAN->value);
273
 
274
/**
275
 * PARAM_INTEGER - deprecated alias for PARAM_INT
276
 * @deprecated since 2.0
277
 */
278
define('PARAM_INTEGER', \core\param::INT->value);
279
 
280
/**
281
 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
282
 * @deprecated since 2.0
283
 */
284
define('PARAM_NUMBER', \core\param::FLOAT->value);
285
 
286
/**
287
 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
288
 * NOTE: originally alias for PARAM_APLHA
289
 * @deprecated since 2.0
290
 */
291
define('PARAM_ACTION', \core\param::ALPHANUMEXT->value);
292
 
293
/**
294
 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
295
 * NOTE: originally alias for PARAM_APLHA
296
 * @deprecated since 2.0
297
 */
298
define('PARAM_FORMAT', \core\param::ALPHANUMEXT->value);
299
 
300
/**
301
 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
302
 * @deprecated since 2.0
303
 */
304
define('PARAM_MULTILANG', \core\param::TEXT->value);
305
 
306
/**
307
 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
308
 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
309
 * America/Port-au-Prince)
310
 */
311
define('PARAM_TIMEZONE', \core\param::TIMEZONE->value);
312
 
313
/**
314
 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
315
 * @deprecated since 2.0
316
 */
317
define('PARAM_CLEANFILE', \core\param::CLEANFILE->value);
318
 
319
/**
320
 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
321
 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
322
 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
323
 * NOTE: numbers and underscores are strongly discouraged in plugin names!
324
 */
325
define('PARAM_COMPONENT', \core\param::COMPONENT->value);
326
 
327
/**
328
 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
329
 * It is usually used together with context id and component.
330
 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
331
 */
332
define('PARAM_AREA', \core\param::AREA->value);
333
 
334
/**
335
 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
336
 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
337
 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
338
 */
339
define('PARAM_PLUGIN', \core\param::PLUGIN->value);
340
 
341
 
342
// Web Services.
343
 
344
/**
345
 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
346
 */
347
define('VALUE_REQUIRED', 1);
348
 
349
/**
350
 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
351
 */
352
define('VALUE_OPTIONAL', 2);
353
 
354
/**
355
 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
356
 */
357
define('VALUE_DEFAULT', 0);
358
 
359
/**
360
 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
361
 */
362
define('NULL_NOT_ALLOWED', false);
363
 
364
/**
365
 * NULL_ALLOWED - the parameter can be set to null in the database
366
 */
367
define('NULL_ALLOWED', true);
368
 
369
// Page types.
370
 
371
/**
372
 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
373
 */
374
define('PAGE_COURSE_VIEW', 'course-view');
375
 
376
/** Get remote addr constant */
377
define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
378
/** Get remote addr constant */
379
define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
380
/**
381
 * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
382
 */
1326 ariadna 383
define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR | GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
1 efrain 384
 
385
// Blog access level constant declaration.
1326 ariadna 386
define('BLOG_USER_LEVEL', 1);
387
define('BLOG_GROUP_LEVEL', 2);
388
define('BLOG_COURSE_LEVEL', 3);
389
define('BLOG_SITE_LEVEL', 4);
390
define('BLOG_GLOBAL_LEVEL', 5);
1 efrain 391
 
392
 
393
// Tag constants.
394
/**
395
 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
396
 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
397
 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
398
 *
399
 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
400
 */
401
define('TAG_MAX_LENGTH', 50);
402
 
403
// Password policy constants.
1326 ariadna 404
define('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
405
define('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
406
define('PASSWORD_DIGITS', '0123456789');
407
define('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
1 efrain 408
 
409
/**
410
 * Required password pepper entropy.
411
 */
1326 ariadna 412
define('PEPPER_ENTROPY', 112);
1 efrain 413
 
414
// Feature constants.
415
// Used for plugin_supports() to report features that are, or are not, supported by a module.
416
 
417
/** True if module can provide a grade */
418
define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
419
/** True if module supports outcomes */
420
define('FEATURE_GRADE_OUTCOMES', 'outcomes');
421
/** True if module supports advanced grading methods */
422
define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
423
/** True if module controls the grade visibility over the gradebook */
424
define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
425
/** True if module supports plagiarism plugins */
426
define('FEATURE_PLAGIARISM', 'plagiarism');
427
 
428
/** True if module has code to track whether somebody viewed it */
429
define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
430
/** True if module has custom completion rules */
431
define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
432
 
433
/** True if module has no 'view' page (like label) */
434
define('FEATURE_NO_VIEW_LINK', 'viewlink');
435
/** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
436
define('FEATURE_IDNUMBER', 'idnumber');
437
/** True if module supports groups */
438
define('FEATURE_GROUPS', 'groups');
439
/** True if module supports groupings */
440
define('FEATURE_GROUPINGS', 'groupings');
441
/**
442
 * True if module supports groupmembersonly (which no longer exists)
443
 * @deprecated Since Moodle 2.8
444
 */
445
define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
446
 
447
/** Type of module */
448
define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
449
/** True if module supports intro editor */
450
define('FEATURE_MOD_INTRO', 'mod_intro');
451
/** True if module has default completion */
452
define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
453
 
454
define('FEATURE_COMMENT', 'comment');
455
 
456
define('FEATURE_RATE', 'rate');
457
/** True if module supports backup/restore of moodle2 format */
458
define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
459
 
460
/** True if module can show description on course main page */
461
define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
462
 
463
/** True if module uses the question bank */
464
define('FEATURE_USES_QUESTIONS', 'usesquestions');
465
 
466
/**
467
 * Maximum filename char size
468
 */
469
define('MAX_FILENAME_SIZE', 100);
470
 
471
/** Unspecified module archetype */
472
define('MOD_ARCHETYPE_OTHER', 0);
473
/** Resource-like type module */
474
define('MOD_ARCHETYPE_RESOURCE', 1);
475
/** Assignment module archetype */
476
define('MOD_ARCHETYPE_ASSIGNMENT', 2);
477
/** System (not user-addable) module archetype */
478
define('MOD_ARCHETYPE_SYSTEM', 3);
479
 
480
/** Type of module */
481
define('FEATURE_MOD_PURPOSE', 'mod_purpose');
482
/** Module purpose administration */
483
define('MOD_PURPOSE_ADMINISTRATION', 'administration');
484
/** Module purpose assessment */
485
define('MOD_PURPOSE_ASSESSMENT', 'assessment');
486
/** Module purpose communication */
487
define('MOD_PURPOSE_COLLABORATION', 'collaboration');
488
/** Module purpose communication */
489
define('MOD_PURPOSE_COMMUNICATION', 'communication');
490
/** Module purpose content */
491
define('MOD_PURPOSE_CONTENT', 'content');
492
/** Module purpose interactive content */
493
define('MOD_PURPOSE_INTERACTIVECONTENT', 'interactivecontent');
494
/** Module purpose other */
495
define('MOD_PURPOSE_OTHER', 'other');
496
/**
497
 * Module purpose interface
498
 * @deprecated since Moodle 4.4
499
 * @todo MDL-80701 Remove in Moodle 4.8
1326 ariadna 500
 */
1 efrain 501
define('MOD_PURPOSE_INTERFACE', 'interface');
502
 
503
/**
504
 * Security token used for allowing access
505
 * from external application such as web services.
506
 * Scripts do not use any session, performance is relatively
507
 * low because we need to load access info in each request.
508
 * Scripts are executed in parallel.
509
 */
510
define('EXTERNAL_TOKEN_PERMANENT', 0);
511
 
512
/**
513
 * Security token used for allowing access
514
 * of embedded applications, the code is executed in the
515
 * active user session. Token is invalidated after user logs out.
516
 * Scripts are executed serially - normal session locking is used.
517
 */
518
define('EXTERNAL_TOKEN_EMBEDDED', 1);
519
 
520
/**
521
 * The home page should be the site home
522
 */
523
define('HOMEPAGE_SITE', 0);
524
/**
525
 * The home page should be the users my page
526
 */
527
define('HOMEPAGE_MY', 1);
528
/**
529
 * The home page can be chosen by the user
530
 */
531
define('HOMEPAGE_USER', 2);
532
/**
533
 * The home page should be the users my courses page
534
 */
535
define('HOMEPAGE_MYCOURSES', 3);
536
 
537
/**
538
 * URL of the Moodle sites registration portal.
539
 */
540
defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
541
 
542
/**
543
 * URL of main Moodle site for marketing, products and services.
544
 */
545
defined('MOODLE_PRODUCTURL') || define('MOODLE_PRODUCTURL', 'https://moodle.com');
546
 
547
/**
548
 * URL of the statistic server public key.
549
 */
550
defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem');
551
 
552
/**
553
 * Moodle mobile app service name
554
 */
555
define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
556
 
557
/**
558
 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
559
 */
560
define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
561
 
562
/**
563
 * Course display settings: display all sections on one page.
564
 */
565
define('COURSE_DISPLAY_SINGLEPAGE', 0);
566
/**
567
 * Course display settings: split pages into a page per section.
568
 */
569
define('COURSE_DISPLAY_MULTIPAGE', 1);
570
 
571
/**
572
 * Authentication constant: String used in password field when password is not stored.
573
 */
574
define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
575
 
576
/**
577
 * Email from header to never include via information.
578
 */
579
define('EMAIL_VIA_NEVER', 0);
580
 
581
/**
582
 * Email from header to always include via information.
583
 */
584
define('EMAIL_VIA_ALWAYS', 1);
585
 
586
/**
587
 * Email from header to only include via information if the address is no-reply.
588
 */
589
define('EMAIL_VIA_NO_REPLY_ONLY', 2);
590
 
591
/**
592
 * Contact site support form/link disabled.
593
 */
594
define('CONTACT_SUPPORT_DISABLED', 0);
595
 
596
/**
597
 * Contact site support form/link only available to authenticated users.
598
 */
599
define('CONTACT_SUPPORT_AUTHENTICATED', 1);
600
 
601
/**
602
 * Contact site support form/link available to anyone visiting the site.
603
 */
604
define('CONTACT_SUPPORT_ANYONE', 2);
605
 
606
/**
607
 * Maximum number of characters for password.
608
 */
609
define('MAX_PASSWORD_CHARACTERS', 128);
610
 
611
/**
612
 * Toggle sensitive feature is disabled. Used for sensitive inputs (passwords, tokens, keys).
613
 */
614
define('TOGGLE_SENSITIVE_DISABLED', 0);
615
 
616
/**
617
 * Toggle sensitive feature is enabled. Used for sensitive inputs (passwords, tokens, keys).
618
 */
619
define('TOGGLE_SENSITIVE_ENABLED', 1);
620
 
621
/**
622
 * Toggle sensitive feature is enabled for small screens only. Used for sensitive inputs (passwords, tokens, keys).
623
 */
624
define('TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY', 2);
625
 
626
// PARAMETER HANDLING.
627
 
628
/**
629
 * Returns a particular value for the named variable, taken from
630
 * POST or GET.  If the parameter doesn't exist then an error is
631
 * thrown because we require this variable.
632
 *
633
 * This function should be used to initialise all required values
634
 * in a script that are based on parameters.  Usually it will be
635
 * used like this:
636
 *    $id = required_param('id', PARAM_INT);
637
 *
638
 * Please note the $type parameter is now required and the value can not be array.
639
 *
640
 * @param string $parname the name of the page parameter we want
641
 * @param string $type expected type of parameter
642
 * @return mixed
643
 * @throws coding_exception
644
 */
1326 ariadna 645
function required_param($parname, $type)
646
{
1 efrain 647
    return \core\param::from_type($type)->required_param($parname);
648
}
649
 
650
/**
651
 * Returns a particular array value for the named variable, taken from
652
 * POST or GET.  If the parameter doesn't exist then an error is
653
 * thrown because we require this variable.
654
 *
655
 * This function should be used to initialise all required values
656
 * in a script that are based on parameters.  Usually it will be
657
 * used like this:
658
 *    $ids = required_param_array('ids', PARAM_INT);
659
 *
660
 *  Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
661
 *
662
 * @param string $parname the name of the page parameter we want
663
 * @param string $type expected type of parameter
664
 * @return array
665
 * @throws coding_exception
666
 */
1326 ariadna 667
function required_param_array($parname, $type)
668
{
1 efrain 669
    return \core\param::from_type($type)->required_param_array($parname);
670
}
671
 
672
/**
673
 * Returns a particular value for the named variable, taken from
674
 * POST or GET, otherwise returning a given default.
675
 *
676
 * This function should be used to initialise all optional values
677
 * in a script that are based on parameters.  Usually it will be
678
 * used like this:
679
 *    $name = optional_param('name', 'Fred', PARAM_TEXT);
680
 *
681
 * Please note the $type parameter is now required and the value can not be array.
682
 *
683
 * @param string $parname the name of the page parameter we want
684
 * @param mixed  $default the default value to return if nothing is found
685
 * @param string $type expected type of parameter
686
 * @return mixed
687
 * @throws coding_exception
688
 */
1326 ariadna 689
function optional_param($parname, $default, $type)
690
{
1 efrain 691
    return \core\param::from_type($type)->optional_param(
692
        paramname: $parname,
693
        default: $default,
694
    );
695
}
696
 
697
/**
698
 * Returns a particular array value for the named variable, taken from
699
 * POST or GET, otherwise returning a given default.
700
 *
701
 * This function should be used to initialise all optional values
702
 * in a script that are based on parameters.  Usually it will be
703
 * used like this:
704
 *    $ids = optional_param('id', array(), PARAM_INT);
705
 *
706
 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
707
 *
708
 * @param string $parname the name of the page parameter we want
709
 * @param mixed $default the default value to return if nothing is found
710
 * @param string $type expected type of parameter
711
 * @return array
712
 * @throws coding_exception
713
 */
1326 ariadna 714
function optional_param_array($parname, $default, $type)
715
{
1 efrain 716
    return \core\param::from_type($type)->optional_param_array(
717
        paramname: $parname,
718
        default: $default,
719
    );
720
}
721
 
722
/**
723
 * Strict validation of parameter values, the values are only converted
724
 * to requested PHP type. Internally it is using clean_param, the values
725
 * before and after cleaning must be equal - otherwise
726
 * an invalid_parameter_exception is thrown.
727
 * Objects and classes are not accepted.
728
 *
729
 * @param mixed $param
730
 * @param string $type PARAM_ constant
731
 * @param bool $allownull are nulls valid value?
732
 * @param string $debuginfo optional debug information
733
 * @return mixed the $param value converted to PHP type
734
 * @throws invalid_parameter_exception if $param is not of given type
735
 */
1326 ariadna 736
function validate_param($param, $type, $allownull = NULL_NOT_ALLOWED, $debuginfo = '')
737
{
1 efrain 738
    return \core\param::from_type($type)->validate_param(
739
        param: $param,
740
        allownull: $allownull,
741
        debuginfo: $debuginfo,
742
    );
743
}
744
 
745
/**
746
 * Makes sure array contains only the allowed types, this function does not validate array key names!
747
 *
748
 * <code>
749
 * $options = clean_param($options, PARAM_INT);
750
 * </code>
751
 *
752
 * @param array|null $param the variable array we are cleaning
753
 * @param string $type expected format of param after cleaning.
754
 * @param bool $recursive clean recursive arrays
755
 * @return array
756
 * @throws coding_exception
757
 */
1326 ariadna 758
function clean_param_array(?array $param, $type, $recursive = false)
759
{
1 efrain 760
    return \core\param::from_type($type)->clean_param_array(
761
        param: $param,
762
        recursive: $recursive,
763
    );
764
}
765
 
766
/**
767
 * Used by {@link optional_param()} and {@link required_param()} to
768
 * clean the variables and/or cast to specific types, based on
769
 * an options field.
770
 * <code>
771
 * $course->format = clean_param($course->format, PARAM_ALPHA);
772
 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
773
 * </code>
774
 *
775
 * @param mixed $param the variable we are cleaning
776
 * @param string $type expected format of param after cleaning.
777
 * @return mixed
778
 * @throws coding_exception
779
 */
1326 ariadna 780
function clean_param($param, $type)
781
{
1 efrain 782
    return \core\param::from_type($type)->clean($param);
783
}
784
 
785
/**
786
 * Whether the PARAM_* type is compatible in RTL.
787
 *
788
 * Being compatible with RTL means that the data they contain can flow
789
 * from right-to-left or left-to-right without compromising the user experience.
790
 *
791
 * Take URLs for example, they are not RTL compatible as they should always
792
 * flow from the left to the right. This also applies to numbers, email addresses,
793
 * configuration snippets, base64 strings, etc...
794
 *
795
 * This function tries to best guess which parameters can contain localised strings.
796
 *
797
 * @param string $paramtype Constant PARAM_*.
798
 * @return bool
799
 */
1326 ariadna 800
function is_rtl_compatible($paramtype)
801
{
1 efrain 802
    return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
803
}
804
 
805
/**
806
 * Makes sure the data is using valid utf8, invalid characters are discarded.
807
 *
808
 * Note: this function is not intended for full objects with methods and private properties.
809
 *
810
 * @param mixed $value
811
 * @return mixed with proper utf-8 encoding
812
 */
1326 ariadna 813
function fix_utf8($value)
814
{
1 efrain 815
    if (is_null($value) or $value === '') {
816
        return $value;
817
    } else if (is_string($value)) {
818
        if ((string)(int)$value === $value) {
819
            // Shortcut.
820
            return $value;
821
        }
822
 
823
        // Remove null bytes or invalid Unicode sequences from value.
824
        $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
825
 
826
        // Note: this duplicates min_fix_utf8() intentionally.
827
        static $buggyiconv = null;
828
        if ($buggyiconv === null) {
1326 ariadna 829
            $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100' . chr(130) . '€') !== '100€');
1 efrain 830
        }
831
 
832
        if ($buggyiconv) {
833
            if (function_exists('mb_convert_encoding')) {
834
                $subst = mb_substitute_character();
835
                mb_substitute_character('none');
836
                $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
837
                mb_substitute_character($subst);
838
            } else {
839
                // Warn admins on admin/index.php page.
840
                $result = $value;
841
            }
842
        } else {
843
            $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
844
        }
845
 
846
        return $result;
847
    } else if (is_array($value)) {
848
        foreach ($value as $k => $v) {
849
            $value[$k] = fix_utf8($v);
850
        }
851
        return $value;
852
    } else if (is_object($value)) {
853
        // Do not modify original.
1326 ariadna 854
        $value = clone ($value);
1 efrain 855
        foreach ($value as $k => $v) {
856
            $value->$k = fix_utf8($v);
857
        }
858
        return $value;
859
    } else {
860
        // This is some other type, no utf-8 here.
861
        return $value;
862
    }
863
}
864
 
865
/**
866
 * Return true if given value is integer or string with integer value
867
 *
868
 * @param mixed $value String or Int
869
 * @return bool true if number, false if not
870
 */
1326 ariadna 871
function is_number($value)
872
{
1 efrain 873
    if (is_int($value)) {
874
        return true;
875
    } else if (is_string($value)) {
876
        return ((string)(int)$value) === $value;
877
    } else {
878
        return false;
879
    }
880
}
881
 
882
/**
883
 * Returns host part from url.
884
 *
885
 * @param string $url full url
886
 * @return string host, null if not found
887
 */
1326 ariadna 888
function get_host_from_url($url)
889
{
1 efrain 890
    preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
891
    if ($matches) {
892
        return $matches[1];
893
    }
894
    return null;
895
}
896
 
897
/**
898
 * Tests whether anything was returned by text editor
899
 *
900
 * This function is useful for testing whether something you got back from
901
 * the HTML editor actually contains anything. Sometimes the HTML editor
902
 * appear to be empty, but actually you get back a <br> tag or something.
903
 *
904
 * @param string $string a string containing HTML.
905
 * @return boolean does the string contain any actual content - that is text,
906
 * images, objects, etc.
907
 */
1326 ariadna 908
function html_is_blank($string)
909
{
1 efrain 910
    return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == '';
911
}
912
 
913
/**
914
 * Set a key in global configuration
915
 *
916
 * Set a key/value pair in both this session's {@link $CFG} global variable
917
 * and in the 'config' database table for future sessions.
918
 *
919
 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
920
 * In that case it doesn't affect $CFG.
921
 *
922
 * A NULL value will delete the entry.
923
 *
924
 * NOTE: this function is called from lib/db/upgrade.php
925
 *
926
 * @param string $name the key to set
927
 * @param string|int|bool|null $value the value to set (without magic quotes),
928
 *               null to unset the value
929
 * @param string $plugin (optional) the plugin scope, default null
930
 * @return bool true or exception
931
 */
1326 ariadna 932
function set_config($name, $value, $plugin = null)
933
{
1 efrain 934
    global $CFG, $DB;
935
 
936
    // Redirect to appropriate handler when value is null.
937
    if ($value === null) {
938
        return unset_config($name, $plugin);
939
    }
940
 
941
    // Set variables determining conditions and where to store the new config.
942
    // Plugin config goes to {config_plugins}, core config goes to {config}.
943
    $iscore = empty($plugin);
944
    if ($iscore) {
945
        // If it's for core config.
946
        $table = 'config';
947
        $conditions = ['name' => $name];
948
        $invalidatecachekey = 'core';
949
    } else {
950
        // If it's a plugin.
951
        $table = 'config_plugins';
952
        $conditions = ['name' => $name, 'plugin' => $plugin];
953
        $invalidatecachekey = $plugin;
954
    }
955
 
956
    // DB handling - checks for existing config, updating or inserting only if necessary.
957
    $invalidatecache = true;
958
    $inserted = false;
959
    $record = $DB->get_record($table, $conditions, 'id, value');
960
    if ($record === false) {
961
        // Inserts a new config record.
962
        $config = new stdClass();
963
        $config->name  = $name;
964
        $config->value = $value;
965
        if (!$iscore) {
966
            $config->plugin = $plugin;
967
        }
968
        $inserted = $DB->insert_record($table, $config, false);
969
    } else if ($invalidatecache = ($record->value !== $value)) {
970
        // Record exists - Check and only set new value if it has changed.
971
        $DB->set_field($table, 'value', $value, ['id' => $record->id]);
972
    }
973
 
974
    if ($iscore && !isset($CFG->config_php_settings[$name])) {
975
        // So it's defined for this invocation at least.
976
        // Settings from db are always strings.
977
        $CFG->$name = (string) $value;
978
    }
979
 
980
    // When setting config during a Behat test (in the CLI script, not in the web browser
981
    // requests), remember which ones are set so that we can clear them later.
982
    if ($iscore && $inserted && defined('BEHAT_TEST')) {
983
        $CFG->behat_cli_added_config[$name] = true;
984
    }
985
 
986
    // Update siteidentifier cache, if required.
987
    if ($iscore && $name === 'siteidentifier') {
988
        cache_helper::update_site_identifier($value);
989
    }
990
 
991
    // Invalidate cache, if required.
992
    if ($invalidatecache) {
993
        cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
994
    }
995
 
996
    return true;
997
}
998
 
999
/**
1000
 * Get configuration values from the global config table
1001
 * or the config_plugins table.
1002
 *
1003
 * If called with one parameter, it will load all the config
1004
 * variables for one plugin, and return them as an object.
1005
 *
1006
 * If called with 2 parameters it will return a string single
1007
 * value or false if the value is not found.
1008
 *
1009
 * NOTE: this function is called from lib/db/upgrade.php
1010
 *
1011
 * @param string $plugin full component name
1012
 * @param string $name default null
1013
 * @return mixed hash-like object or single value, return false no config found
1014
 * @throws dml_exception
1015
 */
1326 ariadna 1016
function get_config($plugin, $name = null)
1017
{
1 efrain 1018
    global $CFG, $DB;
1019
 
1020
    if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1326 ariadna 1021
        $forced = &$CFG->config_php_settings;
1 efrain 1022
        $iscore = true;
1023
        $plugin = 'core';
1024
    } else {
1025
        if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1326 ariadna 1026
            $forced = &$CFG->forced_plugin_settings[$plugin];
1 efrain 1027
        } else {
1028
            $forced = array();
1029
        }
1030
        $iscore = false;
1031
    }
1032
 
1033
    if (!isset($CFG->siteidentifier)) {
1034
        try {
1035
            // This may throw an exception during installation, which is how we detect the
1036
            // need to install the database. For more details see {@see initialise_cfg()}.
1037
            $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1038
        } catch (dml_exception $ex) {
1039
            // Set siteidentifier to false. We don't want to trip this continually.
1040
            $siteidentifier = false;
1041
            throw $ex;
1042
        }
1043
    }
1044
 
1045
    if (!empty($name)) {
1046
        if (array_key_exists($name, $forced)) {
1047
            return (string)$forced[$name];
1048
        } else if ($name === 'siteidentifier' && $plugin == 'core') {
1049
            return $CFG->siteidentifier;
1050
        }
1051
    }
1052
 
1053
    $cache = cache::make('core', 'config');
1054
    $result = $cache->get($plugin);
1055
    if ($result === false) {
1056
        // The user is after a recordset.
1057
        if (!$iscore) {
1058
            $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1059
        } else {
1060
            // This part is not really used any more, but anyway...
1061
            $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1062
        }
1063
        $cache->set($plugin, $result);
1064
    }
1065
 
1066
    if (!empty($name)) {
1067
        if (array_key_exists($name, $result)) {
1068
            return $result[$name];
1069
        }
1070
        return false;
1071
    }
1072
 
1073
    if ($plugin === 'core') {
1074
        $result['siteidentifier'] = $CFG->siteidentifier;
1075
    }
1076
 
1077
    foreach ($forced as $key => $value) {
1078
        if (is_null($value) or is_array($value) or is_object($value)) {
1079
            // We do not want any extra mess here, just real settings that could be saved in db.
1080
            unset($result[$key]);
1081
        } else {
1082
            // Convert to string as if it went through the DB.
1083
            $result[$key] = (string)$value;
1084
        }
1085
    }
1086
 
1087
    return (object)$result;
1088
}
1089
 
1090
/**
1091
 * Removes a key from global configuration.
1092
 *
1093
 * NOTE: this function is called from lib/db/upgrade.php
1094
 *
1095
 * @param string $name the key to set
1096
 * @param string $plugin (optional) the plugin scope
1097
 * @return boolean whether the operation succeeded.
1098
 */
1326 ariadna 1099
function unset_config($name, $plugin = null)
1100
{
1 efrain 1101
    global $CFG, $DB;
1102
 
1103
    if (empty($plugin)) {
1104
        unset($CFG->$name);
1105
        $DB->delete_records('config', array('name' => $name));
1106
        cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1107
    } else {
1108
        $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1109
        cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1110
    }
1111
 
1112
    return true;
1113
}
1114
 
1115
/**
1116
 * Remove all the config variables for a given plugin.
1117
 *
1118
 * NOTE: this function is called from lib/db/upgrade.php
1119
 *
1120
 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1121
 * @return boolean whether the operation succeeded.
1122
 */
1326 ariadna 1123
function unset_all_config_for_plugin($plugin)
1124
{
1 efrain 1125
    global $DB;
1126
    // Delete from the obvious config_plugins first.
1127
    $DB->delete_records('config_plugins', array('plugin' => $plugin));
1128
    // Next delete any suspect settings from config.
1129
    $like = $DB->sql_like('name', '?', true, true, false, '|');
1326 ariadna 1130
    $params = array($DB->sql_like_escape($plugin . '_', '|') . '%');
1 efrain 1131
    $DB->delete_records_select('config', $like, $params);
1132
    // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1133
    cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1134
 
1135
    return true;
1136
}
1137
 
1138
/**
1139
 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1140
 *
1141
 * All users are verified if they still have the necessary capability.
1142
 *
1143
 * @param string $value the value of the config setting.
1144
 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1145
 * @param bool $includeadmins include administrators.
1146
 * @return array of user objects.
1147
 */
1326 ariadna 1148
function get_users_from_config($value, $capability, $includeadmins = true)
1149
{
1 efrain 1150
    if (empty($value) or $value === '$@NONE@$') {
1151
        return array();
1152
    }
1153
 
1154
    // We have to make sure that users still have the necessary capability,
1155
    // it should be faster to fetch them all first and then test if they are present
1156
    // instead of validating them one-by-one.
1157
    $users = get_users_by_capability(context_system::instance(), $capability);
1158
    if ($includeadmins) {
1159
        $admins = get_admins();
1160
        foreach ($admins as $admin) {
1161
            $users[$admin->id] = $admin;
1162
        }
1163
    }
1164
 
1165
    if ($value === '$@ALL@$') {
1166
        return $users;
1167
    }
1168
 
1169
    $result = array(); // Result in correct order.
1170
    $allowed = explode(',', $value);
1171
    foreach ($allowed as $uid) {
1172
        if (isset($users[$uid])) {
1173
            $user = $users[$uid];
1174
            $result[$user->id] = $user;
1175
        }
1176
    }
1177
 
1178
    return $result;
1179
}
1180
 
1181
 
1182
/**
1183
 * Invalidates browser caches and cached data in temp.
1184
 *
1185
 * @return void
1186
 */
1326 ariadna 1187
function purge_all_caches()
1188
{
1 efrain 1189
    purge_caches();
1190
}
1191
 
1192
/**
1193
 * Selectively invalidate different types of cache.
1194
 *
1195
 * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
1196
 * areas alone or in combination.
1197
 *
1198
 * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1199
 *        'muc'    Purge MUC caches?
11 efrain 1200
 *        'courses' Purge all course caches, or specific course caches (CLI only)
1 efrain 1201
 *        'theme'  Purge theme cache?
1202
 *        'lang'   Purge language string cache?
1203
 *        'js'     Purge javascript cache?
1204
 *        'filter' Purge text filter cache?
1205
 *        'other'  Purge all other caches?
1206
 */
1326 ariadna 1207
function purge_caches($options = [])
1208
{
1 efrain 1209
    $defaults = array_fill_keys(['muc', 'courses', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1210
    if (empty(array_filter($options))) {
1211
        $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1212
    } else {
1213
        $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1214
    }
1215
    if ($options['muc']) {
1216
        cache_helper::purge_all();
11 efrain 1217
    } else if ($options['courses']) {
1 efrain 1218
        if ($options['courses'] === true) {
1219
            $courseids = [];
1220
        } else {
1221
            $courseids = preg_split('/\s*,\s*/', $options['courses'], -1, PREG_SPLIT_NO_EMPTY);
1222
        }
1223
        course_modinfo::purge_course_caches($courseids);
1224
    }
1225
    if ($options['theme']) {
1226
        theme_reset_all_caches();
1227
    }
1228
    if ($options['lang']) {
1229
        get_string_manager()->reset_caches();
1230
    }
1231
    if ($options['js']) {
1232
        js_reset_all_caches();
1233
    }
1234
    if ($options['template']) {
1235
        template_reset_all_caches();
1236
    }
1237
    if ($options['filter']) {
1238
        reset_text_filters_cache();
1239
    }
1240
    if ($options['other']) {
1241
        purge_other_caches();
1242
    }
1243
}
1244
 
1245
/**
1246
 * Purge all non-MUC caches not otherwise purged in purge_caches.
1247
 *
1248
 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1249
 * {@link phpunit_util::reset_dataroot()}
1250
 */
1326 ariadna 1251
function purge_other_caches()
1252
{
1 efrain 1253
    global $DB, $CFG;
1254
    if (class_exists('core_plugin_manager')) {
1255
        core_plugin_manager::reset_caches();
1256
    }
1257
 
1258
    // Bump up cacherev field for all courses.
1259
    try {
1260
        increment_revision_number('course', 'cacherev', '');
1261
    } catch (moodle_exception $e) {
1262
        // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1263
    }
1264
 
1265
    $DB->reset_caches();
1266
 
1267
    // Purge all other caches: rss, simplepie, etc.
1268
    clearstatcache();
1326 ariadna 1269
    remove_dir($CFG->cachedir . '', true);
1 efrain 1270
 
1271
    // Make sure cache dir is writable, throws exception if not.
1272
    make_cache_directory('');
1273
 
1274
    // This is the only place where we purge local caches, we are only adding files there.
1275
    // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1276
    remove_dir($CFG->localcachedir, true);
1277
    set_config('localcachedirpurged', time());
1278
    make_localcache_directory('', true);
11 efrain 1279
 
1280
    // Rewarm the bootstrap.php files so the siteid is always present after a purge.
1281
    initialise_local_config_cache();
1 efrain 1282
    \core\task\manager::clear_static_caches();
1283
}
1284
 
1285
/**
1286
 * Get volatile flags
1287
 *
1288
 * @param string $type
1289
 * @param int $changedsince default null
1290
 * @return array records array
1291
 */
1326 ariadna 1292
function get_cache_flags($type, $changedsince = null)
1293
{
1 efrain 1294
    global $DB;
1295
 
1296
    $params = array('type' => $type, 'expiry' => time());
1297
    $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1298
    if ($changedsince !== null) {
1299
        $params['changedsince'] = $changedsince;
1300
        $sqlwhere .= " AND timemodified > :changedsince";
1301
    }
1302
    $cf = array();
1303
    if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1304
        foreach ($flags as $flag) {
1305
            $cf[$flag->name] = $flag->value;
1306
        }
1307
    }
1308
    return $cf;
1309
}
1310
 
1311
/**
1312
 * Get volatile flags
1313
 *
1314
 * @param string $type
1315
 * @param string $name
1316
 * @param int $changedsince default null
1317
 * @return string|false The cache flag value or false
1318
 */
1326 ariadna 1319
function get_cache_flag($type, $name, $changedsince = null)
1320
{
1 efrain 1321
    global $DB;
1322
 
1323
    $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1324
 
1325
    $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1326
    if ($changedsince !== null) {
1327
        $params['changedsince'] = $changedsince;
1328
        $sqlwhere .= " AND timemodified > :changedsince";
1329
    }
1330
 
1331
    return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1332
}
1333
 
1334
/**
1335
 * Set a volatile flag
1336
 *
1337
 * @param string $type the "type" namespace for the key
1338
 * @param string $name the key to set
1339
 * @param string $value the value to set (without magic quotes) - null will remove the flag
1340
 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1341
 * @return bool Always returns true
1342
 */
1326 ariadna 1343
function set_cache_flag($type, $name, $value, $expiry = null)
1344
{
1 efrain 1345
    global $DB;
1346
 
1347
    $timemodified = time();
1348
    if ($expiry === null || $expiry < $timemodified) {
1349
        $expiry = $timemodified + 24 * 60 * 60;
1350
    } else {
1351
        $expiry = (int)$expiry;
1352
    }
1353
 
1354
    if ($value === null) {
1355
        unset_cache_flag($type, $name);
1356
        return true;
1357
    }
1358
 
1359
    if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1360
        // This is a potential problem in DEBUG_DEVELOPER.
1361
        if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1362
            return true; // No need to update.
1363
        }
1364
        $f->value        = $value;
1365
        $f->expiry       = $expiry;
1366
        $f->timemodified = $timemodified;
1367
        $DB->update_record('cache_flags', $f);
1368
    } else {
1369
        $f = new stdClass();
1370
        $f->flagtype     = $type;
1371
        $f->name         = $name;
1372
        $f->value        = $value;
1373
        $f->expiry       = $expiry;
1374
        $f->timemodified = $timemodified;
1375
        $DB->insert_record('cache_flags', $f);
1376
    }
1377
    return true;
1378
}
1379
 
1380
/**
1381
 * Removes a single volatile flag
1382
 *
1383
 * @param string $type the "type" namespace for the key
1384
 * @param string $name the key to set
1385
 * @return bool
1386
 */
1326 ariadna 1387
function unset_cache_flag($type, $name)
1388
{
1 efrain 1389
    global $DB;
1390
    $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1391
    return true;
1392
}
1393
 
1394
/**
1395
 * Garbage-collect volatile flags
1396
 *
1397
 * @return bool Always returns true
1398
 */
1326 ariadna 1399
function gc_cache_flags()
1400
{
1 efrain 1401
    global $DB;
1402
    $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1403
    return true;
1404
}
1405
 
1406
// USER PREFERENCE API.
1407
 
1408
/**
1409
 * Refresh user preference cache. This is used most often for $USER
1410
 * object that is stored in session, but it also helps with performance in cron script.
1411
 *
1412
 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1413
 *
1414
 * @package  core
1415
 * @category preference
1416
 * @access   public
1417
 * @param    stdClass         $user          User object. Preferences are preloaded into 'preference' property
1418
 * @param    int              $cachelifetime Cache life time on the current page (in seconds)
1419
 * @throws   coding_exception
1420
 * @return   null
1421
 */
1326 ariadna 1422
function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120)
1423
{
1 efrain 1424
    global $DB;
1425
    // Static cache, we need to check on each page load, not only every 2 minutes.
1426
    static $loadedusers = array();
1427
 
1428
    if (!isset($user->id)) {
1429
        throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1430
    }
1431
 
1432
    if (empty($user->id) or isguestuser($user->id)) {
1433
        // No permanent storage for not-logged-in users and guest.
1434
        if (!isset($user->preference)) {
1435
            $user->preference = array();
1436
        }
1437
        return;
1438
    }
1439
 
1440
    $timenow = time();
1441
 
1442
    if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1443
        // Already loaded at least once on this page. Are we up to date?
1444
        if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1445
            // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1446
            return;
1447
        } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1448
            // No change since the lastcheck on this page.
1449
            $user->preference['_lastloaded'] = $timenow;
1450
            return;
1451
        }
1452
    }
1453
 
1454
    // OK, so we have to reload all preferences.
1455
    $loadedusers[$user->id] = true;
1456
    $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1457
    $user->preference['_lastloaded'] = $timenow;
1458
}
1459
 
1460
/**
1461
 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1462
 *
1463
 * NOTE: internal function, do not call from other code.
1464
 *
1465
 * @package core
1466
 * @access private
1467
 * @param integer $userid the user whose prefs were changed.
1468
 */
1326 ariadna 1469
function mark_user_preferences_changed($userid)
1470
{
1 efrain 1471
    global $CFG;
1472
 
1473
    if (empty($userid) or isguestuser($userid)) {
1474
        // No cache flags for guest and not-logged-in users.
1475
        return;
1476
    }
1477
 
1478
    set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1479
}
1480
 
1481
/**
1482
 * Sets a preference for the specified user.
1483
 *
1484
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1485
 *
1486
 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1487
 *
1488
 * @package  core
1489
 * @category preference
1490
 * @access   public
1491
 * @param    string            $name  The key to set as preference for the specified user
1492
 * @param    string|int|bool|null $value The value to set for the $name key in the specified user's
1493
 *                                    record, null means delete current value.
1494
 * @param    stdClass|int|null $user  A moodle user object or id, null means current user
1495
 * @throws   coding_exception
1496
 * @return   bool                     Always true or exception
1497
 */
1326 ariadna 1498
function set_user_preference($name, $value, $user = null)
1499
{
1 efrain 1500
    global $USER, $DB;
1501
 
1502
    if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1503
        throw new coding_exception('Invalid preference name in set_user_preference() call');
1504
    }
1505
 
1506
    if (is_null($value)) {
1507
        // Null means delete current.
1508
        return unset_user_preference($name, $user);
1509
    } else if (is_object($value)) {
1510
        throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1511
    } else if (is_array($value)) {
1512
        throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1513
    }
1514
    // Value column maximum length is 1333 characters.
1515
    $value = (string)$value;
1516
    if (core_text::strlen($value) > 1333) {
1517
        throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1518
    }
1519
 
1520
    if (is_null($user)) {
1521
        $user = $USER;
1522
    } else if (isset($user->id)) {
1523
        // It is a valid object.
1524
    } else if (is_numeric($user)) {
1525
        $user = (object)array('id' => (int)$user);
1526
    } else {
1527
        throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1528
    }
1529
 
1530
    check_user_preferences_loaded($user);
1531
 
1532
    if (empty($user->id) or isguestuser($user->id)) {
1533
        // No permanent storage for not-logged-in users and guest.
1534
        $user->preference[$name] = $value;
1535
        return true;
1536
    }
1537
 
1538
    if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1539
        if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1540
            // Preference already set to this value.
1541
            return true;
1542
        }
1543
        $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1544
    } else {
1545
        $preference = new stdClass();
1546
        $preference->userid = $user->id;
1547
        $preference->name   = $name;
1548
        $preference->value  = $value;
1549
        $DB->insert_record('user_preferences', $preference);
1550
    }
1551
 
1552
    // Update value in cache.
1553
    $user->preference[$name] = $value;
1554
    // Update the $USER in case where we've not a direct reference to $USER.
1555
    if ($user !== $USER && $user->id == $USER->id) {
1556
        $USER->preference[$name] = $value;
1557
    }
1558
 
1559
    // Set reload flag for other sessions.
1560
    mark_user_preferences_changed($user->id);
1561
 
1562
    return true;
1563
}
1564
 
1565
/**
1566
 * Sets a whole array of preferences for the current user
1567
 *
1568
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1569
 *
1570
 * @package  core
1571
 * @category preference
1572
 * @access   public
1573
 * @param    array             $prefarray An array of key/value pairs to be set
1574
 * @param    stdClass|int|null $user      A moodle user object or id, null means current user
1575
 * @return   bool                         Always true or exception
1576
 */
1326 ariadna 1577
function set_user_preferences(array $prefarray, $user = null)
1578
{
1 efrain 1579
    foreach ($prefarray as $name => $value) {
1580
        set_user_preference($name, $value, $user);
1581
    }
1582
    return true;
1583
}
1584
 
1585
/**
1586
 * Unsets a preference completely by deleting it from the database
1587
 *
1588
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1589
 *
1590
 * @package  core
1591
 * @category preference
1592
 * @access   public
1593
 * @param    string            $name The key to unset as preference for the specified user
1594
 * @param    stdClass|int|null $user A moodle user object or id, null means current user
1595
 * @throws   coding_exception
1596
 * @return   bool                    Always true or exception
1597
 */
1326 ariadna 1598
function unset_user_preference($name, $user = null)
1599
{
1 efrain 1600
    global $USER, $DB;
1601
 
1602
    if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1603
        throw new coding_exception('Invalid preference name in unset_user_preference() call');
1604
    }
1605
 
1606
    if (is_null($user)) {
1607
        $user = $USER;
1608
    } else if (isset($user->id)) {
1609
        // It is a valid object.
1610
    } else if (is_numeric($user)) {
1611
        $user = (object)array('id' => (int)$user);
1612
    } else {
1613
        throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1614
    }
1615
 
1616
    check_user_preferences_loaded($user);
1617
 
1618
    if (empty($user->id) or isguestuser($user->id)) {
1619
        // No permanent storage for not-logged-in user and guest.
1620
        unset($user->preference[$name]);
1621
        return true;
1622
    }
1623
 
1624
    // Delete from DB.
1625
    $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
1626
 
1627
    // Delete the preference from cache.
1628
    unset($user->preference[$name]);
1629
    // Update the $USER in case where we've not a direct reference to $USER.
1630
    if ($user !== $USER && $user->id == $USER->id) {
1631
        unset($USER->preference[$name]);
1632
    }
1633
 
1634
    // Set reload flag for other sessions.
1635
    mark_user_preferences_changed($user->id);
1636
 
1637
    return true;
1638
}
1639
 
1640
/**
1641
 * Used to fetch user preference(s)
1642
 *
1643
 * If no arguments are supplied this function will return
1644
 * all of the current user preferences as an array.
1645
 *
1646
 * If a name is specified then this function
1647
 * attempts to return that particular preference value.  If
1648
 * none is found, then the optional value $default is returned,
1649
 * otherwise null.
1650
 *
1651
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1652
 *
1653
 * @package  core
1654
 * @category preference
1655
 * @access   public
1656
 * @param    string            $name    Name of the key to use in finding a preference value
1657
 * @param    mixed|null        $default Value to be returned if the $name key is not set in the user preferences
1658
 * @param    stdClass|int|null $user    A moodle user object or id, null means current user
1659
 * @throws   coding_exception
1660
 * @return   string|mixed|null          A string containing the value of a single preference. An
1661
 *                                      array with all of the preferences or null
1662
 */
1326 ariadna 1663
function get_user_preferences($name = null, $default = null, $user = null)
1664
{
1 efrain 1665
    global $USER;
1666
 
1667
    if (is_null($name)) {
1668
        // All prefs.
1669
    } else if (is_numeric($name) or $name === '_lastloaded') {
1670
        throw new coding_exception('Invalid preference name in get_user_preferences() call');
1671
    }
1672
 
1673
    if (is_null($user)) {
1674
        $user = $USER;
1675
    } else if (isset($user->id)) {
1676
        // Is a valid object.
1677
    } else if (is_numeric($user)) {
1678
        if ($USER->id == $user) {
1679
            $user = $USER;
1680
        } else {
1681
            $user = (object)array('id' => (int)$user);
1682
        }
1683
    } else {
1684
        throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1685
    }
1686
 
1687
    check_user_preferences_loaded($user);
1688
 
1689
    if (empty($name)) {
1690
        // All values.
1691
        return $user->preference;
1692
    } else if (isset($user->preference[$name])) {
1693
        // The single string value.
1694
        return $user->preference[$name];
1695
    } else {
1696
        // Default value (null if not specified).
1697
        return $default;
1698
    }
1699
}
1700
 
1701
// FUNCTIONS FOR HANDLING TIME.
1702
 
1703
/**
1704
 * Given Gregorian date parts in user time produce a GMT timestamp.
1705
 *
1706
 * @package core
1707
 * @category time
1708
 * @param int $year The year part to create timestamp of
1709
 * @param int $month The month part to create timestamp of
1710
 * @param int $day The day part to create timestamp of
1711
 * @param int $hour The hour part to create timestamp of
1712
 * @param int $minute The minute part to create timestamp of
1713
 * @param int $second The second part to create timestamp of
1714
 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
1715
 *             if 99 then default user's timezone is used {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1716
 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
1717
 *             applied only if timezone is 99 or string.
1718
 * @return int GMT timestamp
1719
 */
1326 ariadna 1720
function make_timestamp($year, $month = 1, $day = 1, $hour = 0, $minute = 0, $second = 0, $timezone = 99, $applydst = true)
1721
{
1 efrain 1722
    $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
1723
    $date->setDate((int)$year, (int)$month, (int)$day);
1724
    $date->setTime((int)$hour, (int)$minute, (int)$second);
1725
 
1726
    $time = $date->getTimestamp();
1727
 
1728
    if ($time === false) {
1326 ariadna 1729
        throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.' .
1 efrain 1730
            ' This can fail if year is more than 2038 and OS is 32 bit windows');
1731
    }
1732
 
1733
    // Moodle BC DST stuff.
1734
    if (!$applydst) {
1735
        $time += dst_offset_on($time, $timezone);
1736
    }
1737
 
1738
    return $time;
1739
}
1740
 
1741
/**
1742
 * Format a date/time (seconds) as weeks, days, hours etc as needed
1743
 *
1744
 * Given an amount of time in seconds, returns string
1745
 * formatted nicely as years, days, hours etc as needed
1746
 *
1747
 * @package core
1748
 * @category time
1749
 * @uses MINSECS
1750
 * @uses HOURSECS
1751
 * @uses DAYSECS
1752
 * @uses YEARSECS
1753
 * @param int $totalsecs Time in seconds
1754
 * @param stdClass $str Should be a time object
1755
 * @return string A nicely formatted date/time string
1756
 */
1326 ariadna 1757
function format_time($totalsecs, $str = null)
1758
{
1 efrain 1759
 
1760
    $totalsecs = abs($totalsecs);
1761
 
1762
    if (!$str) {
1763
        // Create the str structure the slow way.
1764
        $str = new stdClass();
1765
        $str->day   = get_string('day');
1766
        $str->days  = get_string('days');
1767
        $str->hour  = get_string('hour');
1768
        $str->hours = get_string('hours');
1769
        $str->min   = get_string('min');
1770
        $str->mins  = get_string('mins');
1771
        $str->sec   = get_string('sec');
1772
        $str->secs  = get_string('secs');
1773
        $str->year  = get_string('year');
1774
        $str->years = get_string('years');
1775
    }
1776
 
1326 ariadna 1777
    $years     = floor($totalsecs / YEARSECS);
1778
    $remainder = $totalsecs - ($years * YEARSECS);
1779
    $days      = floor($remainder / DAYSECS);
1780
    $remainder = $totalsecs - ($days * DAYSECS);
1781
    $hours     = floor($remainder / HOURSECS);
1782
    $remainder = $remainder - ($hours * HOURSECS);
1783
    $mins      = floor($remainder / MINSECS);
1784
    $secs      = $remainder - ($mins * MINSECS);
1 efrain 1785
 
1786
    $ss = ($secs == 1)  ? $str->sec  : $str->secs;
1787
    $sm = ($mins == 1)  ? $str->min  : $str->mins;
1788
    $sh = ($hours == 1) ? $str->hour : $str->hours;
1789
    $sd = ($days == 1)  ? $str->day  : $str->days;
1790
    $sy = ($years == 1)  ? $str->year  : $str->years;
1791
 
1792
    $oyears = '';
1793
    $odays = '';
1794
    $ohours = '';
1795
    $omins = '';
1796
    $osecs = '';
1797
 
1798
    if ($years) {
1326 ariadna 1799
        $oyears  = $years . ' ' . $sy;
1 efrain 1800
    }
1801
    if ($days) {
1326 ariadna 1802
        $odays  = $days . ' ' . $sd;
1 efrain 1803
    }
1804
    if ($hours) {
1326 ariadna 1805
        $ohours = $hours . ' ' . $sh;
1 efrain 1806
    }
1807
    if ($mins) {
1326 ariadna 1808
        $omins  = $mins . ' ' . $sm;
1 efrain 1809
    }
1810
    if ($secs) {
1326 ariadna 1811
        $osecs  = $secs . ' ' . $ss;
1 efrain 1812
    }
1813
 
1814
    if ($years) {
1326 ariadna 1815
        return trim($oyears . ' ' . $odays);
1 efrain 1816
    }
1817
    if ($days) {
1326 ariadna 1818
        return trim($odays . ' ' . $ohours);
1 efrain 1819
    }
1820
    if ($hours) {
1326 ariadna 1821
        return trim($ohours . ' ' . $omins);
1 efrain 1822
    }
1823
    if ($mins) {
1326 ariadna 1824
        return trim($omins . ' ' . $osecs);
1 efrain 1825
    }
1826
    if ($secs) {
1827
        return $osecs;
1828
    }
1829
    return get_string('now');
1830
}
1831
 
1832
/**
1833
 * Returns a formatted string that represents a date in user time.
1834
 *
1835
 * @package core
1836
 * @category time
1837
 * @param int $date the timestamp in UTC, as obtained from the database.
1838
 * @param string $format strftime format. You should probably get this using
1839
 *        get_string('strftime...', 'langconfig');
1840
 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1841
 *        not 99 then daylight saving will not be added.
1842
 *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1843
 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1844
 *        If false then the leading zero is maintained.
1845
 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1846
 * @return string the formatted date/time.
1847
 */
1326 ariadna 1848
function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true)
1849
{
1 efrain 1850
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
1851
    return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
1852
}
1853
 
1854
/**
1855
 * Returns a html "time" tag with both the exact user date with timezone information
1856
 * as a datetime attribute in the W3C format, and the user readable date and time as text.
1857
 *
1858
 * @package core
1859
 * @category time
1860
 * @param int $date the timestamp in UTC, as obtained from the database.
1861
 * @param string $format strftime format. You should probably get this using
1862
 *        get_string('strftime...', 'langconfig');
1863
 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1864
 *        not 99 then daylight saving will not be added.
1865
 *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1866
 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1867
 *        If false then the leading zero is maintained.
1868
 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1869
 * @return string the formatted date/time.
1870
 */
1326 ariadna 1871
function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true)
1872
{
1 efrain 1873
    $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
1874
    if (CLI_SCRIPT && !PHPUNIT_TEST) {
1875
        return $userdatestr;
1876
    }
1877
    $machinedate = new DateTime();
1878
    $machinedate->setTimestamp(intval($date));
1879
    $machinedate->setTimezone(core_date::get_user_timezone_object());
1880
 
1881
    return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
1882
}
1883
 
1884
/**
1885
 * Returns a formatted date ensuring it is UTF-8.
1886
 *
1887
 * If we are running under Windows convert to Windows encoding and then back to UTF-8
1888
 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
1889
 *
1890
 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
1891
 * @param string $format strftime format.
1892
 * @param int|float|string $tz the user timezone
1893
 * @return string the formatted date/time.
1894
 * @since Moodle 2.3.3
1895
 */
1326 ariadna 1896
function date_format_string($date, $format, $tz = 99)
1897
{
1 efrain 1898
 
1899
    date_default_timezone_set(core_date::get_user_timezone($tz));
1900
 
1901
    if (date('A', 0) === date('A', HOURSECS * 18)) {
1902
        $datearray = getdate($date);
1903
        $format = str_replace([
1904
            '%P',
1905
            '%p',
1906
        ], [
1907
            $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
1908
            $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
1909
        ], $format);
1910
    }
1911
 
1912
    $datestring = core_date::strftime($format, $date);
1913
    core_date::set_default_server_timezone();
1914
 
1915
    return $datestring;
1916
}
1917
 
1918
/**
1919
 * Given a $time timestamp in GMT (seconds since epoch),
1920
 * returns an array that represents the Gregorian date in user time
1921
 *
1922
 * @package core
1923
 * @category time
1924
 * @param int $time Timestamp in GMT
1925
 * @param float|int|string $timezone user timezone
1926
 * @return array An array that represents the date in user time
1927
 */
1326 ariadna 1928
function usergetdate($time, $timezone = 99)
1929
{
1 efrain 1930
    if ($time === null) {
1931
        // PHP8 and PHP7 return different results when getdate(null) is called.
1932
        // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
1933
        // In the future versions of Moodle we may consider adding a strict typehint.
1934
        debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER);
1935
        $time = 0;
1936
    }
1937
 
1938
    date_default_timezone_set(core_date::get_user_timezone($timezone));
1939
    $result = getdate($time);
1940
    core_date::set_default_server_timezone();
1941
 
1942
    return $result;
1943
}
1944
 
1945
/**
1946
 * Given a GMT timestamp (seconds since epoch), offsets it by
1947
 * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
1948
 *
1949
 * NOTE: this function does not include DST properly,
1950
 *       you should use the PHP date stuff instead!
1951
 *
1952
 * @package core
1953
 * @category time
1954
 * @param int $date Timestamp in GMT
1955
 * @param float|int|string $timezone user timezone
1956
 * @return int
1957
 */
1326 ariadna 1958
function usertime($date, $timezone = 99)
1959
{
1 efrain 1960
    $userdate = new DateTime('@' . $date);
1961
    $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
1962
    $dst = dst_offset_on($date, $timezone);
1963
 
1964
    return $date - $userdate->getOffset() + $dst;
1965
}
1966
 
1967
/**
1968
 * Get a formatted string representation of an interval between two unix timestamps.
1969
 *
1970
 * E.g.
1971
 * $intervalstring = get_time_interval_string(12345600, 12345660);
1972
 * Will produce the string:
1973
 * '0d 0h 1m'
1974
 *
1975
 * @param int $time1 unix timestamp
1976
 * @param int $time2 unix timestamp
1977
 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
1978
 * @param bool $dropzeroes If format is not provided and this is set to true, do not include zero time units.
1979
 *                         e.g. a duration of 3 days and 2 hours will be displayed as '3d 2h' instead of '3d 2h 0s'
1980
 * @param bool $fullformat If format is not provided and this is set to true, display time units in full format.
1981
 *                         e.g. instead of showing "3d", "3 days" will be returned.
1982
 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
1983
 */
1326 ariadna 1984
function get_time_interval_string(
1985
    int $time1,
1986
    int $time2,
1987
    string $format = '',
1988
    bool $dropzeroes = false,
1989
    bool $fullformat = false
1990
): string {
1 efrain 1991
    $dtdate = new DateTime();
1992
    $dtdate->setTimeStamp($time1);
1993
    $dtdate2 = new DateTime();
1994
    $dtdate2->setTimeStamp($time2);
1995
    $interval = $dtdate2->diff($dtdate);
1996
 
1997
    if (empty(trim($format))) {
1998
        // Default to this key.
1999
        $formatkey = 'dateintervaldayhrmin';
2000
 
2001
        if ($dropzeroes) {
2002
            $units = [
2003
                'y' => 'yr',
2004
                'm' => 'mo',
2005
                'd' => 'day',
2006
                'h' => 'hr',
2007
                'i' => 'min',
2008
                's' => 'sec',
2009
            ];
2010
            $formatunits = [];
2011
            foreach ($units as $key => $unit) {
2012
                if (empty($interval->$key)) {
2013
                    continue;
2014
                }
2015
                $formatunits[] = $unit;
2016
            }
2017
            if (!empty($formatunits)) {
2018
                $formatkey = 'dateinterval' . implode("", $formatunits);
2019
            }
2020
        }
2021
 
2022
        if ($fullformat) {
2023
            $formatkey .= 'full';
2024
        }
2025
        $format = get_string($formatkey, 'langconfig');
2026
    }
2027
    return $interval->format($format);
2028
}
2029
 
2030
/**
2031
 * Given a time, return the GMT timestamp of the most recent midnight
2032
 * for the current user.
2033
 *
2034
 * @package core
2035
 * @category time
2036
 * @param int $date Timestamp in GMT
2037
 * @param float|int|string $timezone user timezone
2038
 * @return int Returns a GMT timestamp
2039
 */
1326 ariadna 2040
function usergetmidnight($date, $timezone = 99)
2041
{
1 efrain 2042
 
2043
    $userdate = usergetdate($date, $timezone);
2044
 
2045
    // Time of midnight of this user's day, in GMT.
2046
    return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2047
}
2048
 
2049
/**
2050
 * Returns a string that prints the user's timezone
2051
 *
2052
 * @package core
2053
 * @category time
2054
 * @param float|int|string $timezone user timezone
2055
 * @return string
2056
 */
1326 ariadna 2057
function usertimezone($timezone = 99)
2058
{
1 efrain 2059
    $tz = core_date::get_user_timezone($timezone);
2060
    return core_date::get_localised_timezone($tz);
2061
}
2062
 
2063
/**
2064
 * Returns a float or a string which denotes the user's timezone
2065
 * A float value means that a simple offset from GMT is used, while a string (it will be the name of a timezone in the database)
2066
 * means that for this timezone there are also DST rules to be taken into account
2067
 * Checks various settings and picks the most dominant of those which have a value
2068
 *
2069
 * @package core
2070
 * @category time
2071
 * @param float|int|string $tz timezone to calculate GMT time offset before
2072
 *        calculating user timezone, 99 is default user timezone
2073
 *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2074
 * @return float|string
2075
 */
1326 ariadna 2076
function get_user_timezone($tz = 99)
2077
{
1 efrain 2078
    global $USER, $CFG;
2079
 
2080
    $timezones = array(
2081
        $tz,
2082
        isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2083
        isset($USER->timezone) ? $USER->timezone : 99,
2084
        isset($CFG->timezone) ? $CFG->timezone : 99,
1326 ariadna 2085
    );
1 efrain 2086
 
2087
    $tz = 99;
2088
 
2089
    // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2090
    foreach ($timezones as $nextvalue) {
2091
        if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2092
            $tz = $nextvalue;
2093
        }
2094
    }
2095
    return is_numeric($tz) ? (float) $tz : $tz;
2096
}
2097
 
2098
/**
2099
 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2100
 * - Note: Daylight saving only works for string timezones and not for float.
2101
 *
2102
 * @package core
2103
 * @category time
2104
 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2105
 * @param int|float|string $strtimezone user timezone
2106
 * @return int
2107
 */
1326 ariadna 2108
function dst_offset_on($time, $strtimezone = null)
2109
{
1 efrain 2110
    $tz = core_date::get_user_timezone($strtimezone);
2111
    $date = new DateTime('@' . $time);
2112
    $date->setTimezone(new DateTimeZone($tz));
2113
    if ($date->format('I') == '1') {
2114
        if ($tz === 'Australia/Lord_Howe') {
2115
            return 1800;
2116
        }
2117
        return 3600;
2118
    }
2119
    return 0;
2120
}
2121
 
2122
/**
2123
 * Calculates when the day appears in specific month
2124
 *
2125
 * @package core
2126
 * @category time
2127
 * @param int $startday starting day of the month
2128
 * @param int $weekday The day when week starts (normally taken from user preferences)
2129
 * @param int $month The month whose day is sought
2130
 * @param int $year The year of the month whose day is sought
2131
 * @return int
2132
 */
1326 ariadna 2133
function find_day_in_month($startday, $weekday, $month, $year)
2134
{
1 efrain 2135
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2136
 
2137
    $daysinmonth = days_in_month($month, $year);
2138
    $daysinweek = count($calendartype->get_weekdays());
2139
 
2140
    if ($weekday == -1) {
2141
        // Don't care about weekday, so return:
2142
        //    abs($startday) if $startday != -1
2143
        //    $daysinmonth otherwise.
2144
        return ($startday == -1) ? $daysinmonth : abs($startday);
2145
    }
2146
 
2147
    // From now on we 're looking for a specific weekday.
2148
    // Give "end of month" its actual value, since we know it.
2149
    if ($startday == -1) {
2150
        $startday = -1 * $daysinmonth;
2151
    }
2152
 
2153
    // Starting from day $startday, the sign is the direction.
2154
    if ($startday < 1) {
2155
        $startday = abs($startday);
2156
        $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2157
 
2158
        // This is the last such weekday of the month.
2159
        $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2160
        if ($lastinmonth > $daysinmonth) {
2161
            $lastinmonth -= $daysinweek;
2162
        }
2163
 
2164
        // Find the first such weekday <= $startday.
2165
        while ($lastinmonth > $startday) {
2166
            $lastinmonth -= $daysinweek;
2167
        }
2168
 
2169
        return $lastinmonth;
2170
    } else {
2171
        $indexweekday = dayofweek($startday, $month, $year);
2172
 
2173
        $diff = $weekday - $indexweekday;
2174
        if ($diff < 0) {
2175
            $diff += $daysinweek;
2176
        }
2177
 
2178
        // This is the first such weekday of the month equal to or after $startday.
2179
        $firstfromindex = $startday + $diff;
2180
 
2181
        return $firstfromindex;
2182
    }
2183
}
2184
 
2185
/**
2186
 * Calculate the number of days in a given month
2187
 *
2188
 * @package core
2189
 * @category time
2190
 * @param int $month The month whose day count is sought
2191
 * @param int $year The year of the month whose day count is sought
2192
 * @return int
2193
 */
1326 ariadna 2194
function days_in_month($month, $year)
2195
{
1 efrain 2196
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2197
    return $calendartype->get_num_days_in_month($year, $month);
2198
}
2199
 
2200
/**
2201
 * Calculate the position in the week of a specific calendar day
2202
 *
2203
 * @package core
2204
 * @category time
2205
 * @param int $day The day of the date whose position in the week is sought
2206
 * @param int $month The month of the date whose position in the week is sought
2207
 * @param int $year The year of the date whose position in the week is sought
2208
 * @return int
2209
 */
1326 ariadna 2210
function dayofweek($day, $month, $year)
2211
{
1 efrain 2212
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2213
    return $calendartype->get_weekday($year, $month, $day);
2214
}
2215
 
2216
// USER AUTHENTICATION AND LOGIN.
2217
 
2218
/**
2219
 * Returns full login url.
2220
 *
2221
 * Any form submissions for authentication to this URL must include username,
2222
 * password as well as a logintoken generated by \core\session\manager::get_login_token().
2223
 *
2224
 * @return string login url
2225
 */
1326 ariadna 2226
function get_login_url()
2227
{
1 efrain 2228
    global $CFG;
2229
 
2230
    return "$CFG->wwwroot/login/index.php";
2231
}
2232
 
2233
/**
2234
 * This function checks that the current user is logged in and has the
2235
 * required privileges
2236
 *
2237
 * This function checks that the current user is logged in, and optionally
2238
 * whether they are allowed to be in a particular course and view a particular
2239
 * course module.
2240
 * If they are not logged in, then it redirects them to the site login unless
2241
 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2242
 * case they are automatically logged in as guests.
2243
 * If $courseid is given and the user is not enrolled in that course then the
2244
 * user is redirected to the course enrolment page.
2245
 * If $cm is given and the course module is hidden and the user is not a teacher
2246
 * in the course then the user is redirected to the course home page.
2247
 *
2248
 * When $cm parameter specified, this function sets page layout to 'module'.
2249
 * You need to change it manually later if some other layout needed.
2250
 *
2251
 * @package    core_access
2252
 * @category   access
2253
 *
2254
 * @param mixed $courseorid id of the course or course object
2255
 * @param bool $autologinguest default true
2256
 * @param object $cm course module object
2257
 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2258
 *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2259
 *             in order to keep redirects working properly. MDL-14495
2260
 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2261
 * @return mixed Void, exit, and die depending on path
2262
 * @throws coding_exception
2263
 * @throws require_login_exception
2264
 * @throws moodle_exception
2265
 */
1326 ariadna 2266
function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false)
2267
{
1 efrain 2268
    global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2269
 
2270
    // Must not redirect when byteserving already started.
2271
    if (!empty($_SERVER['HTTP_RANGE'])) {
2272
        $preventredirect = true;
2273
    }
2274
 
2275
    if (AJAX_SCRIPT) {
2276
        // We cannot redirect for AJAX scripts either.
2277
        $preventredirect = true;
2278
    }
2279
 
2280
    // Setup global $COURSE, themes, language and locale.
2281
    if (!empty($courseorid)) {
2282
        if (is_object($courseorid)) {
2283
            $course = $courseorid;
2284
        } else if ($courseorid == SITEID) {
1326 ariadna 2285
            $course = clone ($SITE);
1 efrain 2286
        } else {
2287
            $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2288
        }
2289
        if ($cm) {
2290
            if ($cm->course != $course->id) {
2291
                throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2292
            }
2293
            // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2294
            if (!($cm instanceof cm_info)) {
2295
                // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2296
                // db queries so this is not really a performance concern, however it is obviously
2297
                // better if you use get_fast_modinfo to get the cm before calling this.
2298
                $modinfo = get_fast_modinfo($course);
2299
                $cm = $modinfo->get_cm($cm->id);
2300
            }
2301
        }
2302
    } else {
2303
        // Do not touch global $COURSE via $PAGE->set_course(),
2304
        // the reasons is we need to be able to call require_login() at any time!!
2305
        $course = $SITE;
2306
        if ($cm) {
2307
            throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2308
        }
2309
    }
2310
 
2311
    // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2312
    // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2313
    // risk leading the user back to the AJAX request URL.
2314
    if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2315
        $setwantsurltome = false;
2316
    }
2317
 
2318
    // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2319
    if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2320
        if ($preventredirect) {
2321
            throw new require_login_session_timeout_exception();
2322
        } else {
2323
            if ($setwantsurltome) {
2324
                $SESSION->wantsurl = qualified_me();
2325
            }
2326
            redirect(get_login_url());
2327
        }
2328
    }
2329
 
2330
    // If the user is not even logged in yet then make sure they are.
2331
    if (!isloggedin()) {
2332
        if ($autologinguest && !empty($CFG->autologinguests)) {
2333
            if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2334
                // Misconfigured site guest, just redirect to login page.
2335
                redirect(get_login_url());
2336
                exit; // Never reached.
2337
            }
2338
            $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2339
            complete_user_login($guest);
2340
            $USER->autologinguest = true;
2341
            $SESSION->lang = $lang;
2342
        } else {
2343
            // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2344
            if ($preventredirect) {
2345
                throw new require_login_exception('You are not logged in');
2346
            }
2347
 
2348
            if ($setwantsurltome) {
2349
                $SESSION->wantsurl = qualified_me();
2350
            }
2351
 
2352
            // Give auth plugins an opportunity to authenticate or redirect to an external login page
2353
            $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
1326 ariadna 2354
            foreach ($authsequence as $authname) {
1 efrain 2355
                $authplugin = get_auth_plugin($authname);
2356
                $authplugin->pre_loginpage_hook();
2357
                if (isloggedin()) {
2358
                    if ($cm) {
2359
                        $modinfo = get_fast_modinfo($course);
2360
                        $cm = $modinfo->get_cm($cm->id);
2361
                    }
2362
                    set_access_log_user();
2363
                    break;
2364
                }
2365
            }
2366
 
2367
            // If we're still not logged in then go to the login page
2368
            if (!isloggedin()) {
2369
                redirect(get_login_url());
2370
                exit; // Never reached.
2371
            }
2372
        }
2373
    }
2374
 
2375
    // Loginas as redirection if needed.
2376
    if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2377
        if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2378
            if ($USER->loginascontext->instanceid != $course->id) {
1326 ariadna 2379
                throw new \moodle_exception(
2380
                    'loginasonecourse',
2381
                    '',
2382
                    $CFG->wwwroot . '/course/view.php?id=' . $USER->loginascontext->instanceid
2383
                );
1 efrain 2384
            }
2385
        }
2386
    }
2387
 
2388
    // Check whether the user should be changing password (but only if it is REALLY them).
2389
    if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2390
        $userauth = get_auth_plugin($USER->auth);
2391
        if ($userauth->can_change_password() and !$preventredirect) {
2392
            if ($setwantsurltome) {
2393
                $SESSION->wantsurl = qualified_me();
2394
            }
2395
            if ($changeurl = $userauth->change_password_url()) {
2396
                // Use plugin custom url.
2397
                redirect($changeurl);
2398
            } else {
2399
                // Use moodle internal method.
1326 ariadna 2400
                redirect($CFG->wwwroot . '/login/change_password.php');
1 efrain 2401
            }
2402
        } else if ($userauth->can_change_password()) {
2403
            throw new moodle_exception('forcepasswordchangenotice');
2404
        } else {
2405
            throw new moodle_exception('nopasswordchangeforced', 'auth');
2406
        }
2407
    }
2408
 
2409
    // Check that the user account is properly set up. If we can't redirect to
2410
    // edit their profile and this is not a WS request, perform just the lax check.
2411
    // It will allow them to use filepicker on the profile edit page.
2412
 
2413
    if ($preventredirect && !WS_SERVER) {
2414
        $usernotfullysetup = user_not_fully_set_up($USER, false);
2415
    } else {
2416
        $usernotfullysetup = user_not_fully_set_up($USER, true);
2417
    }
2418
 
2419
    if ($usernotfullysetup) {
2420
        if ($preventredirect) {
2421
            throw new moodle_exception('usernotfullysetup');
2422
        }
2423
        if ($setwantsurltome) {
2424
            $SESSION->wantsurl = qualified_me();
2425
        }
1326 ariadna 2426
        redirect($CFG->wwwroot . '/user/edit.php?id=' . $USER->id . '&amp;course=' . SITEID);
1 efrain 2427
    }
2428
 
2429
    // Make sure the USER has a sesskey set up. Used for CSRF protection.
2430
    sesskey();
2431
 
2432
    if (\core\session\manager::is_loggedinas()) {
2433
        // During a "logged in as" session we should force all content to be cleaned because the
2434
        // logged in user will be viewing potentially malicious user generated content.
2435
        // See MDL-63786 for more details.
2436
        $CFG->forceclean = true;
2437
    }
2438
 
2439
    $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2440
 
2441
    // Do not bother admins with any formalities, except for activities pending deletion.
2442
    if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2443
        // Set the global $COURSE.
2444
        if ($cm) {
2445
            $PAGE->set_cm($cm, $course);
2446
            $PAGE->set_pagelayout('incourse');
2447
        } else if (!empty($courseorid)) {
2448
            $PAGE->set_course($course);
2449
        }
2450
        // Set accesstime or the user will appear offline which messes up messaging.
2451
        // Do not update access time for webservice or ajax requests.
2452
        if (!WS_SERVER && !AJAX_SCRIPT) {
2453
            user_accesstime_log($course->id);
2454
        }
2455
 
2456
        foreach ($afterlogins as $plugintype => $plugins) {
2457
            foreach ($plugins as $pluginfunction) {
2458
                $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2459
            }
2460
        }
2461
        return;
2462
    }
2463
 
2464
    // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2465
    // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2466
    if (!defined('NO_SITEPOLICY_CHECK')) {
2467
        define('NO_SITEPOLICY_CHECK', false);
2468
    }
2469
 
2470
    // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2471
    // Do not test if the script explicitly asked for skipping the site policies check.
2472
    // Or if the user auth type is webservice.
2473
    if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') {
2474
        $manager = new \core_privacy\local\sitepolicy\manager();
2475
        if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2476
            if ($preventredirect) {
2477
                throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2478
            }
2479
            if ($setwantsurltome) {
2480
                $SESSION->wantsurl = qualified_me();
2481
            }
2482
            redirect($policyurl);
2483
        }
2484
    }
2485
 
2486
    // Fetch the system context, the course context, and prefetch its child contexts.
2487
    $sysctx = context_system::instance();
2488
    $coursecontext = context_course::instance($course->id, MUST_EXIST);
2489
    if ($cm) {
2490
        $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2491
    } else {
2492
        $cmcontext = null;
2493
    }
2494
 
2495
    // If the site is currently under maintenance, then print a message.
2496
    if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2497
        if ($preventredirect) {
2498
            throw new require_login_exception('Maintenance in progress');
2499
        }
2500
        $PAGE->set_context(null);
2501
        print_maintenance_message();
2502
    }
2503
 
2504
    // Make sure the course itself is not hidden.
2505
    if ($course->id == SITEID) {
2506
        // Frontpage can not be hidden.
2507
    } else {
2508
        if (is_role_switched($course->id)) {
2509
            // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2510
        } else {
2511
            if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2512
                // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2513
                // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2514
                if ($preventredirect) {
2515
                    throw new require_login_exception('Course is hidden');
2516
                }
2517
                $PAGE->set_context(null);
2518
                // We need to override the navigation URL as the course won't have been added to the navigation and thus
2519
                // the navigation will mess up when trying to find it.
2520
                navigation_node::override_active_url(new moodle_url('/'));
1326 ariadna 2521
                notice(get_string('coursehidden'), $CFG->wwwroot . '/');
1 efrain 2522
            }
2523
        }
2524
    }
2525
 
2526
    // Is the user enrolled?
2527
    if ($course->id == SITEID) {
2528
        // Everybody is enrolled on the frontpage.
2529
    } else {
2530
        if (\core\session\manager::is_loggedinas()) {
2531
            // Make sure the REAL person can access this course first.
2532
            $realuser = \core\session\manager::get_realuser();
1326 ariadna 2533
            if (
2534
                !is_enrolled($coursecontext, $realuser->id, '', true) and
2535
                !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)
2536
            ) {
1 efrain 2537
                if ($preventredirect) {
2538
                    throw new require_login_exception('Invalid course login-as access');
2539
                }
2540
                $PAGE->set_context(null);
2541
                echo $OUTPUT->header();
1326 ariadna 2542
                notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot . '/');
1 efrain 2543
            }
2544
        }
2545
 
2546
        $access = false;
2547
 
2548
        if (is_role_switched($course->id)) {
2549
            // Ok, user had to be inside this course before the switch.
2550
            $access = true;
2551
        } else if (is_viewing($coursecontext, $USER)) {
2552
            // Ok, no need to mess with enrol.
2553
            $access = true;
2554
        } else {
2555
            if (isset($USER->enrol['enrolled'][$course->id])) {
2556
                if ($USER->enrol['enrolled'][$course->id] > time()) {
2557
                    $access = true;
2558
                    if (isset($USER->enrol['tempguest'][$course->id])) {
2559
                        unset($USER->enrol['tempguest'][$course->id]);
2560
                        remove_temp_course_roles($coursecontext);
2561
                    }
2562
                } else {
2563
                    // Expired.
2564
                    unset($USER->enrol['enrolled'][$course->id]);
2565
                }
2566
            }
2567
            if (isset($USER->enrol['tempguest'][$course->id])) {
2568
                if ($USER->enrol['tempguest'][$course->id] == 0) {
2569
                    $access = true;
2570
                } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2571
                    $access = true;
2572
                } else {
2573
                    // Expired.
2574
                    unset($USER->enrol['tempguest'][$course->id]);
2575
                    remove_temp_course_roles($coursecontext);
2576
                }
2577
            }
2578
 
2579
            if (!$access) {
2580
                // Cache not ok.
2581
                $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2582
                if ($until !== false) {
2583
                    // Active participants may always access, a timestamp in the future, 0 (always) or false.
2584
                    if ($until == 0) {
2585
                        $until = ENROL_MAX_TIMESTAMP;
2586
                    }
2587
                    $USER->enrol['enrolled'][$course->id] = $until;
2588
                    $access = true;
2589
                } else if (core_course_category::can_view_course_info($course)) {
2590
                    $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
2591
                    $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
2592
                    $enrols = enrol_get_plugins(true);
2593
                    // First ask all enabled enrol instances in course if they want to auto enrol user.
2594
                    foreach ($instances as $instance) {
2595
                        if (!isset($enrols[$instance->enrol])) {
2596
                            continue;
2597
                        }
2598
                        // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
2599
                        $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2600
                        if ($until !== false) {
2601
                            if ($until == 0) {
2602
                                $until = ENROL_MAX_TIMESTAMP;
2603
                            }
2604
                            $USER->enrol['enrolled'][$course->id] = $until;
2605
                            $access = true;
2606
                            break;
2607
                        }
2608
                    }
2609
                    // If not enrolled yet try to gain temporary guest access.
2610
                    if (!$access) {
2611
                        foreach ($instances as $instance) {
2612
                            if (!isset($enrols[$instance->enrol])) {
2613
                                continue;
2614
                            }
2615
                            // Get a duration for the guest access, a timestamp in the future or false.
2616
                            $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2617
                            if ($until !== false and $until > time()) {
2618
                                $USER->enrol['tempguest'][$course->id] = $until;
2619
                                $access = true;
2620
                                break;
2621
                            }
2622
                        }
2623
                    }
2624
                } else {
2625
                    // User is not enrolled and is not allowed to browse courses here.
2626
                    if ($preventredirect) {
2627
                        throw new require_login_exception('Course is not available');
2628
                    }
2629
                    $PAGE->set_context(null);
2630
                    // We need to override the navigation URL as the course won't have been added to the navigation and thus
2631
                    // the navigation will mess up when trying to find it.
2632
                    navigation_node::override_active_url(new moodle_url('/'));
1326 ariadna 2633
                    notice(get_string('coursehidden'), $CFG->wwwroot . '/');
1 efrain 2634
                }
2635
            }
2636
        }
2637
 
2638
        if (!$access) {
2639
            if ($preventredirect) {
2640
                throw new require_login_exception('Not enrolled');
2641
            }
2642
            if ($setwantsurltome) {
2643
                $SESSION->wantsurl = qualified_me();
2644
            }
1326 ariadna 2645
            redirect($CFG->wwwroot . '/enrol/index.php?id=' . $course->id);
1 efrain 2646
        }
2647
    }
2648
 
2649
    // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
2650
    if ($cm && $cm->deletioninprogress) {
2651
        if ($preventredirect) {
2652
            throw new moodle_exception('activityisscheduledfordeletion');
2653
        }
2654
        require_once($CFG->dirroot . '/course/lib.php');
2655
        redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
2656
    }
2657
 
2658
    // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
2659
    if ($cm && !$cm->uservisible) {
2660
        if ($preventredirect) {
2661
            throw new require_login_exception('Activity is hidden');
2662
        }
2663
        // Get the error message that activity is not available and why (if explanation can be shown to the user).
2664
        $PAGE->set_course($course);
2665
        $renderer = $PAGE->get_renderer('course');
2666
        $message = $renderer->course_section_cm_unavailable_error_message($cm);
2667
        redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
2668
    }
2669
 
2670
    // Set the global $COURSE.
2671
    if ($cm) {
2672
        $PAGE->set_cm($cm, $course);
2673
        $PAGE->set_pagelayout('incourse');
2674
    } else if (!empty($courseorid)) {
2675
        $PAGE->set_course($course);
2676
    }
2677
 
2678
    foreach ($afterlogins as $plugintype => $plugins) {
2679
        foreach ($plugins as $pluginfunction) {
2680
            $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2681
        }
2682
    }
2683
 
2684
    // Finally access granted, update lastaccess times.
2685
    // Do not update access time for webservice or ajax requests.
2686
    if (!WS_SERVER && !AJAX_SCRIPT) {
2687
        user_accesstime_log($course->id);
2688
    }
2689
}
2690
 
2691
/**
2692
 * A convenience function for where we must be logged in as admin
2693
 * @return void
2694
 */
1326 ariadna 2695
function require_admin()
2696
{
1 efrain 2697
    require_login(null, false);
2698
    require_capability('moodle/site:config', context_system::instance());
2699
}
2700
 
2701
/**
2702
 * This function just makes sure a user is logged out.
2703
 *
2704
 * @package    core_access
2705
 * @category   access
2706
 */
1326 ariadna 2707
function require_logout()
2708
{
1 efrain 2709
    global $USER, $DB;
2710
 
2711
    if (!isloggedin()) {
2712
        // This should not happen often, no need for hooks or events here.
2713
        \core\session\manager::terminate_current();
2714
        return;
2715
    }
2716
 
2717
    // Execute hooks before action.
2718
    $authplugins = array();
2719
    $authsequence = get_enabled_auth_plugins();
2720
    foreach ($authsequence as $authname) {
2721
        $authplugins[$authname] = get_auth_plugin($authname);
2722
        $authplugins[$authname]->prelogout_hook();
2723
    }
2724
 
2725
    // Store info that gets removed during logout.
2726
    $sid = session_id();
2727
    $event = \core\event\user_loggedout::create(
2728
        array(
2729
            'userid' => $USER->id,
2730
            'objectid' => $USER->id,
2731
            'other' => array('sessionid' => $sid),
2732
        )
2733
    );
1326 ariadna 2734
    if ($session = $DB->get_record('sessions', array('sid' => $sid))) {
1 efrain 2735
        $event->add_record_snapshot('sessions', $session);
2736
    }
2737
 
2738
    // Clone of $USER object to be used by auth plugins.
2739
    $user = fullclone($USER);
2740
 
2741
    // Delete session record and drop $_SESSION content.
2742
    \core\session\manager::terminate_current();
2743
 
2744
    // Trigger event AFTER action.
2745
    $event->trigger();
2746
 
2747
    // Hook to execute auth plugins redirection after event trigger.
2748
    foreach ($authplugins as $authplugin) {
2749
        $authplugin->postlogout_hook($user);
2750
    }
2751
}
2752
 
2753
/**
2754
 * Weaker version of require_login()
2755
 *
2756
 * This is a weaker version of {@link require_login()} which only requires login
2757
 * when called from within a course rather than the site page, unless
2758
 * the forcelogin option is turned on.
2759
 * @see require_login()
2760
 *
2761
 * @package    core_access
2762
 * @category   access
2763
 *
2764
 * @param mixed $courseorid The course object or id in question
2765
 * @param bool $autologinguest Allow autologin guests if that is wanted
2766
 * @param object $cm Course activity module if known
2767
 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2768
 *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2769
 *             in order to keep redirects working properly. MDL-14495
2770
 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2771
 * @return void
2772
 * @throws coding_exception
2773
 */
1326 ariadna 2774
function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false)
2775
{
1 efrain 2776
    global $CFG, $PAGE, $SITE;
2777
    $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
1326 ariadna 2778
        or (!is_object($courseorid) and $courseorid == SITEID));
1 efrain 2779
    if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
2780
        // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2781
        // db queries so this is not really a performance concern, however it is obviously
2782
        // better if you use get_fast_modinfo to get the cm before calling this.
2783
        if (is_object($courseorid)) {
2784
            $course = $courseorid;
2785
        } else {
1326 ariadna 2786
            $course = clone ($SITE);
1 efrain 2787
        }
2788
        $modinfo = get_fast_modinfo($course);
2789
        $cm = $modinfo->get_cm($cm->id);
2790
    }
2791
    if (!empty($CFG->forcelogin)) {
2792
        // Login required for both SITE and courses.
2793
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2794
    } else if ($issite && !empty($cm) and !$cm->uservisible) {
2795
        // Always login for hidden activities.
2796
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2797
    } else if (isloggedin() && !isguestuser()) {
2798
        // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
2799
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2800
    } else if ($issite) {
2801
        // Login for SITE not required.
2802
        // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
2803
        if (!empty($courseorid)) {
2804
            if (is_object($courseorid)) {
2805
                $course = $courseorid;
2806
            } else {
2807
                $course = clone $SITE;
2808
            }
2809
            if ($cm) {
2810
                if ($cm->course != $course->id) {
2811
                    throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
2812
                }
2813
                $PAGE->set_cm($cm, $course);
2814
                $PAGE->set_pagelayout('incourse');
2815
            } else {
2816
                $PAGE->set_course($course);
2817
            }
2818
        } else {
2819
            // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
2820
            $PAGE->set_course($PAGE->course);
2821
        }
2822
        // Do not update access time for webservice or ajax requests.
2823
        if (!WS_SERVER && !AJAX_SCRIPT) {
2824
            user_accesstime_log(SITEID);
2825
        }
2826
        return;
2827
    } else {
2828
        // Course login always required.
2829
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2830
    }
2831
}
2832
 
2833
/**
2834
 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
2835
 *
2836
 * @param  string $keyvalue the key value
2837
 * @param  string $script   unique script identifier
2838
 * @param  int $instance    instance id
2839
 * @return stdClass the key entry in the user_private_key table
2840
 * @since Moodle 3.2
2841
 * @throws moodle_exception
2842
 */
1326 ariadna 2843
function validate_user_key($keyvalue, $script, $instance)
2844
{
1 efrain 2845
    global $DB;
2846
 
2847
    if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
2848
        throw new \moodle_exception('invalidkey');
2849
    }
2850
 
2851
    if (!empty($key->validuntil) and $key->validuntil < time()) {
2852
        throw new \moodle_exception('expiredkey');
2853
    }
2854
 
2855
    if ($key->iprestriction) {
2856
        $remoteaddr = getremoteaddr(null);
2857
        if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
2858
            throw new \moodle_exception('ipmismatch');
2859
        }
2860
    }
2861
    return $key;
2862
}
2863
 
2864
/**
2865
 * Require key login. Function terminates with error if key not found or incorrect.
2866
 *
2867
 * @uses NO_MOODLE_COOKIES
2868
 * @uses PARAM_ALPHANUM
2869
 * @param string $script unique script identifier
2870
 * @param int $instance optional instance id
2871
 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
2872
 * @return int Instance ID
2873
 */
1326 ariadna 2874
function require_user_key_login($script, $instance = null, $keyvalue = null)
2875
{
1 efrain 2876
    global $DB;
2877
 
2878
    if (!NO_MOODLE_COOKIES) {
2879
        throw new \moodle_exception('sessioncookiesdisable');
2880
    }
2881
 
2882
    // Extra safety.
2883
    \core\session\manager::write_close();
2884
 
2885
    if (null === $keyvalue) {
2886
        $keyvalue = required_param('key', PARAM_ALPHANUM);
2887
    }
2888
 
2889
    $key = validate_user_key($keyvalue, $script, $instance);
2890
 
2891
    if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
2892
        throw new \moodle_exception('invaliduserid');
2893
    }
2894
 
2895
    core_user::require_active_user($user, true, true);
2896
 
2897
    // Emulate normal session.
2898
    enrol_check_plugins($user, false);
2899
    \core\session\manager::set_user($user);
2900
 
2901
    // Note we are not using normal login.
2902
    if (!defined('USER_KEY_LOGIN')) {
2903
        define('USER_KEY_LOGIN', true);
2904
    }
2905
 
2906
    // Return instance id - it might be empty.
2907
    return $key->instance;
2908
}
2909
 
2910
/**
2911
 * Creates a new private user access key.
2912
 *
2913
 * @param string $script unique target identifier
2914
 * @param int $userid
2915
 * @param int $instance optional instance id
2916
 * @param string $iprestriction optional ip restricted access
2917
 * @param int $validuntil key valid only until given data
2918
 * @return string access key value
2919
 */
1326 ariadna 2920
function create_user_key($script, $userid, $instance = null, $iprestriction = null, $validuntil = null)
2921
{
1 efrain 2922
    global $DB;
2923
 
2924
    $key = new stdClass();
2925
    $key->script        = $script;
2926
    $key->userid        = $userid;
2927
    $key->instance      = $instance;
2928
    $key->iprestriction = $iprestriction;
2929
    $key->validuntil    = $validuntil;
2930
    $key->timecreated   = time();
2931
 
2932
    // Something long and unique.
1326 ariadna 2933
    $key->value         = md5($userid . '_' . time() . random_string(40));
1 efrain 2934
    while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
2935
        // Must be unique.
1326 ariadna 2936
        $key->value     = md5($userid . '_' . time() . random_string(40));
1 efrain 2937
    }
2938
    $DB->insert_record('user_private_key', $key);
2939
    return $key->value;
2940
}
2941
 
2942
/**
2943
 * Delete the user's new private user access keys for a particular script.
2944
 *
2945
 * @param string $script unique target identifier
2946
 * @param int $userid
2947
 * @return void
2948
 */
1326 ariadna 2949
function delete_user_key($script, $userid)
2950
{
1 efrain 2951
    global $DB;
2952
    $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
2953
}
2954
 
2955
/**
2956
 * Gets a private user access key (and creates one if one doesn't exist).
2957
 *
2958
 * @param string $script unique target identifier
2959
 * @param int $userid
2960
 * @param int $instance optional instance id
2961
 * @param string $iprestriction optional ip restricted access
2962
 * @param int $validuntil key valid only until given date
2963
 * @return string access key value
2964
 */
1326 ariadna 2965
function get_user_key($script, $userid, $instance = null, $iprestriction = null, $validuntil = null)
2966
{
1 efrain 2967
    global $DB;
2968
 
1326 ariadna 2969
    if ($key = $DB->get_record('user_private_key', array(
2970
        'script' => $script,
2971
        'userid' => $userid,
2972
        'instance' => $instance,
2973
        'iprestriction' => $iprestriction,
2974
        'validuntil' => $validuntil
2975
    ))) {
1 efrain 2976
        return $key->value;
2977
    } else {
2978
        return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
2979
    }
2980
}
2981
 
2982
 
2983
/**
2984
 * Modify the user table by setting the currently logged in user's last login to now.
2985
 *
2986
 * @return bool Always returns true
2987
 */
1326 ariadna 2988
function update_user_login_times()
2989
{
1 efrain 2990
    global $USER, $DB, $SESSION;
2991
 
2992
    if (isguestuser()) {
2993
        // Do not update guest access times/ips for performance.
2994
        return true;
2995
    }
2996
 
2997
    if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) {
2998
        // Do not update user login time when using user key login.
2999
        return true;
3000
    }
3001
 
3002
    $now = time();
3003
 
3004
    $user = new stdClass();
3005
    $user->id = $USER->id;
3006
 
3007
    // Make sure all users that logged in have some firstaccess.
3008
    if ($USER->firstaccess == 0) {
3009
        $USER->firstaccess = $user->firstaccess = $now;
3010
    }
3011
 
3012
    // Store the previous current as lastlogin.
3013
    $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3014
 
3015
    $USER->currentlogin = $user->currentlogin = $now;
3016
 
3017
    // Function user_accesstime_log() may not update immediately, better do it here.
3018
    $USER->lastaccess = $user->lastaccess = $now;
3019
    $SESSION->userpreviousip = $USER->lastip;
3020
    $USER->lastip = $user->lastip = getremoteaddr();
3021
 
3022
    // Note: do not call user_update_user() here because this is part of the login process,
3023
    //       the login event means that these fields were updated.
3024
    $DB->update_record('user', $user);
3025
    return true;
3026
}
3027
 
3028
/**
3029
 * Determines if a user has completed setting up their account.
3030
 *
3031
 * The lax mode (with $strict = false) has been introduced for special cases
3032
 * only where we want to skip certain checks intentionally. This is valid in
3033
 * certain mnet or ajax scenarios when the user cannot / should not be
3034
 * redirected to edit their profile. In most cases, you should perform the
3035
 * strict check.
3036
 *
3037
 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3038
 * @param bool $strict Be more strict and assert id and custom profile fields set, too
3039
 * @return bool
3040
 */
1326 ariadna 3041
function user_not_fully_set_up($user, $strict = true)
3042
{
1 efrain 3043
    global $CFG, $SESSION, $USER;
1326 ariadna 3044
    require_once($CFG->dirroot . '/user/profile/lib.php');
1 efrain 3045
 
3046
    // If the user is setup then store this in the session to avoid re-checking.
3047
    // Some edge cases are when the users email starts to bounce or the
3048
    // configuration for custom fields has changed while they are logged in so
3049
    // we re-check this fully every hour for the rare cases it has changed.
1326 ariadna 3050
    if (
3051
        isset($USER->id) && isset($user->id) && $USER->id === $user->id &&
3052
        isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS
3053
    ) {
1 efrain 3054
        return false;
3055
    }
3056
 
3057
    if (isguestuser($user)) {
3058
        return false;
3059
    }
3060
 
3061
    if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
3062
        return true;
3063
    }
3064
 
3065
    if ($strict) {
3066
        if (empty($user->id)) {
3067
            // Strict mode can be used with existing accounts only.
3068
            return true;
3069
        }
3070
        if (!profile_has_required_custom_fields_set($user->id)) {
3071
            return true;
3072
        }
3073
        if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) {
3074
            $SESSION->fullysetupstrict = time();
3075
        }
3076
    }
3077
 
3078
    return false;
3079
}
3080
 
3081
/**
3082
 * Check whether the user has exceeded the bounce threshold
3083
 *
3084
 * @param stdClass $user A {@link $USER} object
3085
 * @return bool true => User has exceeded bounce threshold
3086
 */
1326 ariadna 3087
function over_bounce_threshold($user)
3088
{
1 efrain 3089
    global $CFG, $DB;
3090
 
3091
    if (empty($CFG->handlebounces)) {
3092
        return false;
3093
    }
3094
 
3095
    if (empty($user->id)) {
3096
        // No real (DB) user, nothing to do here.
3097
        return false;
3098
    }
3099
 
3100
    // Set sensible defaults.
3101
    if (empty($CFG->minbounces)) {
3102
        $CFG->minbounces = 10;
3103
    }
3104
    if (empty($CFG->bounceratio)) {
3105
        $CFG->bounceratio = .20;
3106
    }
3107
    $bouncecount = 0;
3108
    $sendcount = 0;
1326 ariadna 3109
    if ($bounce = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
1 efrain 3110
        $bouncecount = $bounce->value;
3111
    }
3112
    if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3113
        $sendcount = $send->value;
3114
    }
1326 ariadna 3115
    return ($bouncecount >= $CFG->minbounces && $bouncecount / $sendcount >= $CFG->bounceratio);
1 efrain 3116
}
3117
 
3118
/**
3119
 * Used to increment or reset email sent count
3120
 *
3121
 * @param stdClass $user object containing an id
3122
 * @param bool $reset will reset the count to 0
3123
 * @return void
3124
 */
1326 ariadna 3125
function set_send_count($user, $reset = false)
3126
{
1 efrain 3127
    global $DB;
3128
 
3129
    if (empty($user->id)) {
3130
        // No real (DB) user, nothing to do here.
3131
        return;
3132
    }
3133
 
3134
    if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
1326 ariadna 3135
        $pref->value = (!empty($reset)) ? 0 : $pref->value + 1;
1 efrain 3136
        $DB->update_record('user_preferences', $pref);
3137
    } else if (!empty($reset)) {
3138
        // If it's not there and we're resetting, don't bother. Make a new one.
3139
        $pref = new stdClass();
3140
        $pref->name   = 'email_send_count';
3141
        $pref->value  = 1;
3142
        $pref->userid = $user->id;
3143
        $DB->insert_record('user_preferences', $pref, false);
3144
    }
3145
}
3146
 
3147
/**
3148
 * Increment or reset user's email bounce count
3149
 *
3150
 * @param stdClass $user object containing an id
3151
 * @param bool $reset will reset the count to 0
3152
 */
1326 ariadna 3153
function set_bounce_count($user, $reset = false)
3154
{
1 efrain 3155
    global $DB;
3156
 
3157
    if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
1326 ariadna 3158
        $pref->value = (!empty($reset)) ? 0 : $pref->value + 1;
1 efrain 3159
        $DB->update_record('user_preferences', $pref);
3160
    } else if (!empty($reset)) {
3161
        // If it's not there and we're resetting, don't bother. Make a new one.
3162
        $pref = new stdClass();
3163
        $pref->name   = 'email_bounce_count';
3164
        $pref->value  = 1;
3165
        $pref->userid = $user->id;
3166
        $DB->insert_record('user_preferences', $pref, false);
3167
    }
3168
}
3169
 
3170
/**
3171
 * Determines if the logged in user is currently moving an activity
3172
 *
3173
 * @param int $courseid The id of the course being tested
3174
 * @return bool
3175
 */
1326 ariadna 3176
function ismoving($courseid)
3177
{
1 efrain 3178
    global $USER;
3179
 
3180
    if (!empty($USER->activitycopy)) {
3181
        return ($USER->activitycopycourse == $courseid);
3182
    }
3183
    return false;
3184
}
3185
 
3186
/**
3187
 * Returns a persons full name
3188
 *
3189
 * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3190
 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3191
 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3192
 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3193
 *
3194
 * @param stdClass $user A {@link $USER} object to get full name of.
3195
 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3196
 * @return string
3197
 */
1326 ariadna 3198
function fullname($user, $override = false)
3199
{
1 efrain 3200
    // Note: We do not intend to deprecate this function any time soon as it is too widely used at this time.
3201
    // Uses of it should be updated to use the new API and pass updated arguments.
3202
 
3203
    // Return an empty string if there is no user.
3204
    if (empty($user)) {
3205
        return '';
3206
    }
3207
 
3208
    $options = ['override' => $override];
3209
    return core_user::get_fullname($user, null, $options);
3210
}
3211
 
3212
/**
3213
 * Reduces lines of duplicated code for getting user name fields.
3214
 *
3215
 * See also {@link user_picture::unalias()}
3216
 *
3217
 * @param object $addtoobject Object to add user name fields to.
3218
 * @param object $secondobject Object that contains user name field information.
3219
 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3220
 * @param array $additionalfields Additional fields to be matched with data in the second object.
3221
 * The key can be set to the user table field name.
3222
 * @return object User name fields.
3223
 */
1326 ariadna 3224
function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null)
3225
{
1 efrain 3226
    $fields = [];
3227
    foreach (\core_user\fields::get_name_fields() as $field) {
3228
        $fields[$field] = $prefix . $field;
3229
    }
3230
    if ($additionalfields) {
3231
        // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3232
        // the key is a number and then sets the key to the array value.
3233
        foreach ($additionalfields as $key => $value) {
3234
            if (is_numeric($key)) {
3235
                $additionalfields[$value] = $prefix . $value;
3236
                unset($additionalfields[$key]);
3237
            } else {
3238
                $additionalfields[$key] = $prefix . $value;
3239
            }
3240
        }
3241
        $fields = array_merge($fields, $additionalfields);
3242
    }
3243
    foreach ($fields as $key => $field) {
3244
        // Important that we have all of the user name fields present in the object that we are sending back.
3245
        $addtoobject->$key = '';
3246
        if (isset($secondobject->$field)) {
3247
            $addtoobject->$key = $secondobject->$field;
3248
        }
3249
    }
3250
    return $addtoobject;
3251
}
3252
 
3253
/**
3254
 * Returns an array of values in order of occurance in a provided string.
3255
 * The key in the result is the character postion in the string.
3256
 *
3257
 * @param array $values Values to be found in the string format
3258
 * @param string $stringformat The string which may contain values being searched for.
3259
 * @return array An array of values in order according to placement in the string format.
3260
 */
1326 ariadna 3261
function order_in_string($values, $stringformat)
3262
{
1 efrain 3263
    $valuearray = array();
3264
    foreach ($values as $value) {
3265
        $pattern = "/$value\b/";
3266
        // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3267
        if (preg_match($pattern, $stringformat)) {
3268
            $replacement = "thing";
3269
            // Replace the value with something more unique to ensure we get the right position when using strpos().
3270
            $newformat = preg_replace($pattern, $replacement, $stringformat);
3271
            $position = strpos($newformat, $replacement);
3272
            $valuearray[$position] = $value;
3273
        }
3274
    }
3275
    ksort($valuearray);
3276
    return $valuearray;
3277
}
3278
 
3279
/**
3280
 * Returns whether a given authentication plugin exists.
3281
 *
3282
 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3283
 * @return boolean Whether the plugin is available.
3284
 */
1326 ariadna 3285
function exists_auth_plugin($auth)
3286
{
1 efrain 3287
    global $CFG;
3288
 
3289
    if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3290
        return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3291
    }
3292
    return false;
3293
}
3294
 
3295
/**
3296
 * Checks if a given plugin is in the list of enabled authentication plugins.
3297
 *
3298
 * @param string $auth Authentication plugin.
3299
 * @return boolean Whether the plugin is enabled.
3300
 */
1326 ariadna 3301
function is_enabled_auth($auth)
3302
{
1 efrain 3303
    if (empty($auth)) {
3304
        return false;
3305
    }
3306
 
3307
    $enabled = get_enabled_auth_plugins();
3308
 
3309
    return in_array($auth, $enabled);
3310
}
3311
 
3312
/**
3313
 * Returns an authentication plugin instance.
3314
 *
3315
 * @param string $auth name of authentication plugin
3316
 * @return auth_plugin_base An instance of the required authentication plugin.
3317
 */
1326 ariadna 3318
function get_auth_plugin($auth)
3319
{
1 efrain 3320
    global $CFG;
3321
 
3322
    // Check the plugin exists first.
3323
    if (! exists_auth_plugin($auth)) {
3324
        throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth);
3325
    }
3326
 
3327
    // Return auth plugin instance.
3328
    require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3329
    $class = "auth_plugin_$auth";
3330
    return new $class;
3331
}
3332
 
3333
/**
3334
 * Returns array of active auth plugins.
3335
 *
3336
 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3337
 * @return array
3338
 */
1326 ariadna 3339
function get_enabled_auth_plugins($fix = false)
3340
{
1 efrain 3341
    global $CFG;
3342
 
3343
    $default = array('manual', 'nologin');
3344
 
3345
    if (empty($CFG->auth)) {
3346
        $auths = array();
3347
    } else {
3348
        $auths = explode(',', $CFG->auth);
3349
    }
3350
 
3351
    $auths = array_unique($auths);
3352
    $oldauthconfig = implode(',', $auths);
3353
    foreach ($auths as $k => $authname) {
3354
        if (in_array($authname, $default)) {
3355
            // The manual and nologin plugin never need to be stored.
3356
            unset($auths[$k]);
3357
        } else if (!exists_auth_plugin($authname)) {
3358
            debugging(get_string('authpluginnotfound', 'debug', $authname));
3359
            unset($auths[$k]);
3360
        }
3361
    }
3362
 
3363
    // Ideally only explicit interaction from a human admin should trigger a
3364
    // change in auth config, see MDL-70424 for details.
3365
    if ($fix) {
3366
        $newconfig = implode(',', $auths);
3367
        if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3368
            add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
3369
            set_config('auth', $newconfig);
3370
        }
3371
    }
3372
 
3373
    return (array_merge($default, $auths));
3374
}
3375
 
3376
/**
3377
 * Returns true if an internal authentication method is being used.
3378
 * if method not specified then, global default is assumed
3379
 *
3380
 * @param string $auth Form of authentication required
3381
 * @return bool
3382
 */
1326 ariadna 3383
function is_internal_auth($auth)
3384
{
1 efrain 3385
    // Throws error if bad $auth.
3386
    $authplugin = get_auth_plugin($auth);
3387
    return $authplugin->is_internal();
3388
}
3389
 
3390
/**
3391
 * Returns true if the user is a 'restored' one.
3392
 *
3393
 * Used in the login process to inform the user and allow him/her to reset the password
3394
 *
3395
 * @param string $username username to be checked
3396
 * @return bool
3397
 */
1326 ariadna 3398
function is_restored_user($username)
3399
{
1 efrain 3400
    global $CFG, $DB;
3401
 
3402
    return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3403
}
3404
 
3405
/**
3406
 * Returns an array of user fields
3407
 *
3408
 * @return array User field/column names
3409
 */
1326 ariadna 3410
function get_user_fieldnames()
3411
{
1 efrain 3412
    global $DB;
3413
 
3414
    $fieldarray = $DB->get_columns('user');
3415
    unset($fieldarray['id']);
3416
    $fieldarray = array_keys($fieldarray);
3417
 
3418
    return $fieldarray;
3419
}
3420
 
3421
/**
3422
 * Returns the string of the language for the new user.
3423
 *
3424
 * @return string language for the new user
3425
 */
1326 ariadna 3426
function get_newuser_language()
3427
{
1 efrain 3428
    global $CFG, $SESSION;
3429
    return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
3430
}
3431
 
3432
/**
3433
 * Creates a bare-bones user record
3434
 *
3435
 * @todo Outline auth types and provide code example
3436
 *
3437
 * @param string $username New user's username to add to record
3438
 * @param string $password New user's password to add to record
3439
 * @param string $auth Form of authentication required
3440
 * @return stdClass A complete user object
3441
 */
1326 ariadna 3442
function create_user_record($username, $password, $auth = 'manual')
3443
{
1 efrain 3444
    global $CFG, $DB, $SESSION;
1326 ariadna 3445
    require_once($CFG->dirroot . '/user/profile/lib.php');
3446
    require_once($CFG->dirroot . '/user/lib.php');
1 efrain 3447
 
3448
    // Just in case check text case.
3449
    $username = trim(core_text::strtolower($username));
3450
 
3451
    $authplugin = get_auth_plugin($auth);
3452
    $customfields = $authplugin->get_custom_user_profile_fields();
3453
    $newuser = new stdClass();
3454
    if ($newinfo = $authplugin->get_userinfo($username)) {
3455
        $newinfo = truncate_userinfo($newinfo);
3456
        foreach ($newinfo as $key => $value) {
3457
            if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3458
                $newuser->$key = $value;
3459
            }
3460
        }
3461
    }
3462
 
3463
    if (!empty($newuser->email)) {
3464
        if (email_is_not_allowed($newuser->email)) {
3465
            unset($newuser->email);
3466
        }
3467
    }
3468
 
3469
    $newuser->auth = $auth;
3470
    $newuser->username = $username;
3471
 
3472
    // Fix for MDL-8480
3473
    // user CFG lang for user if $newuser->lang is empty
3474
    // or $user->lang is not an installed language.
3475
    if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3476
        $newuser->lang = get_newuser_language();
3477
    }
3478
    $newuser->confirmed = 1;
3479
    $newuser->lastip = getremoteaddr();
3480
    $newuser->timecreated = time();
3481
    $newuser->timemodified = $newuser->timecreated;
3482
    $newuser->mnethostid = $CFG->mnet_localhost_id;
3483
 
3484
    $newuser->id = user_create_user($newuser, false, false);
3485
 
3486
    // Save user profile data.
3487
    profile_save_data($newuser);
3488
 
3489
    $user = get_complete_user_data('id', $newuser->id);
1326 ariadna 3490
    if (!empty($CFG->{'auth_' . $newuser->auth . '_forcechangepassword'})) {
1 efrain 3491
        set_user_preference('auth_forcepasswordchange', 1, $user);
3492
    }
3493
    // Set the password.
3494
    update_internal_user_password($user, $password);
3495
 
3496
    // Trigger event.
3497
    \core\event\user_created::create_from_userid($newuser->id)->trigger();
3498
 
3499
    return $user;
3500
}
3501
 
3502
/**
3503
 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3504
 *
3505
 * @param string $username user's username to update the record
3506
 * @return stdClass A complete user object
3507
 */
1326 ariadna 3508
function update_user_record($username)
3509
{
1 efrain 3510
    global $DB, $CFG;
3511
    // Just in case check text case.
3512
    $username = trim(core_text::strtolower($username));
3513
 
3514
    $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
3515
    return update_user_record_by_id($oldinfo->id);
3516
}
3517
 
3518
/**
3519
 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3520
 *
3521
 * @param int $id user id
3522
 * @return stdClass A complete user object
3523
 */
1326 ariadna 3524
function update_user_record_by_id($id)
3525
{
1 efrain 3526
    global $DB, $CFG;
1326 ariadna 3527
    require_once($CFG->dirroot . "/user/profile/lib.php");
3528
    require_once($CFG->dirroot . '/user/lib.php');
1 efrain 3529
 
3530
    $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
3531
    $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
3532
 
3533
    $newuser = array();
3534
    $userauth = get_auth_plugin($oldinfo->auth);
3535
 
3536
    if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
3537
        $newinfo = truncate_userinfo($newinfo);
3538
        $customfields = $userauth->get_custom_user_profile_fields();
3539
 
3540
        foreach ($newinfo as $key => $value) {
3541
            $iscustom = in_array($key, $customfields);
3542
            if (!$iscustom) {
3543
                $key = strtolower($key);
3544
            }
3545
            if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
1326 ariadna 3546
                or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted'
3547
            ) {
1 efrain 3548
                // Unknown or must not be changed.
3549
                continue;
3550
            }
3551
            if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
3552
                continue;
3553
            }
3554
            $confval = $userauth->config->{'field_updatelocal_' . $key};
3555
            $lockval = $userauth->config->{'field_lock_' . $key};
3556
            if ($confval === 'onlogin') {
3557
                // MDL-4207 Don't overwrite modified user profile values with
3558
                // empty LDAP values when 'unlocked if empty' is set. The purpose
3559
                // of the setting 'unlocked if empty' is to allow the user to fill
3560
                // in a value for the selected field _if LDAP is giving
3561
                // nothing_ for this field. Thus it makes sense to let this value
3562
                // stand in until LDAP is giving a value for this field.
3563
                if (!(empty($value) && $lockval === 'unlockedifempty')) {
3564
                    if ($iscustom || (in_array($key, $userauth->userfields) &&
1326 ariadna 3565
                        ((string)$oldinfo->$key !== (string)$value))) {
1 efrain 3566
                        $newuser[$key] = (string)$value;
3567
                    }
3568
                }
3569
            }
3570
        }
3571
        if ($newuser) {
3572
            $newuser['id'] = $oldinfo->id;
3573
            $newuser['timemodified'] = time();
3574
            user_update_user((object) $newuser, false, false);
3575
 
3576
            // Save user profile data.
3577
            profile_save_data((object) $newuser);
3578
 
3579
            // Trigger event.
3580
            \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
3581
        }
3582
    }
3583
 
3584
    return get_complete_user_data('id', $oldinfo->id);
3585
}
3586
 
3587
/**
3588
 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
3589
 *
3590
 * @param array $info Array of user properties to truncate if needed
3591
 * @return array The now truncated information that was passed in
3592
 */
1326 ariadna 3593
function truncate_userinfo(array $info)
3594
{
1 efrain 3595
    // Define the limits.
3596
    $limit = array(
3597
        'username'    => 100,
3598
        'idnumber'    => 255,
3599
        'firstname'   => 100,
3600
        'lastname'    => 100,
3601
        'email'       => 100,
3602
        'phone1'      =>  20,
3603
        'phone2'      =>  20,
3604
        'institution' => 255,
3605
        'department'  => 255,
3606
        'address'     => 255,
3607
        'city'        => 120,
3608
        'country'     =>   2,
3609
    );
3610
 
3611
    // Apply where needed.
3612
    foreach (array_keys($info) as $key) {
3613
        if (!empty($limit[$key])) {
3614
            $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
3615
        }
3616
    }
3617
 
3618
    return $info;
3619
}
3620
 
3621
/**
3622
 * Marks user deleted in internal user database and notifies the auth plugin.
3623
 * Also unenrols user from all roles and does other cleanup.
3624
 *
3625
 * Any plugin that needs to purge user data should register the 'user_deleted' event.
3626
 *
3627
 * @param stdClass $user full user object before delete
3628
 * @return boolean success
3629
 * @throws coding_exception if invalid $user parameter detected
3630
 */
1326 ariadna 3631
function delete_user(stdClass $user)
3632
{
1 efrain 3633
    global $CFG, $DB, $SESSION;
1326 ariadna 3634
    require_once($CFG->libdir . '/grouplib.php');
3635
    require_once($CFG->libdir . '/gradelib.php');
3636
    require_once($CFG->dirroot . '/message/lib.php');
3637
    require_once($CFG->dirroot . '/user/lib.php');
1 efrain 3638
 
3639
    // Make sure nobody sends bogus record type as parameter.
3640
    if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
3641
        throw new coding_exception('Invalid $user parameter in delete_user() detected');
3642
    }
3643
 
3644
    // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
3645
    if (!$user = $DB->get_record('user', array('id' => $user->id))) {
3646
        debugging('Attempt to delete unknown user account.');
3647
        return false;
3648
    }
3649
 
3650
    // There must be always exactly one guest record, originally the guest account was identified by username only,
3651
    // now we use $CFG->siteguest for performance reasons.
3652
    if ($user->username === 'guest' or isguestuser($user)) {
3653
        debugging('Guest user account can not be deleted.');
3654
        return false;
3655
    }
3656
 
3657
    // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
3658
    // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
3659
    if ($user->auth === 'manual' and is_siteadmin($user)) {
3660
        debugging('Local administrator accounts can not be deleted.');
3661
        return false;
3662
    }
3663
    // Allow plugins to use this user object before we completely delete it.
3664
    if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
3665
        foreach ($pluginsfunction as $plugintype => $plugins) {
3666
            foreach ($plugins as $pluginfunction) {
3667
                $pluginfunction($user);
3668
            }
3669
        }
3670
    }
3671
 
3672
    // Dispatch the hook for pre user update actions.
3673
    $hook = new \core_user\hook\before_user_deleted(
3674
        user: $user,
3675
    );
3676
    di::get(hook\manager::class)->dispatch($hook);
3677
 
3678
    // Keep user record before updating it, as we have to pass this to user_deleted event.
3679
    $olduser = clone $user;
3680
 
3681
    // Keep a copy of user context, we need it for event.
3682
    $usercontext = context_user::instance($user->id);
3683
 
3684
    // Delete all grades - backup is kept in grade_grades_history table.
3685
    grade_user_delete($user->id);
3686
 
3687
    // TODO: remove from cohorts using standard API here.
3688
 
3689
    // Remove user tags.
3690
    core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
3691
 
3692
    // Unconditionally unenrol from all courses.
3693
    enrol_user_delete($user);
3694
 
3695
    // Unenrol from all roles in all contexts.
3696
    // This might be slow but it is really needed - modules might do some extra cleanup!
3697
    role_unassign_all(array('userid' => $user->id));
3698
 
3699
    // Notify the competency subsystem.
3700
    \core_competency\api::hook_user_deleted($user->id);
3701
 
3702
    // Now do a brute force cleanup.
3703
 
3704
    // Delete all user events and subscription events.
3705
    $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
3706
 
3707
    // Now, delete all calendar subscription from the user.
3708
    $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
3709
 
3710
    // Remove from all cohorts.
3711
    $DB->delete_records('cohort_members', array('userid' => $user->id));
3712
 
3713
    // Remove from all groups.
3714
    $DB->delete_records('groups_members', array('userid' => $user->id));
3715
 
3716
    // Brute force unenrol from all courses.
3717
    $DB->delete_records('user_enrolments', array('userid' => $user->id));
3718
 
3719
    // Purge user preferences.
3720
    $DB->delete_records('user_preferences', array('userid' => $user->id));
3721
 
3722
    // Purge user extra profile info.
3723
    $DB->delete_records('user_info_data', array('userid' => $user->id));
3724
 
3725
    // Purge log of previous password hashes.
3726
    $DB->delete_records('user_password_history', array('userid' => $user->id));
3727
 
3728
    // Last course access not necessary either.
3729
    $DB->delete_records('user_lastaccess', array('userid' => $user->id));
3730
    // Remove all user tokens.
3731
    $DB->delete_records('external_tokens', array('userid' => $user->id));
3732
 
3733
    // Unauthorise the user for all services.
3734
    $DB->delete_records('external_services_users', array('userid' => $user->id));
3735
 
3736
    // Remove users private keys.
3737
    $DB->delete_records('user_private_key', array('userid' => $user->id));
3738
 
3739
    // Remove users customised pages.
3740
    $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
3741
 
3742
    // Remove user's oauth2 refresh tokens, if present.
3743
    $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
3744
 
3745
    // Delete user from $SESSION->bulk_users.
3746
    if (isset($SESSION->bulk_users[$user->id])) {
3747
        unset($SESSION->bulk_users[$user->id]);
3748
    }
3749
 
3750
    // Force logout - may fail if file based sessions used, sorry.
3751
    \core\session\manager::kill_user_sessions($user->id);
3752
 
3753
    // Generate username from email address, or a fake email.
3754
    $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
3755
 
3756
    $deltime = time();
3757
 
3758
    // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
3759
    $delnameprefix = clean_param($delemail, PARAM_USERNAME);
3760
    $delnamesuffix = $deltime;
3761
    $delnamesuffixlength = 10;
3762
    do {
3763
        // Workaround for bulk deletes of users with the same email address.
3764
        $delname = sprintf(
3765
            "%s.%10d",
3766
            core_text::substr(
3767
                $delnameprefix,
3768
                0,
3769
                // 100 Character maximum, with a '.' character, and a 10-digit timestamp.
3770
                100 - 1 - $delnamesuffixlength,
3771
            ),
3772
            $delnamesuffix,
3773
        );
3774
        $delnamesuffix++;
3775
 
3776
        // No need to use mnethostid here.
3777
    } while ($DB->record_exists('user', ['username' => $delname]));
3778
 
3779
    // Mark internal user record as "deleted".
3780
    $updateuser = new stdClass();
3781
    $updateuser->id           = $user->id;
3782
    $updateuser->deleted      = 1;
3783
    $updateuser->username     = $delname;            // Remember it just in case.
1326 ariadna 3784
    $updateuser->email        = md5($user->username); // Store hash of username, useful importing/restoring users.
1 efrain 3785
    $updateuser->idnumber     = '';                  // Clear this field to free it up.
3786
    $updateuser->picture      = 0;
3787
    $updateuser->timemodified = $deltime;
3788
 
3789
    // Don't trigger update event, as user is being deleted.
3790
    user_update_user($updateuser, false, false);
3791
 
3792
    // Delete all content associated with the user context, but not the context itself.
3793
    $usercontext->delete_content();
3794
 
3795
    // Delete any search data.
3796
    \core_search\manager::context_deleted($usercontext);
3797
 
3798
    // Any plugin that needs to cleanup should register this event.
3799
    // Trigger event.
3800
    $event = \core\event\user_deleted::create(
1326 ariadna 3801
        array(
3802
            'objectid' => $user->id,
3803
            'relateduserid' => $user->id,
3804
            'context' => $usercontext,
3805
            'other' => array(
3806
                'username' => $user->username,
3807
                'email' => $user->email,
3808
                'idnumber' => $user->idnumber,
3809
                'picture' => $user->picture,
3810
                'mnethostid' => $user->mnethostid
3811
            )
3812
        )
3813
    );
1 efrain 3814
    $event->add_record_snapshot('user', $olduser);
3815
    $event->trigger();
3816
 
3817
    // We will update the user's timemodified, as it will be passed to the user_deleted event, which
3818
    // should know about this updated property persisted to the user's table.
3819
    $user->timemodified = $updateuser->timemodified;
3820
 
3821
    // Notify auth plugin - do not block the delete even when plugin fails.
3822
    $authplugin = get_auth_plugin($user->auth);
3823
    $authplugin->user_delete($user);
3824
 
3825
    return true;
3826
}
3827
 
3828
/**
3829
 * Retrieve the guest user object.
3830
 *
3831
 * @return stdClass A {@link $USER} object
3832
 */
1326 ariadna 3833
function guest_user()
3834
{
1 efrain 3835
    global $CFG, $DB;
3836
 
3837
    if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
3838
        $newuser->confirmed = 1;
3839
        $newuser->lang = get_newuser_language();
3840
        $newuser->lastip = getremoteaddr();
3841
    }
3842
 
3843
    return $newuser;
3844
}
3845
 
3846
/**
3847
 * Authenticates a user against the chosen authentication mechanism
3848
 *
3849
 * Given a username and password, this function looks them
3850
 * up using the currently selected authentication mechanism,
3851
 * and if the authentication is successful, it returns a
3852
 * valid $user object from the 'user' table.
3853
 *
3854
 * Uses auth_ functions from the currently active auth module
3855
 *
3856
 * After authenticate_user_login() returns success, you will need to
3857
 * log that the user has logged in, and call complete_user_login() to set
3858
 * the session up.
3859
 *
3860
 * Note: this function works only with non-mnet accounts!
3861
 *
3862
 * @param string $username  User's username (or also email if $CFG->authloginviaemail enabled)
3863
 * @param string $password  User's password
3864
 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
3865
 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
3866
 * @param string|bool $logintoken If this is set to a string it is validated against the login token for the session.
3867
 * @param string|bool $loginrecaptcha If this is set to a string it is validated against Google reCaptcha.
3868
 * @return stdClass|false A {@link $USER} object or false if error
3869
 */
3870
function authenticate_user_login(
3871
    $username,
3872
    $password,
3873
    $ignorelockout = false,
3874
    &$failurereason = null,
3875
    $logintoken = false,
3876
    string|bool $loginrecaptcha = false,
3877
) {
3878
    global $CFG, $DB, $PAGE, $SESSION;
3879
    require_once("$CFG->libdir/authlib.php");
3880
 
3881
    if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
3882
        // we have found the user
3883
 
3884
    } else if (!empty($CFG->authloginviaemail)) {
3885
        if ($email = clean_param($username, PARAM_EMAIL)) {
3886
            $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
3887
            $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
3888
            $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
3889
            if (count($users) === 1) {
3890
                // Use email for login only if unique.
3891
                $user = reset($users);
3892
                $user = get_complete_user_data('id', $user->id);
3893
                $username = $user->username;
3894
            }
3895
            unset($users);
3896
        }
3897
    }
3898
 
3899
    // Make sure this request came from the login form.
3900
    if (!\core\session\manager::validate_login_token($logintoken)) {
3901
        $failurereason = AUTH_LOGIN_FAILED;
3902
 
3903
        // Trigger login failed event (specifying the ID of the found user, if available).
3904
        \core\event\user_login_failed::create([
3905
            'userid' => ($user->id ?? 0),
3906
            'other' => [
3907
                'username' => $username,
3908
                'reason' => $failurereason,
3909
            ],
3910
        ])->trigger();
3911
 
1326 ariadna 3912
        error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Invalid Login Token:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 3913
        return false;
3914
    }
3915
 
3916
    // Login reCaptcha.
3917
    if (login_captcha_enabled() && !validate_login_captcha($loginrecaptcha)) {
3918
        $failurereason = AUTH_LOGIN_FAILED_RECAPTCHA;
3919
        // Trigger login failed event (specifying the ID of the found user, if available).
3920
        \core\event\user_login_failed::create([
3921
            'userid' => ($user->id ?? 0),
3922
            'other' => [
3923
                'username' => $username,
3924
                'reason' => $failurereason,
3925
            ],
3926
        ])->trigger();
3927
        return false;
3928
    }
3929
 
3930
    $authsenabled = get_enabled_auth_plugins();
3931
 
3932
    if ($user) {
3933
        // Use manual if auth not set.
3934
        $auth = empty($user->auth) ? 'manual' : $user->auth;
3935
 
3936
        if (in_array($user->auth, $authsenabled)) {
3937
            $authplugin = get_auth_plugin($user->auth);
3938
            $authplugin->pre_user_login_hook($user);
3939
        }
3940
 
3941
        if (!empty($user->suspended)) {
3942
            $failurereason = AUTH_LOGIN_SUSPENDED;
3943
 
3944
            // Trigger login failed event.
1326 ariadna 3945
            $event = \core\event\user_login_failed::create(array(
3946
                'userid' => $user->id,
3947
                'other' => array('username' => $username, 'reason' => $failurereason)
3948
            ));
1 efrain 3949
            $event->trigger();
1326 ariadna 3950
            error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Suspended Login:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 3951
            return false;
3952
        }
1326 ariadna 3953
        if ($auth == 'nologin' or !is_enabled_auth($auth)) {
1 efrain 3954
            // Legacy way to suspend user.
3955
            $failurereason = AUTH_LOGIN_SUSPENDED;
3956
 
3957
            // Trigger login failed event.
1326 ariadna 3958
            $event = \core\event\user_login_failed::create(array(
3959
                'userid' => $user->id,
3960
                'other' => array('username' => $username, 'reason' => $failurereason)
3961
            ));
1 efrain 3962
            $event->trigger();
1326 ariadna 3963
            error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Disabled Login:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 3964
            return false;
3965
        }
3966
        $auths = array($auth);
3967
    } else {
3968
        // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
3969
        if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
3970
            $failurereason = AUTH_LOGIN_NOUSER;
3971
 
3972
            // Trigger login failed event.
1326 ariadna 3973
            $event = \core\event\user_login_failed::create(array('other' => array(
3974
                'username' => $username,
3975
                'reason' => $failurereason
3976
            )));
1 efrain 3977
            $event->trigger();
1326 ariadna 3978
            error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Deleted Login:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 3979
            return false;
3980
        }
3981
 
3982
        // User does not exist.
3983
        $auths = $authsenabled;
3984
        $user = new stdClass();
3985
        $user->id = 0;
3986
    }
3987
 
3988
    if ($ignorelockout) {
3989
        // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
3990
        // or this function is called from a SSO script.
3991
    } else if ($user->id) {
3992
        // Verify login lockout after other ways that may prevent user login.
3993
        if (login_is_lockedout($user)) {
3994
            $failurereason = AUTH_LOGIN_LOCKOUT;
3995
 
3996
            // Trigger login failed event.
1326 ariadna 3997
            $event = \core\event\user_login_failed::create(array(
3998
                'userid' => $user->id,
3999
                'other' => array('username' => $username, 'reason' => $failurereason)
4000
            ));
1 efrain 4001
            $event->trigger();
4002
 
1326 ariadna 4003
            error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Login lockout:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 4004
            $SESSION->loginerrormsg = get_string('accountlocked', 'admin');
4005
 
4006
            return false;
4007
        }
4008
    } else {
4009
        // We can not lockout non-existing accounts.
4010
    }
4011
 
4012
    foreach ($auths as $auth) {
4013
        $authplugin = get_auth_plugin($auth);
4014
 
4015
        // On auth fail fall through to the next plugin.
4016
        if (!$authplugin->user_login($username, $password)) {
4017
            continue;
4018
        }
4019
 
4020
        // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
4021
        if (!empty($CFG->passwordpolicycheckonlogin)) {
4022
            $errmsg = '';
4023
            $passed = check_password_policy($password, $errmsg, $user);
4024
            if (!$passed) {
4025
                // First trigger event for failure.
4026
                $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
4027
                $failedevent->trigger();
4028
 
4029
                // If able to change password, set flag and move on.
4030
                if ($authplugin->can_change_password()) {
4031
                    // Check if we are on internal change password page, or service is external, don't show notification.
4032
                    $internalchangeurl = new moodle_url('/login/change_password.php');
4033
                    if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
4034
                        \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
4035
                    }
4036
                    set_user_preference('auth_forcepasswordchange', 1, $user);
4037
                } else if ($authplugin->can_reset_password()) {
4038
                    // Else force a reset if possible.
4039
                    \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
4040
                    redirect(new moodle_url('/login/forgot_password.php'));
4041
                } else {
4042
                    $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
4043
                    // If support page is set, add link for help.
4044
                    if (!empty($CFG->supportpage)) {
4045
                        $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
4046
                        $link = \html_writer::tag('p', $link);
4047
                        $notifymsg .= $link;
4048
                    }
4049
 
4050
                    // If no change or reset is possible, add a notification for user.
4051
                    \core\notification::error($notifymsg);
4052
                }
4053
            }
4054
        }
4055
 
4056
        // Successful authentication.
4057
        if ($user->id) {
4058
            // User already exists in database.
4059
            if (empty($user->auth)) {
4060
                // For some reason auth isn't set yet.
4061
                $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
4062
                $user->auth = $auth;
4063
            }
4064
 
4065
            // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4066
            // the current hash algorithm while we have access to the user's password.
4067
            update_internal_user_password($user, $password);
4068
 
4069
            if ($authplugin->is_synchronised_with_external()) {
4070
                // Update user record from external DB.
4071
                $user = update_user_record_by_id($user->id);
4072
            }
4073
        } else {
4074
            // The user is authenticated but user creation may be disabled.
4075
            if (!empty($CFG->authpreventaccountcreation)) {
4076
                $failurereason = AUTH_LOGIN_UNAUTHORISED;
4077
 
4078
                // Trigger login failed event.
1326 ariadna 4079
                $event = \core\event\user_login_failed::create(array('other' => array(
4080
                    'username' => $username,
4081
                    'reason' => $failurereason
4082
                )));
1 efrain 4083
                $event->trigger();
4084
 
1326 ariadna 4085
                error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  " .
4086
                    $_SERVER['HTTP_USER_AGENT']);
1 efrain 4087
                return false;
4088
            } else {
4089
                $user = create_user_record($username, $password, $auth);
4090
            }
4091
        }
4092
 
4093
        $authplugin->sync_roles($user);
4094
 
4095
        foreach ($authsenabled as $hau) {
4096
            $hauth = get_auth_plugin($hau);
4097
            $hauth->user_authenticated_hook($user, $username, $password);
4098
        }
4099
 
4100
        if (empty($user->id)) {
4101
            $failurereason = AUTH_LOGIN_NOUSER;
4102
            // Trigger login failed event.
1326 ariadna 4103
            $event = \core\event\user_login_failed::create(array('other' => array(
4104
                'username' => $username,
4105
                'reason' => $failurereason
4106
            )));
1 efrain 4107
            $event->trigger();
4108
            return false;
4109
        }
4110
 
4111
        if (!empty($user->suspended)) {
4112
            // Just in case some auth plugin suspended account.
4113
            $failurereason = AUTH_LOGIN_SUSPENDED;
4114
            // Trigger login failed event.
1326 ariadna 4115
            $event = \core\event\user_login_failed::create(array(
4116
                'userid' => $user->id,
4117
                'other' => array('username' => $username, 'reason' => $failurereason)
4118
            ));
1 efrain 4119
            $event->trigger();
1326 ariadna 4120
            error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Suspended Login:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 4121
            return false;
4122
        }
4123
 
4124
        login_attempt_valid($user);
4125
        $failurereason = AUTH_LOGIN_OK;
4126
        return $user;
4127
    }
4128
 
4129
    // Failed if all the plugins have failed.
4130
    if (debugging('', DEBUG_ALL)) {
1326 ariadna 4131
        error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Failed Login:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 4132
    }
4133
 
4134
    if ($user->id) {
4135
        login_attempt_failed($user);
4136
        $failurereason = AUTH_LOGIN_FAILED;
4137
        // Trigger login failed event.
1326 ariadna 4138
        $event = \core\event\user_login_failed::create(array(
4139
            'userid' => $user->id,
4140
            'other' => array('username' => $username, 'reason' => $failurereason)
4141
        ));
1 efrain 4142
        $event->trigger();
4143
    } else {
4144
        $failurereason = AUTH_LOGIN_NOUSER;
4145
        // Trigger login failed event.
1326 ariadna 4146
        $event = \core\event\user_login_failed::create(array('other' => array(
4147
            'username' => $username,
4148
            'reason' => $failurereason
4149
        )));
1 efrain 4150
        $event->trigger();
4151
    }
4152
 
4153
    return false;
4154
}
4155
 
4156
/**
4157
 * Call to complete the user login process after authenticate_user_login()
4158
 * has succeeded. It will setup the $USER variable and other required bits
4159
 * and pieces.
4160
 *
4161
 * NOTE:
4162
 * - It will NOT log anything -- up to the caller to decide what to log.
4163
 * - this function does not set any cookies any more!
4164
 *
4165
 * @param stdClass $user
4166
 * @param array $extrauserinfo
4167
 * @return stdClass A {@link $USER} object - BC only, do not use
4168
 */
1326 ariadna 4169
function complete_user_login($user, array $extrauserinfo = [])
4170
{
1 efrain 4171
    global $CFG, $DB, $USER, $SESSION;
4172
 
4173
    \core\session\manager::login_user($user);
4174
 
4175
    // Reload preferences from DB.
4176
    unset($USER->preference);
4177
    check_user_preferences_loaded($USER);
4178
 
4179
    // Update login times.
4180
    update_user_login_times();
4181
 
4182
    // Extra session prefs init.
4183
    set_login_session_preferences();
4184
 
4185
    // Trigger login event.
4186
    $event = \core\event\user_loggedin::create(
4187
        array(
4188
            'userid' => $USER->id,
4189
            'objectid' => $USER->id,
4190
            'other' => [
4191
                'username' => $USER->username,
4192
                'extrauserinfo' => $extrauserinfo
4193
            ]
4194
        )
4195
    );
4196
    $event->trigger();
4197
 
4198
    // Allow plugins to callback as soon possible after user has completed login.
4199
    di::get(\core\hook\manager::class)->dispatch(new \core_user\hook\after_login_completed());
4200
 
4201
    // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case).
4202
    // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser).
4203
    // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself.
4204
    $loginip = getremoteaddr();
4205
    $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip;
4206
    $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST;
4207
 
4208
    if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) {
4209
 
4210
        $logintime = time();
4211
        $ismoodleapp = false;
4212
        $useragent = \core_useragent::get_user_agent_string();
4213
 
4214
        $sitepreferences = get_message_output_default_preferences();
4215
        // Check if new login notification is disabled at system level.
4216
        $newlogindisabled = $sitepreferences->moodle_newlogin_disable ?? 0;
4217
        // Check if message providers (web, email, mobile) are enabled at system level.
4218
        $msgproviderenabled = isset($sitepreferences->message_provider_moodle_newlogin_enabled);
4219
        // Get message providers enabled for a user.
4220
        $userpreferences = get_user_preferences('message_provider_moodle_newlogin_enabled');
4221
        // Check if notification processor plugins (web, email, mobile) are enabled at system level.
4222
        $msgprocessorsready = !empty(get_message_processors(true));
4223
        // If new login notification is enabled at system level then go for other conditions check.
4224
        $newloginenabled = $newlogindisabled ? 0 : ($userpreferences != 'none' && $msgproviderenabled);
4225
 
4226
        if ($newloginenabled && $msgprocessorsready) {
4227
            // Schedule adhoc task to send a login notification to the user.
4228
            $task = new \core\task\send_login_notifications();
4229
            $task->set_userid($USER->id);
4230
            $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime'));
4231
            $task->set_component('core');
4232
            \core\task\manager::queue_adhoc_task($task);
4233
        }
4234
    }
4235
 
4236
    // Queue migrating the messaging data, if we need to.
4237
    if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4238
        // Check if there are any legacy messages to migrate.
4239
        if (\core_message\helper::legacy_messages_exist($USER->id)) {
4240
            \core_message\task\migrate_message_data::queue_task($USER->id);
4241
        } else {
4242
            set_user_preference('core_message_migrate_data', true, $USER->id);
4243
        }
4244
    }
4245
 
4246
    if (isguestuser()) {
4247
        // No need to continue when user is THE guest.
4248
        return $USER;
4249
    }
4250
 
4251
    if (CLI_SCRIPT) {
4252
        // We can redirect to password change URL only in browser.
4253
        return $USER;
4254
    }
4255
 
4256
    // Select password change url.
4257
    $userauth = get_auth_plugin($USER->auth);
4258
 
4259
    // Check whether the user should be changing password.
4260
    if (get_user_preferences('auth_forcepasswordchange', false)) {
4261
        if ($userauth->can_change_password()) {
4262
            if ($changeurl = $userauth->change_password_url()) {
4263
                redirect($changeurl);
4264
            } else {
4265
                require_once($CFG->dirroot . '/login/lib.php');
4266
                $SESSION->wantsurl = core_login_get_return_url();
1326 ariadna 4267
                redirect($CFG->wwwroot . '/login/change_password.php');
1 efrain 4268
            }
4269
        } else {
4270
            throw new \moodle_exception('nopasswordchangeforced', 'auth');
4271
        }
4272
    }
4273
    return $USER;
4274
}
4275
 
4276
/**
4277
 * Check a password hash to see if it was hashed using the legacy hash algorithm (bcrypt).
4278
 *
4279
 * @param string $password String to check.
4280
 * @return bool True if the $password matches the format of a bcrypt hash.
4281
 */
1326 ariadna 4282
function password_is_legacy_hash(#[\SensitiveParameter] string $password): bool
4283
{
1 efrain 4284
    return (bool) preg_match('/^\$2y\$[\d]{2}\$[A-Za-z0-9\.\/]{53}$/', $password);
4285
}
4286
 
4287
/**
4288
 * Calculate the Shannon entropy of a string.
4289
 *
4290
 * @param string $pepper The pepper to calculate the entropy of.
4291
 * @return float The Shannon entropy of the string.
4292
 */
1326 ariadna 4293
function calculate_entropy(#[\SensitiveParameter] string $pepper): float
4294
{
1 efrain 4295
    // Initialize entropy.
4296
    $h = 0;
4297
 
4298
    // Calculate the length of the string.
4299
    $size = strlen($pepper);
4300
 
4301
    // For each unique character in the string.
4302
    foreach (count_chars($pepper, 1) as $v) {
4303
        // Calculate the probability of the character.
4304
        $p = $v / $size;
4305
 
4306
        // Add the character's contribution to the total entropy.
4307
        // This uses the formula for the entropy of a discrete random variable.
4308
        $h -= $p * log($p) / log(2);
4309
    }
4310
 
4311
    // Instead of returning the average entropy per symbol (Shannon entropy),
4312
    // we multiply by the length of the string to get total entropy.
4313
    return $h * $size;
4314
}
4315
 
4316
/**
4317
 * Get the available password peppers.
4318
 * The latest pepper is checked for minimum entropy as part of this function.
4319
 * We only calculate the entropy of the most recent pepper,
4320
 * because passwords are always updated to the latest pepper,
4321
 * and in the past we may have enforced a lower minimum entropy.
4322
 * Also, we allow the latest pepper to be empty, to allow admins to migrate off peppers.
4323
 *
4324
 * @return array The password peppers.
4325
 * @throws coding_exception If the entropy of the password pepper is less than the recommended minimum.
4326
 */
1326 ariadna 4327
function get_password_peppers(): array
4328
{
1 efrain 4329
    global $CFG;
4330
 
4331
    // Get all available peppers.
4332
    if (isset($CFG->passwordpeppers) && is_array($CFG->passwordpeppers)) {
4333
        // Sort the array in descending order of keys (numerical).
4334
        $peppers = $CFG->passwordpeppers;
4335
        krsort($peppers, SORT_NUMERIC);
4336
    } else {
4337
        $peppers = [];  // Set an empty array if no peppers are found.
4338
    }
4339
 
4340
    // Check if the entropy of the most recent pepper is less than the minimum.
4341
    // Also, we allow the most recent pepper to be empty, to allow admins to migrate off peppers.
4342
    $lastpepper = reset($peppers);
4343
    if (!empty($peppers) && $lastpepper !== '' && calculate_entropy($lastpepper) < PEPPER_ENTROPY) {
4344
        throw new coding_exception(
1326 ariadna 4345
            'password pepper below minimum',
4346
            'The entropy of the password pepper is less than the recommended minimum.'
4347
        );
1 efrain 4348
    }
4349
    return $peppers;
4350
}
4351
 
4352
/**
4353
 * Compare password against hash stored in user object to determine if it is valid.
4354
 *
4355
 * If necessary it also updates the stored hash to the current format.
4356
 *
4357
 * @param stdClass $user (Password property may be updated).
4358
 * @param string $password Plain text password.
4359
 * @return bool True if password is valid.
4360
 */
1326 ariadna 4361
function validate_internal_user_password(stdClass $user, #[\SensitiveParameter] string $password): bool
4362
{
1 efrain 4363
 
4364
    if (exceeds_password_length($password)) {
4365
        // Password cannot be more than MAX_PASSWORD_CHARACTERS characters.
4366
        return false;
4367
    }
4368
 
4369
    if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4370
        // Internal password is not used at all, it can not validate.
4371
        return false;
4372
    }
4373
 
4374
    $peppers = get_password_peppers(); // Get the array of available peppers.
4375
    $islegacy = password_is_legacy_hash($user->password); // Check if the password is a legacy bcrypt hash.
4376
 
4377
    // If the password is a legacy hash, no peppers were used, so verify and update directly.
4378
    if ($islegacy && password_verify($password, $user->password)) {
4379
        update_internal_user_password($user, $password);
4380
        return true;
4381
    }
4382
 
4383
    // If the password is not a legacy hash, iterate through the peppers.
4384
    $latestpepper = reset($peppers);
4385
    // Add an empty pepper to the beginning of the array. To make it easier to check if the password matches without any pepper.
4386
    $peppers = [-1 => ''] + $peppers;
4387
    foreach ($peppers as $pepper) {
4388
        $pepperedpassword = $password . $pepper;
4389
 
4390
        // If the peppered password is correct, update (if necessary) and return true.
4391
        if (password_verify($pepperedpassword, $user->password)) {
4392
            // If the pepper used is not the latest one, update the password.
4393
            if ($pepper !== $latestpepper) {
4394
                update_internal_user_password($user, $password);
4395
            }
4396
            return true;
4397
        }
4398
    }
4399
 
4400
    // If no peppered password was correct, the password is wrong.
4401
    return false;
4402
}
4403
 
4404
/**
4405
 * Calculate hash for a plain text password.
4406
 *
4407
 * @param string $password Plain text password to be hashed.
4408
 * @param bool $fasthash If true, use a low number of rounds when generating the hash
4409
 *                       This is faster to generate but makes the hash less secure.
4410
 *                       It is used when lots of hashes need to be generated quickly.
4411
 * @param int $pepperlength Lenght of the peppers
4412
 * @return string The hashed password.
4413
 *
4414
 * @throws moodle_exception If a problem occurs while generating the hash.
4415
 */
1326 ariadna 4416
function hash_internal_user_password(#[\SensitiveParameter] string $password, $fasthash = false, $pepperlength = 0): string
4417
{
1 efrain 4418
    if (exceeds_password_length($password, $pepperlength)) {
4419
        // Password cannot be more than MAX_PASSWORD_CHARACTERS.
4420
        throw new \moodle_exception(get_string("passwordexceeded", 'error', MAX_PASSWORD_CHARACTERS));
4421
    }
4422
 
4423
    // Set the cost factor to 5000 for fast hashing, otherwise use default cost.
4424
    $rounds = $fasthash ? 5000 : 10000;
4425
 
4426
    // First generate a cryptographically suitable salt.
4427
    $randombytes = random_bytes(16);
4428
    $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16);
4429
 
4430
    // Now construct the password string with the salt and number of rounds.
4431
    // The password string is in the format $algorithm$rounds$salt$hash. ($6 is the SHA512 algorithm).
4432
    $generatedhash = crypt($password, implode('$', [
4433
        '',
4434
        // The SHA512 Algorithm
4435
        '6',
4436
        "rounds={$rounds}",
4437
        $salt,
4438
        '',
4439
    ]));
4440
 
4441
    if ($generatedhash === false || $generatedhash === null) {
4442
        throw new moodle_exception('Failed to generate password hash.');
4443
    }
4444
 
4445
    return $generatedhash;
4446
}
4447
 
4448
/**
4449
 * Update password hash in user object (if necessary).
4450
 *
4451
 * The password is updated if:
4452
 * 1. The password has changed (the hash of $user->password is different
4453
 *    to the hash of $password).
4454
 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4455
 *    md5 algorithm).
4456
 *
4457
 * The password is peppered with the latest pepper before hashing,
4458
 * if peppers are available.
4459
 * Updating the password will modify the $user object and the database
4460
 * record to use the current hashing algorithm.
4461
 * It will remove Web Services user tokens too.
4462
 *
4463
 * @param stdClass $user User object (password property may be updated).
11 efrain 4464
 * @param string|null $password Plain text password.
1 efrain 4465
 * @param bool $fasthash If true, use a low cost factor when generating the hash
4466
 *                       This is much faster to generate but makes the hash
4467
 *                       less secure. It is used when lots of hashes need to
4468
 *                       be generated quickly.
4469
 * @return bool Always returns true.
4470
 */
4471
function update_internal_user_password(
1326 ariadna 4472
    stdClass $user,
4473
    #[\SensitiveParameter] ?string $password,
4474
    bool $fasthash = false
1 efrain 4475
): bool {
4476
    global $CFG, $DB;
4477
 
4478
    // Add the latest password pepper to the password before further processing.
4479
    $peppers = get_password_peppers();
4480
    if (!empty($peppers)) {
4481
        $password = $password . reset($peppers);
4482
    }
4483
 
4484
    // Figure out what the hashed password should be.
4485
    if (!isset($user->auth)) {
1326 ariadna 4486
        debugging(
4487
            'User record in update_internal_user_password() must include field auth',
4488
            DEBUG_DEVELOPER
4489
        );
1 efrain 4490
        $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4491
    }
4492
    $authplugin = get_auth_plugin($user->auth);
4493
    if ($authplugin->prevent_local_passwords()) {
4494
        $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4495
    } else {
4496
        $hashedpassword = hash_internal_user_password($password, $fasthash);
4497
    }
4498
 
4499
    $algorithmchanged = false;
4500
 
4501
    if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4502
        // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4503
        $passwordchanged = ($user->password !== $hashedpassword);
4504
    } else if (isset($user->password)) {
4505
        // If verification fails then it means the password has changed.
4506
        $passwordchanged = !password_verify($password, $user->password);
4507
        $algorithmchanged = password_is_legacy_hash($user->password);
4508
    } else {
4509
        // While creating new user, password in unset in $user object, to avoid
4510
        // saving it with user_create()
4511
        $passwordchanged = true;
4512
    }
4513
 
4514
    if ($passwordchanged || $algorithmchanged) {
4515
        $DB->set_field('user', 'password',  $hashedpassword, array('id' => $user->id));
4516
        $user->password = $hashedpassword;
4517
 
4518
        // Trigger event.
4519
        $user = $DB->get_record('user', array('id' => $user->id));
4520
        \core\event\user_password_updated::create_from_user($user)->trigger();
4521
 
4522
        // Remove WS user tokens.
4523
        if (!empty($CFG->passwordchangetokendeletion)) {
1326 ariadna 4524
            require_once($CFG->dirroot . '/webservice/lib.php');
1 efrain 4525
            webservice::delete_user_ws_tokens($user->id);
4526
        }
4527
    }
4528
 
4529
    return true;
4530
}
4531
 
4532
/**
4533
 * Get a complete user record, which includes all the info in the user record.
4534
 *
4535
 * Intended for setting as $USER session variable
4536
 *
4537
 * @param string $field The user field to be checked for a given value.
4538
 * @param string $value The value to match for $field.
4539
 * @param int $mnethostid
4540
 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4541
 *                              found. Otherwise, it will just return false.
4542
 * @return mixed False, or A {@link $USER} object.
4543
 */
1326 ariadna 4544
function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false)
4545
{
1 efrain 4546
    global $CFG, $DB;
4547
 
4548
    if (!$field || !$value) {
4549
        return false;
4550
    }
4551
 
4552
    // Change the field to lowercase.
4553
    $field = core_text::strtolower($field);
4554
 
4555
    // List of case insensitive fields.
4556
    $caseinsensitivefields = ['email'];
4557
 
4558
    // Username input is forced to lowercase and should be case sensitive.
4559
    if ($field == 'username') {
4560
        $value = core_text::strtolower($value);
4561
    }
4562
 
4563
    // Build the WHERE clause for an SQL query.
4564
    $params = array('fieldval' => $value);
4565
 
4566
    // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4567
    // such as MySQL by pre-filtering users with accent-insensitive subselect.
4568
    if (in_array($field, $caseinsensitivefields)) {
4569
        $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4570
        $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4571
        $params['fieldval2'] = $value;
4572
    } else {
4573
        $fieldselect = "$field = :fieldval";
4574
        $idsubselect = '';
4575
    }
4576
    $constraints = "$fieldselect AND deleted <> 1";
4577
 
4578
    // If we are loading user data based on anything other than id,
4579
    // we must also restrict our search based on mnet host.
4580
    if ($field != 'id') {
4581
        if (empty($mnethostid)) {
4582
            // If empty, we restrict to local users.
4583
            $mnethostid = $CFG->mnet_localhost_id;
4584
        }
4585
    }
4586
    if (!empty($mnethostid)) {
4587
        $params['mnethostid'] = $mnethostid;
4588
        $constraints .= " AND mnethostid = :mnethostid";
4589
    }
4590
 
4591
    if ($idsubselect) {
4592
        $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4593
    }
4594
 
4595
    // Get all the basic user data.
4596
    try {
4597
        // Make sure that there's only a single record that matches our query.
4598
        // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4599
        // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4600
        $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4601
    } catch (dml_exception $exception) {
4602
        if ($throwexception) {
4603
            throw $exception;
4604
        } else {
4605
            // Return false when no records or multiple records were found.
4606
            return false;
4607
        }
4608
    }
4609
 
4610
    // Get various settings and preferences.
4611
 
4612
    // Preload preference cache.
4613
    check_user_preferences_loaded($user);
4614
 
4615
    // Load course enrolment related stuff.
4616
    $user->lastcourseaccess    = array(); // During last session.
4617
    $user->currentcourseaccess = array(); // During current session.
4618
    if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4619
        foreach ($lastaccesses as $lastaccess) {
4620
            $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4621
        }
4622
    }
4623
 
4624
    // Add cohort theme.
4625
    if (!empty($CFG->allowcohortthemes)) {
4626
        require_once($CFG->dirroot . '/cohort/lib.php');
4627
        if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
4628
            $user->cohorttheme = $cohorttheme;
4629
        }
4630
    }
4631
 
4632
    // Add the custom profile fields to the user record.
4633
    $user->profile = array();
4634
    if (!isguestuser($user)) {
1326 ariadna 4635
        require_once($CFG->dirroot . '/user/profile/lib.php');
1 efrain 4636
        profile_load_custom_fields($user);
4637
    }
4638
 
4639
    // Rewrite some variables if necessary.
4640
    if (!empty($user->description)) {
4641
        // No need to cart all of it around.
4642
        $user->description = true;
4643
    }
4644
    if (isguestuser($user)) {
4645
        // Guest language always same as site.
4646
        $user->lang = get_newuser_language();
4647
        // Name always in current language.
4648
        $user->firstname = get_string('guestuser');
4649
        $user->lastname = ' ';
4650
    }
4651
 
4652
    return $user;
4653
}
4654
 
4655
/**
4656
 * Validate a password against the configured password policy
4657
 *
4658
 * @param string $password the password to be checked against the password policy
4659
 * @param string|null $errmsg the error message to display when the password doesn't comply with the policy.
4660
 * @param stdClass|null $user the user object to perform password validation against. Defaults to null if not provided.
4661
 *
4662
 * @return bool true if the password is valid according to the policy. false otherwise.
4663
 */
1326 ariadna 4664
function check_password_policy(string $password, ?string &$errmsg, ?stdClass $user = null)
4665
{
1 efrain 4666
    global $CFG;
4667
    if (!empty($CFG->passwordpolicy) && !isguestuser($user)) {
4668
        $errors = get_password_policy_errors($password, $user);
4669
 
4670
        foreach ($errors as $error) {
4671
            $errmsg .= '<div>' . $error . '</div>';
4672
        }
4673
    }
4674
 
4675
    return $errmsg == '';
4676
}
4677
 
4678
/**
4679
 * Validate a password against the configured password policy.
4680
 * Note: This function is unaffected by whether the password policy is enabled or not.
4681
 *
4682
 * @param string $password the password to be checked against the password policy
4683
 * @param stdClass|null $user the user object to perform password validation against. Defaults to null if not provided.
4684
 *
4685
 * @return string[] Array of error messages.
4686
 */
1326 ariadna 4687
function get_password_policy_errors(string $password, ?stdClass $user = null): array
4688
{
1 efrain 4689
    global $CFG;
4690
 
4691
    $errors = [];
4692
 
4693
    if (core_text::strlen($password) < $CFG->minpasswordlength) {
4694
        $errors[] = get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength);
4695
    }
4696
    if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4697
        $errors[] = get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits);
4698
    }
4699
    if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4700
        $errors[] = get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower);
4701
    }
4702
    if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4703
        $errors[] = get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper);
4704
    }
4705
    if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4706
        $errors[] = get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum);
4707
    }
4708
    if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4709
        $errors[] = get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars);
4710
    }
4711
 
4712
    // Fire any additional password policy functions from plugins.
4713
    // Plugin functions should output an error message string or empty string for success.
4714
    $pluginsfunction = get_plugins_with_function('check_password_policy');
4715
    foreach ($pluginsfunction as $plugintype => $plugins) {
4716
        foreach ($plugins as $pluginfunction) {
4717
            $pluginerr = $pluginfunction($password, $user);
4718
            if ($pluginerr) {
4719
                $errors[] = $pluginerr;
4720
            }
4721
        }
4722
    }
4723
 
4724
    return $errors;
4725
}
4726
 
4727
/**
4728
 * When logging in, this function is run to set certain preferences for the current SESSION.
4729
 */
1326 ariadna 4730
function set_login_session_preferences()
4731
{
1 efrain 4732
    global $SESSION;
4733
 
4734
    $SESSION->justloggedin = true;
4735
 
4736
    unset($SESSION->lang);
4737
    unset($SESSION->forcelang);
4738
    unset($SESSION->load_navigation_admin);
4739
}
4740
 
4741
 
4742
/**
4743
 * Delete a course, including all related data from the database, and any associated files.
4744
 *
4745
 * @param mixed $courseorid The id of the course or course object to delete.
4746
 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4747
 * @return bool true if all the removals succeeded. false if there were any failures. If this
4748
 *             method returns false, some of the removals will probably have succeeded, and others
4749
 *             failed, but you have no way of knowing which.
4750
 */
1326 ariadna 4751
function delete_course($courseorid, $showfeedback = true)
4752
{
1 efrain 4753
    global $DB, $CFG;
4754
 
4755
    if (is_object($courseorid)) {
4756
        $courseid = $courseorid->id;
4757
        $course   = $courseorid;
4758
    } else {
4759
        $courseid = $courseorid;
4760
        if (!$course = $DB->get_record('course', array('id' => $courseid))) {
4761
            return false;
4762
        }
4763
    }
4764
    $context = context_course::instance($courseid);
4765
 
4766
    // Frontpage course can not be deleted!!
4767
    if ($courseid == SITEID) {
4768
        return false;
4769
    }
4770
 
4771
    // Allow plugins to use this course before we completely delete it.
4772
    if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
4773
        foreach ($pluginsfunction as $plugintype => $plugins) {
4774
            foreach ($plugins as $pluginfunction) {
4775
                $pluginfunction($course);
4776
            }
4777
        }
4778
    }
4779
 
4780
    // Dispatch the hook for pre course delete actions.
4781
    $hook = new \core_course\hook\before_course_deleted(
4782
        course: $course,
4783
    );
4784
    \core\di::get(\core\hook\manager::class)->dispatch($hook);
4785
 
4786
    // Tell the search manager we are about to delete a course. This prevents us sending updates
4787
    // for each individual context being deleted.
4788
    \core_search\manager::course_deleting_start($courseid);
4789
 
4790
    $handler = core_course\customfield\course_handler::create();
4791
    $handler->delete_instance($courseid);
4792
 
4793
    // Make the course completely empty.
4794
    remove_course_contents($courseid, $showfeedback);
4795
 
4796
    // Delete the course and related context instance.
4797
    context_helper::delete_instance(CONTEXT_COURSE, $courseid);
4798
 
4799
    $DB->delete_records("course", array("id" => $courseid));
4800
    $DB->delete_records("course_format_options", array("courseid" => $courseid));
4801
 
4802
    // Reset all course related caches here.
4803
    core_courseformat\base::reset_course_cache($courseid);
4804
 
4805
    // Tell search that we have deleted the course so it can delete course data from the index.
4806
    \core_search\manager::course_deleting_finish($courseid);
4807
 
4808
    // Trigger a course deleted event.
4809
    $event = \core\event\course_deleted::create(array(
4810
        'objectid' => $course->id,
4811
        'context' => $context,
4812
        'other' => array(
4813
            'shortname' => $course->shortname,
4814
            'fullname' => $course->fullname,
4815
            'idnumber' => $course->idnumber
1326 ariadna 4816
        )
1 efrain 4817
    ));
4818
    $event->add_record_snapshot('course', $course);
4819
    $event->trigger();
4820
 
4821
    return true;
4822
}
4823
 
4824
/**
4825
 * Clear a course out completely, deleting all content but don't delete the course itself.
4826
 *
4827
 * This function does not verify any permissions.
4828
 *
4829
 * Please note this function also deletes all user enrolments,
4830
 * enrolment instances and role assignments by default.
4831
 *
4832
 * $options:
4833
 *  - 'keep_roles_and_enrolments' - false by default
4834
 *  - 'keep_groups_and_groupings' - false by default
4835
 *
4836
 * @param int $courseid The id of the course that is being deleted
4837
 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4838
 * @param array $options extra options
4839
 * @return bool true if all the removals succeeded. false if there were any failures. If this
4840
 *             method returns false, some of the removals will probably have succeeded, and others
4841
 *             failed, but you have no way of knowing which.
4842
 */
1326 ariadna 4843
function remove_course_contents($courseid, $showfeedback = true, array $options = null)
4844
{
1 efrain 4845
    global $CFG, $DB, $OUTPUT;
4846
 
1326 ariadna 4847
    require_once($CFG->libdir . '/badgeslib.php');
4848
    require_once($CFG->libdir . '/completionlib.php');
4849
    require_once($CFG->libdir . '/questionlib.php');
4850
    require_once($CFG->libdir . '/gradelib.php');
4851
    require_once($CFG->dirroot . '/group/lib.php');
4852
    require_once($CFG->dirroot . '/comment/lib.php');
4853
    require_once($CFG->dirroot . '/rating/lib.php');
4854
    require_once($CFG->dirroot . '/notes/lib.php');
1 efrain 4855
 
4856
    // Handle course badges.
4857
    badges_handle_course_deletion($courseid);
4858
 
4859
    // NOTE: these concatenated strings are suboptimal, but it is just extra info...
1326 ariadna 4860
    $strdeleted = get_string('deleted') . ' - ';
1 efrain 4861
 
4862
    // Some crazy wishlist of stuff we should skip during purging of course content.
4863
    $options = (array)$options;
4864
 
4865
    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
4866
    $coursecontext = context_course::instance($courseid);
4867
    $fs = get_file_storage();
4868
 
4869
    // Delete course completion information, this has to be done before grades and enrols.
4870
    $cc = new completion_info($course);
4871
    $cc->clear_criteria();
4872
    if ($showfeedback) {
1326 ariadna 4873
        echo $OUTPUT->notification($strdeleted . get_string('completion', 'completion'), 'notifysuccess');
1 efrain 4874
    }
4875
 
4876
    // Remove all data from gradebook - this needs to be done before course modules
4877
    // because while deleting this information, the system may need to reference
4878
    // the course modules that own the grades.
4879
    remove_course_grades($courseid, $showfeedback);
4880
    remove_grade_letters($coursecontext, $showfeedback);
4881
 
4882
    // Delete course blocks in any all child contexts,
4883
    // they may depend on modules so delete them first.
4884
    $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4885
    foreach ($childcontexts as $childcontext) {
4886
        blocks_delete_all_for_context($childcontext->id);
4887
    }
4888
    unset($childcontexts);
4889
    blocks_delete_all_for_context($coursecontext->id);
4890
    if ($showfeedback) {
1326 ariadna 4891
        echo $OUTPUT->notification($strdeleted . get_string('type_block_plural', 'plugin'), 'notifysuccess');
1 efrain 4892
    }
4893
 
4894
    $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
4895
    rebuild_course_cache($courseid, true);
4896
 
4897
    // Get the list of all modules that are properly installed.
4898
    $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
4899
 
4900
    // Delete every instance of every module,
4901
    // this has to be done before deleting of course level stuff.
4902
    $locations = core_component::get_plugin_list('mod');
4903
    foreach ($locations as $modname => $moddir) {
4904
        if ($modname === 'NEWMODULE') {
4905
            continue;
4906
        }
4907
        if (array_key_exists($modname, $allmodules)) {
4908
            $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
1326 ariadna 4909
              FROM {" . $modname . "} m
1 efrain 4910
                   LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
4911
             WHERE m.course = :courseid";
1326 ariadna 4912
            $instances = $DB->get_records_sql($sql, array(
4913
                'courseid' => $course->id,
4914
                'modulename' => $modname,
4915
                'moduleid' => $allmodules[$modname]
4916
            ));
1 efrain 4917
 
4918
            include_once("$moddir/lib.php");                 // Shows php warning only if plugin defective.
1326 ariadna 4919
            $moddelete = $modname . '_delete_instance';       // Delete everything connected to an instance.
1 efrain 4920
 
4921
            if ($instances) {
4922
                foreach ($instances as $cm) {
4923
                    if ($cm->id) {
4924
                        // Delete activity context questions and question categories.
4925
                        question_delete_activity($cm);
4926
                        // Notify the competency subsystem.
4927
                        \core_competency\api::hook_course_module_deleted($cm);
4928
 
4929
                        // Delete all tag instances associated with the instance of this module.
4930
                        core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id);
4931
                        core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
4932
                    }
4933
                    if (function_exists($moddelete)) {
4934
                        // This purges all module data in related tables, extra user prefs, settings, etc.
4935
                        $moddelete($cm->modinstance);
4936
                    } else {
4937
                        // NOTE: we should not allow installation of modules with missing delete support!
4938
                        debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4939
                        $DB->delete_records($modname, array('id' => $cm->modinstance));
4940
                    }
4941
 
4942
                    if ($cm->id) {
4943
                        // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
4944
                        context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4945
                        $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
4946
                        $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
4947
                        $DB->delete_records('course_modules', array('id' => $cm->id));
4948
                        rebuild_course_cache($cm->course, true);
4949
                    }
4950
                }
4951
            }
4952
            if ($instances and $showfeedback) {
1326 ariadna 4953
                echo $OUTPUT->notification($strdeleted . get_string('pluginname', $modname), 'notifysuccess');
1 efrain 4954
            }
4955
        } else {
4956
            // Ooops, this module is not properly installed, force-delete it in the next block.
4957
        }
4958
    }
4959
 
4960
    // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
4961
 
4962
    // Delete completion defaults.
4963
    $DB->delete_records("course_completion_defaults", array("course" => $courseid));
4964
 
4965
    // Remove all data from availability and completion tables that is associated
4966
    // with course-modules belonging to this course. Note this is done even if the
4967
    // features are not enabled now, in case they were enabled previously.
1326 ariadna 4968
    $DB->delete_records_subquery(
4969
        'course_modules_completion',
4970
        'coursemoduleid',
4971
        'id',
4972
        'SELECT id from {course_modules} WHERE course = ?',
4973
        [$courseid]
4974
    );
4975
    $DB->delete_records_subquery(
4976
        'course_modules_viewed',
4977
        'coursemoduleid',
4978
        'id',
4979
        'SELECT id from {course_modules} WHERE course = ?',
4980
        [$courseid]
4981
    );
1 efrain 4982
 
4983
    // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
4984
    $cms = $DB->get_records('course_modules', array('course' => $course->id));
4985
    $allmodulesbyid = array_flip($allmodules);
4986
    foreach ($cms as $cm) {
4987
        if (array_key_exists($cm->module, $allmodulesbyid)) {
4988
            try {
4989
                $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
4990
            } catch (Exception $e) {
4991
                // Ignore weird or missing table problems.
4992
            }
4993
        }
4994
        context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4995
        $DB->delete_records('course_modules', array('id' => $cm->id));
4996
        rebuild_course_cache($cm->course, true);
4997
    }
4998
 
4999
    if ($showfeedback) {
1326 ariadna 5000
        echo $OUTPUT->notification($strdeleted . get_string('type_mod_plural', 'plugin'), 'notifysuccess');
1 efrain 5001
    }
5002
 
5003
    // Delete questions and question categories.
5004
    question_delete_course($course);
5005
    if ($showfeedback) {
1326 ariadna 5006
        echo $OUTPUT->notification($strdeleted . get_string('questions', 'question'), 'notifysuccess');
1 efrain 5007
    }
5008
 
5009
    // Delete content bank contents.
5010
    $cb = new \core_contentbank\contentbank();
5011
    $cbdeleted = $cb->delete_contents($coursecontext);
5012
    if ($showfeedback && $cbdeleted) {
1326 ariadna 5013
        echo $OUTPUT->notification($strdeleted . get_string('contentbank', 'contentbank'), 'notifysuccess');
1 efrain 5014
    }
5015
 
5016
    // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
5017
    $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5018
    foreach ($childcontexts as $childcontext) {
5019
        $childcontext->delete();
5020
    }
5021
    unset($childcontexts);
5022
 
5023
    // Remove roles and enrolments by default.
5024
    if (empty($options['keep_roles_and_enrolments'])) {
5025
        // This hack is used in restore when deleting contents of existing course.
5026
        // During restore, we should remove only enrolment related data that the user performing the restore has a
5027
        // permission to remove.
5028
        $userid = $options['userid'] ?? null;
5029
        enrol_course_delete($course, $userid);
5030
        role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
5031
        if ($showfeedback) {
1326 ariadna 5032
            echo $OUTPUT->notification($strdeleted . get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
1 efrain 5033
        }
5034
    }
5035
 
5036
    // Delete any groups, removing members and grouping/course links first.
5037
    if (empty($options['keep_groups_and_groupings'])) {
5038
        groups_delete_groupings($course->id, $showfeedback);
5039
        groups_delete_groups($course->id, $showfeedback);
5040
    }
5041
 
5042
    // Filters be gone!
5043
    filter_delete_all_for_context($coursecontext->id);
5044
 
5045
    // Notes, you shall not pass!
5046
    note_delete_all($course->id);
5047
 
5048
    // Die comments!
5049
    comment::delete_comments($coursecontext->id);
5050
 
5051
    // Ratings are history too.
5052
    $delopt = new stdclass();
5053
    $delopt->contextid = $coursecontext->id;
5054
    $rm = new rating_manager();
5055
    $rm->delete_ratings($delopt);
5056
 
5057
    // Delete course tags.
5058
    core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
5059
 
5060
    // Give the course format the opportunity to remove its obscure data.
5061
    $format = course_get_format($course);
5062
    $format->delete_format_data();
5063
 
5064
    // Notify the competency subsystem.
5065
    \core_competency\api::hook_course_deleted($course);
5066
 
5067
    // Delete calendar events.
5068
    $DB->delete_records('event', array('courseid' => $course->id));
5069
    $fs->delete_area_files($coursecontext->id, 'calendar');
5070
 
5071
    // Delete all related records in other core tables that may have a courseid
5072
    // This array stores the tables that need to be cleared, as
5073
    // table_name => column_name that contains the course id.
5074
    $tablestoclear = array(
5075
        'backup_courses' => 'courseid',  // Scheduled backup stuff.
5076
        'user_lastaccess' => 'courseid', // User access info.
5077
    );
5078
    foreach ($tablestoclear as $table => $col) {
5079
        $DB->delete_records($table, array($col => $course->id));
5080
    }
5081
 
5082
    // Delete all course backup files.
5083
    $fs->delete_area_files($coursecontext->id, 'backup');
5084
 
5085
    // Cleanup course record - remove links to deleted stuff.
5086
    // Do not wipe cacherev, as this course might be reused and we need to ensure that it keeps
5087
    // increasing.
5088
    $oldcourse = new stdClass();
5089
    $oldcourse->id               = $course->id;
5090
    $oldcourse->summary          = '';
5091
    $oldcourse->legacyfiles      = 0;
5092
    if (!empty($options['keep_groups_and_groupings'])) {
5093
        $oldcourse->defaultgroupingid = 0;
5094
    }
5095
    $DB->update_record('course', $oldcourse);
5096
 
5097
    // Delete course sections.
5098
    $DB->delete_records('course_sections', array('course' => $course->id));
5099
 
5100
    // Delete legacy, section and any other course files.
5101
    $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
5102
 
5103
    // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5104
    if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5105
        // Easy, do not delete the context itself...
5106
        $coursecontext->delete_content();
5107
    } else {
5108
        // Hack alert!!!!
5109
        // We can not drop all context stuff because it would bork enrolments and roles,
5110
        // there might be also files used by enrol plugins...
5111
    }
5112
 
5113
    // Delete legacy files - just in case some files are still left there after conversion to new file api,
5114
    // also some non-standard unsupported plugins may try to store something there.
1326 ariadna 5115
    fulldelete($CFG->dataroot . '/' . $course->id);
1 efrain 5116
 
5117
    // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5118
    course_modinfo::purge_course_cache($courseid);
5119
 
5120
    // Trigger a course content deleted event.
5121
    $event = \core\event\course_content_deleted::create(array(
5122
        'objectid' => $course->id,
5123
        'context' => $coursecontext,
1326 ariadna 5124
        'other' => array(
5125
            'shortname' => $course->shortname,
5126
            'fullname' => $course->fullname,
5127
            'options' => $options
5128
        ) // Passing this for legacy reasons.
1 efrain 5129
    ));
5130
    $event->add_record_snapshot('course', $course);
5131
    $event->trigger();
5132
 
5133
    return true;
5134
}
5135
 
5136
/**
5137
 * Change dates in module - used from course reset.
5138
 *
5139
 * @param string $modname forum, assignment, etc
5140
 * @param array $fields array of date fields from mod table
5141
 * @param int $timeshift time difference
5142
 * @param int $courseid
5143
 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5144
 * @return bool success
5145
 */
1326 ariadna 5146
function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0)
5147
{
1 efrain 5148
    global $CFG, $DB;
1326 ariadna 5149
    include_once($CFG->dirroot . '/mod/' . $modname . '/lib.php');
1 efrain 5150
 
5151
    $return = true;
5152
    $params = array($timeshift, $courseid);
5153
    foreach ($fields as $field) {
1326 ariadna 5154
        $updatesql = "UPDATE {" . $modname . "}
1 efrain 5155
                          SET $field = $field + ?
5156
                        WHERE course=? AND $field<>0";
5157
        if ($modid) {
5158
            $updatesql .= ' AND id=?';
5159
            $params[] = $modid;
5160
        }
5161
        $return = $DB->execute($updatesql, $params) && $return;
5162
    }
5163
 
5164
    return $return;
5165
}
5166
 
5167
/**
5168
 * This function will empty a course of user data.
5169
 * It will retain the activities and the structure of the course.
5170
 *
5171
 * @param object $data an object containing all the settings including courseid (without magic quotes)
5172
 * @return array status array of array component, item, error
5173
 */
1326 ariadna 5174
function reset_course_userdata($data)
5175
{
1 efrain 5176
    global $CFG, $DB;
1326 ariadna 5177
    require_once($CFG->libdir . '/gradelib.php');
5178
    require_once($CFG->libdir . '/completionlib.php');
5179
    require_once($CFG->dirroot . '/completion/criteria/completion_criteria_date.php');
5180
    require_once($CFG->dirroot . '/group/lib.php');
1 efrain 5181
 
5182
    $data->courseid = $data->id;
5183
    $context = context_course::instance($data->courseid);
5184
 
5185
    $eventparams = array(
5186
        'context' => $context,
5187
        'courseid' => $data->id,
5188
        'other' => array(
5189
            'reset_options' => (array) $data
5190
        )
5191
    );
5192
    $event = \core\event\course_reset_started::create($eventparams);
5193
    $event->trigger();
5194
 
5195
    // Calculate the time shift of dates.
5196
    if (!empty($data->reset_start_date)) {
5197
        // Time part of course startdate should be zero.
5198
        $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5199
    } else {
5200
        $data->timeshift = 0;
5201
    }
5202
 
5203
    // Result array: component, item, error.
5204
    $status = array();
5205
 
5206
    // Start the resetting.
5207
    $componentstr = get_string('general');
5208
 
5209
    // Move the course start time.
5210
    if (!empty($data->reset_start_date) and $data->timeshift) {
5211
        // Change course start data.
5212
        $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5213
        // Update all course and group events - do not move activity events.
5214
        $updatesql = "UPDATE {event}
5215
                         SET timestart = timestart + ?
5216
                       WHERE courseid=? AND instance=0";
5217
        $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5218
 
5219
        // Update any date activity restrictions.
5220
        if ($CFG->enableavailability) {
5221
            \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5222
        }
5223
 
5224
        // Update completion expected dates.
5225
        if ($CFG->enablecompletion) {
5226
            $modinfo = get_fast_modinfo($data->courseid);
5227
            $changed = false;
5228
            foreach ($modinfo->get_cms() as $cm) {
5229
                if ($cm->completion && !empty($cm->completionexpected)) {
1326 ariadna 5230
                    $DB->set_field(
5231
                        'course_modules',
5232
                        'completionexpected',
5233
                        $cm->completionexpected + $data->timeshift,
5234
                        array('id' => $cm->id)
5235
                    );
1 efrain 5236
                    $changed = true;
5237
                }
5238
            }
5239
 
5240
            // Clear course cache if changes made.
5241
            if ($changed) {
5242
                rebuild_course_cache($data->courseid, true);
5243
            }
5244
 
5245
            // Update course date completion criteria.
5246
            \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5247
        }
5248
 
5249
        $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5250
    }
5251
 
5252
    if (!empty($data->reset_end_date)) {
5253
        // If the user set a end date value respect it.
5254
        $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5255
    } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5256
        // If there is a time shift apply it to the end date as well.
5257
        $enddate = $data->reset_end_date_old + $data->timeshift;
5258
        $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5259
    }
5260
 
5261
    if (!empty($data->reset_events)) {
5262
        $DB->delete_records('event', array('courseid' => $data->courseid));
5263
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5264
    }
5265
 
5266
    if (!empty($data->reset_notes)) {
1326 ariadna 5267
        require_once($CFG->dirroot . '/notes/lib.php');
1 efrain 5268
        note_delete_all($data->courseid);
5269
        $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5270
    }
5271
 
5272
    if (!empty($data->delete_blog_associations)) {
1326 ariadna 5273
        require_once($CFG->dirroot . '/blog/lib.php');
1 efrain 5274
        blog_remove_associations_for_course($data->courseid);
5275
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5276
    }
5277
 
5278
    if (!empty($data->reset_completion)) {
5279
        // Delete course and activity completion information.
5280
        $course = $DB->get_record('course', array('id' => $data->courseid));
5281
        $cc = new completion_info($course);
5282
        $cc->delete_all_completion_data();
1326 ariadna 5283
        $status[] = array(
5284
            'component' => $componentstr,
5285
            'item' => get_string('deletecompletiondata', 'completion'),
5286
            'error' => false
5287
        );
1 efrain 5288
    }
5289
 
5290
    if (!empty($data->reset_competency_ratings)) {
5291
        \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
1326 ariadna 5292
        $status[] = array(
5293
            'component' => $componentstr,
5294
            'item' => get_string('deletecompetencyratings', 'core_competency'),
5295
            'error' => false
5296
        );
1 efrain 5297
    }
5298
 
5299
    $componentstr = get_string('roles');
5300
 
5301
    if (!empty($data->reset_roles_overrides)) {
5302
        $children = $context->get_child_contexts();
5303
        foreach ($children as $child) {
5304
            $child->delete_capabilities();
5305
        }
5306
        $context->delete_capabilities();
5307
        $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5308
    }
5309
 
5310
    if (!empty($data->reset_roles_local)) {
5311
        $children = $context->get_child_contexts();
5312
        foreach ($children as $child) {
5313
            role_unassign_all(array('contextid' => $child->id));
5314
        }
5315
        $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5316
    }
5317
 
5318
    // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5319
    $data->unenrolled = array();
5320
    if (!empty($data->unenrol_users)) {
5321
        $plugins = enrol_get_plugins(true);
5322
        $instances = enrol_get_instances($data->courseid, true);
5323
        foreach ($instances as $key => $instance) {
5324
            if (!isset($plugins[$instance->enrol])) {
5325
                unset($instances[$key]);
5326
                continue;
5327
            }
5328
        }
5329
 
5330
        $usersroles = enrol_get_course_users_roles($data->courseid);
5331
        foreach ($data->unenrol_users as $withroleid) {
5332
            if ($withroleid) {
5333
                $sql = "SELECT ue.*
5334
                          FROM {user_enrolments} ue
5335
                          JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5336
                          JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5337
                          JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5338
                $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5339
            } else {
5340
                // Without any role assigned at course context.
5341
                $sql = "SELECT ue.*
5342
                          FROM {user_enrolments} ue
5343
                          JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5344
                          JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5345
                     LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5346
                         WHERE ra.id IS null";
5347
                $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5348
            }
5349
 
5350
            $rs = $DB->get_recordset_sql($sql, $params);
5351
            foreach ($rs as $ue) {
5352
                if (!isset($instances[$ue->enrolid])) {
5353
                    continue;
5354
                }
5355
                $instance = $instances[$ue->enrolid];
5356
                $plugin = $plugins[$instance->enrol];
5357
                if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5358
                    continue;
5359
                }
5360
 
5361
                if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5362
                    // If we don't remove all roles and user has more than one role, just remove this role.
5363
                    role_unassign($withroleid, $ue->userid, $context->id);
5364
 
5365
                    unset($usersroles[$ue->userid][$withroleid]);
5366
                } else {
5367
                    // If we remove all roles or user has only one role, unenrol user from course.
5368
                    $plugin->unenrol_user($instance, $ue->userid);
5369
                }
5370
                $data->unenrolled[$ue->userid] = $ue->userid;
5371
            }
5372
            $rs->close();
5373
        }
5374
    }
5375
    if (!empty($data->unenrolled)) {
5376
        $status[] = array(
5377
            'component' => $componentstr,
1326 ariadna 5378
            'item' => get_string('unenrol', 'enrol') . ' (' . count($data->unenrolled) . ')',
1 efrain 5379
            'error' => false
5380
        );
5381
    }
5382
 
5383
    $componentstr = get_string('groups');
5384
 
5385
    // Remove all group members.
5386
    if (!empty($data->reset_groups_members)) {
5387
        groups_delete_group_members($data->courseid);
5388
        $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5389
    }
5390
 
5391
    // Remove all groups.
5392
    if (!empty($data->reset_groups_remove)) {
5393
        groups_delete_groups($data->courseid, false);
5394
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5395
    }
5396
 
5397
    // Remove all grouping members.
5398
    if (!empty($data->reset_groupings_members)) {
5399
        groups_delete_groupings_groups($data->courseid, false);
5400
        $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5401
    }
5402
 
5403
    // Remove all groupings.
5404
    if (!empty($data->reset_groupings_remove)) {
5405
        groups_delete_groupings($data->courseid, false);
5406
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5407
    }
5408
 
5409
    // Look in every instance of every module for data to delete.
5410
    $unsupportedmods = array();
1326 ariadna 5411
    if ($allmods = $DB->get_records('modules')) {
1 efrain 5412
        foreach ($allmods as $mod) {
5413
            $modname = $mod->name;
1326 ariadna 5414
            $modfile = $CFG->dirroot . '/mod/' . $modname . '/lib.php';
5415
            $moddeleteuserdata = $modname . '_reset_userdata';   // Function to delete user data.
1 efrain 5416
            if (file_exists($modfile)) {
5417
                if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5418
                    continue; // Skip mods with no instances.
5419
                }
5420
                include_once($modfile);
5421
                if (function_exists($moddeleteuserdata)) {
5422
                    $modstatus = $moddeleteuserdata($data);
5423
                    if (is_array($modstatus)) {
5424
                        $status = array_merge($status, $modstatus);
5425
                    } else {
1326 ariadna 5426
                        debugging('Module ' . $modname . ' returned incorrect staus - must be an array!');
1 efrain 5427
                    }
5428
                } else {
5429
                    $unsupportedmods[] = $mod;
5430
                }
5431
            } else {
1326 ariadna 5432
                debugging('Missing lib.php in ' . $modname . ' module!');
1 efrain 5433
            }
5434
            // Update calendar events for all modules.
5435
            course_module_bulk_update_calendar_events($modname, $data->courseid);
5436
        }
5437
        // Purge the course cache after resetting course start date. MDL-76936
5438
        if ($data->timeshift) {
5439
            course_modinfo::purge_course_cache($data->courseid);
5440
        }
5441
    }
5442
 
5443
    // Mention unsupported mods.
5444
    if (!empty($unsupportedmods)) {
5445
        foreach ($unsupportedmods as $mod) {
5446
            $status[] = array(
5447
                'component' => get_string('modulenameplural', $mod->name),
5448
                'item' => '',
5449
                'error' => get_string('resetnotimplemented')
5450
            );
5451
        }
5452
    }
5453
 
5454
    $componentstr = get_string('gradebook', 'grades');
5455
    // Reset gradebook,.
5456
    if (!empty($data->reset_gradebook_items)) {
5457
        remove_course_grades($data->courseid, false);
5458
        grade_grab_course_grades($data->courseid);
5459
        grade_regrade_final_grades($data->courseid);
5460
        $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5461
    } else if (!empty($data->reset_gradebook_grades)) {
5462
        grade_course_reset($data->courseid);
5463
        $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5464
    }
5465
    // Reset comments.
5466
    if (!empty($data->reset_comments)) {
1326 ariadna 5467
        require_once($CFG->dirroot . '/comment/lib.php');
1 efrain 5468
        comment::reset_course_page_comments($context);
5469
    }
5470
 
5471
    $event = \core\event\course_reset_ended::create($eventparams);
5472
    $event->trigger();
5473
 
5474
    return $status;
5475
}
5476
 
5477
/**
5478
 * Generate an email processing address.
5479
 *
5480
 * @param int $modid
5481
 * @param string $modargs
5482
 * @return string Returns email processing address
5483
 */
1326 ariadna 5484
function generate_email_processing_address($modid, $modargs)
5485
{
1 efrain 5486
    global $CFG;
5487
 
1326 ariadna 5488
    $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2) . $modargs;
5489
    return $header . substr(md5($header . get_site_identifier()), 0, 16) . '@' . $CFG->maildomain;
1 efrain 5490
}
5491
 
5492
/**
5493
 * ?
5494
 *
5495
 * @todo Finish documenting this function
5496
 *
5497
 * @param string $modargs
5498
 * @param string $body Currently unused
5499
 */
1326 ariadna 5500
function moodle_process_email($modargs, $body)
5501
{
1 efrain 5502
    global $DB;
5503
 
5504
    // The first char should be an unencoded letter. We'll take this as an action.
5505
    switch ($modargs[0]) {
5506
        case 'B': { // Bounce.
1326 ariadna 5507
                list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5508
                if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5509
                    // Check the half md5 of their email.
5510
                    $md5check = substr(md5($user->email), 0, 16);
5511
                    if ($md5check == substr($modargs, -16)) {
5512
                        set_bounce_count($user);
5513
                    }
5514
                    // Else maybe they've already changed it?
1 efrain 5515
                }
5516
            }
1326 ariadna 5517
            break;
5518
            // Maybe more later?
1 efrain 5519
    }
5520
}
5521
 
5522
// CORRESPONDENCE.
5523
 
5524
/**
5525
 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5526
 *
5527
 * @param string $action 'get', 'buffer', 'close' or 'flush'
5528
 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5529
 */
1326 ariadna 5530
function get_mailer($action = 'get')
5531
{
1 efrain 5532
    global $CFG;
5533
 
5534
    /** @var moodle_phpmailer $mailer */
5535
    static $mailer  = null;
5536
    static $counter = 0;
5537
 
5538
    if (!isset($CFG->smtpmaxbulk)) {
5539
        $CFG->smtpmaxbulk = 1;
5540
    }
5541
 
5542
    if ($action == 'get') {
5543
        $prevkeepalive = false;
5544
 
5545
        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5546
            if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5547
                $counter++;
5548
                // Reset the mailer.
5549
                $mailer->Priority         = 3;
5550
                $mailer->CharSet          = 'UTF-8'; // Our default.
5551
                $mailer->ContentType      = "text/plain";
5552
                $mailer->Encoding         = "8bit";
5553
                $mailer->From             = "root@localhost";
5554
                $mailer->FromName         = "Root User";
5555
                $mailer->Sender           = "";
5556
                $mailer->Subject          = "";
5557
                $mailer->Body             = "";
5558
                $mailer->AltBody          = "";
5559
                $mailer->ConfirmReadingTo = "";
5560
 
5561
                $mailer->clearAllRecipients();
5562
                $mailer->clearReplyTos();
5563
                $mailer->clearAttachments();
5564
                $mailer->clearCustomHeaders();
5565
                return $mailer;
5566
            }
5567
 
5568
            $prevkeepalive = $mailer->SMTPKeepAlive;
5569
            get_mailer('flush');
5570
        }
5571
 
1326 ariadna 5572
        require_once($CFG->libdir . '/phpmailer/moodle_phpmailer.php');
1 efrain 5573
        $mailer = new moodle_phpmailer();
5574
 
5575
        $counter = 1;
5576
 
5577
        if ($CFG->smtphosts == 'qmail') {
5578
            // Use Qmail system.
5579
            $mailer->isQmail();
5580
        } else if (empty($CFG->smtphosts)) {
5581
            // Use PHP mail() = sendmail.
5582
            $mailer->isMail();
5583
        } else {
5584
            // Use SMTP directly.
5585
            $mailer->isSMTP();
5586
            if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5587
                $mailer->SMTPDebug = 3;
5588
            }
5589
            // Specify main and backup servers.
5590
            $mailer->Host          = $CFG->smtphosts;
5591
            // Specify secure connection protocol.
5592
            $mailer->SMTPSecure    = $CFG->smtpsecure;
5593
            // Use previous keepalive.
5594
            $mailer->SMTPKeepAlive = $prevkeepalive;
5595
 
5596
            if ($CFG->smtpuser) {
5597
                // Use SMTP authentication.
5598
                $mailer->SMTPAuth = true;
5599
                $mailer->Username = $CFG->smtpuser;
5600
                $mailer->Password = $CFG->smtppass;
5601
            }
5602
        }
5603
 
5604
        return $mailer;
5605
    }
5606
 
5607
    $nothing = null;
5608
 
5609
    // Keep smtp session open after sending.
5610
    if ($action == 'buffer') {
5611
        if (!empty($CFG->smtpmaxbulk)) {
5612
            get_mailer('flush');
5613
            $m = get_mailer();
5614
            if ($m->Mailer == 'smtp') {
5615
                $m->SMTPKeepAlive = true;
5616
            }
5617
        }
5618
        return $nothing;
5619
    }
5620
 
5621
    // Close smtp session, but continue buffering.
5622
    if ($action == 'flush') {
5623
        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5624
            if (!empty($mailer->SMTPDebug)) {
1326 ariadna 5625
                echo '<pre>' . "\n";
1 efrain 5626
            }
5627
            $mailer->SmtpClose();
5628
            if (!empty($mailer->SMTPDebug)) {
5629
                echo '</pre>';
5630
            }
5631
        }
5632
        return $nothing;
5633
    }
5634
 
5635
    // Close smtp session, do not buffer anymore.
5636
    if ($action == 'close') {
5637
        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5638
            get_mailer('flush');
5639
            $mailer->SMTPKeepAlive = false;
5640
        }
5641
        $mailer = null; // Better force new instance.
5642
        return $nothing;
5643
    }
5644
}
5645
 
5646
/**
5647
 * A helper function to test for email diversion
5648
 *
5649
 * @param string $email
5650
 * @return bool Returns true if the email should be diverted
5651
 */
1326 ariadna 5652
function email_should_be_diverted($email)
5653
{
1 efrain 5654
    global $CFG;
5655
 
5656
    if (empty($CFG->divertallemailsto)) {
5657
        return false;
5658
    }
5659
 
5660
    if (empty($CFG->divertallemailsexcept)) {
5661
        return true;
5662
    }
5663
 
5664
    $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY));
5665
    foreach ($patterns as $pattern) {
5666
        if (preg_match("/{$pattern}/i", $email)) {
5667
            return false;
5668
        }
5669
    }
5670
 
5671
    return true;
5672
}
5673
 
5674
/**
5675
 * Generate a unique email Message-ID using the moodle domain and install path
5676
 *
5677
 * @param string $localpart An optional unique message id prefix.
5678
 * @return string The formatted ID ready for appending to the email headers.
5679
 */
1326 ariadna 5680
function generate_email_messageid($localpart = null)
5681
{
1 efrain 5682
    global $CFG;
5683
 
5684
    $urlinfo = parse_url($CFG->wwwroot);
5685
    $base = '@' . $urlinfo['host'];
5686
 
5687
    // If multiple moodles are on the same domain we want to tell them
5688
    // apart so we add the install path to the local part. This means
5689
    // that the id local part should never contain a / character so
5690
    // we can correctly parse the id to reassemble the wwwroot.
5691
    if (isset($urlinfo['path'])) {
5692
        $base = $urlinfo['path'] . $base;
5693
    }
5694
 
5695
    if (empty($localpart)) {
5696
        $localpart = uniqid('', true);
5697
    }
5698
 
5699
    // Because we may have an option /installpath suffix to the local part
5700
    // of the id we need to escape any / chars which are in the $localpart.
5701
    $localpart = str_replace('/', '%2F', $localpart);
5702
 
5703
    return '<' . $localpart . $base . '>';
5704
}
5705
 
5706
/**
5707
 * Send an email to a specified user
5708
 *
5709
 * @param stdClass $user  A {@link $USER} object
5710
 * @param stdClass $from A {@link $USER} object
5711
 * @param string $subject plain text subject line of the email
5712
 * @param string $messagetext plain text version of the message
5713
 * @param string $messagehtml complete html version of the message (optional)
5714
 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
5715
 *          the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
5716
 * @param string $attachname the name of the file (extension indicates MIME)
5717
 * @param bool $usetrueaddress determines whether $from email address should
5718
 *          be sent out. Will be overruled by user profile setting for maildisplay
5719
 * @param string $replyto Email address to reply to
5720
 * @param string $replytoname Name of reply to recipient
5721
 * @param int $wordwrapwidth custom word wrap width, default 79
5722
 * @return bool Returns true if mail was sent OK and false if there was an error.
5723
 */
1326 ariadna 5724
function email_to_user(
5725
    $user,
5726
    $from,
5727
    $subject,
5728
    $messagetext,
5729
    $messagehtml = '',
5730
    $attachment = '',
5731
    $attachname = '',
5732
    $usetrueaddress = true,
5733
    $replyto = '',
5734
    $replytoname = '',
5735
    $wordwrapwidth = 79
5736
) {
1 efrain 5737
 
5738
    global $CFG, $PAGE, $SITE;
5739
 
5740
    if (empty($user) or empty($user->id)) {
5741
        debugging('Can not send email to null user', DEBUG_DEVELOPER);
5742
        return false;
5743
    }
5744
 
5745
    if (empty($user->email)) {
1326 ariadna 5746
        debugging('Can not send email to user without email: ' . $user->id, DEBUG_DEVELOPER);
1 efrain 5747
        return false;
5748
    }
5749
 
5750
    if (!empty($user->deleted)) {
1326 ariadna 5751
        debugging('Can not send email to deleted user: ' . $user->id, DEBUG_DEVELOPER);
1 efrain 5752
        return false;
5753
    }
5754
 
5755
    if (defined('BEHAT_SITE_RUNNING')) {
5756
        // Fake email sending in behat.
5757
        return true;
5758
    }
5759
 
5760
    if (!empty($CFG->noemailever)) {
5761
        // Hidden setting for development sites, set in config.php if needed.
5762
        debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
5763
        return true;
5764
    }
5765
 
5766
    if (email_should_be_diverted($user->email)) {
5767
        $subject = "[DIVERTED {$user->email}] $subject";
1326 ariadna 5768
        $user = clone ($user);
1 efrain 5769
        $user->email = $CFG->divertallemailsto;
5770
    }
5771
 
5772
    // Skip mail to suspended users.
1326 ariadna 5773
    if ((isset($user->auth) && $user->auth == 'nologin') or (isset($user->suspended) && $user->suspended)) {
1 efrain 5774
        return true;
5775
    }
5776
 
5777
    if (!validate_email($user->email)) {
5778
        // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
1326 ariadna 5779
        debugging("email_to_user: User $user->id (" . fullname($user) . ") email ($user->email) is invalid! Not sending.");
1 efrain 5780
        return false;
5781
    }
5782
 
5783
    if (over_bounce_threshold($user)) {
1326 ariadna 5784
        debugging("email_to_user: User $user->id (" . fullname($user) . ") is over bounce threshold! Not sending.");
1 efrain 5785
        return false;
5786
    }
5787
 
5788
    // TLD .invalid  is specifically reserved for invalid domain names.
5789
    // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
5790
    if (substr($user->email, -8) == '.invalid') {
1326 ariadna 5791
        debugging("email_to_user: User $user->id (" . fullname($user) . ") email domain ($user->email) is invalid! Not sending.");
1 efrain 5792
        return true; // This is not an error.
5793
    }
5794
 
5795
    // If the user is a remote mnet user, parse the email text for URL to the
5796
    // wwwroot and modify the url to direct the user's browser to login at their
5797
    // home site (identity provider - idp) before hitting the link itself.
5798
    if (is_mnet_remote_user($user)) {
1326 ariadna 5799
        require_once($CFG->dirroot . '/mnet/lib.php');
1 efrain 5800
 
5801
        $jumpurl = mnet_get_idp_jump_url($user);
5802
        $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5803
 
1326 ariadna 5804
        $messagetext = preg_replace_callback(
5805
            "%($CFG->wwwroot[^[:space:]]*)%",
5806
            $callback,
5807
            $messagetext
5808
        );
5809
        $messagehtml = preg_replace_callback(
5810
            "%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5811
            $callback,
5812
            $messagehtml
5813
        );
1 efrain 5814
    }
5815
    $mail = get_mailer();
5816
 
5817
    if (!empty($mail->SMTPDebug)) {
5818
        echo '<pre>' . "\n";
5819
    }
5820
 
5821
    $temprecipients = array();
5822
    $tempreplyto = array();
5823
 
5824
    // Make sure that we fall back onto some reasonable no-reply address.
5825
    $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
5826
    $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
5827
 
5828
    if (!validate_email($noreplyaddress)) {
1326 ariadna 5829
        debugging('email_to_user: Invalid noreply-email ' . s($noreplyaddress));
1 efrain 5830
        $noreplyaddress = $noreplyaddressdefault;
5831
    }
5832
 
5833
    // Make up an email address for handling bounces.
5834
    if (!empty($CFG->handlebounces)) {
1326 ariadna 5835
        $modargs = 'B' . base64_encode(pack('V', $user->id)) . substr(md5($user->email), 0, 16);
1 efrain 5836
        $mail->Sender = generate_email_processing_address(0, $modargs);
5837
    } else {
5838
        $mail->Sender = $noreplyaddress;
5839
    }
5840
 
5841
    // Make sure that the explicit replyto is valid, fall back to the implicit one.
5842
    if (!empty($replyto) && !validate_email($replyto)) {
1326 ariadna 5843
        debugging('email_to_user: Invalid replyto-email ' . s($replyto));
1 efrain 5844
        $replyto = $noreplyaddress;
5845
    }
5846
 
5847
    if (is_string($from)) { // So we can pass whatever we want if there is need.
5848
        $mail->From     = $noreplyaddress;
5849
        $mail->FromName = $from;
1326 ariadna 5850
        // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
5851
        // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
5852
        // in a course with the sender.
1 efrain 5853
    } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
5854
        if (!validate_email($from->email)) {
1326 ariadna 5855
            debugging('email_to_user: Invalid from-email ' . s($from->email) . ' - not sending');
1 efrain 5856
            // Better not to use $noreplyaddress in this case.
5857
            return false;
5858
        }
5859
        $mail->From = $from->email;
5860
        $fromdetails = new stdClass();
5861
        $fromdetails->name = fullname($from);
5862
        $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5863
        $fromdetails->siteshortname = format_string($SITE->shortname);
5864
        $fromstring = $fromdetails->name;
5865
        if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
5866
            $fromstring = get_string('emailvia', 'core', $fromdetails);
5867
        }
5868
        $mail->FromName = $fromstring;
5869
        if (empty($replyto)) {
5870
            $tempreplyto[] = array($from->email, fullname($from));
5871
        }
5872
    } else {
5873
        $mail->From = $noreplyaddress;
5874
        $fromdetails = new stdClass();
5875
        $fromdetails->name = fullname($from);
5876
        $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5877
        $fromdetails->siteshortname = format_string($SITE->shortname);
5878
        $fromstring = $fromdetails->name;
5879
        if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
5880
            $fromstring = get_string('emailvia', 'core', $fromdetails);
5881
        }
5882
        $mail->FromName = $fromstring;
5883
        if (empty($replyto)) {
5884
            $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
5885
        }
5886
    }
5887
 
5888
    if (!empty($replyto)) {
5889
        $tempreplyto[] = array($replyto, $replytoname);
5890
    }
5891
 
5892
    $temprecipients[] = array($user->email, fullname($user));
5893
 
5894
    // Set word wrap.
5895
    $mail->WordWrap = $wordwrapwidth;
5896
 
5897
    if (!empty($from->customheaders)) {
5898
        // Add custom headers.
5899
        if (is_array($from->customheaders)) {
5900
            foreach ($from->customheaders as $customheader) {
5901
                $mail->addCustomHeader($customheader);
5902
            }
5903
        } else {
5904
            $mail->addCustomHeader($from->customheaders);
5905
        }
5906
    }
5907
 
5908
    // If the X-PHP-Originating-Script email header is on then also add an additional
5909
    // header with details of where exactly in moodle the email was triggered from,
5910
    // either a call to message_send() or to email_to_user().
5911
    if (ini_get('mail.add_x_header')) {
5912
 
5913
        $stack = debug_backtrace(false);
5914
        $origin = $stack[0];
5915
 
5916
        foreach ($stack as $depth => $call) {
5917
            if ($call['function'] == 'message_send') {
5918
                $origin = $call;
5919
            }
5920
        }
5921
 
5922
        $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
1326 ariadna 5923
            . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
1 efrain 5924
        $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
5925
    }
5926
 
5927
    if (!empty($CFG->emailheaders)) {
5928
        $headers = array_map('trim', explode("\n", $CFG->emailheaders));
5929
        foreach ($headers as $header) {
5930
            if (!empty($header)) {
5931
                $mail->addCustomHeader($header);
5932
            }
5933
        }
5934
    }
5935
 
5936
    if (!empty($from->priority)) {
5937
        $mail->Priority = $from->priority;
5938
    }
5939
 
5940
    $renderer = $PAGE->get_renderer('core');
5941
    $context = array(
5942
        'sitefullname' => $SITE->fullname,
5943
        'siteshortname' => $SITE->shortname,
5944
        'sitewwwroot' => $CFG->wwwroot,
5945
        'subject' => $subject,
5946
        'prefix' => $CFG->emailsubjectprefix,
5947
        'to' => $user->email,
5948
        'toname' => fullname($user),
5949
        'from' => $mail->From,
5950
        'fromname' => $mail->FromName,
5951
    );
5952
    if (!empty($tempreplyto[0])) {
5953
        $context['replyto'] = $tempreplyto[0][0];
5954
        $context['replytoname'] = $tempreplyto[0][1];
5955
    }
5956
    if ($user->id > 0) {
5957
        $context['touserid'] = $user->id;
5958
        $context['tousername'] = $user->username;
5959
    }
5960
 
5961
    if (!empty($user->mailformat) && $user->mailformat == 1) {
5962
        // Only process html templates if the user preferences allow html email.
5963
 
5964
        if (!$messagehtml) {
5965
            // If no html has been given, BUT there is an html wrapping template then
5966
            // auto convert the text to html and then wrap it.
5967
            $messagehtml = trim(text_to_html($messagetext));
5968
        }
5969
        $context['body'] = $messagehtml;
5970
        $messagehtml = $renderer->render_from_template('core/email_html', $context);
5971
    }
5972
 
5973
    $context['body'] = html_to_text(nl2br($messagetext));
5974
    $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
5975
    $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
5976
    $messagetext = $renderer->render_from_template('core/email_text', $context);
5977
 
5978
    // Autogenerate a MessageID if it's missing.
5979
    if (empty($mail->MessageID)) {
5980
        $mail->MessageID = generate_email_messageid();
5981
    }
5982
 
5983
    if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
5984
        // Don't ever send HTML to users who don't want it.
1326 ariadna 5985
        // Agregar HEADER y FOOTER con imágenes al messagehtml
5986
 
5987
        // Rutas de las imágenes
5988
        $header_image_url = $CFG->wwwroot . '/theme/universe/pix/logo-horizontal-cesa.png'; // 👉 Imagen del Header
5989
        $footer_image_url = $CFG->wwwroot . '/theme/universe/pix/email-footer.jpg'; // 👉 Imagen del Footer
5990
 
5991
        // HTML del Header
5992
        $header_html = '<div style="text-align:center; margin-bottom:20px;">'
5993
            . '<img src="' . $header_image_url . '" alt="Header Image" style="max-width:100%; height:auto;">'
5994
            . '</div>';
5995
 
5996
        // HTML del Footer
5997
        $footer_html = '<div style="text-align:center; margin-top:40px;">'
5998
            . '<img src="' . $footer_image_url . '" alt="Footer Image" style="max-width:100%; height:auto;">'
5999
            . '</div>';
6000
 
6001
        // Unir: Header + Contenido original + Footer
6002
        $messagehtml = $header_html . $messagehtml . $footer_html;
1 efrain 6003
    } else {
6004
        $mail->IsHTML(false);
6005
        $mail->Body =  "\n$messagetext\n";
6006
    }
6007
 
6008
    if ($attachment && $attachname) {
1326 ariadna 6009
        if (preg_match("~\\.\\.~", $attachment)) {
1 efrain 6010
            // Security check for ".." in dir path.
6011
            $supportuser = core_user::get_support_user();
6012
            $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
6013
            $mail->addStringAttachment('Error in attachment.  User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
6014
        } else {
1326 ariadna 6015
            require_once($CFG->libdir . '/filelib.php');
1 efrain 6016
            $mimetype = mimeinfo('type', $attachname);
6017
 
6018
            // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
6019
            // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
6020
            $attachpath = str_replace('\\', '/', realpath($attachment));
6021
 
6022
            // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
1326 ariadna 6023
            $allowedpaths = array_map(function (string $path): string {
1 efrain 6024
                return str_replace('\\', '/', realpath($path));
6025
            }, [
6026
                $CFG->cachedir,
6027
                $CFG->dataroot,
6028
                $CFG->dirroot,
6029
                $CFG->localcachedir,
6030
                $CFG->tempdir,
6031
                $CFG->localrequestdir,
6032
            ]);
6033
 
6034
            // Set addpath to true.
6035
            $addpath = true;
6036
 
6037
            // Check if attachment includes one of the allowed paths.
6038
            foreach (array_filter($allowedpaths) as $allowedpath) {
6039
                // Set addpath to false if the attachment includes one of the allowed paths.
6040
                if (strpos($attachpath, $allowedpath) === 0) {
6041
                    $addpath = false;
6042
                    break;
6043
                }
6044
            }
6045
 
6046
            // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
6047
            // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
6048
            if ($addpath == true) {
6049
                $attachment = $CFG->dataroot . '/' . $attachment;
6050
            }
6051
 
6052
            $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
6053
        }
6054
    }
6055
 
6056
    // Check if the email should be sent in an other charset then the default UTF-8.
6057
    if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
6058
 
6059
        // Use the defined site mail charset or eventually the one preferred by the recipient.
6060
        $charset = $CFG->sitemailcharset;
6061
        if (!empty($CFG->allowusermailcharset)) {
6062
            if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
6063
                $charset = $useremailcharset;
6064
            }
6065
        }
6066
 
6067
        // Convert all the necessary strings if the charset is supported.
6068
        $charsets = get_list_of_charsets();
6069
        unset($charsets['UTF-8']);
6070
        if (in_array($charset, $charsets)) {
6071
            $mail->CharSet  = $charset;
6072
            $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
6073
            $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
6074
            $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
6075
            $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
6076
 
6077
            foreach ($temprecipients as $key => $values) {
6078
                $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6079
            }
6080
            foreach ($tempreplyto as $key => $values) {
6081
                $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6082
            }
6083
        }
6084
    }
6085
 
6086
    foreach ($temprecipients as $values) {
6087
        $mail->addAddress($values[0], $values[1]);
6088
    }
6089
    foreach ($tempreplyto as $values) {
6090
        $mail->addReplyTo($values[0], $values[1]);
6091
    }
6092
 
6093
    if (!empty($CFG->emaildkimselector)) {
6094
        $domain = substr(strrchr($mail->From, "@"), 1);
6095
        $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
6096
        if (file_exists($pempath)) {
6097
            $mail->DKIM_domain      = $domain;
6098
            $mail->DKIM_private     = $pempath;
6099
            $mail->DKIM_selector    = $CFG->emaildkimselector;
6100
            $mail->DKIM_identity    = $mail->From;
6101
        } else {
6102
            debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
6103
        }
6104
    }
6105
 
6106
    if ($mail->send()) {
6107
        set_send_count($user);
6108
        if (!empty($mail->SMTPDebug)) {
6109
            echo '</pre>';
6110
        }
6111
        return true;
6112
    } else {
6113
        // Trigger event for failing to send email.
6114
        $event = \core\event\email_failed::create(array(
6115
            'context' => context_system::instance(),
6116
            'userid' => $from->id,
6117
            'relateduserid' => $user->id,
6118
            'other' => array(
6119
                'subject' => $subject,
6120
                'message' => $messagetext,
6121
                'errorinfo' => $mail->ErrorInfo
6122
            )
6123
        ));
6124
        $event->trigger();
6125
        if (CLI_SCRIPT) {
1326 ariadna 6126
            mtrace('Error: lib/moodlelib.php email_to_user(): ' . $mail->ErrorInfo);
1 efrain 6127
        }
6128
        if (!empty($mail->SMTPDebug)) {
6129
            echo '</pre>';
6130
        }
6131
        return false;
6132
    }
6133
}
6134
 
6135
/**
6136
 * Check to see if a user's real email address should be used for the "From" field.
6137
 *
6138
 * @param  object $from The user object for the user we are sending the email from.
6139
 * @param  object $user The user object that we are sending the email to.
6140
 * @param  array $unused No longer used.
6141
 * @return bool Returns true if we can use the from user's email adress in the "From" field.
6142
 */
1326 ariadna 6143
function can_send_from_real_email_address($from, $user, $unused = null)
6144
{
1 efrain 6145
    global $CFG;
6146
    if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
6147
        return false;
6148
    }
6149
    $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
6150
    // Email is in the list of allowed domains for sending email,
6151
    // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6152
    // in a course with the sender.
1326 ariadna 6153
    if (
6154
        \core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
6155
        && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
6156
            || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
6157
                && enrol_get_shared_courses($user, $from, false, true)))
6158
    ) {
1 efrain 6159
        return true;
6160
    }
6161
    return false;
6162
}
6163
 
6164
/**
6165
 * Generate a signoff for emails based on support settings
6166
 *
6167
 * @return string
6168
 */
1326 ariadna 6169
function generate_email_signoff()
6170
{
1 efrain 6171
    global $CFG, $OUTPUT;
6172
 
6173
    $signoff = "\n";
6174
    if (!empty($CFG->supportname)) {
1326 ariadna 6175
        $signoff .= $CFG->supportname . "\n";
1 efrain 6176
    }
6177
 
6178
    $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']);
6179
 
6180
    if ($supportemail) {
6181
        $signoff .= "\n" . $supportemail . "\n";
6182
    }
6183
 
6184
    return $signoff;
6185
}
6186
 
6187
/**
6188
 * Sets specified user's password and send the new password to the user via email.
6189
 *
6190
 * @param stdClass $user A {@link $USER} object
6191
 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6192
 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6193
 */
1326 ariadna 6194
function setnew_password_and_mail($user, $fasthash = false)
6195
{
1 efrain 6196
    global $CFG, $DB;
6197
 
6198
    // We try to send the mail in language the user understands,
6199
    // unfortunately the filter_string() does not support alternative langs yet
6200
    // so multilang will not work properly for site->fullname.
6201
    $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6202
 
6203
    $site  = get_site();
6204
 
6205
    $supportuser = core_user::get_support_user();
6206
 
6207
    $newpassword = generate_password();
6208
 
6209
    update_internal_user_password($user, $newpassword, $fasthash);
6210
 
6211
    $a = new stdClass();
6212
    $a->firstname   = fullname($user, true);
6213
    $a->sitename    = format_string($site->fullname);
6214
    $a->username    = $user->username;
6215
    $a->newpassword = $newpassword;
1326 ariadna 6216
    $a->link        = $CFG->wwwroot . '/login/?lang=' . $lang;
1 efrain 6217
    $a->signoff     = generate_email_signoff();
6218
 
6219
    $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6220
 
1326 ariadna 6221
    $subject = format_string($site->fullname) . ': ' . (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
1 efrain 6222
 
6223
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6224
    return email_to_user($user, $supportuser, $subject, $message);
6225
}
6226
 
6227
/**
6228
 * Resets specified user's password and send the new password to the user via email.
6229
 *
6230
 * @param stdClass $user A {@link $USER} object
6231
 * @return bool Returns true if mail was sent OK and false if there was an error.
6232
 */
1326 ariadna 6233
function reset_password_and_mail($user)
6234
{
1 efrain 6235
    global $CFG;
6236
 
6237
    $site  = get_site();
6238
    $supportuser = core_user::get_support_user();
6239
 
6240
    $userauth = get_auth_plugin($user->auth);
6241
    if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6242
        trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6243
        return false;
6244
    }
6245
 
6246
    $newpassword = generate_password();
6247
 
6248
    if (!$userauth->user_update_password($user, $newpassword)) {
6249
        throw new \moodle_exception("cannotsetpassword");
6250
    }
6251
 
6252
    $a = new stdClass();
6253
    $a->firstname   = $user->firstname;
6254
    $a->lastname    = $user->lastname;
6255
    $a->sitename    = format_string($site->fullname);
6256
    $a->username    = $user->username;
6257
    $a->newpassword = $newpassword;
1326 ariadna 6258
    $a->link        = $CFG->wwwroot . '/login/change_password.php';
1 efrain 6259
    $a->signoff     = generate_email_signoff();
6260
 
6261
    $message = get_string('newpasswordtext', '', $a);
6262
 
1326 ariadna 6263
    $subject  = format_string($site->fullname) . ': ' . get_string('changedpassword');
1 efrain 6264
 
6265
    unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6266
 
6267
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6268
    return email_to_user($user, $supportuser, $subject, $message);
6269
}
6270
 
6271
/**
6272
 * Send email to specified user with confirmation text and activation link.
6273
 *
6274
 * @param stdClass $user A {@link $USER} object
6275
 * @param string $confirmationurl user confirmation URL
6276
 * @return bool Returns true if mail was sent OK and false if there was an error.
6277
 */
1326 ariadna 6278
function send_confirmation_email($user, $confirmationurl = null)
6279
{
1 efrain 6280
    global $CFG;
6281
 
6282
    $site = get_site();
6283
    $supportuser = core_user::get_support_user();
6284
 
6285
    $data = new stdClass();
6286
    $data->sitename  = format_string($site->fullname);
6287
    $data->admin     = generate_email_signoff();
6288
 
6289
    $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6290
 
6291
    if (empty($confirmationurl)) {
6292
        $confirmationurl = '/login/confirm.php';
6293
    }
6294
 
6295
    $confirmationurl = new moodle_url($confirmationurl);
6296
    // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6297
    $confirmationurl->remove_params('data');
6298
    $confirmationpath = $confirmationurl->out(false);
6299
 
6300
    // We need to custom encode the username to include trailing dots in the link.
6301
    // Because of this custom encoding we can't use moodle_url directly.
6302
    // Determine if a query string is present in the confirmation url.
6303
    $hasquerystring = strpos($confirmationpath, '?') !== false;
6304
    // Perform normal url encoding of the username first.
6305
    $username = urlencode($user->username);
6306
    // Prevent problems with trailing dots not being included as part of link in some mail clients.
6307
    $username = str_replace('.', '%2E', $username);
6308
 
1326 ariadna 6309
    $data->link = $confirmationpath . ($hasquerystring ? '&' : '?') . 'data=' . $user->secret . '/' . $username;
1 efrain 6310
 
6311
    $message     = get_string('emailconfirmation', '', $data);
6312
    $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6313
 
6314
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6315
    return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6316
}
6317
 
6318
/**
6319
 * Sends a password change confirmation email.
6320
 *
6321
 * @param stdClass $user A {@link $USER} object
6322
 * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6323
 * @return bool Returns true if mail was sent OK and false if there was an error.
6324
 */
1326 ariadna 6325
function send_password_change_confirmation_email($user, $resetrecord)
6326
{
1 efrain 6327
    global $CFG;
6328
 
6329
    $site = get_site();
6330
    $supportuser = core_user::get_support_user();
6331
    $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6332
 
6333
    $data = new stdClass();
6334
    $data->firstname = $user->firstname;
6335
    $data->lastname  = $user->lastname;
6336
    $data->username  = $user->username;
6337
    $data->sitename  = format_string($site->fullname);
1326 ariadna 6338
    $data->link      = $CFG->wwwroot . '/login/forgot_password.php?token=' . $resetrecord->token;
1 efrain 6339
    $data->admin     = generate_email_signoff();
6340
    $data->resetminutes = $pwresetmins;
6341
 
6342
    $message = get_string('emailresetconfirmation', '', $data);
6343
    $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6344
 
6345
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6346
    return email_to_user($user, $supportuser, $subject, $message);
6347
}
6348
 
6349
/**
6350
 * Sends an email containing information on how to change your password.
6351
 *
6352
 * @param stdClass $user A {@link $USER} object
6353
 * @return bool Returns true if mail was sent OK and false if there was an error.
6354
 */
1326 ariadna 6355
function send_password_change_info($user)
6356
{
1 efrain 6357
    $site = get_site();
6358
    $supportuser = core_user::get_support_user();
6359
 
6360
    $data = new stdClass();
6361
    $data->firstname = $user->firstname;
6362
    $data->lastname  = $user->lastname;
6363
    $data->username  = $user->username;
6364
    $data->sitename  = format_string($site->fullname);
6365
    $data->admin     = generate_email_signoff();
6366
 
6367
    if (!is_enabled_auth($user->auth)) {
6368
        $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6369
        $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6370
        // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6371
        return email_to_user($user, $supportuser, $subject, $message);
6372
    }
6373
 
6374
    $userauth = get_auth_plugin($user->auth);
6375
    ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6376
 
6377
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6378
    return email_to_user($user, $supportuser, $subject, $message);
6379
}
6380
 
6381
/**
6382
 * Check that an email is allowed.  It returns an error message if there was a problem.
6383
 *
6384
 * @param string $email Content of email
6385
 * @return string|false
6386
 */
1326 ariadna 6387
function email_is_not_allowed($email)
6388
{
1 efrain 6389
    global $CFG;
6390
 
6391
    // Comparing lowercase domains.
6392
    $email = strtolower($email);
6393
    if (!empty($CFG->allowemailaddresses)) {
6394
        $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6395
        foreach ($allowed as $allowedpattern) {
6396
            $allowedpattern = trim($allowedpattern);
6397
            if (!$allowedpattern) {
6398
                continue;
6399
            }
6400
            if (strpos($allowedpattern, '.') === 0) {
6401
                if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6402
                    // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6403
                    return false;
6404
                }
1326 ariadna 6405
            } else if (strpos(strrev($email), strrev('@' . $allowedpattern)) === 0) {
1 efrain 6406
                return false;
6407
            }
6408
        }
6409
        return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6410
    } else if (!empty($CFG->denyemailaddresses)) {
6411
        $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6412
        foreach ($denied as $deniedpattern) {
6413
            $deniedpattern = trim($deniedpattern);
6414
            if (!$deniedpattern) {
6415
                continue;
6416
            }
6417
            if (strpos($deniedpattern, '.') === 0) {
6418
                if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6419
                    // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6420
                    return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6421
                }
1326 ariadna 6422
            } else if (strpos(strrev($email), strrev('@' . $deniedpattern)) === 0) {
1 efrain 6423
                return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6424
            }
6425
        }
6426
    }
6427
 
6428
    return false;
6429
}
6430
 
6431
// FILE HANDLING.
6432
 
6433
/**
6434
 * Returns local file storage instance
6435
 *
6436
 * @return ?file_storage
6437
 */
1326 ariadna 6438
function get_file_storage($reset = false)
6439
{
1 efrain 6440
    global $CFG;
6441
 
6442
    static $fs = null;
6443
 
6444
    if ($reset) {
6445
        $fs = null;
6446
        return;
6447
    }
6448
 
6449
    if ($fs) {
6450
        return $fs;
6451
    }
6452
 
6453
    require_once("$CFG->libdir/filelib.php");
6454
 
6455
    $fs = new file_storage();
6456
 
6457
    return $fs;
6458
}
6459
 
6460
/**
6461
 * Returns local file storage instance
6462
 *
6463
 * @return file_browser
6464
 */
1326 ariadna 6465
function get_file_browser()
6466
{
1 efrain 6467
    global $CFG;
6468
 
6469
    static $fb = null;
6470
 
6471
    if ($fb) {
6472
        return $fb;
6473
    }
6474
 
6475
    require_once("$CFG->libdir/filelib.php");
6476
 
6477
    $fb = new file_browser();
6478
 
6479
    return $fb;
6480
}
6481
 
6482
/**
6483
 * Returns file packer
6484
 *
6485
 * @param string $mimetype default application/zip
6486
 * @return file_packer|false
6487
 */
1326 ariadna 6488
function get_file_packer($mimetype = 'application/zip')
6489
{
1 efrain 6490
    global $CFG;
6491
 
6492
    static $fp = array();
6493
 
6494
    if (isset($fp[$mimetype])) {
6495
        return $fp[$mimetype];
6496
    }
6497
 
6498
    switch ($mimetype) {
6499
        case 'application/zip':
6500
        case 'application/vnd.moodle.profiling':
6501
            $classname = 'zip_packer';
6502
            break;
6503
 
1326 ariadna 6504
        case 'application/x-gzip':
1 efrain 6505
            $classname = 'tgz_packer';
6506
            break;
6507
 
6508
        case 'application/vnd.moodle.backup':
6509
            $classname = 'mbz_packer';
6510
            break;
6511
 
6512
        default:
6513
            return false;
6514
    }
6515
 
6516
    require_once("$CFG->libdir/filestorage/$classname.php");
6517
    $fp[$mimetype] = new $classname();
6518
 
6519
    return $fp[$mimetype];
6520
}
6521
 
6522
/**
6523
 * Returns current name of file on disk if it exists.
6524
 *
6525
 * @param string $newfile File to be verified
6526
 * @return string Current name of file on disk if true
6527
 */
1326 ariadna 6528
function valid_uploaded_file($newfile)
6529
{
1 efrain 6530
    if (empty($newfile)) {
6531
        return '';
6532
    }
6533
    if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6534
        return $newfile['tmp_name'];
6535
    } else {
6536
        return '';
6537
    }
6538
}
6539
 
6540
/**
6541
 * Returns the maximum size for uploading files.
6542
 *
6543
 * There are seven possible upload limits:
6544
 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6545
 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6546
 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6547
 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6548
 * 5. by the Moodle admin in $CFG->maxbytes
6549
 * 6. by the teacher in the current course $course->maxbytes
6550
 * 7. by the teacher for the current module, eg $assignment->maxbytes
6551
 *
6552
 * These last two are passed to this function as arguments (in bytes).
6553
 * Anything defined as 0 is ignored.
6554
 * The smallest of all the non-zero numbers is returned.
6555
 *
6556
 * @todo Finish documenting this function
6557
 *
6558
 * @param int $sitebytes Set maximum size
6559
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6560
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6561
 * @param bool $unused This parameter has been deprecated and is not used any more.
6562
 * @return int The maximum size for uploading files.
6563
 */
1326 ariadna 6564
function get_max_upload_file_size($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $unused = false)
6565
{
1 efrain 6566
 
6567
    if (! $filesize = ini_get('upload_max_filesize')) {
6568
        $filesize = '5M';
6569
    }
6570
    $minimumsize = get_real_size($filesize);
6571
 
6572
    if ($postsize = ini_get('post_max_size')) {
6573
        $postsize = get_real_size($postsize);
6574
        if ($postsize < $minimumsize) {
6575
            $minimumsize = $postsize;
6576
        }
6577
    }
6578
 
6579
    if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6580
        $minimumsize = $sitebytes;
6581
    }
6582
 
6583
    if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6584
        $minimumsize = $coursebytes;
6585
    }
6586
 
6587
    if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6588
        $minimumsize = $modulebytes;
6589
    }
6590
 
6591
    return $minimumsize;
6592
}
6593
 
6594
/**
6595
 * Returns the maximum size for uploading files for the current user
6596
 *
6597
 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6598
 *
6599
 * @param context $context The context in which to check user capabilities
6600
 * @param int $sitebytes Set maximum size
6601
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6602
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6603
 * @param stdClass|int|null $user The user
6604
 * @param bool $unused This parameter has been deprecated and is not used any more.
6605
 * @return int The maximum size for uploading files.
6606
 */
1326 ariadna 6607
function get_user_max_upload_file_size(
6608
    $context,
6609
    $sitebytes = 0,
6610
    $coursebytes = 0,
6611
    $modulebytes = 0,
6612
    $user = null,
6613
    $unused = false
6614
) {
1 efrain 6615
    global $USER;
6616
 
6617
    if (empty($user)) {
6618
        $user = $USER;
6619
    }
6620
 
6621
    if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6622
        return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6623
    }
6624
 
6625
    return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6626
}
6627
 
6628
/**
6629
 * Returns an array of possible sizes in local language
6630
 *
6631
 * Related to {@link get_max_upload_file_size()} - this function returns an
6632
 * array of possible sizes in an array, translated to the
6633
 * local language.
6634
 *
6635
 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6636
 *
6637
 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6638
 * with the value set to 0. This option will be the first in the list.
6639
 *
6640
 * @uses SORT_NUMERIC
6641
 * @param int $sitebytes Set maximum size
6642
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6643
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6644
 * @param int|array $custombytes custom upload size/s which will be added to list,
6645
 *        Only value/s smaller then maxsize will be added to list.
6646
 * @return array
6647
 */
1326 ariadna 6648
function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null)
6649
{
1 efrain 6650
    global $CFG;
6651
 
6652
    if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6653
        return array();
6654
    }
6655
 
6656
    if ($sitebytes == 0) {
6657
        // Will get the minimum of upload_max_filesize or post_max_size.
6658
        $sitebytes = get_max_upload_file_size();
6659
    }
6660
 
6661
    $filesize = array();
1326 ariadna 6662
    $sizelist = array(
6663
        10240,
6664
        51200,
6665
        102400,
6666
        512000,
6667
        1048576,
6668
        2097152,
6669
        5242880,
6670
        10485760,
6671
        20971520,
6672
        52428800,
6673
        104857600,
6674
        262144000,
6675
        524288000,
6676
        786432000,
6677
        1073741824,
6678
        2147483648,
6679
        4294967296,
6680
        8589934592
6681
    );
1 efrain 6682
 
6683
    // If custombytes is given and is valid then add it to the list.
6684
    if (is_number($custombytes) and $custombytes > 0) {
6685
        $custombytes = (int)$custombytes;
6686
        if (!in_array($custombytes, $sizelist)) {
6687
            $sizelist[] = $custombytes;
6688
        }
6689
    } else if (is_array($custombytes)) {
6690
        $sizelist = array_unique(array_merge($sizelist, $custombytes));
6691
    }
6692
 
6693
    // Allow maxbytes to be selected if it falls outside the above boundaries.
6694
    if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6695
        // Note: get_real_size() is used in order to prevent problems with invalid values.
6696
        $sizelist[] = get_real_size($CFG->maxbytes);
6697
    }
6698
 
6699
    foreach ($sizelist as $sizebytes) {
6700
        if ($sizebytes < $maxsize && $sizebytes > 0) {
6701
            $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6702
        }
6703
    }
6704
 
6705
    $limitlevel = '';
6706
    $displaysize = '';
1326 ariadna 6707
    if (
6708
        $modulebytes &&
1 efrain 6709
        (($modulebytes < $coursebytes || $coursebytes == 0) &&
1326 ariadna 6710
            ($modulebytes < $sitebytes || $sitebytes == 0))
6711
    ) {
1 efrain 6712
        $limitlevel = get_string('activity', 'core');
6713
        $displaysize = display_size($modulebytes, 0);
6714
        $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6715
 
6716
    } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6717
        $limitlevel = get_string('course', 'core');
6718
        $displaysize = display_size($coursebytes, 0);
6719
        $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6720
 
6721
    } else if ($sitebytes) {
6722
        $limitlevel = get_string('site', 'core');
6723
        $displaysize = display_size($sitebytes, 0);
6724
        $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6725
    }
6726
 
6727
    krsort($filesize, SORT_NUMERIC);
6728
    if ($limitlevel) {
6729
        $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6730
        $filesize  = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6731
    }
6732
 
6733
    return $filesize;
6734
}
6735
 
6736
/**
6737
 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6738
 *
6739
 * If excludefiles is defined, then that file/directory is ignored
6740
 * If getdirs is true, then (sub)directories are included in the output
6741
 * If getfiles is true, then files are included in the output
6742
 * (at least one of these must be true!)
6743
 *
6744
 * @todo Finish documenting this function. Add examples of $excludefile usage.
6745
 *
6746
 * @param string $rootdir A given root directory to start from
6747
 * @param string|array $excludefiles If defined then the specified file/directory is ignored
6748
 * @param bool $descend If true then subdirectories are recursed as well
6749
 * @param bool $getdirs If true then (sub)directories are included in the output
6750
 * @param bool $getfiles  If true then files are included in the output
6751
 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6752
 */
1326 ariadna 6753
function get_directory_list($rootdir, $excludefiles = '', $descend = true, $getdirs = false, $getfiles = true)
6754
{
1 efrain 6755
 
6756
    $dirs = array();
6757
 
6758
    if (!$getdirs and !$getfiles) {   // Nothing to show.
6759
        return $dirs;
6760
    }
6761
 
6762
    if (!is_dir($rootdir)) {          // Must be a directory.
6763
        return $dirs;
6764
    }
6765
 
6766
    if (!$dir = opendir($rootdir)) {  // Can't open it for some reason.
6767
        return $dirs;
6768
    }
6769
 
6770
    if (!is_array($excludefiles)) {
6771
        $excludefiles = array($excludefiles);
6772
    }
6773
 
6774
    while (false !== ($file = readdir($dir))) {
6775
        $firstchar = substr($file, 0, 1);
6776
        if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6777
            continue;
6778
        }
1326 ariadna 6779
        $fullfile = $rootdir . '/' . $file;
1 efrain 6780
        if (filetype($fullfile) == 'dir') {
6781
            if ($getdirs) {
6782
                $dirs[] = $file;
6783
            }
6784
            if ($descend) {
6785
                $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6786
                foreach ($subdirs as $subdir) {
1326 ariadna 6787
                    $dirs[] = $file . '/' . $subdir;
1 efrain 6788
                }
6789
            }
6790
        } else if ($getfiles) {
6791
            $dirs[] = $file;
6792
        }
6793
    }
6794
    closedir($dir);
6795
 
6796
    asort($dirs);
6797
 
6798
    return $dirs;
6799
}
6800
 
6801
 
6802
/**
6803
 * Adds up all the files in a directory and works out the size.
6804
 *
6805
 * @param string $rootdir  The directory to start from
6806
 * @param string $excludefile A file to exclude when summing directory size
6807
 * @return int The summed size of all files and subfiles within the root directory
6808
 */
1326 ariadna 6809
function get_directory_size($rootdir, $excludefile = '')
6810
{
1 efrain 6811
    global $CFG;
6812
 
6813
    // Do it this way if we can, it's much faster.
6814
    if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
1326 ariadna 6815
        $command = trim($CFG->pathtodu) . ' -sk ' . escapeshellarg($rootdir);
1 efrain 6816
        $output = null;
6817
        $return = null;
6818
        exec($command, $output, $return);
6819
        if (is_array($output)) {
6820
            // We told it to return k.
1326 ariadna 6821
            return get_real_size(intval($output[0]) . 'k');
1 efrain 6822
        }
6823
    }
6824
 
6825
    if (!is_dir($rootdir)) {
6826
        // Must be a directory.
6827
        return 0;
6828
    }
6829
 
6830
    if (!$dir = @opendir($rootdir)) {
6831
        // Can't open it for some reason.
6832
        return 0;
6833
    }
6834
 
6835
    $size = 0;
6836
 
6837
    while (false !== ($file = readdir($dir))) {
6838
        $firstchar = substr($file, 0, 1);
6839
        if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6840
            continue;
6841
        }
1326 ariadna 6842
        $fullfile = $rootdir . '/' . $file;
1 efrain 6843
        if (filetype($fullfile) == 'dir') {
6844
            $size += get_directory_size($fullfile, $excludefile);
6845
        } else {
6846
            $size += filesize($fullfile);
6847
        }
6848
    }
6849
    closedir($dir);
6850
 
6851
    return $size;
6852
}
6853
 
6854
/**
6855
 * Converts bytes into display form
6856
 *
6857
 * @param int $size  The size to convert to human readable form
6858
 * @param int $decimalplaces If specified, uses fixed number of decimal places
6859
 * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
6860
 * @return string Display version of size
6861
 */
1326 ariadna 6862
function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string
6863
{
1 efrain 6864
 
6865
    static $units;
6866
 
6867
    if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6868
        return get_string('unlimited');
6869
    }
6870
 
6871
    if (empty($units)) {
6872
        $units[] = get_string('sizeb');
6873
        $units[] = get_string('sizekb');
6874
        $units[] = get_string('sizemb');
6875
        $units[] = get_string('sizegb');
6876
        $units[] = get_string('sizetb');
6877
        $units[] = get_string('sizepb');
6878
    }
6879
 
6880
    switch ($fixedunits) {
1326 ariadna 6881
        case 'PB':
1 efrain 6882
            $magnitude = 5;
6883
            break;
1326 ariadna 6884
        case 'TB':
1 efrain 6885
            $magnitude = 4;
6886
            break;
1326 ariadna 6887
        case 'GB':
1 efrain 6888
            $magnitude = 3;
6889
            break;
1326 ariadna 6890
        case 'MB':
1 efrain 6891
            $magnitude = 2;
6892
            break;
1326 ariadna 6893
        case 'KB':
1 efrain 6894
            $magnitude = 1;
6895
            break;
1326 ariadna 6896
        case 'B':
1 efrain 6897
            $magnitude = 0;
6898
            break;
6899
        case '':
6900
            $magnitude = floor(log($size, 1024));
6901
            $magnitude = max(0, min(5, $magnitude));
6902
            break;
6903
        default:
6904
            throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
6905
    }
6906
 
6907
    // Special case for magnitude 0 (bytes) - never use decimal places.
6908
    $nbsp = "\xc2\xa0";
6909
    if ($magnitude === 0) {
6910
        return round($size) . $nbsp . $units[$magnitude];
6911
    }
6912
 
6913
    // Convert to specified units.
6914
    $sizeinunit = $size / 1024 ** $magnitude;
6915
 
6916
    // Fixed decimal places.
6917
    return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
6918
}
6919
 
6920
/**
6921
 * Cleans a given filename by removing suspicious or troublesome characters
6922
 *
6923
 * @see clean_param()
6924
 * @param string $string file name
6925
 * @return string cleaned file name
6926
 */
1326 ariadna 6927
function clean_filename($string)
6928
{
1 efrain 6929
    return clean_param($string, PARAM_FILE);
6930
}
6931
 
6932
// STRING TRANSLATION.
6933
 
6934
/**
6935
 * Returns the code for the current language
6936
 *
6937
 * @category string
6938
 * @return string
6939
 */
1326 ariadna 6940
function current_language()
6941
{
1 efrain 6942
    global $CFG, $PAGE, $SESSION, $USER;
6943
 
6944
    if (!empty($SESSION->forcelang)) {
6945
        // Allows overriding course-forced language (useful for admins to check
6946
        // issues in courses whose language they don't understand).
6947
        // Also used by some code to temporarily get language-related information in a
6948
        // specific language (see force_current_language()).
6949
        $return = $SESSION->forcelang;
6950
    } else if (!empty($PAGE->cm->lang)) {
6951
        // Activity language, if set.
6952
        $return = $PAGE->cm->lang;
6953
    } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) {
6954
        // Course language can override all other settings for this page.
6955
        $return = $PAGE->course->lang;
6956
    } else if (!empty($SESSION->lang)) {
6957
        // Session language can override other settings.
6958
        $return = $SESSION->lang;
6959
    } else if (!empty($USER->lang)) {
6960
        $return = $USER->lang;
6961
    } else if (isset($CFG->lang)) {
6962
        $return = $CFG->lang;
6963
    } else {
6964
        $return = 'en';
6965
    }
6966
 
6967
    // Just in case this slipped in from somewhere by accident.
6968
    $return = str_replace('_utf8', '', $return);
6969
 
6970
    return $return;
6971
}
6972
 
6973
/**
6974
 * Fix the current language to the given language code.
6975
 *
6976
 * @param string $lang The language code to use.
6977
 * @return void
6978
 */
1326 ariadna 6979
function fix_current_language(string $lang): void
6980
{
1 efrain 6981
    global $CFG, $COURSE, $SESSION, $USER;
6982
 
6983
    if (!get_string_manager()->translation_exists($lang)) {
6984
        throw new coding_exception("The language pack for $lang is not available");
6985
    }
6986
 
6987
    $fixglobal = '';
6988
    $fixlang = 'lang';
6989
    if (!empty($SESSION->forcelang)) {
6990
        $fixglobal = $SESSION;
6991
        $fixlang = 'forcelang';
6992
    } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) {
6993
        $fixglobal = $COURSE;
6994
    } else if (!empty($SESSION->lang)) {
6995
        $fixglobal = $SESSION;
6996
    } else if (!empty($USER->lang)) {
6997
        $fixglobal = $USER;
6998
    } else if (isset($CFG->lang)) {
6999
        set_config('lang', $lang);
7000
    }
7001
 
7002
    if ($fixglobal) {
7003
        $fixglobal->$fixlang = $lang;
7004
    }
7005
}
7006
 
7007
/**
7008
 * Returns parent language of current active language if defined
7009
 *
7010
 * @category string
7011
 * @param string $lang null means current language
7012
 * @return string
7013
 */
1326 ariadna 7014
function get_parent_language($lang = null)
7015
{
1 efrain 7016
 
7017
    $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
7018
 
7019
    if ($parentlang === 'en') {
7020
        $parentlang = '';
7021
    }
7022
 
7023
    return $parentlang;
7024
}
7025
 
7026
/**
7027
 * Force the current language to get strings and dates localised in the given language.
7028
 *
7029
 * After calling this function, all strings will be provided in the given language
7030
 * until this function is called again, or equivalent code is run.
7031
 *
7032
 * @param string $language
7033
 * @return string previous $SESSION->forcelang value
7034
 */
1326 ariadna 7035
function force_current_language($language)
7036
{
1 efrain 7037
    global $SESSION;
7038
    $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
7039
    if ($language !== $sessionforcelang) {
7040
        // Setting forcelang to null or an empty string disables its effect.
7041
        if (empty($language) || get_string_manager()->translation_exists($language, false)) {
7042
            $SESSION->forcelang = $language;
7043
            moodle_setlocale();
7044
        }
7045
    }
7046
    return $sessionforcelang;
7047
}
7048
 
7049
/**
7050
 * Returns current string_manager instance.
7051
 *
7052
 * The param $forcereload is needed for CLI installer only where the string_manager instance
7053
 * must be replaced during the install.php script life time.
7054
 *
7055
 * @category string
7056
 * @param bool $forcereload shall the singleton be released and new instance created instead?
7057
 * @return core_string_manager
7058
 */
1326 ariadna 7059
function get_string_manager($forcereload = false)
7060
{
1 efrain 7061
    global $CFG;
7062
 
7063
    static $singleton = null;
7064
 
7065
    if ($forcereload) {
7066
        $singleton = null;
7067
    }
7068
    if ($singleton === null) {
7069
        if (empty($CFG->early_install_lang)) {
7070
 
7071
            $transaliases = array();
7072
            if (empty($CFG->langlist)) {
1326 ariadna 7073
                $translist = array();
1 efrain 7074
            } else {
7075
                $translist = explode(',', $CFG->langlist);
7076
                $translist = array_map('trim', $translist);
7077
                // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
7078
                foreach ($translist as $i => $value) {
7079
                    $parts = preg_split('/\s*\|\s*/', $value, 2);
7080
                    if (count($parts) == 2) {
7081
                        $transaliases[$parts[0]] = $parts[1];
7082
                        $translist[$i] = $parts[0];
7083
                    }
7084
                }
7085
            }
7086
 
7087
            if (!empty($CFG->config_php_settings['customstringmanager'])) {
7088
                $classname = $CFG->config_php_settings['customstringmanager'];
7089
 
7090
                if (class_exists($classname)) {
7091
                    $implements = class_implements($classname);
7092
 
7093
                    if (isset($implements['core_string_manager'])) {
7094
                        $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7095
                        return $singleton;
7096
                    } else {
1326 ariadna 7097
                        debugging('Unable to instantiate custom string manager: class ' . $classname .
1 efrain 7098
                            ' does not implement the core_string_manager interface.');
7099
                    }
7100
                } else {
1326 ariadna 7101
                    debugging('Unable to instantiate custom string manager: class ' . $classname . ' can not be found.');
1 efrain 7102
                }
7103
            }
7104
 
7105
            $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7106
        } else {
7107
            $singleton = new core_string_manager_install();
7108
        }
7109
    }
7110
 
7111
    return $singleton;
7112
}
7113
 
7114
/**
7115
 * Returns a localized string.
7116
 *
7117
 * Returns the translated string specified by $identifier as
7118
 * for $module.  Uses the same format files as STphp.
7119
 * $a is an object, string or number that can be used
7120
 * within translation strings
7121
 *
7122
 * eg 'hello {$a->firstname} {$a->lastname}'
7123
 * or 'hello {$a}'
7124
 *
7125
 * If you would like to directly echo the localized string use
7126
 * the function {@link print_string()}
7127
 *
7128
 * Example usage of this function involves finding the string you would
7129
 * like a local equivalent of and using its identifier and module information
7130
 * to retrieve it.<br/>
7131
 * If you open moodle/lang/en/moodle.php and look near line 278
7132
 * you will find a string to prompt a user for their word for 'course'
7133
 * <code>
7134
 * $string['course'] = 'Course';
7135
 * </code>
7136
 * So if you want to display the string 'Course'
7137
 * in any language that supports it on your site
7138
 * you just need to use the identifier 'course'
7139
 * <code>
7140
 * $mystring = '<strong>'. get_string('course') .'</strong>';
7141
 * or
7142
 * </code>
7143
 * If the string you want is in another file you'd take a slightly
7144
 * different approach. Looking in moodle/lang/en/calendar.php you find
7145
 * around line 75:
7146
 * <code>
7147
 * $string['typecourse'] = 'Course event';
7148
 * </code>
7149
 * If you want to display the string "Course event" in any language
7150
 * supported you would use the identifier 'typecourse' and the module 'calendar'
7151
 * (because it is in the file calendar.php):
7152
 * <code>
7153
 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7154
 * </code>
7155
 *
7156
 * As a last resort, should the identifier fail to map to a string
7157
 * the returned string will be [[ $identifier ]]
7158
 *
7159
 * In Moodle 2.3 there is a new argument to this function $lazyload.
7160
 * Setting $lazyload to true causes get_string to return a lang_string object
7161
 * rather than the string itself. The fetching of the string is then put off until
7162
 * the string object is first used. The object can be used by calling it's out
7163
 * method or by casting the object to a string, either directly e.g.
7164
 *     (string)$stringobject
7165
 * or indirectly by using the string within another string or echoing it out e.g.
7166
 *     echo $stringobject
7167
 *     return "<p>{$stringobject}</p>";
7168
 * It is worth noting that using $lazyload and attempting to use the string as an
7169
 * array key will cause a fatal error as objects cannot be used as array keys.
7170
 * But you should never do that anyway!
7171
 * For more information {@link lang_string}
7172
 *
7173
 * @category string
7174
 * @param string $identifier The key identifier for the localized string
7175
 * @param string $component The module where the key identifier is stored,
7176
 *      usually expressed as the filename in the language pack without the
7177
 *      .php on the end but can also be written as mod/forum or grade/export/xls.
7178
 *      If none is specified then moodle.php is used.
7179
 * @param string|object|array|int $a An object, string or number that can be used
7180
 *      within translation strings
7181
 * @param bool $lazyload If set to true a string object is returned instead of
7182
 *      the string itself. The string then isn't calculated until it is first used.
7183
 * @return string The localized string.
7184
 * @throws coding_exception
7185
 */
1326 ariadna 7186
function get_string($identifier, $component = '', $a = null, $lazyload = false)
7187
{
1 efrain 7188
    global $CFG;
7189
 
7190
    // If the lazy load argument has been supplied return a lang_string object
7191
    // instead.
7192
    // We need to make sure it is true (and a bool) as you will see below there
7193
    // used to be a forth argument at one point.
7194
    if ($lazyload === true) {
7195
        return new lang_string($identifier, $component, $a);
7196
    }
7197
 
7198
    if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
7199
        throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
7200
    }
7201
 
7202
    // There is now a forth argument again, this time it is a boolean however so
7203
    // we can still check for the old extralocations parameter.
7204
    if (!is_bool($lazyload) && !empty($lazyload)) {
7205
        debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7206
    }
7207
 
7208
    if (strpos((string)$component, '/') !== false) {
7209
        debugging('The module name you passed to get_string is the deprecated format ' .
1326 ariadna 7210
            'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.', DEBUG_DEVELOPER);
1 efrain 7211
        $componentpath = explode('/', $component);
7212
 
7213
        switch ($componentpath[0]) {
7214
            case 'mod':
7215
                $component = $componentpath[1];
7216
                break;
7217
            case 'blocks':
7218
            case 'block':
1326 ariadna 7219
                $component = 'block_' . $componentpath[1];
1 efrain 7220
                break;
7221
            case 'enrol':
1326 ariadna 7222
                $component = 'enrol_' . $componentpath[1];
1 efrain 7223
                break;
7224
            case 'format':
1326 ariadna 7225
                $component = 'format_' . $componentpath[1];
1 efrain 7226
                break;
7227
            case 'grade':
1326 ariadna 7228
                $component = 'grade' . $componentpath[1] . '_' . $componentpath[2];
1 efrain 7229
                break;
7230
        }
7231
    }
7232
 
7233
    $result = get_string_manager()->get_string($identifier, $component, $a);
7234
 
7235
    // Debugging feature lets you display string identifier and component.
7236
    if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7237
        $result .= ' {' . $identifier . '/' . $component . '}';
7238
    }
7239
    return $result;
7240
}
7241
 
7242
/**
7243
 * Converts an array of strings to their localized value.
7244
 *
7245
 * @param array $array An array of strings
7246
 * @param string $component The language module that these strings can be found in.
7247
 * @return stdClass translated strings.
7248
 */
1326 ariadna 7249
function get_strings($array, $component = '')
7250
{
1 efrain 7251
    $string = new stdClass;
7252
    foreach ($array as $item) {
7253
        $string->$item = get_string($item, $component);
7254
    }
7255
    return $string;
7256
}
7257
 
7258
/**
7259
 * Prints out a translated string.
7260
 *
7261
 * Prints out a translated string using the return value from the {@link get_string()} function.
7262
 *
7263
 * Example usage of this function when the string is in the moodle.php file:<br/>
7264
 * <code>
7265
 * echo '<strong>';
7266
 * print_string('course');
7267
 * echo '</strong>';
7268
 * </code>
7269
 *
7270
 * Example usage of this function when the string is not in the moodle.php file:<br/>
7271
 * <code>
7272
 * echo '<h1>';
7273
 * print_string('typecourse', 'calendar');
7274
 * echo '</h1>';
7275
 * </code>
7276
 *
7277
 * @category string
7278
 * @param string $identifier The key identifier for the localized string
7279
 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7280
 * @param string|object|array $a An object, string or number that can be used within translation strings
7281
 */
1326 ariadna 7282
function print_string($identifier, $component = '', $a = null)
7283
{
1 efrain 7284
    echo get_string($identifier, $component, $a);
7285
}
7286
 
7287
/**
7288
 * Returns a list of charset codes
7289
 *
7290
 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7291
 * (checking that such charset is supported by the texlib library!)
7292
 *
7293
 * @return array And associative array with contents in the form of charset => charset
7294
 */
1326 ariadna 7295
function get_list_of_charsets()
7296
{
1 efrain 7297
 
7298
    $charsets = array(
7299
        'EUC-JP'     => 'EUC-JP',
1326 ariadna 7300
        'ISO-2022-JP' => 'ISO-2022-JP',
1 efrain 7301
        'ISO-8859-1' => 'ISO-8859-1',
7302
        'SHIFT-JIS'  => 'SHIFT-JIS',
7303
        'GB2312'     => 'GB2312',
7304
        'GB18030'    => 'GB18030', // GB18030 not supported by typo and mbstring.
1326 ariadna 7305
        'UTF-8'      => 'UTF-8'
7306
    );
1 efrain 7307
 
7308
    asort($charsets);
7309
 
7310
    return $charsets;
7311
}
7312
 
7313
/**
7314
 * Returns a list of valid and compatible themes
7315
 *
7316
 * @return array
7317
 */
1326 ariadna 7318
function get_list_of_themes()
7319
{
1 efrain 7320
    global $CFG;
7321
 
7322
    $themes = array();
7323
 
7324
    if (!empty($CFG->themelist)) {       // Use admin's list of themes.
7325
        $themelist = explode(',', $CFG->themelist);
7326
    } else {
7327
        $themelist = array_keys(core_component::get_plugin_list("theme"));
7328
    }
7329
 
7330
    foreach ($themelist as $key => $themename) {
7331
        $theme = theme_config::load($themename);
7332
        $themes[$themename] = $theme;
7333
    }
7334
 
7335
    core_collator::asort_objects_by_method($themes, 'get_theme_name');
7336
 
7337
    return $themes;
7338
}
7339
 
7340
/**
7341
 * Factory function for emoticon_manager
7342
 *
7343
 * @return emoticon_manager singleton
7344
 */
1326 ariadna 7345
function get_emoticon_manager()
7346
{
1 efrain 7347
    static $singleton = null;
7348
 
7349
    if (is_null($singleton)) {
7350
        $singleton = new emoticon_manager();
7351
    }
7352
 
7353
    return $singleton;
7354
}
7355
 
7356
/**
7357
 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7358
 *
7359
 * Whenever this manager mentiones 'emoticon object', the following data
7360
 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7361
 * altidentifier and altcomponent
7362
 *
7363
 * @see admin_setting_emoticons
7364
 *
7365
 * @copyright 2010 David Mudrak
7366
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7367
 */
1326 ariadna 7368
class emoticon_manager
7369
{
1 efrain 7370
 
7371
    /**
7372
     * Returns the currently enabled emoticons
7373
     *
7374
     * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7375
     * @return array of emoticon objects
7376
     */
1326 ariadna 7377
    public function get_emoticons($selectable = false)
7378
    {
1 efrain 7379
        global $CFG;
7380
        $notselectable = ['martin', 'egg'];
7381
 
7382
        if (empty($CFG->emoticons)) {
7383
            return array();
7384
        }
7385
 
7386
        $emoticons = $this->decode_stored_config($CFG->emoticons);
7387
 
7388
        if (!is_array($emoticons)) {
7389
            // Something is wrong with the format of stored setting.
7390
            debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7391
            return array();
7392
        }
7393
        if ($selectable) {
7394
            foreach ($emoticons as $index => $emote) {
7395
                if (in_array($emote->altidentifier, $notselectable)) {
7396
                    // Skip this one.
7397
                    unset($emoticons[$index]);
7398
                }
7399
            }
7400
        }
7401
 
7402
        return $emoticons;
7403
    }
7404
 
7405
    /**
7406
     * Converts emoticon object into renderable pix_emoticon object
7407
     *
7408
     * @param stdClass $emoticon emoticon object
7409
     * @param array $attributes explicit HTML attributes to set
7410
     * @return pix_emoticon
7411
     */
1326 ariadna 7412
    public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array())
7413
    {
1 efrain 7414
        $stringmanager = get_string_manager();
7415
        if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7416
            $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7417
        } else {
7418
            $alt = s($emoticon->text);
7419
        }
7420
        return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7421
    }
7422
 
7423
    /**
7424
     * Encodes the array of emoticon objects into a string storable in config table
7425
     *
7426
     * @see self::decode_stored_config()
7427
     * @param array $emoticons array of emtocion objects
7428
     * @return string
7429
     */
1326 ariadna 7430
    public function encode_stored_config(array $emoticons)
7431
    {
1 efrain 7432
        return json_encode($emoticons);
7433
    }
7434
 
7435
    /**
7436
     * Decodes the string into an array of emoticon objects
7437
     *
7438
     * @see self::encode_stored_config()
7439
     * @param string $encoded
7440
     * @return array|null
7441
     */
1326 ariadna 7442
    public function decode_stored_config($encoded)
7443
    {
1 efrain 7444
        $decoded = json_decode($encoded);
7445
        if (!is_array($decoded)) {
7446
            return null;
7447
        }
7448
        return $decoded;
7449
    }
7450
 
7451
    /**
7452
     * Returns default set of emoticons supported by Moodle
7453
     *
7454
     * @return array of sdtClasses
7455
     */
1326 ariadna 7456
    public function default_emoticons()
7457
    {
1 efrain 7458
        return array(
7459
            $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7460
            $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7461
            $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7462
            $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7463
            $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7464
            $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7465
            $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7466
            $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7467
            $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7468
            $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7469
            $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7470
            $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7471
            $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7472
            $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7473
            $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7474
            $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7475
            $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7476
            $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7477
            $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7478
            $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7479
            $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7480
            $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7481
            $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7482
            $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7483
            $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7484
            $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7485
            $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7486
            $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7487
            $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7488
            $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7489
        );
7490
    }
7491
 
7492
    /**
7493
     * Helper method preparing the stdClass with the emoticon properties
7494
     *
7495
     * @param string|array $text or array of strings
7496
     * @param string $imagename to be used by {@link pix_emoticon}
7497
     * @param string $altidentifier alternative string identifier, null for no alt
7498
     * @param string $altcomponent where the alternative string is defined
7499
     * @param string $imagecomponent to be used by {@link pix_emoticon}
7500
     * @return stdClass
7501
     */
1326 ariadna 7502
    protected function prepare_emoticon_object(
7503
        $text,
7504
        $imagename,
7505
        $altidentifier = null,
7506
        $altcomponent = 'core_pix',
7507
        $imagecomponent = 'core'
7508
    ) {
1 efrain 7509
        return (object)array(
7510
            'text'           => $text,
7511
            'imagename'      => $imagename,
7512
            'imagecomponent' => $imagecomponent,
7513
            'altidentifier'  => $altidentifier,
7514
            'altcomponent'   => $altcomponent,
7515
        );
7516
    }
7517
}
7518
 
7519
// ENCRYPTION.
7520
 
7521
/**
7522
 * rc4encrypt
7523
 *
7524
 * @param string $data        Data to encrypt.
7525
 * @return string             The now encrypted data.
7526
 */
1326 ariadna 7527
function rc4encrypt($data)
7528
{
1 efrain 7529
    return endecrypt(get_site_identifier(), $data, '');
7530
}
7531
 
7532
/**
7533
 * rc4decrypt
7534
 *
7535
 * @param string $data        Data to decrypt.
7536
 * @return string             The now decrypted data.
7537
 */
1326 ariadna 7538
function rc4decrypt($data)
7539
{
1 efrain 7540
    return endecrypt(get_site_identifier(), $data, 'de');
7541
}
7542
 
7543
/**
7544
 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7545
 *
7546
 * @todo Finish documenting this function
7547
 *
7548
 * @param string $pwd The password to use when encrypting or decrypting
7549
 * @param string $data The data to be decrypted/encrypted
7550
 * @param string $case Either 'de' for decrypt or '' for encrypt
7551
 * @return string
7552
 */
1326 ariadna 7553
function endecrypt($pwd, $data, $case)
7554
{
1 efrain 7555
 
7556
    if ($case == 'de') {
7557
        $data = urldecode($data);
7558
    }
7559
 
7560
    $key[] = '';
7561
    $box[] = '';
7562
    $pwdlength = strlen($pwd);
7563
 
7564
    for ($i = 0; $i <= 255; $i++) {
7565
        $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7566
        $box[$i] = $i;
7567
    }
7568
 
7569
    $x = 0;
7570
 
7571
    for ($i = 0; $i <= 255; $i++) {
7572
        $x = ($x + $box[$i] + $key[$i]) % 256;
7573
        $tempswap = $box[$i];
7574
        $box[$i] = $box[$x];
7575
        $box[$x] = $tempswap;
7576
    }
7577
 
7578
    $cipher = '';
7579
 
7580
    $a = 0;
7581
    $j = 0;
7582
 
7583
    for ($i = 0; $i < strlen($data); $i++) {
7584
        $a = ($a + 1) % 256;
7585
        $j = ($j + $box[$a]) % 256;
7586
        $temp = $box[$a];
7587
        $box[$a] = $box[$j];
7588
        $box[$j] = $temp;
7589
        $k = $box[(($box[$a] + $box[$j]) % 256)];
7590
        $cipherby = ord(substr($data, $i, 1)) ^ $k;
7591
        $cipher .= chr($cipherby);
7592
    }
7593
 
7594
    if ($case == 'de') {
7595
        $cipher = urldecode(urlencode($cipher));
7596
    } else {
7597
        $cipher = urlencode($cipher);
7598
    }
7599
 
7600
    return $cipher;
7601
}
7602
 
7603
// ENVIRONMENT CHECKING.
7604
 
7605
/**
7606
 * This method validates a plug name. It is much faster than calling clean_param.
7607
 *
7608
 * @param string $name a string that might be a plugin name.
7609
 * @return bool if this string is a valid plugin name.
7610
 */
1326 ariadna 7611
function is_valid_plugin_name($name)
7612
{
1 efrain 7613
    // This does not work for 'mod', bad luck, use any other type.
7614
    return core_component::is_valid_plugin_name('tool', $name);
7615
}
7616
 
7617
/**
7618
 * Get a list of all the plugins of a given type that define a certain API function
7619
 * in a certain file. The plugin component names and function names are returned.
7620
 *
7621
 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7622
 * @param string $function the part of the name of the function after the
7623
 *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7624
 *      names like report_courselist_hook.
7625
 * @param string $file the name of file within the plugin that defines the
7626
 *      function. Defaults to lib.php.
7627
 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7628
 *      and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7629
 */
1326 ariadna 7630
function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php')
7631
{
1 efrain 7632
    global $CFG;
7633
 
7634
    // We don't include here as all plugin types files would be included.
7635
    $plugins = get_plugins_with_function($function, $file, false);
7636
 
7637
    if (empty($plugins[$plugintype])) {
7638
        return array();
7639
    }
7640
 
7641
    $allplugins = core_component::get_plugin_list($plugintype);
7642
 
7643
    // Reformat the array and include the files.
7644
    $pluginfunctions = array();
7645
    foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7646
 
7647
        // Check that it has not been removed and the file is still available.
7648
        if (!empty($allplugins[$pluginname])) {
7649
 
7650
            $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7651
            if (file_exists($filepath)) {
7652
                include_once($filepath);
7653
 
7654
                // Now that the file is loaded, we must verify the function still exists.
7655
                if (function_exists($functionname)) {
7656
                    $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7657
                } else {
7658
                    // Invalidate the cache for next run.
7659
                    \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7660
                }
7661
            }
7662
        }
7663
    }
7664
 
7665
    return $pluginfunctions;
7666
}
7667
 
7668
/**
7669
 * Get a list of all the plugins that define a certain API function in a certain file.
7670
 *
7671
 * @param string $function the part of the name of the function after the
7672
 *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7673
 *      names like report_courselist_hook.
7674
 * @param string $file the name of file within the plugin that defines the
7675
 *      function. Defaults to lib.php.
7676
 * @param bool $include Whether to include the files that contain the functions or not.
7677
 * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
7678
 * @return array with [plugintype][plugin] = functionname
7679
 */
1326 ariadna 7680
function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false)
7681
{
1 efrain 7682
    global $CFG;
7683
 
7684
    if (during_initial_install() || isset($CFG->upgraderunning)) {
7685
        // API functions _must not_ be called during an installation or upgrade.
7686
        return [];
7687
    }
7688
 
7689
    $plugincallback = $function;
1326 ariadna 7690
    $filtermigrated = function ($plugincallback, $pluginfunctions): array {
1 efrain 7691
        foreach ($pluginfunctions as $plugintype => $plugins) {
7692
            foreach ($plugins as $plugin => $unusedfunction) {
7693
                $component = $plugintype . '_' . $plugin;
7694
                if ($hooks = di::get(hook\manager::class)->get_hooks_deprecating_plugin_callback($plugincallback)) {
7695
                    if (di::get(hook\manager::class)->is_deprecating_hook_present($component, $plugincallback)) {
7696
                        // Ignore the old callback, it is there only for older Moodle versions.
7697
                        unset($pluginfunctions[$plugintype][$plugin]);
7698
                    } else {
7699
                        $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
7700
                        debugging(
7701
                            "Callback $plugincallback in $component component should be migrated to new " .
7702
                                "hook callback for $hookmessage",
7703
                            DEBUG_DEVELOPER
7704
                        );
7705
                    }
7706
                }
7707
            }
7708
        }
7709
        return $pluginfunctions;
7710
    };
7711
 
7712
    $cache = \cache::make('core', 'plugin_functions');
7713
 
7714
    // Including both although I doubt that we will find two functions definitions with the same name.
7715
    // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7716
    $pluginfunctions = false;
7717
    if (!empty($CFG->allversionshash)) {
7718
        $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA);
7719
        $pluginfunctions = $cache->get($key);
7720
    }
7721
    $dirty = false;
7722
 
7723
    // Use the plugin manager to check that plugins are currently installed.
7724
    $pluginmanager = \core_plugin_manager::instance();
7725
 
7726
    if ($pluginfunctions !== false) {
7727
 
7728
        // Checking that the files are still available.
7729
        foreach ($pluginfunctions as $plugintype => $plugins) {
7730
 
7731
            $allplugins = \core_component::get_plugin_list($plugintype);
7732
            $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7733
            foreach ($plugins as $plugin => $function) {
7734
                if (!isset($installedplugins[$plugin])) {
7735
                    // Plugin code is still present on disk but it is not installed.
7736
                    $dirty = true;
7737
                    break 2;
7738
                }
7739
 
7740
                // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7741
                if (empty($allplugins[$plugin])) {
7742
                    $dirty = true;
7743
                    break 2;
7744
                }
7745
 
7746
                $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7747
                if ($include && $fileexists) {
7748
                    // Include the files if it was requested.
7749
                    include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7750
                } else if (!$fileexists) {
7751
                    // If the file is not available any more it should not be returned.
7752
                    $dirty = true;
7753
                    break 2;
7754
                }
7755
 
7756
                // Check if the function still exists in the file.
7757
                if ($include && !function_exists($function)) {
7758
                    $dirty = true;
7759
                    break 2;
7760
                }
7761
            }
7762
        }
7763
 
7764
        // If the cache is dirty, we should fall through and let it rebuild.
7765
        if (!$dirty) {
7766
            if ($migratedtohook && $file === 'lib.php') {
7767
                $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7768
            }
7769
            return $pluginfunctions;
7770
        }
7771
    }
7772
 
7773
    $pluginfunctions = array();
7774
 
7775
    // To fill the cached. Also, everything should continue working with cache disabled.
7776
    $plugintypes = \core_component::get_plugin_types();
7777
    foreach ($plugintypes as $plugintype => $unused) {
7778
 
7779
        // We need to include files here.
7780
        $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7781
        $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7782
        foreach ($pluginswithfile as $plugin => $notused) {
7783
 
7784
            if (!isset($installedplugins[$plugin])) {
7785
                continue;
7786
            }
7787
 
7788
            $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7789
 
7790
            $pluginfunction = false;
7791
            if (function_exists($fullfunction)) {
7792
                // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7793
                $pluginfunction = $fullfunction;
7794
            } else if ($plugintype === 'mod') {
7795
                // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7796
                $shortfunction = $plugin . '_' . $function;
7797
                if (function_exists($shortfunction)) {
7798
                    $pluginfunction = $shortfunction;
7799
                }
7800
            }
7801
 
7802
            if ($pluginfunction) {
7803
                if (empty($pluginfunctions[$plugintype])) {
7804
                    $pluginfunctions[$plugintype] = array();
7805
                }
7806
                $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7807
            }
7808
        }
7809
    }
7810
    if (!empty($CFG->allversionshash)) {
7811
        $cache->set($key, $pluginfunctions);
7812
    }
7813
 
7814
    if ($migratedtohook && $file === 'lib.php') {
7815
        $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7816
    }
7817
 
7818
    return $pluginfunctions;
7819
}
7820
 
7821
/**
7822
 * Lists plugin-like directories within specified directory
7823
 *
7824
 * This function was originally used for standard Moodle plugins, please use
7825
 * new core_component::get_plugin_list() now.
7826
 *
7827
 * This function is used for general directory listing and backwards compatility.
7828
 *
7829
 * @param string $directory relative directory from root
7830
 * @param string $exclude dir name to exclude from the list (defaults to none)
7831
 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7832
 * @return array Sorted array of directory names found under the requested parameters
7833
 */
1326 ariadna 7834
function get_list_of_plugins($directory = 'mod', $exclude = '', $basedir = '')
7835
{
1 efrain 7836
    global $CFG;
7837
 
7838
    $plugins = array();
7839
 
7840
    if (empty($basedir)) {
1326 ariadna 7841
        $basedir = $CFG->dirroot . '/' . $directory;
1 efrain 7842
    } else {
1326 ariadna 7843
        $basedir = $basedir . '/' . $directory;
1 efrain 7844
    }
7845
 
7846
    if ($CFG->debugdeveloper and empty($exclude)) {
7847
        // Make sure devs do not use this to list normal plugins,
7848
        // this is intended for general directories that are not plugins!
7849
 
7850
        $subtypes = core_component::get_plugin_types();
7851
        if (in_array($basedir, $subtypes)) {
7852
            debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7853
        }
7854
        unset($subtypes);
7855
    }
7856
 
7857
    $ignorelist = array_flip(array_filter([
7858
        'CVS',
7859
        '_vti_cnf',
7860
        'amd',
7861
        'classes',
7862
        'simpletest',
7863
        'tests',
7864
        'templates',
7865
        'yui',
7866
        $exclude,
7867
    ]));
7868
 
7869
    if (file_exists($basedir) && filetype($basedir) == 'dir') {
7870
        if (!$dirhandle = opendir($basedir)) {
7871
            debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7872
            return array();
7873
        }
7874
        while (false !== ($dir = readdir($dirhandle))) {
7875
            if (strpos($dir, '.') === 0) {
7876
                // Ignore directories starting with .
7877
                // These are treated as hidden directories.
7878
                continue;
7879
            }
7880
            if (array_key_exists($dir, $ignorelist)) {
7881
                // This directory features on the ignore list.
7882
                continue;
7883
            }
1326 ariadna 7884
            if (filetype($basedir . '/' . $dir) != 'dir') {
1 efrain 7885
                continue;
7886
            }
7887
            $plugins[] = $dir;
7888
        }
7889
        closedir($dirhandle);
7890
    }
7891
    if ($plugins) {
7892
        asort($plugins);
7893
    }
7894
    return $plugins;
7895
}
7896
 
7897
/**
7898
 * Invoke plugin's callback functions
7899
 *
7900
 * @param string $type plugin type e.g. 'mod'
7901
 * @param string $name plugin name
7902
 * @param string $feature feature name
7903
 * @param string $action feature's action
7904
 * @param array $params parameters of callback function, should be an array
7905
 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7906
 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7907
 * @return mixed
7908
 *
7909
 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7910
 */
1326 ariadna 7911
function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false)
7912
{
1 efrain 7913
    return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
7914
}
7915
 
7916
/**
7917
 * Invoke component's callback functions
7918
 *
7919
 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7920
 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7921
 * @param array $params parameters of callback function
7922
 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7923
 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7924
 * @return mixed
7925
 */
1326 ariadna 7926
function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false)
7927
{
1 efrain 7928
    $functionname = component_callback_exists($component, $function);
7929
 
7930
    if ($functionname) {
7931
        if ($migratedtohook) {
7932
            $hookmanager = di::get(hook\manager::class);
7933
            if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($function)) {
7934
                if ($hookmanager->is_deprecating_hook_present($component, $function)) {
7935
                    // Do not call the old lib.php callback,
7936
                    // it is there for compatibility with older Moodle versions only.
7937
                    return null;
7938
                } else {
7939
                    $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
7940
                    debugging(
7941
                        "Callback $function in $component component should be migrated to new hook callback for $hookmessage",
1326 ariadna 7942
                        DEBUG_DEVELOPER
7943
                    );
1 efrain 7944
                }
7945
            }
7946
        }
7947
 
7948
        // Function exists, so just return function result.
7949
        $ret = call_user_func_array($functionname, $params);
7950
        if (is_null($ret)) {
7951
            return $default;
7952
        } else {
7953
            return $ret;
7954
        }
7955
    }
7956
    return $default;
7957
}
7958
 
7959
/**
7960
 * Determine if a component callback exists and return the function name to call. Note that this
7961
 * function will include the required library files so that the functioname returned can be
7962
 * called directly.
7963
 *
7964
 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7965
 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7966
 * @return mixed Complete function name to call if the callback exists or false if it doesn't.
7967
 * @throws coding_exception if invalid component specfied
7968
 */
1326 ariadna 7969
function component_callback_exists($component, $function)
7970
{
1 efrain 7971
    global $CFG; // This is needed for the inclusions.
7972
 
7973
    $cleancomponent = clean_param($component, PARAM_COMPONENT);
7974
    if (empty($cleancomponent)) {
7975
        throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7976
    }
7977
    $component = $cleancomponent;
7978
 
7979
    list($type, $name) = core_component::normalize_component($component);
7980
    $component = $type . '_' . $name;
7981
 
1326 ariadna 7982
    $oldfunction = $name . '_' . $function;
7983
    $function = $component . '_' . $function;
1 efrain 7984
 
7985
    $dir = core_component::get_component_directory($component);
7986
    if (empty($dir)) {
7987
        throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7988
    }
7989
 
7990
    // Load library and look for function.
1326 ariadna 7991
    if (file_exists($dir . '/lib.php')) {
7992
        require_once($dir . '/lib.php');
1 efrain 7993
    }
7994
 
7995
    if (!function_exists($function) and function_exists($oldfunction)) {
7996
        if ($type !== 'mod' and $type !== 'core') {
7997
            debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
7998
        }
7999
        $function = $oldfunction;
8000
    }
8001
 
8002
    if (function_exists($function)) {
8003
        return $function;
8004
    }
8005
    return false;
8006
}
8007
 
8008
/**
8009
 * Call the specified callback method on the provided class.
8010
 *
8011
 * If the callback returns null, then the default value is returned instead.
8012
 * If the class does not exist, then the default value is returned.
8013
 *
8014
 * @param   string      $classname The name of the class to call upon.
8015
 * @param   string      $methodname The name of the staticically defined method on the class.
8016
 * @param   array       $params The arguments to pass into the method.
8017
 * @param   mixed       $default The default value.
8018
 * @param   bool        $migratedtohook True if the callback has been migrated to a hook.
8019
 * @return  mixed       The return value.
8020
 */
1326 ariadna 8021
function component_class_callback($classname, $methodname, array $params, $default = null, bool $migratedtohook = false)
8022
{
1 efrain 8023
    if (!class_exists($classname)) {
8024
        return $default;
8025
    }
8026
 
8027
    if (!method_exists($classname, $methodname)) {
8028
        return $default;
8029
    }
8030
 
8031
    $fullfunction = $classname . '::' . $methodname;
8032
 
8033
    if ($migratedtohook) {
8034
        $functionparts = explode('\\', trim($fullfunction, '\\'));
8035
        $component = $functionparts[0];
8036
        $callback = end($functionparts);
8037
        $hookmanager = di::get(hook\manager::class);
8038
        if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($callback)) {
8039
            if ($hookmanager->is_deprecating_hook_present($component, $callback)) {
8040
                // Do not call the old class callback,
8041
                // it is there for compatibility with older Moodle versions only.
8042
                return null;
8043
            } else {
8044
                $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
1326 ariadna 8045
                debugging(
8046
                    "Callback $callback in $component component should be migrated to new hook callback for $hookmessage",
8047
                    DEBUG_DEVELOPER
8048
                );
1 efrain 8049
            }
8050
        }
8051
    }
8052
 
8053
    $result = call_user_func_array($fullfunction, $params);
8054
 
8055
    if (null === $result) {
8056
        return $default;
8057
    } else {
8058
        return $result;
8059
    }
8060
}
8061
 
8062
/**
8063
 * Checks whether a plugin supports a specified feature.
8064
 *
8065
 * @param string $type Plugin type e.g. 'mod'
8066
 * @param string $name Plugin name e.g. 'forum'
8067
 * @param string $feature Feature code (FEATURE_xx constant)
8068
 * @param mixed $default default value if feature support unknown
8069
 * @return mixed Feature result (false if not supported, null if feature is unknown,
8070
 *         otherwise usually true but may have other feature-specific value such as array)
8071
 * @throws coding_exception
8072
 */
1326 ariadna 8073
function plugin_supports($type, $name, $feature, $default = null)
8074
{
1 efrain 8075
    global $CFG;
8076
 
8077
    if ($type === 'mod' and $name === 'NEWMODULE') {
8078
        // Somebody forgot to rename the module template.
8079
        return false;
8080
    }
8081
 
8082
    $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8083
    if (empty($component)) {
8084
        throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8085
    }
8086
 
8087
    $function = null;
8088
 
8089
    if ($type === 'mod') {
8090
        // We need this special case because we support subplugins in modules,
8091
        // otherwise it would end up in infinite loop.
8092
        if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8093
            include_once("$CFG->dirroot/mod/$name/lib.php");
1326 ariadna 8094
            $function = $component . '_supports';
1 efrain 8095
            if (!function_exists($function)) {
8096
                // Legacy non-frankenstyle function name.
1326 ariadna 8097
                $function = $name . '_supports';
1 efrain 8098
            }
8099
        }
8100
    } else {
8101
        if (!$path = core_component::get_plugin_directory($type, $name)) {
8102
            // Non existent plugin type.
8103
            return false;
8104
        }
8105
        if (file_exists("$path/lib.php")) {
8106
            include_once("$path/lib.php");
1326 ariadna 8107
            $function = $component . '_supports';
1 efrain 8108
        }
8109
    }
8110
 
8111
    if ($function and function_exists($function)) {
8112
        $supports = $function($feature);
8113
        if (is_null($supports)) {
8114
            // Plugin does not know - use default.
8115
            return $default;
8116
        } else {
8117
            return $supports;
8118
        }
8119
    }
8120
 
8121
    // Plugin does not care, so use default.
8122
    return $default;
8123
}
8124
 
8125
/**
8126
 * Returns true if the current version of PHP is greater that the specified one.
8127
 *
8128
 * @todo Check PHP version being required here is it too low?
8129
 *
8130
 * @param string $version The version of php being tested.
8131
 * @return bool
8132
 */
1326 ariadna 8133
function check_php_version($version = '5.2.4')
8134
{
1 efrain 8135
    return (version_compare(phpversion(), $version) >= 0);
8136
}
8137
 
8138
/**
8139
 * Determine if moodle installation requires update.
8140
 *
8141
 * Checks version numbers of main code and all plugins to see
8142
 * if there are any mismatches.
8143
 *
8144
 * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
8145
 * @return bool
8146
 */
1326 ariadna 8147
function moodle_needs_upgrading($checkupgradeflag = true)
8148
{
1 efrain 8149
    global $CFG, $DB;
8150
 
8151
    // Say no if there is already an upgrade running.
8152
    if ($checkupgradeflag) {
8153
        $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']);
8154
        $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING);
8155
        // If we ARE locked, but this PHP process is NOT the process running the upgrade,
8156
        // We should always return false.
8157
        // This means the upgrade is running from CLI somewhere, or about to.
8158
        if (!empty($lock) && !$currentprocessrunningupgrade) {
8159
            return false;
8160
        }
8161
    }
8162
 
8163
    if (empty($CFG->version)) {
8164
        return true;
8165
    }
8166
 
8167
    // There is no need to purge plugininfo caches here because
8168
    // these caches are not used during upgrade and they are purged after
8169
    // every upgrade.
8170
 
8171
    if (empty($CFG->allversionshash)) {
8172
        return true;
8173
    }
8174
 
8175
    $hash = core_component::get_all_versions_hash();
8176
 
8177
    return ($hash !== $CFG->allversionshash);
8178
}
8179
 
8180
/**
8181
 * Returns the major version of this site
8182
 *
8183
 * Moodle version numbers consist of three numbers separated by a dot, for
8184
 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
8185
 * called major version. This function extracts the major version from either
8186
 * $CFG->release (default) or eventually from the $release variable defined in
8187
 * the main version.php.
8188
 *
8189
 * @param bool $fromdisk should the version if source code files be used
8190
 * @return string|false the major version like '2.3', false if could not be determined
8191
 */
1326 ariadna 8192
function moodle_major_version($fromdisk = false)
8193
{
1 efrain 8194
    global $CFG;
8195
 
8196
    if ($fromdisk) {
8197
        $release = null;
1326 ariadna 8198
        require($CFG->dirroot . '/version.php');
1 efrain 8199
        if (empty($release)) {
8200
            return false;
8201
        }
8202
    } else {
8203
        if (empty($CFG->release)) {
8204
            return false;
8205
        }
8206
        $release = $CFG->release;
8207
    }
8208
 
8209
    if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
8210
        return $matches[0];
8211
    } else {
8212
        return false;
8213
    }
8214
}
8215
 
8216
// MISCELLANEOUS.
8217
 
8218
/**
8219
 * Gets the system locale
8220
 *
8221
 * @return string Retuns the current locale.
8222
 */
1326 ariadna 8223
function moodle_getlocale()
8224
{
1 efrain 8225
    global $CFG;
8226
 
8227
    // Fetch the correct locale based on ostype.
8228
    if ($CFG->ostype == 'WINDOWS') {
8229
        $stringtofetch = 'localewin';
8230
    } else {
8231
        $stringtofetch = 'locale';
8232
    }
8233
 
8234
    if (!empty($CFG->locale)) { // Override locale for all language packs.
8235
        return $CFG->locale;
8236
    }
8237
 
8238
    return get_string($stringtofetch, 'langconfig');
8239
}
8240
 
8241
/**
8242
 * Sets the system locale
8243
 *
8244
 * @category string
8245
 * @param string $locale Can be used to force a locale
8246
 */
1326 ariadna 8247
function moodle_setlocale($locale = '')
8248
{
1 efrain 8249
    global $CFG;
8250
 
8251
    static $currentlocale = ''; // Last locale caching.
8252
 
8253
    $oldlocale = $currentlocale;
8254
 
8255
    // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8256
    if (!empty($locale)) {
8257
        $currentlocale = $locale;
8258
    } else {
8259
        $currentlocale = moodle_getlocale();
8260
    }
8261
 
8262
    // Do nothing if locale already set up.
8263
    if ($oldlocale == $currentlocale) {
8264
        return;
8265
    }
8266
 
8267
    // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8268
    // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8269
    // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8270
 
8271
    // Get current values.
1326 ariadna 8272
    $monetary = setlocale(LC_MONETARY, 0);
8273
    $numeric = setlocale(LC_NUMERIC, 0);
8274
    $ctype   = setlocale(LC_CTYPE, 0);
1 efrain 8275
    if ($CFG->ostype != 'WINDOWS') {
1326 ariadna 8276
        $messages = setlocale(LC_MESSAGES, 0);
1 efrain 8277
    }
8278
    // Set locale to all.
1326 ariadna 8279
    $result = setlocale(LC_ALL, $currentlocale);
1 efrain 8280
    // If setting of locale fails try the other utf8 or utf-8 variant,
8281
    // some operating systems support both (Debian), others just one (OSX).
8282
    if ($result === false) {
8283
        if (stripos($currentlocale, '.UTF-8') !== false) {
8284
            $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
1326 ariadna 8285
            setlocale(LC_ALL, $newlocale);
1 efrain 8286
        } else if (stripos($currentlocale, '.UTF8') !== false) {
8287
            $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
1326 ariadna 8288
            setlocale(LC_ALL, $newlocale);
1 efrain 8289
        }
8290
    }
8291
    // Set old values.
1326 ariadna 8292
    setlocale(LC_MONETARY, $monetary);
8293
    setlocale(LC_NUMERIC, $numeric);
1 efrain 8294
    if ($CFG->ostype != 'WINDOWS') {
1326 ariadna 8295
        setlocale(LC_MESSAGES, $messages);
1 efrain 8296
    }
8297
    if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8298
        // To workaround a well-known PHP problem with Turkish letter Ii.
1326 ariadna 8299
        setlocale(LC_CTYPE, $ctype);
1 efrain 8300
    }
8301
}
8302
 
8303
/**
8304
 * Count words in a string.
8305
 *
8306
 * Words are defined as things between whitespace.
8307
 *
8308
 * @category string
8309
 * @param string $string The text to be searched for words. May be HTML.
8310
 * @param int|null $format
8311
 * @return int The count of words in the specified string
8312
 */
1326 ariadna 8313
function count_words($string, $format = null)
8314
{
1 efrain 8315
    // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8316
    // Also, br is a special case because it definitely delimits a word, but has no close tag.
8317
    $string = preg_replace('~
8318
            (                                   # Capture the tag we match.
8319
                </                              # Start of close tag.
8320
                (?!                             # Do not match any of these specific close tag names.
8321
                    a> | b> | del> | em> | i> |
8322
                    ins> | s> | small> | span> |
8323
                    strong> | sub> | sup> | u>
8324
                )
8325
                \w+                             # But, apart from those execptions, match any tag name.
8326
                >                               # End of close tag.
8327
            |
8328
                <br> | <br\s*/>                 # Special cases that are not close tags.
8329
            )
8330
            ~x', '$1 ', $string); // Add a space after the close tag.
8331
    if ($format !== null && $format != FORMAT_PLAIN) {
8332
        // Match the usual text cleaning before display.
8333
        // Ideally we should apply multilang filter only here, other filters might add extra text.
8334
        $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8335
    }
8336
    // Now remove HTML tags.
8337
    $string = strip_tags($string);
8338
    // Decode HTML entities.
8339
    $string = html_entity_decode($string, ENT_COMPAT);
8340
 
8341
    // Now, the word count is the number of blocks of characters separated
8342
    // by any sort of space. That seems to be the definition used by all other systems.
8343
    // To be precise about what is considered to separate words:
8344
    // * Anything that Unicode considers a 'Separator'
8345
    // * Anything that Unicode considers a 'Control character'
8346
    // * An em- or en- dash.
8347
    return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8348
}
8349
 
8350
/**
8351
 * Count letters in a string.
8352
 *
8353
 * Letters are defined as chars not in tags and different from whitespace.
8354
 *
8355
 * @category string
8356
 * @param string $string The text to be searched for letters. May be HTML.
8357
 * @param int|null $format
8358
 * @return int The count of letters in the specified text.
8359
 */
1326 ariadna 8360
function count_letters($string, $format = null)
8361
{
1 efrain 8362
    if ($format !== null && $format != FORMAT_PLAIN) {
8363
        // Match the usual text cleaning before display.
8364
        // Ideally we should apply multilang filter only here, other filters might add extra text.
8365
        $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8366
    }
8367
    $string = strip_tags($string); // Tags are out now.
8368
    $string = html_entity_decode($string, ENT_COMPAT);
8369
    $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8370
 
8371
    return core_text::strlen($string);
8372
}
8373
 
8374
/**
8375
 * Generate and return a random string of the specified length.
8376
 *
8377
 * @param int $length The length of the string to be created.
8378
 * @return string
8379
 */
1326 ariadna 8380
function random_string($length = 15)
8381
{
1 efrain 8382
    $randombytes = random_bytes($length);
8383
    $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8384
    $pool .= 'abcdefghijklmnopqrstuvwxyz';
8385
    $pool .= '0123456789';
8386
    $poollen = strlen($pool);
8387
    $string = '';
8388
    for ($i = 0; $i < $length; $i++) {
8389
        $rand = ord($randombytes[$i]);
1326 ariadna 8390
        $string .= substr($pool, ($rand % ($poollen)), 1);
1 efrain 8391
    }
8392
    return $string;
8393
}
8394
 
8395
/**
8396
 * Generate a complex random string (useful for md5 salts)
8397
 *
8398
 * This function is based on the above {@link random_string()} however it uses a
8399
 * larger pool of characters and generates a string between 24 and 32 characters
8400
 *
8401
 * @param int $length Optional if set generates a string to exactly this length
8402
 * @return string
8403
 */
1326 ariadna 8404
function complex_random_string($length = null)
8405
{
1 efrain 8406
    $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8407
    $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8408
    $poollen = strlen($pool);
1326 ariadna 8409
    if ($length === null) {
1 efrain 8410
        $length = floor(rand(24, 32));
8411
    }
8412
    $randombytes = random_bytes($length);
8413
    $string = '';
8414
    for ($i = 0; $i < $length; $i++) {
8415
        $rand = ord($randombytes[$i]);
1326 ariadna 8416
        $string .= $pool[($rand % $poollen)];
1 efrain 8417
    }
8418
    return $string;
8419
}
8420
 
8421
/**
8422
 * Given some text (which may contain HTML) and an ideal length,
8423
 * this function truncates the text neatly on a word boundary if possible
8424
 *
8425
 * @category string
8426
 * @param string $text text to be shortened
8427
 * @param int $ideal ideal string length
8428
 * @param boolean $exact if false, $text will not be cut mid-word
8429
 * @param string $ending The string to append if the passed string is truncated
8430
 * @return string $truncate shortened string
8431
 */
1326 ariadna 8432
function shorten_text($text, $ideal = 30, $exact = false, $ending = '...')
8433
{
1 efrain 8434
    // If the plain text is shorter than the maximum length, return the whole text.
8435
    if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8436
        return $text;
8437
    }
8438
 
8439
    // Splits on HTML tags. Each open/close/empty tag will be the first thing
8440
    // and only tag in its 'line'.
8441
    preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8442
 
8443
    $totallength = core_text::strlen($ending);
8444
    $truncate = '';
8445
 
8446
    // This array stores information about open and close tags and their position
8447
    // in the truncated string. Each item in the array is an object with fields
8448
    // ->open (true if open), ->tag (tag name in lower case), and ->pos
8449
    // (byte position in truncated text).
8450
    $tagdetails = array();
8451
 
8452
    foreach ($lines as $linematchings) {
8453
        // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8454
        if (!empty($linematchings[1])) {
8455
            // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8456
            if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8457
                if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8458
                    // Record closing tag.
8459
                    $tagdetails[] = (object) array(
1326 ariadna 8460
                        'open' => false,
8461
                        'tag'  => core_text::strtolower($tagmatchings[1]),
8462
                        'pos'  => core_text::strlen($truncate),
8463
                    );
1 efrain 8464
                } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8465
                    // Record opening tag.
8466
                    $tagdetails[] = (object) array(
1326 ariadna 8467
                        'open' => true,
8468
                        'tag'  => core_text::strtolower($tagmatchings[1]),
8469
                        'pos'  => core_text::strlen($truncate),
8470
                    );
1 efrain 8471
                } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8472
                    $tagdetails[] = (object) array(
1326 ariadna 8473
                        'open' => true,
8474
                        'tag'  => core_text::strtolower('if'),
8475
                        'pos'  => core_text::strlen($truncate),
1 efrain 8476
                    );
8477
                } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8478
                    $tagdetails[] = (object) array(
1326 ariadna 8479
                        'open' => false,
8480
                        'tag'  => core_text::strtolower('if'),
8481
                        'pos'  => core_text::strlen($truncate),
1 efrain 8482
                    );
8483
                }
8484
            }
8485
            // Add html-tag to $truncate'd text.
8486
            $truncate .= $linematchings[1];
8487
        }
8488
 
8489
        // Calculate the length of the plain text part of the line; handle entities as one character.
8490
        $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8491
        if ($totallength + $contentlength > $ideal) {
8492
            // The number of characters which are left.
8493
            $left = $ideal - $totallength;
8494
            $entitieslength = 0;
8495
            // Search for html entities.
8496
            if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $linematchings[2], $entities, PREG_OFFSET_CAPTURE)) {
8497
                // Calculate the real length of all entities in the legal range.
8498
                foreach ($entities[0] as $entity) {
1326 ariadna 8499
                    if ($entity[1] + 1 - $entitieslength <= $left) {
1 efrain 8500
                        $left--;
8501
                        $entitieslength += core_text::strlen($entity[0]);
8502
                    } else {
8503
                        // No more characters left.
8504
                        break;
8505
                    }
8506
                }
8507
            }
8508
            $breakpos = $left + $entitieslength;
8509
 
8510
            // If the words shouldn't be cut in the middle...
8511
            if (!$exact) {
8512
                // Search the last occurence of a space.
8513
                for (; $breakpos > 0; $breakpos--) {
8514
                    if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8515
                        if ($char === '.' or $char === ' ') {
8516
                            $breakpos += 1;
8517
                            break;
8518
                        } else if (strlen($char) > 2) {
8519
                            // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8520
                            $breakpos += 1;
8521
                            break;
8522
                        }
8523
                    }
8524
                }
8525
            }
8526
            if ($breakpos == 0) {
8527
                // This deals with the test_shorten_text_no_spaces case.
8528
                $breakpos = $left + $entitieslength;
8529
            } else if ($breakpos > $left + $entitieslength) {
8530
                // This deals with the previous for loop breaking on the first char.
8531
                $breakpos = $left + $entitieslength;
8532
            }
8533
 
8534
            $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8535
            // Maximum length is reached, so get off the loop.
8536
            break;
8537
        } else {
8538
            $truncate .= $linematchings[2];
8539
            $totallength += $contentlength;
8540
        }
8541
 
8542
        // If the maximum length is reached, get off the loop.
8543
        if ($totallength >= $ideal) {
8544
            break;
8545
        }
8546
    }
8547
 
8548
    // Add the defined ending to the text.
8549
    $truncate .= $ending;
8550
 
8551
    // Now calculate the list of open html tags based on the truncate position.
8552
    $opentags = array();
8553
    foreach ($tagdetails as $taginfo) {
8554
        if ($taginfo->open) {
8555
            // Add tag to the beginning of $opentags list.
8556
            array_unshift($opentags, $taginfo->tag);
8557
        } else {
8558
            // Can have multiple exact same open tags, close the last one.
8559
            $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8560
            if ($pos !== false) {
8561
                unset($opentags[$pos]);
8562
            }
8563
        }
8564
    }
8565
 
8566
    // Close all unclosed html-tags.
8567
    foreach ($opentags as $tag) {
8568
        if ($tag === 'if') {
8569
            $truncate .= '<!--<![endif]-->';
8570
        } else {
8571
            $truncate .= '</' . $tag . '>';
8572
        }
8573
    }
8574
 
8575
    return $truncate;
8576
}
8577
 
8578
/**
8579
 * Shortens a given filename by removing characters positioned after the ideal string length.
8580
 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8581
 * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8582
 *
8583
 * @param string $filename file name
8584
 * @param int $length ideal string length
8585
 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8586
 * @return string $shortened shortened file name
8587
 */
1326 ariadna 8588
function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false)
8589
{
1 efrain 8590
    $shortened = $filename;
8591
    // Extract a part of the filename if it's char size exceeds the ideal string length.
8592
    if (core_text::strlen($filename) > $length) {
8593
        // Exclude extension if present in filename.
8594
        $mimetypes = get_mimetypes_array();
8595
        $extension = pathinfo($filename, PATHINFO_EXTENSION);
8596
        if ($extension && !empty($mimetypes[$extension])) {
8597
            $basename = pathinfo($filename, PATHINFO_FILENAME);
8598
            $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8599
            $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8600
            $shortened .= '.' . $extension;
8601
        } else {
8602
            $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8603
            $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8604
        }
8605
    }
8606
    return $shortened;
8607
}
8608
 
8609
/**
8610
 * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8611
 *
8612
 * @param array $path The paths to reduce the length.
8613
 * @param int $length Ideal string length
8614
 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8615
 * @return array $result Shortened paths in array.
8616
 */
1326 ariadna 8617
function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false)
8618
{
1 efrain 8619
    $result = null;
8620
 
1326 ariadna 8621
    $result = array_reduce($path, function ($carry, $singlepath) use ($length, $includehash) {
1 efrain 8622
        $carry[] = shorten_filename($singlepath, $length, $includehash);
8623
        return $carry;
8624
    }, []);
8625
 
8626
    return $result;
8627
}
8628
 
8629
/**
8630
 * Given dates in seconds, how many weeks is the date from startdate
8631
 * The first week is 1, the second 2 etc ...
8632
 *
8633
 * @param int $startdate Timestamp for the start date
8634
 * @param int $thedate Timestamp for the end date
8635
 * @return string
8636
 */
1326 ariadna 8637
function getweek($startdate, $thedate)
8638
{
1 efrain 8639
    if ($thedate < $startdate) {
8640
        return 0;
8641
    }
8642
 
8643
    return floor(($thedate - $startdate) / WEEKSECS) + 1;
8644
}
8645
 
8646
/**
8647
 * Returns a randomly generated password of length $maxlen.  inspired by
8648
 *
8649
 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8650
 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8651
 *
8652
 * @param int $maxlen  The maximum size of the password being generated.
8653
 * @return string
8654
 */
1326 ariadna 8655
function generate_password($maxlen = 10)
8656
{
1 efrain 8657
    global $CFG;
8658
 
8659
    if (empty($CFG->passwordpolicy)) {
8660
        $fillers = PASSWORD_DIGITS;
8661
        $wordlist = file($CFG->wordlist);
8662
        $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8663
        $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8664
        $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8665
        $password = $word1 . $filler1 . $word2;
8666
    } else {
8667
        $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8668
        $digits = $CFG->minpassworddigits;
8669
        $lower = $CFG->minpasswordlower;
8670
        $upper = $CFG->minpasswordupper;
8671
        $nonalphanum = $CFG->minpasswordnonalphanum;
8672
        $total = $lower + $upper + $digits + $nonalphanum;
8673
        // Var minlength should be the greater one of the two ( $minlen and $total ).
8674
        $minlen = $minlen < $total ? $total : $minlen;
8675
        // Var maxlen can never be smaller than minlen.
8676
        $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8677
        $additional = $maxlen - $total;
8678
 
8679
        // Make sure we have enough characters to fulfill
8680
        // complexity requirements.
8681
        $passworddigits = PASSWORD_DIGITS;
8682
        while ($digits > strlen($passworddigits)) {
8683
            $passworddigits .= PASSWORD_DIGITS;
8684
        }
8685
        $passwordlower = PASSWORD_LOWER;
8686
        while ($lower > strlen($passwordlower)) {
8687
            $passwordlower .= PASSWORD_LOWER;
8688
        }
8689
        $passwordupper = PASSWORD_UPPER;
8690
        while ($upper > strlen($passwordupper)) {
8691
            $passwordupper .= PASSWORD_UPPER;
8692
        }
8693
        $passwordnonalphanum = PASSWORD_NONALPHANUM;
8694
        while ($nonalphanum > strlen($passwordnonalphanum)) {
8695
            $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8696
        }
8697
 
8698
        // Now mix and shuffle it all.
1326 ariadna 8699
        $password = str_shuffle(substr(str_shuffle($passwordlower), 0, $lower) .
8700
            substr(str_shuffle($passwordupper), 0, $upper) .
8701
            substr(str_shuffle($passworddigits), 0, $digits) .
8702
            substr(str_shuffle($passwordnonalphanum), 0, $nonalphanum) .
8703
            substr(str_shuffle($passwordlower .
8704
                $passwordupper .
8705
                $passworddigits .
8706
                $passwordnonalphanum), 0, $additional));
1 efrain 8707
    }
8708
 
1326 ariadna 8709
    return substr($password, 0, $maxlen);
1 efrain 8710
}
8711
 
8712
/**
8713
 * Given a float, prints it nicely.
8714
 * Localized floats must not be used in calculations!
8715
 *
8716
 * The stripzeros feature is intended for making numbers look nicer in small
8717
 * areas where it is not necessary to indicate the degree of accuracy by showing
8718
 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8719
 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8720
 *
8721
 * @param float $float The float to print
8722
 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8723
 * @param bool $localized use localized decimal separator
8724
 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8725
 *                         the decimal point are always striped if $decimalpoints is -1.
8726
 * @return string locale float
8727
 */
1326 ariadna 8728
function format_float($float, $decimalpoints = 1, $localized = true, $stripzeros = false)
8729
{
1 efrain 8730
    if (is_null($float)) {
8731
        return '';
8732
    }
8733
    if ($localized) {
8734
        $separator = get_string('decsep', 'langconfig');
8735
    } else {
8736
        $separator = '.';
8737
    }
8738
    if ($decimalpoints == -1) {
8739
        // The following counts the number of decimals.
8740
        // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8741
        $floatval = floatval($float);
8742
        for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8743
    }
8744
 
8745
    $result = number_format($float, $decimalpoints, $separator, '');
8746
    if ($stripzeros && $decimalpoints > 0) {
8747
        // Remove zeros and final dot if not needed.
8748
        // However, only do this if there is a decimal point!
8749
        $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8750
    }
8751
    return $result;
8752
}
8753
 
8754
/**
8755
 * Converts locale specific floating point/comma number back to standard PHP float value
8756
 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8757
 *
8758
 * @param string $localefloat locale aware float representation
8759
 * @param bool $strict If true, then check the input and return false if it is not a valid number.
8760
 * @return mixed float|bool - false or the parsed float.
8761
 */
1326 ariadna 8762
function unformat_float($localefloat, $strict = false)
8763
{
1 efrain 8764
    $localefloat = trim((string)$localefloat);
8765
 
8766
    if ($localefloat == '') {
8767
        return null;
8768
    }
8769
 
8770
    $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8771
    $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8772
 
8773
    if ($strict && !is_numeric($localefloat)) {
8774
        return false;
8775
    }
8776
 
8777
    return (float)$localefloat;
8778
}
8779
 
8780
/**
8781
 * Given a simple array, this shuffles it up just like shuffle()
8782
 * Unlike PHP's shuffle() this function works on any machine.
8783
 *
8784
 * @param array $array The array to be rearranged
8785
 * @return array
8786
 */
1326 ariadna 8787
function swapshuffle($array)
8788
{
1 efrain 8789
 
8790
    $last = count($array) - 1;
8791
    for ($i = 0; $i <= $last; $i++) {
8792
        $from = rand(0, $last);
8793
        $curr = $array[$i];
8794
        $array[$i] = $array[$from];
8795
        $array[$from] = $curr;
8796
    }
8797
    return $array;
8798
}
8799
 
8800
/**
8801
 * Like {@link swapshuffle()}, but works on associative arrays
8802
 *
8803
 * @param array $array The associative array to be rearranged
8804
 * @return array
8805
 */
1326 ariadna 8806
function swapshuffle_assoc($array)
8807
{
1 efrain 8808
 
8809
    $newarray = array();
8810
    $newkeys = swapshuffle(array_keys($array));
8811
 
8812
    foreach ($newkeys as $newkey) {
8813
        $newarray[$newkey] = $array[$newkey];
8814
    }
8815
    return $newarray;
8816
}
8817
 
8818
/**
8819
 * Given an arbitrary array, and a number of draws,
8820
 * this function returns an array with that amount
8821
 * of items.  The indexes are retained.
8822
 *
8823
 * @todo Finish documenting this function
8824
 *
8825
 * @param array $array
8826
 * @param int $draws
8827
 * @return array
8828
 */
1326 ariadna 8829
function draw_rand_array($array, $draws)
8830
{
1 efrain 8831
 
8832
    $return = array();
8833
 
8834
    $last = count($array);
8835
 
8836
    if ($draws > $last) {
8837
        $draws = $last;
8838
    }
8839
 
8840
    while ($draws > 0) {
8841
        $last--;
8842
 
8843
        $keys = array_keys($array);
8844
        $rand = rand(0, $last);
8845
 
8846
        $return[$keys[$rand]] = $array[$keys[$rand]];
8847
        unset($array[$keys[$rand]]);
8848
 
8849
        $draws--;
8850
    }
8851
 
8852
    return $return;
8853
}
8854
 
8855
/**
8856
 * Calculate the difference between two microtimes
8857
 *
8858
 * @param string $a The first Microtime
8859
 * @param string $b The second Microtime
8860
 * @return string
8861
 */
1326 ariadna 8862
function microtime_diff($a, $b)
8863
{
1 efrain 8864
    list($adec, $asec) = explode(' ', $a);
8865
    list($bdec, $bsec) = explode(' ', $b);
8866
    return $bsec - $asec + $bdec - $adec;
8867
}
8868
 
8869
/**
8870
 * Given a list (eg a,b,c,d,e) this function returns
8871
 * an array of 1->a, 2->b, 3->c etc
8872
 *
8873
 * @param string $list The string to explode into array bits
8874
 * @param string $separator The separator used within the list string
8875
 * @return array The now assembled array
8876
 */
1326 ariadna 8877
function make_menu_from_list($list, $separator = ',')
8878
{
1 efrain 8879
 
8880
    $array = array_reverse(explode($separator, $list), true);
8881
    foreach ($array as $key => $item) {
1326 ariadna 8882
        $outarray[$key + 1] = trim($item);
1 efrain 8883
    }
8884
    return $outarray;
8885
}
8886
 
8887
/**
8888
 * Creates an array that represents all the current grades that
8889
 * can be chosen using the given grading type.
8890
 *
8891
 * Negative numbers
8892
 * are scales, zero is no grade, and positive numbers are maximum
8893
 * grades.
8894
 *
8895
 * @todo Finish documenting this function or better deprecated this completely!
8896
 *
8897
 * @param int $gradingtype
8898
 * @return array
8899
 */
1326 ariadna 8900
function make_grades_menu($gradingtype)
8901
{
1 efrain 8902
    global $DB;
8903
 
8904
    $grades = array();
8905
    if ($gradingtype < 0) {
1326 ariadna 8906
        if ($scale = $DB->get_record('scale', array('id' => (-$gradingtype)))) {
1 efrain 8907
            return make_menu_from_list($scale->scale);
8908
        }
8909
    } else if ($gradingtype > 0) {
1326 ariadna 8910
        for ($i = $gradingtype; $i >= 0; $i--) {
8911
            $grades[$i] = $i . ' / ' . $gradingtype;
1 efrain 8912
        }
8913
        return $grades;
8914
    }
8915
    return $grades;
8916
}
8917
 
8918
/**
8919
 * make_unique_id_code
8920
 *
8921
 * @todo Finish documenting this function
8922
 *
8923
 * @uses $_SERVER
8924
 * @param string $extra Extra string to append to the end of the code
8925
 * @return string
8926
 */
1326 ariadna 8927
function make_unique_id_code($extra = '')
8928
{
1 efrain 8929
 
8930
    $hostname = 'unknownhost';
8931
    if (!empty($_SERVER['HTTP_HOST'])) {
8932
        $hostname = $_SERVER['HTTP_HOST'];
8933
    } else if (!empty($_ENV['HTTP_HOST'])) {
8934
        $hostname = $_ENV['HTTP_HOST'];
8935
    } else if (!empty($_SERVER['SERVER_NAME'])) {
8936
        $hostname = $_SERVER['SERVER_NAME'];
8937
    } else if (!empty($_ENV['SERVER_NAME'])) {
8938
        $hostname = $_ENV['SERVER_NAME'];
8939
    }
8940
 
8941
    $date = gmdate("ymdHis");
8942
 
8943
    $random =  random_string(6);
8944
 
8945
    if ($extra) {
1326 ariadna 8946
        return $hostname . '+' . $date . '+' . $random . '+' . $extra;
1 efrain 8947
    } else {
1326 ariadna 8948
        return $hostname . '+' . $date . '+' . $random;
1 efrain 8949
    }
8950
}
8951
 
8952
 
8953
/**
8954
 * Function to check the passed address is within the passed subnet
8955
 *
8956
 * The parameter is a comma separated string of subnet definitions.
8957
 * Subnet strings can be in one of three formats:
8958
 *   1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn          (number of bits in net mask)
8959
 *   2: xxx.xxx.xxx.xxx-yyy or  xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx::xxxx-yyyy (a range of IP addresses in the last group)
8960
 *   3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.                  (incomplete address, a bit non-technical ;-)
8961
 * Code for type 1 modified from user posted comments by mediator at
8962
 * {@link http://au.php.net/manual/en/function.ip2long.php}
8963
 *
8964
 * @param string $addr    The address you are checking
8965
 * @param string $subnetstr    The string of subnet addresses
8966
 * @param bool $checkallzeros    The state to whether check for 0.0.0.0
8967
 * @return bool
8968
 */
1326 ariadna 8969
function address_in_subnet($addr, $subnetstr, $checkallzeros = false)
8970
{
1 efrain 8971
 
8972
    if ($addr == '0.0.0.0' && !$checkallzeros) {
8973
        return false;
8974
    }
8975
    $subnets = explode(',', $subnetstr);
8976
    $found = false;
8977
    $addr = trim($addr);
8978
    $addr = cleanremoteaddr($addr, false); // Normalise.
8979
    if ($addr === null) {
8980
        return false;
8981
    }
8982
    $addrparts = explode(':', $addr);
8983
 
8984
    $ipv6 = strpos($addr, ':');
8985
 
8986
    foreach ($subnets as $subnet) {
8987
        $subnet = trim($subnet);
8988
        if ($subnet === '') {
8989
            continue;
8990
        }
8991
 
8992
        if (strpos($subnet, '/') !== false) {
8993
            // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
8994
            list($ip, $mask) = explode('/', $subnet);
8995
            $mask = trim($mask);
8996
            if (!is_number($mask)) {
8997
                continue; // Incorect mask number, eh?
8998
            }
8999
            $ip = cleanremoteaddr($ip, false); // Normalise.
9000
            if ($ip === null) {
9001
                continue;
9002
            }
9003
            if (strpos($ip, ':') !== false) {
9004
                // IPv6.
9005
                if (!$ipv6) {
9006
                    continue;
9007
                }
9008
                if ($mask > 128 or $mask < 0) {
9009
                    continue; // Nonsense.
9010
                }
9011
                if ($mask == 0) {
9012
                    return true; // Any address.
9013
                }
9014
                if ($mask == 128) {
9015
                    if ($ip === $addr) {
9016
                        return true;
9017
                    }
9018
                    continue;
9019
                }
9020
                $ipparts = explode(':', $ip);
9021
                $modulo  = $mask % 16;
1326 ariadna 9022
                $ipnet   = array_slice($ipparts, 0, ($mask - $modulo) / 16);
9023
                $addrnet = array_slice($addrparts, 0, ($mask - $modulo) / 16);
1 efrain 9024
                if (implode(':', $ipnet) === implode(':', $addrnet)) {
9025
                    if ($modulo == 0) {
9026
                        return true;
9027
                    }
1326 ariadna 9028
                    $pos     = ($mask - $modulo) / 16;
1 efrain 9029
                    $ipnet   = hexdec($ipparts[$pos]);
9030
                    $addrnet = hexdec($addrparts[$pos]);
9031
                    $mask    = 0xffff << (16 - $modulo);
9032
                    if (($addrnet & $mask) == ($ipnet & $mask)) {
9033
                        return true;
9034
                    }
9035
                }
9036
            } else {
9037
                // IPv4.
9038
                if ($ipv6) {
9039
                    continue;
9040
                }
9041
                if ($mask > 32 or $mask < 0) {
9042
                    continue; // Nonsense.
9043
                }
9044
                if ($mask == 0) {
9045
                    return true;
9046
                }
9047
                if ($mask == 32) {
9048
                    if ($ip === $addr) {
9049
                        return true;
9050
                    }
9051
                    continue;
9052
                }
9053
                $mask = 0xffffffff << (32 - $mask);
9054
                if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9055
                    return true;
9056
                }
9057
            }
9058
        } else if (strpos($subnet, '-') !== false) {
9059
            // 2: xxx.xxx.xxx.xxx-yyy or  xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx::xxxx-yyyy. A range of IP addresses in the last group.
9060
            $parts = explode('-', $subnet);
9061
            if (count($parts) != 2) {
9062
                continue;
9063
            }
9064
 
9065
            if (strpos($subnet, ':') !== false) {
9066
                // IPv6.
9067
                if (!$ipv6) {
9068
                    continue;
9069
                }
9070
                $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9071
                if ($ipstart === null) {
9072
                    continue;
9073
                }
9074
                $ipparts = explode(':', $ipstart);
9075
                $start = hexdec(array_pop($ipparts));
9076
                $ipparts[] = trim($parts[1]);
9077
                $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
9078
                if ($ipend === null) {
9079
                    continue;
9080
                }
9081
                $ipparts[7] = '';
9082
                $ipnet = implode(':', $ipparts);
9083
                if (strpos($addr, $ipnet) !== 0) {
9084
                    continue;
9085
                }
9086
                $ipparts = explode(':', $ipend);
9087
                $end = hexdec($ipparts[7]);
9088
 
9089
                $addrend = hexdec($addrparts[7]);
9090
 
9091
                if (($addrend >= $start) and ($addrend <= $end)) {
9092
                    return true;
9093
                }
9094
            } else {
9095
                // IPv4.
9096
                if ($ipv6) {
9097
                    continue;
9098
                }
9099
                $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9100
                if ($ipstart === null) {
9101
                    continue;
9102
                }
9103
                $ipparts = explode('.', $ipstart);
9104
                $ipparts[3] = trim($parts[1]);
9105
                $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
9106
                if ($ipend === null) {
9107
                    continue;
9108
                }
9109
 
9110
                if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9111
                    return true;
9112
                }
9113
            }
9114
        } else {
9115
            // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9116
            if (strpos($subnet, ':') !== false) {
9117
                // IPv6.
9118
                if (!$ipv6) {
9119
                    continue;
9120
                }
9121
                $parts = explode(':', $subnet);
9122
                $count = count($parts);
1326 ariadna 9123
                if ($parts[$count - 1] === '') {
9124
                    unset($parts[$count - 1]); // Trim trailing :'s.
1 efrain 9125
                    $count--;
9126
                    $subnet = implode('.', $parts);
9127
                }
9128
                $isip = cleanremoteaddr($subnet, false); // Normalise.
9129
                if ($isip !== null) {
9130
                    if ($isip === $addr) {
9131
                        return true;
9132
                    }
9133
                    continue;
9134
                } else if ($count > 8) {
9135
                    continue;
9136
                }
1326 ariadna 9137
                $zeros = array_fill(0, 8 - $count, '0');
9138
                $subnet = $subnet . ':' . implode(':', $zeros) . '/' . ($count * 16);
1 efrain 9139
                if (address_in_subnet($addr, $subnet)) {
9140
                    return true;
9141
                }
9142
            } else {
9143
                // IPv4.
9144
                if ($ipv6) {
9145
                    continue;
9146
                }
9147
                $parts = explode('.', $subnet);
9148
                $count = count($parts);
1326 ariadna 9149
                if ($parts[$count - 1] === '') {
9150
                    unset($parts[$count - 1]); // Trim trailing .
1 efrain 9151
                    $count--;
9152
                    $subnet = implode('.', $parts);
9153
                }
9154
                if ($count == 4) {
9155
                    $subnet = cleanremoteaddr($subnet, false); // Normalise.
9156
                    if ($subnet === $addr) {
9157
                        return true;
9158
                    }
9159
                    continue;
9160
                } else if ($count > 4) {
9161
                    continue;
9162
                }
1326 ariadna 9163
                $zeros = array_fill(0, 4 - $count, '0');
9164
                $subnet = $subnet . '.' . implode('.', $zeros) . '/' . ($count * 8);
1 efrain 9165
                if (address_in_subnet($addr, $subnet)) {
9166
                    return true;
9167
                }
9168
            }
9169
        }
9170
    }
9171
 
9172
    return false;
9173
}
9174
 
9175
/**
9176
 * For outputting debugging info
9177
 *
9178
 * @param string $string The string to write
9179
 * @param string $eol The end of line char(s) to use
9180
 * @param string $sleep Period to make the application sleep
9181
 *                      This ensures any messages have time to display before redirect
9182
 */
1326 ariadna 9183
function mtrace($string, $eol = "\n", $sleep = 0)
9184
{
1 efrain 9185
    global $CFG;
9186
 
9187
    if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
9188
        $fn = $CFG->mtrace_wrapper;
9189
        $fn($string, $eol);
9190
        return;
9191
    } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
9192
        // We must explicitly call the add_line function here.
9193
        // Uses of fwrite to STDOUT are not picked up by ob_start.
9194
        if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
9195
            fwrite(STDOUT, $output);
9196
        }
9197
    } else {
9198
        echo $string . $eol;
9199
    }
9200
 
9201
    // Flush again.
9202
    flush();
9203
 
9204
    // Delay to keep message on user's screen in case of subsequent redirect.
9205
    if ($sleep) {
9206
        sleep($sleep);
9207
    }
9208
}
9209
 
9210
/**
9211
 * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
9212
 *
9213
 * @param Throwable $e the error to ouptput.
9214
 */
1326 ariadna 9215
function mtrace_exception(Throwable $e): void
9216
{
1 efrain 9217
    $info = get_exception_info($e);
9218
 
9219
    $message = $info->message;
9220
    if ($info->debuginfo) {
9221
        $message .= "\n\n" . $info->debuginfo;
9222
    }
9223
    if ($info->backtrace) {
9224
        $message .= "\n\n" . format_backtrace($info->backtrace, true);
9225
    }
9226
 
9227
    mtrace($message);
9228
}
9229
 
9230
/**
9231
 * Replace 1 or more slashes or backslashes to 1 slash
9232
 *
9233
 * @param string $path The path to strip
9234
 * @return string the path with double slashes removed
9235
 */
1326 ariadna 9236
function cleardoubleslashes($path)
9237
{
1 efrain 9238
    return preg_replace('/(\/|\\\){1,}/', '/', $path);
9239
}
9240
 
9241
/**
9242
 * Is the current ip in a given list?
9243
 *
9244
 * @param string $list
9245
 * @return bool
9246
 */
1326 ariadna 9247
function remoteip_in_list($list)
9248
{
1 efrain 9249
    $clientip = getremoteaddr(null);
9250
 
9251
    if (!$clientip) {
9252
        // Ensure access on cli.
9253
        return true;
9254
    }
9255
    return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
9256
}
9257
 
9258
/**
9259
 * Returns most reliable client address
9260
 *
9261
 * @param string $default If an address can't be determined, then return this
9262
 * @return string The remote IP address
9263
 */
1326 ariadna 9264
function getremoteaddr($default = '0.0.0.0')
9265
{
1 efrain 9266
    global $CFG;
9267
 
9268
    if (!isset($CFG->getremoteaddrconf)) {
9269
        // This will happen, for example, before just after the upgrade, as the
9270
        // user is redirected to the admin screen.
9271
        $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
9272
    } else {
9273
        $variablestoskip = $CFG->getremoteaddrconf;
9274
    }
9275
    if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9276
        if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9277
            $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9278
            return $address ? $address : $default;
9279
        }
9280
    }
9281
    if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9282
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9283
            $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9284
 
1326 ariadna 9285
            $forwardedaddresses = array_filter($forwardedaddresses, function ($ip) {
1 efrain 9286
                global $CFG;
9287
                return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9288
            });
9289
 
9290
            // Multiple proxies can append values to this header including an
9291
            // untrusted original request header so we must only trust the last ip.
9292
            $address = end($forwardedaddresses);
9293
 
9294
            if (substr_count($address, ":") > 1) {
9295
                // Remove port and brackets from IPv6.
9296
                if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9297
                    $address = $matches[1];
9298
                }
9299
            } else {
9300
                // Remove port from IPv4.
9301
                if (substr_count($address, ":") == 1) {
9302
                    $parts = explode(":", $address);
9303
                    $address = $parts[0];
9304
                }
9305
            }
9306
 
9307
            $address = cleanremoteaddr($address);
9308
            return $address ? $address : $default;
9309
        }
9310
    }
9311
    if (!empty($_SERVER['REMOTE_ADDR'])) {
9312
        $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9313
        return $address ? $address : $default;
9314
    } else {
9315
        return $default;
9316
    }
9317
}
9318
 
9319
/**
9320
 * Cleans an ip address. Internal addresses are now allowed.
9321
 * (Originally local addresses were not allowed.)
9322
 *
9323
 * @param string $addr IPv4 or IPv6 address
9324
 * @param bool $compress use IPv6 address compression
9325
 * @return string normalised ip address string, null if error
9326
 */
1326 ariadna 9327
function cleanremoteaddr($addr, $compress = false)
9328
{
1 efrain 9329
    $addr = trim($addr);
9330
 
9331
    if (strpos($addr, ':') !== false) {
9332
        // Can be only IPv6.
9333
        $parts = explode(':', $addr);
9334
        $count = count($parts);
9335
 
1326 ariadna 9336
        if (strpos($parts[$count - 1], '.') !== false) {
1 efrain 9337
            // Legacy ipv4 notation.
9338
            $last = array_pop($parts);
9339
            $ipv4 = cleanremoteaddr($last, true);
9340
            if ($ipv4 === null) {
9341
                return null;
9342
            }
9343
            $bits = explode('.', $ipv4);
1326 ariadna 9344
            $parts[] = dechex($bits[0]) . dechex($bits[1]);
9345
            $parts[] = dechex($bits[2]) . dechex($bits[3]);
1 efrain 9346
            $count = count($parts);
9347
            $addr = implode(':', $parts);
9348
        }
9349
 
9350
        if ($count < 3 or $count > 8) {
9351
            return null; // Severly malformed.
9352
        }
9353
 
9354
        if ($count != 8) {
9355
            if (strpos($addr, '::') === false) {
9356
                return null; // Malformed.
9357
            }
9358
            // Uncompress.
9359
            $insertat = array_search('', $parts, true);
9360
            $missing = array_fill(0, 1 + 8 - $count, '0');
9361
            array_splice($parts, $insertat, 1, $missing);
9362
            foreach ($parts as $key => $part) {
9363
                if ($part === '') {
9364
                    $parts[$key] = '0';
9365
                }
9366
            }
9367
        }
9368
 
9369
        $adr = implode(':', $parts);
9370
        if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9371
            return null; // Incorrect format - sorry.
9372
        }
9373
 
9374
        // Normalise 0s and case.
9375
        $parts = array_map('hexdec', $parts);
9376
        $parts = array_map('dechex', $parts);
9377
 
9378
        $result = implode(':', $parts);
9379
 
9380
        if (!$compress) {
9381
            return $result;
9382
        }
9383
 
9384
        if ($result === '0:0:0:0:0:0:0:0') {
9385
            return '::'; // All addresses.
9386
        }
9387
 
9388
        $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9389
        if ($compressed !== $result) {
9390
            return $compressed;
9391
        }
9392
 
9393
        $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9394
        if ($compressed !== $result) {
9395
            return $compressed;
9396
        }
9397
 
9398
        $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9399
        if ($compressed !== $result) {
9400
            return $compressed;
9401
        }
9402
 
9403
        return $result;
9404
    }
9405
 
9406
    // First get all things that look like IPv4 addresses.
9407
    $parts = array();
9408
    if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9409
        return null;
9410
    }
9411
    unset($parts[0]);
9412
 
9413
    foreach ($parts as $key => $match) {
9414
        if ($match > 255) {
9415
            return null;
9416
        }
9417
        $parts[$key] = (int)$match; // Normalise 0s.
9418
    }
9419
 
9420
    return implode('.', $parts);
9421
}
9422
 
9423
 
9424
/**
9425
 * Is IP address a public address?
9426
 *
9427
 * @param string $ip The ip to check
9428
 * @return bool true if the ip is public
9429
 */
1326 ariadna 9430
function ip_is_public($ip)
9431
{
1 efrain 9432
    return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9433
}
9434
 
9435
/**
9436
 * This function will make a complete copy of anything it's given,
9437
 * regardless of whether it's an object or not.
9438
 *
9439
 * @param mixed $thing Something you want cloned
9440
 * @return mixed What ever it is you passed it
9441
 */
1326 ariadna 9442
function fullclone($thing)
9443
{
1 efrain 9444
    return unserialize(serialize($thing));
9445
}
9446
 
9447
/**
9448
 * Used to make sure that $min <= $value <= $max
9449
 *
9450
 * Make sure that value is between min, and max
9451
 *
9452
 * @param int $min The minimum value
9453
 * @param int $value The value to check
9454
 * @param int $max The maximum value
9455
 * @return int
9456
 */
1326 ariadna 9457
function bounded_number($min, $value, $max)
9458
{
1 efrain 9459
    if ($value < $min) {
9460
        return $min;
9461
    }
9462
    if ($value > $max) {
9463
        return $max;
9464
    }
9465
    return $value;
9466
}
9467
 
9468
/**
9469
 * Check if there is a nested array within the passed array
9470
 *
9471
 * @param array $array
9472
 * @return bool true if there is a nested array false otherwise
9473
 */
1326 ariadna 9474
function array_is_nested($array)
9475
{
1 efrain 9476
    foreach ($array as $value) {
9477
        if (is_array($value)) {
9478
            return true;
9479
        }
9480
    }
9481
    return false;
9482
}
9483
 
9484
/**
9485
 * get_performance_info() pairs up with init_performance_info()
9486
 * loaded in setup.php. Returns an array with 'html' and 'txt'
9487
 * values ready for use, and each of the individual stats provided
9488
 * separately as well.
9489
 *
9490
 * @return array
9491
 */
1326 ariadna 9492
function get_performance_info()
9493
{
1 efrain 9494
    global $CFG, $PERF, $DB, $PAGE;
9495
 
9496
    $info = array();
9497
    $info['txt']  = me() . ' '; // Holds log-friendly representation.
9498
 
9499
    $info['html'] = '';
9500
    if (!empty($CFG->themedesignermode)) {
9501
        // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9502
        $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9503
    }
9504
    $info['html'] .= '<ul class="list-unstyled row mx-md-0">';         // Holds userfriendly HTML representation.
9505
 
9506
    $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9507
 
1326 ariadna 9508
    $info['html'] .= '<li class="timeused col-sm-4">' . $info['realtime'] . ' secs</li> ';
9509
    $info['txt'] .= 'time: ' . $info['realtime'] . 's ';
1 efrain 9510
 
9511
    // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9512
    $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9513
 
9514
    if (function_exists('memory_get_usage')) {
9515
        $info['memory_total'] = memory_get_usage();
9516
        $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
1326 ariadna 9517
        $info['html'] .= '<li class="memoryused col-sm-4">RAM: ' . display_size($info['memory_total']) . '</li> ';
9518
        $info['txt']  .= 'memory_total: ' . $info['memory_total'] . 'B (' . display_size($info['memory_total']) . ') memory_growth: ' .
9519
            $info['memory_growth'] . 'B (' . display_size($info['memory_growth']) . ') ';
1 efrain 9520
    }
9521
 
9522
    if (function_exists('memory_get_peak_usage')) {
9523
        $info['memory_peak'] = memory_get_peak_usage();
1326 ariadna 9524
        $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: ' . display_size($info['memory_peak']) . '</li> ';
9525
        $info['txt']  .= 'memory_peak: ' . $info['memory_peak'] . 'B (' . display_size($info['memory_peak']) . ') ';
1 efrain 9526
    }
9527
 
9528
    $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9529
    $inc = get_included_files();
9530
    $info['includecount'] = count($inc);
1326 ariadna 9531
    $info['html'] .= '<li class="included col-sm-4">Included ' . $info['includecount'] . ' files</li> ';
9532
    $info['txt']  .= 'includecount: ' . $info['includecount'] . ' ';
1 efrain 9533
 
9534
    if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9535
        // We can not track more performance before installation or before PAGE init, sorry.
9536
        return $info;
9537
    }
9538
 
9539
    $filtermanager = filter_manager::instance();
9540
    if (method_exists($filtermanager, 'get_performance_summary')) {
9541
        list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9542
        $info = array_merge($filterinfo, $info);
9543
        foreach ($filterinfo as $key => $value) {
9544
            $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9545
            $info['txt'] .= "$key: $value ";
9546
        }
9547
    }
9548
 
9549
    $stringmanager = get_string_manager();
9550
    if (method_exists($stringmanager, 'get_performance_summary')) {
9551
        list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9552
        $info = array_merge($filterinfo, $info);
9553
        foreach ($filterinfo as $key => $value) {
9554
            $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9555
            $info['txt'] .= "$key: $value ";
9556
        }
9557
    }
9558
 
1326 ariadna 9559
    $info['dbqueries'] = $DB->perf_get_reads() . '/' . $DB->perf_get_writes();
9560
    $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: ' . $info['dbqueries'] . '</li> ';
9561
    $info['txt'] .= 'db reads/writes: ' . $info['dbqueries'] . ' ';
1 efrain 9562
 
9563
    if ($DB->want_read_slave()) {
9564
        $info['dbreads_slave'] = $DB->perf_get_reads_slave();
1326 ariadna 9565
        $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: ' . $info['dbreads_slave'] . '</li> ';
9566
        $info['txt'] .= 'db reads from slave: ' . $info['dbreads_slave'] . ' ';
1 efrain 9567
    }
9568
 
9569
    $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
1326 ariadna 9570
    $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: ' . $info['dbtime'] . ' secs</li> ';
1 efrain 9571
    $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9572
 
9573
    if (function_exists('posix_times')) {
9574
        $ptimes = posix_times();
9575
        if (is_array($ptimes)) {
9576
            foreach ($ptimes as $key => $val) {
9577
                $info[$key] = $ptimes[$key] -  $PERF->startposixtimes[$key];
9578
            }
9579
            $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9580
            $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9581
            $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9582
        }
9583
    }
9584
 
9585
    // Grab the load average for the last minute.
9586
    // /proc will only work under some linux configurations
9587
    // while uptime is there under MacOSX/Darwin and other unices.
9588
    if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9589
        list($serverload) = explode(' ', $loadavg[0]);
9590
        unset($loadavg);
1326 ariadna 9591
    } else if (function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime`) {
1 efrain 9592
        if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9593
            $serverload = $matches[1];
9594
        } else {
9595
            trigger_error('Could not parse uptime output!');
9596
        }
9597
    }
9598
    if (!empty($serverload)) {
9599
        $info['serverload'] = $serverload;
1326 ariadna 9600
        $info['html'] .= '<li class="serverload col-sm-4">Load average: ' . $info['serverload'] . '</li> ';
1 efrain 9601
        $info['txt'] .= "serverload: {$info['serverload']} ";
9602
    }
9603
 
9604
    // Display size of session if session started.
9605
    if ($si = \core\session\manager::get_performance_info()) {
9606
        $info['sessionsize'] = $si['size'];
9607
        $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9608
        $info['txt'] .= $si['txt'];
9609
    }
9610
 
9611
    // Display time waiting for session if applicable.
9612
    if (!empty($PERF->sessionlock['wait'])) {
9613
        $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs';
9614
        $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [
9615
            'class' => 'sessionwait col-sm-4'
9616
        ]);
9617
        $info['txt'] .= 'sessionwait: ' . $sessionwait . ' ';
9618
    }
9619
 
9620
    $info['html'] .= '</ul>';
9621
    $html = '';
9622
    if ($stats = cache_helper::get_stats()) {
9623
 
9624
        $table = new html_table();
9625
        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9626
        $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9627
        $table->data = [];
9628
        $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9629
 
9630
        $text = 'Caches used (hits/misses/sets): ';
9631
        $hits = 0;
9632
        $misses = 0;
9633
        $sets = 0;
9634
        $maxstores = 0;
9635
 
9636
        // We want to align static caches into their own column.
9637
        $hasstatic = false;
9638
        foreach ($stats as $definition => $details) {
9639
            $numstores = count($details['stores']);
9640
            $first = key($details['stores']);
9641
            if ($first !== cache_store::STATIC_ACCEL) {
9642
                $numstores++; // Add a blank space for the missing static store.
9643
            }
9644
            $maxstores = max($maxstores, $numstores);
9645
        }
9646
 
9647
        $storec = 0;
9648
 
9649
        while ($storec++ < ($maxstores - 2)) {
9650
            if ($storec == ($maxstores - 2)) {
9651
                $table->head[] = get_string('mappingfinal', 'cache');
9652
            } else {
9653
                $table->head[] = "Store $storec";
9654
            }
9655
            $table->align[] = 'left';
9656
            $table->align[] = 'right';
9657
            $table->align[] = 'right';
9658
            $table->align[] = 'right';
9659
            $table->align[] = 'right';
9660
            $table->head[] = 'H';
9661
            $table->head[] = 'M';
9662
            $table->head[] = 'S';
9663
            $table->head[] = 'I/O';
9664
        }
9665
 
9666
        ksort($stats);
9667
 
9668
        foreach ($stats as $definition => $details) {
9669
            switch ($details['mode']) {
9670
                case cache_store::MODE_APPLICATION:
9671
                    $modeclass = 'application';
9672
                    $mode = ' <span title="application cache">App</span>';
9673
                    break;
9674
                case cache_store::MODE_SESSION:
9675
                    $modeclass = 'session';
9676
                    $mode = ' <span title="session cache">Ses</span>';
9677
                    break;
9678
                case cache_store::MODE_REQUEST:
9679
                    $modeclass = 'request';
9680
                    $mode = ' <span title="request cache">Req</span>';
9681
                    break;
9682
            }
9683
            $row = [$mode, $definition];
9684
 
9685
            $text .= "$definition {";
9686
 
9687
            $storec = 0;
9688
            foreach ($details['stores'] as $store => $data) {
9689
 
9690
                if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9691
                    $row[] = '';
9692
                    $row[] = '';
9693
                    $row[] = '';
9694
                    $storec++;
9695
                }
9696
 
9697
                $hits   += $data['hits'];
9698
                $misses += $data['misses'];
9699
                $sets   += $data['sets'];
9700
                if ($data['hits'] == 0 and $data['misses'] > 0) {
9701
                    $cachestoreclass = 'nohits bg-danger';
9702
                } else if ($data['hits'] < $data['misses']) {
9703
                    $cachestoreclass = 'lowhits bg-warning text-dark';
9704
                } else {
9705
                    $cachestoreclass = 'hihits';
9706
                }
9707
                $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9708
                $cell = new html_table_cell($store);
9709
                $cell->attributes = ['class' => $cachestoreclass];
9710
                $row[] = $cell;
9711
                $cell = new html_table_cell($data['hits']);
9712
                $cell->attributes = ['class' => $cachestoreclass];
9713
                $row[] = $cell;
9714
                $cell = new html_table_cell($data['misses']);
9715
                $cell->attributes = ['class' => $cachestoreclass];
9716
                $row[] = $cell;
9717
 
9718
                if ($store !== cache_store::STATIC_ACCEL) {
9719
                    // The static cache is never set.
9720
                    $cell = new html_table_cell($data['sets']);
9721
                    $cell->attributes = ['class' => $cachestoreclass];
9722
                    $row[] = $cell;
9723
 
9724
                    if ($data['hits'] || $data['sets']) {
9725
                        if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
9726
                            $size = '-';
9727
                        } else {
9728
                            $size = display_size($data['iobytes'], 1, 'KB');
9729
                            if ($data['iobytes'] >= 10 * 1024) {
9730
                                $cachestoreclass = ' bg-warning text-dark';
9731
                            }
9732
                        }
9733
                    } else {
9734
                        $size = '';
9735
                    }
9736
                    $cell = new html_table_cell($size);
9737
                    $cell->attributes = ['class' => $cachestoreclass];
9738
                    $row[] = $cell;
9739
                }
9740
                $storec++;
9741
            }
9742
            while ($storec++ < $maxstores) {
9743
                $row[] = '';
9744
                $row[] = '';
9745
                $row[] = '';
9746
                $row[] = '';
9747
                $row[] = '';
9748
            }
9749
            $text .= '} ';
9750
 
9751
            $table->data[] = $row;
9752
        }
9753
 
9754
        $html .= html_writer::table($table);
9755
 
9756
        // Now lets also show sub totals for each cache store.
9757
        $storetotals = [];
9758
        $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9759
        foreach ($stats as $definition => $details) {
9760
            foreach ($details['stores'] as $store => $data) {
9761
                if (!array_key_exists($store, $storetotals)) {
9762
                    $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9763
                }
9764
                $storetotals[$store]['class']   = $data['class'];
9765
                $storetotals[$store]['hits']   += $data['hits'];
9766
                $storetotals[$store]['misses'] += $data['misses'];
9767
                $storetotals[$store]['sets']   += $data['sets'];
9768
                $storetotal['hits']   += $data['hits'];
9769
                $storetotal['misses'] += $data['misses'];
9770
                $storetotal['sets']   += $data['sets'];
9771
                if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) {
9772
                    $storetotals[$store]['iobytes'] += $data['iobytes'];
9773
                    $storetotal['iobytes'] += $data['iobytes'];
9774
                }
9775
            }
9776
        }
9777
 
9778
        $table = new html_table();
9779
        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9780
        $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9781
        $table->data = [];
9782
        $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
9783
 
9784
        ksort($storetotals);
9785
 
9786
        foreach ($storetotals as $store => $data) {
9787
            $row = [];
9788
            if ($data['hits'] == 0 and $data['misses'] > 0) {
9789
                $cachestoreclass = 'nohits bg-danger';
9790
            } else if ($data['hits'] < $data['misses']) {
9791
                $cachestoreclass = 'lowhits bg-warning text-dark';
9792
            } else {
9793
                $cachestoreclass = 'hihits';
9794
            }
9795
            $cell = new html_table_cell($store);
9796
            $cell->attributes = ['class' => $cachestoreclass];
9797
            $row[] = $cell;
9798
            $cell = new html_table_cell($data['class']);
9799
            $cell->attributes = ['class' => $cachestoreclass];
9800
            $row[] = $cell;
9801
            $cell = new html_table_cell($data['hits']);
9802
            $cell->attributes = ['class' => $cachestoreclass];
9803
            $row[] = $cell;
9804
            $cell = new html_table_cell($data['misses']);
9805
            $cell->attributes = ['class' => $cachestoreclass];
9806
            $row[] = $cell;
9807
            $cell = new html_table_cell($data['sets']);
9808
            $cell->attributes = ['class' => $cachestoreclass];
9809
            $row[] = $cell;
9810
            if ($data['hits'] || $data['sets']) {
9811
                if ($data['iobytes']) {
9812
                    $size = display_size($data['iobytes'], 1, 'KB');
9813
                } else {
9814
                    $size = '-';
9815
                }
9816
            } else {
9817
                $size = '';
9818
            }
9819
            $cell = new html_table_cell($size);
9820
            $cell->attributes = ['class' => $cachestoreclass];
9821
            $row[] = $cell;
9822
            $table->data[] = $row;
9823
        }
9824
        if (!empty($storetotal['iobytes'])) {
9825
            $size = display_size($storetotal['iobytes'], 1, 'KB');
9826
        } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) {
9827
            $size = '-';
9828
        } else {
9829
            $size = '';
9830
        }
9831
        $row = [
9832
            get_string('total'),
9833
            '',
9834
            $storetotal['hits'],
9835
            $storetotal['misses'],
9836
            $storetotal['sets'],
9837
            $size,
9838
        ];
9839
        $table->data[] = $row;
9840
 
9841
        $html .= html_writer::table($table);
9842
 
9843
        $info['cachesused'] = "$hits / $misses / $sets";
9844
        $info['html'] .= $html;
1326 ariadna 9845
        $info['txt'] .= $text . '. ';
1 efrain 9846
    } else {
9847
        $info['cachesused'] = '0 / 0 / 0';
9848
        $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9849
        $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9850
    }
9851
 
9852
    // Display lock information if any.
9853
    if (!empty($PERF->locks)) {
9854
        $table = new html_table();
9855
        $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered';
9856
        $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)'];
9857
        $table->align = ['left', 'right', 'center', 'right'];
9858
        $table->data = [];
9859
        $text = 'Locks (waited/obtained/held):';
9860
        foreach ($PERF->locks as $locktiming) {
9861
            $row = [];
9862
            $row[] = s($locktiming->type . '/' . $locktiming->resource);
9863
            $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' (';
9864
 
9865
            // The time we had to wait to get the lock.
9866
            $roundedtime = number_format($locktiming->wait, 1);
9867
            $cell = new html_table_cell($roundedtime);
9868
            if ($locktiming->wait > 0.5) {
9869
                $cell->attributes = ['class' => 'bg-warning text-dark'];
9870
            }
9871
            $row[] = $cell;
9872
            $text .= $roundedtime . '/';
9873
 
9874
            // Show a tick or cross for success.
9875
            $row[] = $locktiming->success ? '&#x2713;' : '&#x274c;';
9876
            $text .= ($locktiming->success ? 'y' : 'n') . '/';
9877
 
9878
            // If applicable, show how long we held the lock before releasing it.
9879
            if (property_exists($locktiming, 'held')) {
9880
                $roundedtime = number_format($locktiming->held, 1);
9881
                $cell = new html_table_cell($roundedtime);
9882
                if ($locktiming->held > 0.5) {
9883
                    $cell->attributes = ['class' => 'bg-warning text-dark'];
9884
                }
9885
                $row[] = $cell;
9886
                $text .= $roundedtime;
9887
            } else {
9888
                $row[] = '-';
9889
                $text .= '-';
9890
            }
9891
            $text .= ')';
9892
 
9893
            $table->data[] = $row;
9894
        }
9895
        $info['html'] .= html_writer::table($table);
9896
        $info['txt'] .= $text . '. ';
9897
    }
9898
 
1326 ariadna 9899
    $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">' . $info['html'] . '</div>';
1 efrain 9900
    return $info;
9901
}
9902
 
9903
/**
9904
 * Renames a file or directory to a unique name within the same directory.
9905
 *
9906
 * This function is designed to avoid any potential race conditions, and select an unused name.
9907
 *
9908
 * @param string $filepath Original filepath
9909
 * @param string $prefix Prefix to use for the temporary name
9910
 * @return string|bool New file path or false if failed
9911
 * @since Moodle 3.10
9912
 */
1326 ariadna 9913
function rename_to_unused_name(string $filepath, string $prefix = '_temp_')
9914
{
1 efrain 9915
    $dir = dirname($filepath);
9916
    $basename = $dir . '/' . $prefix;
9917
    $limit = 0;
9918
    while ($limit < 100) {
9919
        // Select a new name based on a random number.
9920
        $newfilepath = $basename . md5(mt_rand());
9921
 
9922
        // Attempt a rename to that new name.
9923
        if (@rename($filepath, $newfilepath)) {
9924
            return $newfilepath;
9925
        }
9926
 
9927
        // The first time, do some sanity checks, maybe it is failing for a good reason and there
9928
        // is no point trying 100 times if so.
9929
        if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
9930
            return false;
9931
        }
9932
        $limit++;
9933
    }
9934
    return false;
9935
}
9936
 
9937
/**
9938
 * Delete directory or only its content
9939
 *
9940
 * @param string $dir directory path
9941
 * @param bool $contentonly
9942
 * @return bool success, true also if dir does not exist
9943
 */
1326 ariadna 9944
function remove_dir($dir, $contentonly = false)
9945
{
1 efrain 9946
    if (!is_dir($dir)) {
9947
        // Nothing to do.
9948
        return true;
9949
    }
9950
 
9951
    if (!$contentonly) {
9952
        // Start by renaming the directory; this will guarantee that other processes don't write to it
9953
        // while it is in the process of being deleted.
9954
        $tempdir = rename_to_unused_name($dir);
9955
        if ($tempdir) {
9956
            // If the rename was successful then delete the $tempdir instead.
9957
            $dir = $tempdir;
9958
        }
9959
        // If the rename fails, we will continue through and attempt to delete the directory
9960
        // without renaming it since that is likely to at least delete most of the files.
9961
    }
9962
 
9963
    if (!$handle = opendir($dir)) {
9964
        return false;
9965
    }
9966
    $result = true;
1326 ariadna 9967
    while (false !== ($item = readdir($handle))) {
1 efrain 9968
        if ($item != '.' && $item != '..') {
1326 ariadna 9969
            if (is_dir($dir . '/' . $item)) {
9970
                $result = remove_dir($dir . '/' . $item) && $result;
1 efrain 9971
            } else {
1326 ariadna 9972
                $result = unlink($dir . '/' . $item) && $result;
1 efrain 9973
            }
9974
        }
9975
    }
9976
    closedir($handle);
9977
    if ($contentonly) {
9978
        clearstatcache(); // Make sure file stat cache is properly invalidated.
9979
        return $result;
9980
    }
9981
    $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9982
    clearstatcache(); // Make sure file stat cache is properly invalidated.
9983
    return $result;
9984
}
9985
 
9986
/**
9987
 * Detect if an object or a class contains a given property
9988
 * will take an actual object or the name of a class
9989
 *
9990
 * @param mixed $obj Name of class or real object to test
9991
 * @param string $property name of property to find
9992
 * @return bool true if property exists
9993
 */
1326 ariadna 9994
function object_property_exists($obj, $property)
9995
{
9996
    if (is_string($obj)) {
9997
        $properties = get_class_vars($obj);
1 efrain 9998
    } else {
1326 ariadna 9999
        $properties = get_object_vars($obj);
1 efrain 10000
    }
1326 ariadna 10001
    return array_key_exists($property, $properties);
1 efrain 10002
}
10003
 
10004
/**
10005
 * Converts an object into an associative array
10006
 *
10007
 * This function converts an object into an associative array by iterating
10008
 * over its public properties. Because this function uses the foreach
10009
 * construct, Iterators are respected. It works recursively on arrays of objects.
10010
 * Arrays and simple values are returned as is.
10011
 *
10012
 * If class has magic properties, it can implement IteratorAggregate
10013
 * and return all available properties in getIterator()
10014
 *
10015
 * @param mixed $var
10016
 * @return array
10017
 */
1326 ariadna 10018
function convert_to_array($var)
10019
{
1 efrain 10020
    $result = array();
10021
 
10022
    // Loop over elements/properties.
10023
    foreach ($var as $key => $value) {
10024
        // Recursively convert objects.
10025
        if (is_object($value) || is_array($value)) {
10026
            $result[$key] = convert_to_array($value);
10027
        } else {
10028
            // Simple values are untouched.
10029
            $result[$key] = $value;
10030
        }
10031
    }
10032
    return $result;
10033
}
10034
 
10035
/**
10036
 * Detect a custom script replacement in the data directory that will
10037
 * replace an existing moodle script
10038
 *
10039
 * @return string|bool full path name if a custom script exists, false if no custom script exists
10040
 */
1326 ariadna 10041
function custom_script_path()
10042
{
1 efrain 10043
    global $CFG, $SCRIPT;
10044
 
10045
    if ($SCRIPT === null) {
10046
        // Probably some weird external script.
10047
        return false;
10048
    }
10049
 
10050
    $scriptpath = $CFG->customscripts . $SCRIPT;
10051
 
10052
    // Check the custom script exists.
10053
    if (file_exists($scriptpath) and is_file($scriptpath)) {
10054
        return $scriptpath;
10055
    } else {
10056
        return false;
10057
    }
10058
}
10059
 
10060
/**
10061
 * Returns whether or not the user object is a remote MNET user. This function
10062
 * is in moodlelib because it does not rely on loading any of the MNET code.
10063
 *
10064
 * @param object $user A valid user object
10065
 * @return bool        True if the user is from a remote Moodle.
10066
 */
1326 ariadna 10067
function is_mnet_remote_user($user)
10068
{
1 efrain 10069
    global $CFG;
10070
 
10071
    if (!isset($CFG->mnet_localhost_id)) {
10072
        include_once($CFG->dirroot . '/mnet/lib.php');
10073
        $env = new mnet_environment();
10074
        $env->init();
10075
        unset($env);
10076
    }
10077
 
10078
    return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10079
}
10080
 
10081
/**
10082
 * This function will search for browser prefereed languages, setting Moodle
10083
 * to use the best one available if $SESSION->lang is undefined
10084
 */
1326 ariadna 10085
function setup_lang_from_browser()
10086
{
1 efrain 10087
    global $CFG, $SESSION, $USER;
10088
 
10089
    if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10090
        // Lang is defined in session or user profile, nothing to do.
10091
        return;
10092
    }
10093
 
10094
    if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
10095
        return;
10096
    }
10097
 
10098
    // Extract and clean langs from headers.
10099
    $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10100
    $rawlangs = str_replace('-', '_', $rawlangs);         // We are using underscores.
10101
    $rawlangs = explode(',', $rawlangs);                  // Convert to array.
10102
    $langs = array();
10103
 
10104
    $order = 1.0;
10105
    foreach ($rawlangs as $lang) {
10106
        if (strpos($lang, ';') === false) {
10107
            $langs[(string)$order] = $lang;
1326 ariadna 10108
            $order = $order - 0.01;
1 efrain 10109
        } else {
10110
            $parts = explode(';', $lang);
10111
            $pos = strpos($parts[1], '=');
1326 ariadna 10112
            $langs[substr($parts[1], $pos + 1)] = $parts[0];
1 efrain 10113
        }
10114
    }
10115
    krsort($langs, SORT_NUMERIC);
10116
 
10117
    // Look for such langs under standard locations.
10118
    foreach ($langs as $lang) {
10119
        // Clean it properly for include.
10120
        $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
10121
        if (get_string_manager()->translation_exists($lang, false)) {
10122
            // If the translation for this language exists then try to set it
10123
            // for the rest of the session, if this is a read only session then
10124
            // we can only set it temporarily in $CFG.
10125
            if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) {
10126
                $CFG->lang = $lang;
10127
            } else {
10128
                $SESSION->lang = $lang;
10129
            }
10130
            // We have finished. Go out.
10131
            break;
10132
        }
10133
    }
10134
    return;
10135
}
10136
 
10137
/**
10138
 * Check if $url matches anything in proxybypass list
10139
 *
10140
 * Any errors just result in the proxy being used (least bad)
10141
 *
10142
 * @param string $url url to check
10143
 * @return boolean true if we should bypass the proxy
10144
 */
1326 ariadna 10145
function is_proxybypass($url)
10146
{
1 efrain 10147
    global $CFG;
10148
 
10149
    // Sanity check.
10150
    if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10151
        return false;
10152
    }
10153
 
10154
    // Get the host part out of the url.
1326 ariadna 10155
    if (!$host = parse_url($url, PHP_URL_HOST)) {
1 efrain 10156
        return false;
10157
    }
10158
 
10159
    // Get the possible bypass hosts into an array.
1326 ariadna 10160
    $matches = explode(',', $CFG->proxybypass);
1 efrain 10161
 
10162
    // Check for a exact match on the IP or in the domains.
10163
    $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches);
10164
    $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ',');
10165
 
10166
    if ($isdomaininallowedlist || $isipinsubnetlist) {
10167
        return true;
10168
    }
10169
 
10170
    // Nothing matched.
10171
    return false;
10172
}
10173
 
10174
/**
10175
 * Check if the passed navigation is of the new style
10176
 *
10177
 * @param mixed $navigation
10178
 * @return bool true for yes false for no
10179
 */
1326 ariadna 10180
function is_newnav($navigation)
10181
{
1 efrain 10182
    if (is_array($navigation) && !empty($navigation['newnav'])) {
10183
        return true;
10184
    } else {
10185
        return false;
10186
    }
10187
}
10188
 
10189
/**
10190
 * Checks whether the given variable name is defined as a variable within the given object.
10191
 *
10192
 * This will NOT work with stdClass objects, which have no class variables.
10193
 *
10194
 * @param string $var The variable name
10195
 * @param object $object The object to check
10196
 * @return boolean
10197
 */
1326 ariadna 10198
function in_object_vars($var, $object)
10199
{
1 efrain 10200
    $classvars = get_class_vars(get_class($object));
10201
    $classvars = array_keys($classvars);
10202
    return in_array($var, $classvars);
10203
}
10204
 
10205
/**
10206
 * Returns an array without repeated objects.
10207
 * This function is similar to array_unique, but for arrays that have objects as values
10208
 *
10209
 * @param array $array
10210
 * @param bool $keepkeyassoc
10211
 * @return array
10212
 */
1326 ariadna 10213
function object_array_unique($array, $keepkeyassoc = true)
10214
{
1 efrain 10215
    $duplicatekeys = array();
10216
    $tmp         = array();
10217
 
10218
    foreach ($array as $key => $val) {
10219
        // Convert objects to arrays, in_array() does not support objects.
10220
        if (is_object($val)) {
10221
            $val = (array)$val;
10222
        }
10223
 
10224
        if (!in_array($val, $tmp)) {
10225
            $tmp[] = $val;
10226
        } else {
10227
            $duplicatekeys[] = $key;
10228
        }
10229
    }
10230
 
10231
    foreach ($duplicatekeys as $key) {
10232
        unset($array[$key]);
10233
    }
10234
 
10235
    return $keepkeyassoc ? $array : array_values($array);
10236
}
10237
 
10238
/**
10239
 * Is a userid the primary administrator?
10240
 *
10241
 * @param int $userid int id of user to check
10242
 * @return boolean
10243
 */
1326 ariadna 10244
function is_primary_admin($userid)
10245
{
1 efrain 10246
    $primaryadmin =  get_admin();
10247
 
10248
    if ($userid == $primaryadmin->id) {
10249
        return true;
10250
    } else {
10251
        return false;
10252
    }
10253
}
10254
 
10255
/**
10256
 * Returns the site identifier
10257
 *
10258
 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10259
 */
1326 ariadna 10260
function get_site_identifier()
10261
{
1 efrain 10262
    global $CFG;
10263
    // Check to see if it is missing. If so, initialise it.
10264
    if (empty($CFG->siteidentifier)) {
10265
        set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10266
    }
10267
    // Return it.
10268
    return $CFG->siteidentifier;
10269
}
10270
 
10271
/**
10272
 * Check whether the given password has no more than the specified
10273
 * number of consecutive identical characters.
10274
 *
10275
 * @param string $password   password to be checked against the password policy
10276
 * @param integer $maxchars  maximum number of consecutive identical characters
10277
 * @return bool
10278
 */
1326 ariadna 10279
function check_consecutive_identical_characters($password, $maxchars)
10280
{
1 efrain 10281
 
10282
    if ($maxchars < 1) {
10283
        return true; // Zero 0 is to disable this check.
10284
    }
10285
    if (strlen($password) <= $maxchars) {
10286
        return true; // Too short to fail this test.
10287
    }
10288
 
10289
    $previouschar = '';
10290
    $consecutivecount = 1;
10291
    foreach (str_split($password) as $char) {
10292
        if ($char != $previouschar) {
10293
            $consecutivecount = 1;
10294
        } else {
10295
            $consecutivecount++;
10296
            if ($consecutivecount > $maxchars) {
10297
                return false; // Check failed already.
10298
            }
10299
        }
10300
 
10301
        $previouschar = $char;
10302
    }
10303
 
10304
    return true;
10305
}
10306
 
10307
/**
10308
 * Helper function to do partial function binding.
10309
 * so we can use it for preg_replace_callback, for example
10310
 * this works with php functions, user functions, static methods and class methods
10311
 * it returns you a callback that you can pass on like so:
10312
 *
10313
 * $callback = partial('somefunction', $arg1, $arg2);
10314
 *     or
10315
 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10316
 *     or even
10317
 * $obj = new someclass();
10318
 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10319
 *
10320
 * and then the arguments that are passed through at calltime are appended to the argument list.
10321
 *
10322
 * @param mixed $function a php callback
10323
 * @param mixed $arg1,... $argv arguments to partially bind with
10324
 * @return array Array callback
10325
 */
1326 ariadna 10326
function partial()
10327
{
1 efrain 10328
    if (!class_exists('partial')) {
10329
        /**
10330
         * Used to manage function binding.
10331
         * @copyright  2009 Penny Leach
10332
         * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10333
         */
1326 ariadna 10334
        class partial
10335
        {
1 efrain 10336
            /** @var array */
10337
            public $values = array();
10338
            /** @var string The function to call as a callback. */
10339
            public $func;
10340
            /**
10341
             * Constructor
10342
             * @param string $func
10343
             * @param array $args
10344
             */
1326 ariadna 10345
            public function __construct($func, $args)
10346
            {
1 efrain 10347
                $this->values = $args;
10348
                $this->func = $func;
10349
            }
10350
            /**
10351
             * Calls the callback function.
10352
             * @return mixed
10353
             */
1326 ariadna 10354
            public function method()
10355
            {
1 efrain 10356
                $args = func_get_args();
10357
                return call_user_func_array($this->func, array_merge($this->values, $args));
10358
            }
10359
        }
10360
    }
10361
    $args = func_get_args();
10362
    $func = array_shift($args);
10363
    $p = new partial($func, $args);
10364
    return array($p, 'method');
10365
}
10366
 
10367
/**
10368
 * helper function to load up and initialise the mnet environment
10369
 * this must be called before you use mnet functions.
10370
 *
10371
 * @return mnet_environment the equivalent of old $MNET global
10372
 */
1326 ariadna 10373
function get_mnet_environment()
10374
{
1 efrain 10375
    global $CFG;
10376
    require_once($CFG->dirroot . '/mnet/lib.php');
10377
    static $instance = null;
10378
    if (empty($instance)) {
10379
        $instance = new mnet_environment();
10380
        $instance->init();
10381
    }
10382
    return $instance;
10383
}
10384
 
10385
/**
10386
 * during xmlrpc server code execution, any code wishing to access
10387
 * information about the remote peer must use this to get it.
10388
 *
10389
 * @return mnet_remote_client|false the equivalent of old $MNETREMOTE_CLIENT global
10390
 */
1326 ariadna 10391
function get_mnet_remote_client()
10392
{
1 efrain 10393
    if (!defined('MNET_SERVER')) {
10394
        debugging(get_string('notinxmlrpcserver', 'mnet'));
10395
        return false;
10396
    }
10397
    global $MNET_REMOTE_CLIENT;
10398
    if (isset($MNET_REMOTE_CLIENT)) {
10399
        return $MNET_REMOTE_CLIENT;
10400
    }
10401
    return false;
10402
}
10403
 
10404
/**
10405
 * during the xmlrpc server code execution, this will be called
10406
 * to setup the object returned by {@link get_mnet_remote_client}
10407
 *
10408
 * @param mnet_remote_client $client the client to set up
10409
 * @throws moodle_exception
10410
 */
1326 ariadna 10411
function set_mnet_remote_client($client)
10412
{
1 efrain 10413
    if (!defined('MNET_SERVER')) {
10414
        throw new moodle_exception('notinxmlrpcserver', 'mnet');
10415
    }
10416
    global $MNET_REMOTE_CLIENT;
10417
    $MNET_REMOTE_CLIENT = $client;
10418
}
10419
 
10420
/**
10421
 * return the jump url for a given remote user
10422
 * this is used for rewriting forum post links in emails, etc
10423
 *
10424
 * @param stdclass $user the user to get the idp url for
10425
 */
1326 ariadna 10426
function mnet_get_idp_jump_url($user)
10427
{
1 efrain 10428
    global $CFG;
10429
 
10430
    static $mnetjumps = array();
10431
    if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10432
        $idp = mnet_get_peer_host($user->mnethostid);
10433
        $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10434
        $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10435
    }
10436
    return $mnetjumps[$user->mnethostid];
10437
}
10438
 
10439
/**
10440
 * Gets the homepage to use for the current user
10441
 *
10442
 * @return int One of HOMEPAGE_*
10443
 */
1326 ariadna 10444
function get_home_page()
10445
{
1 efrain 10446
    global $CFG;
10447
 
10448
    if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10449
        // If dashboard is disabled, home will be set to default page.
10450
        $defaultpage = get_default_home_page();
10451
        if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10452
            if (!empty($CFG->enabledashboard)) {
10453
                return HOMEPAGE_MY;
10454
            } else {
10455
                return $defaultpage;
10456
            }
10457
        } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) {
10458
            return HOMEPAGE_MYCOURSES;
10459
        } else {
10460
            $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage);
10461
            if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) {
10462
                // If the user was using the dashboard but it's disabled, return the default home page.
10463
                $userhomepage = $defaultpage;
10464
            }
10465
            return $userhomepage;
10466
        }
10467
    }
10468
    return HOMEPAGE_SITE;
10469
}
10470
 
10471
/**
10472
 * Returns the default home page to display if current one is not defined or can't be applied.
10473
 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
10474
 *
10475
 * @return int The default home page.
10476
 */
1326 ariadna 10477
function get_default_home_page(): int
10478
{
1 efrain 10479
    global $CFG;
10480
 
10481
    return (!isset($CFG->enabledashboard) || $CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES;
10482
}
10483
 
10484
/**
10485
 * Gets the name of a course to be displayed when showing a list of courses.
10486
 * By default this is just $course->fullname but user can configure it. The
10487
 * result of this function should be passed through print_string.
10488
 * @param stdClass|core_course_list_element $course Moodle course object
10489
 * @return string Display name of course (either fullname or short + fullname)
10490
 */
1326 ariadna 10491
function get_course_display_name_for_list($course)
10492
{
1 efrain 10493
    global $CFG;
10494
    if (!empty($CFG->courselistshortnames)) {
10495
        if (!($course instanceof stdClass)) {
10496
            $course = (object)convert_to_array($course);
10497
        }
10498
        return get_string('courseextendednamedisplay', '', $course);
10499
    } else {
10500
        return $course->fullname;
10501
    }
10502
}
10503
 
10504
/**
10505
 * Safe analogue of unserialize() that can only parse arrays
10506
 *
10507
 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10508
 *
10509
 * @param string $expression
10510
 * @return array|bool either parsed array or false if parsing was impossible.
10511
 */
1326 ariadna 10512
function unserialize_array($expression)
10513
{
1 efrain 10514
 
10515
    // Check the expression is an array.
10516
    if (!preg_match('/^a:(\d+):/', $expression)) {
10517
        return false;
10518
    }
10519
 
10520
    $values = (array) unserialize_object($expression);
10521
 
10522
    // Callback that returns true if the given value is an unserialized object, executes recursively.
1326 ariadna 10523
    $invalidvaluecallback = static function ($value) use (&$invalidvaluecallback): bool {
1 efrain 10524
        if (is_array($value)) {
10525
            return (bool) array_filter($value, $invalidvaluecallback);
10526
        }
10527
        return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class);
10528
    };
10529
 
10530
    // Iterate over the result to ensure there are no stray objects.
10531
    if (array_filter($values, $invalidvaluecallback)) {
10532
        return false;
10533
    }
10534
 
10535
    return $values;
10536
}
10537
 
10538
/**
10539
 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10540
 *
10541
 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10542
 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10543
 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10544
 *
10545
 * @param string $input
10546
 * @return stdClass
10547
 */
1326 ariadna 10548
function unserialize_object(string $input): stdClass
10549
{
1 efrain 10550
    $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10551
    return (object) $instance;
10552
}
10553
 
10554
/**
10555
 * The lang_string class
10556
 *
10557
 * This special class is used to create an object representation of a string request.
10558
 * It is special because processing doesn't occur until the object is first used.
10559
 * The class was created especially to aid performance in areas where strings were
10560
 * required to be generated but were not necessarily used.
10561
 * As an example the admin tree when generated uses over 1500 strings, of which
10562
 * normally only 1/3 are ever actually printed at any time.
10563
 * The performance advantage is achieved by not actually processing strings that
10564
 * arn't being used, as such reducing the processing required for the page.
10565
 *
10566
 * How to use the lang_string class?
10567
 *     There are two methods of using the lang_string class, first through the
10568
 *     forth argument of the get_string function, and secondly directly.
10569
 *     The following are examples of both.
10570
 * 1. Through get_string calls e.g.
10571
 *     $string = get_string($identifier, $component, $a, true);
10572
 *     $string = get_string('yes', 'moodle', null, true);
10573
 * 2. Direct instantiation
10574
 *     $string = new lang_string($identifier, $component, $a, $lang);
10575
 *     $string = new lang_string('yes');
10576
 *
10577
 * How do I use a lang_string object?
10578
 *     The lang_string object makes use of a magic __toString method so that you
10579
 *     are able to use the object exactly as you would use a string in most cases.
10580
 *     This means you are able to collect it into a variable and then directly
10581
 *     echo it, or concatenate it into another string, or similar.
10582
 *     The other thing you can do is manually get the string by calling the
10583
 *     lang_strings out method e.g.
10584
 *         $string = new lang_string('yes');
10585
 *         $string->out();
10586
 *     Also worth noting is that the out method can take one argument, $lang which
10587
 *     allows the developer to change the language on the fly.
10588
 *
10589
 * When should I use a lang_string object?
10590
 *     The lang_string object is designed to be used in any situation where a
10591
 *     string may not be needed, but needs to be generated.
10592
 *     The admin tree is a good example of where lang_string objects should be
10593
 *     used.
10594
 *     A more practical example would be any class that requries strings that may
10595
 *     not be printed (after all classes get renderer by renderers and who knows
10596
 *     what they will do ;))
10597
 *
10598
 * When should I not use a lang_string object?
10599
 *     Don't use lang_strings when you are going to use a string immediately.
10600
 *     There is no need as it will be processed immediately and there will be no
10601
 *     advantage, and in fact perhaps a negative hit as a class has to be
10602
 *     instantiated for a lang_string object, however get_string won't require
10603
 *     that.
10604
 *
10605
 * Limitations:
10606
 * 1. You cannot use a lang_string object as an array offset. Doing so will
10607
 *     result in PHP throwing an error. (You can use it as an object property!)
10608
 *
10609
 * @package    core
10610
 * @category   string
10611
 * @copyright  2011 Sam Hemelryk
10612
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10613
 */
1326 ariadna 10614
class lang_string
10615
{
1 efrain 10616
 
10617
    /** @var string The strings identifier */
10618
    protected $identifier;
10619
    /** @var string The strings component. Default '' */
10620
    protected $component = '';
10621
    /** @var array|stdClass Any arguments required for the string. Default null */
10622
    protected $a = null;
10623
    /** @var string The language to use when processing the string. Default null */
10624
    protected $lang = null;
10625
 
10626
    /** @var string The processed string (once processed) */
10627
    protected $string = null;
10628
 
10629
    /**
10630
     * A special boolean. If set to true then the object has been woken up and
10631
     * cannot be regenerated. If this is set then $this->string MUST be used.
10632
     * @var bool
10633
     */
10634
    protected $forcedstring = false;
10635
 
10636
    /**
10637
     * Constructs a lang_string object
10638
     *
10639
     * This function should do as little processing as possible to ensure the best
10640
     * performance for strings that won't be used.
10641
     *
10642
     * @param string $identifier The strings identifier
10643
     * @param string $component The strings component
10644
     * @param stdClass|array|mixed $a Any arguments the string requires
10645
     * @param string $lang The language to use when processing the string.
10646
     * @throws coding_exception
10647
     */
1326 ariadna 10648
    public function __construct($identifier, $component = '', $a = null, $lang = null)
10649
    {
1 efrain 10650
        if (empty($component)) {
10651
            $component = 'moodle';
10652
        }
10653
 
10654
        $this->identifier = $identifier;
10655
        $this->component = $component;
10656
        $this->lang = $lang;
10657
 
10658
        // We MUST duplicate $a to ensure that it if it changes by reference those
10659
        // changes are not carried across.
10660
        // To do this we always ensure $a or its properties/values are strings
10661
        // and that any properties/values that arn't convertable are forgotten.
10662
        if ($a !== null) {
10663
            if (is_scalar($a)) {
10664
                $this->a = $a;
10665
            } else if ($a instanceof lang_string) {
10666
                $this->a = $a->out();
10667
            } else if (is_object($a) or is_array($a)) {
10668
                $a = (array)$a;
10669
                $this->a = array();
10670
                foreach ($a as $key => $value) {
10671
                    // Make sure conversion errors don't get displayed (results in '').
10672
                    if (is_array($value)) {
10673
                        $this->a[$key] = '';
10674
                    } else if (is_object($value)) {
10675
                        if (method_exists($value, '__toString')) {
10676
                            $this->a[$key] = $value->__toString();
10677
                        } else {
10678
                            $this->a[$key] = '';
10679
                        }
10680
                    } else {
10681
                        $this->a[$key] = (string)$value;
10682
                    }
10683
                }
10684
            }
10685
        }
10686
 
10687
        if (debugging(false, DEBUG_DEVELOPER)) {
10688
            if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10689
                throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10690
            }
10691
            if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10692
                throw new coding_exception('Invalid string compontent. Please check your string definition');
10693
            }
10694
            if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
1326 ariadna 10695
                debugging('String does not exist. Please check your string definition for ' . $this->identifier . '/' . $this->component, DEBUG_DEVELOPER);
1 efrain 10696
            }
10697
        }
10698
    }
10699
 
10700
    /**
10701
     * Processes the string.
10702
     *
10703
     * This function actually processes the string, stores it in the string property
10704
     * and then returns it.
10705
     * You will notice that this function is VERY similar to the get_string method.
10706
     * That is because it is pretty much doing the same thing.
10707
     * However as this function is an upgrade it isn't as tolerant to backwards
10708
     * compatibility.
10709
     *
10710
     * @return string
10711
     * @throws coding_exception
10712
     */
1326 ariadna 10713
    protected function get_string()
10714
    {
1 efrain 10715
        global $CFG;
10716
 
10717
        // Check if we need to process the string.
10718
        if ($this->string === null) {
10719
            // Check the quality of the identifier.
10720
            if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10721
                throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition', DEBUG_DEVELOPER);
10722
            }
10723
 
10724
            // Process the string.
10725
            $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10726
            // Debugging feature lets you display string identifier and component.
10727
            if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10728
                $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10729
            }
10730
        }
10731
        // Return the string.
10732
        return $this->string;
10733
    }
10734
 
10735
    /**
10736
     * Returns the string
10737
     *
10738
     * @param string $lang The langauge to use when processing the string
10739
     * @return string
10740
     */
1326 ariadna 10741
    public function out($lang = null)
10742
    {
1 efrain 10743
        if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10744
            if ($this->forcedstring) {
1326 ariadna 10745
                debugging('lang_string objects that have been used cannot be printed in another language. (' . $this->lang . ' used)', DEBUG_DEVELOPER);
1 efrain 10746
                return $this->get_string();
10747
            }
10748
            $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10749
            return $translatedstring->out();
10750
        }
10751
        return $this->get_string();
10752
    }
10753
 
10754
    /**
10755
     * Magic __toString method for printing a string
10756
     *
10757
     * @return string
10758
     */
1326 ariadna 10759
    public function __toString()
10760
    {
1 efrain 10761
        return $this->get_string();
10762
    }
10763
 
10764
    /**
10765
     * Magic __set_state method used for var_export
10766
     *
10767
     * @param array $array
10768
     * @return self
10769
     */
1326 ariadna 10770
    public static function __set_state(array $array): self
10771
    {
1 efrain 10772
        $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']);
10773
        $tmp->string = $array['string'];
10774
        $tmp->forcedstring = $array['forcedstring'];
10775
        return $tmp;
10776
    }
10777
 
10778
    /**
10779
     * Prepares the lang_string for sleep and stores only the forcedstring and
10780
     * string properties... the string cannot be regenerated so we need to ensure
10781
     * it is generated for this.
10782
     *
10783
     * @return array
10784
     */
1326 ariadna 10785
    public function __sleep()
10786
    {
1 efrain 10787
        $this->get_string();
10788
        $this->forcedstring = true;
10789
        return array('forcedstring', 'string', 'lang');
10790
    }
10791
 
10792
    /**
10793
     * Returns the identifier.
10794
     *
10795
     * @return string
10796
     */
1326 ariadna 10797
    public function get_identifier()
10798
    {
1 efrain 10799
        return $this->identifier;
10800
    }
10801
 
10802
    /**
10803
     * Returns the component.
10804
     *
10805
     * @return string
10806
     */
1326 ariadna 10807
    public function get_component()
10808
    {
1 efrain 10809
        return $this->component;
10810
    }
10811
}
10812
 
10813
/**
10814
 * Get human readable name describing the given callable.
10815
 *
10816
 * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10817
 * It does not check if the callable actually exists.
10818
 *
10819
 * @param callable|string|array $callable
10820
 * @return string|bool Human readable name of callable, or false if not a valid callable.
10821
 */
1326 ariadna 10822
function get_callable_name($callable)
10823
{
1 efrain 10824
 
10825
    if (!is_callable($callable, true, $name)) {
10826
        return false;
10827
    } else {
10828
        return $name;
10829
    }
10830
}
10831
 
10832
/**
10833
 * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10834
 * Never put your faith on this function and rely on its accuracy as there might be false positives.
10835
 * It just performs some simple checks, and mainly is used for places where we want to hide some options
10836
 * such as site registration when $CFG->wwwroot is not publicly accessible.
10837
 * Good thing is there is no false negative.
10838
 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10839
 *
10840
 * @return bool
10841
 */
1326 ariadna 10842
function site_is_public()
10843
{
1 efrain 10844
    global $CFG;
10845
 
10846
    // Return early if site admin has forced this setting.
10847
    if (isset($CFG->site_is_public)) {
10848
        return (bool)$CFG->site_is_public;
10849
    }
10850
 
10851
    $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10852
 
10853
    if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10854
        $ispublic = false;
10855
    } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10856
        $ispublic = false;
10857
    } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10858
        $ispublic = false;
10859
    } else {
10860
        $ispublic = true;
10861
    }
10862
 
10863
    return $ispublic;
10864
}
10865
 
10866
/**
10867
 * Validates user's password length.
10868
 *
10869
 * @param string $password
10870
 * @param int $pepperlength The length of the used peppers
10871
 * @return bool
10872
 */
1326 ariadna 10873
function exceeds_password_length(string $password, int $pepperlength = 0): bool
10874
{
1 efrain 10875
    return (strlen($password) > (MAX_PASSWORD_CHARACTERS + $pepperlength));
10876
}
10877
 
10878
/**
10879
 * A helper to replace PHP 8.3 usage of array_keys with two args.
10880
 *
10881
 * There is an indication that this will become a new method in PHP 8.4, but that has not happened yet.
10882
 * Therefore this non-polyfill has been created with a different naming convention.
10883
 * In the future it can be deprecated if a core PHP method is created.
10884
 *
10885
 * https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#array_keys
10886
 *
10887
 * @param array $array
10888
 * @param mixed $filter The value to filter on
10889
 * @param bool $strict Whether to apply a strit test with the filter
10890
 * @return array
10891
 */
1326 ariadna 10892
function moodle_array_keys_filter(array $array, mixed $filter, bool $strict = false): array
10893
{
1 efrain 10894
    return array_keys(array_filter(
10895
        $array,
1326 ariadna 10896
        function ($value, $key) use ($filter, $strict): bool {
1 efrain 10897
            if ($strict) {
10898
                return $value === $filter;
10899
            }
10900
            return $value == $filter;
10901
        },
10902
        ARRAY_FILTER_USE_BOTH,
10903
    ));
10904
}