Proyectos de Subversion Moodle

Rev

Rev 1328 | Rev 1330 | 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.
1329 ariadna 5985
        $header_image_url = (new moodle_url('/theme/image.php/universe_child/theme/0/logo-horizontal-cesa'))->out(false);
5986
        $footer_image_url = (new moodle_url('/theme/image.php/universe_child/theme/0/email-footer'))->out(false);
1326 ariadna 5987
 
5988
        // HTML del Header
5989
        $header_html = '<div style="text-align:center; margin-bottom:20px;">'
5990
            . '<img src="' . $header_image_url . '" alt="Header Image" style="max-width:100%; height:auto;">'
5991
            . '</div>';
5992
 
5993
        // HTML del Footer
5994
        $footer_html = '<div style="text-align:center; margin-top:40px;">'
5995
            . '<img src="' . $footer_image_url . '" alt="Footer Image" style="max-width:100%; height:auto;">'
5996
            . '</div>';
5997
 
5998
        // Unir: Header + Contenido original + Footer
5999
        $messagehtml = $header_html . $messagehtml . $footer_html;
1327 ariadna 6000
 
6001
        $mail->isHTML(true);
6002
        $mail->Encoding = 'quoted-printable';
6003
        $mail->Body    =  $messagehtml;
6004
        $mail->AltBody =  "\n$messagetext\n";
1 efrain 6005
    } else {
6006
        $mail->IsHTML(false);
6007
        $mail->Body =  "\n$messagetext\n";
6008
    }
6009
 
6010
    if ($attachment && $attachname) {
1326 ariadna 6011
        if (preg_match("~\\.\\.~", $attachment)) {
1 efrain 6012
            // Security check for ".." in dir path.
6013
            $supportuser = core_user::get_support_user();
6014
            $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
6015
            $mail->addStringAttachment('Error in attachment.  User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
6016
        } else {
1326 ariadna 6017
            require_once($CFG->libdir . '/filelib.php');
1 efrain 6018
            $mimetype = mimeinfo('type', $attachname);
6019
 
6020
            // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
6021
            // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
6022
            $attachpath = str_replace('\\', '/', realpath($attachment));
6023
 
6024
            // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
1326 ariadna 6025
            $allowedpaths = array_map(function (string $path): string {
1 efrain 6026
                return str_replace('\\', '/', realpath($path));
6027
            }, [
6028
                $CFG->cachedir,
6029
                $CFG->dataroot,
6030
                $CFG->dirroot,
6031
                $CFG->localcachedir,
6032
                $CFG->tempdir,
6033
                $CFG->localrequestdir,
6034
            ]);
6035
 
6036
            // Set addpath to true.
6037
            $addpath = true;
6038
 
6039
            // Check if attachment includes one of the allowed paths.
6040
            foreach (array_filter($allowedpaths) as $allowedpath) {
6041
                // Set addpath to false if the attachment includes one of the allowed paths.
6042
                if (strpos($attachpath, $allowedpath) === 0) {
6043
                    $addpath = false;
6044
                    break;
6045
                }
6046
            }
6047
 
6048
            // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
6049
            // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
6050
            if ($addpath == true) {
6051
                $attachment = $CFG->dataroot . '/' . $attachment;
6052
            }
6053
 
6054
            $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
6055
        }
6056
    }
6057
 
6058
    // Check if the email should be sent in an other charset then the default UTF-8.
6059
    if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
6060
 
6061
        // Use the defined site mail charset or eventually the one preferred by the recipient.
6062
        $charset = $CFG->sitemailcharset;
6063
        if (!empty($CFG->allowusermailcharset)) {
6064
            if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
6065
                $charset = $useremailcharset;
6066
            }
6067
        }
6068
 
6069
        // Convert all the necessary strings if the charset is supported.
6070
        $charsets = get_list_of_charsets();
6071
        unset($charsets['UTF-8']);
6072
        if (in_array($charset, $charsets)) {
6073
            $mail->CharSet  = $charset;
6074
            $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
6075
            $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
6076
            $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
6077
            $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
6078
 
6079
            foreach ($temprecipients as $key => $values) {
6080
                $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6081
            }
6082
            foreach ($tempreplyto as $key => $values) {
6083
                $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6084
            }
6085
        }
6086
    }
6087
 
6088
    foreach ($temprecipients as $values) {
6089
        $mail->addAddress($values[0], $values[1]);
6090
    }
6091
    foreach ($tempreplyto as $values) {
6092
        $mail->addReplyTo($values[0], $values[1]);
6093
    }
6094
 
6095
    if (!empty($CFG->emaildkimselector)) {
6096
        $domain = substr(strrchr($mail->From, "@"), 1);
6097
        $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
6098
        if (file_exists($pempath)) {
6099
            $mail->DKIM_domain      = $domain;
6100
            $mail->DKIM_private     = $pempath;
6101
            $mail->DKIM_selector    = $CFG->emaildkimselector;
6102
            $mail->DKIM_identity    = $mail->From;
6103
        } else {
6104
            debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
6105
        }
6106
    }
6107
 
6108
    if ($mail->send()) {
6109
        set_send_count($user);
6110
        if (!empty($mail->SMTPDebug)) {
6111
            echo '</pre>';
6112
        }
6113
        return true;
6114
    } else {
6115
        // Trigger event for failing to send email.
6116
        $event = \core\event\email_failed::create(array(
6117
            'context' => context_system::instance(),
6118
            'userid' => $from->id,
6119
            'relateduserid' => $user->id,
6120
            'other' => array(
6121
                'subject' => $subject,
6122
                'message' => $messagetext,
6123
                'errorinfo' => $mail->ErrorInfo
6124
            )
6125
        ));
6126
        $event->trigger();
6127
        if (CLI_SCRIPT) {
1326 ariadna 6128
            mtrace('Error: lib/moodlelib.php email_to_user(): ' . $mail->ErrorInfo);
1 efrain 6129
        }
6130
        if (!empty($mail->SMTPDebug)) {
6131
            echo '</pre>';
6132
        }
6133
        return false;
6134
    }
6135
}
6136
 
6137
/**
6138
 * Check to see if a user's real email address should be used for the "From" field.
6139
 *
6140
 * @param  object $from The user object for the user we are sending the email from.
6141
 * @param  object $user The user object that we are sending the email to.
6142
 * @param  array $unused No longer used.
6143
 * @return bool Returns true if we can use the from user's email adress in the "From" field.
6144
 */
1326 ariadna 6145
function can_send_from_real_email_address($from, $user, $unused = null)
6146
{
1 efrain 6147
    global $CFG;
6148
    if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
6149
        return false;
6150
    }
6151
    $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
6152
    // Email is in the list of allowed domains for sending email,
6153
    // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6154
    // in a course with the sender.
1326 ariadna 6155
    if (
6156
        \core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
6157
        && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
6158
            || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
6159
                && enrol_get_shared_courses($user, $from, false, true)))
6160
    ) {
1 efrain 6161
        return true;
6162
    }
6163
    return false;
6164
}
6165
 
6166
/**
6167
 * Generate a signoff for emails based on support settings
6168
 *
6169
 * @return string
6170
 */
1326 ariadna 6171
function generate_email_signoff()
6172
{
1 efrain 6173
    global $CFG, $OUTPUT;
6174
 
6175
    $signoff = "\n";
6176
    if (!empty($CFG->supportname)) {
1326 ariadna 6177
        $signoff .= $CFG->supportname . "\n";
1 efrain 6178
    }
6179
 
6180
    $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']);
6181
 
6182
    if ($supportemail) {
6183
        $signoff .= "\n" . $supportemail . "\n";
6184
    }
6185
 
6186
    return $signoff;
6187
}
6188
 
6189
/**
6190
 * Sets specified user's password and send the new password to the user via email.
6191
 *
6192
 * @param stdClass $user A {@link $USER} object
6193
 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6194
 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6195
 */
1326 ariadna 6196
function setnew_password_and_mail($user, $fasthash = false)
6197
{
1 efrain 6198
    global $CFG, $DB;
6199
 
6200
    // We try to send the mail in language the user understands,
6201
    // unfortunately the filter_string() does not support alternative langs yet
6202
    // so multilang will not work properly for site->fullname.
6203
    $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6204
 
6205
    $site  = get_site();
6206
 
6207
    $supportuser = core_user::get_support_user();
6208
 
6209
    $newpassword = generate_password();
6210
 
6211
    update_internal_user_password($user, $newpassword, $fasthash);
6212
 
6213
    $a = new stdClass();
6214
    $a->firstname   = fullname($user, true);
6215
    $a->sitename    = format_string($site->fullname);
6216
    $a->username    = $user->username;
6217
    $a->newpassword = $newpassword;
1326 ariadna 6218
    $a->link        = $CFG->wwwroot . '/login/?lang=' . $lang;
1 efrain 6219
    $a->signoff     = generate_email_signoff();
6220
 
6221
    $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6222
 
1326 ariadna 6223
    $subject = format_string($site->fullname) . ': ' . (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
1 efrain 6224
 
6225
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6226
    return email_to_user($user, $supportuser, $subject, $message);
6227
}
6228
 
6229
/**
6230
 * Resets specified user's password and send the new password to the user via email.
6231
 *
6232
 * @param stdClass $user A {@link $USER} object
6233
 * @return bool Returns true if mail was sent OK and false if there was an error.
6234
 */
1326 ariadna 6235
function reset_password_and_mail($user)
6236
{
1 efrain 6237
    global $CFG;
6238
 
6239
    $site  = get_site();
6240
    $supportuser = core_user::get_support_user();
6241
 
6242
    $userauth = get_auth_plugin($user->auth);
6243
    if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6244
        trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6245
        return false;
6246
    }
6247
 
6248
    $newpassword = generate_password();
6249
 
6250
    if (!$userauth->user_update_password($user, $newpassword)) {
6251
        throw new \moodle_exception("cannotsetpassword");
6252
    }
6253
 
6254
    $a = new stdClass();
6255
    $a->firstname   = $user->firstname;
6256
    $a->lastname    = $user->lastname;
6257
    $a->sitename    = format_string($site->fullname);
6258
    $a->username    = $user->username;
6259
    $a->newpassword = $newpassword;
1326 ariadna 6260
    $a->link        = $CFG->wwwroot . '/login/change_password.php';
1 efrain 6261
    $a->signoff     = generate_email_signoff();
6262
 
6263
    $message = get_string('newpasswordtext', '', $a);
6264
 
1326 ariadna 6265
    $subject  = format_string($site->fullname) . ': ' . get_string('changedpassword');
1 efrain 6266
 
6267
    unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6268
 
6269
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6270
    return email_to_user($user, $supportuser, $subject, $message);
6271
}
6272
 
6273
/**
6274
 * Send email to specified user with confirmation text and activation link.
6275
 *
6276
 * @param stdClass $user A {@link $USER} object
6277
 * @param string $confirmationurl user confirmation URL
6278
 * @return bool Returns true if mail was sent OK and false if there was an error.
6279
 */
1326 ariadna 6280
function send_confirmation_email($user, $confirmationurl = null)
6281
{
1 efrain 6282
    global $CFG;
6283
 
6284
    $site = get_site();
6285
    $supportuser = core_user::get_support_user();
6286
 
6287
    $data = new stdClass();
6288
    $data->sitename  = format_string($site->fullname);
6289
    $data->admin     = generate_email_signoff();
6290
 
6291
    $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6292
 
6293
    if (empty($confirmationurl)) {
6294
        $confirmationurl = '/login/confirm.php';
6295
    }
6296
 
6297
    $confirmationurl = new moodle_url($confirmationurl);
6298
    // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6299
    $confirmationurl->remove_params('data');
6300
    $confirmationpath = $confirmationurl->out(false);
6301
 
6302
    // We need to custom encode the username to include trailing dots in the link.
6303
    // Because of this custom encoding we can't use moodle_url directly.
6304
    // Determine if a query string is present in the confirmation url.
6305
    $hasquerystring = strpos($confirmationpath, '?') !== false;
6306
    // Perform normal url encoding of the username first.
6307
    $username = urlencode($user->username);
6308
    // Prevent problems with trailing dots not being included as part of link in some mail clients.
6309
    $username = str_replace('.', '%2E', $username);
6310
 
1326 ariadna 6311
    $data->link = $confirmationpath . ($hasquerystring ? '&' : '?') . 'data=' . $user->secret . '/' . $username;
1 efrain 6312
 
6313
    $message     = get_string('emailconfirmation', '', $data);
6314
    $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6315
 
6316
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6317
    return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6318
}
6319
 
6320
/**
6321
 * Sends a password change confirmation email.
6322
 *
6323
 * @param stdClass $user A {@link $USER} object
6324
 * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6325
 * @return bool Returns true if mail was sent OK and false if there was an error.
6326
 */
1326 ariadna 6327
function send_password_change_confirmation_email($user, $resetrecord)
6328
{
1 efrain 6329
    global $CFG;
6330
 
6331
    $site = get_site();
6332
    $supportuser = core_user::get_support_user();
6333
    $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6334
 
6335
    $data = new stdClass();
6336
    $data->firstname = $user->firstname;
6337
    $data->lastname  = $user->lastname;
6338
    $data->username  = $user->username;
6339
    $data->sitename  = format_string($site->fullname);
1326 ariadna 6340
    $data->link      = $CFG->wwwroot . '/login/forgot_password.php?token=' . $resetrecord->token;
1 efrain 6341
    $data->admin     = generate_email_signoff();
6342
    $data->resetminutes = $pwresetmins;
6343
 
6344
    $message = get_string('emailresetconfirmation', '', $data);
6345
    $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6346
 
6347
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6348
    return email_to_user($user, $supportuser, $subject, $message);
6349
}
6350
 
6351
/**
6352
 * Sends an email containing information on how to change your password.
6353
 *
6354
 * @param stdClass $user A {@link $USER} object
6355
 * @return bool Returns true if mail was sent OK and false if there was an error.
6356
 */
1326 ariadna 6357
function send_password_change_info($user)
6358
{
1 efrain 6359
    $site = get_site();
6360
    $supportuser = core_user::get_support_user();
6361
 
6362
    $data = new stdClass();
6363
    $data->firstname = $user->firstname;
6364
    $data->lastname  = $user->lastname;
6365
    $data->username  = $user->username;
6366
    $data->sitename  = format_string($site->fullname);
6367
    $data->admin     = generate_email_signoff();
6368
 
6369
    if (!is_enabled_auth($user->auth)) {
6370
        $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6371
        $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6372
        // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6373
        return email_to_user($user, $supportuser, $subject, $message);
6374
    }
6375
 
6376
    $userauth = get_auth_plugin($user->auth);
6377
    ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6378
 
6379
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6380
    return email_to_user($user, $supportuser, $subject, $message);
6381
}
6382
 
6383
/**
6384
 * Check that an email is allowed.  It returns an error message if there was a problem.
6385
 *
6386
 * @param string $email Content of email
6387
 * @return string|false
6388
 */
1326 ariadna 6389
function email_is_not_allowed($email)
6390
{
1 efrain 6391
    global $CFG;
6392
 
6393
    // Comparing lowercase domains.
6394
    $email = strtolower($email);
6395
    if (!empty($CFG->allowemailaddresses)) {
6396
        $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6397
        foreach ($allowed as $allowedpattern) {
6398
            $allowedpattern = trim($allowedpattern);
6399
            if (!$allowedpattern) {
6400
                continue;
6401
            }
6402
            if (strpos($allowedpattern, '.') === 0) {
6403
                if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6404
                    // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6405
                    return false;
6406
                }
1326 ariadna 6407
            } else if (strpos(strrev($email), strrev('@' . $allowedpattern)) === 0) {
1 efrain 6408
                return false;
6409
            }
6410
        }
6411
        return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6412
    } else if (!empty($CFG->denyemailaddresses)) {
6413
        $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6414
        foreach ($denied as $deniedpattern) {
6415
            $deniedpattern = trim($deniedpattern);
6416
            if (!$deniedpattern) {
6417
                continue;
6418
            }
6419
            if (strpos($deniedpattern, '.') === 0) {
6420
                if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6421
                    // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6422
                    return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6423
                }
1326 ariadna 6424
            } else if (strpos(strrev($email), strrev('@' . $deniedpattern)) === 0) {
1 efrain 6425
                return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6426
            }
6427
        }
6428
    }
6429
 
6430
    return false;
6431
}
6432
 
6433
// FILE HANDLING.
6434
 
6435
/**
6436
 * Returns local file storage instance
6437
 *
6438
 * @return ?file_storage
6439
 */
1326 ariadna 6440
function get_file_storage($reset = false)
6441
{
1 efrain 6442
    global $CFG;
6443
 
6444
    static $fs = null;
6445
 
6446
    if ($reset) {
6447
        $fs = null;
6448
        return;
6449
    }
6450
 
6451
    if ($fs) {
6452
        return $fs;
6453
    }
6454
 
6455
    require_once("$CFG->libdir/filelib.php");
6456
 
6457
    $fs = new file_storage();
6458
 
6459
    return $fs;
6460
}
6461
 
6462
/**
6463
 * Returns local file storage instance
6464
 *
6465
 * @return file_browser
6466
 */
1326 ariadna 6467
function get_file_browser()
6468
{
1 efrain 6469
    global $CFG;
6470
 
6471
    static $fb = null;
6472
 
6473
    if ($fb) {
6474
        return $fb;
6475
    }
6476
 
6477
    require_once("$CFG->libdir/filelib.php");
6478
 
6479
    $fb = new file_browser();
6480
 
6481
    return $fb;
6482
}
6483
 
6484
/**
6485
 * Returns file packer
6486
 *
6487
 * @param string $mimetype default application/zip
6488
 * @return file_packer|false
6489
 */
1326 ariadna 6490
function get_file_packer($mimetype = 'application/zip')
6491
{
1 efrain 6492
    global $CFG;
6493
 
6494
    static $fp = array();
6495
 
6496
    if (isset($fp[$mimetype])) {
6497
        return $fp[$mimetype];
6498
    }
6499
 
6500
    switch ($mimetype) {
6501
        case 'application/zip':
6502
        case 'application/vnd.moodle.profiling':
6503
            $classname = 'zip_packer';
6504
            break;
6505
 
1326 ariadna 6506
        case 'application/x-gzip':
1 efrain 6507
            $classname = 'tgz_packer';
6508
            break;
6509
 
6510
        case 'application/vnd.moodle.backup':
6511
            $classname = 'mbz_packer';
6512
            break;
6513
 
6514
        default:
6515
            return false;
6516
    }
6517
 
6518
    require_once("$CFG->libdir/filestorage/$classname.php");
6519
    $fp[$mimetype] = new $classname();
6520
 
6521
    return $fp[$mimetype];
6522
}
6523
 
6524
/**
6525
 * Returns current name of file on disk if it exists.
6526
 *
6527
 * @param string $newfile File to be verified
6528
 * @return string Current name of file on disk if true
6529
 */
1326 ariadna 6530
function valid_uploaded_file($newfile)
6531
{
1 efrain 6532
    if (empty($newfile)) {
6533
        return '';
6534
    }
6535
    if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6536
        return $newfile['tmp_name'];
6537
    } else {
6538
        return '';
6539
    }
6540
}
6541
 
6542
/**
6543
 * Returns the maximum size for uploading files.
6544
 *
6545
 * There are seven possible upload limits:
6546
 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6547
 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6548
 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6549
 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6550
 * 5. by the Moodle admin in $CFG->maxbytes
6551
 * 6. by the teacher in the current course $course->maxbytes
6552
 * 7. by the teacher for the current module, eg $assignment->maxbytes
6553
 *
6554
 * These last two are passed to this function as arguments (in bytes).
6555
 * Anything defined as 0 is ignored.
6556
 * The smallest of all the non-zero numbers is returned.
6557
 *
6558
 * @todo Finish documenting this function
6559
 *
6560
 * @param int $sitebytes Set maximum size
6561
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6562
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6563
 * @param bool $unused This parameter has been deprecated and is not used any more.
6564
 * @return int The maximum size for uploading files.
6565
 */
1326 ariadna 6566
function get_max_upload_file_size($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $unused = false)
6567
{
1 efrain 6568
 
6569
    if (! $filesize = ini_get('upload_max_filesize')) {
6570
        $filesize = '5M';
6571
    }
6572
    $minimumsize = get_real_size($filesize);
6573
 
6574
    if ($postsize = ini_get('post_max_size')) {
6575
        $postsize = get_real_size($postsize);
6576
        if ($postsize < $minimumsize) {
6577
            $minimumsize = $postsize;
6578
        }
6579
    }
6580
 
6581
    if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6582
        $minimumsize = $sitebytes;
6583
    }
6584
 
6585
    if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6586
        $minimumsize = $coursebytes;
6587
    }
6588
 
6589
    if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6590
        $minimumsize = $modulebytes;
6591
    }
6592
 
6593
    return $minimumsize;
6594
}
6595
 
6596
/**
6597
 * Returns the maximum size for uploading files for the current user
6598
 *
6599
 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6600
 *
6601
 * @param context $context The context in which to check user capabilities
6602
 * @param int $sitebytes Set maximum size
6603
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6604
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6605
 * @param stdClass|int|null $user The user
6606
 * @param bool $unused This parameter has been deprecated and is not used any more.
6607
 * @return int The maximum size for uploading files.
6608
 */
