Proyectos de Subversion Moodle

Rev

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