Proyectos de Subversion Moodle

Rev

Rev 1361 | | 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
 */
1441 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.
1441 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
 
1441 ariadna 392
/** The maximum length of a tag */
393
define('TAG_MAX_LENGTH', 255);
1 efrain 394
 
395
// Password policy constants.
1441 ariadna 396
define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
397
define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
398
define ('PASSWORD_DIGITS', '0123456789');
399
define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
1 efrain 400
 
401
/**
402
 * Required password pepper entropy.
403
 */
1441 ariadna 404
define ('PEPPER_ENTROPY', 112);
1 efrain 405
 
406
// Feature constants.
407
// Used for plugin_supports() to report features that are, or are not, supported by a module.
408
 
409
/** True if module can provide a grade */
410
define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
1441 ariadna 411
/** True if module can support grade penalty */
412
define('FEATURE_GRADE_HAS_PENALTY', 'grade_has_penalty');
1 efrain 413
/** True if module supports outcomes */
414
define('FEATURE_GRADE_OUTCOMES', 'outcomes');
415
/** True if module supports advanced grading methods */
416
define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
417
/** True if module controls the grade visibility over the gradebook */
418
define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
419
/** True if module supports plagiarism plugins */
420
define('FEATURE_PLAGIARISM', 'plagiarism');
421
 
1441 ariadna 422
/** True if module supports completion (true by default) */
423
define('FEATURE_COMPLETION', 'completion_enabled');
1 efrain 424
/** True if module has code to track whether somebody viewed it */
425
define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
426
/** True if module has custom completion rules */
427
define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
428
 
429
/** True if module has no 'view' page (like label) */
430
define('FEATURE_NO_VIEW_LINK', 'viewlink');
431
/** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
432
define('FEATURE_IDNUMBER', 'idnumber');
433
/** True if module supports groups */
434
define('FEATURE_GROUPS', 'groups');
435
/** True if module supports groupings */
436
define('FEATURE_GROUPINGS', 'groupings');
437
/**
438
 * True if module supports groupmembersonly (which no longer exists)
439
 * @deprecated Since Moodle 2.8
440
 */
441
define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
442
 
443
/** Type of module */
444
define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
445
/** True if module supports intro editor */
446
define('FEATURE_MOD_INTRO', 'mod_intro');
447
/** True if module has default completion */
448
define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
449
 
450
define('FEATURE_COMMENT', 'comment');
451
 
452
define('FEATURE_RATE', 'rate');
453
/** True if module supports backup/restore of moodle2 format */
454
define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
455
 
1441 ariadna 456
/** True if module shares questions with other modules. */
457
define('FEATURE_PUBLISHES_QUESTIONS', 'publishesquestions');
458
 
459
/** Used by {@see course_modinfo::is_mod_type_visible_on_course()} to determine if a plugin should render to display */
460
define('FEATURE_CAN_DISPLAY', 'candisplay');
461
 
462
/** Can this module type be uninstalled */
463
define('FEATURE_CAN_UNINSTALL', 'canuninstall');
464
 
1 efrain 465
/** True if module can show description on course main page */
466
define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
467
 
468
/** True if module uses the question bank */
469
define('FEATURE_USES_QUESTIONS', 'usesquestions');
470
 
471
/**
472
 * Maximum filename char size
473
 */
474
define('MAX_FILENAME_SIZE', 100);
475
 
476
/** Unspecified module archetype */
477
define('MOD_ARCHETYPE_OTHER', 0);
478
/** Resource-like type module */
479
define('MOD_ARCHETYPE_RESOURCE', 1);
480
/** Assignment module archetype */
481
define('MOD_ARCHETYPE_ASSIGNMENT', 2);
482
/** System (not user-addable) module archetype */
483
define('MOD_ARCHETYPE_SYSTEM', 3);
484
 
485
/** Type of module */
486
define('FEATURE_MOD_PURPOSE', 'mod_purpose');
487
/** Module purpose administration */
488
define('MOD_PURPOSE_ADMINISTRATION', 'administration');
489
/** Module purpose assessment */
490
define('MOD_PURPOSE_ASSESSMENT', 'assessment');
491
/** Module purpose communication */
492
define('MOD_PURPOSE_COLLABORATION', 'collaboration');
493
/** Module purpose communication */
494
define('MOD_PURPOSE_COMMUNICATION', 'communication');
495
/** Module purpose content */
496
define('MOD_PURPOSE_CONTENT', 'content');
497
/** Module purpose interactive content */
498
define('MOD_PURPOSE_INTERACTIVECONTENT', 'interactivecontent');
499
/** Module purpose other */
500
define('MOD_PURPOSE_OTHER', 'other');
501
/**
502
 * Module purpose interface
503
 * @deprecated since Moodle 4.4
504
 * @todo MDL-80701 Remove in Moodle 4.8
1441 ariadna 505
*/
1 efrain 506
define('MOD_PURPOSE_INTERFACE', 'interface');
507
 
1441 ariadna 508
/** True if module can be quickly created without filling a previous form. */
509
define('FEATURE_QUICKCREATE', 'quickcreate');
1 efrain 510
/**
511
 * Security token used for allowing access
512
 * from external application such as web services.
513
 * Scripts do not use any session, performance is relatively
514
 * low because we need to load access info in each request.
515
 * Scripts are executed in parallel.
516
 */
517
define('EXTERNAL_TOKEN_PERMANENT', 0);
518
 
519
/**
520
 * Security token used for allowing access
521
 * of embedded applications, the code is executed in the
522
 * active user session. Token is invalidated after user logs out.
523
 * Scripts are executed serially - normal session locking is used.
524
 */
525
define('EXTERNAL_TOKEN_EMBEDDED', 1);
526
 
527
/**
528
 * The home page should be the site home
529
 */
530
define('HOMEPAGE_SITE', 0);
531
/**
532
 * The home page should be the users my page
533
 */
534
define('HOMEPAGE_MY', 1);
535
/**
536
 * The home page can be chosen by the user
537
 */
538
define('HOMEPAGE_USER', 2);
539
/**
540
 * The home page should be the users my courses page
541
 */
542
define('HOMEPAGE_MYCOURSES', 3);
1441 ariadna 543
/**
544
 * The home page is defined as a URL
545
 */
546
define('HOMEPAGE_URL', 4);
1 efrain 547
 
548
/**
549
 * URL of the Moodle sites registration portal.
550
 */
551
defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
552
 
553
/**
554
 * URL of main Moodle site for marketing, products and services.
555
 */
556
defined('MOODLE_PRODUCTURL') || define('MOODLE_PRODUCTURL', 'https://moodle.com');
557
 
558
/**
559
 * URL of the statistic server public key.
560
 */
561
defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem');
562
 
563
/**
564
 * Moodle mobile app service name
565
 */
566
define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
567
 
568
/**
569
 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
570
 */
571
define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
572
 
573
/**
574
 * Course display settings: display all sections on one page.
575
 */
576
define('COURSE_DISPLAY_SINGLEPAGE', 0);
577
/**
578
 * Course display settings: split pages into a page per section.
579
 */
580
define('COURSE_DISPLAY_MULTIPAGE', 1);
581
 
582
/**
583
 * Authentication constant: String used in password field when password is not stored.
584
 */
585
define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
586
 
587
/**
588
 * Email from header to never include via information.
589
 */
590
define('EMAIL_VIA_NEVER', 0);
591
 
592
/**
593
 * Email from header to always include via information.
594
 */
595
define('EMAIL_VIA_ALWAYS', 1);
596
 
597
/**
598
 * Email from header to only include via information if the address is no-reply.
599
 */
600
define('EMAIL_VIA_NO_REPLY_ONLY', 2);
601
 
602
/**
603
 * Contact site support form/link disabled.
604
 */
605
define('CONTACT_SUPPORT_DISABLED', 0);
606
 
607
/**
608
 * Contact site support form/link only available to authenticated users.
609
 */
610
define('CONTACT_SUPPORT_AUTHENTICATED', 1);
611
 
612
/**
613
 * Contact site support form/link available to anyone visiting the site.
614
 */
615
define('CONTACT_SUPPORT_ANYONE', 2);
616
 
617
/**
618
 * Maximum number of characters for password.
619
 */
620
define('MAX_PASSWORD_CHARACTERS', 128);
621
 
622
/**
623
 * Toggle sensitive feature is disabled. Used for sensitive inputs (passwords, tokens, keys).
624
 */
625
define('TOGGLE_SENSITIVE_DISABLED', 0);
626
 
627
/**
628
 * Toggle sensitive feature is enabled. Used for sensitive inputs (passwords, tokens, keys).
629
 */
630
define('TOGGLE_SENSITIVE_ENABLED', 1);
631
 
632
/**
633
 * Toggle sensitive feature is enabled for small screens only. Used for sensitive inputs (passwords, tokens, keys).
634
 */
635
define('TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY', 2);
636
 
637
// PARAMETER HANDLING.
638
 
639
/**
640
 * Returns a particular value for the named variable, taken from
641
 * POST or GET.  If the parameter doesn't exist then an error is
642
 * thrown because we require this variable.
643
 *
644
 * This function should be used to initialise all required values
645
 * in a script that are based on parameters.  Usually it will be
646
 * used like this:
647
 *    $id = required_param('id', PARAM_INT);
648
 *
649
 * Please note the $type parameter is now required and the value can not be array.
650
 *
651
 * @param string $parname the name of the page parameter we want
652
 * @param string $type expected type of parameter
653
 * @return mixed
654
 * @throws coding_exception
655
 */
1441 ariadna 656
function required_param($parname, $type) {
1 efrain 657
    return \core\param::from_type($type)->required_param($parname);
658
}
659
 
660
/**
661
 * Returns a particular array value for the named variable, taken from
662
 * POST or GET.  If the parameter doesn't exist then an error is
663
 * thrown because we require this variable.
664
 *
665
 * This function should be used to initialise all required values
666
 * in a script that are based on parameters.  Usually it will be
667
 * used like this:
668
 *    $ids = required_param_array('ids', PARAM_INT);
669
 *
670
 *  Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
671
 *
672
 * @param string $parname the name of the page parameter we want
673
 * @param string $type expected type of parameter
674
 * @return array
675
 * @throws coding_exception
676
 */
1441 ariadna 677
function required_param_array($parname, $type) {
1 efrain 678
    return \core\param::from_type($type)->required_param_array($parname);
679
}
680
 
681
/**
682
 * Returns a particular value for the named variable, taken from
683
 * POST or GET, otherwise returning a given default.
684
 *
685
 * This function should be used to initialise all optional values
686
 * in a script that are based on parameters.  Usually it will be
687
 * used like this:
688
 *    $name = optional_param('name', 'Fred', PARAM_TEXT);
689
 *
690
 * Please note the $type parameter is now required and the value can not be array.
691
 *
692
 * @param string $parname the name of the page parameter we want
693
 * @param mixed  $default the default value to return if nothing is found
694
 * @param string $type expected type of parameter
695
 * @return mixed
696
 * @throws coding_exception
697
 */
1441 ariadna 698
function optional_param($parname, $default, $type) {
1 efrain 699
    return \core\param::from_type($type)->optional_param(
700
        paramname: $parname,
701
        default: $default,
702
    );
703
}
704
 
705
/**
706
 * Returns a particular array value for the named variable, taken from
707
 * POST or GET, otherwise returning a given default.
708
 *
709
 * This function should be used to initialise all optional values
710
 * in a script that are based on parameters.  Usually it will be
711
 * used like this:
712
 *    $ids = optional_param('id', array(), PARAM_INT);
713
 *
714
 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
715
 *
716
 * @param string $parname the name of the page parameter we want
717
 * @param mixed $default the default value to return if nothing is found
718
 * @param string $type expected type of parameter
719
 * @return array
720
 * @throws coding_exception
721
 */
1441 ariadna 722
function optional_param_array($parname, $default, $type) {
1 efrain 723
    return \core\param::from_type($type)->optional_param_array(
724
        paramname: $parname,
725
        default: $default,
726
    );
727
}
728
 
729
/**
730
 * Strict validation of parameter values, the values are only converted
731
 * to requested PHP type. Internally it is using clean_param, the values
732
 * before and after cleaning must be equal - otherwise
733
 * an invalid_parameter_exception is thrown.
734
 * Objects and classes are not accepted.
735
 *
736
 * @param mixed $param
737
 * @param string $type PARAM_ constant
738
 * @param bool $allownull are nulls valid value?
739
 * @param string $debuginfo optional debug information
740
 * @return mixed the $param value converted to PHP type
741
 * @throws invalid_parameter_exception if $param is not of given type
742
 */
1441 ariadna 743
function validate_param($param, $type, $allownull = NULL_NOT_ALLOWED, $debuginfo = '') {
1 efrain 744
    return \core\param::from_type($type)->validate_param(
745
        param: $param,
746
        allownull: $allownull,
747
        debuginfo: $debuginfo,
748
    );
749
}
750
 
751
/**
752
 * Makes sure array contains only the allowed types, this function does not validate array key names!
753
 *
754
 * <code>
755
 * $options = clean_param($options, PARAM_INT);
756
 * </code>
757
 *
758
 * @param array|null $param the variable array we are cleaning
759
 * @param string $type expected format of param after cleaning.
760
 * @param bool $recursive clean recursive arrays
761
 * @return array
762
 * @throws coding_exception
763
 */
1441 ariadna 764
function clean_param_array(?array $param, $type, $recursive = false) {
1 efrain 765
    return \core\param::from_type($type)->clean_param_array(
766
        param: $param,
767
        recursive: $recursive,
768
    );
769
}
770
 
771
/**
772
 * Used by {@link optional_param()} and {@link required_param()} to
773
 * clean the variables and/or cast to specific types, based on
774
 * an options field.
775
 * <code>
776
 * $course->format = clean_param($course->format, PARAM_ALPHA);
777
 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
778
 * </code>
779
 *
780
 * @param mixed $param the variable we are cleaning
781
 * @param string $type expected format of param after cleaning.
782
 * @return mixed
783
 * @throws coding_exception
784
 */
1441 ariadna 785
function clean_param($param, $type) {
1 efrain 786
    return \core\param::from_type($type)->clean($param);
787
}
788
 
789
/**
790
 * Whether the PARAM_* type is compatible in RTL.
791
 *
792
 * Being compatible with RTL means that the data they contain can flow
793
 * from right-to-left or left-to-right without compromising the user experience.
794
 *
795
 * Take URLs for example, they are not RTL compatible as they should always
796
 * flow from the left to the right. This also applies to numbers, email addresses,
797
 * configuration snippets, base64 strings, etc...
798
 *
799
 * This function tries to best guess which parameters can contain localised strings.
800
 *
801
 * @param string $paramtype Constant PARAM_*.
802
 * @return bool
803
 */
1441 ariadna 804
function is_rtl_compatible($paramtype) {
1 efrain 805
    return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
806
}
807
 
808
/**
809
 * Makes sure the data is using valid utf8, invalid characters are discarded.
810
 *
811
 * Note: this function is not intended for full objects with methods and private properties.
812
 *
813
 * @param mixed $value
814
 * @return mixed with proper utf-8 encoding
815
 */
1441 ariadna 816
function fix_utf8($value) {
1 efrain 817
    if (is_null($value) or $value === '') {
818
        return $value;
1441 ariadna 819
 
1 efrain 820
    } else if (is_string($value)) {
821
        if ((string)(int)$value === $value) {
822
            // Shortcut.
823
            return $value;
824
        }
825
 
826
        // Remove null bytes or invalid Unicode sequences from value.
827
        $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
828
 
829
        // Note: this duplicates min_fix_utf8() intentionally.
830
        static $buggyiconv = null;
831
        if ($buggyiconv === null) {
1441 ariadna 832
            $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1 efrain 833
        }
834
 
835
        if ($buggyiconv) {
836
            if (function_exists('mb_convert_encoding')) {
837
                $subst = mb_substitute_character();
838
                mb_substitute_character('none');
839
                $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
840
                mb_substitute_character($subst);
1441 ariadna 841
 
1 efrain 842
            } else {
843
                // Warn admins on admin/index.php page.
844
                $result = $value;
845
            }
1441 ariadna 846
 
1 efrain 847
        } else {
848
            $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
849
        }
850
 
851
        return $result;
1441 ariadna 852
 
1 efrain 853
    } else if (is_array($value)) {
854
        foreach ($value as $k => $v) {
855
            $value[$k] = fix_utf8($v);
856
        }
857
        return $value;
1441 ariadna 858
 
1 efrain 859
    } else if (is_object($value)) {
860
        // Do not modify original.
1441 ariadna 861
        $value = clone($value);
1 efrain 862
        foreach ($value as $k => $v) {
863
            $value->$k = fix_utf8($v);
864
        }
865
        return $value;
1441 ariadna 866
 
1 efrain 867
    } else {
868
        // This is some other type, no utf-8 here.
869
        return $value;
870
    }
871
}
872
 
873
/**
874
 * Return true if given value is integer or string with integer value
875
 *
876
 * @param mixed $value String or Int
877
 * @return bool true if number, false if not
878
 */
1441 ariadna 879
function is_number($value) {
1 efrain 880
    if (is_int($value)) {
881
        return true;
882
    } else if (is_string($value)) {
883
        return ((string)(int)$value) === $value;
884
    } else {
885
        return false;
886
    }
887
}
888
 
889
/**
890
 * Returns host part from url.
891
 *
892
 * @param string $url full url
893
 * @return string host, null if not found
894
 */
1441 ariadna 895
function get_host_from_url($url) {
1 efrain 896
    preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
897
    if ($matches) {
898
        return $matches[1];
899
    }
900
    return null;
901
}
902
 
903
/**
904
 * Tests whether anything was returned by text editor
905
 *
906
 * This function is useful for testing whether something you got back from
907
 * the HTML editor actually contains anything. Sometimes the HTML editor
908
 * appear to be empty, but actually you get back a <br> tag or something.
909
 *
910
 * @param string $string a string containing HTML.
911
 * @return boolean does the string contain any actual content - that is text,
912
 * images, objects, etc.
913
 */
1441 ariadna 914
function html_is_blank($string) {
1 efrain 915
    return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == '';
916
}
917
 
918
/**
919
 * Set a key in global configuration
920
 *
921
 * Set a key/value pair in both this session's {@link $CFG} global variable
922
 * and in the 'config' database table for future sessions.
923
 *
924
 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
925
 * In that case it doesn't affect $CFG.
926
 *
927
 * A NULL value will delete the entry.
928
 *
929
 * NOTE: this function is called from lib/db/upgrade.php
930
 *
931
 * @param string $name the key to set
932
 * @param string|int|bool|null $value the value to set (without magic quotes),
933
 *               null to unset the value
934
 * @param string $plugin (optional) the plugin scope, default null
1441 ariadna 935
 * @param boolean $log (optional) should this emit to the config log
1 efrain 936
 * @return bool true or exception
937
 */
1441 ariadna 938
function set_config($name, $value, $plugin = null, bool $log = false) {
1 efrain 939
    global $CFG, $DB;
940
 
941
    // Redirect to appropriate handler when value is null.
942
    if ($value === null) {
1441 ariadna 943
        return unset_config($name, $plugin, $log);
1 efrain 944
    }
945
 
1441 ariadna 946
    if ($log) {
947
        $prev = get_config($plugin, $name);
948
        if ($prev === false) {
949
            $prev = null;
950
        }
951
        add_to_config_log($name, $prev, $value, $plugin);
952
    }
953
 
1 efrain 954
    // Set variables determining conditions and where to store the new config.
955
    // Plugin config goes to {config_plugins}, core config goes to {config}.
956
    $iscore = empty($plugin);
957
    if ($iscore) {
958
        // If it's for core config.
959
        $table = 'config';
960
        $conditions = ['name' => $name];
961
        $invalidatecachekey = 'core';
962
    } else {
963
        // If it's a plugin.
964
        $table = 'config_plugins';
965
        $conditions = ['name' => $name, 'plugin' => $plugin];
966
        $invalidatecachekey = $plugin;
967
    }
968
 
969
    // DB handling - checks for existing config, updating or inserting only if necessary.
970
    $invalidatecache = true;
971
    $inserted = false;
972
    $record = $DB->get_record($table, $conditions, 'id, value');
973
    if ($record === false) {
974
        // Inserts a new config record.
975
        $config = new stdClass();
976
        $config->name  = $name;
977
        $config->value = $value;
978
        if (!$iscore) {
979
            $config->plugin = $plugin;
980
        }
981
        $inserted = $DB->insert_record($table, $config, false);
982
    } else if ($invalidatecache = ($record->value !== $value)) {
983
        // Record exists - Check and only set new value if it has changed.
984
        $DB->set_field($table, 'value', $value, ['id' => $record->id]);
985
    }
986
 
987
    if ($iscore && !isset($CFG->config_php_settings[$name])) {
988
        // So it's defined for this invocation at least.
989
        // Settings from db are always strings.
990
        $CFG->$name = (string) $value;
991
    }
992
 
993
    // When setting config during a Behat test (in the CLI script, not in the web browser
994
    // requests), remember which ones are set so that we can clear them later.
995
    if ($iscore && $inserted && defined('BEHAT_TEST')) {
996
        $CFG->behat_cli_added_config[$name] = true;
997
    }
998
 
999
    // Update siteidentifier cache, if required.
1000
    if ($iscore && $name === 'siteidentifier') {
1001
        cache_helper::update_site_identifier($value);
1002
    }
1003
 
1004
    // Invalidate cache, if required.
1005
    if ($invalidatecache) {
1006
        cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
1007
    }
1008
 
1009
    return true;
1010
}
1011
 
1012
/**
1013
 * Get configuration values from the global config table
1014
 * or the config_plugins table.
1015
 *
1016
 * If called with one parameter, it will load all the config
1017
 * variables for one plugin, and return them as an object.
1018
 *
1019
 * If called with 2 parameters it will return a string single
1020
 * value or false if the value is not found.
1021
 *
1022
 * NOTE: this function is called from lib/db/upgrade.php
1023
 *
1024
 * @param string $plugin full component name
1025
 * @param string $name default null
1026
 * @return mixed hash-like object or single value, return false no config found
1027
 * @throws dml_exception
1028
 */
1441 ariadna 1029
function get_config($plugin, $name = null) {
1 efrain 1030
    global $CFG, $DB;
1031
 
1032
    if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1441 ariadna 1033
        $forced =& $CFG->config_php_settings;
1 efrain 1034
        $iscore = true;
1035
        $plugin = 'core';
1036
    } else {
1037
        if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1441 ariadna 1038
            $forced =& $CFG->forced_plugin_settings[$plugin];
1 efrain 1039
        } else {
1040
            $forced = array();
1041
        }
1042
        $iscore = false;
1043
    }
1044
 
1045
    if (!isset($CFG->siteidentifier)) {
1046
        try {
1047
            // This may throw an exception during installation, which is how we detect the
1048
            // need to install the database. For more details see {@see initialise_cfg()}.
1049
            $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1050
        } catch (dml_exception $ex) {
1051
            // Set siteidentifier to false. We don't want to trip this continually.
1052
            $siteidentifier = false;
1053
            throw $ex;
1054
        }
1055
    }
1056
 
1057
    if (!empty($name)) {
1058
        if (array_key_exists($name, $forced)) {
1059
            return (string)$forced[$name];
1060
        } else if ($name === 'siteidentifier' && $plugin == 'core') {
1061
            return $CFG->siteidentifier;
1062
        }
1063
    }
1064
 
1065
    $cache = cache::make('core', 'config');
1066
    $result = $cache->get($plugin);
1067
    if ($result === false) {
1068
        // The user is after a recordset.
1069
        if (!$iscore) {
1070
            $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1071
        } else {
1072
            // This part is not really used any more, but anyway...
1073
            $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1074
        }
1075
        $cache->set($plugin, $result);
1076
    }
1077
 
1078
    if (!empty($name)) {
1079
        if (array_key_exists($name, $result)) {
1080
            return $result[$name];
1081
        }
1082
        return false;
1083
    }
1084
 
1085
    if ($plugin === 'core') {
1086
        $result['siteidentifier'] = $CFG->siteidentifier;
1087
    }
1088
 
1089
    foreach ($forced as $key => $value) {
1090
        if (is_null($value) or is_array($value) or is_object($value)) {
1091
            // We do not want any extra mess here, just real settings that could be saved in db.
1092
            unset($result[$key]);
1093
        } else {
1094
            // Convert to string as if it went through the DB.
1095
            $result[$key] = (string)$value;
1096
        }
1097
    }
1098
 
1099
    return (object)$result;
1100
}
1101
 
1102
/**
1103
 * Removes a key from global configuration.
1104
 *
1105
 * NOTE: this function is called from lib/db/upgrade.php
1106
 *
1107
 * @param string $name the key to set
1108
 * @param string $plugin (optional) the plugin scope
1441 ariadna 1109
 * @param boolean $log (optional) should this emit to the config log
1 efrain 1110
 * @return boolean whether the operation succeeded.
1111
 */
1441 ariadna 1112
function unset_config($name, $plugin = null, bool $log = false) {
1 efrain 1113
    global $CFG, $DB;
1114
 
1441 ariadna 1115
    if ($log) {
1116
        $prev = get_config($plugin, $name);
1117
        if ($prev === false) {
1118
            $prev = null;
1119
        }
1120
        add_to_config_log($name, $prev, null, $plugin);
1121
    }
1122
 
1 efrain 1123
    if (empty($plugin)) {
1124
        unset($CFG->$name);
1125
        $DB->delete_records('config', array('name' => $name));
1126
        cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1127
    } else {
1128
        $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1129
        cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1130
    }
1131
 
1132
    return true;
1133
}
1134
 
1135
/**
1136
 * Remove all the config variables for a given plugin.
1137
 *
1138
 * NOTE: this function is called from lib/db/upgrade.php
1139
 *
1140
 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1141
 * @return boolean whether the operation succeeded.
1142
 */
1441 ariadna 1143
function unset_all_config_for_plugin($plugin) {
1 efrain 1144
    global $DB;
1145
    // Delete from the obvious config_plugins first.
1146
    $DB->delete_records('config_plugins', array('plugin' => $plugin));
1147
    // Next delete any suspect settings from config.
1148
    $like = $DB->sql_like('name', '?', true, true, false, '|');
1441 ariadna 1149
    $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1 efrain 1150
    $DB->delete_records_select('config', $like, $params);
1151
    // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1152
    cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1153
 
1154
    return true;
1155
}
1156
 
1157
/**
1158
 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1159
 *
1160
 * All users are verified if they still have the necessary capability.
1161
 *
1162
 * @param string $value the value of the config setting.
1163
 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1164
 * @param bool $includeadmins include administrators.
1165
 * @return array of user objects.
1166
 */
1441 ariadna 1167
function get_users_from_config($value, $capability, $includeadmins = true) {
1 efrain 1168
    if (empty($value) or $value === '$@NONE@$') {
1169
        return array();
1170
    }
1171
 
1172
    // We have to make sure that users still have the necessary capability,
1173
    // it should be faster to fetch them all first and then test if they are present
1174
    // instead of validating them one-by-one.
1175
    $users = get_users_by_capability(context_system::instance(), $capability);
1176
    if ($includeadmins) {
1177
        $admins = get_admins();
1178
        foreach ($admins as $admin) {
1179
            $users[$admin->id] = $admin;
1180
        }
1181
    }
1182
 
1183
    if ($value === '$@ALL@$') {
1184
        return $users;
1185
    }
1186
 
1187
    $result = array(); // Result in correct order.
1188
    $allowed = explode(',', $value);
1189
    foreach ($allowed as $uid) {
1190
        if (isset($users[$uid])) {
1191
            $user = $users[$uid];
1192
            $result[$user->id] = $user;
1193
        }
1194
    }
1195
 
1196
    return $result;
1197
}
1198
 
1199
 
1200
/**
1201
 * Invalidates browser caches and cached data in temp.
1202
 *
1203
 * @return void
1204
 */
1441 ariadna 1205
function purge_all_caches() {
1 efrain 1206
    purge_caches();
1207
}
1208
 
1209
/**
1210
 * Selectively invalidate different types of cache.
1211
 *
1212
 * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
1213
 * areas alone or in combination.
1214
 *
1215
 * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1216
 *        'muc'    Purge MUC caches?
1441 ariadna 1217
 *        'courses' Purge all course caches, or specific course caches
1 efrain 1218
 *        'theme'  Purge theme cache?
1219
 *        'lang'   Purge language string cache?
1220
 *        'js'     Purge javascript cache?
1441 ariadna 1221
 *        'template' Purge template cache
1 efrain 1222
 *        'filter' Purge text filter cache?
1223
 *        'other'  Purge all other caches?
1224
 */
1441 ariadna 1225
function purge_caches($options = []) {
1 efrain 1226
    $defaults = array_fill_keys(['muc', 'courses', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1227
    if (empty(array_filter($options))) {
1228
        $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1229
    } else {
1230
        $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1231
    }
1232
    if ($options['muc']) {
1233
        cache_helper::purge_all();
11 efrain 1234
    } else if ($options['courses']) {
1 efrain 1235
        if ($options['courses'] === true) {
1236
            $courseids = [];
1237
        } else {
1238
            $courseids = preg_split('/\s*,\s*/', $options['courses'], -1, PREG_SPLIT_NO_EMPTY);
1239
        }
1240
        course_modinfo::purge_course_caches($courseids);
1241
    }
1242
    if ($options['theme']) {
1243
        theme_reset_all_caches();
1244
    }
1245
    if ($options['lang']) {
1246
        get_string_manager()->reset_caches();
1247
    }
1248
    if ($options['js']) {
1249
        js_reset_all_caches();
1250
    }
1251
    if ($options['template']) {
1252
        template_reset_all_caches();
1253
    }
1254
    if ($options['filter']) {
1255
        reset_text_filters_cache();
1256
    }
1257
    if ($options['other']) {
1258
        purge_other_caches();
1259
    }
1260
}
1261
 
1262
/**
1263
 * Purge all non-MUC caches not otherwise purged in purge_caches.
1264
 *
1265
 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1266
 * {@link phpunit_util::reset_dataroot()}
1267
 */
1441 ariadna 1268
function purge_other_caches() {
1 efrain 1269
    global $DB, $CFG;
1270
    if (class_exists('core_plugin_manager')) {
1271
        core_plugin_manager::reset_caches();
1272
    }
1273
 
1274
    // Bump up cacherev field for all courses.
1275
    try {
1276
        increment_revision_number('course', 'cacherev', '');
1277
    } catch (moodle_exception $e) {
1278
        // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1279
    }
1280
 
1281
    $DB->reset_caches();
1282
 
1283
    // Purge all other caches: rss, simplepie, etc.
1284
    clearstatcache();
1441 ariadna 1285
    remove_dir($CFG->cachedir.'', true);
1 efrain 1286
 
1287
    // Make sure cache dir is writable, throws exception if not.
1288
    make_cache_directory('');
1289
 
1290
    // This is the only place where we purge local caches, we are only adding files there.
1291
    // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1292
    remove_dir($CFG->localcachedir, true);
1293
    set_config('localcachedirpurged', time());
1294
    make_localcache_directory('', true);
11 efrain 1295
 
1296
    // Rewarm the bootstrap.php files so the siteid is always present after a purge.
1297
    initialise_local_config_cache();
1 efrain 1298
    \core\task\manager::clear_static_caches();
1299
}
1300
 
1301
/**
1302
 * Get volatile flags
1303
 *
1304
 * @param string $type
1305
 * @param int $changedsince default null
1306
 * @return array records array
1307
 */
1441 ariadna 1308
function get_cache_flags($type, $changedsince = null) {
1 efrain 1309
    global $DB;
1310
 
1311
    $params = array('type' => $type, 'expiry' => time());
1312
    $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1313
    if ($changedsince !== null) {
1314
        $params['changedsince'] = $changedsince;
1315
        $sqlwhere .= " AND timemodified > :changedsince";
1316
    }
1317
    $cf = array();
1318
    if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1319
        foreach ($flags as $flag) {
1320
            $cf[$flag->name] = $flag->value;
1321
        }
1322
    }
1323
    return $cf;
1324
}
1325
 
1326
/**
1327
 * Get volatile flags
1328
 *
1329
 * @param string $type
1330
 * @param string $name
1331
 * @param int $changedsince default null
1332
 * @return string|false The cache flag value or false
1333
 */
1441 ariadna 1334
function get_cache_flag($type, $name, $changedsince=null) {
1 efrain 1335
    global $DB;
1336
 
1337
    $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1338
 
1339
    $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1340
    if ($changedsince !== null) {
1341
        $params['changedsince'] = $changedsince;
1342
        $sqlwhere .= " AND timemodified > :changedsince";
1343
    }
1344
 
1345
    return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1346
}
1347
 
1348
/**
1349
 * Set a volatile flag
1350
 *
1351
 * @param string $type the "type" namespace for the key
1352
 * @param string $name the key to set
1353
 * @param string $value the value to set (without magic quotes) - null will remove the flag
1354
 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1355
 * @return bool Always returns true
1356
 */
1441 ariadna 1357
function set_cache_flag($type, $name, $value, $expiry = null) {
1 efrain 1358
    global $DB;
1359
 
1360
    $timemodified = time();
1361
    if ($expiry === null || $expiry < $timemodified) {
1362
        $expiry = $timemodified + 24 * 60 * 60;
1363
    } else {
1364
        $expiry = (int)$expiry;
1365
    }
1366
 
1367
    if ($value === null) {
1368
        unset_cache_flag($type, $name);
1369
        return true;
1370
    }
1371
 
1372
    if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1373
        // This is a potential problem in DEBUG_DEVELOPER.
1374
        if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1375
            return true; // No need to update.
1376
        }
1377
        $f->value        = $value;
1378
        $f->expiry       = $expiry;
1379
        $f->timemodified = $timemodified;
1380
        $DB->update_record('cache_flags', $f);
1381
    } else {
1382
        $f = new stdClass();
1383
        $f->flagtype     = $type;
1384
        $f->name         = $name;
1385
        $f->value        = $value;
1386
        $f->expiry       = $expiry;
1387
        $f->timemodified = $timemodified;
1388
        $DB->insert_record('cache_flags', $f);
1389
    }
1390
    return true;
1391
}
1392
 
1393
/**
1394
 * Removes a single volatile flag
1395
 *
1396
 * @param string $type the "type" namespace for the key
1397
 * @param string $name the key to set
1398
 * @return bool
1399
 */
1441 ariadna 1400
function unset_cache_flag($type, $name) {
1 efrain 1401
    global $DB;
1402
    $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1403
    return true;
1404
}
1405
 
1406
/**
1407
 * Garbage-collect volatile flags
1408
 *
1409
 * @return bool Always returns true
1410
 */
1441 ariadna 1411
function gc_cache_flags() {
1 efrain 1412
    global $DB;
1413
    $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1414
    return true;
1415
}
1416
 
1417
// USER PREFERENCE API.
1418
 
1419
/**
1420
 * Refresh user preference cache. This is used most often for $USER
1421
 * object that is stored in session, but it also helps with performance in cron script.
1422
 *
1423
 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1424
 *
1425
 * @package  core
1426
 * @category preference
1427
 * @access   public
1428
 * @param    stdClass         $user          User object. Preferences are preloaded into 'preference' property
1429
 * @param    int              $cachelifetime Cache life time on the current page (in seconds)
1430
 * @throws   coding_exception
1431
 * @return   null
1432
 */
1441 ariadna 1433
function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1 efrain 1434
    global $DB;
1435
    // Static cache, we need to check on each page load, not only every 2 minutes.
1436
    static $loadedusers = array();
1437
 
1438
    if (!isset($user->id)) {
1439
        throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1440
    }
1441
 
1442
    if (empty($user->id) or isguestuser($user->id)) {
1443
        // No permanent storage for not-logged-in users and guest.
1444
        if (!isset($user->preference)) {
1445
            $user->preference = array();
1446
        }
1447
        return;
1448
    }
1449
 
1450
    $timenow = time();
1451
 
1452
    if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1453
        // Already loaded at least once on this page. Are we up to date?
1454
        if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1455
            // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1456
            return;
1441 ariadna 1457
 
1 efrain 1458
        } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1459
            // No change since the lastcheck on this page.
1460
            $user->preference['_lastloaded'] = $timenow;
1461
            return;
1462
        }
1463
    }
1464
 
1465
    // OK, so we have to reload all preferences.
1466
    $loadedusers[$user->id] = true;
1467
    $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1468
    $user->preference['_lastloaded'] = $timenow;
1469
}
1470
 
1471
/**
1472
 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1473
 *
1474
 * NOTE: internal function, do not call from other code.
1475
 *
1476
 * @package core
1477
 * @access private
1478
 * @param integer $userid the user whose prefs were changed.
1479
 */
1441 ariadna 1480
function mark_user_preferences_changed($userid) {
1 efrain 1481
    global $CFG;
1482
 
1483
    if (empty($userid) or isguestuser($userid)) {
1484
        // No cache flags for guest and not-logged-in users.
1485
        return;
1486
    }
1487
 
1488
    set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1489
}
1490
 
1491
/**
1492
 * Sets a preference for the specified user.
1493
 *
1494
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1495
 *
1496
 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1497
 *
1498
 * @package  core
1499
 * @category preference
1500
 * @access   public
1501
 * @param    string            $name  The key to set as preference for the specified user
1502
 * @param    string|int|bool|null $value The value to set for the $name key in the specified user's
1503
 *                                    record, null means delete current value.
1504
 * @param    stdClass|int|null $user  A moodle user object or id, null means current user
1505
 * @throws   coding_exception
1506
 * @return   bool                     Always true or exception
1507
 */
1441 ariadna 1508
function set_user_preference($name, $value, $user = null) {
1 efrain 1509
    global $USER, $DB;
1510
 
1511
    if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1512
        throw new coding_exception('Invalid preference name in set_user_preference() call');
1513
    }
1514
 
1515
    if (is_null($value)) {
1516
        // Null means delete current.
1517
        return unset_user_preference($name, $user);
1518
    } else if (is_object($value)) {
1519
        throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1520
    } else if (is_array($value)) {
1521
        throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1522
    }
1523
    $value = (string)$value;
1524
 
1525
    if (is_null($user)) {
1526
        $user = $USER;
1527
    } else if (isset($user->id)) {
1528
        // It is a valid object.
1529
    } else if (is_numeric($user)) {
1530
        $user = (object)array('id' => (int)$user);
1531
    } else {
1532
        throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1533
    }
1534
 
1535
    check_user_preferences_loaded($user);
1536
 
1537
    if (empty($user->id) or isguestuser($user->id)) {
1538
        // No permanent storage for not-logged-in users and guest.
1539
        $user->preference[$name] = $value;
1540
        return true;
1541
    }
1542
 
1441 ariadna 1543
    $retry = 0;
1544
    $saved = false;
1545
 
1546
    while (!$saved && $retry++ < 2) {
1547
        if ($preference = $DB->get_record('user_preferences', ['userid' => $user->id, 'name' => $name])) {
1548
            if ($preference->value === $value && isset($user->preference[$name]) && $user->preference[$name] === $value) {
1549
                // Preference already set to this value.
1550
                return true;
1551
            }
1552
            $DB->set_field('user_preferences', 'value', $value, ['id' => $preference->id]);
1553
            $saved = true;
1554
        } else {
1555
            $preference = new stdClass();
1556
            $preference->userid = $user->id;
1557
            $preference->name   = $name;
1558
            $preference->value  = $value;
1559
            try {
1560
                $DB->insert_record('user_preferences', $preference);
1561
                $saved = true;
1562
            } catch (dml_write_exception $e) {
1563
                // We have an insert race, so just ignore and try again.
1564
                $saved = false;
1565
            }
1 efrain 1566
        }
1567
    }
1568
 
1569
    // Update value in cache.
1570
    $user->preference[$name] = $value;
1571
    // Update the $USER in case where we've not a direct reference to $USER.
1572
    if ($user !== $USER && $user->id == $USER->id) {
1573
        $USER->preference[$name] = $value;
1574
    }
1575
 
1576
    // Set reload flag for other sessions.
1577
    mark_user_preferences_changed($user->id);
1578
 
1579
    return true;
1580
}
1581
 
1582
/**
1583
 * Sets a whole array of preferences for the current user
1584
 *
1585
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1586
 *
1587
 * @package  core
1588
 * @category preference
1589
 * @access   public
1590
 * @param    array             $prefarray An array of key/value pairs to be set
1591
 * @param    stdClass|int|null $user      A moodle user object or id, null means current user
1592
 * @return   bool                         Always true or exception
1593
 */
1441 ariadna 1594
function set_user_preferences(array $prefarray, $user = null) {
1 efrain 1595
    foreach ($prefarray as $name => $value) {
1596
        set_user_preference($name, $value, $user);
1597
    }
1598
    return true;
1599
}
1600
 
1601
/**
1602
 * Unsets a preference completely by deleting it from the database
1603
 *
1604
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1605
 *
1606
 * @package  core
1607
 * @category preference
1608
 * @access   public
1609
 * @param    string            $name The key to unset as preference for the specified user
1610
 * @param    stdClass|int|null $user A moodle user object or id, null means current user
1611
 * @throws   coding_exception
1612
 * @return   bool                    Always true or exception
1613
 */
1441 ariadna 1614
function unset_user_preference($name, $user = null) {
1 efrain 1615
    global $USER, $DB;
1616
 
1617
    if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1618
        throw new coding_exception('Invalid preference name in unset_user_preference() call');
1619
    }
1620
 
1621
    if (is_null($user)) {
1622
        $user = $USER;
1623
    } else if (isset($user->id)) {
1624
        // It is a valid object.
1625
    } else if (is_numeric($user)) {
1626
        $user = (object)array('id' => (int)$user);
1627
    } else {
1628
        throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1629
    }
1630
 
1631
    check_user_preferences_loaded($user);
1632
 
1633
    if (empty($user->id) or isguestuser($user->id)) {
1634
        // No permanent storage for not-logged-in user and guest.
1635
        unset($user->preference[$name]);
1636
        return true;
1637
    }
1638
 
1639
    // Delete from DB.
1640
    $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
1641
 
1642
    // Delete the preference from cache.
1643
    unset($user->preference[$name]);
1644
    // Update the $USER in case where we've not a direct reference to $USER.
1645
    if ($user !== $USER && $user->id == $USER->id) {
1646
        unset($USER->preference[$name]);
1647
    }
1648
 
1649
    // Set reload flag for other sessions.
1650
    mark_user_preferences_changed($user->id);
1651
 
1652
    return true;
1653
}
1654
 
1655
/**
1656
 * Used to fetch user preference(s)
1657
 *
1658
 * If no arguments are supplied this function will return
1659
 * all of the current user preferences as an array.
1660
 *
1661
 * If a name is specified then this function
1662
 * attempts to return that particular preference value.  If
1663
 * none is found, then the optional value $default is returned,
1664
 * otherwise null.
1665
 *
1666
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1667
 *
1668
 * @package  core
1669
 * @category preference
1670
 * @access   public
1671
 * @param    string            $name    Name of the key to use in finding a preference value
1672
 * @param    mixed|null        $default Value to be returned if the $name key is not set in the user preferences
1673
 * @param    stdClass|int|null $user    A moodle user object or id, null means current user
1674
 * @throws   coding_exception
1675
 * @return   string|mixed|null          A string containing the value of a single preference. An
1676
 *                                      array with all of the preferences or null
1677
 */
1441 ariadna 1678
function get_user_preferences($name = null, $default = null, $user = null) {
1 efrain 1679
    global $USER;
1680
 
1681
    if (is_null($name)) {
1682
        // All prefs.
1683
    } else if (is_numeric($name) or $name === '_lastloaded') {
1684
        throw new coding_exception('Invalid preference name in get_user_preferences() call');
1685
    }
1686
 
1687
    if (is_null($user)) {
1688
        $user = $USER;
1689
    } else if (isset($user->id)) {
1690
        // Is a valid object.
1691
    } else if (is_numeric($user)) {
1692
        if ($USER->id == $user) {
1693
            $user = $USER;
1694
        } else {
1695
            $user = (object)array('id' => (int)$user);
1696
        }
1697
    } else {
1698
        throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1699
    }
1700
 
1701
    check_user_preferences_loaded($user);
1702
 
1703
    if (empty($name)) {
1704
        // All values.
1705
        return $user->preference;
1706
    } else if (isset($user->preference[$name])) {
1707
        // The single string value.
1708
        return $user->preference[$name];
1709
    } else {
1710
        // Default value (null if not specified).
1711
        return $default;
1712
    }
1713
}
1714
 
1715
// FUNCTIONS FOR HANDLING TIME.
1716
 
1717
/**
1718
 * Given Gregorian date parts in user time produce a GMT timestamp.
1719
 *
1720
 * @package core
1721
 * @category time
1722
 * @param int $year The year part to create timestamp of
1723
 * @param int $month The month part to create timestamp of
1724
 * @param int $day The day part to create timestamp of
1725
 * @param int $hour The hour part to create timestamp of
1726
 * @param int $minute The minute part to create timestamp of
1727
 * @param int $second The second part to create timestamp of
1728
 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
1729
 *             if 99 then default user's timezone is used {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1730
 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
1731
 *             applied only if timezone is 99 or string.
1732
 * @return int GMT timestamp
1733
 */
1441 ariadna 1734
function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
1 efrain 1735
    $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
1736
    $date->setDate((int)$year, (int)$month, (int)$day);
1737
    $date->setTime((int)$hour, (int)$minute, (int)$second);
1738
 
1739
    $time = $date->getTimestamp();
1740
 
1741
    if ($time === false) {
1441 ariadna 1742
        throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
1 efrain 1743
            ' This can fail if year is more than 2038 and OS is 32 bit windows');
1744
    }
1745
 
1746
    // Moodle BC DST stuff.
1747
    if (!$applydst) {
1748
        $time += dst_offset_on($time, $timezone);
1749
    }
1750
 
1751
    return $time;
1441 ariadna 1752
 
1 efrain 1753
}
1754
 
1755
/**
1756
 * Format a date/time (seconds) as weeks, days, hours etc as needed
1757
 *
1758
 * Given an amount of time in seconds, returns string
1759
 * formatted nicely as years, days, hours etc as needed
1760
 *
1761
 * @package core
1762
 * @category time
1763
 * @uses MINSECS
1764
 * @uses HOURSECS
1765
 * @uses DAYSECS
1766
 * @uses YEARSECS
1767
 * @param int $totalsecs Time in seconds
1768
 * @param stdClass $str Should be a time object
1769
 * @return string A nicely formatted date/time string
1770
 */
1441 ariadna 1771
function format_time($totalsecs, $str = null) {
1 efrain 1772
 
1773
    $totalsecs = abs($totalsecs);
1774
 
1775
    if (!$str) {
1776
        // Create the str structure the slow way.
1777
        $str = new stdClass();
1778
        $str->day   = get_string('day');
1779
        $str->days  = get_string('days');
1780
        $str->hour  = get_string('hour');
1781
        $str->hours = get_string('hours');
1782
        $str->min   = get_string('min');
1783
        $str->mins  = get_string('mins');
1784
        $str->sec   = get_string('sec');
1785
        $str->secs  = get_string('secs');
1786
        $str->year  = get_string('year');
1787
        $str->years = get_string('years');
1788
    }
1789
 
1441 ariadna 1790
    $years     = floor($totalsecs/YEARSECS);
1791
    $remainder = $totalsecs - ($years*YEARSECS);
1792
    $days      = floor($remainder/DAYSECS);
1793
    $remainder = $totalsecs - ($days*DAYSECS);
1794
    $hours     = floor($remainder/HOURSECS);
1795
    $remainder = $remainder - ($hours*HOURSECS);
1796
    $mins      = floor($remainder/MINSECS);
1797
    $secs      = $remainder - ($mins*MINSECS);
1 efrain 1798
 
1799
    $ss = ($secs == 1)  ? $str->sec  : $str->secs;
1800
    $sm = ($mins == 1)  ? $str->min  : $str->mins;
1801
    $sh = ($hours == 1) ? $str->hour : $str->hours;
1802
    $sd = ($days == 1)  ? $str->day  : $str->days;
1803
    $sy = ($years == 1)  ? $str->year  : $str->years;
1804
 
1805
    $oyears = '';
1806
    $odays = '';
1807
    $ohours = '';
1808
    $omins = '';
1809
    $osecs = '';
1810
 
1811
    if ($years) {
1441 ariadna 1812
        $oyears  = $years .' '. $sy;
1 efrain 1813
    }
1814
    if ($days) {
1441 ariadna 1815
        $odays  = $days .' '. $sd;
1 efrain 1816
    }
1817
    if ($hours) {
1441 ariadna 1818
        $ohours = $hours .' '. $sh;
1 efrain 1819
    }
1820
    if ($mins) {
1441 ariadna 1821
        $omins  = $mins .' '. $sm;
1 efrain 1822
    }
1823
    if ($secs) {
1441 ariadna 1824
        $osecs  = $secs .' '. $ss;
1 efrain 1825
    }
1826
 
1827
    if ($years) {
1441 ariadna 1828
        return trim($oyears .' '. $odays);
1 efrain 1829
    }
1830
    if ($days) {
1441 ariadna 1831
        return trim($odays .' '. $ohours);
1 efrain 1832
    }
1833
    if ($hours) {
1441 ariadna 1834
        return trim($ohours .' '. $omins);
1 efrain 1835
    }
1836
    if ($mins) {
1441 ariadna 1837
        return trim($omins .' '. $osecs);
1 efrain 1838
    }
1839
    if ($secs) {
1840
        return $osecs;
1841
    }
1842
    return get_string('now');
1843
}
1844
 
1845
/**
1846
 * Returns a formatted string that represents a date in user time.
1847
 *
1848
 * @package core
1849
 * @category time
1850
 * @param int $date the timestamp in UTC, as obtained from the database.
1851
 * @param string $format strftime format. You should probably get this using
1852
 *        get_string('strftime...', 'langconfig');
1853
 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1854
 *        not 99 then daylight saving will not be added.
1855
 *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1856
 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1857
 *        If false then the leading zero is maintained.
1858
 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1859
 * @return string the formatted date/time.
1860
 */
1441 ariadna 1861
function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
1 efrain 1862
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
1863
    return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
1864
}
1865
 
1866
/**
1867
 * Returns a html "time" tag with both the exact user date with timezone information
1868
 * as a datetime attribute in the W3C format, and the user readable date and time as text.
1869
 *
1870
 * @package core
1871
 * @category time
1872
 * @param int $date the timestamp in UTC, as obtained from the database.
1873
 * @param string $format strftime format. You should probably get this using
1874
 *        get_string('strftime...', 'langconfig');
1875
 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1876
 *        not 99 then daylight saving will not be added.
1877
 *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1878
 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1879
 *        If false then the leading zero is maintained.
1880
 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1881
 * @return string the formatted date/time.
1882
 */
1441 ariadna 1883
function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
1 efrain 1884
    $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
1885
    if (CLI_SCRIPT && !PHPUNIT_TEST) {
1886
        return $userdatestr;
1887
    }
1888
    $machinedate = new DateTime();
1889
    $machinedate->setTimestamp(intval($date));
1890
    $machinedate->setTimezone(core_date::get_user_timezone_object());
1891
 
1892
    return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
1893
}
1894
 
1895
/**
1896
 * Returns a formatted date ensuring it is UTF-8.
1897
 *
1898
 * If we are running under Windows convert to Windows encoding and then back to UTF-8
1899
 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
1900
 *
1901
 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
1902
 * @param string $format strftime format.
1903
 * @param int|float|string $tz the user timezone
1904
 * @return string the formatted date/time.
1905
 * @since Moodle 2.3.3
1906
 */
1441 ariadna 1907
function date_format_string($date, $format, $tz = 99) {
1 efrain 1908
 
1909
    date_default_timezone_set(core_date::get_user_timezone($tz));
1910
 
1911
    if (date('A', 0) === date('A', HOURSECS * 18)) {
1912
        $datearray = getdate($date);
1913
        $format = str_replace([
1914
            '%P',
1915
            '%p',
1916
        ], [
1917
            $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
1918
            $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
1919
        ], $format);
1920
    }
1921
 
1922
    $datestring = core_date::strftime($format, $date);
1923
    core_date::set_default_server_timezone();
1924
 
1925
    return $datestring;
1926
}
1927
 
1928
/**
1929
 * Given a $time timestamp in GMT (seconds since epoch),
1930
 * returns an array that represents the Gregorian date in user time
1931
 *
1932
 * @package core
1933
 * @category time
1934
 * @param int $time Timestamp in GMT
1935
 * @param float|int|string $timezone user timezone
1936
 * @return array An array that represents the date in user time
1937
 */
1441 ariadna 1938
function usergetdate($time, $timezone=99) {
1 efrain 1939
    if ($time === null) {
1940
        // PHP8 and PHP7 return different results when getdate(null) is called.
1941
        // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
1942
        // In the future versions of Moodle we may consider adding a strict typehint.
1943
        debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER);
1944
        $time = 0;
1945
    }
1946
 
1947
    date_default_timezone_set(core_date::get_user_timezone($timezone));
1948
    $result = getdate($time);
1949
    core_date::set_default_server_timezone();
1950
 
1951
    return $result;
1952
}
1953
 
1954
/**
1955
 * Given a GMT timestamp (seconds since epoch), offsets it by
1956
 * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
1957
 *
1958
 * NOTE: this function does not include DST properly,
1959
 *       you should use the PHP date stuff instead!
1960
 *
1961
 * @package core
1962
 * @category time
1963
 * @param int $date Timestamp in GMT
1964
 * @param float|int|string $timezone user timezone
1965
 * @return int
1966
 */
1441 ariadna 1967
function usertime($date, $timezone=99) {
1 efrain 1968
    $userdate = new DateTime('@' . $date);
1969
    $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
1970
    $dst = dst_offset_on($date, $timezone);
1971
 
1972
    return $date - $userdate->getOffset() + $dst;
1973
}
1974
 
1975
/**
1976
 * Get a formatted string representation of an interval between two unix timestamps.
1977
 *
1978
 * E.g.
1979
 * $intervalstring = get_time_interval_string(12345600, 12345660);
1980
 * Will produce the string:
1981
 * '0d 0h 1m'
1982
 *
1983
 * @param int $time1 unix timestamp
1984
 * @param int $time2 unix timestamp
1985
 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
1986
 * @param bool $dropzeroes If format is not provided and this is set to true, do not include zero time units.
1987
 *                         e.g. a duration of 3 days and 2 hours will be displayed as '3d 2h' instead of '3d 2h 0s'
1988
 * @param bool $fullformat If format is not provided and this is set to true, display time units in full format.
1989
 *                         e.g. instead of showing "3d", "3 days" will be returned.
1990
 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
1991
 */
1441 ariadna 1992
function get_time_interval_string(int $time1, int $time2, string $format = '',
1993
        bool $dropzeroes = false, bool $fullformat = false): string {
1 efrain 1994
    $dtdate = new DateTime();
1995
    $dtdate->setTimeStamp($time1);
1996
    $dtdate2 = new DateTime();
1997
    $dtdate2->setTimeStamp($time2);
1998
    $interval = $dtdate2->diff($dtdate);
1999
 
2000
    if (empty(trim($format))) {
2001
        // Default to this key.
2002
        $formatkey = 'dateintervaldayhrmin';
2003
 
2004
        if ($dropzeroes) {
2005
            $units = [
2006
                'y' => 'yr',
2007
                'm' => 'mo',
2008
                'd' => 'day',
2009
                'h' => 'hr',
2010
                'i' => 'min',
2011
                's' => 'sec',
2012
            ];
2013
            $formatunits = [];
2014
            foreach ($units as $key => $unit) {
2015
                if (empty($interval->$key)) {
2016
                    continue;
2017
                }
2018
                $formatunits[] = $unit;
2019
            }
2020
            if (!empty($formatunits)) {
2021
                $formatkey = 'dateinterval' . implode("", $formatunits);
2022
            }
2023
        }
2024
 
2025
        if ($fullformat) {
2026
            $formatkey .= 'full';
2027
        }
2028
        $format = get_string($formatkey, 'langconfig');
2029
    }
2030
    return $interval->format($format);
2031
}
2032
 
2033
/**
2034
 * Given a time, return the GMT timestamp of the most recent midnight
2035
 * for the current user.
2036
 *
2037
 * @package core
2038
 * @category time
2039
 * @param int $date Timestamp in GMT
2040
 * @param float|int|string $timezone user timezone
2041
 * @return int Returns a GMT timestamp
2042
 */
1441 ariadna 2043
function usergetmidnight($date, $timezone=99) {
1 efrain 2044
 
2045
    $userdate = usergetdate($date, $timezone);
2046
 
2047
    // Time of midnight of this user's day, in GMT.
2048
    return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
1441 ariadna 2049
 
1 efrain 2050
}
2051
 
2052
/**
2053
 * Returns a string that prints the user's timezone
2054
 *
2055
 * @package core
2056
 * @category time
2057
 * @param float|int|string $timezone user timezone
2058
 * @return string
2059
 */
1441 ariadna 2060
function usertimezone($timezone=99) {
1 efrain 2061
    $tz = core_date::get_user_timezone($timezone);
2062
    return core_date::get_localised_timezone($tz);
2063
}
2064
 
2065
/**
2066
 * Returns a float or a string which denotes the user's timezone
2067
 * 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)
2068
 * means that for this timezone there are also DST rules to be taken into account
2069
 * Checks various settings and picks the most dominant of those which have a value
2070
 *
2071
 * @package core
2072
 * @category time
2073
 * @param float|int|string $tz timezone to calculate GMT time offset before
2074
 *        calculating user timezone, 99 is default user timezone
2075
 *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2076
 * @return float|string
2077
 */
1441 ariadna 2078
function get_user_timezone($tz = 99) {
1 efrain 2079
    global $USER, $CFG;
2080
 
2081
    $timezones = array(
2082
        $tz,
2083
        isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2084
        isset($USER->timezone) ? $USER->timezone : 99,
2085
        isset($CFG->timezone) ? $CFG->timezone : 99,
1441 ariadna 2086
        );
1 efrain 2087
 
2088
    $tz = 99;
2089
 
2090
    // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2091
    foreach ($timezones as $nextvalue) {
2092
        if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2093
            $tz = $nextvalue;
2094
        }
2095
    }
2096
    return is_numeric($tz) ? (float) $tz : $tz;
2097
}
2098
 
2099
/**
2100
 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2101
 * - Note: Daylight saving only works for string timezones and not for float.
2102
 *
2103
 * @package core
2104
 * @category time
2105
 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2106
 * @param int|float|string $strtimezone user timezone
2107
 * @return int
2108
 */
1441 ariadna 2109
function dst_offset_on($time, $strtimezone = null) {
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
 */
1441 ariadna 2133
function find_day_in_month($startday, $weekday, $month, $year) {
1 efrain 2134
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2135
 
2136
    $daysinmonth = days_in_month($month, $year);
2137
    $daysinweek = count($calendartype->get_weekdays());
2138
 
2139
    if ($weekday == -1) {
2140
        // Don't care about weekday, so return:
2141
        //    abs($startday) if $startday != -1
2142
        //    $daysinmonth otherwise.
2143
        return ($startday == -1) ? $daysinmonth : abs($startday);
2144
    }
2145
 
2146
    // From now on we 're looking for a specific weekday.
2147
    // Give "end of month" its actual value, since we know it.
2148
    if ($startday == -1) {
2149
        $startday = -1 * $daysinmonth;
2150
    }
2151
 
2152
    // Starting from day $startday, the sign is the direction.
2153
    if ($startday < 1) {
2154
        $startday = abs($startday);
2155
        $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2156
 
2157
        // This is the last such weekday of the month.
2158
        $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2159
        if ($lastinmonth > $daysinmonth) {
2160
            $lastinmonth -= $daysinweek;
2161
        }
2162
 
2163
        // Find the first such weekday <= $startday.
2164
        while ($lastinmonth > $startday) {
2165
            $lastinmonth -= $daysinweek;
2166
        }
2167
 
2168
        return $lastinmonth;
2169
    } else {
2170
        $indexweekday = dayofweek($startday, $month, $year);
2171
 
2172
        $diff = $weekday - $indexweekday;
2173
        if ($diff < 0) {
2174
            $diff += $daysinweek;
2175
        }
2176
 
2177
        // This is the first such weekday of the month equal to or after $startday.
2178
        $firstfromindex = $startday + $diff;
2179
 
2180
        return $firstfromindex;
2181
    }
2182
}
2183
 
2184
/**
2185
 * Calculate the number of days in a given month
2186
 *
2187
 * @package core
2188
 * @category time
2189
 * @param int $month The month whose day count is sought
2190
 * @param int $year The year of the month whose day count is sought
2191
 * @return int
2192
 */
1441 ariadna 2193
function days_in_month($month, $year) {
1 efrain 2194
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2195
    return $calendartype->get_num_days_in_month($year, $month);
2196
}
2197
 
2198
/**
2199
 * Calculate the position in the week of a specific calendar day
2200
 *
2201
 * @package core
2202
 * @category time
2203
 * @param int $day The day of the date whose position in the week is sought
2204
 * @param int $month The month of the date whose position in the week is sought
2205
 * @param int $year The year of the date whose position in the week is sought
2206
 * @return int
2207
 */
1441 ariadna 2208
function dayofweek($day, $month, $year) {
1 efrain 2209
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2210
    return $calendartype->get_weekday($year, $month, $day);
2211
}
2212
 
2213
// USER AUTHENTICATION AND LOGIN.
2214
 
2215
/**
2216
 * Returns full login url.
2217
 *
2218
 * Any form submissions for authentication to this URL must include username,
2219
 * password as well as a logintoken generated by \core\session\manager::get_login_token().
2220
 *
2221
 * @return string login url
2222
 */
1441 ariadna 2223
function get_login_url() {
1 efrain 2224
    global $CFG;
2225
 
2226
    return "$CFG->wwwroot/login/index.php";
2227
}
2228
 
2229
/**
2230
 * This function checks that the current user is logged in and has the
2231
 * required privileges
2232
 *
2233
 * This function checks that the current user is logged in, and optionally
2234
 * whether they are allowed to be in a particular course and view a particular
2235
 * course module.
2236
 * If they are not logged in, then it redirects them to the site login unless
2237
 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2238
 * case they are automatically logged in as guests.
2239
 * If $courseid is given and the user is not enrolled in that course then the
2240
 * user is redirected to the course enrolment page.
2241
 * If $cm is given and the course module is hidden and the user is not a teacher
2242
 * in the course then the user is redirected to the course home page.
2243
 *
2244
 * When $cm parameter specified, this function sets page layout to 'module'.
2245
 * You need to change it manually later if some other layout needed.
2246
 *
2247
 * @package    core_access
2248
 * @category   access
2249
 *
2250
 * @param mixed $courseorid id of the course or course object
2251
 * @param bool $autologinguest default true
2252
 * @param object $cm course module object
2253
 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2254
 *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2255
 *             in order to keep redirects working properly. MDL-14495
2256
 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2257
 * @return mixed Void, exit, and die depending on path
2258
 * @throws coding_exception
2259
 * @throws require_login_exception
2260
 * @throws moodle_exception
2261
 */
1441 ariadna 2262
function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
1 efrain 2263
    global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2264
 
2265
    // Must not redirect when byteserving already started.
2266
    if (!empty($_SERVER['HTTP_RANGE'])) {
2267
        $preventredirect = true;
2268
    }
2269
 
2270
    if (AJAX_SCRIPT) {
2271
        // We cannot redirect for AJAX scripts either.
2272
        $preventredirect = true;
2273
    }
2274
 
2275
    // Setup global $COURSE, themes, language and locale.
2276
    if (!empty($courseorid)) {
2277
        if (is_object($courseorid)) {
2278
            $course = $courseorid;
2279
        } else if ($courseorid == SITEID) {
1441 ariadna 2280
            $course = clone($SITE);
1 efrain 2281
        } else {
2282
            $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2283
        }
2284
        if ($cm) {
2285
            if ($cm->course != $course->id) {
2286
                throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2287
            }
2288
            // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2289
            if (!($cm instanceof cm_info)) {
2290
                // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2291
                // db queries so this is not really a performance concern, however it is obviously
2292
                // better if you use get_fast_modinfo to get the cm before calling this.
2293
                $modinfo = get_fast_modinfo($course);
2294
                $cm = $modinfo->get_cm($cm->id);
2295
            }
2296
        }
2297
    } else {
2298
        // Do not touch global $COURSE via $PAGE->set_course(),
2299
        // the reasons is we need to be able to call require_login() at any time!!
2300
        $course = $SITE;
2301
        if ($cm) {
2302
            throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2303
        }
2304
    }
2305
 
2306
    // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2307
    // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2308
    // risk leading the user back to the AJAX request URL.
2309
    if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2310
        $setwantsurltome = false;
2311
    }
2312
 
2313
    // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2314
    if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2315
        if ($preventredirect) {
2316
            throw new require_login_session_timeout_exception();
2317
        } else {
2318
            if ($setwantsurltome) {
2319
                $SESSION->wantsurl = qualified_me();
2320
            }
2321
            redirect(get_login_url());
2322
        }
2323
    }
2324
 
2325
    // If the user is not even logged in yet then make sure they are.
2326
    if (!isloggedin()) {
2327
        if ($autologinguest && !empty($CFG->autologinguests)) {
2328
            if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2329
                // Misconfigured site guest, just redirect to login page.
2330
                redirect(get_login_url());
2331
                exit; // Never reached.
2332
            }
2333
            $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2334
            complete_user_login($guest);
2335
            $USER->autologinguest = true;
2336
            $SESSION->lang = $lang;
2337
        } else {
2338
            // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2339
            if ($preventredirect) {
2340
                throw new require_login_exception('You are not logged in');
2341
            }
2342
 
2343
            if ($setwantsurltome) {
2344
                $SESSION->wantsurl = qualified_me();
2345
            }
2346
 
2347
            // Give auth plugins an opportunity to authenticate or redirect to an external login page
2348
            $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
1441 ariadna 2349
            foreach($authsequence as $authname) {
1 efrain 2350
                $authplugin = get_auth_plugin($authname);
2351
                $authplugin->pre_loginpage_hook();
2352
                if (isloggedin()) {
2353
                    if ($cm) {
2354
                        $modinfo = get_fast_modinfo($course);
2355
                        $cm = $modinfo->get_cm($cm->id);
2356
                    }
2357
                    set_access_log_user();
2358
                    break;
2359
                }
2360
            }
2361
 
2362
            // If we're still not logged in then go to the login page
2363
            if (!isloggedin()) {
2364
                redirect(get_login_url());
2365
                exit; // Never reached.
2366
            }
2367
        }
2368
    }
2369
 
2370
    // Loginas as redirection if needed.
2371
    if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2372
        if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2373
            if ($USER->loginascontext->instanceid != $course->id) {
1441 ariadna 2374
                throw new \moodle_exception('loginasonecourse', '',
2375
                    $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
1 efrain 2376
            }
2377
        }
2378
    }
2379
 
2380
    // Check whether the user should be changing password (but only if it is REALLY them).
2381
    if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2382
        $userauth = get_auth_plugin($USER->auth);
2383
        if ($userauth->can_change_password() and !$preventredirect) {
2384
            if ($setwantsurltome) {
2385
                $SESSION->wantsurl = qualified_me();
2386
            }
2387
            if ($changeurl = $userauth->change_password_url()) {
2388
                // Use plugin custom url.
2389
                redirect($changeurl);
2390
            } else {
2391
                // Use moodle internal method.
1441 ariadna 2392
                redirect($CFG->wwwroot .'/login/change_password.php');
1 efrain 2393
            }
2394
        } else if ($userauth->can_change_password()) {
2395
            throw new moodle_exception('forcepasswordchangenotice');
2396
        } else {
2397
            throw new moodle_exception('nopasswordchangeforced', 'auth');
2398
        }
2399
    }
2400
 
2401
    // Check that the user account is properly set up. If we can't redirect to
2402
    // edit their profile and this is not a WS request, perform just the lax check.
2403
    // It will allow them to use filepicker on the profile edit page.
2404
 
2405
    if ($preventredirect && !WS_SERVER) {
2406
        $usernotfullysetup = user_not_fully_set_up($USER, false);
2407
    } else {
2408
        $usernotfullysetup = user_not_fully_set_up($USER, true);
2409
    }
2410
 
2411
    if ($usernotfullysetup) {
2412
        if ($preventredirect) {
2413
            throw new moodle_exception('usernotfullysetup');
2414
        }
2415
        if ($setwantsurltome) {
2416
            $SESSION->wantsurl = qualified_me();
2417
        }
1441 ariadna 2418
        redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
1 efrain 2419
    }
2420
 
2421
    // Make sure the USER has a sesskey set up. Used for CSRF protection.
2422
    sesskey();
2423
 
2424
    if (\core\session\manager::is_loggedinas()) {
2425
        // During a "logged in as" session we should force all content to be cleaned because the
2426
        // logged in user will be viewing potentially malicious user generated content.
2427
        // See MDL-63786 for more details.
2428
        $CFG->forceclean = true;
2429
    }
2430
 
2431
    $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2432
 
2433
    // Do not bother admins with any formalities, except for activities pending deletion.
2434
    if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2435
        // Set the global $COURSE.
2436
        if ($cm) {
2437
            $PAGE->set_cm($cm, $course);
2438
            $PAGE->set_pagelayout('incourse');
2439
        } else if (!empty($courseorid)) {
2440
            $PAGE->set_course($course);
2441
        }
2442
        // Set accesstime or the user will appear offline which messes up messaging.
2443
        // Do not update access time for webservice or ajax requests.
2444
        if (!WS_SERVER && !AJAX_SCRIPT) {
2445
            user_accesstime_log($course->id);
2446
        }
2447
 
2448
        foreach ($afterlogins as $plugintype => $plugins) {
2449
            foreach ($plugins as $pluginfunction) {
2450
                $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2451
            }
2452
        }
2453
        return;
2454
    }
2455
 
2456
    // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2457
    // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2458
    if (!defined('NO_SITEPOLICY_CHECK')) {
2459
        define('NO_SITEPOLICY_CHECK', false);
2460
    }
2461
 
2462
    // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2463
    // Do not test if the script explicitly asked for skipping the site policies check.
2464
    // Or if the user auth type is webservice.
2465
    if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') {
2466
        $manager = new \core_privacy\local\sitepolicy\manager();
2467
        if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2468
            if ($preventredirect) {
2469
                throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2470
            }
2471
            if ($setwantsurltome) {
2472
                $SESSION->wantsurl = qualified_me();
2473
            }
2474
            redirect($policyurl);
2475
        }
2476
    }
2477
 
2478
    // Fetch the system context, the course context, and prefetch its child contexts.
2479
    $sysctx = context_system::instance();
2480
    $coursecontext = context_course::instance($course->id, MUST_EXIST);
2481
    if ($cm) {
2482
        $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2483
    } else {
2484
        $cmcontext = null;
2485
    }
2486
 
2487
    // If the site is currently under maintenance, then print a message.
2488
    if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2489
        if ($preventredirect) {
2490
            throw new require_login_exception('Maintenance in progress');
2491
        }
2492
        $PAGE->set_context(null);
2493
        print_maintenance_message();
2494
    }
2495
 
2496
    // Make sure the course itself is not hidden.
2497
    if ($course->id == SITEID) {
2498
        // Frontpage can not be hidden.
2499
    } else {
2500
        if (is_role_switched($course->id)) {
2501
            // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2502
        } else {
2503
            if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2504
                // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2505
                // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2506
                if ($preventredirect) {
2507
                    throw new require_login_exception('Course is hidden');
2508
                }
2509
                $PAGE->set_context(null);
2510
                // We need to override the navigation URL as the course won't have been added to the navigation and thus
2511
                // the navigation will mess up when trying to find it.
2512
                navigation_node::override_active_url(new moodle_url('/'));
1441 ariadna 2513
                notice(get_string('coursehidden'), $CFG->wwwroot .'/');
1 efrain 2514
            }
2515
        }
2516
    }
2517
 
2518
    // Is the user enrolled?
2519
    if ($course->id == SITEID) {
2520
        // Everybody is enrolled on the frontpage.
2521
    } else {
2522
        if (\core\session\manager::is_loggedinas()) {
2523
            // Make sure the REAL person can access this course first.
2524
            $realuser = \core\session\manager::get_realuser();
1441 ariadna 2525
            if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2526
                !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
1 efrain 2527
                if ($preventredirect) {
2528
                    throw new require_login_exception('Invalid course login-as access');
2529
                }
2530
                $PAGE->set_context(null);
2531
                echo $OUTPUT->header();
1441 ariadna 2532
                notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
1 efrain 2533
            }
2534
        }
2535
 
2536
        $access = false;
2537
 
2538
        if (is_role_switched($course->id)) {
2539
            // Ok, user had to be inside this course before the switch.
2540
            $access = true;
1441 ariadna 2541
 
1 efrain 2542
        } else if (is_viewing($coursecontext, $USER)) {
2543
            // Ok, no need to mess with enrol.
2544
            $access = true;
1441 ariadna 2545
 
1 efrain 2546
        } else {
2547
            if (isset($USER->enrol['enrolled'][$course->id])) {
2548
                if ($USER->enrol['enrolled'][$course->id] > time()) {
2549
                    $access = true;
2550
                    if (isset($USER->enrol['tempguest'][$course->id])) {
2551
                        unset($USER->enrol['tempguest'][$course->id]);
2552
                        remove_temp_course_roles($coursecontext);
2553
                    }
2554
                } else {
2555
                    // Expired.
2556
                    unset($USER->enrol['enrolled'][$course->id]);
2557
                }
2558
            }
2559
            if (isset($USER->enrol['tempguest'][$course->id])) {
2560
                if ($USER->enrol['tempguest'][$course->id] == 0) {
2561
                    $access = true;
2562
                } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2563
                    $access = true;
2564
                } else {
2565
                    // Expired.
2566
                    unset($USER->enrol['tempguest'][$course->id]);
2567
                    remove_temp_course_roles($coursecontext);
2568
                }
2569
            }
2570
 
2571
            if (!$access) {
2572
                // Cache not ok.
2573
                $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2574
                if ($until !== false) {
2575
                    // Active participants may always access, a timestamp in the future, 0 (always) or false.
2576
                    if ($until == 0) {
2577
                        $until = ENROL_MAX_TIMESTAMP;
2578
                    }
2579
                    $USER->enrol['enrolled'][$course->id] = $until;
2580
                    $access = true;
1441 ariadna 2581
 
1 efrain 2582
                } else if (core_course_category::can_view_course_info($course)) {
2583
                    $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
2584
                    $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
2585
                    $enrols = enrol_get_plugins(true);
2586
                    // First ask all enabled enrol instances in course if they want to auto enrol user.
2587
                    foreach ($instances as $instance) {
2588
                        if (!isset($enrols[$instance->enrol])) {
2589
                            continue;
2590
                        }
2591
                        // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
2592
                        $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2593
                        if ($until !== false) {
2594
                            if ($until == 0) {
2595
                                $until = ENROL_MAX_TIMESTAMP;
2596
                            }
2597
                            $USER->enrol['enrolled'][$course->id] = $until;
2598
                            $access = true;
2599
                            break;
2600
                        }
2601
                    }
2602
                    // If not enrolled yet try to gain temporary guest access.
2603
                    if (!$access) {
2604
                        foreach ($instances as $instance) {
2605
                            if (!isset($enrols[$instance->enrol])) {
2606
                                continue;
2607
                            }
2608
                            // Get a duration for the guest access, a timestamp in the future or false.
2609
                            $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2610
                            if ($until !== false and $until > time()) {
2611
                                $USER->enrol['tempguest'][$course->id] = $until;
2612
                                $access = true;
2613
                                break;
2614
                            }
2615
                        }
2616
                    }
2617
                } else {
2618
                    // User is not enrolled and is not allowed to browse courses here.
2619
                    if ($preventredirect) {
2620
                        throw new require_login_exception('Course is not available');
2621
                    }
2622
                    $PAGE->set_context(null);
2623
                    // We need to override the navigation URL as the course won't have been added to the navigation and thus
2624
                    // the navigation will mess up when trying to find it.
2625
                    navigation_node::override_active_url(new moodle_url('/'));
1441 ariadna 2626
                    notice(get_string('coursehidden'), $CFG->wwwroot .'/');
1 efrain 2627
                }
2628
            }
2629
        }
2630
 
2631
        if (!$access) {
2632
            if ($preventredirect) {
2633
                throw new require_login_exception('Not enrolled');
2634
            }
2635
            if ($setwantsurltome) {
2636
                $SESSION->wantsurl = qualified_me();
2637
            }
1441 ariadna 2638
            redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
1 efrain 2639
        }
2640
    }
2641
 
2642
    // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
2643
    if ($cm && $cm->deletioninprogress) {
2644
        if ($preventredirect) {
2645
            throw new moodle_exception('activityisscheduledfordeletion');
2646
        }
2647
        require_once($CFG->dirroot . '/course/lib.php');
2648
        redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
2649
    }
2650
 
2651
    // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
2652
    if ($cm && !$cm->uservisible) {
2653
        if ($preventredirect) {
2654
            throw new require_login_exception('Activity is hidden');
2655
        }
2656
        // Get the error message that activity is not available and why (if explanation can be shown to the user).
2657
        $PAGE->set_course($course);
2658
        $renderer = $PAGE->get_renderer('course');
2659
        $message = $renderer->course_section_cm_unavailable_error_message($cm);
2660
        redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
2661
    }
2662
 
2663
    // Set the global $COURSE.
2664
    if ($cm) {
2665
        $PAGE->set_cm($cm, $course);
2666
        $PAGE->set_pagelayout('incourse');
2667
    } else if (!empty($courseorid)) {
2668
        $PAGE->set_course($course);
2669
    }
2670
 
2671
    foreach ($afterlogins as $plugintype => $plugins) {
2672
        foreach ($plugins as $pluginfunction) {
2673
            $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2674
        }
2675
    }
2676
 
2677
    // Finally access granted, update lastaccess times.
2678
    // Do not update access time for webservice or ajax requests.
2679
    if (!WS_SERVER && !AJAX_SCRIPT) {
2680
        user_accesstime_log($course->id);
2681
    }
2682
}
2683
 
2684
/**
2685
 * A convenience function for where we must be logged in as admin
2686
 * @return void
2687
 */
1441 ariadna 2688
function require_admin() {
1 efrain 2689
    require_login(null, false);
2690
    require_capability('moodle/site:config', context_system::instance());
2691
}
2692
 
2693
/**
2694
 * This function just makes sure a user is logged out.
2695
 *
2696
 * @package    core_access
2697
 * @category   access
2698
 */
1441 ariadna 2699
function require_logout() {
1 efrain 2700
    global $USER, $DB;
2701
 
2702
    if (!isloggedin()) {
2703
        // This should not happen often, no need for hooks or events here.
2704
        \core\session\manager::terminate_current();
2705
        return;
2706
    }
2707
 
2708
    // Execute hooks before action.
2709
    $authplugins = array();
2710
    $authsequence = get_enabled_auth_plugins();
2711
    foreach ($authsequence as $authname) {
2712
        $authplugins[$authname] = get_auth_plugin($authname);
2713
        $authplugins[$authname]->prelogout_hook();
2714
    }
2715
 
2716
    // Store info that gets removed during logout.
2717
    $sid = session_id();
2718
    $event = \core\event\user_loggedout::create(
2719
        array(
2720
            'userid' => $USER->id,
2721
            'objectid' => $USER->id,
2722
            'other' => array('sessionid' => $sid),
2723
        )
2724
    );
1441 ariadna 2725
    $session = \core\session\manager::get_session_by_sid($sid);
2726
    if (isset($session->id)) {
1 efrain 2727
        $event->add_record_snapshot('sessions', $session);
2728
    }
2729
 
2730
    // Clone of $USER object to be used by auth plugins.
2731
    $user = fullclone($USER);
2732
 
2733
    // Delete session record and drop $_SESSION content.
2734
    \core\session\manager::terminate_current();
2735
 
2736
    // Trigger event AFTER action.
2737
    $event->trigger();
2738
 
2739
    // Hook to execute auth plugins redirection after event trigger.
2740
    foreach ($authplugins as $authplugin) {
2741
        $authplugin->postlogout_hook($user);
2742
    }
2743
}
2744
 
2745
/**
2746
 * Weaker version of require_login()
2747
 *
2748
 * This is a weaker version of {@link require_login()} which only requires login
2749
 * when called from within a course rather than the site page, unless
2750
 * the forcelogin option is turned on.
2751
 * @see require_login()
2752
 *
2753
 * @package    core_access
2754
 * @category   access
2755
 *
2756
 * @param mixed $courseorid The course object or id in question
2757
 * @param bool $autologinguest Allow autologin guests if that is wanted
2758
 * @param object $cm Course activity module if known
2759
 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2760
 *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2761
 *             in order to keep redirects working properly. MDL-14495
2762
 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2763
 * @return void
2764
 * @throws coding_exception
2765
 */
1441 ariadna 2766
function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
1 efrain 2767
    global $CFG, $PAGE, $SITE;
2768
    $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
1441 ariadna 2769
          or (!is_object($courseorid) and $courseorid == SITEID));
1 efrain 2770
    if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
2771
        // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2772
        // db queries so this is not really a performance concern, however it is obviously
2773
        // better if you use get_fast_modinfo to get the cm before calling this.
2774
        if (is_object($courseorid)) {
2775
            $course = $courseorid;
2776
        } else {
1441 ariadna 2777
            $course = clone($SITE);
1 efrain 2778
        }
2779
        $modinfo = get_fast_modinfo($course);
2780
        $cm = $modinfo->get_cm($cm->id);
2781
    }
2782
    if (!empty($CFG->forcelogin)) {
2783
        // Login required for both SITE and courses.
2784
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
1441 ariadna 2785
 
1 efrain 2786
    } else if ($issite && !empty($cm) and !$cm->uservisible) {
2787
        // Always login for hidden activities.
2788
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
1441 ariadna 2789
 
1 efrain 2790
    } else if (isloggedin() && !isguestuser()) {
2791
        // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
2792
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
1441 ariadna 2793
 
1 efrain 2794
    } else if ($issite) {
2795
        // Login for SITE not required.
2796
        // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
2797
        if (!empty($courseorid)) {
2798
            if (is_object($courseorid)) {
2799
                $course = $courseorid;
2800
            } else {
2801
                $course = clone $SITE;
2802
            }
2803
            if ($cm) {
2804
                if ($cm->course != $course->id) {
2805
                    throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
2806
                }
2807
                $PAGE->set_cm($cm, $course);
2808
                $PAGE->set_pagelayout('incourse');
2809
            } else {
2810
                $PAGE->set_course($course);
2811
            }
2812
        } else {
2813
            // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
2814
            $PAGE->set_course($PAGE->course);
2815
        }
2816
        // Do not update access time for webservice or ajax requests.
2817
        if (!WS_SERVER && !AJAX_SCRIPT) {
2818
            user_accesstime_log(SITEID);
2819
        }
2820
        return;
1441 ariadna 2821
 
1 efrain 2822
    } else {
2823
        // Course login always required.
2824
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2825
    }
2826
}
2827
 
2828
/**
2829
 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
2830
 *
2831
 * @param  string $keyvalue the key value
2832
 * @param  string $script   unique script identifier
2833
 * @param  int $instance    instance id
2834
 * @return stdClass the key entry in the user_private_key table
2835
 * @since Moodle 3.2
2836
 * @throws moodle_exception
2837
 */
1441 ariadna 2838
function validate_user_key($keyvalue, $script, $instance) {
1 efrain 2839
    global $DB;
2840
 
2841
    if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
2842
        throw new \moodle_exception('invalidkey');
2843
    }
2844
 
2845
    if (!empty($key->validuntil) and $key->validuntil < time()) {
2846
        throw new \moodle_exception('expiredkey');
2847
    }
2848
 
2849
    if ($key->iprestriction) {
2850
        $remoteaddr = getremoteaddr(null);
2851
        if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
2852
            throw new \moodle_exception('ipmismatch');
2853
        }
2854
    }
2855
    return $key;
2856
}
2857
 
2858
/**
2859
 * Require key login. Function terminates with error if key not found or incorrect.
2860
 *
2861
 * @uses NO_MOODLE_COOKIES
2862
 * @uses PARAM_ALPHANUM
2863
 * @param string $script unique script identifier
2864
 * @param int $instance optional instance id
2865
 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
2866
 * @return int Instance ID
2867
 */
1441 ariadna 2868
function require_user_key_login($script, $instance = null, $keyvalue = null) {
1 efrain 2869
    global $DB;
2870
 
2871
    if (!NO_MOODLE_COOKIES) {
2872
        throw new \moodle_exception('sessioncookiesdisable');
2873
    }
2874
 
2875
    // Extra safety.
2876
    \core\session\manager::write_close();
2877
 
2878
    if (null === $keyvalue) {
2879
        $keyvalue = required_param('key', PARAM_ALPHANUM);
2880
    }
2881
 
2882
    $key = validate_user_key($keyvalue, $script, $instance);
2883
 
2884
    if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
2885
        throw new \moodle_exception('invaliduserid');
2886
    }
2887
 
2888
    core_user::require_active_user($user, true, true);
2889
 
2890
    // Emulate normal session.
2891
    enrol_check_plugins($user, false);
2892
    \core\session\manager::set_user($user);
2893
 
2894
    // Note we are not using normal login.
2895
    if (!defined('USER_KEY_LOGIN')) {
2896
        define('USER_KEY_LOGIN', true);
2897
    }
2898
 
2899
    // Return instance id - it might be empty.
2900
    return $key->instance;
2901
}
2902
 
2903
/**
2904
 * Creates a new private user access key.
2905
 *
2906
 * @param string $script unique target identifier
2907
 * @param int $userid
2908
 * @param int $instance optional instance id
2909
 * @param string $iprestriction optional ip restricted access
2910
 * @param int $validuntil key valid only until given data
2911
 * @return string access key value
2912
 */
1441 ariadna 2913
function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
1 efrain 2914
    global $DB;
2915
 
2916
    $key = new stdClass();
2917
    $key->script        = $script;
2918
    $key->userid        = $userid;
2919
    $key->instance      = $instance;
2920
    $key->iprestriction = $iprestriction;
2921
    $key->validuntil    = $validuntil;
2922
    $key->timecreated   = time();
2923
 
2924
    // Something long and unique.
1441 ariadna 2925
    $key->value         = md5($userid.'_'.time().random_string(40));
1 efrain 2926
    while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
2927
        // Must be unique.
1441 ariadna 2928
        $key->value     = md5($userid.'_'.time().random_string(40));
1 efrain 2929
    }
2930
    $DB->insert_record('user_private_key', $key);
2931
    return $key->value;
2932
}
2933
 
2934
/**
2935
 * Delete the user's new private user access keys for a particular script.
2936
 *
2937
 * @param string $script unique target identifier
2938
 * @param int $userid
2939
 * @return void
2940
 */
1441 ariadna 2941
function delete_user_key($script, $userid) {
1 efrain 2942
    global $DB;
2943
    $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
2944
}
2945
 
2946
/**
2947
 * Gets a private user access key (and creates one if one doesn't exist).
2948
 *
2949
 * @param string $script unique target identifier
2950
 * @param int $userid
2951
 * @param int $instance optional instance id
2952
 * @param string $iprestriction optional ip restricted access
2953
 * @param int $validuntil key valid only until given date
2954
 * @return string access key value
2955
 */
1441 ariadna 2956
function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
1 efrain 2957
    global $DB;
2958
 
1441 ariadna 2959
    if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
2960
                                                         'instance' => $instance, 'iprestriction' => $iprestriction,
2961
                                                         'validuntil' => $validuntil))) {
1 efrain 2962
        return $key->value;
2963
    } else {
2964
        return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
2965
    }
2966
}
2967
 
2968
 
2969
/**
2970
 * Modify the user table by setting the currently logged in user's last login to now.
2971
 *
2972
 * @return bool Always returns true
2973
 */
1441 ariadna 2974
function update_user_login_times() {
1 efrain 2975
    global $USER, $DB, $SESSION;
2976
 
2977
    if (isguestuser()) {
2978
        // Do not update guest access times/ips for performance.
2979
        return true;
2980
    }
2981
 
2982
    if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) {
2983
        // Do not update user login time when using user key login.
2984
        return true;
2985
    }
2986
 
2987
    $now = time();
2988
 
2989
    $user = new stdClass();
2990
    $user->id = $USER->id;
2991
 
2992
    // Make sure all users that logged in have some firstaccess.
2993
    if ($USER->firstaccess == 0) {
2994
        $USER->firstaccess = $user->firstaccess = $now;
2995
    }
2996
 
2997
    // Store the previous current as lastlogin.
2998
    $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
2999
 
3000
    $USER->currentlogin = $user->currentlogin = $now;
3001
 
3002
    // Function user_accesstime_log() may not update immediately, better do it here.
3003
    $USER->lastaccess = $user->lastaccess = $now;
3004
    $SESSION->userpreviousip = $USER->lastip;
3005
    $USER->lastip = $user->lastip = getremoteaddr();
3006
 
3007
    // Note: do not call user_update_user() here because this is part of the login process,
3008
    //       the login event means that these fields were updated.
3009
    $DB->update_record('user', $user);
3010
    return true;
3011
}
3012
 
3013
/**
3014
 * Determines if a user has completed setting up their account.
3015
 *
3016
 * The lax mode (with $strict = false) has been introduced for special cases
3017
 * only where we want to skip certain checks intentionally. This is valid in
3018
 * certain mnet or ajax scenarios when the user cannot / should not be
3019
 * redirected to edit their profile. In most cases, you should perform the
3020
 * strict check.
3021
 *
3022
 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3023
 * @param bool $strict Be more strict and assert id and custom profile fields set, too
3024
 * @return bool
3025
 */
1441 ariadna 3026
function user_not_fully_set_up($user, $strict = true) {
1 efrain 3027
    global $CFG, $SESSION, $USER;
1441 ariadna 3028
    require_once($CFG->dirroot.'/user/profile/lib.php');
1 efrain 3029
 
3030
    // If the user is setup then store this in the session to avoid re-checking.
3031
    // Some edge cases are when the users email starts to bounce or the
3032
    // configuration for custom fields has changed while they are logged in so
3033
    // we re-check this fully every hour for the rare cases it has changed.
1441 ariadna 3034
    if (isset($USER->id) && isset($user->id) && $USER->id === $user->id &&
3035
         isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS) {
1 efrain 3036
        return false;
3037
    }
3038
 
3039
    if (isguestuser($user)) {
3040
        return false;
3041
    }
3042
 
3043
    if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
3044
        return true;
3045
    }
3046
 
3047
    if ($strict) {
3048
        if (empty($user->id)) {
3049
            // Strict mode can be used with existing accounts only.
3050
            return true;
3051
        }
3052
        if (!profile_has_required_custom_fields_set($user->id)) {
3053
            return true;
3054
        }
3055
        if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) {
3056
            $SESSION->fullysetupstrict = time();
3057
        }
3058
    }
3059
 
3060
    return false;
3061
}
3062
 
3063
/**
3064
 * Check whether the user has exceeded the bounce threshold
3065
 *
3066
 * @param stdClass $user A {@link $USER} object
3067
 * @return bool true => User has exceeded bounce threshold
3068
 */
1441 ariadna 3069
function over_bounce_threshold($user) {
1 efrain 3070
    global $CFG, $DB;
3071
 
3072
    if (empty($CFG->handlebounces)) {
3073
        return false;
3074
    }
3075
 
3076
    if (empty($user->id)) {
3077
        // No real (DB) user, nothing to do here.
3078
        return false;
3079
    }
3080
 
3081
    // Set sensible defaults.
3082
    if (empty($CFG->minbounces)) {
3083
        $CFG->minbounces = 10;
3084
    }
3085
    if (empty($CFG->bounceratio)) {
3086
        $CFG->bounceratio = .20;
3087
    }
3088
    $bouncecount = 0;
3089
    $sendcount = 0;
1441 ariadna 3090
    if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
1 efrain 3091
        $bouncecount = $bounce->value;
3092
    }
3093
    if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3094
        $sendcount = $send->value;
3095
    }
1441 ariadna 3096
    return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
1 efrain 3097
}
3098
 
3099
/**
3100
 * Used to increment or reset email sent count
3101
 *
3102
 * @param stdClass $user object containing an id
3103
 * @param bool $reset will reset the count to 0
3104
 * @return void
3105
 */
1441 ariadna 3106
function set_send_count($user, $reset=false) {
1 efrain 3107
    global $DB;
3108
 
3109
    if (empty($user->id)) {
3110
        // No real (DB) user, nothing to do here.
3111
        return;
3112
    }
3113
 
3114
    if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
1441 ariadna 3115
        $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
1 efrain 3116
        $DB->update_record('user_preferences', $pref);
3117
    } else if (!empty($reset)) {
3118
        // If it's not there and we're resetting, don't bother. Make a new one.
3119
        $pref = new stdClass();
3120
        $pref->name   = 'email_send_count';
3121
        $pref->value  = 1;
3122
        $pref->userid = $user->id;
3123
        $DB->insert_record('user_preferences', $pref, false);
3124
    }
3125
}
3126
 
3127
/**
3128
 * Increment or reset user's email bounce count
3129
 *
3130
 * @param stdClass $user object containing an id
3131
 * @param bool $reset will reset the count to 0
3132
 */
1441 ariadna 3133
function set_bounce_count($user, $reset=false) {
1 efrain 3134
    global $DB;
3135
 
3136
    if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
1441 ariadna 3137
        $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
1 efrain 3138
        $DB->update_record('user_preferences', $pref);
3139
    } else if (!empty($reset)) {
3140
        // If it's not there and we're resetting, don't bother. Make a new one.
3141
        $pref = new stdClass();
3142
        $pref->name   = 'email_bounce_count';
3143
        $pref->value  = 1;
3144
        $pref->userid = $user->id;
3145
        $DB->insert_record('user_preferences', $pref, false);
3146
    }
3147
}
3148
 
3149
/**
3150
 * Determines if the logged in user is currently moving an activity
3151
 *
3152
 * @param int $courseid The id of the course being tested
3153
 * @return bool
3154
 */
1441 ariadna 3155
function ismoving($courseid) {
1 efrain 3156
    global $USER;
3157
 
3158
    if (!empty($USER->activitycopy)) {
3159
        return ($USER->activitycopycourse == $courseid);
3160
    }
3161
    return false;
3162
}
3163
 
3164
/**
3165
 * Returns a persons full name
3166
 *
3167
 * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3168
 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3169
 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3170
 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3171
 *
3172
 * @param stdClass $user A {@link $USER} object to get full name of.
3173
 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3174
 * @return string
3175
 */
1441 ariadna 3176
function fullname($user, $override=false) {
1 efrain 3177
    // Note: We do not intend to deprecate this function any time soon as it is too widely used at this time.
3178
    // Uses of it should be updated to use the new API and pass updated arguments.
3179
 
3180
    // Return an empty string if there is no user.
3181
    if (empty($user)) {
3182
        return '';
3183
    }
3184
 
3185
    $options = ['override' => $override];
3186
    return core_user::get_fullname($user, null, $options);
3187
}
3188
 
3189
/**
3190
 * Reduces lines of duplicated code for getting user name fields.
3191
 *
3192
 * See also {@link user_picture::unalias()}
3193
 *
3194
 * @param object $addtoobject Object to add user name fields to.
3195
 * @param object $secondobject Object that contains user name field information.
3196
 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3197
 * @param array $additionalfields Additional fields to be matched with data in the second object.
3198
 * The key can be set to the user table field name.
3199
 * @return object User name fields.
3200
 */
1441 ariadna 3201
function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
1 efrain 3202
    $fields = [];
3203
    foreach (\core_user\fields::get_name_fields() as $field) {
3204
        $fields[$field] = $prefix . $field;
3205
    }
3206
    if ($additionalfields) {
3207
        // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3208
        // the key is a number and then sets the key to the array value.
3209
        foreach ($additionalfields as $key => $value) {
3210
            if (is_numeric($key)) {
3211
                $additionalfields[$value] = $prefix . $value;
3212
                unset($additionalfields[$key]);
3213
            } else {
3214
                $additionalfields[$key] = $prefix . $value;
3215
            }
3216
        }
3217
        $fields = array_merge($fields, $additionalfields);
3218
    }
3219
    foreach ($fields as $key => $field) {
3220
        // Important that we have all of the user name fields present in the object that we are sending back.
3221
        $addtoobject->$key = '';
3222
        if (isset($secondobject->$field)) {
3223
            $addtoobject->$key = $secondobject->$field;
3224
        }
3225
    }
3226
    return $addtoobject;
3227
}
3228
 
3229
/**
3230
 * Returns an array of values in order of occurance in a provided string.
3231
 * The key in the result is the character postion in the string.
3232
 *
3233
 * @param array $values Values to be found in the string format
3234
 * @param string $stringformat The string which may contain values being searched for.
3235
 * @return array An array of values in order according to placement in the string format.
3236
 */
1441 ariadna 3237
function order_in_string($values, $stringformat) {
1 efrain 3238
    $valuearray = array();
3239
    foreach ($values as $value) {
3240
        $pattern = "/$value\b/";
3241
        // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3242
        if (preg_match($pattern, $stringformat)) {
3243
            $replacement = "thing";
3244
            // Replace the value with something more unique to ensure we get the right position when using strpos().
3245
            $newformat = preg_replace($pattern, $replacement, $stringformat);
3246
            $position = strpos($newformat, $replacement);
3247
            $valuearray[$position] = $value;
3248
        }
3249
    }
3250
    ksort($valuearray);
3251
    return $valuearray;
3252
}
3253
 
3254
/**
3255
 * Returns whether a given authentication plugin exists.
3256
 *
3257
 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3258
 * @return boolean Whether the plugin is available.
3259
 */
1441 ariadna 3260
function exists_auth_plugin($auth) {
1 efrain 3261
    global $CFG;
3262
 
3263
    if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3264
        return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3265
    }
3266
    return false;
3267
}
3268
 
3269
/**
3270
 * Checks if a given plugin is in the list of enabled authentication plugins.
3271
 *
3272
 * @param string $auth Authentication plugin.
3273
 * @return boolean Whether the plugin is enabled.
3274
 */
1441 ariadna 3275
function is_enabled_auth($auth) {
1 efrain 3276
    if (empty($auth)) {
3277
        return false;
3278
    }
3279
 
3280
    $enabled = get_enabled_auth_plugins();
3281
 
3282
    return in_array($auth, $enabled);
3283
}
3284
 
3285
/**
3286
 * Returns an authentication plugin instance.
3287
 *
3288
 * @param string $auth name of authentication plugin
3289
 * @return auth_plugin_base An instance of the required authentication plugin.
3290
 */
1441 ariadna 3291
function get_auth_plugin($auth) {
1 efrain 3292
    global $CFG;
3293
 
3294
    // Check the plugin exists first.
3295
    if (! exists_auth_plugin($auth)) {
3296
        throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth);
3297
    }
3298
 
3299
    // Return auth plugin instance.
3300
    require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3301
    $class = "auth_plugin_$auth";
3302
    return new $class;
3303
}
3304
 
3305
/**
3306
 * Returns array of active auth plugins.
3307
 *
3308
 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3309
 * @return array
3310
 */
1441 ariadna 3311
function get_enabled_auth_plugins($fix=false) {
1 efrain 3312
    global $CFG;
3313
 
3314
    $default = array('manual', 'nologin');
3315
 
3316
    if (empty($CFG->auth)) {
3317
        $auths = array();
3318
    } else {
3319
        $auths = explode(',', $CFG->auth);
3320
    }
3321
 
3322
    $auths = array_unique($auths);
3323
    $oldauthconfig = implode(',', $auths);
3324
    foreach ($auths as $k => $authname) {
3325
        if (in_array($authname, $default)) {
3326
            // The manual and nologin plugin never need to be stored.
3327
            unset($auths[$k]);
3328
        } else if (!exists_auth_plugin($authname)) {
3329
            debugging(get_string('authpluginnotfound', 'debug', $authname));
3330
            unset($auths[$k]);
3331
        }
3332
    }
3333
 
3334
    // Ideally only explicit interaction from a human admin should trigger a
3335
    // change in auth config, see MDL-70424 for details.
3336
    if ($fix) {
3337
        $newconfig = implode(',', $auths);
3338
        if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3339
            add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
3340
            set_config('auth', $newconfig);
3341
        }
3342
    }
3343
 
3344
    return (array_merge($default, $auths));
3345
}
3346
 
3347
/**
3348
 * Returns true if an internal authentication method is being used.
3349
 * if method not specified then, global default is assumed
3350
 *
3351
 * @param string $auth Form of authentication required
3352
 * @return bool
3353
 */
1441 ariadna 3354
function is_internal_auth($auth) {
1 efrain 3355
    // Throws error if bad $auth.
3356
    $authplugin = get_auth_plugin($auth);
3357
    return $authplugin->is_internal();
3358
}
3359
 
3360
/**
3361
 * Returns true if the user is a 'restored' one.
3362
 *
3363
 * Used in the login process to inform the user and allow him/her to reset the password
3364
 *
3365
 * @param string $username username to be checked
3366
 * @return bool
3367
 */
1441 ariadna 3368
function is_restored_user($username) {
1 efrain 3369
    global $CFG, $DB;
3370
 
3371
    return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3372
}
3373
 
3374
/**
3375
 * Returns an array of user fields
3376
 *
3377
 * @return array User field/column names
3378
 */
1441 ariadna 3379
function get_user_fieldnames() {
1 efrain 3380
    global $DB;
3381
 
3382
    $fieldarray = $DB->get_columns('user');
3383
    unset($fieldarray['id']);
3384
    $fieldarray = array_keys($fieldarray);
3385
 
3386
    return $fieldarray;
3387
}
3388
 
3389
/**
3390
 * Returns the string of the language for the new user.
3391
 *
3392
 * @return string language for the new user
3393
 */
1441 ariadna 3394
function get_newuser_language() {
1 efrain 3395
    global $CFG, $SESSION;
3396
    return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
3397
}
3398
 
3399
/**
3400
 * Creates a bare-bones user record
3401
 *
3402
 * @todo Outline auth types and provide code example
3403
 *
3404
 * @param string $username New user's username to add to record
3405
 * @param string $password New user's password to add to record
3406
 * @param string $auth Form of authentication required
3407
 * @return stdClass A complete user object
3408
 */
1441 ariadna 3409
function create_user_record($username, $password, $auth = 'manual') {
1 efrain 3410
    global $CFG, $DB, $SESSION;
1441 ariadna 3411
    require_once($CFG->dirroot.'/user/profile/lib.php');
3412
    require_once($CFG->dirroot.'/user/lib.php');
1 efrain 3413
 
3414
    // Just in case check text case.
3415
    $username = trim(core_text::strtolower($username));
3416
 
3417
    $authplugin = get_auth_plugin($auth);
3418
    $customfields = $authplugin->get_custom_user_profile_fields();
3419
    $newuser = new stdClass();
3420
    if ($newinfo = $authplugin->get_userinfo($username)) {
3421
        $newinfo = truncate_userinfo($newinfo);
3422
        foreach ($newinfo as $key => $value) {
3423
            if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3424
                $newuser->$key = $value;
3425
            }
3426
        }
3427
    }
3428
 
3429
    if (!empty($newuser->email)) {
3430
        if (email_is_not_allowed($newuser->email)) {
3431
            unset($newuser->email);
3432
        }
3433
    }
3434
 
3435
    $newuser->auth = $auth;
3436
    $newuser->username = $username;
3437
 
3438
    // Fix for MDL-8480
3439
    // user CFG lang for user if $newuser->lang is empty
3440
    // or $user->lang is not an installed language.
3441
    if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3442
        $newuser->lang = get_newuser_language();
3443
    }
3444
    $newuser->confirmed = 1;
3445
    $newuser->lastip = getremoteaddr();
3446
    $newuser->timecreated = time();
3447
    $newuser->timemodified = $newuser->timecreated;
3448
    $newuser->mnethostid = $CFG->mnet_localhost_id;
3449
 
3450
    $newuser->id = user_create_user($newuser, false, false);
3451
 
3452
    // Save user profile data.
3453
    profile_save_data($newuser);
3454
 
3455
    $user = get_complete_user_data('id', $newuser->id);
1441 ariadna 3456
    if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
1 efrain 3457
        set_user_preference('auth_forcepasswordchange', 1, $user);
3458
    }
3459
    // Set the password.
3460
    update_internal_user_password($user, $password);
3461
 
3462
    // Trigger event.
3463
    \core\event\user_created::create_from_userid($newuser->id)->trigger();
3464
 
3465
    return $user;
3466
}
3467
 
3468
/**
3469
 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3470
 *
3471
 * @param string $username user's username to update the record
3472
 * @return stdClass A complete user object
3473
 */
1441 ariadna 3474
function update_user_record($username) {
1 efrain 3475
    global $DB, $CFG;
3476
    // Just in case check text case.
3477
    $username = trim(core_text::strtolower($username));
3478
 
3479
    $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
3480
    return update_user_record_by_id($oldinfo->id);
3481
}
3482
 
3483
/**
3484
 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3485
 *
3486
 * @param int $id user id
3487
 * @return stdClass A complete user object
3488
 */
1441 ariadna 3489
function update_user_record_by_id($id) {
1 efrain 3490
    global $DB, $CFG;
1441 ariadna 3491
    require_once($CFG->dirroot."/user/profile/lib.php");
3492
    require_once($CFG->dirroot.'/user/lib.php');
1 efrain 3493
 
3494
    $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
3495
    $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
3496
 
3497
    $newuser = array();
3498
    $userauth = get_auth_plugin($oldinfo->auth);
3499
 
3500
    if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
3501
        $newinfo = truncate_userinfo($newinfo);
3502
        $customfields = $userauth->get_custom_user_profile_fields();
3503
 
3504
        foreach ($newinfo as $key => $value) {
3505
            $iscustom = in_array($key, $customfields);
3506
            if (!$iscustom) {
3507
                $key = strtolower($key);
3508
            }
3509
            if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
1441 ariadna 3510
                    or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
1 efrain 3511
                // Unknown or must not be changed.
3512
                continue;
3513
            }
3514
            if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
3515
                continue;
3516
            }
3517
            $confval = $userauth->config->{'field_updatelocal_' . $key};
3518
            $lockval = $userauth->config->{'field_lock_' . $key};
3519
            if ($confval === 'onlogin') {
3520
                // MDL-4207 Don't overwrite modified user profile values with
3521
                // empty LDAP values when 'unlocked if empty' is set. The purpose
3522
                // of the setting 'unlocked if empty' is to allow the user to fill
3523
                // in a value for the selected field _if LDAP is giving
3524
                // nothing_ for this field. Thus it makes sense to let this value
3525
                // stand in until LDAP is giving a value for this field.
3526
                if (!(empty($value) && $lockval === 'unlockedifempty')) {
3527
                    if ($iscustom || (in_array($key, $userauth->userfields) &&
1441 ariadna 3528
                            ((string)$oldinfo->$key !== (string)$value))) {
1 efrain 3529
                        $newuser[$key] = (string)$value;
3530
                    }
3531
                }
3532
            }
3533
        }
3534
        if ($newuser) {
3535
            $newuser['id'] = $oldinfo->id;
3536
            $newuser['timemodified'] = time();
3537
            user_update_user((object) $newuser, false, false);
3538
 
3539
            // Save user profile data.
3540
            profile_save_data((object) $newuser);
3541
 
3542
            // Trigger event.
3543
            \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
3544
        }
3545
    }
3546
 
3547
    return get_complete_user_data('id', $oldinfo->id);
3548
}
3549
 
3550
/**
3551
 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
3552
 *
3553
 * @param array $info Array of user properties to truncate if needed
3554
 * @return array The now truncated information that was passed in
3555
 */
1441 ariadna 3556
function truncate_userinfo(array $info) {
1 efrain 3557
    // Define the limits.
3558
    $limit = array(
3559
        'username'    => 100,
3560
        'idnumber'    => 255,
3561
        'firstname'   => 100,
3562
        'lastname'    => 100,
3563
        'email'       => 100,
3564
        'phone1'      =>  20,
3565
        'phone2'      =>  20,
3566
        'institution' => 255,
3567
        'department'  => 255,
3568
        'address'     => 255,
3569
        'city'        => 120,
3570
        'country'     =>   2,
3571
    );
3572
 
3573
    // Apply where needed.
3574
    foreach (array_keys($info) as $key) {
3575
        if (!empty($limit[$key])) {
3576
            $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
3577
        }
3578
    }
3579
 
3580
    return $info;
3581
}
3582
 
3583
/**
3584
 * Marks user deleted in internal user database and notifies the auth plugin.
3585
 * Also unenrols user from all roles and does other cleanup.
3586
 *
3587
 * Any plugin that needs to purge user data should register the 'user_deleted' event.
3588
 *
3589
 * @param stdClass $user full user object before delete
3590
 * @return boolean success
3591
 * @throws coding_exception if invalid $user parameter detected
3592
 */
1441 ariadna 3593
function delete_user(stdClass $user) {
1 efrain 3594
    global $CFG, $DB, $SESSION;
1441 ariadna 3595
    require_once($CFG->libdir.'/grouplib.php');
3596
    require_once($CFG->libdir.'/gradelib.php');
3597
    require_once($CFG->dirroot.'/message/lib.php');
3598
    require_once($CFG->dirroot.'/user/lib.php');
1 efrain 3599
 
3600
    // Make sure nobody sends bogus record type as parameter.
3601
    if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
3602
        throw new coding_exception('Invalid $user parameter in delete_user() detected');
3603
    }
3604
 
3605
    // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
3606
    if (!$user = $DB->get_record('user', array('id' => $user->id))) {
3607
        debugging('Attempt to delete unknown user account.');
3608
        return false;
3609
    }
3610
 
3611
    // There must be always exactly one guest record, originally the guest account was identified by username only,
3612
    // now we use $CFG->siteguest for performance reasons.
3613
    if ($user->username === 'guest' or isguestuser($user)) {
3614
        debugging('Guest user account can not be deleted.');
3615
        return false;
3616
    }
3617
 
3618
    // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
3619
    // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
3620
    if ($user->auth === 'manual' and is_siteadmin($user)) {
3621
        debugging('Local administrator accounts can not be deleted.');
3622
        return false;
3623
    }
3624
    // Allow plugins to use this user object before we completely delete it.
3625
    if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
3626
        foreach ($pluginsfunction as $plugintype => $plugins) {
3627
            foreach ($plugins as $pluginfunction) {
3628
                $pluginfunction($user);
3629
            }
3630
        }
3631
    }
3632
 
3633
    // Dispatch the hook for pre user update actions.
3634
    $hook = new \core_user\hook\before_user_deleted(
3635
        user: $user,
3636
    );
3637
    di::get(hook\manager::class)->dispatch($hook);
3638
 
3639
    // Keep user record before updating it, as we have to pass this to user_deleted event.
3640
    $olduser = clone $user;
3641
 
3642
    // Keep a copy of user context, we need it for event.
3643
    $usercontext = context_user::instance($user->id);
3644
 
3645
    // Delete all grades - backup is kept in grade_grades_history table.
3646
    grade_user_delete($user->id);
3647
 
3648
    // TODO: remove from cohorts using standard API here.
3649
 
3650
    // Remove user tags.
3651
    core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
3652
 
3653
    // Unconditionally unenrol from all courses.
3654
    enrol_user_delete($user);
3655
 
3656
    // Unenrol from all roles in all contexts.
3657
    // This might be slow but it is really needed - modules might do some extra cleanup!
3658
    role_unassign_all(array('userid' => $user->id));
3659
 
3660
    // Notify the competency subsystem.
3661
    \core_competency\api::hook_user_deleted($user->id);
3662
 
3663
    // Now do a brute force cleanup.
3664
 
3665
    // Delete all user events and subscription events.
3666
    $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
3667
 
3668
    // Now, delete all calendar subscription from the user.
3669
    $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
3670
 
3671
    // Remove from all cohorts.
3672
    $DB->delete_records('cohort_members', array('userid' => $user->id));
3673
 
3674
    // Remove from all groups.
3675
    $DB->delete_records('groups_members', array('userid' => $user->id));
3676
 
3677
    // Brute force unenrol from all courses.
3678
    $DB->delete_records('user_enrolments', array('userid' => $user->id));
3679
 
3680
    // Purge user preferences.
3681
    $DB->delete_records('user_preferences', array('userid' => $user->id));
3682
 
3683
    // Purge user extra profile info.
3684
    $DB->delete_records('user_info_data', array('userid' => $user->id));
3685
 
3686
    // Purge log of previous password hashes.
3687
    $DB->delete_records('user_password_history', array('userid' => $user->id));
3688
 
3689
    // Last course access not necessary either.
3690
    $DB->delete_records('user_lastaccess', array('userid' => $user->id));
3691
    // Remove all user tokens.
3692
    $DB->delete_records('external_tokens', array('userid' => $user->id));
3693
 
3694
    // Unauthorise the user for all services.
3695
    $DB->delete_records('external_services_users', array('userid' => $user->id));
3696
 
3697
    // Remove users private keys.
3698
    $DB->delete_records('user_private_key', array('userid' => $user->id));
3699
 
3700
    // Remove users customised pages.
3701
    $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
3702
 
3703
    // Remove user's oauth2 refresh tokens, if present.
3704
    $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
3705
 
3706
    // Delete user from $SESSION->bulk_users.
3707
    if (isset($SESSION->bulk_users[$user->id])) {
3708
        unset($SESSION->bulk_users[$user->id]);
3709
    }
3710
 
3711
    // Force logout - may fail if file based sessions used, sorry.
1441 ariadna 3712
    \core\session\manager::destroy_user_sessions($user->id);
1 efrain 3713
 
3714
    // Generate username from email address, or a fake email.
3715
    $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
3716
 
3717
    $deltime = time();
3718
 
3719
    // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
3720
    $delnameprefix = clean_param($delemail, PARAM_USERNAME);
3721
    $delnamesuffix = $deltime;
3722
    $delnamesuffixlength = 10;
3723
    do {
3724
        // Workaround for bulk deletes of users with the same email address.
3725
        $delname = sprintf(
3726
            "%s.%10d",
3727
            core_text::substr(
3728
                $delnameprefix,
3729
                0,
3730
                // 100 Character maximum, with a '.' character, and a 10-digit timestamp.
3731
                100 - 1 - $delnamesuffixlength,
3732
            ),
3733
            $delnamesuffix,
3734
        );
3735
        $delnamesuffix++;
3736
 
3737
        // No need to use mnethostid here.
3738
    } while ($DB->record_exists('user', ['username' => $delname]));
3739
 
3740
    // Mark internal user record as "deleted".
3741
    $updateuser = new stdClass();
3742
    $updateuser->id           = $user->id;
3743
    $updateuser->deleted      = 1;
3744
    $updateuser->username     = $delname;            // Remember it just in case.
1441 ariadna 3745
    $updateuser->email        = md5($user->username);// Store hash of username, useful importing/restoring users.
1 efrain 3746
    $updateuser->idnumber     = '';                  // Clear this field to free it up.
3747
    $updateuser->picture      = 0;
3748
    $updateuser->timemodified = $deltime;
3749
 
3750
    // Don't trigger update event, as user is being deleted.
3751
    user_update_user($updateuser, false, false);
3752
 
3753
    // Delete all content associated with the user context, but not the context itself.
3754
    $usercontext->delete_content();
3755
 
3756
    // Delete any search data.
3757
    \core_search\manager::context_deleted($usercontext);
3758
 
3759
    // Any plugin that needs to cleanup should register this event.
3760
    // Trigger event.
3761
    $event = \core\event\user_deleted::create(
1441 ariadna 3762
            array(
3763
                'objectid' => $user->id,
3764
                'relateduserid' => $user->id,
3765
                'context' => $usercontext,
3766
                'other' => array(
3767
                    'username' => $user->username,
3768
                    'email' => $user->email,
3769
                    'idnumber' => $user->idnumber,
3770
                    'picture' => $user->picture,
3771
                    'mnethostid' => $user->mnethostid
3772
                    )
3773
                )
3774
            );
1 efrain 3775
    $event->add_record_snapshot('user', $olduser);
3776
    $event->trigger();
3777
 
3778
    // We will update the user's timemodified, as it will be passed to the user_deleted event, which
3779
    // should know about this updated property persisted to the user's table.
3780
    $user->timemodified = $updateuser->timemodified;
3781
 
3782
    // Notify auth plugin - do not block the delete even when plugin fails.
3783
    $authplugin = get_auth_plugin($user->auth);
3784
    $authplugin->user_delete($user);
3785
 
3786
    return true;
3787
}
3788
 
3789
/**
3790
 * Retrieve the guest user object.
3791
 *
3792
 * @return stdClass A {@link $USER} object
3793
 */
1441 ariadna 3794
function guest_user() {
1 efrain 3795
    global $CFG, $DB;
3796
 
3797
    if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
3798
        $newuser->confirmed = 1;
3799
        $newuser->lang = get_newuser_language();
3800
        $newuser->lastip = getremoteaddr();
3801
    }
3802
 
3803
    return $newuser;
3804
}
3805
 
3806
/**
3807
 * Authenticates a user against the chosen authentication mechanism
3808
 *
3809
 * Given a username and password, this function looks them
3810
 * up using the currently selected authentication mechanism,
3811
 * and if the authentication is successful, it returns a
3812
 * valid $user object from the 'user' table.
3813
 *
3814
 * Uses auth_ functions from the currently active auth module
3815
 *
3816
 * After authenticate_user_login() returns success, you will need to
3817
 * log that the user has logged in, and call complete_user_login() to set
3818
 * the session up.
3819
 *
3820
 * Note: this function works only with non-mnet accounts!
3821
 *
3822
 * @param string $username  User's username (or also email if $CFG->authloginviaemail enabled)
3823
 * @param string $password  User's password
3824
 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
3825
 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
3826
 * @param string|bool $logintoken If this is set to a string it is validated against the login token for the session.
3827
 * @param string|bool $loginrecaptcha If this is set to a string it is validated against Google reCaptcha.
3828
 * @return stdClass|false A {@link $USER} object or false if error
3829
 */
3830
function authenticate_user_login(
3831
    $username,
3832
    $password,
3833
    $ignorelockout = false,
3834
    &$failurereason = null,
3835
    $logintoken = false,
3836
    string|bool $loginrecaptcha = false,
3837
) {
3838
    global $CFG, $DB, $PAGE, $SESSION;
3839
    require_once("$CFG->libdir/authlib.php");
3840
 
3841
    if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
3842
        // we have found the user
3843
 
3844
    } else if (!empty($CFG->authloginviaemail)) {
3845
        if ($email = clean_param($username, PARAM_EMAIL)) {
3846
            $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
3847
            $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
3848
            $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
3849
            if (count($users) === 1) {
3850
                // Use email for login only if unique.
3851
                $user = reset($users);
3852
                $user = get_complete_user_data('id', $user->id);
3853
                $username = $user->username;
3854
            }
3855
            unset($users);
3856
        }
3857
    }
3858
 
3859
    // Make sure this request came from the login form.
3860
    if (!\core\session\manager::validate_login_token($logintoken)) {
3861
        $failurereason = AUTH_LOGIN_FAILED;
3862
 
3863
        // Trigger login failed event (specifying the ID of the found user, if available).
3864
        \core\event\user_login_failed::create([
3865
            'userid' => ($user->id ?? 0),
3866
            'other' => [
3867
                'username' => $username,
3868
                'reason' => $failurereason,
3869
            ],
3870
        ])->trigger();
3871
 
1441 ariadna 3872
        error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Invalid Login Token:  $username  ".$_SERVER['HTTP_USER_AGENT']);
1 efrain 3873
        return false;
3874
    }
3875
 
3876
    // Login reCaptcha.
3877
    if (login_captcha_enabled() && !validate_login_captcha($loginrecaptcha)) {
3878
        $failurereason = AUTH_LOGIN_FAILED_RECAPTCHA;
3879
        // Trigger login failed event (specifying the ID of the found user, if available).
3880
        \core\event\user_login_failed::create([
3881
            'userid' => ($user->id ?? 0),
3882
            'other' => [
3883
                'username' => $username,
3884
                'reason' => $failurereason,
3885
            ],
3886
        ])->trigger();
3887
        return false;
3888
    }
3889
 
3890
    $authsenabled = get_enabled_auth_plugins();
3891
 
3892
    if ($user) {
3893
        // Use manual if auth not set.
3894
        $auth = empty($user->auth) ? 'manual' : $user->auth;
3895
 
3896
        if (in_array($user->auth, $authsenabled)) {
3897
            $authplugin = get_auth_plugin($user->auth);
3898
            $authplugin->pre_user_login_hook($user);
3899
        }
3900
 
3901
        if (!empty($user->suspended)) {
3902
            $failurereason = AUTH_LOGIN_SUSPENDED;
3903
 
3904
            // Trigger login failed event.
1441 ariadna 3905
            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3906
                    'other' => array('username' => $username, 'reason' => $failurereason)));
1 efrain 3907
            $event->trigger();
1441 ariadna 3908
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
1 efrain 3909
            return false;
3910
        }
1441 ariadna 3911
        if ($auth=='nologin' or !is_enabled_auth($auth)) {
1 efrain 3912
            // Legacy way to suspend user.
3913
            $failurereason = AUTH_LOGIN_SUSPENDED;
3914
 
3915
            // Trigger login failed event.
1441 ariadna 3916
            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3917
                    'other' => array('username' => $username, 'reason' => $failurereason)));
1 efrain 3918
            $event->trigger();
1441 ariadna 3919
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
1 efrain 3920
            return false;
3921
        }
3922
        $auths = array($auth);
1441 ariadna 3923
 
1 efrain 3924
    } else {
3925
        // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
3926
        if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
3927
            $failurereason = AUTH_LOGIN_NOUSER;
3928
 
3929
            // Trigger login failed event.
1441 ariadna 3930
            $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
3931
                    'reason' => $failurereason)));
1 efrain 3932
            $event->trigger();
1441 ariadna 3933
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
1 efrain 3934
            return false;
3935
        }
3936
 
3937
        // User does not exist.
3938
        $auths = $authsenabled;
3939
        $user = new stdClass();
3940
        $user->id = 0;
3941
    }
3942
 
3943
    if ($ignorelockout) {
3944
        // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
3945
        // or this function is called from a SSO script.
3946
    } else if ($user->id) {
3947
        // Verify login lockout after other ways that may prevent user login.
3948
        if (login_is_lockedout($user)) {
3949
            $failurereason = AUTH_LOGIN_LOCKOUT;
3950
 
3951
            // Trigger login failed event.
1441 ariadna 3952
            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3953
                    'other' => array('username' => $username, 'reason' => $failurereason)));
1 efrain 3954
            $event->trigger();
3955
 
1441 ariadna 3956
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
1 efrain 3957
            $SESSION->loginerrormsg = get_string('accountlocked', 'admin');
3958
 
3959
            return false;
3960
        }
3961
    } else {
3962
        // We can not lockout non-existing accounts.
3963
    }
3964
 
3965
    foreach ($auths as $auth) {
3966
        $authplugin = get_auth_plugin($auth);
3967
 
3968
        // On auth fail fall through to the next plugin.
3969
        if (!$authplugin->user_login($username, $password)) {
3970
            continue;
3971
        }
3972
 
3973
        // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
3974
        if (!empty($CFG->passwordpolicycheckonlogin)) {
3975
            $errmsg = '';
3976
            $passed = check_password_policy($password, $errmsg, $user);
3977
            if (!$passed) {
3978
                // First trigger event for failure.
3979
                $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
3980
                $failedevent->trigger();
3981
 
3982
                // If able to change password, set flag and move on.
3983
                if ($authplugin->can_change_password()) {
3984
                    // Check if we are on internal change password page, or service is external, don't show notification.
3985
                    $internalchangeurl = new moodle_url('/login/change_password.php');
3986
                    if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
3987
                        \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
3988
                    }
3989
                    set_user_preference('auth_forcepasswordchange', 1, $user);
3990
                } else if ($authplugin->can_reset_password()) {
3991
                    // Else force a reset if possible.
3992
                    \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
3993
                    redirect(new moodle_url('/login/forgot_password.php'));
3994
                } else {
3995
                    $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
3996
                    // If support page is set, add link for help.
3997
                    if (!empty($CFG->supportpage)) {
3998
                        $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
3999
                        $link = \html_writer::tag('p', $link);
4000
                        $notifymsg .= $link;
4001
                    }
4002
 
4003
                    // If no change or reset is possible, add a notification for user.
4004
                    \core\notification::error($notifymsg);
4005
                }
4006
            }
4007
        }
4008
 
4009
        // Successful authentication.
4010
        if ($user->id) {
4011
            // User already exists in database.
4012
            if (empty($user->auth)) {
4013
                // For some reason auth isn't set yet.
4014
                $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
4015
                $user->auth = $auth;
4016
            }
4017
 
4018
            // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4019
            // the current hash algorithm while we have access to the user's password.
4020
            update_internal_user_password($user, $password);
4021
 
4022
            if ($authplugin->is_synchronised_with_external()) {
4023
                // Update user record from external DB.
4024
                $user = update_user_record_by_id($user->id);
4025
            }
4026
        } else {
4027
            // The user is authenticated but user creation may be disabled.
4028
            if (!empty($CFG->authpreventaccountcreation)) {
4029
                $failurereason = AUTH_LOGIN_UNAUTHORISED;
4030
 
4031
                // Trigger login failed event.
1441 ariadna 4032
                $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4033
                        'reason' => $failurereason)));
1 efrain 4034
                $event->trigger();
4035
 
1441 ariadna 4036
                error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".
4037
                        $_SERVER['HTTP_USER_AGENT']);
1 efrain 4038
                return false;
4039
            } else {
4040
                $user = create_user_record($username, $password, $auth);
4041
            }
4042
        }
4043
 
4044
        $authplugin->sync_roles($user);
4045
 
4046
        foreach ($authsenabled as $hau) {
4047
            $hauth = get_auth_plugin($hau);
4048
            $hauth->user_authenticated_hook($user, $username, $password);
4049
        }
4050
 
4051
        if (empty($user->id)) {
4052
            $failurereason = AUTH_LOGIN_NOUSER;
4053
            // Trigger login failed event.
1441 ariadna 4054
            $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4055
                    'reason' => $failurereason)));
1 efrain 4056
            $event->trigger();
4057
            return false;
4058
        }
4059
 
4060
        if (!empty($user->suspended)) {
4061
            // Just in case some auth plugin suspended account.
4062
            $failurereason = AUTH_LOGIN_SUSPENDED;
4063
            // Trigger login failed event.
1441 ariadna 4064
            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4065
                    'other' => array('username' => $username, 'reason' => $failurereason)));
1 efrain 4066
            $event->trigger();
1441 ariadna 4067
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
1 efrain 4068
            return false;
4069
        }
4070
 
4071
        login_attempt_valid($user);
4072
        $failurereason = AUTH_LOGIN_OK;
4073
        return $user;
4074
    }
4075
 
4076
    // Failed if all the plugins have failed.
4077
    if (debugging('', DEBUG_ALL)) {
1441 ariadna 4078
        error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Failed Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
1 efrain 4079
    }
4080
 
4081
    if ($user->id) {
4082
        login_attempt_failed($user);
4083
        $failurereason = AUTH_LOGIN_FAILED;
4084
        // Trigger login failed event.
1441 ariadna 4085
        $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4086
                'other' => array('username' => $username, 'reason' => $failurereason)));
1 efrain 4087
        $event->trigger();
4088
    } else {
4089
        $failurereason = AUTH_LOGIN_NOUSER;
4090
        // Trigger login failed event.
1441 ariadna 4091
        $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4092
                'reason' => $failurereason)));
1 efrain 4093
        $event->trigger();
4094
    }
4095
 
4096
    return false;
4097
}
4098
 
4099
/**
4100
 * Call to complete the user login process after authenticate_user_login()
4101
 * has succeeded. It will setup the $USER variable and other required bits
4102
 * and pieces.
4103
 *
4104
 * NOTE:
4105
 * - It will NOT log anything -- up to the caller to decide what to log.
4106
 * - this function does not set any cookies any more!
4107
 *
4108
 * @param stdClass $user
4109
 * @param array $extrauserinfo
4110
 * @return stdClass A {@link $USER} object - BC only, do not use
4111
 */
1441 ariadna 4112
function complete_user_login($user, array $extrauserinfo = []) {
1 efrain 4113
    global $CFG, $DB, $USER, $SESSION;
4114
 
4115
    \core\session\manager::login_user($user);
4116
 
4117
    // Reload preferences from DB.
4118
    unset($USER->preference);
4119
    check_user_preferences_loaded($USER);
4120
 
4121
    // Update login times.
4122
    update_user_login_times();
4123
 
4124
    // Extra session prefs init.
4125
    set_login_session_preferences();
4126
 
4127
    // Trigger login event.
4128
    $event = \core\event\user_loggedin::create(
4129
        array(
4130
            'userid' => $USER->id,
4131
            'objectid' => $USER->id,
4132
            'other' => [
4133
                'username' => $USER->username,
4134
                'extrauserinfo' => $extrauserinfo
4135
            ]
4136
        )
4137
    );
4138
    $event->trigger();
4139
 
4140
    // Allow plugins to callback as soon possible after user has completed login.
4141
    di::get(\core\hook\manager::class)->dispatch(new \core_user\hook\after_login_completed());
4142
 
4143
    // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case).
4144
    // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser).
4145
    // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself.
4146
    $loginip = getremoteaddr();
4147
    $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip;
4148
    $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST;
4149
 
4150
    if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) {
4151
 
4152
        $logintime = time();
4153
        $ismoodleapp = false;
4154
        $useragent = \core_useragent::get_user_agent_string();
4155
 
4156
        $sitepreferences = get_message_output_default_preferences();
4157
        // Check if new login notification is disabled at system level.
4158
        $newlogindisabled = $sitepreferences->moodle_newlogin_disable ?? 0;
4159
        // Check if message providers (web, email, mobile) are enabled at system level.
4160
        $msgproviderenabled = isset($sitepreferences->message_provider_moodle_newlogin_enabled);
4161
        // Get message providers enabled for a user.
4162
        $userpreferences = get_user_preferences('message_provider_moodle_newlogin_enabled');
4163
        // Check if notification processor plugins (web, email, mobile) are enabled at system level.
4164
        $msgprocessorsready = !empty(get_message_processors(true));
4165
        // If new login notification is enabled at system level then go for other conditions check.
4166
        $newloginenabled = $newlogindisabled ? 0 : ($userpreferences != 'none' && $msgproviderenabled);
4167
 
4168
        if ($newloginenabled && $msgprocessorsready) {
4169
            // Schedule adhoc task to send a login notification to the user.
4170
            $task = new \core\task\send_login_notifications();
4171
            $task->set_userid($USER->id);
4172
            $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime'));
4173
            $task->set_component('core');
4174
            \core\task\manager::queue_adhoc_task($task);
4175
        }
4176
    }
4177
 
4178
    // Queue migrating the messaging data, if we need to.
4179
    if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4180
        // Check if there are any legacy messages to migrate.
4181
        if (\core_message\helper::legacy_messages_exist($USER->id)) {
4182
            \core_message\task\migrate_message_data::queue_task($USER->id);
4183
        } else {
4184
            set_user_preference('core_message_migrate_data', true, $USER->id);
4185
        }
4186
    }
4187
 
4188
    if (isguestuser()) {
4189
        // No need to continue when user is THE guest.
4190
        return $USER;
4191
    }
4192
 
4193
    if (CLI_SCRIPT) {
4194
        // We can redirect to password change URL only in browser.
4195
        return $USER;
4196
    }
4197
 
4198
    // Select password change url.
4199
    $userauth = get_auth_plugin($USER->auth);
4200
 
4201
    // Check whether the user should be changing password.
4202
    if (get_user_preferences('auth_forcepasswordchange', false)) {
4203
        if ($userauth->can_change_password()) {
4204
            if ($changeurl = $userauth->change_password_url()) {
4205
                redirect($changeurl);
4206
            } else {
4207
                require_once($CFG->dirroot . '/login/lib.php');
4208
                $SESSION->wantsurl = core_login_get_return_url();
1441 ariadna 4209
                redirect($CFG->wwwroot.'/login/change_password.php');
1 efrain 4210
            }
4211
        } else {
4212
            throw new \moodle_exception('nopasswordchangeforced', 'auth');
4213
        }
4214
    }
4215
    return $USER;
4216
}
4217
 
4218
/**
4219
 * Check a password hash to see if it was hashed using the legacy hash algorithm (bcrypt).
4220
 *
4221
 * @param string $password String to check.
4222
 * @return bool True if the $password matches the format of a bcrypt hash.
4223
 */
1441 ariadna 4224
function password_is_legacy_hash(#[\SensitiveParameter] string $password): bool {
1 efrain 4225
    return (bool) preg_match('/^\$2y\$[\d]{2}\$[A-Za-z0-9\.\/]{53}$/', $password);
4226
}
4227
 
4228
/**
4229
 * Calculate the Shannon entropy of a string.
4230
 *
4231
 * @param string $pepper The pepper to calculate the entropy of.
4232
 * @return float The Shannon entropy of the string.
4233
 */
1441 ariadna 4234
function calculate_entropy(#[\SensitiveParameter] string $pepper): float {
1 efrain 4235
    // Initialize entropy.
4236
    $h = 0;
4237
 
4238
    // Calculate the length of the string.
4239
    $size = strlen($pepper);
4240
 
4241
    // For each unique character in the string.
4242
    foreach (count_chars($pepper, 1) as $v) {
4243
        // Calculate the probability of the character.
4244
        $p = $v / $size;
4245
 
4246
        // Add the character's contribution to the total entropy.
4247
        // This uses the formula for the entropy of a discrete random variable.
4248
        $h -= $p * log($p) / log(2);
4249
    }
4250
 
4251
    // Instead of returning the average entropy per symbol (Shannon entropy),
4252
    // we multiply by the length of the string to get total entropy.
4253
    return $h * $size;
4254
}
4255
 
4256
/**
4257
 * Get the available password peppers.
4258
 * The latest pepper is checked for minimum entropy as part of this function.
4259
 * We only calculate the entropy of the most recent pepper,
4260
 * because passwords are always updated to the latest pepper,
4261
 * and in the past we may have enforced a lower minimum entropy.
4262
 * Also, we allow the latest pepper to be empty, to allow admins to migrate off peppers.
4263
 *
4264
 * @return array The password peppers.
4265
 * @throws coding_exception If the entropy of the password pepper is less than the recommended minimum.
4266
 */
1441 ariadna 4267
function get_password_peppers(): array {
1 efrain 4268
    global $CFG;
4269
 
4270
    // Get all available peppers.
4271
    if (isset($CFG->passwordpeppers) && is_array($CFG->passwordpeppers)) {
4272
        // Sort the array in descending order of keys (numerical).
4273
        $peppers = $CFG->passwordpeppers;
4274
        krsort($peppers, SORT_NUMERIC);
4275
    } else {
4276
        $peppers = [];  // Set an empty array if no peppers are found.
4277
    }
4278
 
4279
    // Check if the entropy of the most recent pepper is less than the minimum.
4280
    // Also, we allow the most recent pepper to be empty, to allow admins to migrate off peppers.
4281
    $lastpepper = reset($peppers);
4282
    if (!empty($peppers) && $lastpepper !== '' && calculate_entropy($lastpepper) < PEPPER_ENTROPY) {
4283
        throw new coding_exception(
1441 ariadna 4284
                'password pepper below minimum',
4285
                'The entropy of the password pepper is less than the recommended minimum.');
1 efrain 4286
    }
4287
    return $peppers;
4288
}
4289
 
4290
/**
4291
 * Compare password against hash stored in user object to determine if it is valid.
4292
 *
4293
 * If necessary it also updates the stored hash to the current format.
4294
 *
4295
 * @param stdClass $user (Password property may be updated).
4296
 * @param string $password Plain text password.
4297
 * @return bool True if password is valid.
4298
 */
1441 ariadna 4299
function validate_internal_user_password(stdClass $user, #[\SensitiveParameter] string $password): bool {
1 efrain 4300
 
4301
    if (exceeds_password_length($password)) {
4302
        // Password cannot be more than MAX_PASSWORD_CHARACTERS characters.
4303
        return false;
4304
    }
4305
 
4306
    if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4307
        // Internal password is not used at all, it can not validate.
4308
        return false;
4309
    }
4310
 
4311
    $peppers = get_password_peppers(); // Get the array of available peppers.
4312
    $islegacy = password_is_legacy_hash($user->password); // Check if the password is a legacy bcrypt hash.
4313
 
4314
    // If the password is a legacy hash, no peppers were used, so verify and update directly.
4315
    if ($islegacy && password_verify($password, $user->password)) {
4316
        update_internal_user_password($user, $password);
4317
        return true;
4318
    }
4319
 
4320
    // If the password is not a legacy hash, iterate through the peppers.
4321
    $latestpepper = reset($peppers);
4322
    // Add an empty pepper to the beginning of the array. To make it easier to check if the password matches without any pepper.
4323
    $peppers = [-1 => ''] + $peppers;
4324
    foreach ($peppers as $pepper) {
4325
        $pepperedpassword = $password . $pepper;
4326
 
4327
        // If the peppered password is correct, update (if necessary) and return true.
4328
        if (password_verify($pepperedpassword, $user->password)) {
4329
            // If the pepper used is not the latest one, update the password.
4330
            if ($pepper !== $latestpepper) {
4331
                update_internal_user_password($user, $password);
4332
            }
4333
            return true;
4334
        }
4335
    }
4336
 
4337
    // If no peppered password was correct, the password is wrong.
4338
    return false;
4339
}
4340
 
4341
/**
4342
 * Calculate hash for a plain text password.
4343
 *
4344
 * @param string $password Plain text password to be hashed.
4345
 * @param bool $fasthash If true, use a low number of rounds when generating the hash
4346
 *                       This is faster to generate but makes the hash less secure.
4347
 *                       It is used when lots of hashes need to be generated quickly.
4348
 * @param int $pepperlength Lenght of the peppers
4349
 * @return string The hashed password.
4350
 *
4351
 * @throws moodle_exception If a problem occurs while generating the hash.
4352
 */
1441 ariadna 4353
function hash_internal_user_password(#[\SensitiveParameter] string $password, $fasthash = false, $pepperlength = 0): string {
1 efrain 4354
    if (exceeds_password_length($password, $pepperlength)) {
4355
        // Password cannot be more than MAX_PASSWORD_CHARACTERS.
4356
        throw new \moodle_exception(get_string("passwordexceeded", 'error', MAX_PASSWORD_CHARACTERS));
4357
    }
4358
 
4359
    // Set the cost factor to 5000 for fast hashing, otherwise use default cost.
4360
    $rounds = $fasthash ? 5000 : 10000;
4361
 
4362
    // First generate a cryptographically suitable salt.
4363
    $randombytes = random_bytes(16);
4364
    $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16);
4365
 
4366
    // Now construct the password string with the salt and number of rounds.
4367
    // The password string is in the format $algorithm$rounds$salt$hash. ($6 is the SHA512 algorithm).
4368
    $generatedhash = crypt($password, implode('$', [
4369
        '',
4370
        // The SHA512 Algorithm
4371
        '6',
4372
        "rounds={$rounds}",
4373
        $salt,
4374
        '',
4375
    ]));
4376
 
4377
    if ($generatedhash === false || $generatedhash === null) {
4378
        throw new moodle_exception('Failed to generate password hash.');
4379
    }
4380
 
4381
    return $generatedhash;
4382
}
4383
 
4384
/**
4385
 * Update password hash in user object (if necessary).
4386
 *
4387
 * The password is updated if:
4388
 * 1. The password has changed (the hash of $user->password is different
4389
 *    to the hash of $password).
4390
 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4391
 *    md5 algorithm).
4392
 *
4393
 * The password is peppered with the latest pepper before hashing,
4394
 * if peppers are available.
4395
 * Updating the password will modify the $user object and the database
4396
 * record to use the current hashing algorithm.
4397
 * It will remove Web Services user tokens too.
4398
 *
4399
 * @param stdClass $user User object (password property may be updated).
11 efrain 4400
 * @param string|null $password Plain text password.
1 efrain 4401
 * @param bool $fasthash If true, use a low cost factor when generating the hash
4402
 *                       This is much faster to generate but makes the hash
4403
 *                       less secure. It is used when lots of hashes need to
4404
 *                       be generated quickly.
4405
 * @return bool Always returns true.
4406
 */
4407
function update_internal_user_password(
1441 ariadna 4408
        stdClass $user,
4409
        #[\SensitiveParameter] ?string $password,
4410
        bool $fasthash = false
1 efrain 4411
): bool {
4412
    global $CFG, $DB;
4413
 
4414
    // Add the latest password pepper to the password before further processing.
4415
    $peppers = get_password_peppers();
4416
    if (!empty($peppers)) {
4417
        $password = $password . reset($peppers);
4418
    }
4419
 
4420
    // Figure out what the hashed password should be.
4421
    if (!isset($user->auth)) {
1441 ariadna 4422
        debugging('User record in update_internal_user_password() must include field auth',
4423
                DEBUG_DEVELOPER);
1 efrain 4424
        $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4425
    }
4426
    $authplugin = get_auth_plugin($user->auth);
4427
    if ($authplugin->prevent_local_passwords()) {
4428
        $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4429
    } else {
4430
        $hashedpassword = hash_internal_user_password($password, $fasthash);
4431
    }
4432
 
4433
    $algorithmchanged = false;
4434
 
4435
    if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4436
        // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4437
        $passwordchanged = ($user->password !== $hashedpassword);
1441 ariadna 4438
 
1 efrain 4439
    } else if (isset($user->password)) {
4440
        // If verification fails then it means the password has changed.
4441
        $passwordchanged = !password_verify($password, $user->password);
4442
        $algorithmchanged = password_is_legacy_hash($user->password);
4443
    } else {
4444
        // While creating new user, password in unset in $user object, to avoid
4445
        // saving it with user_create()
4446
        $passwordchanged = true;
4447
    }
4448
 
4449
    if ($passwordchanged || $algorithmchanged) {
4450
        $DB->set_field('user', 'password',  $hashedpassword, array('id' => $user->id));
4451
        $user->password = $hashedpassword;
4452
 
4453
        // Trigger event.
4454
        $user = $DB->get_record('user', array('id' => $user->id));
4455
        \core\event\user_password_updated::create_from_user($user)->trigger();
4456
 
4457
        // Remove WS user tokens.
4458
        if (!empty($CFG->passwordchangetokendeletion)) {
1441 ariadna 4459
            require_once($CFG->dirroot.'/webservice/lib.php');
1 efrain 4460
            webservice::delete_user_ws_tokens($user->id);
4461
        }
4462
    }
4463
 
4464
    return true;
4465
}
4466
 
4467
/**
4468
 * Get a complete user record, which includes all the info in the user record.
4469
 *
4470
 * Intended for setting as $USER session variable
4471
 *
4472
 * @param string $field The user field to be checked for a given value.
4473
 * @param string $value The value to match for $field.
4474
 * @param int $mnethostid
4475
 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4476
 *                              found. Otherwise, it will just return false.
4477
 * @return mixed False, or A {@link $USER} object.
4478
 */
1441 ariadna 4479
function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
1 efrain 4480
    global $CFG, $DB;
4481
 
4482
    if (!$field || !$value) {
4483
        return false;
4484
    }
4485
 
4486
    // Change the field to lowercase.
4487
    $field = core_text::strtolower($field);
4488
 
4489
    // List of case insensitive fields.
4490
    $caseinsensitivefields = ['email'];
4491
 
4492
    // Username input is forced to lowercase and should be case sensitive.
4493
    if ($field == 'username') {
4494
        $value = core_text::strtolower($value);
4495
    }
4496
 
4497
    // Build the WHERE clause for an SQL query.
4498
    $params = array('fieldval' => $value);
4499
 
4500
    // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4501
    // such as MySQL by pre-filtering users with accent-insensitive subselect.
4502
    if (in_array($field, $caseinsensitivefields)) {
4503
        $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4504
        $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4505
        $params['fieldval2'] = $value;
4506
    } else {
4507
        $fieldselect = "$field = :fieldval";
4508
        $idsubselect = '';
4509
    }
4510
    $constraints = "$fieldselect AND deleted <> 1";
4511
 
4512
    // If we are loading user data based on anything other than id,
4513
    // we must also restrict our search based on mnet host.
4514
    if ($field != 'id') {
4515
        if (empty($mnethostid)) {
4516
            // If empty, we restrict to local users.
4517
            $mnethostid = $CFG->mnet_localhost_id;
4518
        }
4519
    }
4520
    if (!empty($mnethostid)) {
4521
        $params['mnethostid'] = $mnethostid;
4522
        $constraints .= " AND mnethostid = :mnethostid";
4523
    }
4524
 
4525
    if ($idsubselect) {
4526
        $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4527
    }
4528
 
4529
    // Get all the basic user data.
4530
    try {
4531
        // Make sure that there's only a single record that matches our query.
4532
        // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4533
        // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4534
        $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4535
    } catch (dml_exception $exception) {
4536
        if ($throwexception) {
4537
            throw $exception;
4538
        } else {
4539
            // Return false when no records or multiple records were found.
4540
            return false;
4541
        }
4542
    }
4543
 
4544
    // Get various settings and preferences.
4545
 
4546
    // Preload preference cache.
4547
    check_user_preferences_loaded($user);
4548
 
4549
    // Load course enrolment related stuff.
4550
    $user->lastcourseaccess    = array(); // During last session.
4551
    $user->currentcourseaccess = array(); // During current session.
4552
    if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4553
        foreach ($lastaccesses as $lastaccess) {
4554
            $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4555
        }
4556
    }
4557
 
4558
    // Add cohort theme.
4559
    if (!empty($CFG->allowcohortthemes)) {
4560
        require_once($CFG->dirroot . '/cohort/lib.php');
4561
        if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
4562
            $user->cohorttheme = $cohorttheme;
4563
        }
4564
    }
4565
 
4566
    // Add the custom profile fields to the user record.
4567
    $user->profile = array();
4568
    if (!isguestuser($user)) {
1441 ariadna 4569
        require_once($CFG->dirroot.'/user/profile/lib.php');
1 efrain 4570
        profile_load_custom_fields($user);
4571
    }
4572
 
4573
    // Rewrite some variables if necessary.
4574
    if (!empty($user->description)) {
4575
        // No need to cart all of it around.
4576
        $user->description = true;
4577
    }
4578
    if (isguestuser($user)) {
4579
        // Guest language always same as site.
4580
        $user->lang = get_newuser_language();
4581
        // Name always in current language.
4582
        $user->firstname = get_string('guestuser');
4583
        $user->lastname = ' ';
4584
    }
4585
 
4586
    return $user;
4587
}
4588
 
4589
/**
4590
 * Validate a password against the configured password policy
4591
 *
4592
 * @param string $password the password to be checked against the password policy
4593
 * @param string|null $errmsg the error message to display when the password doesn't comply with the policy.
4594
 * @param stdClass|null $user the user object to perform password validation against. Defaults to null if not provided.
4595
 *
4596
 * @return bool true if the password is valid according to the policy. false otherwise.
4597
 */
1441 ariadna 4598
function check_password_policy(string $password, ?string &$errmsg, ?stdClass $user = null) {
1 efrain 4599
    global $CFG;
4600
    if (!empty($CFG->passwordpolicy) && !isguestuser($user)) {
4601
        $errors = get_password_policy_errors($password, $user);
4602
 
4603
        foreach ($errors as $error) {
4604
            $errmsg .= '<div>' . $error . '</div>';
4605
        }
4606
    }
4607
 
4608
    return $errmsg == '';
4609
}
4610
 
4611
/**
4612
 * Validate a password against the configured password policy.
4613
 * Note: This function is unaffected by whether the password policy is enabled or not.
4614
 *
4615
 * @param string $password the password to be checked against the password policy
4616
 * @param stdClass|null $user the user object to perform password validation against. Defaults to null if not provided.
4617
 *
4618
 * @return string[] Array of error messages.
4619
 */
1441 ariadna 4620
function get_password_policy_errors(string $password, ?stdClass $user = null) : array {
1 efrain 4621
    global $CFG;
4622
 
4623
    $errors = [];
4624
 
4625
    if (core_text::strlen($password) < $CFG->minpasswordlength) {
4626
        $errors[] = get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength);
4627
    }
4628
    if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4629
        $errors[] = get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits);
4630
    }
4631
    if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4632
        $errors[] = get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower);
4633
    }
4634
    if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4635
        $errors[] = get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper);
4636
    }
4637
    if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4638
        $errors[] = get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum);
4639
    }
4640
    if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4641
        $errors[] = get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars);
4642
    }
4643
 
4644
    // Fire any additional password policy functions from plugins.
4645
    // Plugin functions should output an error message string or empty string for success.
4646
    $pluginsfunction = get_plugins_with_function('check_password_policy');
4647
    foreach ($pluginsfunction as $plugintype => $plugins) {
4648
        foreach ($plugins as $pluginfunction) {
4649
            $pluginerr = $pluginfunction($password, $user);
4650
            if ($pluginerr) {
4651
                $errors[] = $pluginerr;
4652
            }
4653
        }
4654
    }
4655
 
4656
    return $errors;
4657
}
4658
 
4659
/**
4660
 * When logging in, this function is run to set certain preferences for the current SESSION.
4661
 */
1441 ariadna 4662
function set_login_session_preferences() {
1 efrain 4663
    global $SESSION;
4664
 
4665
    $SESSION->justloggedin = true;
4666
 
4667
    unset($SESSION->lang);
4668
    unset($SESSION->forcelang);
4669
    unset($SESSION->load_navigation_admin);
4670
}
4671
 
4672
 
4673
/**
4674
 * Delete a course, including all related data from the database, and any associated files.
4675
 *
4676
 * @param mixed $courseorid The id of the course or course object to delete.
4677
 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4678
 * @return bool true if all the removals succeeded. false if there were any failures. If this
4679
 *             method returns false, some of the removals will probably have succeeded, and others
4680
 *             failed, but you have no way of knowing which.
4681
 */
1441 ariadna 4682
function delete_course($courseorid, $showfeedback = true) {
1 efrain 4683
    global $DB, $CFG;
4684
 
4685
    if (is_object($courseorid)) {
4686
        $courseid = $courseorid->id;
4687
        $course   = $courseorid;
4688
    } else {
4689
        $courseid = $courseorid;
4690
        if (!$course = $DB->get_record('course', array('id' => $courseid))) {
4691
            return false;
4692
        }
4693
    }
4694
    $context = context_course::instance($courseid);
4695
 
4696
    // Frontpage course can not be deleted!!
4697
    if ($courseid == SITEID) {
4698
        return false;
4699
    }
4700
 
4701
    // Allow plugins to use this course before we completely delete it.
4702
    if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
4703
        foreach ($pluginsfunction as $plugintype => $plugins) {
4704
            foreach ($plugins as $pluginfunction) {
4705
                $pluginfunction($course);
4706
            }
4707
        }
4708
    }
4709
 
4710
    // Dispatch the hook for pre course delete actions.
4711
    $hook = new \core_course\hook\before_course_deleted(
4712
        course: $course,
4713
    );
4714
    \core\di::get(\core\hook\manager::class)->dispatch($hook);
4715
 
4716
    // Tell the search manager we are about to delete a course. This prevents us sending updates
4717
    // for each individual context being deleted.
4718
    \core_search\manager::course_deleting_start($courseid);
4719
 
4720
    $handler = core_course\customfield\course_handler::create();
4721
    $handler->delete_instance($courseid);
4722
 
4723
    // Make the course completely empty.
4724
    remove_course_contents($courseid, $showfeedback);
4725
 
4726
    // Delete the course and related context instance.
4727
    context_helper::delete_instance(CONTEXT_COURSE, $courseid);
4728
 
4729
    $DB->delete_records("course", array("id" => $courseid));
4730
    $DB->delete_records("course_format_options", array("courseid" => $courseid));
4731
 
4732
    // Reset all course related caches here.
4733
    core_courseformat\base::reset_course_cache($courseid);
4734
 
4735
    // Tell search that we have deleted the course so it can delete course data from the index.
4736
    \core_search\manager::course_deleting_finish($courseid);
4737
 
4738
    // Trigger a course deleted event.
4739
    $event = \core\event\course_deleted::create(array(
4740
        'objectid' => $course->id,
4741
        'context' => $context,
4742
        'other' => array(
4743
            'shortname' => $course->shortname,
4744
            'fullname' => $course->fullname,
4745
            'idnumber' => $course->idnumber
1441 ariadna 4746
            )
1 efrain 4747
    ));
4748
    $event->add_record_snapshot('course', $course);
4749
    $event->trigger();
4750
 
4751
    return true;
4752
}
4753
 
4754
/**
4755
 * Clear a course out completely, deleting all content but don't delete the course itself.
4756
 *
4757
 * This function does not verify any permissions.
4758
 *
4759
 * Please note this function also deletes all user enrolments,
4760
 * enrolment instances and role assignments by default.
4761
 *
4762
 * $options:
4763
 *  - 'keep_roles_and_enrolments' - false by default
4764
 *  - 'keep_groups_and_groupings' - false by default
4765
 *
4766
 * @param int $courseid The id of the course that is being deleted
4767
 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4768
 * @param array $options extra options
1441 ariadna 4769
 * @param bool $coursedeletion Are we calling this as part of deleting the course?
1 efrain 4770
 * @return bool true if all the removals succeeded. false if there were any failures. If this
4771
 *             method returns false, some of the removals will probably have succeeded, and others
4772
 *             failed, but you have no way of knowing which.
4773
 */
1441 ariadna 4774
function remove_course_contents($courseid, $showfeedback = true, ?array $options = null, bool $coursedeletion = true) {
1 efrain 4775
    global $CFG, $DB, $OUTPUT;
4776
 
1441 ariadna 4777
    require_once($CFG->libdir.'/badgeslib.php');
4778
    require_once($CFG->libdir.'/completionlib.php');
4779
    require_once($CFG->libdir.'/questionlib.php');
4780
    require_once($CFG->libdir.'/gradelib.php');
4781
    require_once($CFG->dirroot.'/group/lib.php');
4782
    require_once($CFG->dirroot.'/comment/lib.php');
4783
    require_once($CFG->dirroot.'/rating/lib.php');
4784
    require_once($CFG->dirroot.'/notes/lib.php');
1 efrain 4785
 
4786
    // Handle course badges.
4787
    badges_handle_course_deletion($courseid);
4788
 
4789
    // NOTE: these concatenated strings are suboptimal, but it is just extra info...
1441 ariadna 4790
    $strdeleted = get_string('deleted').' - ';
1 efrain 4791
 
4792
    // Some crazy wishlist of stuff we should skip during purging of course content.
4793
    $options = (array)$options;
4794
 
4795
    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
4796
    $coursecontext = context_course::instance($courseid);
4797
    $fs = get_file_storage();
4798
 
4799
    // Delete course completion information, this has to be done before grades and enrols.
4800
    $cc = new completion_info($course);
4801
    $cc->clear_criteria();
4802
    if ($showfeedback) {
1441 ariadna 4803
        echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
1 efrain 4804
    }
4805
 
4806
    // Remove all data from gradebook - this needs to be done before course modules
4807
    // because while deleting this information, the system may need to reference
4808
    // the course modules that own the grades.
4809
    remove_course_grades($courseid, $showfeedback);
4810
    remove_grade_letters($coursecontext, $showfeedback);
4811
 
4812
    // Delete course blocks in any all child contexts,
4813
    // they may depend on modules so delete them first.
4814
    $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4815
    foreach ($childcontexts as $childcontext) {
4816
        blocks_delete_all_for_context($childcontext->id);
4817
    }
4818
    unset($childcontexts);
4819
    blocks_delete_all_for_context($coursecontext->id);
4820
    if ($showfeedback) {
1441 ariadna 4821
        echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
1 efrain 4822
    }
4823
 
4824
    $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
4825
    rebuild_course_cache($courseid, true);
4826
 
4827
    // Get the list of all modules that are properly installed.
4828
    $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
4829
 
4830
    // Delete every instance of every module,
4831
    // this has to be done before deleting of course level stuff.
4832
    $locations = core_component::get_plugin_list('mod');
1441 ariadna 4833
    // Sort mod instances that publish questions to the end of the list, so that they will be removed last.
4834
    // This is because they could have questions in use by other activities in this course.
4835
    uksort($locations, static function ($a, $b) {
4836
        return plugin_supports('mod', $a, FEATURE_PUBLISHES_QUESTIONS) <=> plugin_supports('mod', $b, FEATURE_PUBLISHES_QUESTIONS);
4837
    });
1 efrain 4838
    foreach ($locations as $modname => $moddir) {
4839
        if ($modname === 'NEWMODULE') {
4840
            continue;
4841
        }
4842
        if (array_key_exists($modname, $allmodules)) {
4843
            $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
1441 ariadna 4844
              FROM {".$modname."} m
1 efrain 4845
                   LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
4846
             WHERE m.course = :courseid";
1441 ariadna 4847
            $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
4848
                'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
1 efrain 4849
 
4850
            include_once("$moddir/lib.php");                 // Shows php warning only if plugin defective.
1441 ariadna 4851
            $moddelete = $modname .'_delete_instance';       // Delete everything connected to an instance.
1 efrain 4852
 
4853
            if ($instances) {
4854
                foreach ($instances as $cm) {
1441 ariadna 4855
                    // Warning! there is very similar code in course_delete_module.
4856
                    // If you are changing this code, you probably need to change that too.
1 efrain 4857
                    if (function_exists($moddelete)) {
4858
                        // This purges all module data in related tables, extra user prefs, settings, etc.
4859
                        $moddelete($cm->modinstance);
4860
                    } else {
4861
                        // NOTE: we should not allow installation of modules with missing delete support!
4862
                        debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4863
                        $DB->delete_records($modname, array('id' => $cm->modinstance));
4864
                    }
4865
 
4866
                    if ($cm->id) {
1441 ariadna 4867
                        // Delete activity context questions and question categories.
4868
                        // We delete the questions after the activity database is removed,
4869
                        // because questions are referenced via question reference tables
4870
                        // and cannot be deleted while the activities that use them still exist.
4871
                        question_delete_activity($cm, coursedeletion: $coursedeletion);
4872
                        // Delete all tag instances associated with the instance of this module.
4873
                        core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id);
4874
                        core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
4875
                        // Notify the competency subsystem.
4876
                        \core_competency\api::hook_course_module_deleted($cm);
1 efrain 4877
                        // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
4878
                        context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4879
                        $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
4880
                        $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
4881
                        $DB->delete_records('course_modules', array('id' => $cm->id));
4882
                        rebuild_course_cache($cm->course, true);
4883
                    }
4884
                }
4885
            }
4886
            if ($instances and $showfeedback) {
1441 ariadna 4887
                echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
1 efrain 4888
            }
4889
        } else {
4890
            // Ooops, this module is not properly installed, force-delete it in the next block.
4891
        }
4892
    }
4893
 
4894
    // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
4895
 
4896
    // Delete completion defaults.
4897
    $DB->delete_records("course_completion_defaults", array("course" => $courseid));
4898
 
4899
    // Remove all data from availability and completion tables that is associated
4900
    // with course-modules belonging to this course. Note this is done even if the
4901
    // features are not enabled now, in case they were enabled previously.
1441 ariadna 4902
    $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
4903
            'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
4904
    $DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id',
4905
        'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
1 efrain 4906
 
4907
    // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
4908
    $cms = $DB->get_records('course_modules', array('course' => $course->id));
4909
    $allmodulesbyid = array_flip($allmodules);
4910
    foreach ($cms as $cm) {
4911
        if (array_key_exists($cm->module, $allmodulesbyid)) {
4912
            try {
4913
                $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
4914
            } catch (Exception $e) {
4915
                // Ignore weird or missing table problems.
4916
            }
4917
        }
4918
        context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4919
        $DB->delete_records('course_modules', array('id' => $cm->id));
4920
        rebuild_course_cache($cm->course, true);
4921
    }
4922
 
4923
    if ($showfeedback) {
1441 ariadna 4924
        echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
1 efrain 4925
    }
4926
 
4927
    // Delete content bank contents.
4928
    $cb = new \core_contentbank\contentbank();
4929
    $cbdeleted = $cb->delete_contents($coursecontext);
4930
    if ($showfeedback && $cbdeleted) {
1441 ariadna 4931
        echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
1 efrain 4932
    }
4933
 
4934
    // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
4935
    $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4936
    foreach ($childcontexts as $childcontext) {
4937
        $childcontext->delete();
4938
    }
4939
    unset($childcontexts);
4940
 
4941
    // Remove roles and enrolments by default.
4942
    if (empty($options['keep_roles_and_enrolments'])) {
4943
        // This hack is used in restore when deleting contents of existing course.
4944
        // During restore, we should remove only enrolment related data that the user performing the restore has a
4945
        // permission to remove.
4946
        $userid = $options['userid'] ?? null;
4947
        enrol_course_delete($course, $userid);
4948
        role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
4949
        if ($showfeedback) {
1441 ariadna 4950
            echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
1 efrain 4951
        }
4952
    }
4953
 
4954
    // Delete any groups, removing members and grouping/course links first.
4955
    if (empty($options['keep_groups_and_groupings'])) {
4956
        groups_delete_groupings($course->id, $showfeedback);
4957
        groups_delete_groups($course->id, $showfeedback);
4958
    }
4959
 
4960
    // Filters be gone!
4961
    filter_delete_all_for_context($coursecontext->id);
4962
 
4963
    // Notes, you shall not pass!
4964
    note_delete_all($course->id);
4965
 
4966
    // Die comments!
4967
    comment::delete_comments($coursecontext->id);
4968
 
4969
    // Ratings are history too.
4970
    $delopt = new stdclass();
4971
    $delopt->contextid = $coursecontext->id;
4972
    $rm = new rating_manager();
4973
    $rm->delete_ratings($delopt);
4974
 
4975
    // Delete course tags.
4976
    core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
4977
 
4978
    // Give the course format the opportunity to remove its obscure data.
4979
    $format = course_get_format($course);
4980
    $format->delete_format_data();
4981
 
4982
    // Notify the competency subsystem.
4983
    \core_competency\api::hook_course_deleted($course);
4984
 
4985
    // Delete calendar events.
4986
    $DB->delete_records('event', array('courseid' => $course->id));
4987
    $fs->delete_area_files($coursecontext->id, 'calendar');
4988
 
4989
    // Delete all related records in other core tables that may have a courseid
4990
    // This array stores the tables that need to be cleared, as
4991
    // table_name => column_name that contains the course id.
4992
    $tablestoclear = array(
4993
        'backup_courses' => 'courseid',  // Scheduled backup stuff.
4994
        'user_lastaccess' => 'courseid', // User access info.
4995
    );
4996
    foreach ($tablestoclear as $table => $col) {
4997
        $DB->delete_records($table, array($col => $course->id));
4998
    }
4999
 
5000
    // Delete all course backup files.
5001
    $fs->delete_area_files($coursecontext->id, 'backup');
5002
 
5003
    // Cleanup course record - remove links to deleted stuff.
5004
    // Do not wipe cacherev, as this course might be reused and we need to ensure that it keeps
5005
    // increasing.
5006
    $oldcourse = new stdClass();
5007
    $oldcourse->id               = $course->id;
5008
    $oldcourse->summary          = '';
5009
    $oldcourse->legacyfiles      = 0;
5010
    if (!empty($options['keep_groups_and_groupings'])) {
5011
        $oldcourse->defaultgroupingid = 0;
5012
    }
5013
    $DB->update_record('course', $oldcourse);
5014
 
5015
    // Delete course sections.
5016
    $DB->delete_records('course_sections', array('course' => $course->id));
5017
 
5018
    // Delete legacy, section and any other course files.
5019
    $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
5020
 
5021
    // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5022
    if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5023
        // Easy, do not delete the context itself...
5024
        $coursecontext->delete_content();
5025
    } else {
5026
        // Hack alert!!!!
5027
        // We can not drop all context stuff because it would bork enrolments and roles,
5028
        // there might be also files used by enrol plugins...
5029
    }
5030
 
5031
    // Delete legacy files - just in case some files are still left there after conversion to new file api,
5032
    // also some non-standard unsupported plugins may try to store something there.
1441 ariadna 5033
    fulldelete($CFG->dataroot.'/'.$course->id);
1 efrain 5034
 
5035
    // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5036
    course_modinfo::purge_course_cache($courseid);
5037
 
5038
    // Trigger a course content deleted event.
5039
    $event = \core\event\course_content_deleted::create(array(
5040
        'objectid' => $course->id,
5041
        'context' => $coursecontext,
1441 ariadna 5042
        'other' => array('shortname' => $course->shortname,
5043
                         'fullname' => $course->fullname,
5044
                         'options' => $options) // Passing this for legacy reasons.
1 efrain 5045
    ));
5046
    $event->add_record_snapshot('course', $course);
5047
    $event->trigger();
5048
 
5049
    return true;
5050
}
5051
 
5052
/**
5053
 * Change dates in module - used from course reset.
5054
 *
5055
 * @param string $modname forum, assignment, etc
5056
 * @param array $fields array of date fields from mod table
5057
 * @param int $timeshift time difference
5058
 * @param int $courseid
5059
 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5060
 * @return bool success
5061
 */
1441 ariadna 5062
function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
1 efrain 5063
    global $CFG, $DB;
1441 ariadna 5064
    include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
1 efrain 5065
 
5066
    $return = true;
5067
    $params = array($timeshift, $courseid);
5068
    foreach ($fields as $field) {
1441 ariadna 5069
        $updatesql = "UPDATE {".$modname."}
1 efrain 5070
                          SET $field = $field + ?
5071
                        WHERE course=? AND $field<>0";
5072
        if ($modid) {
5073
            $updatesql .= ' AND id=?';
5074
            $params[] = $modid;
5075
        }
5076
        $return = $DB->execute($updatesql, $params) && $return;
5077
    }
5078
 
5079
    return $return;
5080
}
5081
 
5082
/**
5083
 * This function will empty a course of user data.
5084
 * It will retain the activities and the structure of the course.
5085
 *
5086
 * @param object $data an object containing all the settings including courseid (without magic quotes)
5087
 * @return array status array of array component, item, error
5088
 */
1441 ariadna 5089
function reset_course_userdata($data) {
1 efrain 5090
    global $CFG, $DB;
1441 ariadna 5091
    require_once($CFG->libdir.'/gradelib.php');
5092
    require_once($CFG->libdir.'/completionlib.php');
5093
    require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
5094
    require_once($CFG->dirroot.'/group/lib.php');
1 efrain 5095
 
5096
    $data->courseid = $data->id;
5097
    $context = context_course::instance($data->courseid);
5098
 
5099
    $eventparams = array(
5100
        'context' => $context,
5101
        'courseid' => $data->id,
5102
        'other' => array(
5103
            'reset_options' => (array) $data
5104
        )
5105
    );
5106
    $event = \core\event\course_reset_started::create($eventparams);
5107
    $event->trigger();
5108
 
5109
    // Calculate the time shift of dates.
5110
    if (!empty($data->reset_start_date)) {
1441 ariadna 5111
        $data->timeshift = $data->reset_start_date - $data->reset_start_date_old;
1 efrain 5112
    } else {
5113
        $data->timeshift = 0;
5114
    }
5115
 
5116
    // Result array: component, item, error.
5117
    $status = array();
5118
 
5119
    // Start the resetting.
5120
    $componentstr = get_string('general');
5121
 
5122
    // Move the course start time.
5123
    if (!empty($data->reset_start_date) and $data->timeshift) {
5124
        // Change course start data.
5125
        $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5126
        // Update all course and group events - do not move activity events.
5127
        $updatesql = "UPDATE {event}
5128
                         SET timestart = timestart + ?
5129
                       WHERE courseid=? AND instance=0";
5130
        $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5131
 
5132
        // Update any date activity restrictions.
5133
        if ($CFG->enableavailability) {
5134
            \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5135
        }
5136
 
5137
        // Update completion expected dates.
5138
        if ($CFG->enablecompletion) {
5139
            $modinfo = get_fast_modinfo($data->courseid);
5140
            $changed = false;
5141
            foreach ($modinfo->get_cms() as $cm) {
5142
                if ($cm->completion && !empty($cm->completionexpected)) {
1441 ariadna 5143
                    $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
5144
                        array('id' => $cm->id));
1 efrain 5145
                    $changed = true;
5146
                }
5147
            }
5148
 
5149
            // Clear course cache if changes made.
5150
            if ($changed) {
5151
                rebuild_course_cache($data->courseid, true);
5152
            }
5153
 
5154
            // Update course date completion criteria.
5155
            \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5156
        }
5157
 
1441 ariadna 5158
        $status[] = ['component' => $componentstr, 'item' => get_string('date'), 'error' => false];
1 efrain 5159
    }
5160
 
5161
    if (!empty($data->reset_end_date)) {
5162
        // If the user set a end date value respect it.
5163
        $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5164
    } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5165
        // If there is a time shift apply it to the end date as well.
5166
        $enddate = $data->reset_end_date_old + $data->timeshift;
5167
        $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5168
    }
5169
 
5170
    if (!empty($data->reset_events)) {
5171
        $DB->delete_records('event', array('courseid' => $data->courseid));
5172
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5173
    }
5174
 
5175
    if (!empty($data->reset_notes)) {
1441 ariadna 5176
        require_once($CFG->dirroot.'/notes/lib.php');
1 efrain 5177
        note_delete_all($data->courseid);
5178
        $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5179
    }
5180
 
5181
    if (!empty($data->delete_blog_associations)) {
1441 ariadna 5182
        require_once($CFG->dirroot.'/blog/lib.php');
1 efrain 5183
        blog_remove_associations_for_course($data->courseid);
5184
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5185
    }
5186
 
5187
    if (!empty($data->reset_completion)) {
5188
        // Delete course and activity completion information.
5189
        $course = $DB->get_record('course', array('id' => $data->courseid));
5190
        $cc = new completion_info($course);
5191
        $cc->delete_all_completion_data();
1441 ariadna 5192
        $status[] = array('component' => $componentstr,
5193
                'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
1 efrain 5194
    }
5195
 
5196
    if (!empty($data->reset_competency_ratings)) {
5197
        \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
1441 ariadna 5198
        $status[] = array('component' => $componentstr,
5199
            'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
1 efrain 5200
    }
5201
 
5202
    $componentstr = get_string('roles');
5203
 
5204
    if (!empty($data->reset_roles_overrides)) {
5205
        $children = $context->get_child_contexts();
5206
        foreach ($children as $child) {
5207
            $child->delete_capabilities();
5208
        }
5209
        $context->delete_capabilities();
5210
        $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5211
    }
5212
 
5213
    if (!empty($data->reset_roles_local)) {
5214
        $children = $context->get_child_contexts();
5215
        foreach ($children as $child) {
5216
            role_unassign_all(array('contextid' => $child->id));
5217
        }
5218
        $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5219
    }
5220
 
5221
    // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5222
    $data->unenrolled = array();
5223
    if (!empty($data->unenrol_users)) {
5224
        $plugins = enrol_get_plugins(true);
5225
        $instances = enrol_get_instances($data->courseid, true);
5226
        foreach ($instances as $key => $instance) {
5227
            if (!isset($plugins[$instance->enrol])) {
5228
                unset($instances[$key]);
5229
                continue;
5230
            }
5231
        }
5232
 
5233
        $usersroles = enrol_get_course_users_roles($data->courseid);
5234
        foreach ($data->unenrol_users as $withroleid) {
5235
            if ($withroleid) {
5236
                $sql = "SELECT ue.*
5237
                          FROM {user_enrolments} ue
5238
                          JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5239
                          JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5240
                          JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5241
                $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
1441 ariadna 5242
 
1 efrain 5243
            } else {
5244
                // Without any role assigned at course context.
5245
                $sql = "SELECT ue.*
5246
                          FROM {user_enrolments} ue
5247
                          JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5248
                          JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5249
                     LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5250
                         WHERE ra.id IS null";
5251
                $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5252
            }
5253
 
5254
            $rs = $DB->get_recordset_sql($sql, $params);
5255
            foreach ($rs as $ue) {
5256
                if (!isset($instances[$ue->enrolid])) {
5257
                    continue;
5258
                }
5259
                $instance = $instances[$ue->enrolid];
5260
                $plugin = $plugins[$instance->enrol];
5261
                if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5262
                    continue;
5263
                }
5264
 
5265
                if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5266
                    // If we don't remove all roles and user has more than one role, just remove this role.
5267
                    role_unassign($withroleid, $ue->userid, $context->id);
5268
 
5269
                    unset($usersroles[$ue->userid][$withroleid]);
5270
                } else {
5271
                    // If we remove all roles or user has only one role, unenrol user from course.
5272
                    $plugin->unenrol_user($instance, $ue->userid);
5273
                }
5274
                $data->unenrolled[$ue->userid] = $ue->userid;
5275
            }
5276
            $rs->close();
5277
        }
5278
    }
5279
    if (!empty($data->unenrolled)) {
5280
        $status[] = array(
5281
            'component' => $componentstr,
1441 ariadna 5282
            'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
1 efrain 5283
            'error' => false
5284
        );
5285
    }
5286
 
5287
    $componentstr = get_string('groups');
5288
 
5289
    // Remove all group members.
5290
    if (!empty($data->reset_groups_members)) {
5291
        groups_delete_group_members($data->courseid);
5292
        $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5293
    }
5294
 
5295
    // Remove all groups.
5296
    if (!empty($data->reset_groups_remove)) {
5297
        groups_delete_groups($data->courseid, false);
5298
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5299
    }
5300
 
5301
    // Remove all grouping members.
5302
    if (!empty($data->reset_groupings_members)) {
5303
        groups_delete_groupings_groups($data->courseid, false);
5304
        $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5305
    }
5306
 
5307
    // Remove all groupings.
5308
    if (!empty($data->reset_groupings_remove)) {
5309
        groups_delete_groupings($data->courseid, false);
5310
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5311
    }
5312
 
5313
    // Look in every instance of every module for data to delete.
5314
    $unsupportedmods = array();
1441 ariadna 5315
    if ($allmods = $DB->get_records('modules') ) {
1 efrain 5316
        foreach ($allmods as $mod) {
5317
            $modname = $mod->name;
1441 ariadna 5318
            $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5319
            $moddeleteuserdata = $modname.'_reset_userdata';   // Function to delete user data.
1 efrain 5320
            if (file_exists($modfile)) {
5321
                if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5322
                    continue; // Skip mods with no instances.
5323
                }
5324
                include_once($modfile);
5325
                if (function_exists($moddeleteuserdata)) {
5326
                    $modstatus = $moddeleteuserdata($data);
5327
                    if (is_array($modstatus)) {
5328
                        $status = array_merge($status, $modstatus);
5329
                    } else {
1441 ariadna 5330
                        debugging('Module '.$modname.' returned incorrect staus - must be an array!');
1 efrain 5331
                    }
5332
                } else {
5333
                    $unsupportedmods[] = $mod;
5334
                }
5335
            } else {
1441 ariadna 5336
                debugging('Missing lib.php in '.$modname.' module!');
1 efrain 5337
            }
5338
            // Update calendar events for all modules.
5339
            course_module_bulk_update_calendar_events($modname, $data->courseid);
5340
        }
5341
        // Purge the course cache after resetting course start date. MDL-76936
5342
        if ($data->timeshift) {
5343
            course_modinfo::purge_course_cache($data->courseid);
5344
        }
5345
    }
5346
 
5347
    // Mention unsupported mods.
5348
    if (!empty($unsupportedmods)) {
5349
        foreach ($unsupportedmods as $mod) {
5350
            $status[] = array(
5351
                'component' => get_string('modulenameplural', $mod->name),
5352
                'item' => '',
5353
                'error' => get_string('resetnotimplemented')
5354
            );
5355
        }
5356
    }
5357
 
5358
    $componentstr = get_string('gradebook', 'grades');
5359
    // Reset gradebook,.
5360
    if (!empty($data->reset_gradebook_items)) {
5361
        remove_course_grades($data->courseid, false);
5362
        grade_grab_course_grades($data->courseid);
1441 ariadna 5363
        grade_regrade_final_grades($data->courseid, async: true);
1 efrain 5364
        $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
1441 ariadna 5365
 
1 efrain 5366
    } else if (!empty($data->reset_gradebook_grades)) {
5367
        grade_course_reset($data->courseid);
5368
        $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5369
    }
5370
    // Reset comments.
5371
    if (!empty($data->reset_comments)) {
1441 ariadna 5372
        require_once($CFG->dirroot.'/comment/lib.php');
1 efrain 5373
        comment::reset_course_page_comments($context);
5374
    }
5375
 
5376
    $event = \core\event\course_reset_ended::create($eventparams);
5377
    $event->trigger();
5378
 
5379
    return $status;
5380
}
5381
 
5382
/**
5383
 * Generate an email processing address.
5384
 *
5385
 * @param int $modid
5386
 * @param string $modargs
5387
 * @return string Returns email processing address
5388
 */
1441 ariadna 5389
function generate_email_processing_address($modid, $modargs) {
1 efrain 5390
    global $CFG;
5391
 
1441 ariadna 5392
    $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5393
    return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
1 efrain 5394
}
5395
 
5396
// CORRESPONDENCE.
5397
 
5398
/**
5399
 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5400
 *
5401
 * @param string $action 'get', 'buffer', 'close' or 'flush'
5402
 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5403
 */
1441 ariadna 5404
function get_mailer($action='get') {
1 efrain 5405
    global $CFG;
5406
 
5407
    /** @var moodle_phpmailer $mailer */
5408
    static $mailer  = null;
5409
    static $counter = 0;
5410
 
5411
    if (!isset($CFG->smtpmaxbulk)) {
5412
        $CFG->smtpmaxbulk = 1;
5413
    }
5414
 
5415
    if ($action == 'get') {
5416
        $prevkeepalive = false;
5417
 
5418
        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5419
            if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5420
                $counter++;
5421
                // Reset the mailer.
5422
                $mailer->Priority         = 3;
5423
                $mailer->CharSet          = 'UTF-8'; // Our default.
5424
                $mailer->ContentType      = "text/plain";
5425
                $mailer->Encoding         = "8bit";
5426
                $mailer->From             = "root@localhost";
5427
                $mailer->FromName         = "Root User";
5428
                $mailer->Sender           = "";
5429
                $mailer->Subject          = "";
5430
                $mailer->Body             = "";
5431
                $mailer->AltBody          = "";
5432
                $mailer->ConfirmReadingTo = "";
5433
 
5434
                $mailer->clearAllRecipients();
5435
                $mailer->clearReplyTos();
5436
                $mailer->clearAttachments();
5437
                $mailer->clearCustomHeaders();
5438
                return $mailer;
5439
            }
5440
 
5441
            $prevkeepalive = $mailer->SMTPKeepAlive;
5442
            get_mailer('flush');
5443
        }
5444
 
1441 ariadna 5445
        require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
1 efrain 5446
        $mailer = new moodle_phpmailer();
5447
 
5448
        $counter = 1;
5449
 
5450
        if ($CFG->smtphosts == 'qmail') {
5451
            // Use Qmail system.
5452
            $mailer->isQmail();
1441 ariadna 5453
 
1 efrain 5454
        } else if (empty($CFG->smtphosts)) {
5455
            // Use PHP mail() = sendmail.
5456
            $mailer->isMail();
1441 ariadna 5457
 
1 efrain 5458
        } else {
5459
            // Use SMTP directly.
5460
            $mailer->isSMTP();
5461
            if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5462
                $mailer->SMTPDebug = 3;
5463
            }
5464
            // Specify main and backup servers.
5465
            $mailer->Host          = $CFG->smtphosts;
5466
            // Specify secure connection protocol.
5467
            $mailer->SMTPSecure    = $CFG->smtpsecure;
5468
            // Use previous keepalive.
5469
            $mailer->SMTPKeepAlive = $prevkeepalive;
5470
 
5471
            if ($CFG->smtpuser) {
5472
                // Use SMTP authentication.
5473
                $mailer->SMTPAuth = true;
5474
                $mailer->Username = $CFG->smtpuser;
5475
                $mailer->Password = $CFG->smtppass;
5476
            }
5477
        }
5478
 
5479
        return $mailer;
5480
    }
5481
 
5482
    $nothing = null;
5483
 
5484
    // Keep smtp session open after sending.
5485
    if ($action == 'buffer') {
5486
        if (!empty($CFG->smtpmaxbulk)) {
5487
            get_mailer('flush');
5488
            $m = get_mailer();
5489
            if ($m->Mailer == 'smtp') {
5490
                $m->SMTPKeepAlive = true;
5491
            }
5492
        }
5493
        return $nothing;
5494
    }
5495
 
5496
    // Close smtp session, but continue buffering.
5497
    if ($action == 'flush') {
5498
        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5499
            if (!empty($mailer->SMTPDebug)) {
1441 ariadna 5500
                echo '<pre>'."\n";
1 efrain 5501
            }
5502
            $mailer->SmtpClose();
5503
            if (!empty($mailer->SMTPDebug)) {
5504
                echo '</pre>';
5505
            }
5506
        }
5507
        return $nothing;
5508
    }
5509
 
5510
    // Close smtp session, do not buffer anymore.
5511
    if ($action == 'close') {
5512
        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5513
            get_mailer('flush');
5514
            $mailer->SMTPKeepAlive = false;
5515
        }
5516
        $mailer = null; // Better force new instance.
5517
        return $nothing;
5518
    }
5519
}
5520
 
5521
/**
5522
 * A helper function to test for email diversion
5523
 *
5524
 * @param string $email
5525
 * @return bool Returns true if the email should be diverted
5526
 */
1441 ariadna 5527
function email_should_be_diverted($email) {
1 efrain 5528
    global $CFG;
5529
 
5530
    if (empty($CFG->divertallemailsto)) {
5531
        return false;
5532
    }
5533
 
5534
    if (empty($CFG->divertallemailsexcept)) {
5535
        return true;
5536
    }
5537
 
5538
    $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY));
5539
    foreach ($patterns as $pattern) {
5540
        if (preg_match("/{$pattern}/i", $email)) {
5541
            return false;
5542
        }
5543
    }
5544
 
5545
    return true;
5546
}
5547
 
5548
/**
5549
 * Generate a unique email Message-ID using the moodle domain and install path
5550
 *
5551
 * @param string $localpart An optional unique message id prefix.
5552
 * @return string The formatted ID ready for appending to the email headers.
5553
 */
1441 ariadna 5554
function generate_email_messageid($localpart = null) {
1 efrain 5555
    global $CFG;
5556
 
5557
    $urlinfo = parse_url($CFG->wwwroot);
5558
    $base = '@' . $urlinfo['host'];
5559
 
5560
    // If multiple moodles are on the same domain we want to tell them
5561
    // apart so we add the install path to the local part. This means
5562
    // that the id local part should never contain a / character so
5563
    // we can correctly parse the id to reassemble the wwwroot.
5564
    if (isset($urlinfo['path'])) {
5565
        $base = $urlinfo['path'] . $base;
5566
    }
5567
 
5568
    if (empty($localpart)) {
5569
        $localpart = uniqid('', true);
5570
    }
5571
 
5572
    // Because we may have an option /installpath suffix to the local part
5573
    // of the id we need to escape any / chars which are in the $localpart.
5574
    $localpart = str_replace('/', '%2F', $localpart);
5575
 
5576
    return '<' . $localpart . $base . '>';
5577
}
5578
 
5579
/**
5580
 * Send an email to a specified user
5581
 *
5582
 * @param stdClass $user  A {@link $USER} object
5583
 * @param stdClass $from A {@link $USER} object
5584
 * @param string $subject plain text subject line of the email
5585
 * @param string $messagetext plain text version of the message
5586
 * @param string $messagehtml complete html version of the message (optional)
5587
 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
5588
 *          the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
5589
 * @param string $attachname the name of the file (extension indicates MIME)
5590
 * @param bool $usetrueaddress determines whether $from email address should
5591
 *          be sent out. Will be overruled by user profile setting for maildisplay
5592
 * @param string $replyto Email address to reply to
5593
 * @param string $replytoname Name of reply to recipient
5594
 * @param int $wordwrapwidth custom word wrap width, default 79
5595
 * @return bool Returns true if mail was sent OK and false if there was an error.
5596
 */
1441 ariadna 5597
function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
5598
                       $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
1 efrain 5599
 
5600
    global $CFG, $PAGE, $SITE;
5601
 
5602
    if (empty($user) or empty($user->id)) {
5603
        debugging('Can not send email to null user', DEBUG_DEVELOPER);
5604
        return false;
5605
    }
5606
 
5607
    if (empty($user->email)) {
1441 ariadna 5608
        debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
1 efrain 5609
        return false;
5610
    }
5611
 
5612
    if (!empty($user->deleted)) {
1441 ariadna 5613
        debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
1 efrain 5614
        return false;
5615
    }
5616
 
1441 ariadna 5617
    if (defined('BEHAT_SITE_RUNNING') && !defined('TEST_EMAILCATCHER_MAIL_SERVER') &&
5618
            !defined('TEST_EMAILCATCHER_API_SERVER')) {
5619
 
5620
        // Behat tests are running and we are not using email catcher so fake email sending.
1 efrain 5621
        return true;
5622
    }
5623
 
5624
    if (!empty($CFG->noemailever)) {
5625
        // Hidden setting for development sites, set in config.php if needed.
1441 ariadna 5626
        debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_DEVELOPER);
1 efrain 5627
        return true;
5628
    }
5629
 
5630
    if (email_should_be_diverted($user->email)) {
5631
        $subject = "[DIVERTED {$user->email}] $subject";
1441 ariadna 5632
        $user = clone($user);
1 efrain 5633
        $user->email = $CFG->divertallemailsto;
5634
    }
5635
 
5636
    // Skip mail to suspended users.
1441 ariadna 5637
    if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
1 efrain 5638
        return true;
5639
    }
5640
 
5641
    if (!validate_email($user->email)) {
5642
        // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
1441 ariadna 5643
        debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
1 efrain 5644
        return false;
5645
    }
5646
 
5647
    if (over_bounce_threshold($user)) {
1441 ariadna 5648
        debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
1 efrain 5649
        return false;
5650
    }
5651
 
5652
    // TLD .invalid  is specifically reserved for invalid domain names.
5653
    // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
5654
    if (substr($user->email, -8) == '.invalid') {
1441 ariadna 5655
        debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
1 efrain 5656
        return true; // This is not an error.
5657
    }
5658
 
5659
    // If the user is a remote mnet user, parse the email text for URL to the
5660
    // wwwroot and modify the url to direct the user's browser to login at their
5661
    // home site (identity provider - idp) before hitting the link itself.
5662
    if (is_mnet_remote_user($user)) {
1441 ariadna 5663
        require_once($CFG->dirroot.'/mnet/lib.php');
1 efrain 5664
 
5665
        $jumpurl = mnet_get_idp_jump_url($user);
5666
        $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5667
 
1441 ariadna 5668
        $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5669
                $callback,
5670
                $messagetext);
5671
        $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5672
                $callback,
5673
                $messagehtml);
1 efrain 5674
    }
5675
    $mail = get_mailer();
5676
 
5677
    if (!empty($mail->SMTPDebug)) {
5678
        echo '<pre>' . "\n";
5679
    }
5680
 
5681
    $temprecipients = array();
5682
    $tempreplyto = array();
5683
 
5684
    // Make sure that we fall back onto some reasonable no-reply address.
5685
    $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
5686
    $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
5687
 
5688
    if (!validate_email($noreplyaddress)) {
1441 ariadna 5689
        debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
1 efrain 5690
        $noreplyaddress = $noreplyaddressdefault;
5691
    }
5692
 
5693
    // Make up an email address for handling bounces.
5694
    if (!empty($CFG->handlebounces)) {
1441 ariadna 5695
        $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
1 efrain 5696
        $mail->Sender = generate_email_processing_address(0, $modargs);
5697
    } else {
5698
        $mail->Sender = $noreplyaddress;
5699
    }
5700
 
5701
    // Make sure that the explicit replyto is valid, fall back to the implicit one.
5702
    if (!empty($replyto) && !validate_email($replyto)) {
1441 ariadna 5703
        debugging('email_to_user: Invalid replyto-email '.s($replyto));
1 efrain 5704
        $replyto = $noreplyaddress;
5705
    }
5706
 
5707
    if (is_string($from)) { // So we can pass whatever we want if there is need.
5708
        $mail->From     = $noreplyaddress;
5709
        $mail->FromName = $from;
1441 ariadna 5710
    // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
5711
    // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
5712
    // in a course with the sender.
1 efrain 5713
    } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
5714
        if (!validate_email($from->email)) {
1441 ariadna 5715
            debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
1 efrain 5716
            // Better not to use $noreplyaddress in this case.
5717
            return false;
5718
        }
5719
        $mail->From = $from->email;
5720
        $fromdetails = new stdClass();
5721
        $fromdetails->name = fullname($from);
5722
        $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5723
        $fromdetails->siteshortname = format_string($SITE->shortname);
5724
        $fromstring = $fromdetails->name;
5725
        if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
5726
            $fromstring = get_string('emailvia', 'core', $fromdetails);
5727
        }
5728
        $mail->FromName = $fromstring;
5729
        if (empty($replyto)) {
5730
            $tempreplyto[] = array($from->email, fullname($from));
5731
        }
5732
    } else {
5733
        $mail->From = $noreplyaddress;
5734
        $fromdetails = new stdClass();
5735
        $fromdetails->name = fullname($from);
5736
        $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5737
        $fromdetails->siteshortname = format_string($SITE->shortname);
5738
        $fromstring = $fromdetails->name;
5739
        if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
5740
            $fromstring = get_string('emailvia', 'core', $fromdetails);
5741
        }
5742
        $mail->FromName = $fromstring;
5743
        if (empty($replyto)) {
5744
            $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
5745
        }
5746
    }
5747
 
5748
    if (!empty($replyto)) {
5749
        $tempreplyto[] = array($replyto, $replytoname);
5750
    }
5751
 
5752
    $temprecipients[] = array($user->email, fullname($user));
5753
 
5754
    // Set word wrap.
5755
    $mail->WordWrap = $wordwrapwidth;
5756
 
5757
    if (!empty($from->customheaders)) {
5758
        // Add custom headers.
5759
        if (is_array($from->customheaders)) {
5760
            foreach ($from->customheaders as $customheader) {
5761
                $mail->addCustomHeader($customheader);
5762
            }
5763
        } else {
5764
            $mail->addCustomHeader($from->customheaders);
5765
        }
5766
    }
5767
 
5768
    // If the X-PHP-Originating-Script email header is on then also add an additional
5769
    // header with details of where exactly in moodle the email was triggered from,
5770
    // either a call to message_send() or to email_to_user().
5771
    if (ini_get('mail.add_x_header')) {
5772
 
5773
        $stack = debug_backtrace(false);
5774
        $origin = $stack[0];
5775
 
5776
        foreach ($stack as $depth => $call) {
5777
            if ($call['function'] == 'message_send') {
5778
                $origin = $call;
5779
            }
5780
        }
5781
 
5782
        $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
1441 ariadna 5783
             . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
1 efrain 5784
        $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
5785
    }
5786
 
5787
    if (!empty($CFG->emailheaders)) {
5788
        $headers = array_map('trim', explode("\n", $CFG->emailheaders));
5789
        foreach ($headers as $header) {
5790
            if (!empty($header)) {
5791
                $mail->addCustomHeader($header);
5792
            }
5793
        }
5794
    }
5795
 
5796
    if (!empty($from->priority)) {
5797
        $mail->Priority = $from->priority;
5798
    }
5799
 
5800
    $renderer = $PAGE->get_renderer('core');
5801
    $context = array(
5802
        'sitefullname' => $SITE->fullname,
5803
        'siteshortname' => $SITE->shortname,
5804
        'sitewwwroot' => $CFG->wwwroot,
5805
        'subject' => $subject,
5806
        'prefix' => $CFG->emailsubjectprefix,
5807
        'to' => $user->email,
5808
        'toname' => fullname($user),
5809
        'from' => $mail->From,
5810
        'fromname' => $mail->FromName,
5811
    );
5812
    if (!empty($tempreplyto[0])) {
5813
        $context['replyto'] = $tempreplyto[0][0];
5814
        $context['replytoname'] = $tempreplyto[0][1];
5815
    }
5816
    if ($user->id > 0) {
5817
        $context['touserid'] = $user->id;
5818
        $context['tousername'] = $user->username;
5819
    }
5820
 
5821
    if (!empty($user->mailformat) && $user->mailformat == 1) {
5822
        // Only process html templates if the user preferences allow html email.
5823
 
5824
        if (!$messagehtml) {
5825
            // If no html has been given, BUT there is an html wrapping template then
5826
            // auto convert the text to html and then wrap it.
5827
            $messagehtml = trim(text_to_html($messagetext));
5828
        }
5829
        $context['body'] = $messagehtml;
5830
        $messagehtml = $renderer->render_from_template('core/email_html', $context);
5831
    }
5832
 
5833
    $context['body'] = html_to_text(nl2br($messagetext));
1361 ariadna 5834
    $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
5835
    $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
1 efrain 5836
    $messagetext = $renderer->render_from_template('core/email_text', $context);
5837
 
5838
    // Autogenerate a MessageID if it's missing.
5839
    if (empty($mail->MessageID)) {
5840
        $mail->MessageID = generate_email_messageid();
5841
    }
5842
 
5843
    if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
1441 ariadna 5844
        // Don't ever send HTML to users who don't want it.
1327 ariadna 5845
        $mail->isHTML(true);
5846
        $mail->Encoding = 'quoted-printable';
1441 ariadna 5847
        $mail->Body    =  $messagehtml;
5848
        $mail->AltBody =  "\n$messagetext\n";
1 efrain 5849
    } else {
5850
        $mail->IsHTML(false);
5851
        $mail->Body =  "\n$messagetext\n";
5852
    }
5853
 
5854
    if ($attachment && $attachname) {
1441 ariadna 5855
        if (preg_match( "~\\.\\.~" , $attachment )) {
1 efrain 5856
            // Security check for ".." in dir path.
5857
            $supportuser = core_user::get_support_user();
5858
            $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
5859
            $mail->addStringAttachment('Error in attachment.  User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
5860
        } else {
1441 ariadna 5861
            require_once($CFG->libdir.'/filelib.php');
1 efrain 5862
            $mimetype = mimeinfo('type', $attachname);
5863
 
5864
            // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
5865
            // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
5866
            $attachpath = str_replace('\\', '/', realpath($attachment));
5867
 
5868
            // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
1441 ariadna 5869
            $allowedpaths = array_map(function(string $path): string {
1 efrain 5870
                return str_replace('\\', '/', realpath($path));
5871
            }, [
5872
                $CFG->cachedir,
5873
                $CFG->dataroot,
5874
                $CFG->dirroot,
5875
                $CFG->localcachedir,
5876
                $CFG->tempdir,
5877
                $CFG->localrequestdir,
5878
            ]);
5879
 
5880
            // Set addpath to true.
5881
            $addpath = true;
5882
 
5883
            // Check if attachment includes one of the allowed paths.
5884
            foreach (array_filter($allowedpaths) as $allowedpath) {
5885
                // Set addpath to false if the attachment includes one of the allowed paths.
5886
                if (strpos($attachpath, $allowedpath) === 0) {
5887
                    $addpath = false;
5888
                    break;
5889
                }
5890
            }
5891
 
5892
            // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
5893
            // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
5894
            if ($addpath == true) {
5895
                $attachment = $CFG->dataroot . '/' . $attachment;
5896
            }
5897
 
5898
            $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
5899
        }
5900
    }
5901
 
5902
    // Check if the email should be sent in an other charset then the default UTF-8.
5903
    if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
5904
 
5905
        // Use the defined site mail charset or eventually the one preferred by the recipient.
5906
        $charset = $CFG->sitemailcharset;
5907
        if (!empty($CFG->allowusermailcharset)) {
5908
            if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
5909
                $charset = $useremailcharset;
5910
            }
5911
        }
5912
 
5913
        // Convert all the necessary strings if the charset is supported.
5914
        $charsets = get_list_of_charsets();
5915
        unset($charsets['UTF-8']);
5916
        if (in_array($charset, $charsets)) {
5917
            $mail->CharSet  = $charset;
5918
            $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
5919
            $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
5920
            $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
5921
            $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
5922
 
5923
            foreach ($temprecipients as $key => $values) {
5924
                $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5925
            }
5926
            foreach ($tempreplyto as $key => $values) {
5927
                $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5928
            }
5929
        }
5930
    }
5931
 
5932
    foreach ($temprecipients as $values) {
5933
        $mail->addAddress($values[0], $values[1]);
5934
    }
5935
    foreach ($tempreplyto as $values) {
5936
        $mail->addReplyTo($values[0], $values[1]);
5937
    }
5938
 
5939
    if (!empty($CFG->emaildkimselector)) {
5940
        $domain = substr(strrchr($mail->From, "@"), 1);
5941
        $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
5942
        if (file_exists($pempath)) {
5943
            $mail->DKIM_domain      = $domain;
5944
            $mail->DKIM_private     = $pempath;
5945
            $mail->DKIM_selector    = $CFG->emaildkimselector;
5946
            $mail->DKIM_identity    = $mail->From;
5947
        } else {
5948
            debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
5949
        }
5950
    }
5951
 
5952
    if ($mail->send()) {
5953
        set_send_count($user);
5954
        if (!empty($mail->SMTPDebug)) {
5955
            echo '</pre>';
5956
        }
5957
        return true;
5958
    } else {
5959
        // Trigger event for failing to send email.
5960
        $event = \core\event\email_failed::create(array(
5961
            'context' => context_system::instance(),
5962
            'userid' => $from->id,
5963
            'relateduserid' => $user->id,
5964
            'other' => array(
5965
                'subject' => $subject,
5966
                'message' => $messagetext,
5967
                'errorinfo' => $mail->ErrorInfo
5968
            )
5969
        ));
5970
        $event->trigger();
5971
        if (CLI_SCRIPT) {
1441 ariadna 5972
            mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
1 efrain 5973
        }
5974
        if (!empty($mail->SMTPDebug)) {
5975
            echo '</pre>';
5976
        }
5977
        return false;
5978
    }
5979
}
5980
 
5981
/**
5982
 * Check to see if a user's real email address should be used for the "From" field.
5983
 *
5984
 * @param  object $from The user object for the user we are sending the email from.
5985
 * @param  object $user The user object that we are sending the email to.
5986
 * @param  array $unused No longer used.
5987
 * @return bool Returns true if we can use the from user's email adress in the "From" field.
5988
 */
1441 ariadna 5989
function can_send_from_real_email_address($from, $user, $unused = null) {
1 efrain 5990
    global $CFG;
5991
    if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
5992
        return false;
5993
    }
5994
    $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
5995
    // Email is in the list of allowed domains for sending email,
5996
    // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
5997
    // in a course with the sender.
1441 ariadna 5998
    if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
5999
                && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
6000
                || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
6001
                && enrol_get_shared_courses($user, $from, false, true)))) {
1 efrain 6002
        return true;
6003
    }
6004
    return false;
6005
}
6006
 
6007
/**
6008
 * Generate a signoff for emails based on support settings
6009
 *
6010
 * @return string
6011
 */
1441 ariadna 6012
function generate_email_signoff() {
1 efrain 6013
    global $CFG, $OUTPUT;
6014
 
6015
    $signoff = "\n";
6016
    if (!empty($CFG->supportname)) {
1441 ariadna 6017
        $signoff .= $CFG->supportname."\n";
1 efrain 6018
    }
6019
 
1441 ariadna 6020
    $supportemail = $OUTPUT->supportemail(['class' => 'fw-bold']);
1 efrain 6021
 
6022
    if ($supportemail) {
6023
        $signoff .= "\n" . $supportemail . "\n";
6024
    }
6025
 
6026
    return $signoff;
6027
}
6028
 
6029
/**
6030
 * Sets specified user's password and send the new password to the user via email.
6031
 *
6032
 * @param stdClass $user A {@link $USER} object
6033
 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6034
 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6035
 */
1441 ariadna 6036
function setnew_password_and_mail($user, $fasthash = false) {
1 efrain 6037
    global $CFG, $DB;
6038
 
6039
    // We try to send the mail in language the user understands,
6040
    // unfortunately the filter_string() does not support alternative langs yet
6041
    // so multilang will not work properly for site->fullname.
6042
    $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6043
 
6044
    $site  = get_site();
6045
 
6046
    $supportuser = core_user::get_support_user();
6047
 
6048
    $newpassword = generate_password();
6049
 
6050
    update_internal_user_password($user, $newpassword, $fasthash);
6051
 
6052
    $a = new stdClass();
1441 ariadna 6053
    $placeholders = \core_user::get_name_placeholders($user);
6054
    foreach ($placeholders as $field => $value) {
6055
        $a->{$field} = $value;
6056
    }
1 efrain 6057
    $a->sitename    = format_string($site->fullname);
6058
    $a->username    = $user->username;
6059
    $a->newpassword = $newpassword;
1441 ariadna 6060
    $a->link        = $CFG->wwwroot .'/login/?lang='.$lang;
1 efrain 6061
    $a->signoff     = generate_email_signoff();
6062
 
6063
    $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6064
 
1441 ariadna 6065
    $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
1 efrain 6066
 
6067
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6068
    return email_to_user($user, $supportuser, $subject, $message);
6069
 
6070
}
6071
 
6072
/**
6073
 * Send email to specified user with confirmation text and activation link.
6074
 *
6075
 * @param stdClass $user A {@link $USER} object
6076
 * @param string $confirmationurl user confirmation URL
6077
 * @return bool Returns true if mail was sent OK and false if there was an error.
6078
 */
1441 ariadna 6079
function send_confirmation_email($user, $confirmationurl = null) {
1 efrain 6080
    global $CFG;
6081
 
6082
    $site = get_site();
6083
    $supportuser = core_user::get_support_user();
6084
 
6085
    $data = new stdClass();
6086
    $data->sitename  = format_string($site->fullname);
6087
    $data->admin     = generate_email_signoff();
1441 ariadna 6088
    // Add user name fields to $data based on $user.
6089
    $placeholders = \core_user::get_name_placeholders($user);
6090
    foreach ($placeholders as $field => $value) {
6091
        $data->{$field} = $value;
6092
    }
1 efrain 6093
 
6094
    $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6095
 
6096
    if (empty($confirmationurl)) {
6097
        $confirmationurl = '/login/confirm.php';
6098
    }
6099
 
6100
    $confirmationurl = new moodle_url($confirmationurl);
6101
    // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6102
    $confirmationurl->remove_params('data');
6103
    $confirmationpath = $confirmationurl->out(false);
6104
 
6105
    // We need to custom encode the username to include trailing dots in the link.
6106
    // Because of this custom encoding we can't use moodle_url directly.
6107
    // Determine if a query string is present in the confirmation url.
6108
    $hasquerystring = strpos($confirmationpath, '?') !== false;
6109
    // Perform normal url encoding of the username first.
6110
    $username = urlencode($user->username);
6111
    // Prevent problems with trailing dots not being included as part of link in some mail clients.
6112
    $username = str_replace('.', '%2E', $username);
6113
 
1441 ariadna 6114
    $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username;
1 efrain 6115
 
6116
    $message     = get_string('emailconfirmation', '', $data);
6117
    $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6118
 
6119
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6120
    return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6121
}
6122
 
6123
/**
6124
 * Sends a password change confirmation email.
6125
 *
6126
 * @param stdClass $user A {@link $USER} object
6127
 * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6128
 * @return bool Returns true if mail was sent OK and false if there was an error.
6129
 */
1441 ariadna 6130
function send_password_change_confirmation_email($user, $resetrecord) {
1 efrain 6131
    global $CFG;
6132
 
6133
    $site = get_site();
6134
    $supportuser = core_user::get_support_user();
6135
    $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6136
 
6137
    $data = new stdClass();
1441 ariadna 6138
    $placeholders = \core_user::get_name_placeholders($user);
6139
    foreach ($placeholders as $field => $value) {
6140
        $data->{$field} = $value;
6141
    }
1 efrain 6142
    $data->username  = $user->username;
6143
    $data->sitename  = format_string($site->fullname);
1441 ariadna 6144
    $data->link      = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
1 efrain 6145
    $data->admin     = generate_email_signoff();
6146
    $data->resetminutes = $pwresetmins;
6147
 
6148
    $message = get_string('emailresetconfirmation', '', $data);
6149
    $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6150
 
6151
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6152
    return email_to_user($user, $supportuser, $subject, $message);
1441 ariadna 6153
 
1 efrain 6154
}
6155
 
6156
/**
6157
 * Sends an email containing information on how to change your password.
6158
 *
6159
 * @param stdClass $user A {@link $USER} object
6160
 * @return bool Returns true if mail was sent OK and false if there was an error.
6161
 */
1441 ariadna 6162
function send_password_change_info($user) {
1 efrain 6163
    $site = get_site();
6164
    $supportuser = core_user::get_support_user();
6165
 
6166
    $data = new stdClass();
1441 ariadna 6167
    $placeholders = \core_user::get_name_placeholders($user);
6168
    foreach ($placeholders as $field => $value) {
6169
        $data->{$field} = $value;
6170
    }
1 efrain 6171
    $data->username  = $user->username;
6172
    $data->sitename  = format_string($site->fullname);
6173
    $data->admin     = generate_email_signoff();
6174
 
6175
    if (!is_enabled_auth($user->auth)) {
6176
        $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6177
        $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6178
        // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6179
        return email_to_user($user, $supportuser, $subject, $message);
6180
    }
6181
 
6182
    $userauth = get_auth_plugin($user->auth);
6183
    ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6184
 
6185
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6186
    return email_to_user($user, $supportuser, $subject, $message);
6187
}
6188
 
6189
/**
6190
 * Check that an email is allowed.  It returns an error message if there was a problem.
6191
 *
6192
 * @param string $email Content of email
6193
 * @return string|false
6194
 */
1441 ariadna 6195
function email_is_not_allowed($email) {
1 efrain 6196
    global $CFG;
6197
 
6198
    // Comparing lowercase domains.
6199
    $email = strtolower($email);
6200
    if (!empty($CFG->allowemailaddresses)) {
6201
        $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6202
        foreach ($allowed as $allowedpattern) {
6203
            $allowedpattern = trim($allowedpattern);
6204
            if (!$allowedpattern) {
6205
                continue;
6206
            }
6207
            if (strpos($allowedpattern, '.') === 0) {
6208
                if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6209
                    // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6210
                    return false;
6211
                }
1441 ariadna 6212
 
6213
            } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
1 efrain 6214
                return false;
6215
            }
6216
        }
6217
        return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
1441 ariadna 6218
 
1 efrain 6219
    } else if (!empty($CFG->denyemailaddresses)) {
6220
        $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6221
        foreach ($denied as $deniedpattern) {
6222
            $deniedpattern = trim($deniedpattern);
6223
            if (!$deniedpattern) {
6224
                continue;
6225
            }
6226
            if (strpos($deniedpattern, '.') === 0) {
6227
                if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6228
                    // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6229
                    return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6230
                }
1441 ariadna 6231
 
6232
            } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
1 efrain 6233
                return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6234
            }
6235
        }
6236
    }
6237
 
6238
    return false;
6239
}
6240
 
6241
// FILE HANDLING.
6242
 
6243
/**
6244
 * Returns local file storage instance
6245
 *
6246
 * @return ?file_storage
6247
 */
1441 ariadna 6248
function get_file_storage($reset = false) {
1 efrain 6249
    global $CFG;
6250
 
6251
    static $fs = null;
6252
 
6253
    if ($reset) {
6254
        $fs = null;
6255
        return;
6256
    }
6257
 
6258
    if ($fs) {
6259
        return $fs;
6260
    }
6261
 
6262
    require_once("$CFG->libdir/filelib.php");
6263
 
6264
    $fs = new file_storage();
6265
 
6266
    return $fs;
6267
}
6268
 
6269
/**
6270
 * Returns local file storage instance
6271
 *
6272
 * @return file_browser
6273
 */
1441 ariadna 6274
function get_file_browser() {
1 efrain 6275
    global $CFG;
6276
 
6277
    static $fb = null;
6278
 
6279
    if ($fb) {
6280
        return $fb;
6281
    }
6282
 
6283
    require_once("$CFG->libdir/filelib.php");
6284
 
6285
    $fb = new file_browser();
6286
 
6287
    return $fb;
6288
}
6289
 
6290
/**
6291
 * Returns file packer
6292
 *
6293
 * @param string $mimetype default application/zip
6294
 * @return file_packer|false
6295
 */
1441 ariadna 6296
function get_file_packer($mimetype='application/zip') {
1 efrain 6297
    global $CFG;
6298
 
6299
    static $fp = array();
6300
 
6301
    if (isset($fp[$mimetype])) {
6302
        return $fp[$mimetype];
6303
    }
6304
 
6305
    switch ($mimetype) {
6306
        case 'application/zip':
6307
        case 'application/vnd.moodle.profiling':
6308
            $classname = 'zip_packer';
6309
            break;
6310
 
1441 ariadna 6311
        case 'application/x-gzip' :
1 efrain 6312
            $classname = 'tgz_packer';
6313
            break;
6314
 
6315
        case 'application/vnd.moodle.backup':
6316
            $classname = 'mbz_packer';
6317
            break;
6318
 
6319
        default:
6320
            return false;
6321
    }
6322
 
6323
    require_once("$CFG->libdir/filestorage/$classname.php");
6324
    $fp[$mimetype] = new $classname();
6325
 
6326
    return $fp[$mimetype];
6327
}
6328
 
6329
/**
6330
 * Returns current name of file on disk if it exists.
6331
 *
6332
 * @param string $newfile File to be verified
6333
 * @return string Current name of file on disk if true
6334
 */
1441 ariadna 6335
function valid_uploaded_file($newfile) {
1 efrain 6336
    if (empty($newfile)) {
6337
        return '';
6338
    }
6339
    if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6340
        return $newfile['tmp_name'];
6341
    } else {
6342
        return '';
6343
    }
6344
}
6345
 
6346
/**
6347
 * Returns the maximum size for uploading files.
6348
 *
6349
 * There are seven possible upload limits:
6350
 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6351
 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6352
 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6353
 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6354
 * 5. by the Moodle admin in $CFG->maxbytes
6355
 * 6. by the teacher in the current course $course->maxbytes
6356
 * 7. by the teacher for the current module, eg $assignment->maxbytes
6357
 *
6358
 * These last two are passed to this function as arguments (in bytes).
6359
 * Anything defined as 0 is ignored.
6360
 * The smallest of all the non-zero numbers is returned.
6361
 *
6362
 * @todo Finish documenting this function
6363
 *
6364
 * @param int $sitebytes Set maximum size
6365
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6366
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6367
 * @param bool $unused This parameter has been deprecated and is not used any more.
6368
 * @return int The maximum size for uploading files.
6369
 */
1441 ariadna 6370
function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
1 efrain 6371
 
6372
    if (! $filesize = ini_get('upload_max_filesize')) {
6373
        $filesize = '5M';
6374
    }
6375
    $minimumsize = get_real_size($filesize);
6376
 
6377
    if ($postsize = ini_get('post_max_size')) {
6378
        $postsize = get_real_size($postsize);
6379
        if ($postsize < $minimumsize) {
6380
            $minimumsize = $postsize;
6381
        }
6382
    }
6383
 
6384
    if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6385
        $minimumsize = $sitebytes;
6386
    }
6387
 
6388
    if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6389
        $minimumsize = $coursebytes;
6390
    }
6391
 
6392
    if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6393
        $minimumsize = $modulebytes;
6394
    }
6395
 
6396
    return $minimumsize;
6397
}
6398
 
6399
/**
6400
 * Returns the maximum size for uploading files for the current user
6401
 *
6402
 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6403
 *
6404
 * @param context $context The context in which to check user capabilities
6405
 * @param int $sitebytes Set maximum size
6406
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6407
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6408
 * @param stdClass|int|null $user The user
6409
 * @param bool $unused This parameter has been deprecated and is not used any more.
6410
 * @return int The maximum size for uploading files.
6411
 */
1441 ariadna 6412
function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6413
        $unused = false) {
1 efrain 6414
    global $USER;
6415
 
6416
    if (empty($user)) {
6417
        $user = $USER;
6418
    }
6419
 
6420
    if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6421
        return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6422
    }
6423
 
6424
    return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6425
}
6426
 
6427
/**
6428
 * Returns an array of possible sizes in local language
6429
 *
6430
 * Related to {@link get_max_upload_file_size()} - this function returns an
6431
 * array of possible sizes in an array, translated to the
6432
 * local language.
6433
 *
6434
 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6435
 *
6436
 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6437
 * with the value set to 0. This option will be the first in the list.
6438
 *
6439
 * @uses SORT_NUMERIC
6440
 * @param int $sitebytes Set maximum size
6441
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6442
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6443
 * @param int|array $custombytes custom upload size/s which will be added to list,
6444
 *        Only value/s smaller then maxsize will be added to list.
6445
 * @return array
6446
 */
1441 ariadna 6447
function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
1 efrain 6448
    global $CFG;
6449
 
6450
    if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6451
        return array();
6452
    }
6453
 
6454
    if ($sitebytes == 0) {
6455
        // Will get the minimum of upload_max_filesize or post_max_size.
6456
        $sitebytes = get_max_upload_file_size();
6457
    }
6458
 
6459
    $filesize = array();
1441 ariadna 6460
    $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6461
                      5242880, 10485760, 20971520, 52428800, 104857600,
6462
                      262144000, 524288000, 786432000, 1073741824,
6463
                      2147483648, 4294967296, 8589934592);
1 efrain 6464
 
6465
    // If custombytes is given and is valid then add it to the list.
6466
    if (is_number($custombytes) and $custombytes > 0) {
6467
        $custombytes = (int)$custombytes;
6468
        if (!in_array($custombytes, $sizelist)) {
6469
            $sizelist[] = $custombytes;
6470
        }
6471
    } else if (is_array($custombytes)) {
6472
        $sizelist = array_unique(array_merge($sizelist, $custombytes));
6473
    }
6474
 
6475
    // Allow maxbytes to be selected if it falls outside the above boundaries.
6476
    if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6477
        // Note: get_real_size() is used in order to prevent problems with invalid values.
6478
        $sizelist[] = get_real_size($CFG->maxbytes);
6479
    }
6480
 
6481
    foreach ($sizelist as $sizebytes) {
6482
        if ($sizebytes < $maxsize && $sizebytes > 0) {
6483
            $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6484
        }
6485
    }
6486
 
6487
    $limitlevel = '';
6488
    $displaysize = '';
1441 ariadna 6489
    if ($modulebytes &&
1 efrain 6490
        (($modulebytes < $coursebytes || $coursebytes == 0) &&
1441 ariadna 6491
         ($modulebytes < $sitebytes || $sitebytes == 0))) {
1 efrain 6492
        $limitlevel = get_string('activity', 'core');
6493
        $displaysize = display_size($modulebytes, 0);
6494
        $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6495
 
6496
    } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6497
        $limitlevel = get_string('course', 'core');
6498
        $displaysize = display_size($coursebytes, 0);
6499
        $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6500
 
6501
    } else if ($sitebytes) {
6502
        $limitlevel = get_string('site', 'core');
6503
        $displaysize = display_size($sitebytes, 0);
6504
        $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6505
    }
6506
 
6507
    krsort($filesize, SORT_NUMERIC);
6508
    if ($limitlevel) {
6509
        $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6510
        $filesize  = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6511
    }
6512
 
6513
    return $filesize;
6514
}
6515
 
6516
/**
6517
 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6518
 *
6519
 * If excludefiles is defined, then that file/directory is ignored
6520
 * If getdirs is true, then (sub)directories are included in the output
6521
 * If getfiles is true, then files are included in the output
6522
 * (at least one of these must be true!)
6523
 *
6524
 * @todo Finish documenting this function. Add examples of $excludefile usage.
6525
 *
6526
 * @param string $rootdir A given root directory to start from
6527
 * @param string|array $excludefiles If defined then the specified file/directory is ignored
6528
 * @param bool $descend If true then subdirectories are recursed as well
6529
 * @param bool $getdirs If true then (sub)directories are included in the output
6530
 * @param bool $getfiles  If true then files are included in the output
6531
 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6532
 */
1441 ariadna 6533
function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
1 efrain 6534
 
6535
    $dirs = array();
6536
 
6537
    if (!$getdirs and !$getfiles) {   // Nothing to show.
6538
        return $dirs;
6539
    }
6540
 
6541
    if (!is_dir($rootdir)) {          // Must be a directory.
6542
        return $dirs;
6543
    }
6544
 
6545
    if (!$dir = opendir($rootdir)) {  // Can't open it for some reason.
6546
        return $dirs;
6547
    }
6548
 
6549
    if (!is_array($excludefiles)) {
6550
        $excludefiles = array($excludefiles);
6551
    }
6552
 
6553
    while (false !== ($file = readdir($dir))) {
6554
        $firstchar = substr($file, 0, 1);
6555
        if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6556
            continue;
6557
        }
1441 ariadna 6558
        $fullfile = $rootdir .'/'. $file;
1 efrain 6559
        if (filetype($fullfile) == 'dir') {
6560
            if ($getdirs) {
6561
                $dirs[] = $file;
6562
            }
6563
            if ($descend) {
6564
                $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6565
                foreach ($subdirs as $subdir) {
1441 ariadna 6566
                    $dirs[] = $file .'/'. $subdir;
1 efrain 6567
                }
6568
            }
6569
        } else if ($getfiles) {
6570
            $dirs[] = $file;
6571
        }
6572
    }
6573
    closedir($dir);
6574
 
6575
    asort($dirs);
6576
 
6577
    return $dirs;
6578
}
6579
 
6580
 
6581
/**
6582
 * Adds up all the files in a directory and works out the size.
6583
 *
6584
 * @param string $rootdir  The directory to start from
6585
 * @param string $excludefile A file to exclude when summing directory size
6586
 * @return int The summed size of all files and subfiles within the root directory
6587
 */
1441 ariadna 6588
function get_directory_size($rootdir, $excludefile='') {
1 efrain 6589
    global $CFG;
6590
 
6591
    // Do it this way if we can, it's much faster.
6592
    if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
1441 ariadna 6593
        $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
1 efrain 6594
        $output = null;
6595
        $return = null;
6596
        exec($command, $output, $return);
6597
        if (is_array($output)) {
6598
            // We told it to return k.
1441 ariadna 6599
            return get_real_size(intval($output[0]).'k');
1 efrain 6600
        }
6601
    }
6602
 
6603
    if (!is_dir($rootdir)) {
6604
        // Must be a directory.
6605
        return 0;
6606
    }
6607
 
6608
    if (!$dir = @opendir($rootdir)) {
6609
        // Can't open it for some reason.
6610
        return 0;
6611
    }
6612
 
6613
    $size = 0;
6614
 
6615
    while (false !== ($file = readdir($dir))) {
6616
        $firstchar = substr($file, 0, 1);
6617
        if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6618
            continue;
6619
        }
1441 ariadna 6620
        $fullfile = $rootdir .'/'. $file;
1 efrain 6621
        if (filetype($fullfile) == 'dir') {
6622
            $size += get_directory_size($fullfile, $excludefile);
6623
        } else {
6624
            $size += filesize($fullfile);
6625
        }
6626
    }
6627
    closedir($dir);
6628
 
6629
    return $size;
6630
}
6631
 
6632
/**
6633
 * Converts bytes into display form
6634
 *
6635
 * @param int $size  The size to convert to human readable form
6636
 * @param int $decimalplaces If specified, uses fixed number of decimal places
6637
 * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
6638
 * @return string Display version of size
6639
 */
1441 ariadna 6640
function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string {
1 efrain 6641
 
6642
    static $units;
6643
 
6644
    if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6645
        return get_string('unlimited');
6646
    }
6647
 
6648
    if (empty($units)) {
6649
        $units[] = get_string('sizeb');
6650
        $units[] = get_string('sizekb');
6651
        $units[] = get_string('sizemb');
6652
        $units[] = get_string('sizegb');
6653
        $units[] = get_string('sizetb');
6654
        $units[] = get_string('sizepb');
6655
    }
6656
 
6657
    switch ($fixedunits) {
1441 ariadna 6658
        case 'PB' :
1 efrain 6659
            $magnitude = 5;
6660
            break;
1441 ariadna 6661
        case 'TB' :
1 efrain 6662
            $magnitude = 4;
6663
            break;
1441 ariadna 6664
        case 'GB' :
1 efrain 6665
            $magnitude = 3;
6666
            break;
1441 ariadna 6667
        case 'MB' :
1 efrain 6668
            $magnitude = 2;
6669
            break;
1441 ariadna 6670
        case 'KB' :
1 efrain 6671
            $magnitude = 1;
6672
            break;
1441 ariadna 6673
        case 'B' :
1 efrain 6674
            $magnitude = 0;
6675
            break;
6676
        case '':
6677
            $magnitude = floor(log($size, 1024));
6678
            $magnitude = max(0, min(5, $magnitude));
6679
            break;
6680
        default:
6681
            throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
6682
    }
6683
 
6684
    // Special case for magnitude 0 (bytes) - never use decimal places.
6685
    $nbsp = "\xc2\xa0";
6686
    if ($magnitude === 0) {
6687
        return round($size) . $nbsp . $units[$magnitude];
6688
    }
6689
 
6690
    // Convert to specified units.
6691
    $sizeinunit = $size / 1024 ** $magnitude;
6692
 
6693
    // Fixed decimal places.
6694
    return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
6695
}
6696
 
6697
/**
6698
 * Cleans a given filename by removing suspicious or troublesome characters
6699
 *
6700
 * @see clean_param()
6701
 * @param string $string file name
6702
 * @return string cleaned file name
6703
 */
1441 ariadna 6704
function clean_filename($string) {
1 efrain 6705
    return clean_param($string, PARAM_FILE);
6706
}
6707
 
6708
// STRING TRANSLATION.
6709
 
6710
/**
6711
 * Returns the code for the current language
6712
 *
6713
 * @category string
6714
 * @return string
6715
 */
1441 ariadna 6716
function current_language() {
1 efrain 6717
    global $CFG, $PAGE, $SESSION, $USER;
6718
 
6719
    if (!empty($SESSION->forcelang)) {
6720
        // Allows overriding course-forced language (useful for admins to check
6721
        // issues in courses whose language they don't understand).
6722
        // Also used by some code to temporarily get language-related information in a
6723
        // specific language (see force_current_language()).
6724
        $return = $SESSION->forcelang;
1441 ariadna 6725
 
1 efrain 6726
    } else if (!empty($PAGE->cm->lang)) {
6727
        // Activity language, if set.
6728
        $return = $PAGE->cm->lang;
1441 ariadna 6729
 
1 efrain 6730
    } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) {
6731
        // Course language can override all other settings for this page.
6732
        $return = $PAGE->course->lang;
1441 ariadna 6733
 
1 efrain 6734
    } else if (!empty($SESSION->lang)) {
6735
        // Session language can override other settings.
6736
        $return = $SESSION->lang;
1441 ariadna 6737
 
1 efrain 6738
    } else if (!empty($USER->lang)) {
6739
        $return = $USER->lang;
1441 ariadna 6740
 
1 efrain 6741
    } else if (isset($CFG->lang)) {
6742
        $return = $CFG->lang;
1441 ariadna 6743
 
1 efrain 6744
    } else {
6745
        $return = 'en';
6746
    }
6747
 
6748
    // Just in case this slipped in from somewhere by accident.
6749
    $return = str_replace('_utf8', '', $return);
6750
 
6751
    return $return;
6752
}
6753
 
6754
/**
6755
 * Fix the current language to the given language code.
6756
 *
6757
 * @param string $lang The language code to use.
6758
 * @return void
6759
 */
1441 ariadna 6760
function fix_current_language(string $lang): void {
1 efrain 6761
    global $CFG, $COURSE, $SESSION, $USER;
6762
 
6763
    if (!get_string_manager()->translation_exists($lang)) {
6764
        throw new coding_exception("The language pack for $lang is not available");
6765
    }
6766
 
6767
    $fixglobal = '';
6768
    $fixlang = 'lang';
6769
    if (!empty($SESSION->forcelang)) {
6770
        $fixglobal = $SESSION;
6771
        $fixlang = 'forcelang';
6772
    } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) {
6773
        $fixglobal = $COURSE;
6774
    } else if (!empty($SESSION->lang)) {
6775
        $fixglobal = $SESSION;
6776
    } else if (!empty($USER->lang)) {
6777
        $fixglobal = $USER;
6778
    } else if (isset($CFG->lang)) {
6779
        set_config('lang', $lang);
6780
    }
6781
 
6782
    if ($fixglobal) {
6783
        $fixglobal->$fixlang = $lang;
6784
    }
6785
}
6786
 
6787
/**
6788
 * Returns parent language of current active language if defined
6789
 *
6790
 * @category string
6791
 * @param string $lang null means current language
6792
 * @return string
6793
 */
1441 ariadna 6794
function get_parent_language($lang=null) {
1 efrain 6795
 
6796
    $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
6797
 
6798
    if ($parentlang === 'en') {
6799
        $parentlang = '';
6800
    }
6801
 
6802
    return $parentlang;
6803
}
6804
 
6805
/**
6806
 * Force the current language to get strings and dates localised in the given language.
6807
 *
6808
 * After calling this function, all strings will be provided in the given language
6809
 * until this function is called again, or equivalent code is run.
6810
 *
6811
 * @param string $language
6812
 * @return string previous $SESSION->forcelang value
6813
 */
1441 ariadna 6814
function force_current_language($language) {
1 efrain 6815
    global $SESSION;
6816
    $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
6817
    if ($language !== $sessionforcelang) {
6818
        // Setting forcelang to null or an empty string disables its effect.
6819
        if (empty($language) || get_string_manager()->translation_exists($language, false)) {
6820
            $SESSION->forcelang = $language;
6821
            moodle_setlocale();
6822
        }
6823
    }
6824
    return $sessionforcelang;
6825
}
6826
 
6827
/**
6828
 * Returns current string_manager instance.
6829
 *
6830
 * The param $forcereload is needed for CLI installer only where the string_manager instance
6831
 * must be replaced during the install.php script life time.
6832
 *
6833
 * @category string
6834
 * @param bool $forcereload shall the singleton be released and new instance created instead?
6835
 * @return core_string_manager
6836
 */
1441 ariadna 6837
function get_string_manager($forcereload=false) {
1 efrain 6838
    global $CFG;
6839
 
6840
    static $singleton = null;
6841
 
6842
    if ($forcereload) {
6843
        $singleton = null;
6844
    }
6845
    if ($singleton === null) {
6846
        if (empty($CFG->early_install_lang)) {
6847
 
6848
            $transaliases = array();
6849
            if (empty($CFG->langlist)) {
1441 ariadna 6850
                 $translist = array();
1 efrain 6851
            } else {
6852
                $translist = explode(',', $CFG->langlist);
6853
                $translist = array_map('trim', $translist);
6854
                // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
6855
                foreach ($translist as $i => $value) {
6856
                    $parts = preg_split('/\s*\|\s*/', $value, 2);
6857
                    if (count($parts) == 2) {
6858
                        $transaliases[$parts[0]] = $parts[1];
6859
                        $translist[$i] = $parts[0];
6860
                    }
6861
                }
6862
            }
6863
 
6864
            if (!empty($CFG->config_php_settings['customstringmanager'])) {
6865
                $classname = $CFG->config_php_settings['customstringmanager'];
6866
 
6867
                if (class_exists($classname)) {
6868
                    $implements = class_implements($classname);
6869
 
6870
                    if (isset($implements['core_string_manager'])) {
6871
                        $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
6872
                        return $singleton;
1441 ariadna 6873
 
1 efrain 6874
                    } else {
1441 ariadna 6875
                        debugging('Unable to instantiate custom string manager: class '.$classname.
1 efrain 6876
                            ' does not implement the core_string_manager interface.');
6877
                    }
1441 ariadna 6878
 
1 efrain 6879
                } else {
1441 ariadna 6880
                    debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
1 efrain 6881
                }
6882
            }
6883
 
6884
            $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
1441 ariadna 6885
 
1 efrain 6886
        } else {
6887
            $singleton = new core_string_manager_install();
6888
        }
6889
    }
6890
 
6891
    return $singleton;
6892
}
6893
 
6894
/**
6895
 * Returns a localized string.
6896
 *
6897
 * Returns the translated string specified by $identifier as
6898
 * for $module.  Uses the same format files as STphp.
6899
 * $a is an object, string or number that can be used
6900
 * within translation strings
6901
 *
6902
 * eg 'hello {$a->firstname} {$a->lastname}'
6903
 * or 'hello {$a}'
6904
 *
6905
 * If you would like to directly echo the localized string use
6906
 * the function {@link print_string()}
6907
 *
6908
 * Example usage of this function involves finding the string you would
6909
 * like a local equivalent of and using its identifier and module information
6910
 * to retrieve it.<br/>
6911
 * If you open moodle/lang/en/moodle.php and look near line 278
6912
 * you will find a string to prompt a user for their word for 'course'
6913
 * <code>
6914
 * $string['course'] = 'Course';
6915
 * </code>
6916
 * So if you want to display the string 'Course'
6917
 * in any language that supports it on your site
6918
 * you just need to use the identifier 'course'
6919
 * <code>
6920
 * $mystring = '<strong>'. get_string('course') .'</strong>';
6921
 * or
6922
 * </code>
6923
 * If the string you want is in another file you'd take a slightly
6924
 * different approach. Looking in moodle/lang/en/calendar.php you find
6925
 * around line 75:
6926
 * <code>
6927
 * $string['typecourse'] = 'Course event';
6928
 * </code>
6929
 * If you want to display the string "Course event" in any language
6930
 * supported you would use the identifier 'typecourse' and the module 'calendar'
6931
 * (because it is in the file calendar.php):
6932
 * <code>
6933
 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
6934
 * </code>
6935
 *
6936
 * As a last resort, should the identifier fail to map to a string
6937
 * the returned string will be [[ $identifier ]]
6938
 *
6939
 * In Moodle 2.3 there is a new argument to this function $lazyload.
6940
 * Setting $lazyload to true causes get_string to return a lang_string object
6941
 * rather than the string itself. The fetching of the string is then put off until
6942
 * the string object is first used. The object can be used by calling it's out
6943
 * method or by casting the object to a string, either directly e.g.
6944
 *     (string)$stringobject
6945
 * or indirectly by using the string within another string or echoing it out e.g.
6946
 *     echo $stringobject
6947
 *     return "<p>{$stringobject}</p>";
6948
 * It is worth noting that using $lazyload and attempting to use the string as an
6949
 * array key will cause a fatal error as objects cannot be used as array keys.
6950
 * But you should never do that anyway!
6951
 * For more information {@link lang_string}
6952
 *
6953
 * @category string
6954
 * @param string $identifier The key identifier for the localized string
6955
 * @param string $component The module where the key identifier is stored,
6956
 *      usually expressed as the filename in the language pack without the
6957
 *      .php on the end but can also be written as mod/forum or grade/export/xls.
6958
 *      If none is specified then moodle.php is used.
6959
 * @param string|object|array|int $a An object, string or number that can be used
6960
 *      within translation strings
6961
 * @param bool $lazyload If set to true a string object is returned instead of
6962
 *      the string itself. The string then isn't calculated until it is first used.
6963
 * @return string The localized string.
6964
 * @throws coding_exception
6965
 */
1441 ariadna 6966
function get_string($identifier, $component = '', $a = null, $lazyload = false) {
1 efrain 6967
    global $CFG;
6968
 
6969
    // If the lazy load argument has been supplied return a lang_string object
6970
    // instead.
6971
    // We need to make sure it is true (and a bool) as you will see below there
6972
    // used to be a forth argument at one point.
6973
    if ($lazyload === true) {
6974
        return new lang_string($identifier, $component, $a);
6975
    }
6976
 
6977
    if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
6978
        throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
6979
    }
6980
 
6981
    // There is now a forth argument again, this time it is a boolean however so
6982
    // we can still check for the old extralocations parameter.
6983
    if (!is_bool($lazyload) && !empty($lazyload)) {
6984
        debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
6985
    }
6986
 
6987
    if (strpos((string)$component, '/') !== false) {
6988
        debugging('The module name you passed to get_string is the deprecated format ' .
1441 ariadna 6989
                'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
1 efrain 6990
        $componentpath = explode('/', $component);
6991
 
6992
        switch ($componentpath[0]) {
6993
            case 'mod':
6994
                $component = $componentpath[1];
6995
                break;
6996
            case 'blocks':
6997
            case 'block':
1441 ariadna 6998
                $component = 'block_'.$componentpath[1];
1 efrain 6999
                break;
7000
            case 'enrol':
1441 ariadna 7001
                $component = 'enrol_'.$componentpath[1];
1 efrain 7002
                break;
7003
            case 'format':
1441 ariadna 7004
                $component = 'format_'.$componentpath[1];
1 efrain 7005
                break;
7006
            case 'grade':
1441 ariadna 7007
                $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
1 efrain 7008
                break;
7009
        }
7010
    }
7011
 
7012
    $result = get_string_manager()->get_string($identifier, $component, $a);
7013
 
7014
    // Debugging feature lets you display string identifier and component.
7015
    if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7016
        $result .= ' {' . $identifier . '/' . $component . '}';
7017
    }
7018
    return $result;
7019
}
7020
 
7021
/**
7022
 * Converts an array of strings to their localized value.
7023
 *
7024
 * @param array $array An array of strings
7025
 * @param string $component The language module that these strings can be found in.
7026
 * @return stdClass translated strings.
7027
 */
1441 ariadna 7028
function get_strings($array, $component = '') {
1 efrain 7029
    $string = new stdClass;
7030
    foreach ($array as $item) {
7031
        $string->$item = get_string($item, $component);
7032
    }
7033
    return $string;
7034
}
7035
 
7036
/**
7037
 * Prints out a translated string.
7038
 *
7039
 * Prints out a translated string using the return value from the {@link get_string()} function.
7040
 *
7041
 * Example usage of this function when the string is in the moodle.php file:<br/>
7042
 * <code>
7043
 * echo '<strong>';
7044
 * print_string('course');
7045
 * echo '</strong>';
7046
 * </code>
7047
 *
7048
 * Example usage of this function when the string is not in the moodle.php file:<br/>
7049
 * <code>
7050
 * echo '<h1>';
7051
 * print_string('typecourse', 'calendar');
7052
 * echo '</h1>';
7053
 * </code>
7054
 *
7055
 * @category string
7056
 * @param string $identifier The key identifier for the localized string
7057
 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7058
 * @param string|object|array $a An object, string or number that can be used within translation strings
7059
 */
1441 ariadna 7060
function print_string($identifier, $component = '', $a = null) {
1 efrain 7061
    echo get_string($identifier, $component, $a);
7062
}
7063
 
7064
/**
7065
 * Returns a list of charset codes
7066
 *
7067
 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7068
 * (checking that such charset is supported by the texlib library!)
7069
 *
7070
 * @return array And associative array with contents in the form of charset => charset
7071
 */
1441 ariadna 7072
function get_list_of_charsets() {
1 efrain 7073
 
7074
    $charsets = array(
7075
        'EUC-JP'     => 'EUC-JP',
1441 ariadna 7076
        'ISO-2022-JP'=> 'ISO-2022-JP',
1 efrain 7077
        'ISO-8859-1' => 'ISO-8859-1',
7078
        'SHIFT-JIS'  => 'SHIFT-JIS',
7079
        'GB2312'     => 'GB2312',
7080
        'GB18030'    => 'GB18030', // GB18030 not supported by typo and mbstring.
1441 ariadna 7081
        'UTF-8'      => 'UTF-8');
1 efrain 7082
 
7083
    asort($charsets);
7084
 
7085
    return $charsets;
7086
}
7087
 
7088
/**
7089
 * Returns a list of valid and compatible themes
7090
 *
7091
 * @return array
7092
 */
1441 ariadna 7093
function get_list_of_themes() {
1 efrain 7094
    global $CFG;
7095
 
7096
    $themes = array();
7097
 
7098
    if (!empty($CFG->themelist)) {       // Use admin's list of themes.
7099
        $themelist = explode(',', $CFG->themelist);
7100
    } else {
7101
        $themelist = array_keys(core_component::get_plugin_list("theme"));
7102
    }
7103
 
7104
    foreach ($themelist as $key => $themename) {
7105
        $theme = theme_config::load($themename);
7106
        $themes[$themename] = $theme;
7107
    }
7108
 
7109
    core_collator::asort_objects_by_method($themes, 'get_theme_name');
7110
 
7111
    return $themes;
7112
}
7113
 
7114
/**
1441 ariadna 7115
 * Factory function for {@see \core\emoticon_manager}
1 efrain 7116
 *
1441 ariadna 7117
 * @return \core\emoticon_manager singleton
1 efrain 7118
 */
1441 ariadna 7119
function get_emoticon_manager(): \core\emoticon_manager {
1 efrain 7120
    static $singleton = null;
7121
 
7122
    if (is_null($singleton)) {
1441 ariadna 7123
        $singleton = new \core\emoticon_manager();
1 efrain 7124
    }
7125
 
7126
    return $singleton;
7127
}
7128
 
7129
// ENVIRONMENT CHECKING.
7130
 
7131
/**
7132
 * This method validates a plug name. It is much faster than calling clean_param.
7133
 *
7134
 * @param string $name a string that might be a plugin name.
7135
 * @return bool if this string is a valid plugin name.
7136
 */
1441 ariadna 7137
function is_valid_plugin_name($name) {
1 efrain 7138
    // This does not work for 'mod', bad luck, use any other type.
7139
    return core_component::is_valid_plugin_name('tool', $name);
7140
}
7141
 
7142
/**
7143
 * Get a list of all the plugins of a given type that define a certain API function
7144
 * in a certain file. The plugin component names and function names are returned.
7145
 *
7146
 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7147
 * @param string $function the part of the name of the function after the
7148
 *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7149
 *      names like report_courselist_hook.
7150
 * @param string $file the name of file within the plugin that defines the
7151
 *      function. Defaults to lib.php.
7152
 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7153
 *      and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7154
 */
1441 ariadna 7155
function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
1 efrain 7156
    global $CFG;
7157
 
7158
    // We don't include here as all plugin types files would be included.
7159
    $plugins = get_plugins_with_function($function, $file, false);
7160
 
7161
    if (empty($plugins[$plugintype])) {
7162
        return array();
7163
    }
7164
 
7165
    $allplugins = core_component::get_plugin_list($plugintype);
7166
 
7167
    // Reformat the array and include the files.
7168
    $pluginfunctions = array();
7169
    foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7170
 
7171
        // Check that it has not been removed and the file is still available.
7172
        if (!empty($allplugins[$pluginname])) {
7173
 
7174
            $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7175
            if (file_exists($filepath)) {
7176
                include_once($filepath);
7177
 
7178
                // Now that the file is loaded, we must verify the function still exists.
7179
                if (function_exists($functionname)) {
7180
                    $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7181
                } else {
7182
                    // Invalidate the cache for next run.
7183
                    \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7184
                }
7185
            }
7186
        }
7187
    }
7188
 
7189
    return $pluginfunctions;
7190
}
7191
 
7192
/**
7193
 * Get a list of all the plugins that define a certain API function in a certain file.
7194
 *
7195
 * @param string $function the part of the name of the function after the
7196
 *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7197
 *      names like report_courselist_hook.
7198
 * @param string $file the name of file within the plugin that defines the
7199
 *      function. Defaults to lib.php.
7200
 * @param bool $include Whether to include the files that contain the functions or not.
7201
 * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
7202
 * @return array with [plugintype][plugin] = functionname
7203
 */
1441 ariadna 7204
function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false) {
1 efrain 7205
    global $CFG;
7206
 
7207
    if (during_initial_install() || isset($CFG->upgraderunning)) {
7208
        // API functions _must not_ be called during an installation or upgrade.
7209
        return [];
7210
    }
7211
 
7212
    $plugincallback = $function;
1441 ariadna 7213
    $filtermigrated = function($plugincallback, $pluginfunctions): array {
1 efrain 7214
        foreach ($pluginfunctions as $plugintype => $plugins) {
7215
            foreach ($plugins as $plugin => $unusedfunction) {
7216
                $component = $plugintype . '_' . $plugin;
1441 ariadna 7217
                $hookmanager = di::get(hook\manager::class);
7218
                if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($plugincallback)) {
7219
                    if ($hookmanager->is_deprecating_hook_present($component, $plugincallback)) {
1 efrain 7220
                        // Ignore the old callback, it is there only for older Moodle versions.
7221
                        unset($pluginfunctions[$plugintype][$plugin]);
1441 ariadna 7222
                    } else if ($hookmanager->warn_on_unmigrated_legacy_hooks()) {
1 efrain 7223
                        $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
7224
                        debugging(
7225
                            "Callback $plugincallback in $component component should be migrated to new " .
7226
                                "hook callback for $hookmessage",
7227
                            DEBUG_DEVELOPER
7228
                        );
7229
                    }
7230
                }
7231
            }
7232
        }
7233
        return $pluginfunctions;
7234
    };
7235
 
7236
    $cache = \cache::make('core', 'plugin_functions');
7237
 
7238
    // Including both although I doubt that we will find two functions definitions with the same name.
7239
    // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7240
    $pluginfunctions = false;
7241
    if (!empty($CFG->allversionshash)) {
7242
        $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA);
7243
        $pluginfunctions = $cache->get($key);
7244
    }
7245
    $dirty = false;
7246
 
7247
    // Use the plugin manager to check that plugins are currently installed.
7248
    $pluginmanager = \core_plugin_manager::instance();
7249
 
7250
    if ($pluginfunctions !== false) {
7251
 
7252
        // Checking that the files are still available.
7253
        foreach ($pluginfunctions as $plugintype => $plugins) {
7254
 
7255
            $allplugins = \core_component::get_plugin_list($plugintype);
7256
            $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7257
            foreach ($plugins as $plugin => $function) {
7258
                if (!isset($installedplugins[$plugin])) {
7259
                    // Plugin code is still present on disk but it is not installed.
7260
                    $dirty = true;
7261
                    break 2;
7262
                }
7263
 
7264
                // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7265
                if (empty($allplugins[$plugin])) {
7266
                    $dirty = true;
7267
                    break 2;
7268
                }
7269
 
7270
                $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7271
                if ($include && $fileexists) {
7272
                    // Include the files if it was requested.
7273
                    include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7274
                } else if (!$fileexists) {
7275
                    // If the file is not available any more it should not be returned.
7276
                    $dirty = true;
7277
                    break 2;
7278
                }
7279
 
7280
                // Check if the function still exists in the file.
7281
                if ($include && !function_exists($function)) {
7282
                    $dirty = true;
7283
                    break 2;
7284
                }
7285
            }
7286
        }
7287
 
7288
        // If the cache is dirty, we should fall through and let it rebuild.
7289
        if (!$dirty) {
7290
            if ($migratedtohook && $file === 'lib.php') {
7291
                $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7292
            }
7293
            return $pluginfunctions;
7294
        }
7295
    }
7296
 
7297
    $pluginfunctions = array();
7298
 
7299
    // To fill the cached. Also, everything should continue working with cache disabled.
7300
    $plugintypes = \core_component::get_plugin_types();
7301
    foreach ($plugintypes as $plugintype => $unused) {
7302
 
7303
        // We need to include files here.
7304
        $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7305
        $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7306
        foreach ($pluginswithfile as $plugin => $notused) {
7307
 
7308
            if (!isset($installedplugins[$plugin])) {
7309
                continue;
7310
            }
7311
 
7312
            $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7313
 
7314
            $pluginfunction = false;
7315
            if (function_exists($fullfunction)) {
7316
                // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7317
                $pluginfunction = $fullfunction;
1441 ariadna 7318
 
1 efrain 7319
            } else if ($plugintype === 'mod') {
7320
                // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7321
                $shortfunction = $plugin . '_' . $function;
7322
                if (function_exists($shortfunction)) {
7323
                    $pluginfunction = $shortfunction;
7324
                }
7325
            }
7326
 
7327
            if ($pluginfunction) {
7328
                if (empty($pluginfunctions[$plugintype])) {
7329
                    $pluginfunctions[$plugintype] = array();
7330
                }
7331
                $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7332
            }
1441 ariadna 7333
 
1 efrain 7334
        }
7335
    }
7336
    if (!empty($CFG->allversionshash)) {
7337
        $cache->set($key, $pluginfunctions);
7338
    }
7339
 
7340
    if ($migratedtohook && $file === 'lib.php') {
7341
        $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7342
    }
7343
 
7344
    return $pluginfunctions;
1441 ariadna 7345
 
1 efrain 7346
}
7347
 
7348
/**
7349
 * Lists plugin-like directories within specified directory
7350
 *
7351
 * This function was originally used for standard Moodle plugins, please use
7352
 * new core_component::get_plugin_list() now.
7353
 *
7354
 * This function is used for general directory listing and backwards compatility.
7355
 *
7356
 * @param string $directory relative directory from root
7357
 * @param string $exclude dir name to exclude from the list (defaults to none)
7358
 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7359
 * @return array Sorted array of directory names found under the requested parameters
7360
 */
1441 ariadna 7361
function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
1 efrain 7362
    global $CFG;
7363
 
7364
    $plugins = array();
7365
 
7366
    if (empty($basedir)) {
1441 ariadna 7367
        $basedir = $CFG->dirroot .'/'. $directory;
7368
 
1 efrain 7369
    } else {
1441 ariadna 7370
        $basedir = $basedir .'/'. $directory;
1 efrain 7371
    }
7372
 
7373
    if ($CFG->debugdeveloper and empty($exclude)) {
7374
        // Make sure devs do not use this to list normal plugins,
7375
        // this is intended for general directories that are not plugins!
7376
 
7377
        $subtypes = core_component::get_plugin_types();
7378
        if (in_array($basedir, $subtypes)) {
7379
            debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7380
        }
7381
        unset($subtypes);
7382
    }
7383
 
7384
    $ignorelist = array_flip(array_filter([
7385
        'CVS',
7386
        '_vti_cnf',
7387
        'amd',
7388
        'classes',
7389
        'simpletest',
7390
        'tests',
7391
        'templates',
7392
        'yui',
7393
        $exclude,
7394
    ]));
7395
 
7396
    if (file_exists($basedir) && filetype($basedir) == 'dir') {
7397
        if (!$dirhandle = opendir($basedir)) {
7398
            debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7399
            return array();
7400
        }
7401
        while (false !== ($dir = readdir($dirhandle))) {
7402
            if (strpos($dir, '.') === 0) {
7403
                // Ignore directories starting with .
7404
                // These are treated as hidden directories.
7405
                continue;
7406
            }
7407
            if (array_key_exists($dir, $ignorelist)) {
7408
                // This directory features on the ignore list.
7409
                continue;
7410
            }
1441 ariadna 7411
            if (filetype($basedir .'/'. $dir) != 'dir') {
1 efrain 7412
                continue;
7413
            }
7414
            $plugins[] = $dir;
7415
        }
7416
        closedir($dirhandle);
7417
    }
7418
    if ($plugins) {
7419
        asort($plugins);
7420
    }
7421
    return $plugins;
7422
}
7423
 
7424
/**
7425
 * Invoke plugin's callback functions
7426
 *
7427
 * @param string $type plugin type e.g. 'mod'
7428
 * @param string $name plugin name
7429
 * @param string $feature feature name
7430
 * @param string $action feature's action
7431
 * @param array $params parameters of callback function, should be an array
7432
 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7433
 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7434
 * @return mixed
7435
 *
7436
 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7437
 */
1441 ariadna 7438
function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false) {
1 efrain 7439
    return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
7440
}
7441
 
7442
/**
7443
 * Invoke component's callback functions
7444
 *
7445
 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7446
 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7447
 * @param array $params parameters of callback function
7448
 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7449
 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7450
 * @return mixed
7451
 */
1441 ariadna 7452
function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false) {
1 efrain 7453
    $functionname = component_callback_exists($component, $function);
7454
 
7455
    if ($functionname) {
7456
        if ($migratedtohook) {
7457
            $hookmanager = di::get(hook\manager::class);
7458
            if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($function)) {
7459
                if ($hookmanager->is_deprecating_hook_present($component, $function)) {
7460
                    // Do not call the old lib.php callback,
7461
                    // it is there for compatibility with older Moodle versions only.
7462
                    return null;
1441 ariadna 7463
                } else if ($hookmanager->warn_on_unmigrated_legacy_hooks()) {
1 efrain 7464
                    $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
7465
                    debugging(
7466
                        "Callback $function in $component component should be migrated to new hook callback for $hookmessage",
1441 ariadna 7467
                        DEBUG_DEVELOPER);
1 efrain 7468
                }
7469
            }
7470
        }
7471
 
7472
        // Function exists, so just return function result.
7473
        $ret = call_user_func_array($functionname, $params);
7474
        if (is_null($ret)) {
7475
            return $default;
7476
        } else {
7477
            return $ret;
7478
        }
7479
    }
7480
    return $default;
7481
}
7482
 
7483
/**
7484
 * Determine if a component callback exists and return the function name to call. Note that this
7485
 * function will include the required library files so that the functioname returned can be
7486
 * called directly.
7487
 *
7488
 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7489
 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7490
 * @return mixed Complete function name to call if the callback exists or false if it doesn't.
7491
 * @throws coding_exception if invalid component specfied
7492
 */
1441 ariadna 7493
function component_callback_exists($component, $function) {
1 efrain 7494
    global $CFG; // This is needed for the inclusions.
7495
 
7496
    $cleancomponent = clean_param($component, PARAM_COMPONENT);
7497
    if (empty($cleancomponent)) {
7498
        throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7499
    }
7500
    $component = $cleancomponent;
7501
 
7502
    list($type, $name) = core_component::normalize_component($component);
7503
    $component = $type . '_' . $name;
7504
 
1441 ariadna 7505
    // Deprecated plugin type: callbacks not supported.
7506
    if (\core_component::is_plugintype_in_deprecation($type)) {
7507
        return false;
7508
    }
1 efrain 7509
 
1441 ariadna 7510
    $oldfunction = $name.'_'.$function;
7511
    $function = $component.'_'.$function;
7512
 
1 efrain 7513
    $dir = core_component::get_component_directory($component);
7514
    if (empty($dir)) {
7515
        throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7516
    }
7517
 
7518
    // Load library and look for function.
1441 ariadna 7519
    if (file_exists($dir.'/lib.php')) {
7520
        require_once($dir.'/lib.php');
1 efrain 7521
    }
7522
 
7523
    if (!function_exists($function) and function_exists($oldfunction)) {
7524
        if ($type !== 'mod' and $type !== 'core') {
7525
            debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
7526
        }
7527
        $function = $oldfunction;
7528
    }
7529
 
7530
    if (function_exists($function)) {
7531
        return $function;
7532
    }
7533
    return false;
7534
}
7535
 
7536
/**
7537
 * Call the specified callback method on the provided class.
7538
 *
7539
 * If the callback returns null, then the default value is returned instead.
7540
 * If the class does not exist, then the default value is returned.
7541
 *
7542
 * @param   string      $classname The name of the class to call upon.
7543
 * @param   string      $methodname The name of the staticically defined method on the class.
7544
 * @param   array       $params The arguments to pass into the method.
7545
 * @param   mixed       $default The default value.
7546
 * @param   bool        $migratedtohook True if the callback has been migrated to a hook.
7547
 * @return  mixed       The return value.
7548
 */
1441 ariadna 7549
function component_class_callback($classname, $methodname, array $params, $default = null, bool $migratedtohook = false) {
1 efrain 7550
    if (!class_exists($classname)) {
7551
        return $default;
7552
    }
7553
 
7554
    if (!method_exists($classname, $methodname)) {
7555
        return $default;
7556
    }
7557
 
1441 ariadna 7558
    // If component can be found (flat class names not supported), and it's a deprecated plugintype, callbacks are unsupported.
7559
    $component = \core_component::get_component_from_classname($classname);
7560
    if ($component) {
7561
        [$type] = \core_component::normalize_component($component);
7562
        if (\core_component::is_plugintype_in_deprecation($type)) {
7563
            return $default;
7564
        }
7565
    }
7566
 
1 efrain 7567
    $fullfunction = $classname . '::' . $methodname;
7568
 
7569
    if ($migratedtohook) {
7570
        $functionparts = explode('\\', trim($fullfunction, '\\'));
7571
        $component = $functionparts[0];
7572
        $callback = end($functionparts);
7573
        $hookmanager = di::get(hook\manager::class);
7574
        if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($callback)) {
7575
            if ($hookmanager->is_deprecating_hook_present($component, $callback)) {
7576
                // Do not call the old class callback,
7577
                // it is there for compatibility with older Moodle versions only.
7578
                return null;
1441 ariadna 7579
            } else if ($hookmanager->warn_on_unmigrated_legacy_hooks()) {
1 efrain 7580
                $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
1441 ariadna 7581
                debugging("Callback $callback in $component component should be migrated to new hook callback for $hookmessage",
7582
                        DEBUG_DEVELOPER);
1 efrain 7583
            }
7584
        }
7585
    }
7586
 
7587
    $result = call_user_func_array($fullfunction, $params);
7588
 
7589
    if (null === $result) {
7590
        return $default;
7591
    } else {
7592
        return $result;
7593
    }
7594
}
7595
 
7596
/**
7597
 * Checks whether a plugin supports a specified feature.
7598
 *
7599
 * @param string $type Plugin type e.g. 'mod'
7600
 * @param string $name Plugin name e.g. 'forum'
7601
 * @param string $feature Feature code (FEATURE_xx constant)
7602
 * @param mixed $default default value if feature support unknown
7603
 * @return mixed Feature result (false if not supported, null if feature is unknown,
7604
 *         otherwise usually true but may have other feature-specific value such as array)
7605
 * @throws coding_exception
7606
 */
1441 ariadna 7607
function plugin_supports($type, $name, $feature, $default = null) {
1 efrain 7608
    global $CFG;
7609
 
7610
    if ($type === 'mod' and $name === 'NEWMODULE') {
7611
        // Somebody forgot to rename the module template.
7612
        return false;
7613
    }
7614
 
7615
    $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
7616
    if (empty($component)) {
7617
        throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
7618
    }
7619
 
7620
    $function = null;
7621
 
7622
    if ($type === 'mod') {
7623
        // We need this special case because we support subplugins in modules,
7624
        // otherwise it would end up in infinite loop.
7625
        if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
7626
            include_once("$CFG->dirroot/mod/$name/lib.php");
1441 ariadna 7627
            $function = $component.'_supports';
1 efrain 7628
            if (!function_exists($function)) {
7629
                // Legacy non-frankenstyle function name.
1441 ariadna 7630
                $function = $name.'_supports';
1 efrain 7631
            }
7632
        }
1441 ariadna 7633
 
1 efrain 7634
    } else {
7635
        if (!$path = core_component::get_plugin_directory($type, $name)) {
7636
            // Non existent plugin type.
7637
            return false;
7638
        }
7639
        if (file_exists("$path/lib.php")) {
7640
            include_once("$path/lib.php");
1441 ariadna 7641
            $function = $component.'_supports';
1 efrain 7642
        }
7643
    }
7644
 
7645
    if ($function and function_exists($function)) {
7646
        $supports = $function($feature);
7647
        if (is_null($supports)) {
7648
            // Plugin does not know - use default.
7649
            return $default;
7650
        } else {
7651
            return $supports;
7652
        }
7653
    }
7654
 
7655
    // Plugin does not care, so use default.
7656
    return $default;
7657
}
7658
 
7659
/**
7660
 * Returns true if the current version of PHP is greater that the specified one.
7661
 *
7662
 * @todo Check PHP version being required here is it too low?
7663
 *
7664
 * @param string $version The version of php being tested.
7665
 * @return bool
7666
 */
1441 ariadna 7667
function check_php_version($version='5.2.4') {
1 efrain 7668
    return (version_compare(phpversion(), $version) >= 0);
7669
}
7670
 
7671
/**
7672
 * Determine if moodle installation requires update.
7673
 *
7674
 * Checks version numbers of main code and all plugins to see
7675
 * if there are any mismatches.
7676
 *
7677
 * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
7678
 * @return bool
7679
 */
1441 ariadna 7680
function moodle_needs_upgrading($checkupgradeflag = true) {
1 efrain 7681
    global $CFG, $DB;
7682
 
7683
    // Say no if there is already an upgrade running.
7684
    if ($checkupgradeflag) {
7685
        $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']);
7686
        $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING);
7687
        // If we ARE locked, but this PHP process is NOT the process running the upgrade,
7688
        // We should always return false.
7689
        // This means the upgrade is running from CLI somewhere, or about to.
7690
        if (!empty($lock) && !$currentprocessrunningupgrade) {
7691
            return false;
7692
        }
7693
    }
7694
 
7695
    if (empty($CFG->version)) {
7696
        return true;
7697
    }
7698
 
7699
    // There is no need to purge plugininfo caches here because
7700
    // these caches are not used during upgrade and they are purged after
7701
    // every upgrade.
7702
 
7703
    if (empty($CFG->allversionshash)) {
7704
        return true;
7705
    }
7706
 
7707
    $hash = core_component::get_all_versions_hash();
7708
 
7709
    return ($hash !== $CFG->allversionshash);
7710
}
7711
 
7712
/**
7713
 * Returns the major version of this site
7714
 *
7715
 * Moodle version numbers consist of three numbers separated by a dot, for
7716
 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
7717
 * called major version. This function extracts the major version from either
7718
 * $CFG->release (default) or eventually from the $release variable defined in
7719
 * the main version.php.
7720
 *
7721
 * @param bool $fromdisk should the version if source code files be used
7722
 * @return string|false the major version like '2.3', false if could not be determined
7723
 */
1441 ariadna 7724
function moodle_major_version($fromdisk = false) {
1 efrain 7725
    global $CFG;
7726
 
7727
    if ($fromdisk) {
7728
        $release = null;
1441 ariadna 7729
        require($CFG->dirroot.'/version.php');
1 efrain 7730
        if (empty($release)) {
7731
            return false;
7732
        }
1441 ariadna 7733
 
1 efrain 7734
    } else {
7735
        if (empty($CFG->release)) {
7736
            return false;
7737
        }
7738
        $release = $CFG->release;
7739
    }
7740
 
7741
    if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
7742
        return $matches[0];
7743
    } else {
7744
        return false;
7745
    }
7746
}
7747
 
7748
// MISCELLANEOUS.
7749
 
7750
/**
7751
 * Gets the system locale
7752
 *
7753
 * @return string Retuns the current locale.
7754
 */
1441 ariadna 7755
function moodle_getlocale() {
1 efrain 7756
    global $CFG;
7757
 
7758
    // Fetch the correct locale based on ostype.
7759
    if ($CFG->ostype == 'WINDOWS') {
7760
        $stringtofetch = 'localewin';
7761
    } else {
7762
        $stringtofetch = 'locale';
7763
    }
7764
 
7765
    if (!empty($CFG->locale)) { // Override locale for all language packs.
7766
        return $CFG->locale;
7767
    }
7768
 
7769
    return get_string($stringtofetch, 'langconfig');
7770
}
7771
 
7772
/**
7773
 * Sets the system locale
7774
 *
7775
 * @category string
7776
 * @param string $locale Can be used to force a locale
7777
 */
1441 ariadna 7778
function moodle_setlocale($locale='') {
1 efrain 7779
    global $CFG;
7780
 
7781
    static $currentlocale = ''; // Last locale caching.
7782
 
7783
    $oldlocale = $currentlocale;
7784
 
7785
    // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
7786
    if (!empty($locale)) {
7787
        $currentlocale = $locale;
7788
    } else {
7789
        $currentlocale = moodle_getlocale();
7790
    }
7791
 
7792
    // Do nothing if locale already set up.
7793
    if ($oldlocale == $currentlocale) {
7794
        return;
7795
    }
7796
 
7797
    // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
7798
    // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
7799
    // Some day, numeric, monetary and other categories should be set too, I think. :-/.
7800
 
7801
    // Get current values.
1441 ariadna 7802
    $monetary= setlocale (LC_MONETARY, 0);
7803
    $numeric = setlocale (LC_NUMERIC, 0);
7804
    $ctype   = setlocale (LC_CTYPE, 0);
1 efrain 7805
    if ($CFG->ostype != 'WINDOWS') {
1441 ariadna 7806
        $messages= setlocale (LC_MESSAGES, 0);
1 efrain 7807
    }
7808
    // Set locale to all.
1441 ariadna 7809
    $result = setlocale (LC_ALL, $currentlocale);
1 efrain 7810
    // If setting of locale fails try the other utf8 or utf-8 variant,
7811
    // some operating systems support both (Debian), others just one (OSX).
7812
    if ($result === false) {
7813
        if (stripos($currentlocale, '.UTF-8') !== false) {
7814
            $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
1441 ariadna 7815
            setlocale (LC_ALL, $newlocale);
1 efrain 7816
        } else if (stripos($currentlocale, '.UTF8') !== false) {
7817
            $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
1441 ariadna 7818
            setlocale (LC_ALL, $newlocale);
1 efrain 7819
        }
7820
    }
7821
    // Set old values.
1441 ariadna 7822
    setlocale (LC_MONETARY, $monetary);
7823
    setlocale (LC_NUMERIC, $numeric);
1 efrain 7824
    if ($CFG->ostype != 'WINDOWS') {
1441 ariadna 7825
        setlocale (LC_MESSAGES, $messages);
1 efrain 7826
    }
7827
    if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
7828
        // To workaround a well-known PHP problem with Turkish letter Ii.
1441 ariadna 7829
        setlocale (LC_CTYPE, $ctype);
1 efrain 7830
    }
7831
}
7832
 
7833
/**
7834
 * Count words in a string.
7835
 *
7836
 * Words are defined as things between whitespace.
7837
 *
7838
 * @category string
7839
 * @param string $string The text to be searched for words. May be HTML.
7840
 * @param int|null $format
7841
 * @return int The count of words in the specified string
7842
 */
1441 ariadna 7843
function count_words($string, $format = null) {
1 efrain 7844
    // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
7845
    // Also, br is a special case because it definitely delimits a word, but has no close tag.
7846
    $string = preg_replace('~
7847
            (                                   # Capture the tag we match.
7848
                </                              # Start of close tag.
7849
                (?!                             # Do not match any of these specific close tag names.
7850
                    a> | b> | del> | em> | i> |
7851
                    ins> | s> | small> | span> |
7852
                    strong> | sub> | sup> | u>
7853
                )
7854
                \w+                             # But, apart from those execptions, match any tag name.
7855
                >                               # End of close tag.
7856
            |
7857
                <br> | <br\s*/>                 # Special cases that are not close tags.
7858
            )
7859
            ~x', '$1 ', $string); // Add a space after the close tag.
7860
    if ($format !== null && $format != FORMAT_PLAIN) {
7861
        // Match the usual text cleaning before display.
7862
        // Ideally we should apply multilang filter only here, other filters might add extra text.
7863
        $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
7864
    }
7865
    // Now remove HTML tags.
7866
    $string = strip_tags($string);
7867
    // Decode HTML entities.
7868
    $string = html_entity_decode($string, ENT_COMPAT);
7869
 
7870
    // Now, the word count is the number of blocks of characters separated
7871
    // by any sort of space. That seems to be the definition used by all other systems.
7872
    // To be precise about what is considered to separate words:
7873
    // * Anything that Unicode considers a 'Separator'
7874
    // * Anything that Unicode considers a 'Control character'
7875
    // * An em- or en- dash.
7876
    return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
7877
}
7878
 
7879
/**
7880
 * Count letters in a string.
7881
 *
7882
 * Letters are defined as chars not in tags and different from whitespace.
7883
 *
7884
 * @category string
7885
 * @param string $string The text to be searched for letters. May be HTML.
7886
 * @param int|null $format
7887
 * @return int The count of letters in the specified text.
7888
 */
1441 ariadna 7889
function count_letters($string, $format = null) {
1 efrain 7890
    if ($format !== null && $format != FORMAT_PLAIN) {
7891
        // Match the usual text cleaning before display.
7892
        // Ideally we should apply multilang filter only here, other filters might add extra text.
7893
        $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
7894
    }
7895
    $string = strip_tags($string); // Tags are out now.
7896
    $string = html_entity_decode($string, ENT_COMPAT);
7897
    $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
7898
 
7899
    return core_text::strlen($string);
7900
}
7901
 
7902
/**
7903
 * Generate and return a random string of the specified length.
7904
 *
7905
 * @param int $length The length of the string to be created.
7906
 * @return string
7907
 */
1441 ariadna 7908
function random_string($length=15) {
1 efrain 7909
    $randombytes = random_bytes($length);
7910
    $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
7911
    $pool .= 'abcdefghijklmnopqrstuvwxyz';
7912
    $pool .= '0123456789';
7913
    $poollen = strlen($pool);
7914
    $string = '';
7915
    for ($i = 0; $i < $length; $i++) {
7916
        $rand = ord($randombytes[$i]);
1441 ariadna 7917
        $string .= substr($pool, ($rand%($poollen)), 1);
1 efrain 7918
    }
7919
    return $string;
7920
}
7921
 
7922
/**
7923
 * Generate a complex random string (useful for md5 salts)
7924
 *
7925
 * This function is based on the above {@link random_string()} however it uses a
7926
 * larger pool of characters and generates a string between 24 and 32 characters
7927
 *
7928
 * @param int $length Optional if set generates a string to exactly this length
7929
 * @return string
7930
 */
1441 ariadna 7931
function complex_random_string($length=null) {
1 efrain 7932
    $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
7933
    $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
7934
    $poollen = strlen($pool);
1441 ariadna 7935
    if ($length===null) {
1 efrain 7936
        $length = floor(rand(24, 32));
7937
    }
7938
    $randombytes = random_bytes($length);
7939
    $string = '';
7940
    for ($i = 0; $i < $length; $i++) {
7941
        $rand = ord($randombytes[$i]);
1441 ariadna 7942
        $string .= $pool[($rand%$poollen)];
1 efrain 7943
    }
7944
    return $string;
7945
}
7946
 
7947
/**
7948
 * Given some text (which may contain HTML) and an ideal length,
7949
 * this function truncates the text neatly on a word boundary if possible
7950
 *
7951
 * @category string
7952
 * @param string $text text to be shortened
7953
 * @param int $ideal ideal string length
7954
 * @param boolean $exact if false, $text will not be cut mid-word
7955
 * @param string $ending The string to append if the passed string is truncated
7956
 * @return string $truncate shortened string
7957
 */
1441 ariadna 7958
function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
1 efrain 7959
    // If the plain text is shorter than the maximum length, return the whole text.
7960
    if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
7961
        return $text;
7962
    }
7963
 
7964
    // Splits on HTML tags. Each open/close/empty tag will be the first thing
7965
    // and only tag in its 'line'.
7966
    preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
7967
 
7968
    $totallength = core_text::strlen($ending);
7969
    $truncate = '';
7970
 
7971
    // This array stores information about open and close tags and their position
7972
    // in the truncated string. Each item in the array is an object with fields
7973
    // ->open (true if open), ->tag (tag name in lower case), and ->pos
7974
    // (byte position in truncated text).
7975
    $tagdetails = array();
7976
 
7977
    foreach ($lines as $linematchings) {
7978
        // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
7979
        if (!empty($linematchings[1])) {
7980
            // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
7981
            if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
7982
                if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
7983
                    // Record closing tag.
7984
                    $tagdetails[] = (object) array(
1441 ariadna 7985
                            'open' => false,
7986
                            'tag'  => core_text::strtolower($tagmatchings[1]),
7987
                            'pos'  => core_text::strlen($truncate),
7988
                        );
7989
 
1 efrain 7990
                } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
7991
                    // Record opening tag.
7992
                    $tagdetails[] = (object) array(
1441 ariadna 7993
                            'open' => true,
7994
                            'tag'  => core_text::strtolower($tagmatchings[1]),
7995
                            'pos'  => core_text::strlen($truncate),
7996
                        );
1 efrain 7997
                } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
7998
                    $tagdetails[] = (object) array(
1441 ariadna 7999
                            'open' => true,
8000
                            'tag'  => core_text::strtolower('if'),
8001
                            'pos'  => core_text::strlen($truncate),
1 efrain 8002
                    );
8003
                } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8004
                    $tagdetails[] = (object) array(
1441 ariadna 8005
                            'open' => false,
8006
                            'tag'  => core_text::strtolower('if'),
8007
                            'pos'  => core_text::strlen($truncate),
1 efrain 8008
                    );
8009
                }
8010
            }
8011
            // Add html-tag to $truncate'd text.
8012
            $truncate .= $linematchings[1];
8013
        }
8014
 
8015
        // Calculate the length of the plain text part of the line; handle entities as one character.
8016
        $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8017
        if ($totallength + $contentlength > $ideal) {
8018
            // The number of characters which are left.
8019
            $left = $ideal - $totallength;
8020
            $entitieslength = 0;
8021
            // Search for html entities.
8022
            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)) {
8023
                // Calculate the real length of all entities in the legal range.
8024
                foreach ($entities[0] as $entity) {
1441 ariadna 8025
                    if ($entity[1]+1-$entitieslength <= $left) {
1 efrain 8026
                        $left--;
8027
                        $entitieslength += core_text::strlen($entity[0]);
8028
                    } else {
8029
                        // No more characters left.
8030
                        break;
8031
                    }
8032
                }
8033
            }
8034
            $breakpos = $left + $entitieslength;
8035
 
8036
            // If the words shouldn't be cut in the middle...
8037
            if (!$exact) {
8038
                // Search the last occurence of a space.
8039
                for (; $breakpos > 0; $breakpos--) {
8040
                    if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8041
                        if ($char === '.' or $char === ' ') {
8042
                            $breakpos += 1;
8043
                            break;
8044
                        } else if (strlen($char) > 2) {
8045
                            // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8046
                            $breakpos += 1;
8047
                            break;
8048
                        }
8049
                    }
8050
                }
8051
            }
8052
            if ($breakpos == 0) {
8053
                // This deals with the test_shorten_text_no_spaces case.
8054
                $breakpos = $left + $entitieslength;
8055
            } else if ($breakpos > $left + $entitieslength) {
8056
                // This deals with the previous for loop breaking on the first char.
8057
                $breakpos = $left + $entitieslength;
8058
            }
8059
 
8060
            $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8061
            // Maximum length is reached, so get off the loop.
8062
            break;
8063
        } else {
8064
            $truncate .= $linematchings[2];
8065
            $totallength += $contentlength;
8066
        }
8067
 
8068
        // If the maximum length is reached, get off the loop.
8069
        if ($totallength >= $ideal) {
8070
            break;
8071
        }
8072
    }
8073
 
8074
    // Add the defined ending to the text.
8075
    $truncate .= $ending;
8076
 
8077
    // Now calculate the list of open html tags based on the truncate position.
8078
    $opentags = array();
8079
    foreach ($tagdetails as $taginfo) {
8080
        if ($taginfo->open) {
8081
            // Add tag to the beginning of $opentags list.
8082
            array_unshift($opentags, $taginfo->tag);
8083
        } else {
8084
            // Can have multiple exact same open tags, close the last one.
8085
            $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8086
            if ($pos !== false) {
8087
                unset($opentags[$pos]);
8088
            }
8089
        }
8090
    }
8091
 
8092
    // Close all unclosed html-tags.
8093
    foreach ($opentags as $tag) {
8094
        if ($tag === 'if') {
8095
            $truncate .= '<!--<![endif]-->';
8096
        } else {
8097
            $truncate .= '</' . $tag . '>';
8098
        }
8099
    }
8100
 
8101
    return $truncate;
8102
}
8103
 
8104
/**
8105
 * Shortens a given filename by removing characters positioned after the ideal string length.
8106
 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8107
 * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8108
 *
8109
 * @param string $filename file name
8110
 * @param int $length ideal string length
8111
 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8112
 * @return string $shortened shortened file name
8113
 */
1441 ariadna 8114
function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) {
1 efrain 8115
    $shortened = $filename;
8116
    // Extract a part of the filename if it's char size exceeds the ideal string length.
8117
    if (core_text::strlen($filename) > $length) {
8118
        // Exclude extension if present in filename.
8119
        $mimetypes = get_mimetypes_array();
8120
        $extension = pathinfo($filename, PATHINFO_EXTENSION);
8121
        if ($extension && !empty($mimetypes[$extension])) {
8122
            $basename = pathinfo($filename, PATHINFO_FILENAME);
8123
            $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8124
            $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8125
            $shortened .= '.' . $extension;
8126
        } else {
8127
            $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8128
            $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8129
        }
8130
    }
8131
    return $shortened;
8132
}
8133
 
8134
/**
8135
 * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8136
 *
8137
 * @param array $path The paths to reduce the length.
8138
 * @param int $length Ideal string length
8139
 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8140
 * @return array $result Shortened paths in array.
8141
 */
1441 ariadna 8142
function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) {
1 efrain 8143
    $result = null;
8144
 
1441 ariadna 8145
    $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
1 efrain 8146
        $carry[] = shorten_filename($singlepath, $length, $includehash);
8147
        return $carry;
8148
    }, []);
8149
 
8150
    return $result;
8151
}
8152
 
8153
/**
8154
 * Given dates in seconds, how many weeks is the date from startdate
8155
 * The first week is 1, the second 2 etc ...
8156
 *
8157
 * @param int $startdate Timestamp for the start date
8158
 * @param int $thedate Timestamp for the end date
8159
 * @return string
8160
 */
1441 ariadna 8161
function getweek ($startdate, $thedate) {
1 efrain 8162
    if ($thedate < $startdate) {
8163
        return 0;
8164
    }
8165
 
8166
    return floor(($thedate - $startdate) / WEEKSECS) + 1;
8167
}
8168
 
8169
/**
8170
 * Returns a randomly generated password of length $maxlen.  inspired by
8171
 *
8172
 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8173
 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8174
 *
8175
 * @param int $maxlen  The maximum size of the password being generated.
8176
 * @return string
8177
 */
1441 ariadna 8178
function generate_password($maxlen=10) {
1 efrain 8179
    global $CFG;
8180
 
8181
    if (empty($CFG->passwordpolicy)) {
8182
        $fillers = PASSWORD_DIGITS;
8183
        $wordlist = file($CFG->wordlist);
8184
        $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8185
        $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8186
        $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8187
        $password = $word1 . $filler1 . $word2;
8188
    } else {
8189
        $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8190
        $digits = $CFG->minpassworddigits;
8191
        $lower = $CFG->minpasswordlower;
8192
        $upper = $CFG->minpasswordupper;
8193
        $nonalphanum = $CFG->minpasswordnonalphanum;
8194
        $total = $lower + $upper + $digits + $nonalphanum;
8195
        // Var minlength should be the greater one of the two ( $minlen and $total ).
8196
        $minlen = $minlen < $total ? $total : $minlen;
8197
        // Var maxlen can never be smaller than minlen.
8198
        $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8199
        $additional = $maxlen - $total;
8200
 
8201
        // Make sure we have enough characters to fulfill
8202
        // complexity requirements.
8203
        $passworddigits = PASSWORD_DIGITS;
8204
        while ($digits > strlen($passworddigits)) {
8205
            $passworddigits .= PASSWORD_DIGITS;
8206
        }
8207
        $passwordlower = PASSWORD_LOWER;
8208
        while ($lower > strlen($passwordlower)) {
8209
            $passwordlower .= PASSWORD_LOWER;
8210
        }
8211
        $passwordupper = PASSWORD_UPPER;
8212
        while ($upper > strlen($passwordupper)) {
8213
            $passwordupper .= PASSWORD_UPPER;
8214
        }
8215
        $passwordnonalphanum = PASSWORD_NONALPHANUM;
8216
        while ($nonalphanum > strlen($passwordnonalphanum)) {
8217
            $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8218
        }
8219
 
8220
        // Now mix and shuffle it all.
1441 ariadna 8221
        $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8222
                                 substr(str_shuffle ($passwordupper), 0, $upper) .
8223
                                 substr(str_shuffle ($passworddigits), 0, $digits) .
8224
                                 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8225
                                 substr(str_shuffle ($passwordlower .
8226
                                                     $passwordupper .
8227
                                                     $passworddigits .
8228
                                                     $passwordnonalphanum), 0 , $additional));
1 efrain 8229
    }
8230
 
1441 ariadna 8231
    return substr ($password, 0, $maxlen);
1 efrain 8232
}
8233
 
8234
/**
8235
 * Given a float, prints it nicely.
8236
 * Localized floats must not be used in calculations!
8237
 *
8238
 * The stripzeros feature is intended for making numbers look nicer in small
8239
 * areas where it is not necessary to indicate the degree of accuracy by showing
8240
 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8241
 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8242
 *
8243
 * @param float $float The float to print
8244
 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8245
 * @param bool $localized use localized decimal separator
8246
 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8247
 *                         the decimal point are always striped if $decimalpoints is -1.
8248
 * @return string locale float
8249
 */
1441 ariadna 8250
function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
1 efrain 8251
    if (is_null($float)) {
8252
        return '';
8253
    }
8254
    if ($localized) {
8255
        $separator = get_string('decsep', 'langconfig');
8256
    } else {
8257
        $separator = '.';
8258
    }
8259
    if ($decimalpoints == -1) {
8260
        // The following counts the number of decimals.
8261
        // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8262
        $floatval = floatval($float);
8263
        for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8264
    }
8265
 
8266
    $result = number_format($float, $decimalpoints, $separator, '');
8267
    if ($stripzeros && $decimalpoints > 0) {
8268
        // Remove zeros and final dot if not needed.
8269
        // However, only do this if there is a decimal point!
8270
        $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8271
    }
8272
    return $result;
8273
}
8274
 
8275
/**
8276
 * Converts locale specific floating point/comma number back to standard PHP float value
8277
 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8278
 *
8279
 * @param string $localefloat locale aware float representation
8280
 * @param bool $strict If true, then check the input and return false if it is not a valid number.
8281
 * @return mixed float|bool - false or the parsed float.
8282
 */
1441 ariadna 8283
function unformat_float($localefloat, $strict = false) {
1 efrain 8284
    $localefloat = trim((string)$localefloat);
8285
 
8286
    if ($localefloat == '') {
8287
        return null;
8288
    }
8289
 
8290
    $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8291
    $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8292
 
8293
    if ($strict && !is_numeric($localefloat)) {
8294
        return false;
8295
    }
8296
 
8297
    return (float)$localefloat;
8298
}
8299
 
8300
/**
8301
 * Given a simple array, this shuffles it up just like shuffle()
8302
 * Unlike PHP's shuffle() this function works on any machine.
8303
 *
8304
 * @param array $array The array to be rearranged
8305
 * @return array
8306
 */
1441 ariadna 8307
function swapshuffle($array) {
1 efrain 8308
 
8309
    $last = count($array) - 1;
8310
    for ($i = 0; $i <= $last; $i++) {
8311
        $from = rand(0, $last);
8312
        $curr = $array[$i];
8313
        $array[$i] = $array[$from];
8314
        $array[$from] = $curr;
8315
    }
8316
    return $array;
8317
}
8318
 
8319
/**
8320
 * Like {@link swapshuffle()}, but works on associative arrays
8321
 *
8322
 * @param array $array The associative array to be rearranged
8323
 * @return array
8324
 */
1441 ariadna 8325
function swapshuffle_assoc($array) {
1 efrain 8326
 
8327
    $newarray = array();
8328
    $newkeys = swapshuffle(array_keys($array));
8329
 
8330
    foreach ($newkeys as $newkey) {
8331
        $newarray[$newkey] = $array[$newkey];
8332
    }
8333
    return $newarray;
8334
}
8335
 
8336
/**
8337
 * Given an arbitrary array, and a number of draws,
8338
 * this function returns an array with that amount
8339
 * of items.  The indexes are retained.
8340
 *
8341
 * @todo Finish documenting this function
8342
 *
8343
 * @param array $array
8344
 * @param int $draws
8345
 * @return array
8346
 */
1441 ariadna 8347
function draw_rand_array($array, $draws) {
1 efrain 8348
 
8349
    $return = array();
8350
 
8351
    $last = count($array);
8352
 
8353
    if ($draws > $last) {
8354
        $draws = $last;
8355
    }
8356
 
8357
    while ($draws > 0) {
8358
        $last--;
8359
 
8360
        $keys = array_keys($array);
8361
        $rand = rand(0, $last);
8362
 
8363
        $return[$keys[$rand]] = $array[$keys[$rand]];
8364
        unset($array[$keys[$rand]]);
8365
 
8366
        $draws--;
8367
    }
8368
 
8369
    return $return;
8370
}
8371
 
8372
/**
8373
 * Calculate the difference between two microtimes
8374
 *
8375
 * @param string $a The first Microtime
8376
 * @param string $b The second Microtime
8377
 * @return string
8378
 */
1441 ariadna 8379
function microtime_diff($a, $b) {
1 efrain 8380
    list($adec, $asec) = explode(' ', $a);
8381
    list($bdec, $bsec) = explode(' ', $b);
8382
    return $bsec - $asec + $bdec - $adec;
8383
}
8384
 
8385
/**
8386
 * Given a list (eg a,b,c,d,e) this function returns
8387
 * an array of 1->a, 2->b, 3->c etc
8388
 *
8389
 * @param string $list The string to explode into array bits
8390
 * @param string $separator The separator used within the list string
8391
 * @return array The now assembled array
8392
 */
1441 ariadna 8393
function make_menu_from_list($list, $separator=',') {
1 efrain 8394
 
8395
    $array = array_reverse(explode($separator, $list), true);
8396
    foreach ($array as $key => $item) {
1441 ariadna 8397
        $outarray[$key+1] = trim($item);
1 efrain 8398
    }
8399
    return $outarray;
8400
}
8401
 
8402
/**
8403
 * Creates an array that represents all the current grades that
8404
 * can be chosen using the given grading type.
8405
 *
8406
 * Negative numbers
8407
 * are scales, zero is no grade, and positive numbers are maximum
8408
 * grades.
8409
 *
8410
 * @todo Finish documenting this function or better deprecated this completely!
8411
 *
8412
 * @param int $gradingtype
8413
 * @return array
8414
 */
1441 ariadna 8415
function make_grades_menu($gradingtype) {
1 efrain 8416
    global $DB;
8417
 
8418
    $grades = array();
8419
    if ($gradingtype < 0) {
1441 ariadna 8420
        if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
1 efrain 8421
            return make_menu_from_list($scale->scale);
8422
        }
8423
    } else if ($gradingtype > 0) {
1441 ariadna 8424
        for ($i=$gradingtype; $i>=0; $i--) {
8425
            $grades[$i] = $i .' / '. $gradingtype;
1 efrain 8426
        }
8427
        return $grades;
8428
    }
8429
    return $grades;
8430
}
8431
 
8432
/**
8433
 * make_unique_id_code
8434
 *
8435
 * @todo Finish documenting this function
8436
 *
8437
 * @uses $_SERVER
8438
 * @param string $extra Extra string to append to the end of the code
8439
 * @return string
8440
 */
1441 ariadna 8441
function make_unique_id_code($extra = '') {
1 efrain 8442
 
8443
    $hostname = 'unknownhost';
8444
    if (!empty($_SERVER['HTTP_HOST'])) {
8445
        $hostname = $_SERVER['HTTP_HOST'];
8446
    } else if (!empty($_ENV['HTTP_HOST'])) {
8447
        $hostname = $_ENV['HTTP_HOST'];
8448
    } else if (!empty($_SERVER['SERVER_NAME'])) {
8449
        $hostname = $_SERVER['SERVER_NAME'];
8450
    } else if (!empty($_ENV['SERVER_NAME'])) {
8451
        $hostname = $_ENV['SERVER_NAME'];
8452
    }
8453
 
8454
    $date = gmdate("ymdHis");
8455
 
8456
    $random =  random_string(6);
8457
 
8458
    if ($extra) {
1441 ariadna 8459
        return $hostname .'+'. $date .'+'. $random .'+'. $extra;
1 efrain 8460
    } else {
1441 ariadna 8461
        return $hostname .'+'. $date .'+'. $random;
1 efrain 8462
    }
8463
}
8464
 
8465
 
8466
/**
8467
 * Function to check the passed address is within the passed subnet
8468
 *
8469
 * The parameter is a comma separated string of subnet definitions.
8470
 * Subnet strings can be in one of three formats:
8471
 *   1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn          (number of bits in net mask)
8472
 *   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)
8473
 *   3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.                  (incomplete address, a bit non-technical ;-)
8474
 * Code for type 1 modified from user posted comments by mediator at
8475
 * {@link http://au.php.net/manual/en/function.ip2long.php}
8476
 *
8477
 * @param string $addr    The address you are checking
8478
 * @param string $subnetstr    The string of subnet addresses
8479
 * @param bool $checkallzeros    The state to whether check for 0.0.0.0
8480
 * @return bool
8481
 */
1441 ariadna 8482
function address_in_subnet($addr, $subnetstr, $checkallzeros = false) {
1 efrain 8483
 
8484
    if ($addr == '0.0.0.0' && !$checkallzeros) {
8485
        return false;
8486
    }
8487
    $subnets = explode(',', $subnetstr);
8488
    $found = false;
8489
    $addr = trim($addr);
8490
    $addr = cleanremoteaddr($addr, false); // Normalise.
8491
    if ($addr === null) {
8492
        return false;
8493
    }
8494
    $addrparts = explode(':', $addr);
8495
 
8496
    $ipv6 = strpos($addr, ':');
8497
 
8498
    foreach ($subnets as $subnet) {
8499
        $subnet = trim($subnet);
8500
        if ($subnet === '') {
8501
            continue;
8502
        }
8503
 
8504
        if (strpos($subnet, '/') !== false) {
8505
            // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
8506
            list($ip, $mask) = explode('/', $subnet);
8507
            $mask = trim($mask);
8508
            if (!is_number($mask)) {
8509
                continue; // Incorect mask number, eh?
8510
            }
8511
            $ip = cleanremoteaddr($ip, false); // Normalise.
8512
            if ($ip === null) {
8513
                continue;
8514
            }
8515
            if (strpos($ip, ':') !== false) {
8516
                // IPv6.
8517
                if (!$ipv6) {
8518
                    continue;
8519
                }
8520
                if ($mask > 128 or $mask < 0) {
8521
                    continue; // Nonsense.
8522
                }
8523
                if ($mask == 0) {
8524
                    return true; // Any address.
8525
                }
8526
                if ($mask == 128) {
8527
                    if ($ip === $addr) {
8528
                        return true;
8529
                    }
8530
                    continue;
8531
                }
8532
                $ipparts = explode(':', $ip);
8533
                $modulo  = $mask % 16;
1441 ariadna 8534
                $ipnet   = array_slice($ipparts, 0, ($mask-$modulo)/16);
8535
                $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
1 efrain 8536
                if (implode(':', $ipnet) === implode(':', $addrnet)) {
8537
                    if ($modulo == 0) {
8538
                        return true;
8539
                    }
1441 ariadna 8540
                    $pos     = ($mask-$modulo)/16;
1 efrain 8541
                    $ipnet   = hexdec($ipparts[$pos]);
8542
                    $addrnet = hexdec($addrparts[$pos]);
8543
                    $mask    = 0xffff << (16 - $modulo);
8544
                    if (($addrnet & $mask) == ($ipnet & $mask)) {
8545
                        return true;
8546
                    }
8547
                }
1441 ariadna 8548
 
1 efrain 8549
            } else {
8550
                // IPv4.
8551
                if ($ipv6) {
8552
                    continue;
8553
                }
8554
                if ($mask > 32 or $mask < 0) {
8555
                    continue; // Nonsense.
8556
                }
8557
                if ($mask == 0) {
8558
                    return true;
8559
                }
8560
                if ($mask == 32) {
8561
                    if ($ip === $addr) {
8562
                        return true;
8563
                    }
8564
                    continue;
8565
                }
8566
                $mask = 0xffffffff << (32 - $mask);
8567
                if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
8568
                    return true;
8569
                }
8570
            }
1441 ariadna 8571
 
1 efrain 8572
        } else if (strpos($subnet, '-') !== false) {
8573
            // 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.
8574
            $parts = explode('-', $subnet);
8575
            if (count($parts) != 2) {
8576
                continue;
8577
            }
8578
 
8579
            if (strpos($subnet, ':') !== false) {
8580
                // IPv6.
8581
                if (!$ipv6) {
8582
                    continue;
8583
                }
8584
                $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8585
                if ($ipstart === null) {
8586
                    continue;
8587
                }
8588
                $ipparts = explode(':', $ipstart);
8589
                $start = hexdec(array_pop($ipparts));
8590
                $ipparts[] = trim($parts[1]);
8591
                $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
8592
                if ($ipend === null) {
8593
                    continue;
8594
                }
8595
                $ipparts[7] = '';
8596
                $ipnet = implode(':', $ipparts);
8597
                if (strpos($addr, $ipnet) !== 0) {
8598
                    continue;
8599
                }
8600
                $ipparts = explode(':', $ipend);
8601
                $end = hexdec($ipparts[7]);
8602
 
8603
                $addrend = hexdec($addrparts[7]);
8604
 
8605
                if (($addrend >= $start) and ($addrend <= $end)) {
8606
                    return true;
8607
                }
1441 ariadna 8608
 
1 efrain 8609
            } else {
8610
                // IPv4.
8611
                if ($ipv6) {
8612
                    continue;
8613
                }
8614
                $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8615
                if ($ipstart === null) {
8616
                    continue;
8617
                }
8618
                $ipparts = explode('.', $ipstart);
8619
                $ipparts[3] = trim($parts[1]);
8620
                $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
8621
                if ($ipend === null) {
8622
                    continue;
8623
                }
8624
 
8625
                if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
8626
                    return true;
8627
                }
8628
            }
1441 ariadna 8629
 
1 efrain 8630
        } else {
8631
            // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
8632
            if (strpos($subnet, ':') !== false) {
8633
                // IPv6.
8634
                if (!$ipv6) {
8635
                    continue;
8636
                }
8637
                $parts = explode(':', $subnet);
8638
                $count = count($parts);
1441 ariadna 8639
                if ($parts[$count-1] === '') {
8640
                    unset($parts[$count-1]); // Trim trailing :'s.
1 efrain 8641
                    $count--;
8642
                    $subnet = implode('.', $parts);
8643
                }
8644
                $isip = cleanremoteaddr($subnet, false); // Normalise.
8645
                if ($isip !== null) {
8646
                    if ($isip === $addr) {
8647
                        return true;
8648
                    }
8649
                    continue;
8650
                } else if ($count > 8) {
8651
                    continue;
8652
                }
1441 ariadna 8653
                $zeros = array_fill(0, 8-$count, '0');
8654
                $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
1 efrain 8655
                if (address_in_subnet($addr, $subnet)) {
8656
                    return true;
8657
                }
1441 ariadna 8658
 
1 efrain 8659
            } else {
8660
                // IPv4.
8661
                if ($ipv6) {
8662
                    continue;
8663
                }
8664
                $parts = explode('.', $subnet);
8665
                $count = count($parts);
1441 ariadna 8666
                if ($parts[$count-1] === '') {
8667
                    unset($parts[$count-1]); // Trim trailing .
1 efrain 8668
                    $count--;
8669
                    $subnet = implode('.', $parts);
8670
                }
8671
                if ($count == 4) {
8672
                    $subnet = cleanremoteaddr($subnet, false); // Normalise.
8673
                    if ($subnet === $addr) {
8674
                        return true;
8675
                    }
8676
                    continue;
8677
                } else if ($count > 4) {
8678
                    continue;
8679
                }
1441 ariadna 8680
                $zeros = array_fill(0, 4-$count, '0');
8681
                $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
1 efrain 8682
                if (address_in_subnet($addr, $subnet)) {
8683
                    return true;
8684
                }
8685
            }
8686
        }
8687
    }
8688
 
8689
    return false;
8690
}
8691
 
8692
/**
8693
 * For outputting debugging info
8694
 *
8695
 * @param string $string The string to write
8696
 * @param string $eol The end of line char(s) to use
8697
 * @param string $sleep Period to make the application sleep
8698
 *                      This ensures any messages have time to display before redirect
8699
 */
1441 ariadna 8700
function mtrace($string, $eol="\n", $sleep=0) {
1 efrain 8701
    global $CFG;
8702
 
8703
    if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
8704
        $fn = $CFG->mtrace_wrapper;
8705
        $fn($string, $eol);
8706
        return;
8707
    } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
8708
        // We must explicitly call the add_line function here.
8709
        // Uses of fwrite to STDOUT are not picked up by ob_start.
8710
        if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
8711
            fwrite(STDOUT, $output);
8712
        }
8713
    } else {
8714
        echo $string . $eol;
8715
    }
8716
 
8717
    // Flush again.
8718
    flush();
8719
 
8720
    // Delay to keep message on user's screen in case of subsequent redirect.
8721
    if ($sleep) {
8722
        sleep($sleep);
8723
    }
8724
}
8725
 
8726
/**
8727
 * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
8728
 *
8729
 * @param Throwable $e the error to ouptput.
8730
 */
1441 ariadna 8731
function mtrace_exception(Throwable $e): void {
1 efrain 8732
    $info = get_exception_info($e);
8733
 
8734
    $message = $info->message;
8735
    if ($info->debuginfo) {
8736
        $message .= "\n\n" . $info->debuginfo;
8737
    }
8738
    if ($info->backtrace) {
8739
        $message .= "\n\n" . format_backtrace($info->backtrace, true);
8740
    }
8741
 
8742
    mtrace($message);
8743
}
8744
 
8745
/**
8746
 * Replace 1 or more slashes or backslashes to 1 slash
8747
 *
8748
 * @param string $path The path to strip
8749
 * @return string the path with double slashes removed
8750
 */
1441 ariadna 8751
function cleardoubleslashes ($path) {
1 efrain 8752
    return preg_replace('/(\/|\\\){1,}/', '/', $path);
8753
}
8754
 
8755
/**
8756
 * Is the current ip in a given list?
8757
 *
8758
 * @param string $list
8759
 * @return bool
8760
 */
1441 ariadna 8761
function remoteip_in_list($list) {
1 efrain 8762
    $clientip = getremoteaddr(null);
8763
 
8764
    if (!$clientip) {
8765
        // Ensure access on cli.
8766
        return true;
8767
    }
8768
    return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
8769
}
8770
 
8771
/**
8772
 * Returns most reliable client address
8773
 *
8774
 * @param string $default If an address can't be determined, then return this
8775
 * @return string The remote IP address
8776
 */
1441 ariadna 8777
function getremoteaddr($default='0.0.0.0') {
1 efrain 8778
    global $CFG;
8779
 
8780
    if (!isset($CFG->getremoteaddrconf)) {
8781
        // This will happen, for example, before just after the upgrade, as the
8782
        // user is redirected to the admin screen.
8783
        $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
8784
    } else {
8785
        $variablestoskip = $CFG->getremoteaddrconf;
8786
    }
8787
    if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
8788
        if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
8789
            $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
8790
            return $address ? $address : $default;
8791
        }
8792
    }
8793
    if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
8794
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
8795
            $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
8796
 
1441 ariadna 8797
            $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
1 efrain 8798
                global $CFG;
8799
                return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
8800
            });
8801
 
8802
            // Multiple proxies can append values to this header including an
8803
            // untrusted original request header so we must only trust the last ip.
8804
            $address = end($forwardedaddresses);
8805
 
8806
            if (substr_count($address, ":") > 1) {
8807
                // Remove port and brackets from IPv6.
8808
                if (preg_match("/\[(.*)\]:/", $address, $matches)) {
8809
                    $address = $matches[1];
8810
                }
8811
            } else {
8812
                // Remove port from IPv4.
8813
                if (substr_count($address, ":") == 1) {
8814
                    $parts = explode(":", $address);
8815
                    $address = $parts[0];
8816
                }
8817
            }
8818
 
8819
            $address = cleanremoteaddr($address);
8820
            return $address ? $address : $default;
8821
        }
8822
    }
8823
    if (!empty($_SERVER['REMOTE_ADDR'])) {
8824
        $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
8825
        return $address ? $address : $default;
8826
    } else {
8827
        return $default;
8828
    }
8829
}
8830
 
8831
/**
8832
 * Cleans an ip address. Internal addresses are now allowed.
8833
 * (Originally local addresses were not allowed.)
8834
 *
8835
 * @param string $addr IPv4 or IPv6 address
8836
 * @param bool $compress use IPv6 address compression
8837
 * @return string normalised ip address string, null if error
8838
 */
1441 ariadna 8839
function cleanremoteaddr($addr, $compress=false) {
1 efrain 8840
    $addr = trim($addr);
8841
 
8842
    if (strpos($addr, ':') !== false) {
8843
        // Can be only IPv6.
8844
        $parts = explode(':', $addr);
8845
        $count = count($parts);
8846
 
1441 ariadna 8847
        if (strpos($parts[$count-1], '.') !== false) {
1 efrain 8848
            // Legacy ipv4 notation.
8849
            $last = array_pop($parts);
8850
            $ipv4 = cleanremoteaddr($last, true);
8851
            if ($ipv4 === null) {
8852
                return null;
8853
            }
8854
            $bits = explode('.', $ipv4);
1441 ariadna 8855
            $parts[] = dechex($bits[0]).dechex($bits[1]);
8856
            $parts[] = dechex($bits[2]).dechex($bits[3]);
1 efrain 8857
            $count = count($parts);
8858
            $addr = implode(':', $parts);
8859
        }
8860
 
8861
        if ($count < 3 or $count > 8) {
8862
            return null; // Severly malformed.
8863
        }
8864
 
8865
        if ($count != 8) {
8866
            if (strpos($addr, '::') === false) {
8867
                return null; // Malformed.
8868
            }
8869
            // Uncompress.
8870
            $insertat = array_search('', $parts, true);
8871
            $missing = array_fill(0, 1 + 8 - $count, '0');
8872
            array_splice($parts, $insertat, 1, $missing);
8873
            foreach ($parts as $key => $part) {
8874
                if ($part === '') {
8875
                    $parts[$key] = '0';
8876
                }
8877
            }
8878
        }
8879
 
8880
        $adr = implode(':', $parts);
8881
        if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
8882
            return null; // Incorrect format - sorry.
8883
        }
8884
 
8885
        // Normalise 0s and case.
8886
        $parts = array_map('hexdec', $parts);
8887
        $parts = array_map('dechex', $parts);
8888
 
8889
        $result = implode(':', $parts);
8890
 
8891
        if (!$compress) {
8892
            return $result;
8893
        }
8894
 
8895
        if ($result === '0:0:0:0:0:0:0:0') {
8896
            return '::'; // All addresses.
8897
        }
8898
 
8899
        $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
8900
        if ($compressed !== $result) {
8901
            return $compressed;
8902
        }
8903
 
8904
        $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
8905
        if ($compressed !== $result) {
8906
            return $compressed;
8907
        }
8908
 
8909
        $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
8910
        if ($compressed !== $result) {
8911
            return $compressed;
8912
        }
8913
 
8914
        return $result;
8915
    }
8916
 
8917
    // First get all things that look like IPv4 addresses.
8918
    $parts = array();
8919
    if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
8920
        return null;
8921
    }
8922
    unset($parts[0]);
8923
 
8924
    foreach ($parts as $key => $match) {
8925
        if ($match > 255) {
8926
            return null;
8927
        }
8928
        $parts[$key] = (int)$match; // Normalise 0s.
8929
    }
8930
 
8931
    return implode('.', $parts);
8932
}
8933
 
8934
 
8935
/**
8936
 * Is IP address a public address?
8937
 *
8938
 * @param string $ip The ip to check
8939
 * @return bool true if the ip is public
8940
 */
1441 ariadna 8941
function ip_is_public($ip) {
1 efrain 8942
    return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
8943
}
8944
 
8945
/**
8946
 * This function will make a complete copy of anything it's given,
8947
 * regardless of whether it's an object or not.
8948
 *
8949
 * @param mixed $thing Something you want cloned
8950
 * @return mixed What ever it is you passed it
8951
 */
1441 ariadna 8952
function fullclone($thing) {
1 efrain 8953
    return unserialize(serialize($thing));
8954
}
8955
 
8956
/**
8957
 * Used to make sure that $min <= $value <= $max
8958
 *
8959
 * Make sure that value is between min, and max
8960
 *
8961
 * @param int $min The minimum value
8962
 * @param int $value The value to check
8963
 * @param int $max The maximum value
8964
 * @return int
8965
 */
1441 ariadna 8966
function bounded_number($min, $value, $max) {
1 efrain 8967
    if ($value < $min) {
8968
        return $min;
8969
    }
8970
    if ($value > $max) {
8971
        return $max;
8972
    }
8973
    return $value;
8974
}
8975
 
8976
/**
8977
 * Check if there is a nested array within the passed array
8978
 *
8979
 * @param array $array
8980
 * @return bool true if there is a nested array false otherwise
8981
 */
1441 ariadna 8982
function array_is_nested($array) {
1 efrain 8983
    foreach ($array as $value) {
8984
        if (is_array($value)) {
8985
            return true;
8986
        }
8987
    }
8988
    return false;
8989
}
8990
 
8991
/**
8992
 * get_performance_info() pairs up with init_performance_info()
8993
 * loaded in setup.php. Returns an array with 'html' and 'txt'
8994
 * values ready for use, and each of the individual stats provided
8995
 * separately as well.
8996
 *
8997
 * @return array
8998
 */
1441 ariadna 8999
function get_performance_info() {
1 efrain 9000
    global $CFG, $PERF, $DB, $PAGE;
9001
 
9002
    $info = array();
9003
    $info['txt']  = me() . ' '; // Holds log-friendly representation.
9004
 
9005
    $info['html'] = '';
9006
    if (!empty($CFG->themedesignermode)) {
9007
        // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9008
        $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9009
    }
9010
    $info['html'] .= '<ul class="list-unstyled row mx-md-0">';         // Holds userfriendly HTML representation.
9011
 
9012
    $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9013
 
1441 ariadna 9014
    $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9015
    $info['txt'] .= 'time: '.$info['realtime'].'s ';
1 efrain 9016
 
9017
    // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9018
    $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9019
 
9020
    if (function_exists('memory_get_usage')) {
9021
        $info['memory_total'] = memory_get_usage();
9022
        $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
1441 ariadna 9023
        $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9024
        $info['txt']  .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9025
            $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
1 efrain 9026
    }
9027
 
9028
    if (function_exists('memory_get_peak_usage')) {
9029
        $info['memory_peak'] = memory_get_peak_usage();
1441 ariadna 9030
        $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9031
        $info['txt']  .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
1 efrain 9032
    }
9033
 
9034
    $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9035
    $inc = get_included_files();
9036
    $info['includecount'] = count($inc);
1441 ariadna 9037
    $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9038
    $info['txt']  .= 'includecount: '.$info['includecount'].' ';
1 efrain 9039
 
9040
    if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9041
        // We can not track more performance before installation or before PAGE init, sorry.
9042
        return $info;
9043
    }
9044
 
9045
    $filtermanager = filter_manager::instance();
9046
    if (method_exists($filtermanager, 'get_performance_summary')) {
9047
        list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9048
        $info = array_merge($filterinfo, $info);
9049
        foreach ($filterinfo as $key => $value) {
9050
            $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9051
            $info['txt'] .= "$key: $value ";
9052
        }
9053
    }
9054
 
9055
    $stringmanager = get_string_manager();
9056
    if (method_exists($stringmanager, 'get_performance_summary')) {
9057
        list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9058
        $info = array_merge($filterinfo, $info);
9059
        foreach ($filterinfo as $key => $value) {
9060
            $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9061
            $info['txt'] .= "$key: $value ";
9062
        }
9063
    }
9064
 
1441 ariadna 9065
    $info['dbqueries'] = $DB->perf_get_reads().'/'.$DB->perf_get_writes();
9066
    $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9067
    $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
1 efrain 9068
 
1441 ariadna 9069
    if ($DB->want_read_replica()) {
9070
        $info['dbreads_replica'] = $DB->perf_get_reads_replica();
9071
        $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from replica: '.$info['dbreads_replica'].'</li> ';
9072
        $info['txt'] .= 'db reads from replica: '.$info['dbreads_replica'].' ';
1 efrain 9073
    }
9074
 
9075
    $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
1441 ariadna 9076
    $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
1 efrain 9077
    $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9078
 
9079
    if (function_exists('posix_times')) {
9080
        $ptimes = posix_times();
9081
        if (is_array($ptimes)) {
9082
            foreach ($ptimes as $key => $val) {
9083
                $info[$key] = $ptimes[$key] -  $PERF->startposixtimes[$key];
9084
            }
9085
            $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9086
            $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9087
            $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9088
        }
9089
    }
9090
 
9091
    // Grab the load average for the last minute.
9092
    // /proc will only work under some linux configurations
9093
    // while uptime is there under MacOSX/Darwin and other unices.
9094
    if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9095
        list($serverload) = explode(' ', $loadavg[0]);
9096
        unset($loadavg);
1441 ariadna 9097
    } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
1 efrain 9098
        if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9099
            $serverload = $matches[1];
9100
        } else {
9101
            trigger_error('Could not parse uptime output!');
9102
        }
9103
    }
9104
    if (!empty($serverload)) {
9105
        $info['serverload'] = $serverload;
1441 ariadna 9106
        $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
1 efrain 9107
        $info['txt'] .= "serverload: {$info['serverload']} ";
9108
    }
9109
 
9110
    // Display size of session if session started.
9111
    if ($si = \core\session\manager::get_performance_info()) {
9112
        $info['sessionsize'] = $si['size'];
9113
        $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9114
        $info['txt'] .= $si['txt'];
9115
    }
9116
 
9117
    // Display time waiting for session if applicable.
9118
    if (!empty($PERF->sessionlock['wait'])) {
9119
        $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs';
9120
        $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [
9121
            'class' => 'sessionwait col-sm-4'
9122
        ]);
9123
        $info['txt'] .= 'sessionwait: ' . $sessionwait . ' ';
9124
    }
9125
 
9126
    $info['html'] .= '</ul>';
9127
    $html = '';
9128
    if ($stats = cache_helper::get_stats()) {
9129
 
9130
        $table = new html_table();
9131
        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9132
        $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9133
        $table->data = [];
9134
        $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9135
 
9136
        $text = 'Caches used (hits/misses/sets): ';
9137
        $hits = 0;
9138
        $misses = 0;
9139
        $sets = 0;
9140
        $maxstores = 0;
9141
 
9142
        // We want to align static caches into their own column.
9143
        $hasstatic = false;
9144
        foreach ($stats as $definition => $details) {
9145
            $numstores = count($details['stores']);
9146
            $first = key($details['stores']);
9147
            if ($first !== cache_store::STATIC_ACCEL) {
9148
                $numstores++; // Add a blank space for the missing static store.
9149
            }
9150
            $maxstores = max($maxstores, $numstores);
9151
        }
9152
 
9153
        $storec = 0;
9154
 
9155
        while ($storec++ < ($maxstores - 2)) {
9156
            if ($storec == ($maxstores - 2)) {
9157
                $table->head[] = get_string('mappingfinal', 'cache');
9158
            } else {
9159
                $table->head[] = "Store $storec";
9160
            }
9161
            $table->align[] = 'left';
9162
            $table->align[] = 'right';
9163
            $table->align[] = 'right';
9164
            $table->align[] = 'right';
9165
            $table->align[] = 'right';
9166
            $table->head[] = 'H';
9167
            $table->head[] = 'M';
9168
            $table->head[] = 'S';
9169
            $table->head[] = 'I/O';
9170
        }
9171
 
9172
        ksort($stats);
9173
 
9174
        foreach ($stats as $definition => $details) {
9175
            switch ($details['mode']) {
9176
                case cache_store::MODE_APPLICATION:
9177
                    $modeclass = 'application';
9178
                    $mode = ' <span title="application cache">App</span>';
9179
                    break;
9180
                case cache_store::MODE_SESSION:
9181
                    $modeclass = 'session';
9182
                    $mode = ' <span title="session cache">Ses</span>';
9183
                    break;
9184
                case cache_store::MODE_REQUEST:
9185
                    $modeclass = 'request';
9186
                    $mode = ' <span title="request cache">Req</span>';
9187
                    break;
9188
            }
9189
            $row = [$mode, $definition];
9190
 
9191
            $text .= "$definition {";
9192
 
9193
            $storec = 0;
9194
            foreach ($details['stores'] as $store => $data) {
9195
 
9196
                if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9197
                    $row[] = '';
9198
                    $row[] = '';
9199
                    $row[] = '';
9200
                    $storec++;
9201
                }
9202
 
9203
                $hits   += $data['hits'];
9204
                $misses += $data['misses'];
9205
                $sets   += $data['sets'];
9206
                if ($data['hits'] == 0 and $data['misses'] > 0) {
9207
                    $cachestoreclass = 'nohits bg-danger';
9208
                } else if ($data['hits'] < $data['misses']) {
9209
                    $cachestoreclass = 'lowhits bg-warning text-dark';
9210
                } else {
9211
                    $cachestoreclass = 'hihits';
9212
                }
9213
                $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9214
                $cell = new html_table_cell($store);
9215
                $cell->attributes = ['class' => $cachestoreclass];
9216
                $row[] = $cell;
9217
                $cell = new html_table_cell($data['hits']);
9218
                $cell->attributes = ['class' => $cachestoreclass];
9219
                $row[] = $cell;
9220
                $cell = new html_table_cell($data['misses']);
9221
                $cell->attributes = ['class' => $cachestoreclass];
9222
                $row[] = $cell;
9223
 
9224
                if ($store !== cache_store::STATIC_ACCEL) {
9225
                    // The static cache is never set.
9226
                    $cell = new html_table_cell($data['sets']);
9227
                    $cell->attributes = ['class' => $cachestoreclass];
9228
                    $row[] = $cell;
9229
 
9230
                    if ($data['hits'] || $data['sets']) {
9231
                        if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
9232
                            $size = '-';
9233
                        } else {
9234
                            $size = display_size($data['iobytes'], 1, 'KB');
9235
                            if ($data['iobytes'] >= 10 * 1024) {
9236
                                $cachestoreclass = ' bg-warning text-dark';
9237
                            }
9238
                        }
9239
                    } else {
9240
                        $size = '';
9241
                    }
9242
                    $cell = new html_table_cell($size);
9243
                    $cell->attributes = ['class' => $cachestoreclass];
9244
                    $row[] = $cell;
9245
                }
9246
                $storec++;
9247
            }
9248
            while ($storec++ < $maxstores) {
9249
                $row[] = '';
9250
                $row[] = '';
9251
                $row[] = '';
9252
                $row[] = '';
9253
                $row[] = '';
9254
            }
9255
            $text .= '} ';
9256
 
9257
            $table->data[] = $row;
9258
        }
9259
 
9260
        $html .= html_writer::table($table);
9261
 
9262
        // Now lets also show sub totals for each cache store.
9263
        $storetotals = [];
9264
        $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9265
        foreach ($stats as $definition => $details) {
9266
            foreach ($details['stores'] as $store => $data) {
9267
                if (!array_key_exists($store, $storetotals)) {
9268
                    $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9269
                }
9270
                $storetotals[$store]['class']   = $data['class'];
9271
                $storetotals[$store]['hits']   += $data['hits'];
9272
                $storetotals[$store]['misses'] += $data['misses'];
9273
                $storetotals[$store]['sets']   += $data['sets'];
9274
                $storetotal['hits']   += $data['hits'];
9275
                $storetotal['misses'] += $data['misses'];
9276
                $storetotal['sets']   += $data['sets'];
9277
                if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) {
9278
                    $storetotals[$store]['iobytes'] += $data['iobytes'];
9279
                    $storetotal['iobytes'] += $data['iobytes'];
9280
                }
9281
            }
9282
        }
9283
 
9284
        $table = new html_table();
9285
        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9286
        $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9287
        $table->data = [];
9288
        $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
9289
 
9290
        ksort($storetotals);
9291
 
9292
        foreach ($storetotals as $store => $data) {
9293
            $row = [];
9294
            if ($data['hits'] == 0 and $data['misses'] > 0) {
9295
                $cachestoreclass = 'nohits bg-danger';
9296
            } else if ($data['hits'] < $data['misses']) {
9297
                $cachestoreclass = 'lowhits bg-warning text-dark';
9298
            } else {
9299
                $cachestoreclass = 'hihits';
9300
            }
9301
            $cell = new html_table_cell($store);
9302
            $cell->attributes = ['class' => $cachestoreclass];
9303
            $row[] = $cell;
9304
            $cell = new html_table_cell($data['class']);
9305
            $cell->attributes = ['class' => $cachestoreclass];
9306
            $row[] = $cell;
9307
            $cell = new html_table_cell($data['hits']);
9308
            $cell->attributes = ['class' => $cachestoreclass];
9309
            $row[] = $cell;
9310
            $cell = new html_table_cell($data['misses']);
9311
            $cell->attributes = ['class' => $cachestoreclass];
9312
            $row[] = $cell;
9313
            $cell = new html_table_cell($data['sets']);
9314
            $cell->attributes = ['class' => $cachestoreclass];
9315
            $row[] = $cell;
9316
            if ($data['hits'] || $data['sets']) {
9317
                if ($data['iobytes']) {
9318
                    $size = display_size($data['iobytes'], 1, 'KB');
9319
                } else {
9320
                    $size = '-';
9321
                }
9322
            } else {
9323
                $size = '';
9324
            }
9325
            $cell = new html_table_cell($size);
9326
            $cell->attributes = ['class' => $cachestoreclass];
9327
            $row[] = $cell;
9328
            $table->data[] = $row;
9329
        }
9330
        if (!empty($storetotal['iobytes'])) {
9331
            $size = display_size($storetotal['iobytes'], 1, 'KB');
9332
        } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) {
9333
            $size = '-';
9334
        } else {
9335
            $size = '';
9336
        }
9337
        $row = [
9338
            get_string('total'),
9339
            '',
9340
            $storetotal['hits'],
9341
            $storetotal['misses'],
9342
            $storetotal['sets'],
9343
            $size,
9344
        ];
9345
        $table->data[] = $row;
9346
 
9347
        $html .= html_writer::table($table);
9348
 
9349
        $info['cachesused'] = "$hits / $misses / $sets";
9350
        $info['html'] .= $html;
1441 ariadna 9351
        $info['txt'] .= $text.'. ';
1 efrain 9352
    } else {
9353
        $info['cachesused'] = '0 / 0 / 0';
9354
        $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9355
        $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9356
    }
9357
 
9358
    // Display lock information if any.
9359
    if (!empty($PERF->locks)) {
9360
        $table = new html_table();
9361
        $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered';
9362
        $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)'];
9363
        $table->align = ['left', 'right', 'center', 'right'];
9364
        $table->data = [];
9365
        $text = 'Locks (waited/obtained/held):';
9366
        foreach ($PERF->locks as $locktiming) {
9367
            $row = [];
9368
            $row[] = s($locktiming->type . '/' . $locktiming->resource);
9369
            $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' (';
9370
 
9371
            // The time we had to wait to get the lock.
9372
            $roundedtime = number_format($locktiming->wait, 1);
9373
            $cell = new html_table_cell($roundedtime);
9374
            if ($locktiming->wait > 0.5) {
9375
                $cell->attributes = ['class' => 'bg-warning text-dark'];
9376
            }
9377
            $row[] = $cell;
9378
            $text .= $roundedtime . '/';
9379
 
9380
            // Show a tick or cross for success.
9381
            $row[] = $locktiming->success ? '&#x2713;' : '&#x274c;';
9382
            $text .= ($locktiming->success ? 'y' : 'n') . '/';
9383
 
9384
            // If applicable, show how long we held the lock before releasing it.
9385
            if (property_exists($locktiming, 'held')) {
9386
                $roundedtime = number_format($locktiming->held, 1);
9387
                $cell = new html_table_cell($roundedtime);
9388
                if ($locktiming->held > 0.5) {
9389
                    $cell->attributes = ['class' => 'bg-warning text-dark'];
9390
                }
9391
                $row[] = $cell;
9392
                $text .= $roundedtime;
9393
            } else {
9394
                $row[] = '-';
9395
                $text .= '-';
9396
            }
9397
            $text .= ')';
9398
 
9399
            $table->data[] = $row;
9400
        }
9401
        $info['html'] .= html_writer::table($table);
9402
        $info['txt'] .= $text . '. ';
9403
    }
9404
 
1441 ariadna 9405
    $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>';
1 efrain 9406
    return $info;
9407
}
9408
 
9409
/**
9410
 * Renames a file or directory to a unique name within the same directory.
9411
 *
9412
 * This function is designed to avoid any potential race conditions, and select an unused name.
9413
 *
9414
 * @param string $filepath Original filepath
9415
 * @param string $prefix Prefix to use for the temporary name
9416
 * @return string|bool New file path or false if failed
9417
 * @since Moodle 3.10
9418
 */
1441 ariadna 9419
function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
1 efrain 9420
    $dir = dirname($filepath);
9421
    $basename = $dir . '/' . $prefix;
9422
    $limit = 0;
9423
    while ($limit < 100) {
9424
        // Select a new name based on a random number.
9425
        $newfilepath = $basename . md5(mt_rand());
9426
 
9427
        // Attempt a rename to that new name.
9428
        if (@rename($filepath, $newfilepath)) {
9429
            return $newfilepath;
9430
        }
9431
 
9432
        // The first time, do some sanity checks, maybe it is failing for a good reason and there
9433
        // is no point trying 100 times if so.
9434
        if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
9435
            return false;
9436
        }
9437
        $limit++;
9438
    }
9439
    return false;
9440
}
9441
 
9442
/**
9443
 * Delete directory or only its content
9444
 *
9445
 * @param string $dir directory path
9446
 * @param bool $contentonly
9447
 * @return bool success, true also if dir does not exist
9448
 */
1441 ariadna 9449
function remove_dir($dir, $contentonly=false) {
1 efrain 9450
    if (!is_dir($dir)) {
9451
        // Nothing to do.
9452
        return true;
9453
    }
9454
 
9455
    if (!$contentonly) {
9456
        // Start by renaming the directory; this will guarantee that other processes don't write to it
9457
        // while it is in the process of being deleted.
9458
        $tempdir = rename_to_unused_name($dir);
9459
        if ($tempdir) {
9460
            // If the rename was successful then delete the $tempdir instead.
9461
            $dir = $tempdir;
9462
        }
9463
        // If the rename fails, we will continue through and attempt to delete the directory
9464
        // without renaming it since that is likely to at least delete most of the files.
9465
    }
9466
 
9467
    if (!$handle = opendir($dir)) {
9468
        return false;
9469
    }
9470
    $result = true;
1441 ariadna 9471
    while (false!==($item = readdir($handle))) {
1 efrain 9472
        if ($item != '.' && $item != '..') {
1441 ariadna 9473
            if (is_dir($dir.'/'.$item)) {
9474
                $result = remove_dir($dir.'/'.$item) && $result;
1 efrain 9475
            } else {
1441 ariadna 9476
                $result = unlink($dir.'/'.$item) && $result;
1 efrain 9477
            }
9478
        }
9479
    }
9480
    closedir($handle);
9481
    if ($contentonly) {
9482
        clearstatcache(); // Make sure file stat cache is properly invalidated.
9483
        return $result;
9484
    }
9485
    $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9486
    clearstatcache(); // Make sure file stat cache is properly invalidated.
9487
    return $result;
9488
}
9489
 
9490
/**
9491
 * Detect if an object or a class contains a given property
9492
 * will take an actual object or the name of a class
9493
 *
9494
 * @param mixed $obj Name of class or real object to test
9495
 * @param string $property name of property to find
9496
 * @return bool true if property exists
9497
 */
1441 ariadna 9498
function object_property_exists( $obj, $property ) {
9499
    if (is_string( $obj )) {
9500
        $properties = get_class_vars( $obj );
1 efrain 9501
    } else {
1441 ariadna 9502
        $properties = get_object_vars( $obj );
1 efrain 9503
    }
1441 ariadna 9504
    return array_key_exists( $property, $properties );
1 efrain 9505
}
9506
 
9507
/**
9508
 * Converts an object into an associative array
9509
 *
9510
 * This function converts an object into an associative array by iterating
9511
 * over its public properties. Because this function uses the foreach
9512
 * construct, Iterators are respected. It works recursively on arrays of objects.
9513
 * Arrays and simple values are returned as is.
9514
 *
9515
 * If class has magic properties, it can implement IteratorAggregate
9516
 * and return all available properties in getIterator()
9517
 *
9518
 * @param mixed $var
9519
 * @return array
9520
 */
1441 ariadna 9521
function convert_to_array($var) {
1 efrain 9522
    $result = array();
9523
 
9524
    // Loop over elements/properties.
9525
    foreach ($var as $key => $value) {
9526
        // Recursively convert objects.
9527
        if (is_object($value) || is_array($value)) {
9528
            $result[$key] = convert_to_array($value);
9529
        } else {
9530
            // Simple values are untouched.
9531
            $result[$key] = $value;
9532
        }
9533
    }
9534
    return $result;
9535
}
9536
 
9537
/**
9538
 * Detect a custom script replacement in the data directory that will
9539
 * replace an existing moodle script
9540
 *
9541
 * @return string|bool full path name if a custom script exists, false if no custom script exists
9542
 */
1441 ariadna 9543
function custom_script_path() {
1 efrain 9544
    global $CFG, $SCRIPT;
9545
 
9546
    if ($SCRIPT === null) {
9547
        // Probably some weird external script.
9548
        return false;
9549
    }
9550
 
9551
    $scriptpath = $CFG->customscripts . $SCRIPT;
9552
 
9553
    // Check the custom script exists.
9554
    if (file_exists($scriptpath) and is_file($scriptpath)) {
9555
        return $scriptpath;
9556
    } else {
9557
        return false;
9558
    }
9559
}
9560
 
9561
/**
9562
 * Returns whether or not the user object is a remote MNET user. This function
9563
 * is in moodlelib because it does not rely on loading any of the MNET code.
9564
 *
9565
 * @param object $user A valid user object
9566
 * @return bool        True if the user is from a remote Moodle.
9567
 */
1441 ariadna 9568
function is_mnet_remote_user($user) {
1 efrain 9569
    global $CFG;
9570
 
9571
    if (!isset($CFG->mnet_localhost_id)) {
9572
        include_once($CFG->dirroot . '/mnet/lib.php');
9573
        $env = new mnet_environment();
9574
        $env->init();
9575
        unset($env);
9576
    }
9577
 
9578
    return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
9579
}
9580
 
9581
/**
9582
 * This function will search for browser prefereed languages, setting Moodle
9583
 * to use the best one available if $SESSION->lang is undefined
9584
 */
1441 ariadna 9585
function setup_lang_from_browser() {
1 efrain 9586
    global $CFG, $SESSION, $USER;
9587
 
9588
    if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
9589
        // Lang is defined in session or user profile, nothing to do.
9590
        return;
9591
    }
9592
 
9593
    if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
9594
        return;
9595
    }
9596
 
9597
    // Extract and clean langs from headers.
9598
    $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
9599
    $rawlangs = str_replace('-', '_', $rawlangs);         // We are using underscores.
9600
    $rawlangs = explode(',', $rawlangs);                  // Convert to array.
9601
    $langs = array();
9602
 
9603
    $order = 1.0;
9604
    foreach ($rawlangs as $lang) {
9605
        if (strpos($lang, ';') === false) {
9606
            $langs[(string)$order] = $lang;
1441 ariadna 9607
            $order = $order-0.01;
1 efrain 9608
        } else {
9609
            $parts = explode(';', $lang);
9610
            $pos = strpos($parts[1], '=');
1441 ariadna 9611
            $langs[substr($parts[1], $pos+1)] = $parts[0];
1 efrain 9612
        }
9613
    }
9614
    krsort($langs, SORT_NUMERIC);
9615
 
9616
    // Look for such langs under standard locations.
9617
    foreach ($langs as $lang) {
9618
        // Clean it properly for include.
9619
        $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
9620
        if (get_string_manager()->translation_exists($lang, false)) {
9621
            // If the translation for this language exists then try to set it
9622
            // for the rest of the session, if this is a read only session then
9623
            // we can only set it temporarily in $CFG.
9624
            if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) {
9625
                $CFG->lang = $lang;
9626
            } else {
9627
                $SESSION->lang = $lang;
9628
            }
9629
            // We have finished. Go out.
9630
            break;
9631
        }
9632
    }
9633
    return;
9634
}
9635
 
9636
/**
9637
 * Check if $url matches anything in proxybypass list
9638
 *
9639
 * Any errors just result in the proxy being used (least bad)
9640
 *
9641
 * @param string $url url to check
9642
 * @return boolean true if we should bypass the proxy
9643
 */
1441 ariadna 9644
function is_proxybypass( $url ) {
1 efrain 9645
    global $CFG;
9646
 
9647
    // Sanity check.
9648
    if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
9649
        return false;
9650
    }
9651
 
9652
    // Get the host part out of the url.
1441 ariadna 9653
    if (!$host = parse_url( $url, PHP_URL_HOST )) {
1 efrain 9654
        return false;
9655
    }
9656
 
9657
    // Get the possible bypass hosts into an array.
1441 ariadna 9658
    $matches = explode( ',', $CFG->proxybypass );
1 efrain 9659
 
9660
    // Check for a exact match on the IP or in the domains.
9661
    $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches);
9662
    $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ',');
9663
 
9664
    if ($isdomaininallowedlist || $isipinsubnetlist) {
9665
        return true;
9666
    }
9667
 
9668
    // Nothing matched.
9669
    return false;
9670
}
9671
 
9672
/**
9673
 * Check if the passed navigation is of the new style
9674
 *
9675
 * @param mixed $navigation
9676
 * @return bool true for yes false for no
9677
 */
1441 ariadna 9678
function is_newnav($navigation) {
1 efrain 9679
    if (is_array($navigation) && !empty($navigation['newnav'])) {
9680
        return true;
9681
    } else {
9682
        return false;
9683
    }
9684
}
9685
 
9686
/**
9687
 * Checks whether the given variable name is defined as a variable within the given object.
9688
 *
9689
 * This will NOT work with stdClass objects, which have no class variables.
9690
 *
9691
 * @param string $var The variable name
9692
 * @param object $object The object to check
9693
 * @return boolean
9694
 */
1441 ariadna 9695
function in_object_vars($var, $object) {
1 efrain 9696
    $classvars = get_class_vars(get_class($object));
9697
    $classvars = array_keys($classvars);
9698
    return in_array($var, $classvars);
9699
}
9700
 
9701
/**
9702
 * Returns an array without repeated objects.
9703
 * This function is similar to array_unique, but for arrays that have objects as values
9704
 *
9705
 * @param array $array
9706
 * @param bool $keepkeyassoc
9707
 * @return array
9708
 */
1441 ariadna 9709
function object_array_unique($array, $keepkeyassoc = true) {
1 efrain 9710
    $duplicatekeys = array();
9711
    $tmp         = array();
9712
 
9713
    foreach ($array as $key => $val) {
9714
        // Convert objects to arrays, in_array() does not support objects.
9715
        if (is_object($val)) {
9716
            $val = (array)$val;
9717
        }
9718
 
9719
        if (!in_array($val, $tmp)) {
9720
            $tmp[] = $val;
9721
        } else {
9722
            $duplicatekeys[] = $key;
9723
        }
9724
    }
9725
 
9726
    foreach ($duplicatekeys as $key) {
9727
        unset($array[$key]);
9728
    }
9729
 
9730
    return $keepkeyassoc ? $array : array_values($array);
9731
}
9732
 
9733
/**
9734
 * Is a userid the primary administrator?
9735
 *
9736
 * @param int $userid int id of user to check
9737
 * @return boolean
9738
 */
1441 ariadna 9739
function is_primary_admin($userid) {
1 efrain 9740
    $primaryadmin =  get_admin();
9741
 
9742
    if ($userid == $primaryadmin->id) {
9743
        return true;
9744
    } else {
9745
        return false;
9746
    }
9747
}
9748
 
9749
/**
9750
 * Returns the site identifier
9751
 *
9752
 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
9753
 */
1441 ariadna 9754
function get_site_identifier() {
1 efrain 9755
    global $CFG;
9756
    // Check to see if it is missing. If so, initialise it.
9757
    if (empty($CFG->siteidentifier)) {
9758
        set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
9759
    }
9760
    // Return it.
9761
    return $CFG->siteidentifier;
9762
}
9763
 
9764
/**
9765
 * Check whether the given password has no more than the specified
9766
 * number of consecutive identical characters.
9767
 *
9768
 * @param string $password   password to be checked against the password policy
9769
 * @param integer $maxchars  maximum number of consecutive identical characters
9770
 * @return bool
9771
 */
1441 ariadna 9772
function check_consecutive_identical_characters($password, $maxchars) {
1 efrain 9773
 
9774
    if ($maxchars < 1) {
9775
        return true; // Zero 0 is to disable this check.
9776
    }
9777
    if (strlen($password) <= $maxchars) {
9778
        return true; // Too short to fail this test.
9779
    }
9780
 
9781
    $previouschar = '';
9782
    $consecutivecount = 1;
9783
    foreach (str_split($password) as $char) {
9784
        if ($char != $previouschar) {
9785
            $consecutivecount = 1;
9786
        } else {
9787
            $consecutivecount++;
9788
            if ($consecutivecount > $maxchars) {
9789
                return false; // Check failed already.
9790
            }
9791
        }
9792
 
9793
        $previouschar = $char;
9794
    }
9795
 
9796
    return true;
9797
}
9798
 
9799
/**
9800
 * Helper function to do partial function binding.
9801
 *
1441 ariadna 9802
 * This is useful for cases such as preg_replace_callback where you may want to partially bind values.
9803
 *
9804
 * The use of named arguments is recommended for clarity.
9805
 * Please note that providing arguments in a different order may have mixed results for built-in functions.
9806
 *
1 efrain 9807
 * $callback = partial('somefunction', $arg1, $arg2);
9808
 *     or
9809
 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
9810
 *     or even
9811
 * $obj = new someclass();
9812
 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
9813
 *
9814
 * and then the arguments that are passed through at calltime are appended to the argument list.
9815
 *
1441 ariadna 9816
 * @param callable $function a php callback
9817
 * @param mixed ...$initialargs The arguments to provide for the initial bind
9818
 * @return callable
1 efrain 9819
 */
1441 ariadna 9820
function partial(callable $callable, ...$initialargs): callable {
9821
    return fn (...$args) => $callable(...$initialargs, ...$args);
1 efrain 9822
}
9823
 
9824
/**
9825
 * helper function to load up and initialise the mnet environment
9826
 * this must be called before you use mnet functions.
9827
 *
9828
 * @return mnet_environment the equivalent of old $MNET global
9829
 */
1441 ariadna 9830
function get_mnet_environment() {
1 efrain 9831
    global $CFG;
9832
    require_once($CFG->dirroot . '/mnet/lib.php');
9833
    static $instance = null;
9834
    if (empty($instance)) {
9835
        $instance = new mnet_environment();
9836
        $instance->init();
9837
    }
9838
    return $instance;
9839
}
9840
 
9841
/**
9842
 * during xmlrpc server code execution, any code wishing to access
9843
 * information about the remote peer must use this to get it.
9844
 *
9845
 * @return mnet_remote_client|false the equivalent of old $MNETREMOTE_CLIENT global
9846
 */
1441 ariadna 9847
function get_mnet_remote_client() {
1 efrain 9848
    if (!defined('MNET_SERVER')) {
9849
        debugging(get_string('notinxmlrpcserver', 'mnet'));
9850
        return false;
9851
    }
9852
    global $MNET_REMOTE_CLIENT;
9853
    if (isset($MNET_REMOTE_CLIENT)) {
9854
        return $MNET_REMOTE_CLIENT;
9855
    }
9856
    return false;
9857
}
9858
 
9859
/**
9860
 * during the xmlrpc server code execution, this will be called
9861
 * to setup the object returned by {@link get_mnet_remote_client}
9862
 *
9863
 * @param mnet_remote_client $client the client to set up
9864
 * @throws moodle_exception
9865
 */
1441 ariadna 9866
function set_mnet_remote_client($client) {
1 efrain 9867
    if (!defined('MNET_SERVER')) {
9868
        throw new moodle_exception('notinxmlrpcserver', 'mnet');
9869
    }
9870
    global $MNET_REMOTE_CLIENT;
9871
    $MNET_REMOTE_CLIENT = $client;
9872
}
9873
 
9874
/**
9875
 * return the jump url for a given remote user
9876
 * this is used for rewriting forum post links in emails, etc
9877
 *
9878
 * @param stdclass $user the user to get the idp url for
9879
 */
1441 ariadna 9880
function mnet_get_idp_jump_url($user) {
1 efrain 9881
    global $CFG;
9882
 
9883
    static $mnetjumps = array();
9884
    if (!array_key_exists($user->mnethostid, $mnetjumps)) {
9885
        $idp = mnet_get_peer_host($user->mnethostid);
9886
        $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
9887
        $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
9888
    }
9889
    return $mnetjumps[$user->mnethostid];
9890
}
9891
 
9892
/**
9893
 * Gets the homepage to use for the current user
9894
 *
9895
 * @return int One of HOMEPAGE_*
9896
 */
1441 ariadna 9897
function get_home_page() {
1 efrain 9898
    global $CFG;
9899
 
1441 ariadna 9900
    if (isloggedin() && !empty($CFG->defaulthomepage)) {
1 efrain 9901
        // If dashboard is disabled, home will be set to default page.
9902
        $defaultpage = get_default_home_page();
1441 ariadna 9903
        if ($CFG->defaulthomepage == HOMEPAGE_MY && (!isguestuser() || !empty($CFG->allowguestmymoodle))) {
1 efrain 9904
            if (!empty($CFG->enabledashboard)) {
9905
                return HOMEPAGE_MY;
9906
            } else {
9907
                return $defaultpage;
9908
            }
1441 ariadna 9909
        } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES && !isguestuser()) {
1 efrain 9910
            return HOMEPAGE_MYCOURSES;
1441 ariadna 9911
        } else if ($CFG->defaulthomepage == HOMEPAGE_USER && !isguestuser()) {
9912
            $userhomepage = get_user_preferences('user_home_page_preference', $defaultpage);
1 efrain 9913
            if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) {
9914
                // If the user was using the dashboard but it's disabled, return the default home page.
9915
                $userhomepage = $defaultpage;
1441 ariadna 9916
            } else if (get_default_home_page_url()) {
9917
                return HOMEPAGE_URL;
1 efrain 9918
            }
1441 ariadna 9919
            return (int) $userhomepage;
9920
        } else if (get_default_home_page_url()) {
9921
            return HOMEPAGE_URL;
1 efrain 9922
        }
9923
    }
9924
    return HOMEPAGE_SITE;
9925
}
9926
 
9927
/**
9928
 * Returns the default home page to display if current one is not defined or can't be applied.
9929
 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
9930
 *
9931
 * @return int The default home page.
9932
 */
1441 ariadna 9933
function get_default_home_page(): int {
1 efrain 9934
    global $CFG;
9935
 
9936
    return (!isset($CFG->enabledashboard) || $CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES;
9937
}
9938
 
9939
/**
1441 ariadna 9940
 * Get the default home page as a URL where it has been configured as one via site configuration or user preference
9941
 *
9942
 * It is assumed that callers have already checked that {@see get_home_page} returns {@see HOMEPAGE_URL} prior to
9943
 * calling this method
9944
 *
9945
 * @return \core\url|null
9946
 */
9947
function get_default_home_page_url(): ?\core\url {
9948
    global $CFG;
9949
 
9950
    if (substr((string)$CFG->defaulthomepage, 0, 1) === '/' &&
9951
            ($defaulthomepage = clean_param($CFG->wwwroot . $CFG->defaulthomepage, PARAM_LOCALURL))) {
9952
        return new \core\url($defaulthomepage);
9953
    }
9954
 
9955
    if ($CFG->defaulthomepage == HOMEPAGE_USER) {
9956
        $userhomepage = get_user_preferences('user_home_page_preference');
9957
        if (substr((string)$userhomepage, 0, 1) === '/' &&
9958
                ($userhomepage = clean_param($CFG->wwwroot . $userhomepage, PARAM_LOCALURL))) {
9959
            return new \core\url($userhomepage);
9960
        }
9961
    }
9962
 
9963
    return null;
9964
}
9965
 
9966
/**
1 efrain 9967
 * Gets the name of a course to be displayed when showing a list of courses.
9968
 * By default this is just $course->fullname but user can configure it. The
9969
 * result of this function should be passed through print_string.
9970
 * @param stdClass|core_course_list_element $course Moodle course object
9971
 * @return string Display name of course (either fullname or short + fullname)
9972
 */
1441 ariadna 9973
function get_course_display_name_for_list($course) {
1 efrain 9974
    global $CFG;
9975
    if (!empty($CFG->courselistshortnames)) {
9976
        if (!($course instanceof stdClass)) {
9977
            $course = (object)convert_to_array($course);
9978
        }
9979
        return get_string('courseextendednamedisplay', '', $course);
9980
    } else {
9981
        return $course->fullname;
9982
    }
9983
}
9984
 
9985
/**
9986
 * Safe analogue of unserialize() that can only parse arrays
9987
 *
9988
 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
9989
 *
9990
 * @param string $expression
9991
 * @return array|bool either parsed array or false if parsing was impossible.
9992
 */
1441 ariadna 9993
function unserialize_array($expression) {
1 efrain 9994
 
9995
    // Check the expression is an array.
9996
    if (!preg_match('/^a:(\d+):/', $expression)) {
9997
        return false;
9998
    }
9999
 
10000
    $values = (array) unserialize_object($expression);
10001
 
10002
    // Callback that returns true if the given value is an unserialized object, executes recursively.
1441 ariadna 10003
    $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool {
1 efrain 10004
        if (is_array($value)) {
10005
            return (bool) array_filter($value, $invalidvaluecallback);
10006
        }
10007
        return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class);
10008
    };
10009
 
10010
    // Iterate over the result to ensure there are no stray objects.
10011
    if (array_filter($values, $invalidvaluecallback)) {
10012
        return false;
10013
    }
10014
 
10015
    return $values;
10016
}
10017
 
10018
/**
10019
 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10020
 *
10021
 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10022
 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10023
 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10024
 *
10025
 * @param string $input
10026
 * @return stdClass
10027
 */
1441 ariadna 10028
function unserialize_object(string $input): stdClass {
1 efrain 10029
    $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10030
    return (object) $instance;
10031
}
10032
 
10033
/**
10034
 * Get human readable name describing the given callable.
10035
 *
10036
 * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10037
 * It does not check if the callable actually exists.
10038
 *
10039
 * @param callable|string|array $callable
10040
 * @return string|bool Human readable name of callable, or false if not a valid callable.
10041
 */
1441 ariadna 10042
function get_callable_name($callable) {
1 efrain 10043
 
10044
    if (!is_callable($callable, true, $name)) {
10045
        return false;
1441 ariadna 10046
 
1 efrain 10047
    } else {
10048
        return $name;
10049
    }
10050
}
10051
 
10052
/**
10053
 * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10054
 * Never put your faith on this function and rely on its accuracy as there might be false positives.
10055
 * It just performs some simple checks, and mainly is used for places where we want to hide some options
10056
 * such as site registration when $CFG->wwwroot is not publicly accessible.
10057
 * Good thing is there is no false negative.
10058
 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10059
 *
10060
 * @return bool
10061
 */
1441 ariadna 10062
function site_is_public() {
1 efrain 10063
    global $CFG;
10064
 
10065
    // Return early if site admin has forced this setting.
10066
    if (isset($CFG->site_is_public)) {
10067
        return (bool)$CFG->site_is_public;
10068
    }
10069
 
10070
    $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10071
 
10072
    if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10073
        $ispublic = false;
10074
    } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10075
        $ispublic = false;
10076
    } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10077
        $ispublic = false;
10078
    } else {
10079
        $ispublic = true;
10080
    }
10081
 
10082
    return $ispublic;
10083
}
10084
 
10085
/**
10086
 * Validates user's password length.
10087
 *
10088
 * @param string $password
10089
 * @param int $pepperlength The length of the used peppers
10090
 * @return bool
10091
 */
1441 ariadna 10092
function exceeds_password_length(string $password, int $pepperlength = 0): bool {
1 efrain 10093
    return (strlen($password) > (MAX_PASSWORD_CHARACTERS + $pepperlength));
10094
}
10095
 
10096
/**
10097
 * A helper to replace PHP 8.3 usage of array_keys with two args.
10098
 *
10099
 * There is an indication that this will become a new method in PHP 8.4, but that has not happened yet.
10100
 * Therefore this non-polyfill has been created with a different naming convention.
10101
 * In the future it can be deprecated if a core PHP method is created.
10102
 *
10103
 * https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#array_keys
10104
 *
10105
 * @param array $array
10106
 * @param mixed $filter The value to filter on
10107
 * @param bool $strict Whether to apply a strit test with the filter
10108
 * @return array
10109
 */
1441 ariadna 10110
function moodle_array_keys_filter(array $array, mixed $filter, bool $strict = false): array {
1 efrain 10111
    return array_keys(array_filter(
10112
        $array,
1441 ariadna 10113
        function($value, $key) use ($filter, $strict): bool {
1 efrain 10114
            if ($strict) {
10115
                return $value === $filter;
10116
            }
10117
            return $value == $filter;
10118
        },
10119
        ARRAY_FILTER_USE_BOTH,
10120
    ));
10121
}