1326 ariadna 6609
function get_user_max_upload_file_size(
6610
    $context,
6611
    $sitebytes = 0,
6612
    $coursebytes = 0,
6613
    $modulebytes = 0,
6614
    $user = null,
6615
    $unused = false
6616
) {
1 efrain 6617
    global $USER;
6618
 
6619
    if (empty($user)) {
6620
        $user = $USER;
6621
    }
6622
 
6623
    if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6624
        return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6625
    }
6626
 
6627
    return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6628
}
6629
 
6630
/**
6631
 * Returns an array of possible sizes in local language
6632
 *
6633
 * Related to {@link get_max_upload_file_size()} - this function returns an
6634
 * array of possible sizes in an array, translated to the
6635
 * local language.
6636
 *
6637
 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6638
 *
6639
 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6640
 * with the value set to 0. This option will be the first in the list.
6641
 *
6642
 * @uses SORT_NUMERIC
6643
 * @param int $sitebytes Set maximum size
6644
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6645
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6646
 * @param int|array $custombytes custom upload size/s which will be added to list,
6647
 *        Only value/s smaller then maxsize will be added to list.
6648
 * @return array
6649
 */
1326 ariadna 6650
function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null)
6651
{
1 efrain 6652
    global $CFG;
6653
 
6654
    if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6655
        return array();
6656
    }
6657
 
6658
    if ($sitebytes == 0) {
6659
        // Will get the minimum of upload_max_filesize or post_max_size.
6660
        $sitebytes = get_max_upload_file_size();
6661
    }
6662
 
6663
    $filesize = array();
1326 ariadna 6664
    $sizelist = array(
6665
        10240,
6666
        51200,
6667
        102400,
6668
        512000,
6669
        1048576,
6670
        2097152,
6671
        5242880,
6672
        10485760,
6673
        20971520,
6674
        52428800,
6675
        104857600,
6676
        262144000,
6677
        524288000,
6678
        786432000,
6679
        1073741824,
6680
        2147483648,
6681
        4294967296,
6682
        8589934592
6683
    );
1 efrain 6684
 
6685
    // If custombytes is given and is valid then add it to the list.
6686
    if (is_number($custombytes) and $custombytes > 0) {
6687
        $custombytes = (int)$custombytes;
6688
        if (!in_array($custombytes, $sizelist)) {
6689
            $sizelist[] = $custombytes;
6690
        }
6691
    } else if (is_array($custombytes)) {
6692
        $sizelist = array_unique(array_merge($sizelist, $custombytes));
6693
    }
6694
 
6695
    // Allow maxbytes to be selected if it falls outside the above boundaries.
6696
    if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6697
        // Note: get_real_size() is used in order to prevent problems with invalid values.
6698
        $sizelist[] = get_real_size($CFG->maxbytes);
6699
    }
6700
 
6701
    foreach ($sizelist as $sizebytes) {
6702
        if ($sizebytes < $maxsize && $sizebytes > 0) {
6703
            $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6704
        }
6705
    }
6706
 
6707
    $limitlevel = '';
6708
    $displaysize = '';
1326 ariadna 6709
    if (
6710
        $modulebytes &&
1 efrain 6711
        (($modulebytes < $coursebytes || $coursebytes == 0) &&
1326 ariadna 6712
            ($modulebytes < $sitebytes || $sitebytes == 0))
6713
    ) {
1 efrain 6714
        $limitlevel = get_string('activity', 'core');
6715
        $displaysize = display_size($modulebytes, 0);
6716
        $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6717
 
6718
    } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6719
        $limitlevel = get_string('course', 'core');
6720
        $displaysize = display_size($coursebytes, 0);
6721
        $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6722
 
6723
    } else if ($sitebytes) {
6724
        $limitlevel = get_string('site', 'core');
6725
        $displaysize = display_size($sitebytes, 0);
6726
        $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6727
    }
6728
 
6729
    krsort($filesize, SORT_NUMERIC);
6730
    if ($limitlevel) {
6731
        $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6732
        $filesize  = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6733
    }
6734
 
6735
    return $filesize;
6736
}
6737
 
6738
/**
6739
 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6740
 *
6741
 * If excludefiles is defined, then that file/directory is ignored
6742
 * If getdirs is true, then (sub)directories are included in the output
6743
 * If getfiles is true, then files are included in the output
6744
 * (at least one of these must be true!)
6745
 *
6746
 * @todo Finish documenting this function. Add examples of $excludefile usage.
6747
 *
6748
 * @param string $rootdir A given root directory to start from
6749
 * @param string|array $excludefiles If defined then the specified file/directory is ignored
6750
 * @param bool $descend If true then subdirectories are recursed as well
6751
 * @param bool $getdirs If true then (sub)directories are included in the output
6752
 * @param bool $getfiles  If true then files are included in the output
6753
 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6754
 */
1326 ariadna 6755
function get_directory_list($rootdir, $excludefiles = '', $descend = true, $getdirs = false, $getfiles = true)
6756
{
1 efrain 6757
 
6758
    $dirs = array();
6759
 
6760
    if (!$getdirs and !$getfiles) {   // Nothing to show.
6761
        return $dirs;
6762
    }
6763
 
6764
    if (!is_dir($rootdir)) {          // Must be a directory.
6765
        return $dirs;
6766
    }
6767
 
6768
    if (!$dir = opendir($rootdir)) {  // Can't open it for some reason.
6769
        return $dirs;
6770
    }
6771
 
6772
    if (!is_array($excludefiles)) {
6773
        $excludefiles = array($excludefiles);
6774
    }
6775
 
6776
    while (false !== ($file = readdir($dir))) {
6777
        $firstchar = substr($file, 0, 1);
6778
        if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6779
            continue;
6780
        }
1326 ariadna 6781
        $fullfile = $rootdir . '/' . $file;
1 efrain 6782
        if (filetype($fullfile) == 'dir') {
6783
            if ($getdirs) {
6784
                $dirs[] = $file;
6785
            }
6786
            if ($descend) {
6787
                $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6788
                foreach ($subdirs as $subdir) {
1326 ariadna 6789
                    $dirs[] = $file . '/' . $subdir;
1 efrain 6790
                }
6791
            }
6792
        } else if ($getfiles) {
6793
            $dirs[] = $file;
6794
        }
6795
    }
6796
    closedir($dir);
6797
 
6798
    asort($dirs);
6799
 
6800
    return $dirs;
6801
}
6802
 
6803
 
6804
/**
6805
 * Adds up all the files in a directory and works out the size.
6806
 *
6807
 * @param string $rootdir  The directory to start from
6808
 * @param string $excludefile A file to exclude when summing directory size
6809
 * @return int The summed size of all files and subfiles within the root directory
6810
 */
1326 ariadna 6811
function get_directory_size($rootdir, $excludefile = '')
6812
{
1 efrain 6813
    global $CFG;
6814
 
6815
    // Do it this way if we can, it's much faster.
6816
    if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
1326 ariadna 6817
        $command = trim($CFG->pathtodu) . ' -sk ' . escapeshellarg($rootdir);
1 efrain 6818
        $output = null;
6819
        $return = null;
6820
        exec($command, $output, $return);
6821
        if (is_array($output)) {
6822
            // We told it to return k.
1326 ariadna 6823
            return get_real_size(intval($output[0]) . 'k');
1 efrain 6824
        }
6825
    }
6826
 
6827
    if (!is_dir($rootdir)) {
6828
        // Must be a directory.
6829
        return 0;
6830
    }
6831
 
6832
    if (!$dir = @opendir($rootdir)) {
6833
        // Can't open it for some reason.
6834
        return 0;
6835
    }
6836
 
6837
    $size = 0;
6838
 
6839
    while (false !== ($file = readdir($dir))) {
6840
        $firstchar = substr($file, 0, 1);
6841
        if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6842
            continue;
6843
        }
1326 ariadna 6844
        $fullfile = $rootdir . '/' . $file;
1 efrain 6845
        if (filetype($fullfile) == 'dir') {
6846
            $size += get_directory_size($fullfile, $excludefile);
6847
        } else {
6848
            $size += filesize($fullfile);
6849
        }
6850
    }
6851
    closedir($dir);
6852
 
6853
    return $size;
6854
}
6855
 
6856
/**
6857
 * Converts bytes into display form
6858
 *
6859
 * @param int $size  The size to convert to human readable form
6860
 * @param int $decimalplaces If specified, uses fixed number of decimal places
6861
 * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
6862
 * @return string Display version of size
6863
 */
1326 ariadna 6864
function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string
6865
{
1 efrain 6866
 
6867
    static $units;
6868
 
6869
    if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6870
        return get_string('unlimited');
6871
    }
6872
 
6873
    if (empty($units)) {
6874
        $units[] = get_string('sizeb');
6875
        $units[] = get_string('sizekb');
6876
        $units[] = get_string('sizemb');
6877
        $units[] = get_string('sizegb');
6878
        $units[] = get_string('sizetb');
6879
        $units[] = get_string('sizepb');
6880
    }
6881
 
6882
    switch ($fixedunits) {
1326 ariadna 6883
        case 'PB':
1 efrain 6884
            $magnitude = 5;
6885
            break;
1326 ariadna 6886
        case 'TB':
1 efrain 6887
            $magnitude = 4;
6888
            break;
1326 ariadna 6889
        case 'GB':
1 efrain 6890
            $magnitude = 3;
6891
            break;
1326 ariadna 6892
        case 'MB':
1 efrain 6893
            $magnitude = 2;
6894
            break;
1326 ariadna 6895
        case 'KB':
1 efrain 6896
            $magnitude = 1;
6897
            break;
1326 ariadna 6898
        case 'B':
1 efrain 6899
            $magnitude = 0;
6900
            break;
6901
        case '':
6902
            $magnitude = floor(log($size, 1024));
6903
            $magnitude = max(0, min(5, $magnitude));
6904
            break;
6905
        default:
6906
            throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
6907
    }
6908
 
6909
    // Special case for magnitude 0 (bytes) - never use decimal places.
6910
    $nbsp = "\xc2\xa0";
6911
    if ($magnitude === 0) {
6912
        return round($size) . $nbsp . $units[$magnitude];
6913
    }
6914
 
6915
    // Convert to specified units.
6916
    $sizeinunit = $size / 1024 ** $magnitude;
6917
 
6918
    // Fixed decimal places.
6919
    return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
6920
}
6921
 
6922
/**
6923
 * Cleans a given filename by removing suspicious or troublesome characters
6924
 *
6925
 * @see clean_param()
6926
 * @param string $string file name
6927
 * @return string cleaned file name
6928
 */
1326 ariadna 6929
function clean_filename($string)
6930
{
1 efrain 6931
    return clean_param($string, PARAM_FILE);
6932
}
6933
 
6934
// STRING TRANSLATION.
6935
 
6936
/**
6937
 * Returns the code for the current language
6938
 *
6939
 * @category string
6940
 * @return string
6941
 */
1326 ariadna 6942
function current_language()
6943
{
1 efrain 6944
    global $CFG, $PAGE, $SESSION, $USER;
6945
 
6946
    if (!empty($SESSION->forcelang)) {
6947
        // Allows overriding course-forced language (useful for admins to check
6948
        // issues in courses whose language they don't understand).
6949
        // Also used by some code to temporarily get language-related information in a
6950
        // specific language (see force_current_language()).
6951
        $return = $SESSION->forcelang;
6952
    } else if (!empty($PAGE->cm->lang)) {
6953
        // Activity language, if set.
6954
        $return = $PAGE->cm->lang;
6955
    } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) {
6956
        // Course language can override all other settings for this page.
6957
        $return = $PAGE->course->lang;
6958
    } else if (!empty($SESSION->lang)) {
6959
        // Session language can override other settings.
6960
        $return = $SESSION->lang;
6961
    } else if (!empty($USER->lang)) {
6962
        $return = $USER->lang;
6963
    } else if (isset($CFG->lang)) {
6964
        $return = $CFG->lang;
6965
    } else {
6966
        $return = 'en';
6967
    }
6968
 
6969
    // Just in case this slipped in from somewhere by accident.
6970
    $return = str_replace('_utf8', '', $return);
6971
 
6972
    return $return;
6973
}
6974
 
6975
/**
6976
 * Fix the current language to the given language code.
6977
 *
6978
 * @param string $lang The language code to use.
6979
 * @return void
6980
 */
1326 ariadna 6981
function fix_current_language(string $lang): void
6982
{
1 efrain 6983
    global $CFG, $COURSE, $SESSION, $USER;
6984
 
6985
    if (!get_string_manager()->translation_exists($lang)) {
6986
        throw new coding_exception("The language pack for $lang is not available");
6987
    }
6988
 
6989
    $fixglobal = '';
6990
    $fixlang = 'lang';
6991
    if (!empty($SESSION->forcelang)) {
6992
        $fixglobal = $SESSION;
6993
        $fixlang = 'forcelang';
6994
    } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) {
6995
        $fixglobal = $COURSE;
6996
    } else if (!empty($SESSION->lang)) {
6997
        $fixglobal = $SESSION;
6998
    } else if (!empty($USER->lang)) {
6999
        $fixglobal = $USER;
7000
    } else if (isset($CFG->lang)) {
7001
        set_config('lang', $lang);
7002
    }
7003
 
7004
    if ($fixglobal) {
7005
        $fixglobal->$fixlang = $lang;
7006
    }
7007
}
7008
 
7009
/**
7010
 * Returns parent language of current active language if defined
7011
 *
7012
 * @category string
7013
 * @param string $lang null means current language
7014
 * @return string
7015
 */
1326 ariadna 7016
function get_parent_language($lang = null)
7017
{
1 efrain 7018
 
7019
    $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
7020
 
7021
    if ($parentlang === 'en') {
7022
        $parentlang = '';
7023
    }
7024
 
7025
    return $parentlang;
7026
}
7027
 
7028
/**
7029
 * Force the current language to get strings and dates localised in the given language.
7030
 *
7031
 * After calling this function, all strings will be provided in the given language
7032
 * until this function is called again, or equivalent code is run.
7033
 *
7034
 * @param string $language
7035
 * @return string previous $SESSION->forcelang value
7036
 */
1326 ariadna 7037
function force_current_language($language)
7038
{
1 efrain 7039
    global $SESSION;
7040
    $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
7041
    if ($language !== $sessionforcelang) {
7042
        // Setting forcelang to null or an empty string disables its effect.
7043
        if (empty($language) || get_string_manager()->translation_exists($language, false)) {
7044
            $SESSION->forcelang = $language;
7045
            moodle_setlocale();
7046
        }
7047
    }
7048
    return $sessionforcelang;
7049
}
7050
 
7051
/**
7052
 * Returns current string_manager instance.
7053
 *
7054
 * The param $forcereload is needed for CLI installer only where the string_manager instance
7055
 * must be replaced during the install.php script life time.
7056
 *
7057
 * @category string
7058
 * @param bool $forcereload shall the singleton be released and new instance created instead?
7059
 * @return core_string_manager
7060
 */
1326 ariadna 7061
function get_string_manager($forcereload = false)
7062
{
1 efrain 7063
    global $CFG;
7064
 
7065
    static $singleton = null;
7066
 
7067
    if ($forcereload) {
7068
        $singleton = null;
7069
    }
7070
    if ($singleton === null) {
7071
        if (empty($CFG->early_install_lang)) {
7072
 
7073
            $transaliases = array();
7074
            if (empty($CFG->langlist)) {
1326 ariadna 7075
                $translist = array();
1 efrain 7076
            } else {
7077
                $translist = explode(',', $CFG->langlist);
7078
                $translist = array_map('trim', $translist);
7079
                // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
7080
                foreach ($translist as $i => $value) {
7081
                    $parts = preg_split('/\s*\|\s*/', $value, 2);
7082
                    if (count($parts) == 2) {
7083
                        $transaliases[$parts[0]] = $parts[1];
7084
                        $translist[$i] = $parts[0];
7085
                    }
7086
                }
7087
            }
7088
 
7089
            if (!empty($CFG->config_php_settings['customstringmanager'])) {
7090
                $classname = $CFG->config_php_settings['customstringmanager'];
7091
 
7092
                if (class_exists($classname)) {
7093
                    $implements = class_implements($classname);
7094
 
7095
                    if (isset($implements['core_string_manager'])) {
7096
                        $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7097
                        return $singleton;
7098
                    } else {
1326 ariadna 7099
                        debugging('Unable to instantiate custom string manager: class ' . $classname .
1 efrain 7100
                            ' does not implement the core_string_manager interface.');
7101
                    }
7102
                } else {
1326 ariadna 7103
                    debugging('Unable to instantiate custom string manager: class ' . $classname . ' can not be found.');
1 efrain 7104
                }
7105
            }
7106
 
7107
            $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7108
        } else {
7109
            $singleton = new core_string_manager_install();
7110
        }
7111
    }
7112
 
7113
    return $singleton;
7114
}
7115
 
7116
/**
7117
 * Returns a localized string.
7118
 *
7119
 * Returns the translated string specified by $identifier as
7120
 * for $module.  Uses the same format files as STphp.
7121
 * $a is an object, string or number that can be used
7122
 * within translation strings
7123
 *
7124
 * eg 'hello {$a->firstname} {$a->lastname}'
7125
 * or 'hello {$a}'
7126
 *
7127
 * If you would like to directly echo the localized string use
7128
 * the function {@link print_string()}
7129
 *
7130
 * Example usage of this function involves finding the string you would
7131
 * like a local equivalent of and using its identifier and module information
7132
 * to retrieve it.<br/>
7133
 * If you open moodle/lang/en/moodle.php and look near line 278
7134
 * you will find a string to prompt a user for their word for 'course'
7135
 * <code>
7136
 * $string['course'] = 'Course';
7137
 * </code>
7138
 * So if you want to display the string 'Course'
7139
 * in any language that supports it on your site
7140
 * you just need to use the identifier 'course'
7141
 * <code>
7142
 * $mystring = '<strong>'. get_string('course') .'</strong>';
7143
 * or
7144
 * </code>
7145
 * If the string you want is in another file you'd take a slightly
7146
 * different approach. Looking in moodle/lang/en/calendar.php you find
7147
 * around line 75:
7148
 * <code>
7149
 * $string['typecourse'] = 'Course event';
7150
 * </code>
7151
 * If you want to display the string "Course event" in any language
7152
 * supported you would use the identifier 'typecourse' and the module 'calendar'
7153
 * (because it is in the file calendar.php):
7154
 * <code>
7155
 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7156
 * </code>
7157
 *
7158
 * As a last resort, should the identifier fail to map to a string
7159
 * the returned string will be [[ $identifier ]]
7160
 *
7161
 * In Moodle 2.3 there is a new argument to this function $lazyload.
7162
 * Setting $lazyload to true causes get_string to return a lang_string object
7163
 * rather than the string itself. The fetching of the string is then put off until
7164
 * the string object is first used. The object can be used by calling it's out
7165
 * method or by casting the object to a string, either directly e.g.
7166
 *     (string)$stringobject
7167
 * or indirectly by using the string within another string or echoing it out e.g.
7168
 *     echo $stringobject
7169
 *     return "<p>{$stringobject}</p>";
7170
 * It is worth noting that using $lazyload and attempting to use the string as an
7171
 * array key will cause a fatal error as objects cannot be used as array keys.
7172
 * But you should never do that anyway!
7173
 * For more information {@link lang_string}
7174
 *
7175
 * @category string
7176
 * @param string $identifier The key identifier for the localized string
7177
 * @param string $component The module where the key identifier is stored,
7178
 *      usually expressed as the filename in the language pack without the
7179
 *      .php on the end but can also be written as mod/forum or grade/export/xls.
7180
 *      If none is specified then moodle.php is used.
7181
 * @param string|object|array|int $a An object, string or number that can be used
7182
 *      within translation strings
7183
 * @param bool $lazyload If set to true a string object is returned instead of
7184
 *      the string itself. The string then isn't calculated until it is first used.
7185
 * @return string The localized string.
7186
 * @throws coding_exception
7187
 */
1326 ariadna 7188
function get_string($identifier, $component = '', $a = null, $lazyload = false)
7189
{
1 efrain 7190
    global $CFG;
7191
 
7192
    // If the lazy load argument has been supplied return a lang_string object
7193
    // instead.
7194
    // We need to make sure it is true (and a bool) as you will see below there
7195
    // used to be a forth argument at one point.
7196
    if ($lazyload === true) {
7197
        return new lang_string($identifier, $component, $a);
7198
    }
7199
 
7200
    if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
7201
        throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
7202
    }
7203
 
7204
    // There is now a forth argument again, this time it is a boolean however so
7205
    // we can still check for the old extralocations parameter.
7206
    if (!is_bool($lazyload) && !empty($lazyload)) {
7207
        debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7208
    }
7209
 
7210
    if (strpos((string)$component, '/') !== false) {
7211
        debugging('The module name you passed to get_string is the deprecated format ' .
1326 ariadna 7212
            'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.', DEBUG_DEVELOPER);
1 efrain 7213
        $componentpath = explode('/', $component);
7214
 
7215
        switch ($componentpath[0]) {
7216
            case 'mod':
7217
                $component = $componentpath[1];
7218
                break;
7219
            case 'blocks':
7220
            case 'block':
1326 ariadna 7221
                $component = 'block_' . $componentpath[1];
1 efrain 7222
                break;
7223
            case 'enrol':
1326 ariadna 7224
                $component = 'enrol_' . $componentpath[1];
1 efrain 7225
                break;
7226
            case 'format':
1326 ariadna 7227
                $component = 'format_' . $componentpath[1];
1 efrain 7228
                break;
7229
            case 'grade':
1326 ariadna 7230
                $component = 'grade' . $componentpath[1] . '_' . $componentpath[2];
1 efrain 7231
                break;
7232
        }
7233
    }
7234
 
7235
    $result = get_string_manager()->get_string($identifier, $component, $a);
7236
 
7237
    // Debugging feature lets you display string identifier and component.
7238
    if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7239
        $result .= ' {' . $identifier . '/' . $component . '}';
7240
    }
7241
    return $result;
7242
}
7243
 
7244
/**
7245
 * Converts an array of strings to their localized value.
7246
 *
7247
 * @param array $array An array of strings
7248
 * @param string $component The language module that these strings can be found in.
7249
 * @return stdClass translated strings.
7250
 */
1326 ariadna 7251
function get_strings($array, $component = '')
7252
{
1 efrain 7253
    $string = new stdClass;
7254
    foreach ($array as $item) {
7255
        $string->$item = get_string($item, $component);
7256
    }
7257
    return $string;
7258
}
7259
 
7260
/**
7261
 * Prints out a translated string.
7262
 *
7263
 * Prints out a translated string using the return value from the {@link get_string()} function.
7264
 *
7265
 * Example usage of this function when the string is in the moodle.php file:<br/>
7266
 * <code>
7267
 * echo '<strong>';
7268
 * print_string('course');
7269
 * echo '</strong>';
7270
 * </code>
7271
 *
7272
 * Example usage of this function when the string is not in the moodle.php file:<br/>
7273
 * <code>
7274
 * echo '<h1>';
7275
 * print_string('typecourse', 'calendar');
7276
 * echo '</h1>';
7277
 * </code>
7278
 *
7279
 * @category string
7280
 * @param string $identifier The key identifier for the localized string
7281
 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7282
 * @param string|object|array $a An object, string or number that can be used within translation strings
7283
 */
1326 ariadna 7284
function print_string($identifier, $component = '', $a = null)
7285
{
1 efrain 7286
    echo get_string($identifier, $component, $a);
7287
}
7288
 
7289
/**
7290
 * Returns a list of charset codes
7291
 *
7292
 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7293
 * (checking that such charset is supported by the texlib library!)
7294
 *
7295
 * @return array And associative array with contents in the form of charset => charset
7296
 */
1326 ariadna 7297
function get_list_of_charsets()
7298
{
1 efrain 7299
 
7300
    $charsets = array(
7301
        'EUC-JP'     => 'EUC-JP',
1326 ariadna 7302
        'ISO-2022-JP' => 'ISO-2022-JP',
1 efrain 7303
        'ISO-8859-1' => 'ISO-8859-1',
7304
        'SHIFT-JIS'  => 'SHIFT-JIS',
7305
        'GB2312'     => 'GB2312',
7306
        'GB18030'    => 'GB18030', // GB18030 not supported by typo and mbstring.
1326 ariadna 7307
        'UTF-8'      => 'UTF-8'
7308
    );
1 efrain 7309
 
7310
    asort($charsets);
7311
 
7312
    return $charsets;
7313
}
7314
 
7315
/**
7316
 * Returns a list of valid and compatible themes
7317
 *
7318
 * @return array
7319
 */
1326 ariadna 7320
function get_list_of_themes()
7321
{
1 efrain 7322
    global $CFG;
7323
 
7324
    $themes = array();
7325
 
7326
    if (!empty($CFG->themelist)) {       // Use admin's list of themes.
7327
        $themelist = explode(',', $CFG->themelist);
7328
    } else {
7329
        $themelist = array_keys(core_component::get_plugin_list("theme"));
7330
    }
7331
 
7332
    foreach ($themelist as $key => $themename) {
7333
        $theme = theme_config::load($themename);
7334
        $themes[$themename] = $theme;
7335
    }
7336
 
7337
    core_collator::asort_objects_by_method($themes, 'get_theme_name');
7338
 
7339
    return $themes;
7340
}
7341
 
7342
/**
7343
 * Factory function for emoticon_manager
7344
 *
7345
 * @return emoticon_manager singleton
7346
 */
1326 ariadna 7347
function get_emoticon_manager()
7348
{
1 efrain 7349
    static $singleton = null;
7350
 
7351
    if (is_null($singleton)) {
7352
        $singleton = new emoticon_manager();
7353
    }
7354
 
7355
    return $singleton;
7356
}
7357
 
7358
/**
7359
 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7360
 *
7361
 * Whenever this manager mentiones 'emoticon object', the following data
7362
 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7363
 * altidentifier and altcomponent
7364
 *
7365
 * @see admin_setting_emoticons
7366
 *
7367
 * @copyright 2010 David Mudrak
7368
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7369
 */
1326 ariadna 7370
class emoticon_manager
7371
{
1 efrain 7372
 
7373
    /**
7374
     * Returns the currently enabled emoticons
7375
     *
7376
     * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7377
     * @return array of emoticon objects
7378
     */
1326 ariadna 7379
    public function get_emoticons($selectable = false)
7380
    {
1 efrain 7381
        global $CFG;
7382
        $notselectable = ['martin', 'egg'];
7383
 
7384
        if (empty($CFG->emoticons)) {
7385
            return array();
7386
        }
7387
 
7388
        $emoticons = $this->decode_stored_config($CFG->emoticons);
7389
 
7390
        if (!is_array($emoticons)) {
7391
            // Something is wrong with the format of stored setting.
7392
            debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7393
            return array();
7394
        }
7395
        if ($selectable) {
7396
            foreach ($emoticons as $index => $emote) {
7397
                if (in_array($emote->altidentifier, $notselectable)) {
7398
                    // Skip this one.
7399
                    unset($emoticons[$index]);
7400
                }
7401
            }
7402
        }
7403
 
7404
        return $emoticons;
7405
    }
7406
 
7407
    /**
7408
     * Converts emoticon object into renderable pix_emoticon object
7409
     *
7410
     * @param stdClass $emoticon emoticon object
7411
     * @param array $attributes explicit HTML attributes to set
7412
     * @return pix_emoticon
7413
     */
1326 ariadna 7414
    public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array())
7415
    {
1 efrain 7416
        $stringmanager = get_string_manager();
7417
        if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7418
            $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7419
        } else {
7420
            $alt = s($emoticon->text);
7421
        }
7422
        return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7423
    }
7424
 
7425
    /**
7426
     * Encodes the array of emoticon objects into a string storable in config table
7427
     *
7428
     * @see self::decode_stored_config()
7429
     * @param array $emoticons array of emtocion objects
7430
     * @return string
7431
     */
1326 ariadna 7432
    public function encode_stored_config(array $emoticons)
7433
    {
1 efrain 7434
        return json_encode($emoticons);
7435
    }
7436
 
7437
    /**
7438
     * Decodes the string into an array of emoticon objects
7439
     *
7440
     * @see self::encode_stored_config()
7441
     * @param string $encoded
7442
     * @return array|null
7443
     */
1326 ariadna 7444
    public function decode_stored_config($encoded)
7445
    {
1 efrain 7446
        $decoded = json_decode($encoded);
7447
        if (!is_array($decoded)) {
7448
            return null;
7449
        }
7450
        return $decoded;
7451
    }
7452
 
7453
    /**
7454
     * Returns default set of emoticons supported by Moodle
7455
     *
7456
     * @return array of sdtClasses
7457
     */
1326 ariadna 7458
    public function default_emoticons()
7459
    {
1 efrain 7460
        return array(
7461
            $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7462
            $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7463
            $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7464
            $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7465
            $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7466
            $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7467
            $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7468
            $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7469
            $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7470
            $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7471
            $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7472
            $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7473
            $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7474
            $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7475
            $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7476
            $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7477
            $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7478
            $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7479
            $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7480
            $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7481
            $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7482
            $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7483
            $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7484
            $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7485
            $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7486
            $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7487
            $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7488
            $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7489
            $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7490
            $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7491
        );
7492
    }
7493
 
7494
    /**
7495
     * Helper method preparing the stdClass with the emoticon properties
7496
     *
7497
     * @param string|array $text or array of strings
7498
     * @param string $imagename to be used by {@link pix_emoticon}
7499
     * @param string $altidentifier alternative string identifier, null for no alt
7500
     * @param string $altcomponent where the alternative string is defined
7501
     * @param string $imagecomponent to be used by {@link pix_emoticon}
7502
     * @return stdClass
7503
     */
1326 ariadna 7504
    protected function prepare_emoticon_object(
7505
        $text,
7506
        $imagename,
7507
        $altidentifier = null,
7508
        $altcomponent = 'core_pix',
7509
        $imagecomponent = 'core'
7510
    ) {
1 efrain 7511
        return (object)array(
7512
            'text'           => $text,
7513
            'imagename'      => $imagename,
7514
            'imagecomponent' => $imagecomponent,
7515
            'altidentifier'  => $altidentifier,
7516
            'altcomponent'   => $altcomponent,
7517
        );
7518
    }
7519
}
7520
 
7521
// ENCRYPTION.
7522
 
7523
/**
7524
 * rc4encrypt
7525
 *
7526
 * @param string $data        Data to encrypt.
7527
 * @return string             The now encrypted data.
7528
 */
1326 ariadna 7529
function rc4encrypt($data)
7530
{
1 efrain 7531
    return endecrypt(get_site_identifier(), $data, '');
7532
}
7533
 
7534
/**
7535
 * rc4decrypt
7536
 *
7537
 * @param string $data        Data to decrypt.
7538
 * @return string             The now decrypted data.
7539
 */
1326 ariadna 7540
function rc4decrypt($data)
7541
{
1 efrain 7542
    return endecrypt(get_site_identifier(), $data, 'de');
7543
}
7544
 
7545
/**
7546
 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7547
 *
7548
 * @todo Finish documenting this function
7549
 *
7550
 * @param string $pwd The password to use when encrypting or decrypting
7551
 * @param string $data The data to be decrypted/encrypted
7552
 * @param string $case Either 'de' for decrypt or '' for encrypt
7553
 * @return string
7554
 */
1326 ariadna 7555
function endecrypt($pwd, $data, $case)
7556
{
1 efrain 7557
 
7558
    if ($case == 'de') {
7559
        $data = urldecode($data);
7560
    }
7561
 
7562
    $key[] = '';
7563
    $box[] = '';
7564
    $pwdlength = strlen($pwd);
7565
 
7566
    for ($i = 0; $i <= 255; $i++) {
7567
        $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7568
        $box[$i] = $i;
7569
    }
7570
 
7571
    $x = 0;
7572
 
7573
    for ($i = 0; $i <= 255; $i++) {
7574
        $x = ($x + $box[$i] + $key[$i]) % 256;
7575
        $tempswap = $box[$i];
7576
        $box[$i] = $box[$x];
7577
        $box[$x] = $tempswap;
7578
    }
7579
 
7580
    $cipher = '';
7581
 
7582
    $a = 0;
7583
    $j = 0;
7584
 
7585
    for ($i = 0; $i < strlen($data); $i++) {
7586
        $a = ($a + 1) % 256;
7587
        $j = ($j + $box[$a]) % 256;
7588
        $temp = $box[$a];
7589
        $box[$a] = $box[$j];
7590
        $box[$j] = $temp;
7591
        $k = $box[(($box[$a] + $box[$j]) % 256)];
7592
        $cipherby = ord(substr($data, $i, 1)) ^ $k;
7593
        $cipher .= chr($cipherby);
7594
    }
7595
 
7596
    if ($case == 'de') {
7597
        $cipher = urldecode(urlencode($cipher));
7598
    } else {
7599
        $cipher = urlencode($cipher);
7600
    }
7601
 
7602
    return $cipher;
7603
}
7604
 
7605
// ENVIRONMENT CHECKING.
7606
 
7607
/**
7608
 * This method validates a plug name. It is much faster than calling clean_param.
7609
 *
7610
 * @param string $name a string that might be a plugin name.
7611
 * @return bool if this string is a valid plugin name.
7612
 */
1326 ariadna 7613
function is_valid_plugin_name($name)
7614
{
1 efrain 7615
    // This does not work for 'mod', bad luck, use any other type.
7616
    return core_component::is_valid_plugin_name('tool', $name);
7617
}
7618
 
7619
/**
7620
 * Get a list of all the plugins of a given type that define a certain API function
7621
 * in a certain file. The plugin component names and function names are returned.
7622
 *
7623
 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7624
 * @param string $function the part of the name of the function after the
7625
 *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7626
 *      names like report_courselist_hook.
7627
 * @param string $file the name of file within the plugin that defines the
7628
 *      function. Defaults to lib.php.
7629
 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7630
 *      and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7631
 */
1326 ariadna 7632
function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php')
7633
{
1 efrain 7634
    global $CFG;
7635
 
7636
    // We don't include here as all plugin types files would be included.
7637
    $plugins = get_plugins_with_function($function, $file, false);
7638
 
7639
    if (empty($plugins[$plugintype])) {
7640
        return array();
7641
    }
7642
 
7643
    $allplugins = core_component::get_plugin_list($plugintype);
7644
 
7645
    // Reformat the array and include the files.
7646
    $pluginfunctions = array();
7647
    foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7648
 
7649
        // Check that it has not been removed and the file is still available.
7650
        if (!empty($allplugins[$pluginname])) {
7651
 
7652
            $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7653
            if (file_exists($filepath)) {
7654
                include_once($filepath);
7655
 
7656
                // Now that the file is loaded, we must verify the function still exists.
7657
                if (function_exists($functionname)) {
7658
                    $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7659
                } else {
7660
                    // Invalidate the cache for next run.
7661
                    \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7662
                }
7663
            }
7664
        }
7665
    }
7666
 
7667
    return $pluginfunctions;
7668
}
7669
 
7670
/**
7671
 * Get a list of all the plugins that define a certain API function in a certain file.
7672
 *
7673
 * @param string $function the part of the name of the function after the
7674
 *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7675
 *      names like report_courselist_hook.
7676
 * @param string $file the name of file within the plugin that defines the
7677
 *      function. Defaults to lib.php.
7678
 * @param bool $include Whether to include the files that contain the functions or not.
7679
 * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
7680
 * @return array with [plugintype][plugin] = functionname
7681
 */
1326 ariadna 7682
function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false)
7683
{
1 efrain 7684
    global $CFG;
7685
 
7686
    if (during_initial_install() || isset($CFG->upgraderunning)) {
7687
        // API functions _must not_ be called during an installation or upgrade.
7688
        return [];
7689
    }
7690
 
7691
    $plugincallback = $function;
1326 ariadna 7692
    $filtermigrated = function ($plugincallback, $pluginfunctions): array {
1 efrain 7693
        foreach ($pluginfunctions as $plugintype => $plugins) {
7694
            foreach ($plugins as $plugin => $unusedfunction) {
7695
                $component = $plugintype . '_' . $plugin;
7696
                if ($hooks = di::get(hook\manager::class)->get_hooks_deprecating_plugin_callback($plugincallback)) {
7697
                    if (di::get(hook\manager::class)->is_deprecating_hook_present($component, $plugincallback)) {
7698
                        // Ignore the old callback, it is there only for older Moodle versions.
7699
                        unset($pluginfunctions[$plugintype][$plugin]);
7700
                    } else {
7701
                        $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
7702
                        debugging(
7703
                            "Callback $plugincallback in $component component should be migrated to new " .
7704
                                "hook callback for $hookmessage",
7705
                            DEBUG_DEVELOPER
7706
                        );
7707
                    }
7708
                }
7709
            }
7710
        }
7711
        return $pluginfunctions;
7712
    };
7713
 
7714
    $cache = \cache::make('core', 'plugin_functions');
7715
 
7716
    // Including both although I doubt that we will find two functions definitions with the same name.
7717
    // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7718
    $pluginfunctions = false;
7719
    if (!empty($CFG->allversionshash)) {
7720
        $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA);
7721
        $pluginfunctions = $cache->get($key);
7722
    }
7723
    $dirty = false;
7724
 
7725
    // Use the plugin manager to check that plugins are currently installed.
7726
    $pluginmanager = \core_plugin_manager::instance();
7727
 
7728
    if ($pluginfunctions !== false) {
7729
 
7730
        // Checking that the files are still available.
7731
        foreach ($pluginfunctions as $plugintype => $plugins) {
7732
 
7733
            $allplugins = \core_component::get_plugin_list($plugintype);
7734
            $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7735
            foreach ($plugins as $plugin => $function) {
7736
                if (!isset($installedplugins[$plugin])) {
7737
                    // Plugin code is still present on disk but it is not installed.
7738
                    $dirty = true;
7739
                    break 2;
7740
                }
7741
 
7742
                // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7743
                if (empty($allplugins[$plugin])) {
7744
                    $dirty = true;
7745
                    break 2;
7746
                }
7747
 
7748
                $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7749
                if ($include && $fileexists) {
7750
                    // Include the files if it was requested.
7751
                    include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7752
                } else if (!$fileexists) {
7753
                    // If the file is not available any more it should not be returned.
7754
                    $dirty = true;
7755
                    break 2;
7756
                }
7757
 
7758
                // Check if the function still exists in the file.
7759
                if ($include && !function_exists($function)) {
7760
                    $dirty = true;
7761
                    break 2;
7762
                }
7763
            }
7764
        }
7765
 
7766
        // If the cache is dirty, we should fall through and let it rebuild.
7767
        if (!$dirty) {
7768
            if ($migratedtohook && $file === 'lib.php') {
7769
                $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7770
            }
7771
            return $pluginfunctions;
7772
        }
7773
    }
7774
 
7775
    $pluginfunctions = array();
7776
 
7777
    // To fill the cached. Also, everything should continue working with cache disabled.
7778
    $plugintypes = \core_component::get_plugin_types();
7779
    foreach ($plugintypes as $plugintype => $unused) {
7780
 
7781
        // We need to include files here.
7782
        $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7783
        $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7784
        foreach ($pluginswithfile as $plugin => $notused) {
7785
 
7786
            if (!isset($installedplugins[$plugin])) {
7787
                continue;
7788
            }
7789
 
7790
            $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7791
 
7792
            $pluginfunction = false;
7793
            if (function_exists($fullfunction)) {
7794
                // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7795
                $pluginfunction = $fullfunction;
7796
            } else if ($plugintype === 'mod') {
7797
                // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7798
                $shortfunction = $plugin . '_' . $function;
7799
                if (function_exists($shortfunction)) {
7800
                    $pluginfunction = $shortfunction;
7801
                }
7802
            }
7803
 
7804
            if ($pluginfunction) {
7805
                if (empty($pluginfunctions[$plugintype])) {
7806
                    $pluginfunctions[$plugintype] = array();
7807
                }
7808
                $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7809
            }
7810
        }
7811
    }
7812
    if (!empty($CFG->allversionshash)) {
7813
        $cache->set($key, $pluginfunctions);
7814
    }
7815
 
7816
    if ($migratedtohook && $file === 'lib.php') {
7817
        $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7818
    }
7819
 
7820
    return $pluginfunctions;
7821
}
7822
 
7823
/**
7824
 * Lists plugin-like directories within specified directory
7825
 *
7826
 * This function was originally used for standard Moodle plugins, please use
7827
 * new core_component::get_plugin_list() now.
7828
 *
7829
 * This function is used for general directory listing and backwards compatility.
7830
 *
7831
 * @param string $directory relative directory from root
7832
 * @param string $exclude dir name to exclude from the list (defaults to none)
7833
 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7834
 * @return array Sorted array of directory names found under the requested parameters
7835
 */
1326 ariadna 7836
function get_list_of_plugins($directory = 'mod', $exclude = '', $basedir = '')
7837
{
1 efrain 7838
    global $CFG;
7839
 
7840
    $plugins = array();
7841
 
7842
    if (empty($basedir)) {
1326 ariadna 7843
        $basedir = $CFG->dirroot . '/' . $directory;
1 efrain 7844
    } else {
1326 ariadna 7845
        $basedir = $basedir . '/' . $directory;
1 efrain 7846
    }
7847
 
7848
    if ($CFG->debugdeveloper and empty($exclude)) {
7849
        // Make sure devs do not use this to list normal plugins,
7850
        // this is intended for general directories that are not plugins!
7851
 
7852
        $subtypes = core_component::get_plugin_types();
7853
        if (in_array($basedir, $subtypes)) {
7854
            debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7855
        }
7856
        unset($subtypes);
7857
    }
7858
 
7859
    $ignorelist = array_flip(array_filter([
7860
        'CVS',
7861
        '_vti_cnf',
7862
        'amd',
7863
        'classes',
7864
        'simpletest',
7865
        'tests',
7866
        'templates',
7867
        'yui',
7868
        $exclude,
7869
    ]));
7870
 
7871
    if (file_exists($basedir) && filetype($basedir) == 'dir') {
7872
        if (!$dirhandle = opendir($basedir)) {
7873
            debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7874
            return array();
7875
        }
7876
        while (false !== ($dir = readdir($dirhandle))) {
7877
            if (strpos($dir, '.') === 0) {
7878
                // Ignore directories starting with .
7879
                // These are treated as hidden directories.
7880
                continue;
7881
            }
7882
            if (array_key_exists($dir, $ignorelist)) {
7883
                // This directory features on the ignore list.
7884
                continue;
7885
            }
1326 ariadna 7886
            if (filetype($basedir . '/' . $dir) != 'dir') {
1 efrain 7887
                continue;
7888
            }
7889
            $plugins[] = $dir;
7890
        }
7891
        closedir($dirhandle);
7892
    }
7893
    if ($plugins) {
7894
        asort($plugins);
7895
    }
7896
    return $plugins;
7897
}
7898
 
7899
/**
7900
 * Invoke plugin's callback functions
7901
 *
7902
 * @param string $type plugin type e.g. 'mod'
7903
 * @param string $name plugin name
7904
 * @param string $feature feature name
7905
 * @param string $action feature's action
7906
 * @param array $params parameters of callback function, should be an array
7907
 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7908
 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7909
 * @return mixed
7910
 *
7911
 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7912
 */
1326 ariadna 7913
function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false)
7914
{
1 efrain 7915
    return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
7916
}
7917
 
7918
/**
7919
 * Invoke component's callback functions
7920
 *
7921
 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7922
 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7923
 * @param array $params parameters of callback function
7924
 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7925
 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7926
 * @return mixed
7927
 */
1326 ariadna 7928
function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false)
7929
{
1 efrain 7930
    $functionname = component_callback_exists($component, $function);
7931
 
7932
    if ($functionname) {
7933
        if ($migratedtohook) {
7934
            $hookmanager = di::get(hook\manager::class);
7935
            if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($function)) {
7936
                if ($hookmanager->is_deprecating_hook_present($component, $function)) {
7937
                    // Do not call the old lib.php callback,
7938
                    // it is there for compatibility with older Moodle versions only.
7939
                    return null;
7940
                } else {
7941
                    $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
7942
                    debugging(
7943
                        "Callback $function in $component component should be migrated to new hook callback for $hookmessage",
1326 ariadna 7944
                        DEBUG_DEVELOPER
7945
                    );
1 efrain 7946
                }
7947
            }
7948
        }
7949
 
7950
        // Function exists, so just return function result.
7951
        $ret = call_user_func_array($functionname, $params);
7952
        if (is_null($ret)) {
7953
            return $default;
7954
        } else {
7955
            return $ret;
7956
        }
7957
    }
7958
    return $default;
7959
}
7960
 
7961
/**
7962
 * Determine if a component callback exists and return the function name to call. Note that this
7963
 * function will include the required library files so that the functioname returned can be
7964
 * called directly.
7965
 *
7966
 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7967
 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7968
 * @return mixed Complete function name to call if the callback exists or false if it doesn't.
7969
 * @throws coding_exception if invalid component specfied
7970
 */
1326 ariadna 7971
function component_callback_exists($component, $function)
7972
{
1 efrain 7973
    global $CFG; // This is needed for the inclusions.
7974
 
7975
    $cleancomponent = clean_param($component, PARAM_COMPONENT);
7976
    if (empty($cleancomponent)) {
7977
        throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7978
    }
7979
    $component = $cleancomponent;
7980
 
7981
    list($type, $name) = core_component::normalize_component($component);
7982
    $component = $type . '_' . $name;
7983
 
1326 ariadna 7984
    $oldfunction = $name . '_' . $function;
7985
    $function = $component . '_' . $function;
1 efrain 7986
 
7987
    $dir = core_component::get_component_directory($component);
7988
    if (empty($dir)) {
7989
        throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7990
    }
7991
 
7992
    // Load library and look for function.
1326 ariadna 7993
    if (file_exists($dir . '/lib.php')) {
7994
        require_once($dir . '/lib.php');
1 efrain 7995
    }
7996
 
7997
    if (!function_exists($function) and function_exists($oldfunction)) {
7998
        if ($type !== 'mod' and $type !== 'core') {
7999
            debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
8000
        }
8001
        $function = $oldfunction;
8002
    }
8003
 
8004
    if (function_exists($function)) {
8005
        return $function;
8006
    }
8007
    return false;
8008
}
8009
 
8010
/**
8011
 * Call the specified callback method on the provided class.
8012
 *
8013
 * If the callback returns null, then the default value is returned instead.
8014
 * If the class does not exist, then the default value is returned.
8015
 *
8016
 * @param   string      $classname The name of the class to call upon.
8017
 * @param   string      $methodname The name of the staticically defined method on the class.
8018
 * @param   array       $params The arguments to pass into the method.
8019
 * @param   mixed       $default The default value.
8020
 * @param   bool        $migratedtohook True if the callback has been migrated to a hook.
8021
 * @return  mixed       The return value.
8022
 */
1326 ariadna 8023
function component_class_callback($classname, $methodname, array $params, $default = null, bool $migratedtohook = false)
8024
{
1 efrain 8025
    if (!class_exists($classname)) {
8026
        return $default;
8027
    }
8028
 
8029
    if (!method_exists($classname, $methodname)) {
8030
        return $default;
8031
    }
8032
 
8033
    $fullfunction = $classname . '::' . $methodname;
8034
 
8035
    if ($migratedtohook) {
8036
        $functionparts = explode('\\', trim($fullfunction, '\\'));
8037
        $component = $functionparts[0];
8038
        $callback = end($functionparts);
8039
        $hookmanager = di::get(hook\manager::class);
8040
        if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($callback)) {
8041
            if ($hookmanager->is_deprecating_hook_present($component, $callback)) {
8042
                // Do not call the old class callback,
8043
                // it is there for compatibility with older Moodle versions only.
8044
                return null;
8045
            } else {
8046
                $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
1326 ariadna 8047
                debugging(
8048
                    "Callback $callback in $component component should be migrated to new hook callback for $hookmessage",
8049
                    DEBUG_DEVELOPER
8050
                );
1 efrain 8051
            }
8052
        }
8053
    }
8054
 
8055
    $result = call_user_func_array($fullfunction, $params);
8056
 
8057
    if (null === $result) {
8058
        return $default;
8059
    } else {
8060
        return $result;
8061
    }
8062
}
8063
 
8064
/**
8065
 * Checks whether a plugin supports a specified feature.
8066
 *
8067
 * @param string $type Plugin type e.g. 'mod'
8068
 * @param string $name Plugin name e.g. 'forum'
8069
 * @param string $feature Feature code (FEATURE_xx constant)
8070
 * @param mixed $default default value if feature support unknown
8071
 * @return mixed Feature result (false if not supported, null if feature is unknown,
8072
 *         otherwise usually true but may have other feature-specific value such as array)
8073
 * @throws coding_exception
8074
 */
1326 ariadna 8075
function plugin_supports($type, $name, $feature, $default = null)
8076
{
1 efrain 8077
    global $CFG;
8078
 
8079
    if ($type === 'mod' and $name === 'NEWMODULE') {
8080
        // Somebody forgot to rename the module template.
8081
        return false;
8082
    }
8083
 
8084
    $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8085
    if (empty($component)) {
8086
        throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8087
    }
8088
 
8089
    $function = null;
8090
 
8091
    if ($type === 'mod') {
8092
        // We need this special case because we support subplugins in modules,
8093
        // otherwise it would end up in infinite loop.
8094
        if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8095
            include_once("$CFG->dirroot/mod/$name/lib.php");
1326 ariadna 8096
            $function = $component . '_supports';
1 efrain 8097
            if (!function_exists($function)) {
8098
                // Legacy non-frankenstyle function name.
1326 ariadna 8099
                $function = $name . '_supports';
1 efrain 8100
            }
8101
        }
8102
    } else {
8103
        if (!$path = core_component::get_plugin_directory($type, $name)) {
8104
            // Non existent plugin type.
8105
            return false;
8106
        }
8107
        if (file_exists("$path/lib.php")) {
8108
            include_once("$path/lib.php");
1326 ariadna 8109
            $function = $component . '_supports';
1 efrain 8110
        }
8111
    }
8112
 
8113
    if ($function and function_exists($function)) {
8114
        $supports = $function($feature);
8115
        if (is_null($supports)) {
8116
            // Plugin does not know - use default.
8117
            return $default;
8118
        } else {
8119
            return $supports;
8120
        }
8121
    }
8122
 
8123
    // Plugin does not care, so use default.
8124
    return $default;
8125
}
8126
 
8127
/**
8128
 * Returns true if the current version of PHP is greater that the specified one.
8129
 *
8130
 * @todo Check PHP version being required here is it too low?
8131
 *
8132
 * @param string $version The version of php being tested.
8133
 * @return bool
8134
 */
1326 ariadna 8135
function check_php_version($version = '5.2.4')
8136
{
1 efrain 8137
    return (version_compare(phpversion(), $version) >= 0);
8138
}
8139
 
8140
/**
8141
 * Determine if moodle installation requires update.
8142
 *
8143
 * Checks version numbers of main code and all plugins to see
8144
 * if there are any mismatches.
8145
 *
8146
 * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
8147
 * @return bool
8148
 */
1326 ariadna 8149
function moodle_needs_upgrading($checkupgradeflag = true)
8150
{
1 efrain 8151
    global $CFG, $DB;
8152
 
8153
    // Say no if there is already an upgrade running.
8154
    if ($checkupgradeflag) {
8155
        $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']);
8156
        $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING);
8157
        // If we ARE locked, but this PHP process is NOT the process running the upgrade,
8158
        // We should always return false.
8159
        // This means the upgrade is running from CLI somewhere, or about to.
8160
        if (!empty($lock) && !$currentprocessrunningupgrade) {
8161
            return false;
8162
        }
8163
    }
8164
 
8165
    if (empty($CFG->version)) {
8166
        return true;
8167
    }
8168
 
8169
    // There is no need to purge plugininfo caches here because
8170
    // these caches are not used during upgrade and they are purged after
8171
    // every upgrade.
8172
 
8173
    if (empty($CFG->allversionshash)) {
8174
        return true;
8175
    }
8176
 
8177
    $hash = core_component::get_all_versions_hash();
8178
 
8179
    return ($hash !== $CFG->allversionshash);
8180
}
8181
 
8182
/**
8183
 * Returns the major version of this site
8184
 *
8185
 * Moodle version numbers consist of three numbers separated by a dot, for
8186
 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
8187
 * called major version. This function extracts the major version from either
8188
 * $CFG->release (default) or eventually from the $release variable defined in
8189
 * the main version.php.
8190
 *
8191
 * @param bool $fromdisk should the version if source code files be used
8192
 * @return string|false the major version like '2.3', false if could not be determined
8193
 */
1326 ariadna 8194
function moodle_major_version($fromdisk = false)
8195
{
1 efrain 8196
    global $CFG;
8197
 
8198
    if ($fromdisk) {
8199
        $release = null;
1326 ariadna 8200
        require($CFG->dirroot . '/version.php');
1 efrain 8201
        if (empty($release)) {
8202
            return false;
8203
        }
8204
    } else {
8205
        if (empty($CFG->release)) {
8206
            return false;
8207
        }
8208
        $release = $CFG->release;
8209
    }
8210
 
8211
    if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
8212
        return $matches[0];
8213
    } else {
8214
        return false;
8215
    }
8216
}
8217
 
8218
// MISCELLANEOUS.
8219
 
8220
/**
8221
 * Gets the system locale
8222
 *
8223
 * @return string Retuns the current locale.
8224
 */
1326 ariadna 8225
function moodle_getlocale()
8226
{
1 efrain 8227
    global $CFG;
8228
 
8229
    // Fetch the correct locale based on ostype.
8230
    if ($CFG->ostype == 'WINDOWS') {
8231
        $stringtofetch = 'localewin';
8232
    } else {
8233
        $stringtofetch = 'locale';
8234
    }
8235
 
8236
    if (!empty($CFG->locale)) { // Override locale for all language packs.
8237
        return $CFG->locale;
8238
    }
8239
 
8240
    return get_string($stringtofetch, 'langconfig');
8241
}
8242
 
8243
/**
8244
 * Sets the system locale
8245
 *
8246
 * @category string
8247
 * @param string $locale Can be used to force a locale
8248
 */
1326 ariadna 8249
function moodle_setlocale($locale = '')
8250
{
1 efrain 8251
    global $CFG;
8252
 
8253
    static $currentlocale = ''; // Last locale caching.
8254
 
8255
    $oldlocale = $currentlocale;
8256
 
8257
    // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8258
    if (!empty($locale)) {
8259
        $currentlocale = $locale;
8260
    } else {
8261
        $currentlocale = moodle_getlocale();
8262
    }
8263
 
8264
    // Do nothing if locale already set up.
8265
    if ($oldlocale == $currentlocale) {
8266
        return;
8267
    }
8268
 
8269
    // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8270
    // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8271
    // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8272
 
8273
    // Get current values.
1326 ariadna 8274
    $monetary = setlocale(LC_MONETARY, 0);
8275
    $numeric = setlocale(LC_NUMERIC, 0);
8276
    $ctype   = setlocale(LC_CTYPE, 0);
1 efrain 8277
    if ($CFG->ostype != 'WINDOWS') {
1326 ariadna 8278
        $messages = setlocale(LC_MESSAGES, 0);
1 efrain 8279
    }
8280
    // Set locale to all.
1326 ariadna 8281
    $result = setlocale(LC_ALL, $currentlocale);
1 efrain 8282
    // If setting of locale fails try the other utf8 or utf-8 variant,
8283
    // some operating systems support both (Debian), others just one (OSX).
8284
    if ($result === false) {
8285
        if (stripos($currentlocale, '.UTF-8') !== false) {
8286
            $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
1326 ariadna 8287
            setlocale(LC_ALL, $newlocale);
1 efrain 8288
        } else if (stripos($currentlocale, '.UTF8') !== false) {
8289
            $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
1326 ariadna 8290
            setlocale(LC_ALL, $newlocale);
1 efrain 8291
        }
8292
    }
8293
    // Set old values.
1326 ariadna 8294
    setlocale(LC_MONETARY, $monetary);
8295
    setlocale(LC_NUMERIC, $numeric);
1 efrain 8296
    if ($CFG->ostype != 'WINDOWS') {
1326 ariadna 8297
        setlocale(LC_MESSAGES, $messages);
1 efrain 8298
    }
8299
    if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8300
        // To workaround a well-known PHP problem with Turkish letter Ii.
1326 ariadna 8301
        setlocale(LC_CTYPE, $ctype);
1 efrain 8302
    }
8303
}
8304
 
8305
/**
8306
 * Count words in a string.
8307
 *
8308
 * Words are defined as things between whitespace.
8309
 *
8310
 * @category string
8311
 * @param string $string The text to be searched for words. May be HTML.
8312
 * @param int|null $format
8313
 * @return int The count of words in the specified string
8314
 */
1326 ariadna 8315
function count_words($string, $format = null)
8316
{
1 efrain 8317
    // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8318
    // Also, br is a special case because it definitely delimits a word, but has no close tag.
8319
    $string = preg_replace('~
8320
            (                                   # Capture the tag we match.
8321
                </                              # Start of close tag.
8322
                (?!                             # Do not match any of these specific close tag names.
8323
                    a> | b> | del> | em> | i> |
8324
                    ins> | s> | small> | span> |
8325
                    strong> | sub> | sup> | u>
8326
                )
8327
                \w+                             # But, apart from those execptions, match any tag name.
8328
                >                               # End of close tag.
8329
            |
8330
                <br> | <br\s*/>                 # Special cases that are not close tags.
8331
            )
8332
            ~x', '$1 ', $string); // Add a space after the close tag.
8333
    if ($format !== null && $format != FORMAT_PLAIN) {
8334
        // Match the usual text cleaning before display.
8335
        // Ideally we should apply multilang filter only here, other filters might add extra text.
8336
        $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8337
    }
8338
    // Now remove HTML tags.
8339
    $string = strip_tags($string);
8340
    // Decode HTML entities.
8341
    $string = html_entity_decode($string, ENT_COMPAT);
8342
 
8343
    // Now, the word count is the number of blocks of characters separated
8344
    // by any sort of space. That seems to be the definition used by all other systems.
8345
    // To be precise about what is considered to separate words:
8346
    // * Anything that Unicode considers a 'Separator'
8347
    // * Anything that Unicode considers a 'Control character'
8348
    // * An em- or en- dash.
8349
    return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8350
}
8351
 
8352
/**
8353
 * Count letters in a string.
8354
 *
8355
 * Letters are defined as chars not in tags and different from whitespace.
8356
 *
8357
 * @category string
8358
 * @param string $string The text to be searched for letters. May be HTML.
8359
 * @param int|null $format
8360
 * @return int The count of letters in the specified text.
8361
 */
1326 ariadna 8362
function count_letters($string, $format = null)
8363
{
1 efrain 8364
    if ($format !== null && $format != FORMAT_PLAIN) {
8365
        // Match the usual text cleaning before display.
8366
        // Ideally we should apply multilang filter only here, other filters might add extra text.
8367
        $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8368
    }
8369
    $string = strip_tags($string); // Tags are out now.
8370
    $string = html_entity_decode($string, ENT_COMPAT);
8371
    $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8372
 
8373
    return core_text::strlen($string);
8374
}
8375
 
8376
/**
8377
 * Generate and return a random string of the specified length.
8378
 *
8379
 * @param int $length The length of the string to be created.
8380
 * @return string
8381
 */
1326 ariadna 8382
function random_string($length = 15)
8383
{
1 efrain 8384
    $randombytes = random_bytes($length);
8385
    $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8386
    $pool .= 'abcdefghijklmnopqrstuvwxyz';
8387
    $pool .= '0123456789';
8388
    $poollen = strlen($pool);
8389
    $string = '';
8390
    for ($i = 0; $i < $length; $i++) {
8391
        $rand = ord($randombytes[$i]);
1326 ariadna 8392
        $string .= substr($pool, ($rand % ($poollen)), 1);
1 efrain 8393
    }
8394
    return $string;
8395
}
8396
 
8397
/**
8398
 * Generate a complex random string (useful for md5 salts)
8399
 *
8400
 * This function is based on the above {@link random_string()} however it uses a
8401
 * larger pool of characters and generates a string between 24 and 32 characters
8402
 *
8403
 * @param int $length Optional if set generates a string to exactly this length
8404
 * @return string
8405
 */
1326 ariadna 8406
function complex_random_string($length = null)
8407
{
1 efrain 8408
    $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8409
    $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8410
    $poollen = strlen($pool);
1326 ariadna 8411
    if ($length === null) {
1 efrain 8412
        $length = floor(rand(24, 32));
8413
    }
8414
    $randombytes = random_bytes($length);
8415
    $string = '';
8416
    for ($i = 0; $i < $length; $i++) {
8417
        $rand = ord($randombytes[$i]);
1326 ariadna 8418
        $string .= $pool[($rand % $poollen)];
1 efrain 8419
    }
8420
    return $string;
8421
}
8422
 
8423
/**
8424
 * Given some text (which may contain HTML) and an ideal length,
8425
 * this function truncates the text neatly on a word boundary if possible
8426
 *
8427
 * @category string
8428
 * @param string $text text to be shortened
8429
 * @param int $ideal ideal string length
8430
 * @param boolean $exact if false, $text will not be cut mid-word
8431
 * @param string $ending The string to append if the passed string is truncated
8432
 * @return string $truncate shortened string
8433
 */
1326 ariadna 8434
function shorten_text($text, $ideal = 30, $exact = false, $ending = '...')
8435
{
1 efrain 8436
    // If the plain text is shorter than the maximum length, return the whole text.
8437
    if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8438
        return $text;
8439
    }
8440
 
8441
    // Splits on HTML tags. Each open/close/empty tag will be the first thing
8442
    // and only tag in its 'line'.
8443
    preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8444
 
8445
    $totallength = core_text::strlen($ending);
8446
    $truncate = '';
8447
 
8448
    // This array stores information about open and close tags and their position
8449
    // in the truncated string. Each item in the array is an object with fields
8450
    // ->open (true if open), ->tag (tag name in lower case), and ->pos
8451
    // (byte position in truncated text).
8452
    $tagdetails = array();
8453
 
8454
    foreach ($lines as $linematchings) {
8455
        // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8456
        if (!empty($linematchings[1])) {
8457
            // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8458
            if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8459
                if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8460
                    // Record closing tag.
8461
                    $tagdetails[] = (object) array(
1326 ariadna 8462
                        'open' => false,
8463
                        'tag'  => core_text::strtolower($tagmatchings[1]),
8464
                        'pos'  => core_text::strlen($truncate),
8465
                    );
1 efrain 8466
                } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8467
                    // Record opening tag.
8468
                    $tagdetails[] = (object) array(
1326 ariadna 8469
                        'open' => true,
8470
                        'tag'  => core_text::strtolower($tagmatchings[1]),
8471
                        'pos'  => core_text::strlen($truncate),
8472
                    );
1 efrain 8473
                } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8474
                    $tagdetails[] = (object) array(
1326 ariadna 8475
                        'open' => true,
8476
                        'tag'  => core_text::strtolower('if'),
8477
                        'pos'  => core_text::strlen($truncate),
1 efrain 8478
                    );
8479
                } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8480
                    $tagdetails[] = (object) array(
1326 ariadna 8481
                        'open' => false,
8482
                        'tag'  => core_text::strtolower('if'),
8483
                        'pos'  => core_text::strlen($truncate),
1 efrain 8484
                    );
8485
                }
8486
            }
8487
            // Add html-tag to $truncate'd text.
8488
            $truncate .= $linematchings[1];
8489
        }
8490
 
8491
        // Calculate the length of the plain text part of the line; handle entities as one character.
8492
        $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8493
        if ($totallength + $contentlength > $ideal) {
8494
            // The number of characters which are left.
8495
            $left = $ideal - $totallength;
8496
            $entitieslength = 0;
8497
            // Search for html entities.
8498
            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)) {
8499
                // Calculate the real length of all entities in the legal range.
8500
                foreach ($entities[0] as $entity) {
1326 ariadna 8501
                    if ($entity[1] + 1 - $entitieslength <= $left) {
1 efrain 8502
                        $left--;
8503
                        $entitieslength += core_text::strlen($entity[0]);
8504
                    } else {
8505
                        // No more characters left.
8506
                        break;
8507
                    }
8508
                }
8509
            }
8510
            $breakpos = $left + $entitieslength;
8511
 
8512
            // If the words shouldn't be cut in the middle...
8513
            if (!$exact) {
8514
                // Search the last occurence of a space.
8515
                for (; $breakpos > 0; $breakpos--) {
8516
                    if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8517
                        if ($char === '.' or $char === ' ') {
8518
                            $breakpos += 1;
8519
                            break;
8520
                        } else if (strlen($char) > 2) {
8521
                            // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8522
                            $breakpos += 1;
8523
                            break;
8524
                        }
8525
                    }
8526
                }
8527
            }
8528
            if ($breakpos == 0) {
8529
                // This deals with the test_shorten_text_no_spaces case.
8530
                $breakpos = $left + $entitieslength;
8531
            } else if ($breakpos > $left + $entitieslength) {
8532
                // This deals with the previous for loop breaking on the first char.
8533
                $breakpos = $left + $entitieslength;
8534
            }
8535
 
8536
            $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8537
            // Maximum length is reached, so get off the loop.
8538
            break;
8539
        } else {
8540
            $truncate .= $linematchings[2];
8541
            $totallength += $contentlength;
8542
        }
8543
 
8544
        // If the maximum length is reached, get off the loop.
8545
        if ($totallength >= $ideal) {
8546
            break;
8547
        }
8548
    }
8549
 
8550
    // Add the defined ending to the text.
8551
    $truncate .= $ending;
8552
 
8553
    // Now calculate the list of open html tags based on the truncate position.
8554
    $opentags = array();
8555
    foreach ($tagdetails as $taginfo) {
8556
        if ($taginfo->open) {
8557
            // Add tag to the beginning of $opentags list.
8558
            array_unshift($opentags, $taginfo->tag);
8559
        } else {
8560
            // Can have multiple exact same open tags, close the last one.
8561
            $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8562
            if ($pos !== false) {
8563
                unset($opentags[$pos]);
8564
            }
8565
        }
8566
    }
8567
 
8568
    // Close all unclosed html-tags.
8569
    foreach ($opentags as $tag) {
8570
        if ($tag === 'if') {
8571
            $truncate .= '<!--<![endif]-->';
8572
        } else {
8573
            $truncate .= '</' . $tag . '>';
8574
        }
8575
    }
8576
 
8577
    return $truncate;
8578
}
8579
 
8580
/**
8581
 * Shortens a given filename by removing characters positioned after the ideal string length.
8582
 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8583
 * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8584
 *
8585
 * @param string $filename file name
8586
 * @param int $length ideal string length
8587
 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8588
 * @return string $shortened shortened file name
8589
 */
1326 ariadna 8590
function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false)
8591
{
1 efrain 8592
    $shortened = $filename;
8593
    // Extract a part of the filename if it's char size exceeds the ideal string length.
8594
    if (core_text::strlen($filename) > $length) {
8595
        // Exclude extension if present in filename.
8596
        $mimetypes = get_mimetypes_array();
8597
        $extension = pathinfo($filename, PATHINFO_EXTENSION);
8598
        if ($extension && !empty($mimetypes[$extension])) {
8599
            $basename = pathinfo($filename, PATHINFO_FILENAME);
8600
            $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8601
            $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8602
            $shortened .= '.' . $extension;
8603
        } else {
8604
            $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8605
            $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8606
        }
8607
    }
8608
    return $shortened;
8609
}
8610
 
8611
/**
8612
 * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8613
 *
8614
 * @param array $path The paths to reduce the length.
8615
 * @param int $length Ideal string length
8616
 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8617
 * @return array $result Shortened paths in array.
8618
 */
1326 ariadna 8619
function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false)
8620
{
1 efrain 8621
    $result = null;
8622
 
1326 ariadna 8623
    $result = array_reduce($path, function ($carry, $singlepath) use ($length, $includehash) {
1 efrain 8624
        $carry[] = shorten_filename($singlepath, $length, $includehash);
8625
        return $carry;
8626
    }, []);
8627
 
8628
    return $result;
8629
}
8630
 
8631
/**
8632
 * Given dates in seconds, how many weeks is the date from startdate
8633
 * The first week is 1, the second 2 etc ...
8634
 *
8635
 * @param int $startdate Timestamp for the start date
8636
 * @param int $thedate Timestamp for the end date
8637
 * @return string
8638
 */
1326 ariadna 8639
function getweek($startdate, $thedate)
8640
{
1 efrain 8641
    if ($thedate < $startdate) {
8642
        return 0;
8643
    }
8644
 
8645
    return floor(($thedate - $startdate) / WEEKSECS) + 1;
8646
}
8647
 
8648
/**
8649
 * Returns a randomly generated password of length $maxlen.  inspired by
8650
 *
8651
 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8652
 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8653
 *
8654
 * @param int $maxlen  The maximum size of the password being generated.
8655
 * @return string
8656
 */
1326 ariadna 8657
function generate_password($maxlen = 10)
8658
{
1 efrain 8659
    global $CFG;
8660
 
8661
    if (empty($CFG->passwordpolicy)) {
8662
        $fillers = PASSWORD_DIGITS;
8663
        $wordlist = file($CFG->wordlist);
8664
        $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8665
        $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8666
        $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8667
        $password = $word1 . $filler1 . $word2;
8668
    } else {
8669
        $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8670
        $digits = $CFG->minpassworddigits;
8671
        $lower = $CFG->minpasswordlower;
8672
        $upper = $CFG->minpasswordupper;
8673
        $nonalphanum = $CFG->minpasswordnonalphanum;
8674
        $total = $lower + $upper + $digits + $nonalphanum;
8675
        // Var minlength should be the greater one of the two ( $minlen and $total ).
8676
        $minlen = $minlen < $total ? $total : $minlen;
8677
        // Var maxlen can never be smaller than minlen.
8678
        $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8679
        $additional = $maxlen - $total;
8680
 
8681
        // Make sure we have enough characters to fulfill
8682
        // complexity requirements.
8683
        $passworddigits = PASSWORD_DIGITS;
8684
        while ($digits > strlen($passworddigits)) {
8685
            $passworddigits .= PASSWORD_DIGITS;
8686
        }
8687
        $passwordlower = PASSWORD_LOWER;
8688
        while ($lower > strlen($passwordlower)) {
8689
            $passwordlower .= PASSWORD_LOWER;
8690
        }
8691
        $passwordupper = PASSWORD_UPPER;
8692
        while ($upper > strlen($passwordupper)) {
8693
            $passwordupper .= PASSWORD_UPPER;
8694
        }
8695
        $passwordnonalphanum = PASSWORD_NONALPHANUM;
8696
        while ($nonalphanum > strlen($passwordnonalphanum)) {
8697
            $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8698
        }
8699
 
8700
        // Now mix and shuffle it all.
1326 ariadna 8701
        $password = str_shuffle(substr(str_shuffle($passwordlower), 0, $lower) .
8702
            substr(str_shuffle($passwordupper), 0, $upper) .
8703
            substr(str_shuffle($passworddigits), 0, $digits) .
8704
            substr(str_shuffle($passwordnonalphanum), 0, $nonalphanum) .
8705
            substr(str_shuffle($passwordlower .
8706
                $passwordupper .
8707
                $passworddigits .
8708
                $passwordnonalphanum), 0, $additional));
1 efrain 8709
    }
8710
 
1326 ariadna 8711
    return substr($password, 0, $maxlen);
1 efrain 8712
}
8713
 
8714
/**
8715
 * Given a float, prints it nicely.
8716
 * Localized floats must not be used in calculations!
8717
 *
8718
 * The stripzeros feature is intended for making numbers look nicer in small
8719
 * areas where it is not necessary to indicate the degree of accuracy by showing
8720
 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8721
 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8722
 *
8723
 * @param float $float The float to print
8724
 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8725
 * @param bool $localized use localized decimal separator
8726
 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8727
 *                         the decimal point are always striped if $decimalpoints is -1.
8728
 * @return string locale float
8729
 */
1326 ariadna 8730
function format_float($float, $decimalpoints = 1, $localized = true, $stripzeros = false)
8731
{
1 efrain 8732
    if (is_null($float)) {
8733
        return '';
8734
    }
8735
    if ($localized) {
8736
        $separator = get_string('decsep', 'langconfig');
8737
    } else {
8738
        $separator = '.';
8739
    }
8740
    if ($decimalpoints == -1) {
8741
        // The following counts the number of decimals.
8742
        // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8743
        $floatval = floatval($float);
8744
        for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8745
    }
8746
 
8747
    $result = number_format($float, $decimalpoints, $separator, '');
8748
    if ($stripzeros && $decimalpoints > 0) {
8749
        // Remove zeros and final dot if not needed.
8750
        // However, only do this if there is a decimal point!
8751
        $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8752
    }
8753
    return $result;
8754
}
8755
 
8756
/**
8757
 * Converts locale specific floating point/comma number back to standard PHP float value
8758
 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8759
 *
8760
 * @param string $localefloat locale aware float representation
8761
 * @param bool $strict If true, then check the input and return false if it is not a valid number.
8762
 * @return mixed float|bool - false or the parsed float.
8763
 */
1326 ariadna 8764
function unformat_float($localefloat, $strict = false)
8765
{
1 efrain 8766
    $localefloat = trim((string)$localefloat);
8767
 
8768
    if ($localefloat == '') {
8769
        return null;
8770
    }
8771
 
8772
    $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8773
    $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8774
 
8775
    if ($strict && !is_numeric($localefloat)) {
8776
        return false;
8777
    }
8778
 
8779
    return (float)$localefloat;
8780
}
8781
 
8782
/**
8783
 * Given a simple array, this shuffles it up just like shuffle()
8784
 * Unlike PHP's shuffle() this function works on any machine.
8785
 *
8786
 * @param array $array The array to be rearranged
8787
 * @return array
8788
 */
1326 ariadna 8789
function swapshuffle($array)
8790
{
1 efrain 8791
 
8792
    $last = count($array) - 1;
8793
    for ($i = 0; $i <= $last; $i++) {
8794
        $from = rand(0, $last);
8795
        $curr = $array[$i];
8796
        $array[$i] = $array[$from];
8797
        $array[$from] = $curr;
8798
    }
8799
    return $array;
8800
}
8801
 
8802
/**
8803
 * Like {@link swapshuffle()}, but works on associative arrays
8804
 *
8805
 * @param array $array The associative array to be rearranged
8806
 * @return array
8807
 */
1326 ariadna 8808
function swapshuffle_assoc($array)
8809
{
1 efrain 8810
 
8811
    $newarray = array();
8812
    $newkeys = swapshuffle(array_keys($array));
8813
 
8814
    foreach ($newkeys as $newkey) {
8815
        $newarray[$newkey] = $array[$newkey];
8816
    }
8817
    return $newarray;
8818
}
8819
 
8820
/**
8821
 * Given an arbitrary array, and a number of draws,
8822
 * this function returns an array with that amount
8823
 * of items.  The indexes are retained.
8824
 *
8825
 * @todo Finish documenting this function
8826
 *
8827
 * @param array $array
8828
 * @param int $draws
8829
 * @return array
8830
 */
1326 ariadna 8831
function draw_rand_array($array, $draws)
8832
{
1 efrain 8833
 
8834
    $return = array();
8835
 
8836
    $last = count($array);
8837
 
8838
    if ($draws > $last) {
8839
        $draws = $last;
8840
    }
8841
 
8842
    while ($draws > 0) {
8843
        $last--;
8844
 
8845
        $keys = array_keys($array);
8846
        $rand = rand(0, $last);
8847
 
8848
        $return[$keys[$rand]] = $array[$keys[$rand]];
8849
        unset($array[$keys[$rand]]);
8850
 
8851
        $draws--;
8852
    }
8853
 
8854
    return $return;
8855
}
8856
 
8857
/**
8858
 * Calculate the difference between two microtimes
8859
 *
8860
 * @param string $a The first Microtime
8861
 * @param string $b The second Microtime
8862
 * @return string
8863
 */
1326 ariadna 8864
function microtime_diff($a, $b)
8865
{
1 efrain 8866
    list($adec, $asec) = explode(' ', $a);
8867
    list($bdec, $bsec) = explode(' ', $b);
8868
    return $bsec - $asec + $bdec - $adec;
8869
}
8870
 
8871
/**
8872
 * Given a list (eg a,b,c,d,e) this function returns
8873
 * an array of 1->a, 2->b, 3->c etc
8874
 *
8875
 * @param string $list The string to explode into array bits
8876
 * @param string $separator The separator used within the list string
8877
 * @return array The now assembled array
8878
 */
1326 ariadna 8879
function make_menu_from_list($list, $separator = ',')
8880
{
1 efrain 8881
 
8882
    $array = array_reverse(explode($separator, $list), true);
8883
    foreach ($array as $key => $item) {
1326 ariadna 8884
        $outarray[$key + 1] = trim($item);
1 efrain 8885
    }
8886
    return $outarray;
8887
}
8888
 
8889
/**
8890
 * Creates an array that represents all the current grades that
8891
 * can be chosen using the given grading type.
8892
 *
8893
 * Negative numbers
8894
 * are scales, zero is no grade, and positive numbers are maximum
8895
 * grades.
8896
 *
8897
 * @todo Finish documenting this function or better deprecated this completely!
8898
 *
8899
 * @param int $gradingtype
8900
 * @return array
8901
 */
1326 ariadna 8902
function make_grades_menu($gradingtype)
8903
{
1 efrain 8904
    global $DB;
8905
 
8906
    $grades = array();
8907
    if ($gradingtype < 0) {
1326 ariadna 8908
        if ($scale = $DB->get_record('scale', array('id' => (-$gradingtype)))) {
1 efrain 8909
            return make_menu_from_list($scale->scale);
8910
        }
8911
    } else if ($gradingtype > 0) {
1326 ariadna 8912
        for ($i = $gradingtype; $i >= 0; $i--) {
8913
            $grades[$i] = $i . ' / ' . $gradingtype;
1 efrain 8914
        }
8915
        return $grades;
8916
    }
8917
    return $grades;
8918
}
8919
 
8920
/**
8921
 * make_unique_id_code
8922
 *
8923
 * @todo Finish documenting this function
8924
 *
8925
 * @uses $_SERVER
8926
 * @param string $extra Extra string to append to the end of the code
8927
 * @return string
8928
 */
1326 ariadna 8929
function make_unique_id_code($extra = '')
8930
{
1 efrain 8931
 
8932
    $hostname = 'unknownhost';
8933
    if (!empty($_SERVER['HTTP_HOST'])) {
8934
        $hostname = $_SERVER['HTTP_HOST'];
8935
    } else if (!empty($_ENV['HTTP_HOST'])) {
8936
        $hostname = $_ENV['HTTP_HOST'];
8937
    } else if (!empty($_SERVER['SERVER_NAME'])) {
8938
        $hostname = $_SERVER['SERVER_NAME'];
8939
    } else if (!empty($_ENV['SERVER_NAME'])) {
8940
        $hostname = $_ENV['SERVER_NAME'];
8941
    }
8942
 
8943
    $date = gmdate("ymdHis");
8944
 
8945
    $random =  random_string(6);
8946
 
8947
    if ($extra) {
1326 ariadna 8948
        return $hostname . '+' . $date . '+' . $random . '+' . $extra;
1 efrain 8949
    } else {
1326 ariadna 8950
        return $hostname . '+' . $date . '+' . $random;
1 efrain 8951
    }
8952
}
8953
 
8954
 
8955
/**
8956
 * Function to check the passed address is within the passed subnet
8957
 *
8958
 * The parameter is a comma separated string of subnet definitions.
8959
 * Subnet strings can be in one of three formats:
8960
 *   1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn          (number of bits in net mask)
8961
 *   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)
8962
 *   3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.                  (incomplete address, a bit non-technical ;-)
8963
 * Code for type 1 modified from user posted comments by mediator at
8964
 * {@link http://au.php.net/manual/en/function.ip2long.php}
8965
 *
8966
 * @param string $addr    The address you are checking
8967
 * @param string $subnetstr    The string of subnet addresses
8968
 * @param bool $checkallzeros    The state to whether check for 0.0.0.0
8969
 * @return bool
8970
 */
1326 ariadna 8971
function address_in_subnet($addr, $subnetstr, $checkallzeros = false)
8972
{
1 efrain 8973
 
8974
    if ($addr == '0.0.0.0' && !$checkallzeros) {
8975
        return false;
8976
    }
8977
    $subnets = explode(',', $subnetstr);
8978
    $found = false;
8979
    $addr = trim($addr);
8980
    $addr = cleanremoteaddr($addr, false); // Normalise.
8981
    if ($addr === null) {
8982
        return false;
8983
    }
8984
    $addrparts = explode(':', $addr);
8985
 
8986
    $ipv6 = strpos($addr, ':');
8987
 
8988
    foreach ($subnets as $subnet) {
8989
        $subnet = trim($subnet);
8990
        if ($subnet === '') {
8991
            continue;
8992
        }
8993
 
8994
        if (strpos($subnet, '/') !== false) {
8995
            // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
8996
            list($ip, $mask) = explode('/', $subnet);
8997
            $mask = trim($mask);
8998
            if (!is_number($mask)) {
8999
                continue; // Incorect mask number, eh?
9000
            }
9001
            $ip = cleanremoteaddr($ip, false); // Normalise.
9002
            if ($ip === null) {
9003
                continue;
9004
            }
9005
            if (strpos($ip, ':') !== false) {
9006
                // IPv6.
9007
                if (!$ipv6) {
9008
                    continue;
9009
                }
9010
                if ($mask > 128 or $mask < 0) {
9011
                    continue; // Nonsense.
9012
                }
9013
                if ($mask == 0) {
9014
                    return true; // Any address.
9015
                }
9016
                if ($mask == 128) {
9017
                    if ($ip === $addr) {
9018
                        return true;
9019
                    }
9020
                    continue;
9021
                }
9022
                $ipparts = explode(':', $ip);
9023
                $modulo  = $mask % 16;
1326 ariadna 9024
                $ipnet   = array_slice($ipparts, 0, ($mask - $modulo) / 16);
9025
                $addrnet = array_slice($addrparts, 0, ($mask - $modulo) / 16);
1 efrain 9026
                if (implode(':', $ipnet) === implode(':', $addrnet)) {
9027
                    if ($modulo == 0) {
9028
                        return true;
9029
                    }
1326 ariadna 9030
                    $pos     = ($mask - $modulo) / 16;
1 efrain 9031
                    $ipnet   = hexdec($ipparts[$pos]);
9032
                    $addrnet = hexdec($addrparts[$pos]);
9033
                    $mask    = 0xffff << (16 - $modulo);
9034
                    if (($addrnet & $mask) == ($ipnet & $mask)) {
9035
                        return true;
9036
                    }
9037
                }
9038
            } else {
9039
                // IPv4.
9040
                if ($ipv6) {
9041
                    continue;
9042
                }
9043
                if ($mask > 32 or $mask < 0) {
9044
                    continue; // Nonsense.
9045
                }
9046
                if ($mask == 0) {
9047
                    return true;
9048
                }
9049
                if ($mask == 32) {
9050
                    if ($ip === $addr) {
9051
                        return true;
9052
                    }
9053
                    continue;
9054
                }
9055
                $mask = 0xffffffff << (32 - $mask);
9056
                if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9057
                    return true;
9058
                }
9059
            }
9060
        } else if (strpos($subnet, '-') !== false) {
9061
            // 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.
9062
            $parts = explode('-', $subnet);
9063
            if (count($parts) != 2) {
9064
                continue;
9065
            }
9066
 
9067
            if (strpos($subnet, ':') !== false) {
9068
                // IPv6.
9069
                if (!$ipv6) {
9070
                    continue;
9071
                }
9072
                $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9073
                if ($ipstart === null) {
9074
                    continue;
9075
                }
9076
                $ipparts = explode(':', $ipstart);
9077
                $start = hexdec(array_pop($ipparts));
9078
                $ipparts[] = trim($parts[1]);
9079
                $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
9080
                if ($ipend === null) {
9081
                    continue;
9082
                }
9083
                $ipparts[7] = '';
9084
                $ipnet = implode(':', $ipparts);
9085
                if (strpos($addr, $ipnet) !== 0) {
9086
                    continue;
9087
                }
9088
                $ipparts = explode(':', $ipend);
9089
                $end = hexdec($ipparts[7]);
9090
 
9091
                $addrend = hexdec($addrparts[7]);
9092
 
9093
                if (($addrend >= $start) and ($addrend <= $end)) {
9094
                    return true;
9095
                }
9096
            } else {
9097
                // IPv4.
9098
                if ($ipv6) {
9099
                    continue;
9100
                }
9101
                $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9102
                if ($ipstart === null) {
9103
                    continue;
9104
                }
9105
                $ipparts = explode('.', $ipstart);
9106
                $ipparts[3] = trim($parts[1]);
9107
                $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
9108
                if ($ipend === null) {
9109
                    continue;
9110
                }
9111
 
9112
                if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9113
                    return true;
9114
                }
9115
            }
9116
        } else {
9117
            // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9118
            if (strpos($subnet, ':') !== false) {
9119
                // IPv6.
9120
                if (!$ipv6) {
9121
                    continue;
9122
                }
9123
                $parts = explode(':', $subnet);
9124
                $count = count($parts);
1326 ariadna 9125
                if ($parts[$count - 1] === '') {
9126
                    unset($parts[$count - 1]); // Trim trailing :'s.
1 efrain 9127
                    $count--;
9128
                    $subnet = implode('.', $parts);
9129
                }
9130
                $isip = cleanremoteaddr($subnet, false); // Normalise.
9131
                if ($isip !== null) {
9132
                    if ($isip === $addr) {
9133
                        return true;
9134
                    }
9135
                    continue;
9136
                } else if ($count > 8) {
9137
                    continue;
9138
                }
1326 ariadna 9139
                $zeros = array_fill(0, 8 - $count, '0');
9140
                $subnet = $subnet . ':' . implode(':', $zeros) . '/' . ($count * 16);
1 efrain 9141
                if (address_in_subnet($addr, $subnet)) {
9142
                    return true;
9143
                }
9144
            } else {
9145
                // IPv4.
9146
                if ($ipv6) {
9147
                    continue;
9148
                }
9149
                $parts = explode('.', $subnet);
9150
                $count = count($parts);
1326 ariadna 9151
                if ($parts[$count - 1] === '') {
9152
                    unset($parts[$count - 1]); // Trim trailing .
1 efrain 9153
                    $count--;
9154
                    $subnet = implode('.', $parts);
9155
                }
9156
                if ($count == 4) {
9157
                    $subnet = cleanremoteaddr($subnet, false); // Normalise.
9158
                    if ($subnet === $addr) {
9159
                        return true;
9160
                    }
9161
                    continue;
9162
                } else if ($count > 4) {
9163
                    continue;
9164
                }
1326 ariadna 9165
                $zeros = array_fill(0, 4 - $count, '0');
9166
                $subnet = $subnet . '.' . implode('.', $zeros) . '/' . ($count * 8);
1 efrain 9167
                if (address_in_subnet($addr, $subnet)) {
9168
                    return true;
9169
                }
9170
            }
9171
        }
9172
    }
9173
 
9174
    return false;
9175
}
9176
 
9177
/**
9178
 * For outputting debugging info
9179
 *
9180
 * @param string $string The string to write
9181
 * @param string $eol The end of line char(s) to use
9182
 * @param string $sleep Period to make the application sleep
9183
 *                      This ensures any messages have time to display before redirect
9184
 */
1326 ariadna 9185
function mtrace($string, $eol = "\n", $sleep = 0)
9186
{
1 efrain 9187
    global $CFG;
9188
 
9189
    if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
9190
        $fn = $CFG->mtrace_wrapper;
9191
        $fn($string, $eol);
9192
        return;
9193
    } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
9194
        // We must explicitly call the add_line function here.
9195
        // Uses of fwrite to STDOUT are not picked up by ob_start.
9196
        if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
9197
            fwrite(STDOUT, $output);
9198
        }
9199
    } else {
9200
        echo $string . $eol;
9201
    }
9202
 
9203
    // Flush again.
9204
    flush();
9205
 
9206
    // Delay to keep message on user's screen in case of subsequent redirect.
9207
    if ($sleep) {
9208
        sleep($sleep);
9209
    }
9210
}
9211
 
9212
/**
9213
 * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
9214
 *
9215
 * @param Throwable $e the error to ouptput.
9216
 */
1326 ariadna 9217
function mtrace_exception(Throwable $e): void
9218
{
1 efrain 9219
    $info = get_exception_info($e);
9220
 
9221
    $message = $info->message;
9222
    if ($info->debuginfo) {
9223
        $message .= "\n\n" . $info->debuginfo;
9224
    }
9225
    if ($info->backtrace) {
9226
        $message .= "\n\n" . format_backtrace($info->backtrace, true);
9227
    }
9228
 
9229
    mtrace($message);
9230
}
9231
 
9232
/**
9233
 * Replace 1 or more slashes or backslashes to 1 slash
9234
 *
9235
 * @param string $path The path to strip
9236
 * @return string the path with double slashes removed
9237
 */
1326 ariadna 9238
function cleardoubleslashes($path)
9239
{
1 efrain 9240
    return preg_replace('/(\/|\\\){1,}/', '/', $path);
9241
}
9242
 
9243
/**
9244
 * Is the current ip in a given list?
9245
 *
9246
 * @param string $list
9247
 * @return bool
9248
 */
1326 ariadna 9249
function remoteip_in_list($list)
9250
{
1 efrain 9251
    $clientip = getremoteaddr(null);
9252
 
9253
    if (!$clientip) {
9254
        // Ensure access on cli.
9255
        return true;
9256
    }
9257
    return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
9258
}
9259
 
9260
/**
9261
 * Returns most reliable client address
9262
 *
9263
 * @param string $default If an address can't be determined, then return this
9264
 * @return string The remote IP address
9265
 */
1326 ariadna 9266
function getremoteaddr($default = '0.0.0.0')
9267
{
1 efrain 9268
    global $CFG;
9269
 
9270
    if (!isset($CFG->getremoteaddrconf)) {
9271
        // This will happen, for example, before just after the upgrade, as the
9272
        // user is redirected to the admin screen.
9273
        $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
9274
    } else {
9275
        $variablestoskip = $CFG->getremoteaddrconf;
9276
    }
9277
    if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9278
        if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9279
            $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9280
            return $address ? $address : $default;
9281
        }
9282
    }
9283
    if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9284
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9285
            $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9286
 
1326 ariadna 9287
            $forwardedaddresses = array_filter($forwardedaddresses, function ($ip) {
1 efrain 9288
                global $CFG;
9289
                return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9290
            });
9291
 
9292
            // Multiple proxies can append values to this header including an
9293
            // untrusted original request header so we must only trust the last ip.
9294
            $address = end($forwardedaddresses);
9295
 
9296
            if (substr_count($address, ":") > 1) {
9297
                // Remove port and brackets from IPv6.
9298
                if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9299
                    $address = $matches[1];
9300
                }
9301
            } else {
9302
                // Remove port from IPv4.
9303
                if (substr_count($address, ":") == 1) {
9304
                    $parts = explode(":", $address);
9305
                    $address = $parts[0];
9306
                }
9307
            }
9308
 
9309
            $address = cleanremoteaddr($address);
9310
            return $address ? $address : $default;
9311
        }
9312
    }
9313
    if (!empty($_SERVER['REMOTE_ADDR'])) {
9314
        $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9315
        return $address ? $address : $default;
9316
    } else {
9317
        return $default;
9318
    }
9319
}
9320
 
9321
/**
9322
 * Cleans an ip address. Internal addresses are now allowed.
9323
 * (Originally local addresses were not allowed.)
9324
 *
9325
 * @param string $addr IPv4 or IPv6 address
9326
 * @param bool $compress use IPv6 address compression
9327
 * @return string normalised ip address string, null if error
9328
 */
1326 ariadna 9329
function cleanremoteaddr($addr, $compress = false)
9330
{
1 efrain 9331
    $addr = trim($addr);
9332
 
9333
    if (strpos($addr, ':') !== false) {
9334
        // Can be only IPv6.
9335
        $parts = explode(':', $addr);
9336
        $count = count($parts);
9337
 
1326 ariadna 9338
        if (strpos($parts[$count - 1], '.') !== false) {
1 efrain 9339
            // Legacy ipv4 notation.
9340
            $last = array_pop($parts);
9341
            $ipv4 = cleanremoteaddr($last, true);
9342
            if ($ipv4 === null) {
9343
                return null;
9344
            }
9345
            $bits = explode('.', $ipv4);
1326 ariadna 9346
            $parts[] = dechex($bits[0]) . dechex($bits[1]);
9347
            $parts[] = dechex($bits[2]) . dechex($bits[3]);
1 efrain 9348
            $count = count($parts);
9349
            $addr = implode(':', $parts);
9350
        }
9351
 
9352
        if ($count < 3 or $count > 8) {
9353
            return null; // Severly malformed.
9354
        }
9355
 
9356
        if ($count != 8) {
9357
            if (strpos($addr, '::') === false) {
9358
                return null; // Malformed.
9359
            }
9360
            // Uncompress.
9361
            $insertat = array_search('', $parts, true);
9362
            $missing = array_fill(0, 1 + 8 - $count, '0');
9363
            array_splice($parts, $insertat, 1, $missing);
9364
            foreach ($parts as $key => $part) {
9365
                if ($part === '') {
9366
                    $parts[$key] = '0';
9367
                }
9368
            }
9369
        }
9370
 
9371
        $adr = implode(':', $parts);
9372
        if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9373
            return null; // Incorrect format - sorry.
9374
        }
9375
 
9376
        // Normalise 0s and case.
9377
        $parts = array_map('hexdec', $parts);
9378
        $parts = array_map('dechex', $parts);
9379
 
9380
        $result = implode(':', $parts);
9381
 
9382
        if (!$compress) {
9383
            return $result;
9384
        }
9385
 
9386
        if ($result === '0:0:0:0:0:0:0:0') {
9387
            return '::'; // All addresses.
9388
        }
9389
 
9390
        $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9391
        if ($compressed !== $result) {
9392
            return $compressed;
9393
        }
9394
 
9395
        $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9396
        if ($compressed !== $result) {
9397
            return $compressed;
9398
        }
9399
 
9400
        $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9401
        if ($compressed !== $result) {
9402
            return $compressed;
9403
        }
9404
 
9405
        return $result;
9406
    }
9407
 
9408
    // First get all things that look like IPv4 addresses.
9409
    $parts = array();
9410
    if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9411
        return null;
9412
    }
9413
    unset($parts[0]);
9414
 
9415
    foreach ($parts as $key => $match) {
9416
        if ($match > 255) {
9417
            return null;
9418
        }
9419
        $parts[$key] = (int)$match; // Normalise 0s.
9420
    }
9421
 
9422
    return implode('.', $parts);
9423
}
9424
 
9425
 
9426
/**
9427
 * Is IP address a public address?
9428
 *
9429
 * @param string $ip The ip to check
9430
 * @return bool true if the ip is public
9431
 */
1326 ariadna 9432
function ip_is_public($ip)
9433
{
1 efrain 9434
    return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9435
}
9436
 
9437
/**
9438
 * This function will make a complete copy of anything it's given,
9439
 * regardless of whether it's an object or not.
9440
 *
9441
 * @param mixed $thing Something you want cloned
9442
 * @return mixed What ever it is you passed it
9443
 */
1326 ariadna 9444
function fullclone($thing)
9445
{
1 efrain 9446
    return unserialize(serialize($thing));
9447
}
9448
 
9449
/**
9450
 * Used to make sure that $min <= $value <= $max
9451
 *
9452
 * Make sure that value is between min, and max
9453
 *
9454
 * @param int $min The minimum value
9455
 * @param int $value The value to check
9456
 * @param int $max The maximum value
9457
 * @return int
9458
 */
1326 ariadna 9459
function bounded_number($min, $value, $max)
9460
{
1 efrain 9461
    if ($value < $min) {
9462
        return $min;
9463
    }
9464
    if ($value > $max) {
9465
        return $max;
9466
    }
9467
    return $value;
9468
}
9469
 
9470
/**
9471
 * Check if there is a nested array within the passed array
9472
 *
9473
 * @param array $array
9474
 * @return bool true if there is a nested array false otherwise
9475
 */
1326 ariadna 9476
function array_is_nested($array)
9477
{
1 efrain 9478
    foreach ($array as $value) {
9479
        if (is_array($value)) {
9480
            return true;
9481
        }
9482
    }
9483
    return false;
9484
}
9485
 
9486
/**
9487
 * get_performance_info() pairs up with init_performance_info()
9488
 * loaded in setup.php. Returns an array with 'html' and 'txt'
9489
 * values ready for use, and each of the individual stats provided
9490
 * separately as well.
9491
 *
9492
 * @return array
9493
 */
1326 ariadna 9494
function get_performance_info()
9495
{
1 efrain 9496
    global $CFG, $PERF, $DB, $PAGE;
9497
 
9498
    $info = array();
9499
    $info['txt']  = me() . ' '; // Holds log-friendly representation.
9500
 
9501
    $info['html'] = '';
9502
    if (!empty($CFG->themedesignermode)) {
9503
        // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9504
        $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9505
    }
9506
    $info['html'] .= '<ul class="list-unstyled row mx-md-0">';         // Holds userfriendly HTML representation.
9507
 
9508
    $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9509
 
1326 ariadna 9510
    $info['html'] .= '<li class="timeused col-sm-4">' . $info['realtime'] . ' secs</li> ';
9511
    $info['txt'] .= 'time: ' . $info['realtime'] . 's ';
1 efrain 9512
 
9513
    // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9514
    $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9515
 
9516
    if (function_exists('memory_get_usage')) {
9517
        $info['memory_total'] = memory_get_usage();
9518
        $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
1326 ariadna 9519
        $info['html'] .= '<li class="memoryused col-sm-4">RAM: ' . display_size($info['memory_total']) . '</li> ';
9520
        $info['txt']  .= 'memory_total: ' . $info['memory_total'] . 'B (' . display_size($info['memory_total']) . ') memory_growth: ' .
9521
            $info['memory_growth'] . 'B (' . display_size($info['memory_growth']) . ') ';
1 efrain 9522
    }
9523
 
9524
    if (function_exists('memory_get_peak_usage')) {
9525
        $info['memory_peak'] = memory_get_peak_usage();
1326 ariadna 9526
        $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: ' . display_size($info['memory_peak']) . '</li> ';
9527
        $info['txt']  .= 'memory_peak: ' . $info['memory_peak'] . 'B (' . display_size($info['memory_peak']) . ') ';
1 efrain 9528
    }
9529
 
9530
    $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9531
    $inc = get_included_files();
9532
    $info['includecount'] = count($inc);
1326 ariadna 9533
    $info['html'] .= '<li class="included col-sm-4">Included ' . $info['includecount'] . ' files</li> ';
9534
    $info['txt']  .= 'includecount: ' . $info['includecount'] . ' ';
1 efrain 9535
 
9536
    if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9537
        // We can not track more performance before installation or before PAGE init, sorry.
9538
        return $info;
9539
    }
9540
 
9541
    $filtermanager = filter_manager::instance();
9542
    if (method_exists($filtermanager, 'get_performance_summary')) {
9543
        list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9544
        $info = array_merge($filterinfo, $info);
9545
        foreach ($filterinfo as $key => $value) {
9546
            $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9547
            $info['txt'] .= "$key: $value ";
9548
        }
9549
    }
9550
 
9551
    $stringmanager = get_string_manager();
9552
    if (method_exists($stringmanager, 'get_performance_summary')) {
9553
        list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9554
        $info = array_merge($filterinfo, $info);
9555
        foreach ($filterinfo as $key => $value) {
9556
            $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9557
            $info['txt'] .= "$key: $value ";
9558
        }
9559
    }
9560
 
1326 ariadna 9561
    $info['dbqueries'] = $DB->perf_get_reads() . '/' . $DB->perf_get_writes();
9562
    $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: ' . $info['dbqueries'] . '</li> ';
9563
    $info['txt'] .= 'db reads/writes: ' . $info['dbqueries'] . ' ';
1 efrain 9564
 
9565
    if ($DB->want_read_slave()) {
9566
        $info['dbreads_slave'] = $DB->perf_get_reads_slave();
1326 ariadna 9567
        $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: ' . $info['dbreads_slave'] . '</li> ';
9568
        $info['txt'] .= 'db reads from slave: ' . $info['dbreads_slave'] . ' ';
1 efrain 9569
    }
9570
 
9571
    $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
1326 ariadna 9572
    $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: ' . $info['dbtime'] . ' secs</li> ';
1 efrain 9573
    $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9574
 
9575
    if (function_exists('posix_times')) {
9576
        $ptimes = posix_times();
9577
        if (is_array($ptimes)) {
9578
            foreach ($ptimes as $key => $val) {
9579
                $info[$key] = $ptimes[$key] -  $PERF->startposixtimes[$key];
9580
            }
9581
            $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9582
            $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9583
            $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9584
        }
9585
    }
9586
 
9587
    // Grab the load average for the last minute.
9588
    // /proc will only work under some linux configurations
9589
    // while uptime is there under MacOSX/Darwin and other unices.
9590
    if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9591
        list($serverload) = explode(' ', $loadavg[0]);
9592
        unset($loadavg);
1326 ariadna 9593
    } else if (function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime`) {
1 efrain 9594
        if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9595
            $serverload = $matches[1];
9596
        } else {
9597
            trigger_error('Could not parse uptime output!');
9598
        }
9599
    }
9600
    if (!empty($serverload)) {
9601
        $info['serverload'] = $serverload;
1326 ariadna 9602
        $info['html'] .= '<li class="serverload col-sm-4">Load average: ' . $info['serverload'] . '</li> ';
1 efrain 9603
        $info['txt'] .= "serverload: {$info['serverload']} ";
9604
    }
9605
 
9606
    // Display size of session if session started.
9607
    if ($si = \core\session\manager::get_performance_info()) {
9608
        $info['sessionsize'] = $si['size'];
9609
        $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9610
        $info['txt'] .= $si['txt'];
9611
    }
9612
 
9613
    // Display time waiting for session if applicable.
9614
    if (!empty($PERF->sessionlock['wait'])) {
9615
        $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs';
9616
        $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [
9617
            'class' => 'sessionwait col-sm-4'
9618
        ]);
9619
        $info['txt'] .= 'sessionwait: ' . $sessionwait . ' ';
9620
    }
9621
 
9622
    $info['html'] .= '</ul>';
9623
    $html = '';
9624
    if ($stats = cache_helper::get_stats()) {
9625
 
9626
        $table = new html_table();
9627
        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9628
        $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9629
        $table->data = [];
9630
        $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9631
 
9632
        $text = 'Caches used (hits/misses/sets): ';
9633
        $hits = 0;
9634
        $misses = 0;
9635
        $sets = 0;
9636
        $maxstores = 0;
9637
 
9638
        // We want to align static caches into their own column.
9639
        $hasstatic = false;
9640
        foreach ($stats as $definition => $details) {
9641
            $numstores = count($details['stores']);
9642
            $first = key($details['stores']);
9643
            if ($first !== cache_store::STATIC_ACCEL) {
9644
                $numstores++; // Add a blank space for the missing static store.
9645
            }
9646
            $maxstores = max($maxstores, $numstores);
9647
        }
9648
 
9649
        $storec = 0;
9650
 
9651
        while ($storec++ < ($maxstores - 2)) {
9652
            if ($storec == ($maxstores - 2)) {
9653
                $table->head[] = get_string('mappingfinal', 'cache');
9654
            } else {
9655
                $table->head[] = "Store $storec";
9656
            }
9657
            $table->align[] = 'left';
9658
            $table->align[] = 'right';
9659
            $table->align[] = 'right';
9660
            $table->align[] = 'right';
9661
            $table->align[] = 'right';
9662
            $table->head[] = 'H';
9663
            $table->head[] = 'M';
9664
            $table->head[] = 'S';
9665
            $table->head[] = 'I/O';
9666
        }
9667
 
9668
        ksort($stats);
9669
 
9670
        foreach ($stats as $definition => $details) {
9671
            switch ($details['mode']) {
9672
                case cache_store::MODE_APPLICATION:
9673
                    $modeclass = 'application';
9674
                    $mode = ' <span title="application cache">App</span>';
9675
                    break;
9676
                case cache_store::MODE_SESSION:
9677
                    $modeclass = 'session';
9678
                    $mode = ' <span title="session cache">Ses</span>';
9679
                    break;
9680
                case cache_store::MODE_REQUEST:
9681
                    $modeclass = 'request';
9682
                    $mode = ' <span title="request cache">Req</span>';
9683
                    break;
9684
            }
9685
            $row = [$mode, $definition];
9686
 
9687
            $text .= "$definition {";
9688
 
9689
            $storec = 0;
9690
            foreach ($details['stores'] as $store => $data) {
9691
 
9692
                if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9693
                    $row[] = '';
9694
                    $row[] = '';
9695
                    $row[] = '';
9696
                    $storec++;
9697
                }
9698
 
9699
                $hits   += $data['hits'];
9700
                $misses += $data['misses'];
9701
                $sets   += $data['sets'];
9702
                if ($data['hits'] == 0 and $data['misses'] > 0) {
9703
                    $cachestoreclass = 'nohits bg-danger';
9704
                } else if ($data['hits'] < $data['misses']) {
9705
                    $cachestoreclass = 'lowhits bg-warning text-dark';
9706
                } else {
9707
                    $cachestoreclass = 'hihits';
9708
                }
9709
                $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9710
                $cell = new html_table_cell($store);
9711
                $cell->attributes = ['class' => $cachestoreclass];
9712
                $row[] = $cell;
9713
                $cell = new html_table_cell($data['hits']);
9714
                $cell->attributes = ['class' => $cachestoreclass];
9715
                $row[] = $cell;
9716
                $cell = new html_table_cell($data['misses']);
9717
                $cell->attributes = ['class' => $cachestoreclass];
9718
                $row[] = $cell;
9719
 
9720
                if ($store !== cache_store::STATIC_ACCEL) {
9721
                    // The static cache is never set.
9722
                    $cell = new html_table_cell($data['sets']);
9723
                    $cell->attributes = ['class' => $cachestoreclass];
9724
                    $row[] = $cell;
9725
 
9726
                    if ($data['hits'] || $data['sets']) {
9727
                        if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
9728
                            $size = '-';
9729
                        } else {
9730
                            $size = display_size($data['iobytes'], 1, 'KB');
9731
                            if ($data['iobytes'] >= 10 * 1024) {
9732
                                $cachestoreclass = ' bg-warning text-dark';
9733
                            }
9734
                        }
9735
                    } else {
9736
                        $size = '';
9737
                    }
9738
                    $cell = new html_table_cell($size);
9739
                    $cell->attributes = ['class' => $cachestoreclass];
9740
                    $row[] = $cell;
9741
                }
9742
                $storec++;
9743
            }
9744
            while ($storec++ < $maxstores) {
9745
                $row[] = '';
9746
                $row[] = '';
9747
                $row[] = '';
9748
                $row[] = '';
9749
                $row[] = '';
9750
            }
9751
            $text .= '} ';
9752
 
9753
            $table->data[] = $row;
9754
        }
9755
 
9756
        $html .= html_writer::table($table);
9757
 
9758
        // Now lets also show sub totals for each cache store.
9759
        $storetotals = [];
9760
        $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9761
        foreach ($stats as $definition => $details) {
9762
            foreach ($details['stores'] as $store => $data) {
9763
                if (!array_key_exists($store, $storetotals)) {
9764
                    $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9765
                }
9766
                $storetotals[$store]['class']   = $data['class'];
9767
                $storetotals[$store]['hits']   += $data['hits'];
9768
                $storetotals[$store]['misses'] += $data['misses'];
9769
                $storetotals[$store]['sets']   += $data['sets'];
9770
                $storetotal['hits']   += $data['hits'];
9771
                $storetotal['misses'] += $data['misses'];
9772
                $storetotal['sets']   += $data['sets'];
9773
                if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) {
9774
                    $storetotals[$store]['iobytes'] += $data['iobytes'];
9775
                    $storetotal['iobytes'] += $data['iobytes'];
9776
                }
9777
            }
9778
        }
9779
 
9780
        $table = new html_table();
9781
        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9782
        $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9783
        $table->data = [];
9784
        $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
9785
 
9786
        ksort($storetotals);
9787
 
9788
        foreach ($storetotals as $store => $data) {
9789
            $row = [];
9790
            if ($data['hits'] == 0 and $data['misses'] > 0) {
9791
                $cachestoreclass = 'nohits bg-danger';
9792
            } else if ($data['hits'] < $data['misses']) {
9793
                $cachestoreclass = 'lowhits bg-warning text-dark';
9794
            } else {
9795
                $cachestoreclass = 'hihits';
9796
            }
9797
            $cell = new html_table_cell($store);
9798
            $cell->attributes = ['class' => $cachestoreclass];
9799
            $row[] = $cell;
9800
            $cell = new html_table_cell($data['class']);
9801
            $cell->attributes = ['class' => $cachestoreclass];
9802
            $row[] = $cell;
9803
            $cell = new html_table_cell($data['hits']);
9804
            $cell->attributes = ['class' => $cachestoreclass];
9805
            $row[] = $cell;
9806
            $cell = new html_table_cell($data['misses']);
9807
            $cell->attributes = ['class' => $cachestoreclass];
9808
            $row[] = $cell;
9809
            $cell = new html_table_cell($data['sets']);
9810
            $cell->attributes = ['class' => $cachestoreclass];
9811
            $row[] = $cell;
9812
            if ($data['hits'] || $data['sets']) {
9813
                if ($data['iobytes']) {
9814
                    $size = display_size($data['iobytes'], 1, 'KB');
9815
                } else {
9816
                    $size = '-';
9817
                }
9818
            } else {
9819
                $size = '';
9820
            }
9821
            $cell = new html_table_cell($size);
9822
            $cell->attributes = ['class' => $cachestoreclass];
9823
            $row[] = $cell;
9824
            $table->data[] = $row;
9825
        }
9826
        if (!empty($storetotal['iobytes'])) {
9827
            $size = display_size($storetotal['iobytes'], 1, 'KB');
9828
        } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) {
9829
            $size = '-';
9830
        } else {
9831
            $size = '';
9832
        }
9833
        $row = [
9834
            get_string('total'),
9835
            '',
9836
            $storetotal['hits'],
9837
            $storetotal['misses'],
9838
            $storetotal['sets'],
9839
            $size,
9840
        ];
9841
        $table->data[] = $row;
9842
 
9843
        $html .= html_writer::table($table);
9844
 
9845
        $info['cachesused'] = "$hits / $misses / $sets";
9846
        $info['html'] .= $html;
1326 ariadna 9847
        $info['txt'] .= $text . '. ';
1 efrain 9848
    } else {
9849
        $info['cachesused'] = '0 / 0 / 0';
9850
        $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9851
        $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9852
    }
9853
 
9854
    // Display lock information if any.
9855
    if (!empty($PERF->locks)) {
9856
        $table = new html_table();
9857
        $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered';
9858
        $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)'];
9859
        $table->align = ['left', 'right', 'center', 'right'];
9860
        $table->data = [];
9861
        $text = 'Locks (waited/obtained/held):';
9862
        foreach ($PERF->locks as $locktiming) {
9863
            $row = [];
9864
            $row[] = s($locktiming->type . '/' . $locktiming->resource);
9865
            $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' (';
9866
 
9867
            // The time we had to wait to get the lock.
9868
            $roundedtime = number_format($locktiming->wait, 1);
9869
            $cell = new html_table_cell($roundedtime);
9870
            if ($locktiming->wait > 0.5) {
9871
                $cell->attributes = ['class' => 'bg-warning text-dark'];
9872
            }
9873
            $row[] = $cell;
9874
            $text .= $roundedtime . '/';
9875
 
9876
            // Show a tick or cross for success.
9877
            $row[] = $locktiming->success ? '&#x2713;' : '&#x274c;';
9878
            $text .= ($locktiming->success ? 'y' : 'n') . '/';
9879
 
9880
            // If applicable, show how long we held the lock before releasing it.
9881
            if (property_exists($locktiming, 'held')) {
9882
                $roundedtime = number_format($locktiming->held, 1);
9883
                $cell = new html_table_cell($roundedtime);
9884
                if ($locktiming->held > 0.5) {
9885
                    $cell->attributes = ['class' => 'bg-warning text-dark'];
9886
                }
9887
                $row[] = $cell;
9888
                $text .= $roundedtime;
9889
            } else {
9890
                $row[] = '-';
9891
                $text .= '-';
9892
            }
9893
            $text .= ')';
9894
 
9895
            $table->data[] = $row;
9896
        }
9897
        $info['html'] .= html_writer::table($table);
9898
        $info['txt'] .= $text . '. ';
9899
    }
9900
 
1326 ariadna 9901
    $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">' . $info['html'] . '</div>';
1 efrain 9902
    return $info;
9903
}
9904
 
9905
/**
9906
 * Renames a file or directory to a unique name within the same directory.
9907
 *
9908
 * This function is designed to avoid any potential race conditions, and select an unused name.
9909
 *
9910
 * @param string $filepath Original filepath
9911
 * @param string $prefix Prefix to use for the temporary name
9912
 * @return string|bool New file path or false if failed
9913
 * @since Moodle 3.10
9914
 */
1326 ariadna 9915
function rename_to_unused_name(string $filepath, string $prefix = '_temp_')
9916
{
1 efrain 9917
    $dir = dirname($filepath);
9918
    $basename = $dir . '/' . $prefix;
9919
    $limit = 0;
9920
    while ($limit < 100) {
9921
        // Select a new name based on a random number.
9922
        $newfilepath = $basename . md5(mt_rand());
9923
 
9924
        // Attempt a rename to that new name.
9925
        if (@rename($filepath, $newfilepath)) {
9926
            return $newfilepath;
9927
        }
9928
 
9929
        // The first time, do some sanity checks, maybe it is failing for a good reason and there
9930
        // is no point trying 100 times if so.
9931
        if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
9932
            return false;
9933
        }
9934
        $limit++;
9935
    }
9936
    return false;
9937
}
9938
 
9939
/**
9940
 * Delete directory or only its content
9941
 *
9942
 * @param string $dir directory path
9943
 * @param bool $contentonly
9944
 * @return bool success, true also if dir does not exist
9945
 */
1326 ariadna 9946
function remove_dir($dir, $contentonly = false)
9947
{
1 efrain 9948
    if (!is_dir($dir)) {
9949
        // Nothing to do.
9950
        return true;
9951
    }
9952
 
9953
    if (!$contentonly) {
9954
        // Start by renaming the directory; this will guarantee that other processes don't write to it
9955
        // while it is in the process of being deleted.
9956
        $tempdir = rename_to_unused_name($dir);
9957
        if ($tempdir) {
9958
            // If the rename was successful then delete the $tempdir instead.
9959
            $dir = $tempdir;
9960
        }
9961
        // If the rename fails, we will continue through and attempt to delete the directory
9962
        // without renaming it since that is likely to at least delete most of the files.
9963
    }
9964
 
9965
    if (!$handle = opendir($dir)) {
9966
        return false;
9967
    }
9968
    $result = true;
1326 ariadna 9969
    while (false !== ($item = readdir($handle))) {
1 efrain 9970
        if ($item != '.' && $item != '..') {
1326 ariadna 9971
            if (is_dir($dir . '/' . $item)) {
9972
                $result = remove_dir($dir . '/' . $item) && $result;
1 efrain 9973
            } else {
1326 ariadna 9974
                $result = unlink($dir . '/' . $item) && $result;
1 efrain 9975
            }
9976
        }
9977
    }
9978
    closedir($handle);
9979
    if ($contentonly) {
9980
        clearstatcache(); // Make sure file stat cache is properly invalidated.
9981
        return $result;
9982
    }
9983
    $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9984
    clearstatcache(); // Make sure file stat cache is properly invalidated.
9985
    return $result;
9986
}
9987
 
9988
/**
9989
 * Detect if an object or a class contains a given property
9990
 * will take an actual object or the name of a class
9991
 *
9992
 * @param mixed $obj Name of class or real object to test
9993
 * @param string $property name of property to find
9994
 * @return bool true if property exists
9995
 */
1326 ariadna 9996
function object_property_exists($obj, $property)
9997
{
9998
    if (is_string($obj)) {
9999
        $properties = get_class_vars($obj);
1 efrain 10000
    } else {
1326 ariadna 10001
        $properties = get_object_vars($obj);
1 efrain 10002
    }
1326 ariadna 10003
    return array_key_exists($property, $properties);
1 efrain 10004
}
10005
 
10006
/**
10007
 * Converts an object into an associative array
10008
 *
10009
 * This function converts an object into an associative array by iterating
10010
 * over its public properties. Because this function uses the foreach
10011
 * construct, Iterators are respected. It works recursively on arrays of objects.
10012
 * Arrays and simple values are returned as is.
10013
 *
10014
 * If class has magic properties, it can implement IteratorAggregate
10015
 * and return all available properties in getIterator()
10016
 *
10017
 * @param mixed $var
10018
 * @return array
10019
 */
1326 ariadna 10020
function convert_to_array($var)
10021
{
1 efrain 10022
    $result = array();
10023
 
10024
    // Loop over elements/properties.
10025
    foreach ($var as $key => $value) {
10026
        // Recursively convert objects.
10027
        if (is_object($value) || is_array($value)) {
10028
            $result[$key] = convert_to_array($value);
10029
        } else {
10030
            // Simple values are untouched.
10031
            $result[$key] = $value;
10032
        }
10033
    }
10034
    return $result;
10035
}
10036
 
10037
/**
10038
 * Detect a custom script replacement in the data directory that will
10039
 * replace an existing moodle script
10040
 *
10041
 * @return string|bool full path name if a custom script exists, false if no custom script exists
10042
 */
1326 ariadna 10043
function custom_script_path()
10044
{
1 efrain 10045
    global $CFG, $SCRIPT;
10046
 
10047
    if ($SCRIPT === null) {
10048
        // Probably some weird external script.
10049
        return false;
10050
    }
10051
 
10052
    $scriptpath = $CFG->customscripts . $SCRIPT;
10053
 
10054
    // Check the custom script exists.
10055
    if (file_exists($scriptpath) and is_file($scriptpath)) {
10056
        return $scriptpath;
10057
    } else {
10058
        return false;
10059
    }
10060
}
10061
 
10062
/**
10063
 * Returns whether or not the user object is a remote MNET user. This function
10064
 * is in moodlelib because it does not rely on loading any of the MNET code.
10065
 *
10066
 * @param object $user A valid user object
10067
 * @return bool        True if the user is from a remote Moodle.
10068
 */
1326 ariadna 10069
function is_mnet_remote_user($user)
10070
{
1 efrain 10071
    global $CFG;
10072
 
10073
    if (!isset($CFG->mnet_localhost_id)) {
10074
        include_once($CFG->dirroot . '/mnet/lib.php');
10075
        $env = new mnet_environment();
10076
        $env->init();
10077
        unset($env);
10078
    }
10079
 
10080
    return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10081
}
10082
 
10083
/**
10084
 * This function will search for browser prefereed languages, setting Moodle
10085
 * to use the best one available if $SESSION->lang is undefined
10086
 */
1326 ariadna 10087
function setup_lang_from_browser()
10088
{
1 efrain 10089
    global $CFG, $SESSION, $USER;
10090
 
10091
    if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10092
        // Lang is defined in session or user profile, nothing to do.
10093
        return;
10094
    }
10095
 
10096
    if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
10097
        return;
10098
    }
10099
 
10100
    // Extract and clean langs from headers.
10101
    $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10102
    $rawlangs = str_replace('-', '_', $rawlangs);         // We are using underscores.
10103
    $rawlangs = explode(',', $rawlangs);                  // Convert to array.
10104
    $langs = array();
10105
 
10106
    $order = 1.0;
10107
    foreach ($rawlangs as $lang) {
10108
        if (strpos($lang, ';') === false) {
10109
            $langs[(string)$order] = $lang;
1326 ariadna 10110
            $order = $order - 0.01;
1 efrain 10111
        } else {
10112
            $parts = explode(';', $lang);
10113
            $pos = strpos($parts[1], '=');
1326 ariadna 10114
            $langs[substr($parts[1], $pos + 1)] = $parts[0];
1 efrain 10115
        }
10116
    }
10117
    krsort($langs, SORT_NUMERIC);
10118
 
10119
    // Look for such langs under standard locations.
10120
    foreach ($langs as $lang) {
10121
        // Clean it properly for include.
10122
        $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
10123
        if (get_string_manager()->translation_exists($lang, false)) {
10124
            // If the translation for this language exists then try to set it
10125
            // for the rest of the session, if this is a read only session then
10126
            // we can only set it temporarily in $CFG.
10127
            if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) {
10128
                $CFG->lang = $lang;
10129
            } else {
10130
                $SESSION->lang = $lang;
10131
            }
10132
            // We have finished. Go out.
10133
            break;
10134
        }
10135
    }
10136
    return;
10137
}
10138
 
10139
/**
10140
 * Check if $url matches anything in proxybypass list
10141
 *
10142
 * Any errors just result in the proxy being used (least bad)
10143
 *
10144
 * @param string $url url to check
10145
 * @return boolean true if we should bypass the proxy
10146
 */
1326 ariadna 10147
function is_proxybypass($url)
10148
{
1 efrain 10149
    global $CFG;
10150
 
10151
    // Sanity check.
10152
    if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10153
        return false;
10154
    }
10155
 
10156
    // Get the host part out of the url.
1326 ariadna 10157
    if (!$host = parse_url($url, PHP_URL_HOST)) {
1 efrain 10158
        return false;
10159
    }
10160
 
10161
    // Get the possible bypass hosts into an array.
1326 ariadna 10162
    $matches = explode(',', $CFG->proxybypass);
1 efrain 10163
 
10164
    // Check for a exact match on the IP or in the domains.
10165
    $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches);
10166
    $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ',');
10167
 
10168
    if ($isdomaininallowedlist || $isipinsubnetlist) {
10169
        return true;
10170
    }
10171
 
10172
    // Nothing matched.
10173
    return false;
10174
}
10175
 
10176
/**
10177
 * Check if the passed navigation is of the new style
10178
 *
10179
 * @param mixed $navigation
10180
 * @return bool true for yes false for no
10181
 */
1326 ariadna 10182
function is_newnav($navigation)
10183
{
1 efrain 10184
    if (is_array($navigation) && !empty($navigation['newnav'])) {
10185
        return true;
10186
    } else {
10187
        return false;
10188
    }
10189
}
10190
 
10191
/**
10192
 * Checks whether the given variable name is defined as a variable within the given object.
10193
 *
10194
 * This will NOT work with stdClass objects, which have no class variables.
10195
 *
10196
 * @param string $var The variable name
10197
 * @param object $object The object to check
10198
 * @return boolean
10199
 */
1326 ariadna 10200
function in_object_vars($var, $object)
10201
{
1 efrain 10202
    $classvars = get_class_vars(get_class($object));
10203
    $classvars = array_keys($classvars);
10204
    return in_array($var, $classvars);
10205
}
10206
 
10207
/**
10208
 * Returns an array without repeated objects.
10209
 * This function is similar to array_unique, but for arrays that have objects as values
10210
 *
10211
 * @param array $array
10212
 * @param bool $keepkeyassoc
10213
 * @return array
10214
 */
1326 ariadna 10215
function object_array_unique($array, $keepkeyassoc = true)
10216
{
1 efrain 10217
    $duplicatekeys = array();
10218
    $tmp         = array();
10219
 
10220
    foreach ($array as $key => $val) {
10221
        // Convert objects to arrays, in_array() does not support objects.
10222
        if (is_object($val)) {
10223
            $val = (array)$val;
10224
        }
10225
 
10226
        if (!in_array($val, $tmp)) {
10227
            $tmp[] = $val;
10228
        } else {
10229
            $duplicatekeys[] = $key;
10230
        }
10231
    }
10232
 
10233
    foreach ($duplicatekeys as $key) {
10234
        unset($array[$key]);
10235
    }
10236
 
10237
    return $keepkeyassoc ? $array : array_values($array);
10238
}
10239
 
10240
/**
10241
 * Is a userid the primary administrator?
10242
 *
10243
 * @param int $userid int id of user to check
10244
 * @return boolean
10245
 */
1326 ariadna 10246
function is_primary_admin($userid)
10247
{
1 efrain 10248
    $primaryadmin =  get_admin();
10249
 
10250
    if ($userid == $primaryadmin->id) {
10251
        return true;
10252
    } else {
10253
        return false;
10254
    }
10255
}
10256
 
10257
/**
10258
 * Returns the site identifier
10259
 *
10260
 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10261
 */
1326 ariadna 10262
function get_site_identifier()
10263
{
1 efrain 10264
    global $CFG;
10265
    // Check to see if it is missing. If so, initialise it.
10266
    if (empty($CFG->siteidentifier)) {
10267
        set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10268
    }
10269
    // Return it.
10270
    return $CFG->siteidentifier;
10271
}
10272
 
10273
/**
10274
 * Check whether the given password has no more than the specified
10275
 * number of consecutive identical characters.
10276
 *
10277
 * @param string $password   password to be checked against the password policy
10278
 * @param integer $maxchars  maximum number of consecutive identical characters
10279
 * @return bool
10280
 */
1326 ariadna 10281
function check_consecutive_identical_characters($password, $maxchars)
10282
{
1 efrain 10283
 
10284
    if ($maxchars < 1) {
10285
        return true; // Zero 0 is to disable this check.
10286
    }
10287
    if (strlen($password) <= $maxchars) {
10288
        return true; // Too short to fail this test.
10289
    }
10290
 
10291
    $previouschar = '';
10292
    $consecutivecount = 1;
10293
    foreach (str_split($password) as $char) {
10294
        if ($char != $previouschar) {
10295
            $consecutivecount = 1;
10296
        } else {
10297
            $consecutivecount++;
10298
            if ($consecutivecount > $maxchars) {
10299
                return false; // Check failed already.
10300
            }
10301
        }
10302
 
10303
        $previouschar = $char;
10304
    }
10305
 
10306
    return true;
10307
}
10308
 
10309
/**
10310
 * Helper function to do partial function binding.
10311
 * so we can use it for preg_replace_callback, for example
10312
 * this works with php functions, user functions, static methods and class methods
10313
 * it returns you a callback that you can pass on like so:
10314
 *
10315
 * $callback = partial('somefunction', $arg1, $arg2);
10316
 *     or
10317
 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10318
 *     or even
10319
 * $obj = new someclass();
10320
 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10321
 *
10322
 * and then the arguments that are passed through at calltime are appended to the argument list.
10323
 *
10324
 * @param mixed $function a php callback
10325
 * @param mixed $arg1,... $argv arguments to partially bind with
10326
 * @return array Array callback
10327
 */
1326 ariadna 10328
function partial()
10329
{
1 efrain 10330
    if (!class_exists('partial')) {
10331
        /**
10332
         * Used to manage function binding.
10333
         * @copyright  2009 Penny Leach
10334
         * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10335
         */
1326 ariadna 10336
        class partial
10337
        {
1 efrain 10338
            /** @var array */
10339
            public $values = array();
10340
            /** @var string The function to call as a callback. */
10341
            public $func;
10342
            /**
10343
             * Constructor
10344
             * @param string $func
10345
             * @param array $args
10346
             */
1326 ariadna 10347
            public function __construct($func, $args)
10348
            {
1 efrain 10349
                $this->values = $args;
10350
                $this->func = $func;
10351
            }
10352
            /**
10353
             * Calls the callback function.
10354
             * @return mixed
10355
             */
1326 ariadna 10356
            public function method()
10357
            {
1 efrain 10358
                $args = func_get_args();
10359
                return call_user_func_array($this->func, array_merge($this->values, $args));
10360
            }
10361
        }
10362
    }
10363
    $args = func_get_args();
10364
    $func = array_shift($args);
10365
    $p = new partial($func, $args);
10366
    return array($p, 'method');
10367
}
10368
 
10369
/**
10370
 * helper function to load up and initialise the mnet environment
10371
 * this must be called before you use mnet functions.
10372
 *
10373
 * @return mnet_environment the equivalent of old $MNET global
10374
 */
1326 ariadna 10375
function get_mnet_environment()
10376
{
1 efrain 10377
    global $CFG;
10378
    require_once($CFG->dirroot . '/mnet/lib.php');
10379
    static $instance = null;
10380
    if (empty($instance)) {
10381
        $instance = new mnet_environment();
10382
        $instance->init();
10383
    }
10384
    return $instance;
10385
}
10386
 
10387
/**
10388
 * during xmlrpc server code execution, any code wishing to access
10389
 * information about the remote peer must use this to get it.
10390
 *
10391
 * @return mnet_remote_client|false the equivalent of old $MNETREMOTE_CLIENT global
10392
 */
1326 ariadna 10393
function get_mnet_remote_client()
10394
{
1 efrain 10395
    if (!defined('MNET_SERVER')) {
10396
        debugging(get_string('notinxmlrpcserver', 'mnet'));
10397
        return false;
10398
    }
10399
    global $MNET_REMOTE_CLIENT;
10400
    if (isset($MNET_REMOTE_CLIENT)) {
10401
        return $MNET_REMOTE_CLIENT;
10402
    }
10403
    return false;
10404
}
10405
 
10406
/**
10407
 * during the xmlrpc server code execution, this will be called
10408
 * to setup the object returned by {@link get_mnet_remote_client}
10409
 *
10410
 * @param mnet_remote_client $client the client to set up
10411
 * @throws moodle_exception
10412
 */
1326 ariadna 10413
function set_mnet_remote_client($client)
10414
{
1 efrain 10415
    if (!defined('MNET_SERVER')) {
10416
        throw new moodle_exception('notinxmlrpcserver', 'mnet');
10417
    }
10418
    global $MNET_REMOTE_CLIENT;
10419
    $MNET_REMOTE_CLIENT = $client;
10420
}
10421
 
10422
/**
10423
 * return the jump url for a given remote user
10424
 * this is used for rewriting forum post links in emails, etc
10425
 *
10426
 * @param stdclass $user the user to get the idp url for
10427
 */
1326 ariadna 10428
function mnet_get_idp_jump_url($user)
10429
{
1 efrain 10430
    global $CFG;
10431
 
10432
    static $mnetjumps = array();
10433
    if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10434
        $idp = mnet_get_peer_host($user->mnethostid);
10435
        $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10436
        $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10437
    }
10438
    return $mnetjumps[$user->mnethostid];
10439
}
10440
 
10441
/**
10442
 * Gets the homepage to use for the current user
10443
 *
10444
 * @return int One of HOMEPAGE_*
10445
 */
1326 ariadna 10446
function get_home_page()
10447
{
1 efrain 10448
    global $CFG;
10449
 
10450
    if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10451
        // If dashboard is disabled, home will be set to default page.
10452
        $defaultpage = get_default_home_page();
10453
        if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10454
            if (!empty($CFG->enabledashboard)) {
10455
                return HOMEPAGE_MY;
10456
            } else {
10457
                return $defaultpage;
10458
            }
10459
        } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) {
10460
            return HOMEPAGE_MYCOURSES;
10461
        } else {
10462
            $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage);
10463
            if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) {
10464
                // If the user was using the dashboard but it's disabled, return the default home page.
10465
                $userhomepage = $defaultpage;
10466
            }
10467
            return $userhomepage;
10468
        }
10469
    }
10470
    return HOMEPAGE_SITE;
10471
}
10472
 
10473
/**
10474
 * Returns the default home page to display if current one is not defined or can't be applied.
10475
 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
10476
 *
10477
 * @return int The default home page.
10478
 */
1326 ariadna 10479
function get_default_home_page(): int
10480
{
1 efrain 10481
    global $CFG;
10482
 
10483
    return (!isset($CFG->enabledashboard) || $CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES;
10484
}
10485
 
10486
/**
10487
 * Gets the name of a course to be displayed when showing a list of courses.
10488
 * By default this is just $course->fullname but user can configure it. The
10489
 * result of this function should be passed through print_string.
10490
 * @param stdClass|core_course_list_element $course Moodle course object
10491
 * @return string Display name of course (either fullname or short + fullname)
10492
 */
1326 ariadna 10493
function get_course_display_name_for_list($course)
10494
{
1 efrain 10495
    global $CFG;
10496
    if (!empty($CFG->courselistshortnames)) {
10497
        if (!($course instanceof stdClass)) {
10498
            $course = (object)convert_to_array($course);
10499
        }
10500
        return get_string('courseextendednamedisplay', '', $course);
10501
    } else {
10502
        return $course->fullname;
10503
    }
10504
}
10505
 
10506
/**
10507
 * Safe analogue of unserialize() that can only parse arrays
10508
 *
10509
 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10510
 *
10511
 * @param string $expression
10512
 * @return array|bool either parsed array or false if parsing was impossible.
10513
 */
1326 ariadna 10514
function unserialize_array($expression)
10515
{
1 efrain 10516
 
10517
    // Check the expression is an array.
10518
    if (!preg_match('/^a:(\d+):/', $expression)) {
10519
        return false;
10520
    }
10521
 
10522
    $values = (array) unserialize_object($expression);
10523
 
10524
    // Callback that returns true if the given value is an unserialized object, executes recursively.
1326 ariadna 10525
    $invalidvaluecallback = static function ($value) use (&$invalidvaluecallback): bool {
1 efrain 10526
        if (is_array($value)) {
10527
            return (bool) array_filter($value, $invalidvaluecallback);
10528
        }
10529
        return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class);
10530
    };
10531
 
10532
    // Iterate over the result to ensure there are no stray objects.
10533
    if (array_filter($values, $invalidvaluecallback)) {
10534
        return false;
10535
    }
10536
 
10537
    return $values;
10538
}
10539
 
10540
/**
10541
 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10542
 *
10543
 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10544
 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10545
 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10546
 *
10547
 * @param string $input
10548
 * @return stdClass
10549
 */
1326 ariadna 10550
function unserialize_object(string $input): stdClass
10551
{
1 efrain 10552
    $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10553
    return (object) $instance;
10554
}
10555
 
10556
/**
10557
 * The lang_string class
10558
 *
10559
 * This special class is used to create an object representation of a string request.
10560
 * It is special because processing doesn't occur until the object is first used.
10561
 * The class was created especially to aid performance in areas where strings were
10562
 * required to be generated but were not necessarily used.
10563
 * As an example the admin tree when generated uses over 1500 strings, of which
10564
 * normally only 1/3 are ever actually printed at any time.
10565
 * The performance advantage is achieved by not actually processing strings that
10566
 * arn't being used, as such reducing the processing required for the page.
10567
 *
10568
 * How to use the lang_string class?
10569
 *     There are two methods of using the lang_string class, first through the
10570
 *     forth argument of the get_string function, and secondly directly.
10571
 *     The following are examples of both.
10572
 * 1. Through get_string calls e.g.
10573
 *     $string = get_string($identifier, $component, $a, true);
10574
 *     $string = get_string('yes', 'moodle', null, true);
10575
 * 2. Direct instantiation
10576
 *     $string = new lang_string($identifier, $component, $a, $lang);
10577
 *     $string = new lang_string('yes');
10578
 *
10579
 * How do I use a lang_string object?
10580
 *     The lang_string object makes use of a magic __toString method so that you
10581
 *     are able to use the object exactly as you would use a string in most cases.
10582
 *     This means you are able to collect it into a variable and then directly
10583
 *     echo it, or concatenate it into another string, or similar.
10584
 *     The other thing you can do is manually get the string by calling the
10585
 *     lang_strings out method e.g.
10586
 *         $string = new lang_string('yes');
10587
 *         $string->out();
10588
 *     Also worth noting is that the out method can take one argument, $lang which
10589
 *     allows the developer to change the language on the fly.
10590
 *
10591
 * When should I use a lang_string object?
10592
 *     The lang_string object is designed to be used in any situation where a
10593
 *     string may not be needed, but needs to be generated.
10594
 *     The admin tree is a good example of where lang_string objects should be
10595
 *     used.
10596
 *     A more practical example would be any class that requries strings that may
10597
 *     not be printed (after all classes get renderer by renderers and who knows
10598
 *     what they will do ;))
10599
 *
10600
 * When should I not use a lang_string object?
10601
 *     Don't use lang_strings when you are going to use a string immediately.
10602
 *     There is no need as it will be processed immediately and there will be no
10603
 *     advantage, and in fact perhaps a negative hit as a class has to be
10604
 *     instantiated for a lang_string object, however get_string won't require
10605
 *     that.
10606
 *
10607
 * Limitations:
10608
 * 1. You cannot use a lang_string object as an array offset. Doing so will
10609
 *     result in PHP throwing an error. (You can use it as an object property!)
10610
 *
10611
 * @package    core
10612
 * @category   string
10613
 * @copyright  2011 Sam Hemelryk
10614
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10615
 */
1326 ariadna 10616
class lang_string
10617
{
1 efrain 10618
 
10619
    /** @var string The strings identifier */
10620
    protected $identifier;
10621
    /** @var string The strings component. Default '' */
10622
    protected $component = '';
10623
    /** @var array|stdClass Any arguments required for the string. Default null */
10624
    protected $a = null;
10625
    /** @var string The language to use when processing the string. Default null */
10626
    protected $lang = null;
10627
 
10628
    /** @var string The processed string (once processed) */
10629
    protected $string = null;
10630
 
10631
    /**
10632
     * A special boolean. If set to true then the object has been woken up and
10633
     * cannot be regenerated. If this is set then $this->string MUST be used.
10634
     * @var bool
10635
     */
10636
    protected $forcedstring = false;
10637
 
10638
    /**
10639
     * Constructs a lang_string object
10640
     *
10641
     * This function should do as little processing as possible to ensure the best
10642
     * performance for strings that won't be used.
10643
     *
10644
     * @param string $identifier The strings identifier
10645
     * @param string $component The strings component
10646
     * @param stdClass|array|mixed $a Any arguments the string requires
10647
     * @param string $lang The language to use when processing the string.
10648
     * @throws coding_exception
10649
     */
1326 ariadna 10650
    public function __construct($identifier, $component = '', $a = null, $lang = null)
10651
    {
1 efrain 10652
        if (empty($component)) {
10653
            $component = 'moodle';
10654
        }
10655
 
10656
        $this->identifier = $identifier;
10657
        $this->component = $component;
10658
        $this->lang = $lang;
10659
 
10660
        // We MUST duplicate $a to ensure that it if it changes by reference those
10661
        // changes are not carried across.
10662
        // To do this we always ensure $a or its properties/values are strings
10663
        // and that any properties/values that arn't convertable are forgotten.
10664
        if ($a !== null) {
10665
            if (is_scalar($a)) {
10666
                $this->a = $a;
10667
            } else if ($a instanceof lang_string) {
10668
                $this->a = $a->out();
10669
            } else if (is_object($a) or is_array($a)) {
10670
                $a = (array)$a;
10671
                $this->a = array();
10672
                foreach ($a as $key => $value) {
10673
                    // Make sure conversion errors don't get displayed (results in '').
10674
                    if (is_array($value)) {
10675
                        $this->a[$key] = '';
10676
                    } else if (is_object($value)) {
10677
                        if (method_exists($value, '__toString')) {
10678
                            $this->a[$key] = $value->__toString();
10679
                        } else {
10680
                            $this->a[$key] = '';
10681
                        }
10682
                    } else {
10683
                        $this->a[$key] = (string)$value;
10684
                    }
10685
                }
10686
            }
10687
        }
10688
 
10689
        if (debugging(false, DEBUG_DEVELOPER)) {
10690
            if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10691
                throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10692
            }
10693
            if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10694
                throw new coding_exception('Invalid string compontent. Please check your string definition');
10695
            }
10696
            if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
1326 ariadna 10697
                debugging('String does not exist. Please check your string definition for ' . $this->identifier . '/' . $this->component, DEBUG_DEVELOPER);
1 efrain 10698
            }
10699
        }
10700
    }
10701
 
10702
    /**
10703
     * Processes the string.
10704
     *
10705
     * This function actually processes the string, stores it in the string property
10706
     * and then returns it.
10707
     * You will notice that this function is VERY similar to the get_string method.
10708
     * That is because it is pretty much doing the same thing.
10709
     * However as this function is an upgrade it isn't as tolerant to backwards
10710
     * compatibility.
10711
     *
10712
     * @return string
10713
     * @throws coding_exception
10714
     */
1326 ariadna 10715
    protected function get_string()
10716
    {
1 efrain 10717
        global $CFG;
10718
 
10719
        // Check if we need to process the string.
10720
        if ($this->string === null) {
10721
            // Check the quality of the identifier.
10722
            if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10723
                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);
10724
            }
10725
 
10726
            // Process the string.
10727
            $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10728
            // Debugging feature lets you display string identifier and component.
10729
            if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10730
                $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10731
            }
10732
        }
10733
        // Return the string.
10734
        return $this->string;
10735
    }
10736
 
10737
    /**
10738
     * Returns the string
10739
     *
10740
     * @param string $lang The langauge to use when processing the string
10741
     * @return string
10742
     */
1326 ariadna 10743
    public function out($lang = null)
10744
    {
1 efrain 10745
        if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10746
            if ($this->forcedstring) {
1326 ariadna 10747
                debugging('lang_string objects that have been used cannot be printed in another language. (' . $this->lang . ' used)', DEBUG_DEVELOPER);
1 efrain 10748
                return $this->get_string();
10749
            }
10750
            $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10751
            return $translatedstring->out();
10752
        }
10753
        return $this->get_string();
10754
    }
10755
 
10756
    /**
10757
     * Magic __toString method for printing a string
10758
     *
10759
     * @return string
10760
     */
1326 ariadna 10761
    public function __toString()
10762
    {
1 efrain 10763
        return $this->get_string();
10764
    }
10765
 
10766
    /**
10767
     * Magic __set_state method used for var_export
10768
     *
10769
     * @param array $array
10770
     * @return self
10771
     */
1326 ariadna 10772
    public static function __set_state(array $array): self
10773
    {
1 efrain 10774
        $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']);
10775
        $tmp->string = $array['string'];
10776
        $tmp->forcedstring = $array['forcedstring'];
10777
        return $tmp;
10778
    }
10779
 
10780
    /**
10781
     * Prepares the lang_string for sleep and stores only the forcedstring and
10782
     * string properties... the string cannot be regenerated so we need to ensure
10783
     * it is generated for this.
10784
     *
10785
     * @return array
10786
     */
1326 ariadna 10787
    public function __sleep()
10788
    {
1 efrain 10789
        $this->get_string();
10790
        $this->forcedstring = true;
10791
        return array('forcedstring', 'string', 'lang');
10792
    }
10793
 
10794
    /**
10795
     * Returns the identifier.
10796
     *
10797
     * @return string
10798
     */
1326 ariadna 10799
    public function get_identifier()
10800
    {
1 efrain 10801
        return $this->identifier;
10802
    }
10803
 
10804
    /**
10805
     * Returns the component.
10806
     *
10807
     * @return string
10808
     */
1326 ariadna 10809
    public function get_component()
10810
    {
1 efrain 10811
        return $this->component;
10812
    }
10813
}
10814
 
10815
/**
10816
 * Get human readable name describing the given callable.
10817
 *
10818
 * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10819
 * It does not check if the callable actually exists.
10820
 *
10821
 * @param callable|string|array $callable
10822
 * @return string|bool Human readable name of callable, or false if not a valid callable.
10823
 */
1326 ariadna 10824
function get_callable_name($callable)
10825
{
1 efrain 10826
 
10827
    if (!is_callable($callable, true, $name)) {
10828
        return false;
10829
    } else {
10830
        return $name;
10831
    }
10832
}
10833
 
10834
/**
10835
 * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10836
 * Never put your faith on this function and rely on its accuracy as there might be false positives.
10837
 * It just performs some simple checks, and mainly is used for places where we want to hide some options
10838
 * such as site registration when $CFG->wwwroot is not publicly accessible.
10839
 * Good thing is there is no false negative.
10840
 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10841
 *
10842
 * @return bool
10843
 */
1326 ariadna 10844
function site_is_public()
10845
{
1 efrain 10846
    global $CFG;
10847
 
10848
    // Return early if site admin has forced this setting.
10849
    if (isset($CFG->site_is_public)) {
10850
        return (bool)$CFG->site_is_public;
10851
    }
10852
 
10853
    $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10854
 
10855
    if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10856
        $ispublic = false;
10857
    } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10858
        $ispublic = false;
10859
    } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10860
        $ispublic = false;
10861
    } else {
10862
        $ispublic = true;
10863
    }
10864
 
10865
    return $ispublic;
10866
}
10867
 
10868
/**
10869
 * Validates user's password length.
10870
 *
10871
 * @param string $password
10872
 * @param int $pepperlength The length of the used peppers
10873
 * @return bool
10874
 */
1326 ariadna 10875
function exceeds_password_length(string $password, int $pepperlength = 0): bool
10876
{
1 efrain 10877
    return (strlen($password) > (MAX_PASSWORD_CHARACTERS + $pepperlength));
10878
}
10879
 
10880
/**
10881
 * A helper to replace PHP 8.3 usage of array_keys with two args.
10882
 *
10883
 * There is an indication that this will become a new method in PHP 8.4, but that has not happened yet.
10884
 * Therefore this non-polyfill has been created with a different naming convention.
10885
 * In the future it can be deprecated if a core PHP method is created.
10886
 *
10887
 * https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#array_keys
10888
 *
10889
 * @param array $array
10890
 * @param mixed $filter The value to filter on
10891
 * @param bool $strict Whether to apply a strit test with the filter
10892
 * @return array
10893
 */
1326 ariadna 10894
function moodle_array_keys_filter(array $array, mixed $filter, bool $strict = false): array
10895
{
1 efrain 10896
    return array_keys(array_filter(
10897
        $array,
1326 ariadna 10898
        function ($value, $key) use ($filter, $strict): bool {
1 efrain 10899
            if ($strict) {
10900
                return $value === $filter;
10901
            }
10902
            return $value == $filter;
10903
        },
10904
        ARRAY_FILTER_USE_BOTH,
10905
    ));
10906
}