Proyectos de Subversion Moodle

Rev

Rev 1332 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * moodlelib.php - Moodle main library
19
 *
20
 * Main library file of miscellaneous general-purpose Moodle functions.
21
 * Other main libraries:
22
 *  - weblib.php      - functions that produce web output
23
 *  - datalib.php     - functions that access the database
24
 *
25
 * @package    core
26
 * @subpackage lib
27
 * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
28
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29
 */
30
 
31
use core\di;
32
use core\hook;
33
 
34
defined('MOODLE_INTERNAL') || die();
35
 
36
// CONSTANTS (Encased in phpdoc proper comments).
37
 
38
// Date and time constants.
39
/**
40
 * Time constant - the number of seconds in a year
41
 */
42
define('YEARSECS', 31536000);
43
 
44
/**
45
 * Time constant - the number of seconds in a week
46
 */
47
define('WEEKSECS', 604800);
48
 
49
/**
50
 * Time constant - the number of seconds in a day
51
 */
52
define('DAYSECS', 86400);
53
 
54
/**
55
 * Time constant - the number of seconds in an hour
56
 */
57
define('HOURSECS', 3600);
58
 
59
/**
60
 * Time constant - the number of seconds in a minute
61
 */
62
define('MINSECS', 60);
63
 
64
/**
65
 * Time constant - the number of minutes in a day
66
 */
67
define('DAYMINS', 1440);
68
 
69
/**
70
 * Time constant - the number of minutes in an hour
71
 */
72
define('HOURMINS', 60);
73
 
74
// Parameter constants - every call to optional_param(), required_param()
75
// or clean_param() should have a specified type of parameter.
76
 
77
// We currently include \core\param manually here to avoid broken upgrades.
78
// This may change after the next LTS release as LTS releases require the previous LTS release.
79
require_once(__DIR__ . '/classes/deprecation.php');
80
require_once(__DIR__ . '/classes/param.php');
81
 
82
/**
83
 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
84
 */
85
define('PARAM_ALPHA', \core\param::ALPHA->value);
86
 
87
/**
88
 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed
89
 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
90
 */
91
define('PARAM_ALPHAEXT', \core\param::ALPHAEXT->value);
92
 
93
/**
94
 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
95
 */
96
define('PARAM_ALPHANUM', \core\param::ALPHANUM->value);
97
 
98
/**
99
 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
100
 */
101
define('PARAM_ALPHANUMEXT', \core\param::ALPHANUMEXT->value);
102
 
103
/**
104
 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
105
 */
106
define('PARAM_AUTH', \core\param::AUTH->value);
107
 
108
/**
109
 * PARAM_BASE64 - Base 64 encoded format
110
 */
111
define('PARAM_BASE64', \core\param::BASE64->value);
112
 
113
/**
114
 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
115
 */
116
define('PARAM_BOOL', \core\param::BOOL->value);
117
 
118
/**
119
 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
120
 * checked against the list of capabilities in the database.
121
 */
122
define('PARAM_CAPABILITY', \core\param::CAPABILITY->value);
123
 
124
/**
125
 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
126
 * to use this. The normal mode of operation is to use PARAM_RAW when receiving
127
 * the input (required/optional_param or formslib) and then sanitise the HTML
128
 * using format_text on output. This is for the rare cases when you want to
129
 * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
130
 */
131
define('PARAM_CLEANHTML', \core\param::CLEANHTML->value);
132
 
133
/**
134
 * PARAM_EMAIL - an email address following the RFC
135
 */
136
define('PARAM_EMAIL', \core\param::EMAIL->value);
137
 
138
/**
139
 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
140
 */
141
define('PARAM_FILE', \core\param::FILE->value);
142
 
143
/**
144
 * PARAM_FLOAT - a real/floating point number.
145
 *
146
 * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
147
 * It does not work for languages that use , as a decimal separator.
148
 * Use PARAM_LOCALISEDFLOAT instead.
149
 */
150
define('PARAM_FLOAT', \core\param::FLOAT->value);
151
 
152
/**
153
 * PARAM_LOCALISEDFLOAT - a localised real/floating point number.
154
 * This is preferred over PARAM_FLOAT for numbers typed in by the user.
155
 * Cleans localised numbers to computer readable numbers; false for invalid numbers.
156
 */
157
define('PARAM_LOCALISEDFLOAT', \core\param::LOCALISEDFLOAT->value);
158
 
159
/**
160
 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
161
 */
162
define('PARAM_HOST', \core\param::HOST->value);
163
 
164
/**
165
 * PARAM_INT - integers only, use when expecting only numbers.
166
 */
167
define('PARAM_INT', \core\param::INT->value);
168
 
169
/**
170
 * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
171
 */
172
define('PARAM_LANG', \core\param::LANG->value);
173
 
174
/**
175
 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
176
 * others! Implies PARAM_URL!)
177
 */
178
define('PARAM_LOCALURL', \core\param::LOCALURL->value);
179
 
180
/**
181
 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
182
 */
183
define('PARAM_NOTAGS', \core\param::NOTAGS->value);
184
 
185
/**
186
 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
187
 * traversals note: the leading slash is not removed, window drive letter is not allowed
188
 */
189
define('PARAM_PATH', \core\param::PATH->value);
190
 
191
/**
192
 * PARAM_PEM - Privacy Enhanced Mail format
193
 */
194
define('PARAM_PEM', \core\param::PEM->value);
195
 
196
/**
197
 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
198
 */
199
define('PARAM_PERMISSION', \core\param::PERMISSION->value);
200
 
201
/**
202
 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
203
 */
204
define('PARAM_RAW', \core\param::RAW->value);
205
 
206
/**
207
 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
208
 */
209
define('PARAM_RAW_TRIMMED', \core\param::RAW_TRIMMED->value);
210
 
211
/**
212
 * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
213
 */
214
define('PARAM_SAFEDIR', \core\param::SAFEDIR->value);
215
 
216
/**
217
 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
218
 * and other references to Moodle code files.
219
 *
220
 * This is NOT intended to be used for absolute paths or any user uploaded files.
221
 */
222
define('PARAM_SAFEPATH', \core\param::SAFEPATH->value);
223
 
224
/**
225
 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9.  Numbers and comma only.
226
 */
227
define('PARAM_SEQUENCE', \core\param::SEQUENCE->value);
228
 
229
/**
230
 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
231
 */
232
define('PARAM_TAG', \core\param::TAG->value);
233
 
234
/**
235
 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
236
 */
237
define('PARAM_TAGLIST', \core\param::TAGLIST->value);
238
 
239
/**
240
 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
241
 */
242
define('PARAM_TEXT', \core\param::TEXT->value);
243
 
244
/**
245
 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
246
 */
247
define('PARAM_THEME', \core\param::THEME->value);
248
 
249
/**
250
 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
251
 * http://localhost.localdomain/ is ok.
252
 */
253
define('PARAM_URL', \core\param::URL->value);
254
 
255
/**
256
 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
257
 * accounts, do NOT use when syncing with external systems!!
258
 */
259
define('PARAM_USERNAME', \core\param::USERNAME->value);
260
 
261
/**
262
 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
263
 */
264
define('PARAM_STRINGID', \core\param::STRINGID->value);
265
 
266
// DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
267
/**
268
 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
269
 * It was one of the first types, that is why it is abused so much ;-)
270
 * @deprecated since 2.0
271
 */
272
define('PARAM_CLEAN', \core\param::CLEAN->value);
273
 
274
/**
275
 * PARAM_INTEGER - deprecated alias for PARAM_INT
276
 * @deprecated since 2.0
277
 */
278
define('PARAM_INTEGER', \core\param::INT->value);
279
 
280
/**
281
 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
282
 * @deprecated since 2.0
283
 */
284
define('PARAM_NUMBER', \core\param::FLOAT->value);
285
 
286
/**
287
 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
288
 * NOTE: originally alias for PARAM_APLHA
289
 * @deprecated since 2.0
290
 */
291
define('PARAM_ACTION', \core\param::ALPHANUMEXT->value);
292
 
293
/**
294
 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
295
 * NOTE: originally alias for PARAM_APLHA
296
 * @deprecated since 2.0
297
 */
298
define('PARAM_FORMAT', \core\param::ALPHANUMEXT->value);
299
 
300
/**
301
 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
302
 * @deprecated since 2.0
303
 */
304
define('PARAM_MULTILANG', \core\param::TEXT->value);
305
 
306
/**
307
 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
308
 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
309
 * America/Port-au-Prince)
310
 */
311
define('PARAM_TIMEZONE', \core\param::TIMEZONE->value);
312
 
313
/**
314
 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
315
 * @deprecated since 2.0
316
 */
317
define('PARAM_CLEANFILE', \core\param::CLEANFILE->value);
318
 
319
/**
320
 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
321
 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
322
 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
323
 * NOTE: numbers and underscores are strongly discouraged in plugin names!
324
 */
325
define('PARAM_COMPONENT', \core\param::COMPONENT->value);
326
 
327
/**
328
 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
329
 * It is usually used together with context id and component.
330
 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
331
 */
332
define('PARAM_AREA', \core\param::AREA->value);
333
 
334
/**
335
 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
336
 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
337
 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
338
 */
339
define('PARAM_PLUGIN', \core\param::PLUGIN->value);
340
 
341
 
342
// Web Services.
343
 
344
/**
345
 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
346
 */
347
define('VALUE_REQUIRED', 1);
348
 
349
/**
350
 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
351
 */
352
define('VALUE_OPTIONAL', 2);
353
 
354
/**
355
 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
356
 */
357
define('VALUE_DEFAULT', 0);
358
 
359
/**
360
 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
361
 */
362
define('NULL_NOT_ALLOWED', false);
363
 
364
/**
365
 * NULL_ALLOWED - the parameter can be set to null in the database
366
 */
367
define('NULL_ALLOWED', true);
368
 
369
// Page types.
370
 
371
/**
372
 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
373
 */
374
define('PAGE_COURSE_VIEW', 'course-view');
375
 
376
/** Get remote addr constant */
377
define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
378
/** Get remote addr constant */
379
define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
380
/**
381
 * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
382
 */
1326 ariadna 383
define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR | GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
1 efrain 384
 
385
// Blog access level constant declaration.
1326 ariadna 386
define('BLOG_USER_LEVEL', 1);
387
define('BLOG_GROUP_LEVEL', 2);
388
define('BLOG_COURSE_LEVEL', 3);
389
define('BLOG_SITE_LEVEL', 4);
390
define('BLOG_GLOBAL_LEVEL', 5);
1 efrain 391
 
392
 
393
// Tag constants.
394
/**
395
 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
396
 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
397
 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
398
 *
399
 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
400
 */
401
define('TAG_MAX_LENGTH', 50);
402
 
403
// Password policy constants.
1326 ariadna 404
define('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
405
define('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
406
define('PASSWORD_DIGITS', '0123456789');
407
define('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
1 efrain 408
 
409
/**
410
 * Required password pepper entropy.
411
 */
1326 ariadna 412
define('PEPPER_ENTROPY', 112);
1 efrain 413
 
414
// Feature constants.
415
// Used for plugin_supports() to report features that are, or are not, supported by a module.
416
 
417
/** True if module can provide a grade */
418
define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
419
/** True if module supports outcomes */
420
define('FEATURE_GRADE_OUTCOMES', 'outcomes');
421
/** True if module supports advanced grading methods */
422
define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
423
/** True if module controls the grade visibility over the gradebook */
424
define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
425
/** True if module supports plagiarism plugins */
426
define('FEATURE_PLAGIARISM', 'plagiarism');
427
 
428
/** True if module has code to track whether somebody viewed it */
429
define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
430
/** True if module has custom completion rules */
431
define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
432
 
433
/** True if module has no 'view' page (like label) */
434
define('FEATURE_NO_VIEW_LINK', 'viewlink');
435
/** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
436
define('FEATURE_IDNUMBER', 'idnumber');
437
/** True if module supports groups */
438
define('FEATURE_GROUPS', 'groups');
439
/** True if module supports groupings */
440
define('FEATURE_GROUPINGS', 'groupings');
441
/**
442
 * True if module supports groupmembersonly (which no longer exists)
443
 * @deprecated Since Moodle 2.8
444
 */
445
define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
446
 
447
/** Type of module */
448
define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
449
/** True if module supports intro editor */
450
define('FEATURE_MOD_INTRO', 'mod_intro');
451
/** True if module has default completion */
452
define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
453
 
454
define('FEATURE_COMMENT', 'comment');
455
 
456
define('FEATURE_RATE', 'rate');
457
/** True if module supports backup/restore of moodle2 format */
458
define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
459
 
460
/** True if module can show description on course main page */
461
define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
462
 
463
/** True if module uses the question bank */
464
define('FEATURE_USES_QUESTIONS', 'usesquestions');
465
 
466
/**
467
 * Maximum filename char size
468
 */
469
define('MAX_FILENAME_SIZE', 100);
470
 
471
/** Unspecified module archetype */
472
define('MOD_ARCHETYPE_OTHER', 0);
473
/** Resource-like type module */
474
define('MOD_ARCHETYPE_RESOURCE', 1);
475
/** Assignment module archetype */
476
define('MOD_ARCHETYPE_ASSIGNMENT', 2);
477
/** System (not user-addable) module archetype */
478
define('MOD_ARCHETYPE_SYSTEM', 3);
479
 
480
/** Type of module */
481
define('FEATURE_MOD_PURPOSE', 'mod_purpose');
482
/** Module purpose administration */
483
define('MOD_PURPOSE_ADMINISTRATION', 'administration');
484
/** Module purpose assessment */
485
define('MOD_PURPOSE_ASSESSMENT', 'assessment');
486
/** Module purpose communication */
487
define('MOD_PURPOSE_COLLABORATION', 'collaboration');
488
/** Module purpose communication */
489
define('MOD_PURPOSE_COMMUNICATION', 'communication');
490
/** Module purpose content */
491
define('MOD_PURPOSE_CONTENT', 'content');
492
/** Module purpose interactive content */
493
define('MOD_PURPOSE_INTERACTIVECONTENT', 'interactivecontent');
494
/** Module purpose other */
495
define('MOD_PURPOSE_OTHER', 'other');
496
/**
497
 * Module purpose interface
498
 * @deprecated since Moodle 4.4
499
 * @todo MDL-80701 Remove in Moodle 4.8
1326 ariadna 500
 */
1 efrain 501
define('MOD_PURPOSE_INTERFACE', 'interface');
502
 
503
/**
504
 * Security token used for allowing access
505
 * from external application such as web services.
506
 * Scripts do not use any session, performance is relatively
507
 * low because we need to load access info in each request.
508
 * Scripts are executed in parallel.
509
 */
510
define('EXTERNAL_TOKEN_PERMANENT', 0);
511
 
512
/**
513
 * Security token used for allowing access
514
 * of embedded applications, the code is executed in the
515
 * active user session. Token is invalidated after user logs out.
516
 * Scripts are executed serially - normal session locking is used.
517
 */
518
define('EXTERNAL_TOKEN_EMBEDDED', 1);
519
 
520
/**
521
 * The home page should be the site home
522
 */
523
define('HOMEPAGE_SITE', 0);
524
/**
525
 * The home page should be the users my page
526
 */
527
define('HOMEPAGE_MY', 1);
528
/**
529
 * The home page can be chosen by the user
530
 */
531
define('HOMEPAGE_USER', 2);
532
/**
533
 * The home page should be the users my courses page
534
 */
535
define('HOMEPAGE_MYCOURSES', 3);
536
 
537
/**
538
 * URL of the Moodle sites registration portal.
539
 */
540
defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
541
 
542
/**
543
 * URL of main Moodle site for marketing, products and services.
544
 */
545
defined('MOODLE_PRODUCTURL') || define('MOODLE_PRODUCTURL', 'https://moodle.com');
546
 
547
/**
548
 * URL of the statistic server public key.
549
 */
550
defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem');
551
 
552
/**
553
 * Moodle mobile app service name
554
 */
555
define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
556
 
557
/**
558
 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
559
 */
560
define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
561
 
562
/**
563
 * Course display settings: display all sections on one page.
564
 */
565
define('COURSE_DISPLAY_SINGLEPAGE', 0);
566
/**
567
 * Course display settings: split pages into a page per section.
568
 */
569
define('COURSE_DISPLAY_MULTIPAGE', 1);
570
 
571
/**
572
 * Authentication constant: String used in password field when password is not stored.
573
 */
574
define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
575
 
576
/**
577
 * Email from header to never include via information.
578
 */
579
define('EMAIL_VIA_NEVER', 0);
580
 
581
/**
582
 * Email from header to always include via information.
583
 */
584
define('EMAIL_VIA_ALWAYS', 1);
585
 
586
/**
587
 * Email from header to only include via information if the address is no-reply.
588
 */
589
define('EMAIL_VIA_NO_REPLY_ONLY', 2);
590
 
591
/**
592
 * Contact site support form/link disabled.
593
 */
594
define('CONTACT_SUPPORT_DISABLED', 0);
595
 
596
/**
597
 * Contact site support form/link only available to authenticated users.
598
 */
599
define('CONTACT_SUPPORT_AUTHENTICATED', 1);
600
 
601
/**
602
 * Contact site support form/link available to anyone visiting the site.
603
 */
604
define('CONTACT_SUPPORT_ANYONE', 2);
605
 
606
/**
607
 * Maximum number of characters for password.
608
 */
609
define('MAX_PASSWORD_CHARACTERS', 128);
610
 
611
/**
612
 * Toggle sensitive feature is disabled. Used for sensitive inputs (passwords, tokens, keys).
613
 */
614
define('TOGGLE_SENSITIVE_DISABLED', 0);
615
 
616
/**
617
 * Toggle sensitive feature is enabled. Used for sensitive inputs (passwords, tokens, keys).
618
 */
619
define('TOGGLE_SENSITIVE_ENABLED', 1);
620
 
621
/**
622
 * Toggle sensitive feature is enabled for small screens only. Used for sensitive inputs (passwords, tokens, keys).
623
 */
624
define('TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY', 2);
625
 
626
// PARAMETER HANDLING.
627
 
628
/**
629
 * Returns a particular value for the named variable, taken from
630
 * POST or GET.  If the parameter doesn't exist then an error is
631
 * thrown because we require this variable.
632
 *
633
 * This function should be used to initialise all required values
634
 * in a script that are based on parameters.  Usually it will be
635
 * used like this:
636
 *    $id = required_param('id', PARAM_INT);
637
 *
638
 * Please note the $type parameter is now required and the value can not be array.
639
 *
640
 * @param string $parname the name of the page parameter we want
641
 * @param string $type expected type of parameter
642
 * @return mixed
643
 * @throws coding_exception
644
 */
1326 ariadna 645
function required_param($parname, $type)
646
{
1 efrain 647
    return \core\param::from_type($type)->required_param($parname);
648
}
649
 
650
/**
651
 * Returns a particular array value for the named variable, taken from
652
 * POST or GET.  If the parameter doesn't exist then an error is
653
 * thrown because we require this variable.
654
 *
655
 * This function should be used to initialise all required values
656
 * in a script that are based on parameters.  Usually it will be
657
 * used like this:
658
 *    $ids = required_param_array('ids', PARAM_INT);
659
 *
660
 *  Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
661
 *
662
 * @param string $parname the name of the page parameter we want
663
 * @param string $type expected type of parameter
664
 * @return array
665
 * @throws coding_exception
666
 */
1326 ariadna 667
function required_param_array($parname, $type)
668
{
1 efrain 669
    return \core\param::from_type($type)->required_param_array($parname);
670
}
671
 
672
/**
673
 * Returns a particular value for the named variable, taken from
674
 * POST or GET, otherwise returning a given default.
675
 *
676
 * This function should be used to initialise all optional values
677
 * in a script that are based on parameters.  Usually it will be
678
 * used like this:
679
 *    $name = optional_param('name', 'Fred', PARAM_TEXT);
680
 *
681
 * Please note the $type parameter is now required and the value can not be array.
682
 *
683
 * @param string $parname the name of the page parameter we want
684
 * @param mixed  $default the default value to return if nothing is found
685
 * @param string $type expected type of parameter
686
 * @return mixed
687
 * @throws coding_exception
688
 */
1326 ariadna 689
function optional_param($parname, $default, $type)
690
{
1 efrain 691
    return \core\param::from_type($type)->optional_param(
692
        paramname: $parname,
693
        default: $default,
694
    );
695
}
696
 
697
/**
698
 * Returns a particular array value for the named variable, taken from
699
 * POST or GET, otherwise returning a given default.
700
 *
701
 * This function should be used to initialise all optional values
702
 * in a script that are based on parameters.  Usually it will be
703
 * used like this:
704
 *    $ids = optional_param('id', array(), PARAM_INT);
705
 *
706
 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
707
 *
708
 * @param string $parname the name of the page parameter we want
709
 * @param mixed $default the default value to return if nothing is found
710
 * @param string $type expected type of parameter
711
 * @return array
712
 * @throws coding_exception
713
 */
1326 ariadna 714
function optional_param_array($parname, $default, $type)
715
{
1 efrain 716
    return \core\param::from_type($type)->optional_param_array(
717
        paramname: $parname,
718
        default: $default,
719
    );
720
}
721
 
722
/**
723
 * Strict validation of parameter values, the values are only converted
724
 * to requested PHP type. Internally it is using clean_param, the values
725
 * before and after cleaning must be equal - otherwise
726
 * an invalid_parameter_exception is thrown.
727
 * Objects and classes are not accepted.
728
 *
729
 * @param mixed $param
730
 * @param string $type PARAM_ constant
731
 * @param bool $allownull are nulls valid value?
732
 * @param string $debuginfo optional debug information
733
 * @return mixed the $param value converted to PHP type
734
 * @throws invalid_parameter_exception if $param is not of given type
735
 */
1326 ariadna 736
function validate_param($param, $type, $allownull = NULL_NOT_ALLOWED, $debuginfo = '')
737
{
1 efrain 738
    return \core\param::from_type($type)->validate_param(
739
        param: $param,
740
        allownull: $allownull,
741
        debuginfo: $debuginfo,
742
    );
743
}
744
 
745
/**
746
 * Makes sure array contains only the allowed types, this function does not validate array key names!
747
 *
748
 * <code>
749
 * $options = clean_param($options, PARAM_INT);
750
 * </code>
751
 *
752
 * @param array|null $param the variable array we are cleaning
753
 * @param string $type expected format of param after cleaning.
754
 * @param bool $recursive clean recursive arrays
755
 * @return array
756
 * @throws coding_exception
757
 */
1326 ariadna 758
function clean_param_array(?array $param, $type, $recursive = false)
759
{
1 efrain 760
    return \core\param::from_type($type)->clean_param_array(
761
        param: $param,
762
        recursive: $recursive,
763
    );
764
}
765
 
766
/**
767
 * Used by {@link optional_param()} and {@link required_param()} to
768
 * clean the variables and/or cast to specific types, based on
769
 * an options field.
770
 * <code>
771
 * $course->format = clean_param($course->format, PARAM_ALPHA);
772
 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
773
 * </code>
774
 *
775
 * @param mixed $param the variable we are cleaning
776
 * @param string $type expected format of param after cleaning.
777
 * @return mixed
778
 * @throws coding_exception
779
 */
1326 ariadna 780
function clean_param($param, $type)
781
{
1 efrain 782
    return \core\param::from_type($type)->clean($param);
783
}
784
 
785
/**
786
 * Whether the PARAM_* type is compatible in RTL.
787
 *
788
 * Being compatible with RTL means that the data they contain can flow
789
 * from right-to-left or left-to-right without compromising the user experience.
790
 *
791
 * Take URLs for example, they are not RTL compatible as they should always
792
 * flow from the left to the right. This also applies to numbers, email addresses,
793
 * configuration snippets, base64 strings, etc...
794
 *
795
 * This function tries to best guess which parameters can contain localised strings.
796
 *
797
 * @param string $paramtype Constant PARAM_*.
798
 * @return bool
799
 */
1326 ariadna 800
function is_rtl_compatible($paramtype)
801
{
1 efrain 802
    return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
803
}
804
 
805
/**
806
 * Makes sure the data is using valid utf8, invalid characters are discarded.
807
 *
808
 * Note: this function is not intended for full objects with methods and private properties.
809
 *
810
 * @param mixed $value
811
 * @return mixed with proper utf-8 encoding
812
 */
1326 ariadna 813
function fix_utf8($value)
814
{
1 efrain 815
    if (is_null($value) or $value === '') {
816
        return $value;
817
    } else if (is_string($value)) {
818
        if ((string)(int)$value === $value) {
819
            // Shortcut.
820
            return $value;
821
        }
822
 
823
        // Remove null bytes or invalid Unicode sequences from value.
824
        $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
825
 
826
        // Note: this duplicates min_fix_utf8() intentionally.
827
        static $buggyiconv = null;
828
        if ($buggyiconv === null) {
1326 ariadna 829
            $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100' . chr(130) . '€') !== '100€');
1 efrain 830
        }
831
 
832
        if ($buggyiconv) {
833
            if (function_exists('mb_convert_encoding')) {
834
                $subst = mb_substitute_character();
835
                mb_substitute_character('none');
836
                $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
837
                mb_substitute_character($subst);
838
            } else {
839
                // Warn admins on admin/index.php page.
840
                $result = $value;
841
            }
842
        } else {
843
            $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
844
        }
845
 
846
        return $result;
847
    } else if (is_array($value)) {
848
        foreach ($value as $k => $v) {
849
            $value[$k] = fix_utf8($v);
850
        }
851
        return $value;
852
    } else if (is_object($value)) {
853
        // Do not modify original.
1326 ariadna 854
        $value = clone ($value);
1 efrain 855
        foreach ($value as $k => $v) {
856
            $value->$k = fix_utf8($v);
857
        }
858
        return $value;
859
    } else {
860
        // This is some other type, no utf-8 here.
861
        return $value;
862
    }
863
}
864
 
865
/**
866
 * Return true if given value is integer or string with integer value
867
 *
868
 * @param mixed $value String or Int
869
 * @return bool true if number, false if not
870
 */
1326 ariadna 871
function is_number($value)
872
{
1 efrain 873
    if (is_int($value)) {
874
        return true;
875
    } else if (is_string($value)) {
876
        return ((string)(int)$value) === $value;
877
    } else {
878
        return false;
879
    }
880
}
881
 
882
/**
883
 * Returns host part from url.
884
 *
885
 * @param string $url full url
886
 * @return string host, null if not found
887
 */
1326 ariadna 888
function get_host_from_url($url)
889
{
1 efrain 890
    preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
891
    if ($matches) {
892
        return $matches[1];
893
    }
894
    return null;
895
}
896
 
897
/**
898
 * Tests whether anything was returned by text editor
899
 *
900
 * This function is useful for testing whether something you got back from
901
 * the HTML editor actually contains anything. Sometimes the HTML editor
902
 * appear to be empty, but actually you get back a <br> tag or something.
903
 *
904
 * @param string $string a string containing HTML.
905
 * @return boolean does the string contain any actual content - that is text,
906
 * images, objects, etc.
907
 */
1326 ariadna 908
function html_is_blank($string)
909
{
1 efrain 910
    return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == '';
911
}
912
 
913
/**
914
 * Set a key in global configuration
915
 *
916
 * Set a key/value pair in both this session's {@link $CFG} global variable
917
 * and in the 'config' database table for future sessions.
918
 *
919
 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
920
 * In that case it doesn't affect $CFG.
921
 *
922
 * A NULL value will delete the entry.
923
 *
924
 * NOTE: this function is called from lib/db/upgrade.php
925
 *
926
 * @param string $name the key to set
927
 * @param string|int|bool|null $value the value to set (without magic quotes),
928
 *               null to unset the value
929
 * @param string $plugin (optional) the plugin scope, default null
930
 * @return bool true or exception
931
 */
1326 ariadna 932
function set_config($name, $value, $plugin = null)
933
{
1 efrain 934
    global $CFG, $DB;
935
 
936
    // Redirect to appropriate handler when value is null.
937
    if ($value === null) {
938
        return unset_config($name, $plugin);
939
    }
940
 
941
    // Set variables determining conditions and where to store the new config.
942
    // Plugin config goes to {config_plugins}, core config goes to {config}.
943
    $iscore = empty($plugin);
944
    if ($iscore) {
945
        // If it's for core config.
946
        $table = 'config';
947
        $conditions = ['name' => $name];
948
        $invalidatecachekey = 'core';
949
    } else {
950
        // If it's a plugin.
951
        $table = 'config_plugins';
952
        $conditions = ['name' => $name, 'plugin' => $plugin];
953
        $invalidatecachekey = $plugin;
954
    }
955
 
956
    // DB handling - checks for existing config, updating or inserting only if necessary.
957
    $invalidatecache = true;
958
    $inserted = false;
959
    $record = $DB->get_record($table, $conditions, 'id, value');
960
    if ($record === false) {
961
        // Inserts a new config record.
962
        $config = new stdClass();
963
        $config->name  = $name;
964
        $config->value = $value;
965
        if (!$iscore) {
966
            $config->plugin = $plugin;
967
        }
968
        $inserted = $DB->insert_record($table, $config, false);
969
    } else if ($invalidatecache = ($record->value !== $value)) {
970
        // Record exists - Check and only set new value if it has changed.
971
        $DB->set_field($table, 'value', $value, ['id' => $record->id]);
972
    }
973
 
974
    if ($iscore && !isset($CFG->config_php_settings[$name])) {
975
        // So it's defined for this invocation at least.
976
        // Settings from db are always strings.
977
        $CFG->$name = (string) $value;
978
    }
979
 
980
    // When setting config during a Behat test (in the CLI script, not in the web browser
981
    // requests), remember which ones are set so that we can clear them later.
982
    if ($iscore && $inserted && defined('BEHAT_TEST')) {
983
        $CFG->behat_cli_added_config[$name] = true;
984
    }
985
 
986
    // Update siteidentifier cache, if required.
987
    if ($iscore && $name === 'siteidentifier') {
988
        cache_helper::update_site_identifier($value);
989
    }
990
 
991
    // Invalidate cache, if required.
992
    if ($invalidatecache) {
993
        cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
994
    }
995
 
996
    return true;
997
}
998
 
999
/**
1000
 * Get configuration values from the global config table
1001
 * or the config_plugins table.
1002
 *
1003
 * If called with one parameter, it will load all the config
1004
 * variables for one plugin, and return them as an object.
1005
 *
1006
 * If called with 2 parameters it will return a string single
1007
 * value or false if the value is not found.
1008
 *
1009
 * NOTE: this function is called from lib/db/upgrade.php
1010
 *
1011
 * @param string $plugin full component name
1012
 * @param string $name default null
1013
 * @return mixed hash-like object or single value, return false no config found
1014
 * @throws dml_exception
1015
 */
1326 ariadna 1016
function get_config($plugin, $name = null)
1017
{
1 efrain 1018
    global $CFG, $DB;
1019
 
1020
    if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1326 ariadna 1021
        $forced = &$CFG->config_php_settings;
1 efrain 1022
        $iscore = true;
1023
        $plugin = 'core';
1024
    } else {
1025
        if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1326 ariadna 1026
            $forced = &$CFG->forced_plugin_settings[$plugin];
1 efrain 1027
        } else {
1028
            $forced = array();
1029
        }
1030
        $iscore = false;
1031
    }
1032
 
1033
    if (!isset($CFG->siteidentifier)) {
1034
        try {
1035
            // This may throw an exception during installation, which is how we detect the
1036
            // need to install the database. For more details see {@see initialise_cfg()}.
1037
            $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1038
        } catch (dml_exception $ex) {
1039
            // Set siteidentifier to false. We don't want to trip this continually.
1040
            $siteidentifier = false;
1041
            throw $ex;
1042
        }
1043
    }
1044
 
1045
    if (!empty($name)) {
1046
        if (array_key_exists($name, $forced)) {
1047
            return (string)$forced[$name];
1048
        } else if ($name === 'siteidentifier' && $plugin == 'core') {
1049
            return $CFG->siteidentifier;
1050
        }
1051
    }
1052
 
1053
    $cache = cache::make('core', 'config');
1054
    $result = $cache->get($plugin);
1055
    if ($result === false) {
1056
        // The user is after a recordset.
1057
        if (!$iscore) {
1058
            $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1059
        } else {
1060
            // This part is not really used any more, but anyway...
1061
            $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1062
        }
1063
        $cache->set($plugin, $result);
1064
    }
1065
 
1066
    if (!empty($name)) {
1067
        if (array_key_exists($name, $result)) {
1068
            return $result[$name];
1069
        }
1070
        return false;
1071
    }
1072
 
1073
    if ($plugin === 'core') {
1074
        $result['siteidentifier'] = $CFG->siteidentifier;
1075
    }
1076
 
1077
    foreach ($forced as $key => $value) {
1078
        if (is_null($value) or is_array($value) or is_object($value)) {
1079
            // We do not want any extra mess here, just real settings that could be saved in db.
1080
            unset($result[$key]);
1081
        } else {
1082
            // Convert to string as if it went through the DB.
1083
            $result[$key] = (string)$value;
1084
        }
1085
    }
1086
 
1087
    return (object)$result;
1088
}
1089
 
1090
/**
1091
 * Removes a key from global configuration.
1092
 *
1093
 * NOTE: this function is called from lib/db/upgrade.php
1094
 *
1095
 * @param string $name the key to set
1096
 * @param string $plugin (optional) the plugin scope
1097
 * @return boolean whether the operation succeeded.
1098
 */
1326 ariadna 1099
function unset_config($name, $plugin = null)
1100
{
1 efrain 1101
    global $CFG, $DB;
1102
 
1103
    if (empty($plugin)) {
1104
        unset($CFG->$name);
1105
        $DB->delete_records('config', array('name' => $name));
1106
        cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1107
    } else {
1108
        $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1109
        cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1110
    }
1111
 
1112
    return true;
1113
}
1114
 
1115
/**
1116
 * Remove all the config variables for a given plugin.
1117
 *
1118
 * NOTE: this function is called from lib/db/upgrade.php
1119
 *
1120
 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1121
 * @return boolean whether the operation succeeded.
1122
 */
1326 ariadna 1123
function unset_all_config_for_plugin($plugin)
1124
{
1 efrain 1125
    global $DB;
1126
    // Delete from the obvious config_plugins first.
1127
    $DB->delete_records('config_plugins', array('plugin' => $plugin));
1128
    // Next delete any suspect settings from config.
1129
    $like = $DB->sql_like('name', '?', true, true, false, '|');
1326 ariadna 1130
    $params = array($DB->sql_like_escape($plugin . '_', '|') . '%');
1 efrain 1131
    $DB->delete_records_select('config', $like, $params);
1132
    // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1133
    cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1134
 
1135
    return true;
1136
}
1137
 
1138
/**
1139
 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1140
 *
1141
 * All users are verified if they still have the necessary capability.
1142
 *
1143
 * @param string $value the value of the config setting.
1144
 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1145
 * @param bool $includeadmins include administrators.
1146
 * @return array of user objects.
1147
 */
1326 ariadna 1148
function get_users_from_config($value, $capability, $includeadmins = true)
1149
{
1 efrain 1150
    if (empty($value) or $value === '$@NONE@$') {
1151
        return array();
1152
    }
1153
 
1154
    // We have to make sure that users still have the necessary capability,
1155
    // it should be faster to fetch them all first and then test if they are present
1156
    // instead of validating them one-by-one.
1157
    $users = get_users_by_capability(context_system::instance(), $capability);
1158
    if ($includeadmins) {
1159
        $admins = get_admins();
1160
        foreach ($admins as $admin) {
1161
            $users[$admin->id] = $admin;
1162
        }
1163
    }
1164
 
1165
    if ($value === '$@ALL@$') {
1166
        return $users;
1167
    }
1168
 
1169
    $result = array(); // Result in correct order.
1170
    $allowed = explode(',', $value);
1171
    foreach ($allowed as $uid) {
1172
        if (isset($users[$uid])) {
1173
            $user = $users[$uid];
1174
            $result[$user->id] = $user;
1175
        }
1176
    }
1177
 
1178
    return $result;
1179
}
1180
 
1181
 
1182
/**
1183
 * Invalidates browser caches and cached data in temp.
1184
 *
1185
 * @return void
1186
 */
1326 ariadna 1187
function purge_all_caches()
1188
{
1 efrain 1189
    purge_caches();
1190
}
1191
 
1192
/**
1193
 * Selectively invalidate different types of cache.
1194
 *
1195
 * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
1196
 * areas alone or in combination.
1197
 *
1198
 * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1199
 *        'muc'    Purge MUC caches?
11 efrain 1200
 *        'courses' Purge all course caches, or specific course caches (CLI only)
1 efrain 1201
 *        'theme'  Purge theme cache?
1202
 *        'lang'   Purge language string cache?
1203
 *        'js'     Purge javascript cache?
1204
 *        'filter' Purge text filter cache?
1205
 *        'other'  Purge all other caches?
1206
 */
1326 ariadna 1207
function purge_caches($options = [])
1208
{
1 efrain 1209
    $defaults = array_fill_keys(['muc', 'courses', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1210
    if (empty(array_filter($options))) {
1211
        $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1212
    } else {
1213
        $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1214
    }
1215
    if ($options['muc']) {
1216
        cache_helper::purge_all();
11 efrain 1217
    } else if ($options['courses']) {
1 efrain 1218
        if ($options['courses'] === true) {
1219
            $courseids = [];
1220
        } else {
1221
            $courseids = preg_split('/\s*,\s*/', $options['courses'], -1, PREG_SPLIT_NO_EMPTY);
1222
        }
1223
        course_modinfo::purge_course_caches($courseids);
1224
    }
1225
    if ($options['theme']) {
1226
        theme_reset_all_caches();
1227
    }
1228
    if ($options['lang']) {
1229
        get_string_manager()->reset_caches();
1230
    }
1231
    if ($options['js']) {
1232
        js_reset_all_caches();
1233
    }
1234
    if ($options['template']) {
1235
        template_reset_all_caches();
1236
    }
1237
    if ($options['filter']) {
1238
        reset_text_filters_cache();
1239
    }
1240
    if ($options['other']) {
1241
        purge_other_caches();
1242
    }
1243
}
1244
 
1245
/**
1246
 * Purge all non-MUC caches not otherwise purged in purge_caches.
1247
 *
1248
 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1249
 * {@link phpunit_util::reset_dataroot()}
1250
 */
1326 ariadna 1251
function purge_other_caches()
1252
{
1 efrain 1253
    global $DB, $CFG;
1254
    if (class_exists('core_plugin_manager')) {
1255
        core_plugin_manager::reset_caches();
1256
    }
1257
 
1258
    // Bump up cacherev field for all courses.
1259
    try {
1260
        increment_revision_number('course', 'cacherev', '');
1261
    } catch (moodle_exception $e) {
1262
        // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1263
    }
1264
 
1265
    $DB->reset_caches();
1266
 
1267
    // Purge all other caches: rss, simplepie, etc.
1268
    clearstatcache();
1326 ariadna 1269
    remove_dir($CFG->cachedir . '', true);
1 efrain 1270
 
1271
    // Make sure cache dir is writable, throws exception if not.
1272
    make_cache_directory('');
1273
 
1274
    // This is the only place where we purge local caches, we are only adding files there.
1275
    // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1276
    remove_dir($CFG->localcachedir, true);
1277
    set_config('localcachedirpurged', time());
1278
    make_localcache_directory('', true);
11 efrain 1279
 
1280
    // Rewarm the bootstrap.php files so the siteid is always present after a purge.
1281
    initialise_local_config_cache();
1 efrain 1282
    \core\task\manager::clear_static_caches();
1283
}
1284
 
1285
/**
1286
 * Get volatile flags
1287
 *
1288
 * @param string $type
1289
 * @param int $changedsince default null
1290
 * @return array records array
1291
 */
1326 ariadna 1292
function get_cache_flags($type, $changedsince = null)
1293
{
1 efrain 1294
    global $DB;
1295
 
1296
    $params = array('type' => $type, 'expiry' => time());
1297
    $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1298
    if ($changedsince !== null) {
1299
        $params['changedsince'] = $changedsince;
1300
        $sqlwhere .= " AND timemodified > :changedsince";
1301
    }
1302
    $cf = array();
1303
    if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1304
        foreach ($flags as $flag) {
1305
            $cf[$flag->name] = $flag->value;
1306
        }
1307
    }
1308
    return $cf;
1309
}
1310
 
1311
/**
1312
 * Get volatile flags
1313
 *
1314
 * @param string $type
1315
 * @param string $name
1316
 * @param int $changedsince default null
1317
 * @return string|false The cache flag value or false
1318
 */
1326 ariadna 1319
function get_cache_flag($type, $name, $changedsince = null)
1320
{
1 efrain 1321
    global $DB;
1322
 
1323
    $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1324
 
1325
    $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1326
    if ($changedsince !== null) {
1327
        $params['changedsince'] = $changedsince;
1328
        $sqlwhere .= " AND timemodified > :changedsince";
1329
    }
1330
 
1331
    return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1332
}
1333
 
1334
/**
1335
 * Set a volatile flag
1336
 *
1337
 * @param string $type the "type" namespace for the key
1338
 * @param string $name the key to set
1339
 * @param string $value the value to set (without magic quotes) - null will remove the flag
1340
 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1341
 * @return bool Always returns true
1342
 */
1326 ariadna 1343
function set_cache_flag($type, $name, $value, $expiry = null)
1344
{
1 efrain 1345
    global $DB;
1346
 
1347
    $timemodified = time();
1348
    if ($expiry === null || $expiry < $timemodified) {
1349
        $expiry = $timemodified + 24 * 60 * 60;
1350
    } else {
1351
        $expiry = (int)$expiry;
1352
    }
1353
 
1354
    if ($value === null) {
1355
        unset_cache_flag($type, $name);
1356
        return true;
1357
    }
1358
 
1359
    if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1360
        // This is a potential problem in DEBUG_DEVELOPER.
1361
        if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1362
            return true; // No need to update.
1363
        }
1364
        $f->value        = $value;
1365
        $f->expiry       = $expiry;
1366
        $f->timemodified = $timemodified;
1367
        $DB->update_record('cache_flags', $f);
1368
    } else {
1369
        $f = new stdClass();
1370
        $f->flagtype     = $type;
1371
        $f->name         = $name;
1372
        $f->value        = $value;
1373
        $f->expiry       = $expiry;
1374
        $f->timemodified = $timemodified;
1375
        $DB->insert_record('cache_flags', $f);
1376
    }
1377
    return true;
1378
}
1379
 
1380
/**
1381
 * Removes a single volatile flag
1382
 *
1383
 * @param string $type the "type" namespace for the key
1384
 * @param string $name the key to set
1385
 * @return bool
1386
 */
1326 ariadna 1387
function unset_cache_flag($type, $name)
1388
{
1 efrain 1389
    global $DB;
1390
    $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1391
    return true;
1392
}
1393
 
1394
/**
1395
 * Garbage-collect volatile flags
1396
 *
1397
 * @return bool Always returns true
1398
 */
1326 ariadna 1399
function gc_cache_flags()
1400
{
1 efrain 1401
    global $DB;
1402
    $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1403
    return true;
1404
}
1405
 
1406
// USER PREFERENCE API.
1407
 
1408
/**
1409
 * Refresh user preference cache. This is used most often for $USER
1410
 * object that is stored in session, but it also helps with performance in cron script.
1411
 *
1412
 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1413
 *
1414
 * @package  core
1415
 * @category preference
1416
 * @access   public
1417
 * @param    stdClass         $user          User object. Preferences are preloaded into 'preference' property
1418
 * @param    int              $cachelifetime Cache life time on the current page (in seconds)
1419
 * @throws   coding_exception
1420
 * @return   null
1421
 */
1326 ariadna 1422
function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120)
1423
{
1 efrain 1424
    global $DB;
1425
    // Static cache, we need to check on each page load, not only every 2 minutes.
1426
    static $loadedusers = array();
1427
 
1428
    if (!isset($user->id)) {
1429
        throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1430
    }
1431
 
1432
    if (empty($user->id) or isguestuser($user->id)) {
1433
        // No permanent storage for not-logged-in users and guest.
1434
        if (!isset($user->preference)) {
1435
            $user->preference = array();
1436
        }
1437
        return;
1438
    }
1439
 
1440
    $timenow = time();
1441
 
1442
    if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1443
        // Already loaded at least once on this page. Are we up to date?
1444
        if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1445
            // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1446
            return;
1447
        } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1448
            // No change since the lastcheck on this page.
1449
            $user->preference['_lastloaded'] = $timenow;
1450
            return;
1451
        }
1452
    }
1453
 
1454
    // OK, so we have to reload all preferences.
1455
    $loadedusers[$user->id] = true;
1456
    $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1457
    $user->preference['_lastloaded'] = $timenow;
1458
}
1459
 
1460
/**
1461
 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1462
 *
1463
 * NOTE: internal function, do not call from other code.
1464
 *
1465
 * @package core
1466
 * @access private
1467
 * @param integer $userid the user whose prefs were changed.
1468
 */
1326 ariadna 1469
function mark_user_preferences_changed($userid)
1470
{
1 efrain 1471
    global $CFG;
1472
 
1473
    if (empty($userid) or isguestuser($userid)) {
1474
        // No cache flags for guest and not-logged-in users.
1475
        return;
1476
    }
1477
 
1478
    set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1479
}
1480
 
1481
/**
1482
 * Sets a preference for the specified user.
1483
 *
1484
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1485
 *
1486
 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1487
 *
1488
 * @package  core
1489
 * @category preference
1490
 * @access   public
1491
 * @param    string            $name  The key to set as preference for the specified user
1492
 * @param    string|int|bool|null $value The value to set for the $name key in the specified user's
1493
 *                                    record, null means delete current value.
1494
 * @param    stdClass|int|null $user  A moodle user object or id, null means current user
1495
 * @throws   coding_exception
1496
 * @return   bool                     Always true or exception
1497
 */
1326 ariadna 1498
function set_user_preference($name, $value, $user = null)
1499
{
1 efrain 1500
    global $USER, $DB;
1501
 
1502
    if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1503
        throw new coding_exception('Invalid preference name in set_user_preference() call');
1504
    }
1505
 
1506
    if (is_null($value)) {
1507
        // Null means delete current.
1508
        return unset_user_preference($name, $user);
1509
    } else if (is_object($value)) {
1510
        throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1511
    } else if (is_array($value)) {
1512
        throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1513
    }
1514
    // Value column maximum length is 1333 characters.
1515
    $value = (string)$value;
1516
    if (core_text::strlen($value) > 1333) {
1517
        throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1518
    }
1519
 
1520
    if (is_null($user)) {
1521
        $user = $USER;
1522
    } else if (isset($user->id)) {
1523
        // It is a valid object.
1524
    } else if (is_numeric($user)) {
1525
        $user = (object)array('id' => (int)$user);
1526
    } else {
1527
        throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1528
    }
1529
 
1530
    check_user_preferences_loaded($user);
1531
 
1532
    if (empty($user->id) or isguestuser($user->id)) {
1533
        // No permanent storage for not-logged-in users and guest.
1534
        $user->preference[$name] = $value;
1535
        return true;
1536
    }
1537
 
1538
    if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1539
        if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1540
            // Preference already set to this value.
1541
            return true;
1542
        }
1543
        $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1544
    } else {
1545
        $preference = new stdClass();
1546
        $preference->userid = $user->id;
1547
        $preference->name   = $name;
1548
        $preference->value  = $value;
1549
        $DB->insert_record('user_preferences', $preference);
1550
    }
1551
 
1552
    // Update value in cache.
1553
    $user->preference[$name] = $value;
1554
    // Update the $USER in case where we've not a direct reference to $USER.
1555
    if ($user !== $USER && $user->id == $USER->id) {
1556
        $USER->preference[$name] = $value;
1557
    }
1558
 
1559
    // Set reload flag for other sessions.
1560
    mark_user_preferences_changed($user->id);
1561
 
1562
    return true;
1563
}
1564
 
1565
/**
1566
 * Sets a whole array of preferences for the current user
1567
 *
1568
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1569
 *
1570
 * @package  core
1571
 * @category preference
1572
 * @access   public
1573
 * @param    array             $prefarray An array of key/value pairs to be set
1574
 * @param    stdClass|int|null $user      A moodle user object or id, null means current user
1575
 * @return   bool                         Always true or exception
1576
 */
1326 ariadna 1577
function set_user_preferences(array $prefarray, $user = null)
1578
{
1 efrain 1579
    foreach ($prefarray as $name => $value) {
1580
        set_user_preference($name, $value, $user);
1581
    }
1582
    return true;
1583
}
1584
 
1585
/**
1586
 * Unsets a preference completely by deleting it from the database
1587
 *
1588
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1589
 *
1590
 * @package  core
1591
 * @category preference
1592
 * @access   public
1593
 * @param    string            $name The key to unset as preference for the specified user
1594
 * @param    stdClass|int|null $user A moodle user object or id, null means current user
1595
 * @throws   coding_exception
1596
 * @return   bool                    Always true or exception
1597
 */
1326 ariadna 1598
function unset_user_preference($name, $user = null)
1599
{
1 efrain 1600
    global $USER, $DB;
1601
 
1602
    if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1603
        throw new coding_exception('Invalid preference name in unset_user_preference() call');
1604
    }
1605
 
1606
    if (is_null($user)) {
1607
        $user = $USER;
1608
    } else if (isset($user->id)) {
1609
        // It is a valid object.
1610
    } else if (is_numeric($user)) {
1611
        $user = (object)array('id' => (int)$user);
1612
    } else {
1613
        throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1614
    }
1615
 
1616
    check_user_preferences_loaded($user);
1617
 
1618
    if (empty($user->id) or isguestuser($user->id)) {
1619
        // No permanent storage for not-logged-in user and guest.
1620
        unset($user->preference[$name]);
1621
        return true;
1622
    }
1623
 
1624
    // Delete from DB.
1625
    $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
1626
 
1627
    // Delete the preference from cache.
1628
    unset($user->preference[$name]);
1629
    // Update the $USER in case where we've not a direct reference to $USER.
1630
    if ($user !== $USER && $user->id == $USER->id) {
1631
        unset($USER->preference[$name]);
1632
    }
1633
 
1634
    // Set reload flag for other sessions.
1635
    mark_user_preferences_changed($user->id);
1636
 
1637
    return true;
1638
}
1639
 
1640
/**
1641
 * Used to fetch user preference(s)
1642
 *
1643
 * If no arguments are supplied this function will return
1644
 * all of the current user preferences as an array.
1645
 *
1646
 * If a name is specified then this function
1647
 * attempts to return that particular preference value.  If
1648
 * none is found, then the optional value $default is returned,
1649
 * otherwise null.
1650
 *
1651
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1652
 *
1653
 * @package  core
1654
 * @category preference
1655
 * @access   public
1656
 * @param    string            $name    Name of the key to use in finding a preference value
1657
 * @param    mixed|null        $default Value to be returned if the $name key is not set in the user preferences
1658
 * @param    stdClass|int|null $user    A moodle user object or id, null means current user
1659
 * @throws   coding_exception
1660
 * @return   string|mixed|null          A string containing the value of a single preference. An
1661
 *                                      array with all of the preferences or null
1662
 */
1326 ariadna 1663
function get_user_preferences($name = null, $default = null, $user = null)
1664
{
1 efrain 1665
    global $USER;
1666
 
1667
    if (is_null($name)) {
1668
        // All prefs.
1669
    } else if (is_numeric($name) or $name === '_lastloaded') {
1670
        throw new coding_exception('Invalid preference name in get_user_preferences() call');
1671
    }
1672
 
1673
    if (is_null($user)) {
1674
        $user = $USER;
1675
    } else if (isset($user->id)) {
1676
        // Is a valid object.
1677
    } else if (is_numeric($user)) {
1678
        if ($USER->id == $user) {
1679
            $user = $USER;
1680
        } else {
1681
            $user = (object)array('id' => (int)$user);
1682
        }
1683
    } else {
1684
        throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1685
    }
1686
 
1687
    check_user_preferences_loaded($user);
1688
 
1689
    if (empty($name)) {
1690
        // All values.
1691
        return $user->preference;
1692
    } else if (isset($user->preference[$name])) {
1693
        // The single string value.
1694
        return $user->preference[$name];
1695
    } else {
1696
        // Default value (null if not specified).
1697
        return $default;
1698
    }
1699
}
1700
 
1701
// FUNCTIONS FOR HANDLING TIME.
1702
 
1703
/**
1704
 * Given Gregorian date parts in user time produce a GMT timestamp.
1705
 *
1706
 * @package core
1707
 * @category time
1708
 * @param int $year The year part to create timestamp of
1709
 * @param int $month The month part to create timestamp of
1710
 * @param int $day The day part to create timestamp of
1711
 * @param int $hour The hour part to create timestamp of
1712
 * @param int $minute The minute part to create timestamp of
1713
 * @param int $second The second part to create timestamp of
1714
 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
1715
 *             if 99 then default user's timezone is used {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1716
 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
1717
 *             applied only if timezone is 99 or string.
1718
 * @return int GMT timestamp
1719
 */
1326 ariadna 1720
function make_timestamp($year, $month = 1, $day = 1, $hour = 0, $minute = 0, $second = 0, $timezone = 99, $applydst = true)
1721
{
1 efrain 1722
    $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
1723
    $date->setDate((int)$year, (int)$month, (int)$day);
1724
    $date->setTime((int)$hour, (int)$minute, (int)$second);
1725
 
1726
    $time = $date->getTimestamp();
1727
 
1728
    if ($time === false) {
1326 ariadna 1729
        throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.' .
1 efrain 1730
            ' This can fail if year is more than 2038 and OS is 32 bit windows');
1731
    }
1732
 
1733
    // Moodle BC DST stuff.
1734
    if (!$applydst) {
1735
        $time += dst_offset_on($time, $timezone);
1736
    }
1737
 
1738
    return $time;
1739
}
1740
 
1741
/**
1742
 * Format a date/time (seconds) as weeks, days, hours etc as needed
1743
 *
1744
 * Given an amount of time in seconds, returns string
1745
 * formatted nicely as years, days, hours etc as needed
1746
 *
1747
 * @package core
1748
 * @category time
1749
 * @uses MINSECS
1750
 * @uses HOURSECS
1751
 * @uses DAYSECS
1752
 * @uses YEARSECS
1753
 * @param int $totalsecs Time in seconds
1754
 * @param stdClass $str Should be a time object
1755
 * @return string A nicely formatted date/time string
1756
 */
1326 ariadna 1757
function format_time($totalsecs, $str = null)
1758
{
1 efrain 1759
 
1760
    $totalsecs = abs($totalsecs);
1761
 
1762
    if (!$str) {
1763
        // Create the str structure the slow way.
1764
        $str = new stdClass();
1765
        $str->day   = get_string('day');
1766
        $str->days  = get_string('days');
1767
        $str->hour  = get_string('hour');
1768
        $str->hours = get_string('hours');
1769
        $str->min   = get_string('min');
1770
        $str->mins  = get_string('mins');
1771
        $str->sec   = get_string('sec');
1772
        $str->secs  = get_string('secs');
1773
        $str->year  = get_string('year');
1774
        $str->years = get_string('years');
1775
    }
1776
 
1326 ariadna 1777
    $years     = floor($totalsecs / YEARSECS);
1778
    $remainder = $totalsecs - ($years * YEARSECS);
1779
    $days      = floor($remainder / DAYSECS);
1780
    $remainder = $totalsecs - ($days * DAYSECS);
1781
    $hours     = floor($remainder / HOURSECS);
1782
    $remainder = $remainder - ($hours * HOURSECS);
1783
    $mins      = floor($remainder / MINSECS);
1784
    $secs      = $remainder - ($mins * MINSECS);
1 efrain 1785
 
1786
    $ss = ($secs == 1)  ? $str->sec  : $str->secs;
1787
    $sm = ($mins == 1)  ? $str->min  : $str->mins;
1788
    $sh = ($hours == 1) ? $str->hour : $str->hours;
1789
    $sd = ($days == 1)  ? $str->day  : $str->days;
1790
    $sy = ($years == 1)  ? $str->year  : $str->years;
1791
 
1792
    $oyears = '';
1793
    $odays = '';
1794
    $ohours = '';
1795
    $omins = '';
1796
    $osecs = '';
1797
 
1798
    if ($years) {
1326 ariadna 1799
        $oyears  = $years . ' ' . $sy;
1 efrain 1800
    }
1801
    if ($days) {
1326 ariadna 1802
        $odays  = $days . ' ' . $sd;
1 efrain 1803
    }
1804
    if ($hours) {
1326 ariadna 1805
        $ohours = $hours . ' ' . $sh;
1 efrain 1806
    }
1807
    if ($mins) {
1326 ariadna 1808
        $omins  = $mins . ' ' . $sm;
1 efrain 1809
    }
1810
    if ($secs) {
1326 ariadna 1811
        $osecs  = $secs . ' ' . $ss;
1 efrain 1812
    }
1813
 
1814
    if ($years) {
1326 ariadna 1815
        return trim($oyears . ' ' . $odays);
1 efrain 1816
    }
1817
    if ($days) {
1326 ariadna 1818
        return trim($odays . ' ' . $ohours);
1 efrain 1819
    }
1820
    if ($hours) {
1326 ariadna 1821
        return trim($ohours . ' ' . $omins);
1 efrain 1822
    }
1823
    if ($mins) {
1326 ariadna 1824
        return trim($omins . ' ' . $osecs);
1 efrain 1825
    }
1826
    if ($secs) {
1827
        return $osecs;
1828
    }
1829
    return get_string('now');
1830
}
1831
 
1832
/**
1833
 * Returns a formatted string that represents a date in user time.
1834
 *
1835
 * @package core
1836
 * @category time
1837
 * @param int $date the timestamp in UTC, as obtained from the database.
1838
 * @param string $format strftime format. You should probably get this using
1839
 *        get_string('strftime...', 'langconfig');
1840
 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1841
 *        not 99 then daylight saving will not be added.
1842
 *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1843
 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1844
 *        If false then the leading zero is maintained.
1845
 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1846
 * @return string the formatted date/time.
1847
 */
1326 ariadna 1848
function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true)
1849
{
1 efrain 1850
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
1851
    return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
1852
}
1853
 
1854
/**
1855
 * Returns a html "time" tag with both the exact user date with timezone information
1856
 * as a datetime attribute in the W3C format, and the user readable date and time as text.
1857
 *
1858
 * @package core
1859
 * @category time
1860
 * @param int $date the timestamp in UTC, as obtained from the database.
1861
 * @param string $format strftime format. You should probably get this using
1862
 *        get_string('strftime...', 'langconfig');
1863
 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1864
 *        not 99 then daylight saving will not be added.
1865
 *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1866
 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1867
 *        If false then the leading zero is maintained.
1868
 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1869
 * @return string the formatted date/time.
1870
 */
1326 ariadna 1871
function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true)
1872
{
1 efrain 1873
    $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
1874
    if (CLI_SCRIPT && !PHPUNIT_TEST) {
1875
        return $userdatestr;
1876
    }
1877
    $machinedate = new DateTime();
1878
    $machinedate->setTimestamp(intval($date));
1879
    $machinedate->setTimezone(core_date::get_user_timezone_object());
1880
 
1881
    return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
1882
}
1883
 
1884
/**
1885
 * Returns a formatted date ensuring it is UTF-8.
1886
 *
1887
 * If we are running under Windows convert to Windows encoding and then back to UTF-8
1888
 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
1889
 *
1890
 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
1891
 * @param string $format strftime format.
1892
 * @param int|float|string $tz the user timezone
1893
 * @return string the formatted date/time.
1894
 * @since Moodle 2.3.3
1895
 */
1326 ariadna 1896
function date_format_string($date, $format, $tz = 99)
1897
{
1 efrain 1898
 
1899
    date_default_timezone_set(core_date::get_user_timezone($tz));
1900
 
1901
    if (date('A', 0) === date('A', HOURSECS * 18)) {
1902
        $datearray = getdate($date);
1903
        $format = str_replace([
1904
            '%P',
1905
            '%p',
1906
        ], [
1907
            $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
1908
            $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
1909
        ], $format);
1910
    }
1911
 
1912
    $datestring = core_date::strftime($format, $date);
1913
    core_date::set_default_server_timezone();
1914
 
1915
    return $datestring;
1916
}
1917
 
1918
/**
1919
 * Given a $time timestamp in GMT (seconds since epoch),
1920
 * returns an array that represents the Gregorian date in user time
1921
 *
1922
 * @package core
1923
 * @category time
1924
 * @param int $time Timestamp in GMT
1925
 * @param float|int|string $timezone user timezone
1926
 * @return array An array that represents the date in user time
1927
 */
1326 ariadna 1928
function usergetdate($time, $timezone = 99)
1929
{
1 efrain 1930
    if ($time === null) {
1931
        // PHP8 and PHP7 return different results when getdate(null) is called.
1932
        // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
1933
        // In the future versions of Moodle we may consider adding a strict typehint.
1934
        debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER);
1935
        $time = 0;
1936
    }
1937
 
1938
    date_default_timezone_set(core_date::get_user_timezone($timezone));
1939
    $result = getdate($time);
1940
    core_date::set_default_server_timezone();
1941
 
1942
    return $result;
1943
}
1944
 
1945
/**
1946
 * Given a GMT timestamp (seconds since epoch), offsets it by
1947
 * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
1948
 *
1949
 * NOTE: this function does not include DST properly,
1950
 *       you should use the PHP date stuff instead!
1951
 *
1952
 * @package core
1953
 * @category time
1954
 * @param int $date Timestamp in GMT
1955
 * @param float|int|string $timezone user timezone
1956
 * @return int
1957
 */
1326 ariadna 1958
function usertime($date, $timezone = 99)
1959
{
1 efrain 1960
    $userdate = new DateTime('@' . $date);
1961
    $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
1962
    $dst = dst_offset_on($date, $timezone);
1963
 
1964
    return $date - $userdate->getOffset() + $dst;
1965
}
1966
 
1967
/**
1968
 * Get a formatted string representation of an interval between two unix timestamps.
1969
 *
1970
 * E.g.
1971
 * $intervalstring = get_time_interval_string(12345600, 12345660);
1972
 * Will produce the string:
1973
 * '0d 0h 1m'
1974
 *
1975
 * @param int $time1 unix timestamp
1976
 * @param int $time2 unix timestamp
1977
 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
1978
 * @param bool $dropzeroes If format is not provided and this is set to true, do not include zero time units.
1979
 *                         e.g. a duration of 3 days and 2 hours will be displayed as '3d 2h' instead of '3d 2h 0s'
1980
 * @param bool $fullformat If format is not provided and this is set to true, display time units in full format.
1981
 *                         e.g. instead of showing "3d", "3 days" will be returned.
1982
 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
1983
 */
1326 ariadna 1984
function get_time_interval_string(
1985
    int $time1,
1986
    int $time2,
1987
    string $format = '',
1988
    bool $dropzeroes = false,
1989
    bool $fullformat = false
1990
): string {
1 efrain 1991
    $dtdate = new DateTime();
1992
    $dtdate->setTimeStamp($time1);
1993
    $dtdate2 = new DateTime();
1994
    $dtdate2->setTimeStamp($time2);
1995
    $interval = $dtdate2->diff($dtdate);
1996
 
1997
    if (empty(trim($format))) {
1998
        // Default to this key.
1999
        $formatkey = 'dateintervaldayhrmin';
2000
 
2001
        if ($dropzeroes) {
2002
            $units = [
2003
                'y' => 'yr',
2004
                'm' => 'mo',
2005
                'd' => 'day',
2006
                'h' => 'hr',
2007
                'i' => 'min',
2008
                's' => 'sec',
2009
            ];
2010
            $formatunits = [];
2011
            foreach ($units as $key => $unit) {
2012
                if (empty($interval->$key)) {
2013
                    continue;
2014
                }
2015
                $formatunits[] = $unit;
2016
            }
2017
            if (!empty($formatunits)) {
2018
                $formatkey = 'dateinterval' . implode("", $formatunits);
2019
            }
2020
        }
2021
 
2022
        if ($fullformat) {
2023
            $formatkey .= 'full';
2024
        }
2025
        $format = get_string($formatkey, 'langconfig');
2026
    }
2027
    return $interval->format($format);
2028
}
2029
 
2030
/**
2031
 * Given a time, return the GMT timestamp of the most recent midnight
2032
 * for the current user.
2033
 *
2034
 * @package core
2035
 * @category time
2036
 * @param int $date Timestamp in GMT
2037
 * @param float|int|string $timezone user timezone
2038
 * @return int Returns a GMT timestamp
2039
 */
1326 ariadna 2040
function usergetmidnight($date, $timezone = 99)
2041
{
1 efrain 2042
 
2043
    $userdate = usergetdate($date, $timezone);
2044
 
2045
    // Time of midnight of this user's day, in GMT.
2046
    return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2047
}
2048
 
2049
/**
2050
 * Returns a string that prints the user's timezone
2051
 *
2052
 * @package core
2053
 * @category time
2054
 * @param float|int|string $timezone user timezone
2055
 * @return string
2056
 */
1326 ariadna 2057
function usertimezone($timezone = 99)
2058
{
1 efrain 2059
    $tz = core_date::get_user_timezone($timezone);
2060
    return core_date::get_localised_timezone($tz);
2061
}
2062
 
2063
/**
2064
 * Returns a float or a string which denotes the user's timezone
2065
 * A float value means that a simple offset from GMT is used, while a string (it will be the name of a timezone in the database)
2066
 * means that for this timezone there are also DST rules to be taken into account
2067
 * Checks various settings and picks the most dominant of those which have a value
2068
 *
2069
 * @package core
2070
 * @category time
2071
 * @param float|int|string $tz timezone to calculate GMT time offset before
2072
 *        calculating user timezone, 99 is default user timezone
2073
 *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2074
 * @return float|string
2075
 */
1326 ariadna 2076
function get_user_timezone($tz = 99)
2077
{
1 efrain 2078
    global $USER, $CFG;
2079
 
2080
    $timezones = array(
2081
        $tz,
2082
        isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2083
        isset($USER->timezone) ? $USER->timezone : 99,
2084
        isset($CFG->timezone) ? $CFG->timezone : 99,
1326 ariadna 2085
    );
1 efrain 2086
 
2087
    $tz = 99;
2088
 
2089
    // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2090
    foreach ($timezones as $nextvalue) {
2091
        if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2092
            $tz = $nextvalue;
2093
        }
2094
    }
2095
    return is_numeric($tz) ? (float) $tz : $tz;
2096
}
2097
 
2098
/**
2099
 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2100
 * - Note: Daylight saving only works for string timezones and not for float.
2101
 *
2102
 * @package core
2103
 * @category time
2104
 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2105
 * @param int|float|string $strtimezone user timezone
2106
 * @return int
2107
 */
1326 ariadna 2108
function dst_offset_on($time, $strtimezone = null)
2109
{
1 efrain 2110
    $tz = core_date::get_user_timezone($strtimezone);
2111
    $date = new DateTime('@' . $time);
2112
    $date->setTimezone(new DateTimeZone($tz));
2113
    if ($date->format('I') == '1') {
2114
        if ($tz === 'Australia/Lord_Howe') {
2115
            return 1800;
2116
        }
2117
        return 3600;
2118
    }
2119
    return 0;
2120
}
2121
 
2122
/**
2123
 * Calculates when the day appears in specific month
2124
 *
2125
 * @package core
2126
 * @category time
2127
 * @param int $startday starting day of the month
2128
 * @param int $weekday The day when week starts (normally taken from user preferences)
2129
 * @param int $month The month whose day is sought
2130
 * @param int $year The year of the month whose day is sought
2131
 * @return int
2132
 */
1326 ariadna 2133
function find_day_in_month($startday, $weekday, $month, $year)
2134
{
1 efrain 2135
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2136
 
2137
    $daysinmonth = days_in_month($month, $year);
2138
    $daysinweek = count($calendartype->get_weekdays());
2139
 
2140
    if ($weekday == -1) {
2141
        // Don't care about weekday, so return:
2142
        //    abs($startday) if $startday != -1
2143
        //    $daysinmonth otherwise.
2144
        return ($startday == -1) ? $daysinmonth : abs($startday);
2145
    }
2146
 
2147
    // From now on we 're looking for a specific weekday.
2148
    // Give "end of month" its actual value, since we know it.
2149
    if ($startday == -1) {
2150
        $startday = -1 * $daysinmonth;
2151
    }
2152
 
2153
    // Starting from day $startday, the sign is the direction.
2154
    if ($startday < 1) {
2155
        $startday = abs($startday);
2156
        $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2157
 
2158
        // This is the last such weekday of the month.
2159
        $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2160
        if ($lastinmonth > $daysinmonth) {
2161
            $lastinmonth -= $daysinweek;
2162
        }
2163
 
2164
        // Find the first such weekday <= $startday.
2165
        while ($lastinmonth > $startday) {
2166
            $lastinmonth -= $daysinweek;
2167
        }
2168
 
2169
        return $lastinmonth;
2170
    } else {
2171
        $indexweekday = dayofweek($startday, $month, $year);
2172
 
2173
        $diff = $weekday - $indexweekday;
2174
        if ($diff < 0) {
2175
            $diff += $daysinweek;
2176
        }
2177
 
2178
        // This is the first such weekday of the month equal to or after $startday.
2179
        $firstfromindex = $startday + $diff;
2180
 
2181
        return $firstfromindex;
2182
    }
2183
}
2184
 
2185
/**
2186
 * Calculate the number of days in a given month
2187
 *
2188
 * @package core
2189
 * @category time
2190
 * @param int $month The month whose day count is sought
2191
 * @param int $year The year of the month whose day count is sought
2192
 * @return int
2193
 */
1326 ariadna 2194
function days_in_month($month, $year)
2195
{
1 efrain 2196
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2197
    return $calendartype->get_num_days_in_month($year, $month);
2198
}
2199
 
2200
/**
2201
 * Calculate the position in the week of a specific calendar day
2202
 *
2203
 * @package core
2204
 * @category time
2205
 * @param int $day The day of the date whose position in the week is sought
2206
 * @param int $month The month of the date whose position in the week is sought
2207
 * @param int $year The year of the date whose position in the week is sought
2208
 * @return int
2209
 */
1326 ariadna 2210
function dayofweek($day, $month, $year)
2211
{
1 efrain 2212
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2213
    return $calendartype->get_weekday($year, $month, $day);
2214
}
2215
 
2216
// USER AUTHENTICATION AND LOGIN.
2217
 
2218
/**
2219
 * Returns full login url.
2220
 *
2221
 * Any form submissions for authentication to this URL must include username,
2222
 * password as well as a logintoken generated by \core\session\manager::get_login_token().
2223
 *
2224
 * @return string login url
2225
 */
1326 ariadna 2226
function get_login_url()
2227
{
1 efrain 2228
    global $CFG;
2229
 
2230
    return "$CFG->wwwroot/login/index.php";
2231
}
2232
 
2233
/**
2234
 * This function checks that the current user is logged in and has the
2235
 * required privileges
2236
 *
2237
 * This function checks that the current user is logged in, and optionally
2238
 * whether they are allowed to be in a particular course and view a particular
2239
 * course module.
2240
 * If they are not logged in, then it redirects them to the site login unless
2241
 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2242
 * case they are automatically logged in as guests.
2243
 * If $courseid is given and the user is not enrolled in that course then the
2244
 * user is redirected to the course enrolment page.
2245
 * If $cm is given and the course module is hidden and the user is not a teacher
2246
 * in the course then the user is redirected to the course home page.
2247
 *
2248
 * When $cm parameter specified, this function sets page layout to 'module'.
2249
 * You need to change it manually later if some other layout needed.
2250
 *
2251
 * @package    core_access
2252
 * @category   access
2253
 *
2254
 * @param mixed $courseorid id of the course or course object
2255
 * @param bool $autologinguest default true
2256
 * @param object $cm course module object
2257
 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2258
 *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2259
 *             in order to keep redirects working properly. MDL-14495
2260
 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2261
 * @return mixed Void, exit, and die depending on path
2262
 * @throws coding_exception
2263
 * @throws require_login_exception
2264
 * @throws moodle_exception
2265
 */
1326 ariadna 2266
function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false)
2267
{
1 efrain 2268
    global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2269
 
2270
    // Must not redirect when byteserving already started.
2271
    if (!empty($_SERVER['HTTP_RANGE'])) {
2272
        $preventredirect = true;
2273
    }
2274
 
2275
    if (AJAX_SCRIPT) {
2276
        // We cannot redirect for AJAX scripts either.
2277
        $preventredirect = true;
2278
    }
2279
 
2280
    // Setup global $COURSE, themes, language and locale.
2281
    if (!empty($courseorid)) {
2282
        if (is_object($courseorid)) {
2283
            $course = $courseorid;
2284
        } else if ($courseorid == SITEID) {
1326 ariadna 2285
            $course = clone ($SITE);
1 efrain 2286
        } else {
2287
            $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2288
        }
2289
        if ($cm) {
2290
            if ($cm->course != $course->id) {
2291
                throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2292
            }
2293
            // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2294
            if (!($cm instanceof cm_info)) {
2295
                // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2296
                // db queries so this is not really a performance concern, however it is obviously
2297
                // better if you use get_fast_modinfo to get the cm before calling this.
2298
                $modinfo = get_fast_modinfo($course);
2299
                $cm = $modinfo->get_cm($cm->id);
2300
            }
2301
        }
2302
    } else {
2303
        // Do not touch global $COURSE via $PAGE->set_course(),
2304
        // the reasons is we need to be able to call require_login() at any time!!
2305
        $course = $SITE;
2306
        if ($cm) {
2307
            throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2308
        }
2309
    }
2310
 
2311
    // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2312
    // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2313
    // risk leading the user back to the AJAX request URL.
2314
    if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2315
        $setwantsurltome = false;
2316
    }
2317
 
2318
    // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2319
    if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2320
        if ($preventredirect) {
2321
            throw new require_login_session_timeout_exception();
2322
        } else {
2323
            if ($setwantsurltome) {
2324
                $SESSION->wantsurl = qualified_me();
2325
            }
2326
            redirect(get_login_url());
2327
        }
2328
    }
2329
 
2330
    // If the user is not even logged in yet then make sure they are.
2331
    if (!isloggedin()) {
2332
        if ($autologinguest && !empty($CFG->autologinguests)) {
2333
            if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2334
                // Misconfigured site guest, just redirect to login page.
2335
                redirect(get_login_url());
2336
                exit; // Never reached.
2337
            }
2338
            $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2339
            complete_user_login($guest);
2340
            $USER->autologinguest = true;
2341
            $SESSION->lang = $lang;
2342
        } else {
2343
            // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2344
            if ($preventredirect) {
2345
                throw new require_login_exception('You are not logged in');
2346
            }
2347
 
2348
            if ($setwantsurltome) {
2349
                $SESSION->wantsurl = qualified_me();
2350
            }
2351
 
2352
            // Give auth plugins an opportunity to authenticate or redirect to an external login page
2353
            $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
1326 ariadna 2354
            foreach ($authsequence as $authname) {
1 efrain 2355
                $authplugin = get_auth_plugin($authname);
2356
                $authplugin->pre_loginpage_hook();
2357
                if (isloggedin()) {
2358
                    if ($cm) {
2359
                        $modinfo = get_fast_modinfo($course);
2360
                        $cm = $modinfo->get_cm($cm->id);
2361
                    }
2362
                    set_access_log_user();
2363
                    break;
2364
                }
2365
            }
2366
 
2367
            // If we're still not logged in then go to the login page
2368
            if (!isloggedin()) {
2369
                redirect(get_login_url());
2370
                exit; // Never reached.
2371
            }
2372
        }
2373
    }
2374
 
2375
    // Loginas as redirection if needed.
2376
    if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2377
        if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2378
            if ($USER->loginascontext->instanceid != $course->id) {
1326 ariadna 2379
                throw new \moodle_exception(
2380
                    'loginasonecourse',
2381
                    '',
2382
                    $CFG->wwwroot . '/course/view.php?id=' . $USER->loginascontext->instanceid
2383
                );
1 efrain 2384
            }
2385
        }
2386
    }
2387
 
2388
    // Check whether the user should be changing password (but only if it is REALLY them).
2389
    if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2390
        $userauth = get_auth_plugin($USER->auth);
2391
        if ($userauth->can_change_password() and !$preventredirect) {
2392
            if ($setwantsurltome) {
2393
                $SESSION->wantsurl = qualified_me();
2394
            }
2395
            if ($changeurl = $userauth->change_password_url()) {
2396
                // Use plugin custom url.
2397
                redirect($changeurl);
2398
            } else {
2399
                // Use moodle internal method.
1326 ariadna 2400
                redirect($CFG->wwwroot . '/login/change_password.php');
1 efrain 2401
            }
2402
        } else if ($userauth->can_change_password()) {
2403
            throw new moodle_exception('forcepasswordchangenotice');
2404
        } else {
2405
            throw new moodle_exception('nopasswordchangeforced', 'auth');
2406
        }
2407
    }
2408
 
2409
    // Check that the user account is properly set up. If we can't redirect to
2410
    // edit their profile and this is not a WS request, perform just the lax check.
2411
    // It will allow them to use filepicker on the profile edit page.
2412
 
2413
    if ($preventredirect && !WS_SERVER) {
2414
        $usernotfullysetup = user_not_fully_set_up($USER, false);
2415
    } else {
2416
        $usernotfullysetup = user_not_fully_set_up($USER, true);
2417
    }
2418
 
2419
    if ($usernotfullysetup) {
2420
        if ($preventredirect) {
2421
            throw new moodle_exception('usernotfullysetup');
2422
        }
2423
        if ($setwantsurltome) {
2424
            $SESSION->wantsurl = qualified_me();
2425
        }
1326 ariadna 2426
        redirect($CFG->wwwroot . '/user/edit.php?id=' . $USER->id . '&amp;course=' . SITEID);
1 efrain 2427
    }
2428
 
2429
    // Make sure the USER has a sesskey set up. Used for CSRF protection.
2430
    sesskey();
2431
 
2432
    if (\core\session\manager::is_loggedinas()) {
2433
        // During a "logged in as" session we should force all content to be cleaned because the
2434
        // logged in user will be viewing potentially malicious user generated content.
2435
        // See MDL-63786 for more details.
2436
        $CFG->forceclean = true;
2437
    }
2438
 
2439
    $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2440
 
2441
    // Do not bother admins with any formalities, except for activities pending deletion.
2442
    if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2443
        // Set the global $COURSE.
2444
        if ($cm) {
2445
            $PAGE->set_cm($cm, $course);
2446
            $PAGE->set_pagelayout('incourse');
2447
        } else if (!empty($courseorid)) {
2448
            $PAGE->set_course($course);
2449
        }
2450
        // Set accesstime or the user will appear offline which messes up messaging.
2451
        // Do not update access time for webservice or ajax requests.
2452
        if (!WS_SERVER && !AJAX_SCRIPT) {
2453
            user_accesstime_log($course->id);
2454
        }
2455
 
2456
        foreach ($afterlogins as $plugintype => $plugins) {
2457
            foreach ($plugins as $pluginfunction) {
2458
                $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2459
            }
2460
        }
2461
        return;
2462
    }
2463
 
2464
    // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2465
    // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2466
    if (!defined('NO_SITEPOLICY_CHECK')) {
2467
        define('NO_SITEPOLICY_CHECK', false);
2468
    }
2469
 
2470
    // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2471
    // Do not test if the script explicitly asked for skipping the site policies check.
2472
    // Or if the user auth type is webservice.
2473
    if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') {
2474
        $manager = new \core_privacy\local\sitepolicy\manager();
2475
        if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2476
            if ($preventredirect) {
2477
                throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2478
            }
2479
            if ($setwantsurltome) {
2480
                $SESSION->wantsurl = qualified_me();
2481
            }
2482
            redirect($policyurl);
2483
        }
2484
    }
2485
 
2486
    // Fetch the system context, the course context, and prefetch its child contexts.
2487
    $sysctx = context_system::instance();
2488
    $coursecontext = context_course::instance($course->id, MUST_EXIST);
2489
    if ($cm) {
2490
        $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2491
    } else {
2492
        $cmcontext = null;
2493
    }
2494
 
2495
    // If the site is currently under maintenance, then print a message.
2496
    if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2497
        if ($preventredirect) {
2498
            throw new require_login_exception('Maintenance in progress');
2499
        }
2500
        $PAGE->set_context(null);
2501
        print_maintenance_message();
2502
    }
2503
 
2504
    // Make sure the course itself is not hidden.
2505
    if ($course->id == SITEID) {
2506
        // Frontpage can not be hidden.
2507
    } else {
2508
        if (is_role_switched($course->id)) {
2509
            // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2510
        } else {
2511
            if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2512
                // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2513
                // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2514
                if ($preventredirect) {
2515
                    throw new require_login_exception('Course is hidden');
2516
                }
2517
                $PAGE->set_context(null);
2518
                // We need to override the navigation URL as the course won't have been added to the navigation and thus
2519
                // the navigation will mess up when trying to find it.
2520
                navigation_node::override_active_url(new moodle_url('/'));
1326 ariadna 2521
                notice(get_string('coursehidden'), $CFG->wwwroot . '/');
1 efrain 2522
            }
2523
        }
2524
    }
2525
 
2526
    // Is the user enrolled?
2527
    if ($course->id == SITEID) {
2528
        // Everybody is enrolled on the frontpage.
2529
    } else {
2530
        if (\core\session\manager::is_loggedinas()) {
2531
            // Make sure the REAL person can access this course first.
2532
            $realuser = \core\session\manager::get_realuser();
1326 ariadna 2533
            if (
2534
                !is_enrolled($coursecontext, $realuser->id, '', true) and
2535
                !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)
2536
            ) {
1 efrain 2537
                if ($preventredirect) {
2538
                    throw new require_login_exception('Invalid course login-as access');
2539
                }
2540
                $PAGE->set_context(null);
2541
                echo $OUTPUT->header();
1326 ariadna 2542
                notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot . '/');
1 efrain 2543
            }
2544
        }
2545
 
2546
        $access = false;
2547
 
2548
        if (is_role_switched($course->id)) {
2549
            // Ok, user had to be inside this course before the switch.
2550
            $access = true;
2551
        } else if (is_viewing($coursecontext, $USER)) {
2552
            // Ok, no need to mess with enrol.
2553
            $access = true;
2554
        } else {
2555
            if (isset($USER->enrol['enrolled'][$course->id])) {
2556
                if ($USER->enrol['enrolled'][$course->id] > time()) {
2557
                    $access = true;
2558
                    if (isset($USER->enrol['tempguest'][$course->id])) {
2559
                        unset($USER->enrol['tempguest'][$course->id]);
2560
                        remove_temp_course_roles($coursecontext);
2561
                    }
2562
                } else {
2563
                    // Expired.
2564
                    unset($USER->enrol['enrolled'][$course->id]);
2565
                }
2566
            }
2567
            if (isset($USER->enrol['tempguest'][$course->id])) {
2568
                if ($USER->enrol['tempguest'][$course->id] == 0) {
2569
                    $access = true;
2570
                } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2571
                    $access = true;
2572
                } else {
2573
                    // Expired.
2574
                    unset($USER->enrol['tempguest'][$course->id]);
2575
                    remove_temp_course_roles($coursecontext);
2576
                }
2577
            }
2578
 
2579
            if (!$access) {
2580
                // Cache not ok.
2581
                $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2582
                if ($until !== false) {
2583
                    // Active participants may always access, a timestamp in the future, 0 (always) or false.
2584
                    if ($until == 0) {
2585
                        $until = ENROL_MAX_TIMESTAMP;
2586
                    }
2587
                    $USER->enrol['enrolled'][$course->id] = $until;
2588
                    $access = true;
2589
                } else if (core_course_category::can_view_course_info($course)) {
2590
                    $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
2591
                    $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
2592
                    $enrols = enrol_get_plugins(true);
2593
                    // First ask all enabled enrol instances in course if they want to auto enrol user.
2594
                    foreach ($instances as $instance) {
2595
                        if (!isset($enrols[$instance->enrol])) {
2596
                            continue;
2597
                        }
2598
                        // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
2599
                        $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2600
                        if ($until !== false) {
2601
                            if ($until == 0) {
2602
                                $until = ENROL_MAX_TIMESTAMP;
2603
                            }
2604
                            $USER->enrol['enrolled'][$course->id] = $until;
2605
                            $access = true;
2606
                            break;
2607
                        }
2608
                    }
2609
                    // If not enrolled yet try to gain temporary guest access.
2610
                    if (!$access) {
2611
                        foreach ($instances as $instance) {
2612
                            if (!isset($enrols[$instance->enrol])) {
2613
                                continue;
2614
                            }
2615
                            // Get a duration for the guest access, a timestamp in the future or false.
2616
                            $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2617
                            if ($until !== false and $until > time()) {
2618
                                $USER->enrol['tempguest'][$course->id] = $until;
2619
                                $access = true;
2620
                                break;
2621
                            }
2622
                        }
2623
                    }
2624
                } else {
2625
                    // User is not enrolled and is not allowed to browse courses here.
2626
                    if ($preventredirect) {
2627
                        throw new require_login_exception('Course is not available');
2628
                    }
2629
                    $PAGE->set_context(null);
2630
                    // We need to override the navigation URL as the course won't have been added to the navigation and thus
2631
                    // the navigation will mess up when trying to find it.
2632
                    navigation_node::override_active_url(new moodle_url('/'));
1326 ariadna 2633
                    notice(get_string('coursehidden'), $CFG->wwwroot . '/');
1 efrain 2634
                }
2635
            }
2636
        }
2637
 
2638
        if (!$access) {
2639
            if ($preventredirect) {
2640
                throw new require_login_exception('Not enrolled');
2641
            }
2642
            if ($setwantsurltome) {
2643
                $SESSION->wantsurl = qualified_me();
2644
            }
1326 ariadna 2645
            redirect($CFG->wwwroot . '/enrol/index.php?id=' . $course->id);
1 efrain 2646
        }
2647
    }
2648
 
2649
    // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
2650
    if ($cm && $cm->deletioninprogress) {
2651
        if ($preventredirect) {
2652
            throw new moodle_exception('activityisscheduledfordeletion');
2653
        }
2654
        require_once($CFG->dirroot . '/course/lib.php');
2655
        redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
2656
    }
2657
 
2658
    // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
2659
    if ($cm && !$cm->uservisible) {
2660
        if ($preventredirect) {
2661
            throw new require_login_exception('Activity is hidden');
2662
        }
2663
        // Get the error message that activity is not available and why (if explanation can be shown to the user).
2664
        $PAGE->set_course($course);
2665
        $renderer = $PAGE->get_renderer('course');
2666
        $message = $renderer->course_section_cm_unavailable_error_message($cm);
2667
        redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
2668
    }
2669
 
2670
    // Set the global $COURSE.
2671
    if ($cm) {
2672
        $PAGE->set_cm($cm, $course);
2673
        $PAGE->set_pagelayout('incourse');
2674
    } else if (!empty($courseorid)) {
2675
        $PAGE->set_course($course);
2676
    }
2677
 
2678
    foreach ($afterlogins as $plugintype => $plugins) {
2679
        foreach ($plugins as $pluginfunction) {
2680
            $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2681
        }
2682
    }
2683
 
2684
    // Finally access granted, update lastaccess times.
2685
    // Do not update access time for webservice or ajax requests.
2686
    if (!WS_SERVER && !AJAX_SCRIPT) {
2687
        user_accesstime_log($course->id);
2688
    }
2689
}
2690
 
2691
/**
2692
 * A convenience function for where we must be logged in as admin
2693
 * @return void
2694
 */
1326 ariadna 2695
function require_admin()
2696
{
1 efrain 2697
    require_login(null, false);
2698
    require_capability('moodle/site:config', context_system::instance());
2699
}
2700
 
2701
/**
2702
 * This function just makes sure a user is logged out.
2703
 *
2704
 * @package    core_access
2705
 * @category   access
2706
 */
1326 ariadna 2707
function require_logout()
2708
{
1 efrain 2709
    global $USER, $DB;
2710
 
2711
    if (!isloggedin()) {
2712
        // This should not happen often, no need for hooks or events here.
2713
        \core\session\manager::terminate_current();
2714
        return;
2715
    }
2716
 
2717
    // Execute hooks before action.
2718
    $authplugins = array();
2719
    $authsequence = get_enabled_auth_plugins();
2720
    foreach ($authsequence as $authname) {
2721
        $authplugins[$authname] = get_auth_plugin($authname);
2722
        $authplugins[$authname]->prelogout_hook();
2723
    }
2724
 
2725
    // Store info that gets removed during logout.
2726
    $sid = session_id();
2727
    $event = \core\event\user_loggedout::create(
2728
        array(
2729
            'userid' => $USER->id,
2730
            'objectid' => $USER->id,
2731
            'other' => array('sessionid' => $sid),
2732
        )
2733
    );
1326 ariadna 2734
    if ($session = $DB->get_record('sessions', array('sid' => $sid))) {
1 efrain 2735
        $event->add_record_snapshot('sessions', $session);
2736
    }
2737
 
2738
    // Clone of $USER object to be used by auth plugins.
2739
    $user = fullclone($USER);
2740
 
2741
    // Delete session record and drop $_SESSION content.
2742
    \core\session\manager::terminate_current();
2743
 
2744
    // Trigger event AFTER action.
2745
    $event->trigger();
2746
 
2747
    // Hook to execute auth plugins redirection after event trigger.
2748
    foreach ($authplugins as $authplugin) {
2749
        $authplugin->postlogout_hook($user);
2750
    }
2751
}
2752
 
2753
/**
2754
 * Weaker version of require_login()
2755
 *
2756
 * This is a weaker version of {@link require_login()} which only requires login
2757
 * when called from within a course rather than the site page, unless
2758
 * the forcelogin option is turned on.
2759
 * @see require_login()
2760
 *
2761
 * @package    core_access
2762
 * @category   access
2763
 *
2764
 * @param mixed $courseorid The course object or id in question
2765
 * @param bool $autologinguest Allow autologin guests if that is wanted
2766
 * @param object $cm Course activity module if known
2767
 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2768
 *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2769
 *             in order to keep redirects working properly. MDL-14495
2770
 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2771
 * @return void
2772
 * @throws coding_exception
2773
 */
1326 ariadna 2774
function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false)
2775
{
1 efrain 2776
    global $CFG, $PAGE, $SITE;
2777
    $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
1326 ariadna 2778
        or (!is_object($courseorid) and $courseorid == SITEID));
1 efrain 2779
    if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
2780
        // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2781
        // db queries so this is not really a performance concern, however it is obviously
2782
        // better if you use get_fast_modinfo to get the cm before calling this.
2783
        if (is_object($courseorid)) {
2784
            $course = $courseorid;
2785
        } else {
1326 ariadna 2786
            $course = clone ($SITE);
1 efrain 2787
        }
2788
        $modinfo = get_fast_modinfo($course);
2789
        $cm = $modinfo->get_cm($cm->id);
2790
    }
2791
    if (!empty($CFG->forcelogin)) {
2792
        // Login required for both SITE and courses.
2793
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2794
    } else if ($issite && !empty($cm) and !$cm->uservisible) {
2795
        // Always login for hidden activities.
2796
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2797
    } else if (isloggedin() && !isguestuser()) {
2798
        // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
2799
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2800
    } else if ($issite) {
2801
        // Login for SITE not required.
2802
        // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
2803
        if (!empty($courseorid)) {
2804
            if (is_object($courseorid)) {
2805
                $course = $courseorid;
2806
            } else {
2807
                $course = clone $SITE;
2808
            }
2809
            if ($cm) {
2810
                if ($cm->course != $course->id) {
2811
                    throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
2812
                }
2813
                $PAGE->set_cm($cm, $course);
2814
                $PAGE->set_pagelayout('incourse');
2815
            } else {
2816
                $PAGE->set_course($course);
2817
            }
2818
        } else {
2819
            // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
2820
            $PAGE->set_course($PAGE->course);
2821
        }
2822
        // Do not update access time for webservice or ajax requests.
2823
        if (!WS_SERVER && !AJAX_SCRIPT) {
2824
            user_accesstime_log(SITEID);
2825
        }
2826
        return;
2827
    } else {
2828
        // Course login always required.
2829
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2830
    }
2831
}
2832
 
2833
/**
2834
 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
2835
 *
2836
 * @param  string $keyvalue the key value
2837
 * @param  string $script   unique script identifier
2838
 * @param  int $instance    instance id
2839
 * @return stdClass the key entry in the user_private_key table
2840
 * @since Moodle 3.2
2841
 * @throws moodle_exception
2842
 */
1326 ariadna 2843
function validate_user_key($keyvalue, $script, $instance)
2844
{
1 efrain 2845
    global $DB;
2846
 
2847
    if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
2848
        throw new \moodle_exception('invalidkey');
2849
    }
2850
 
2851
    if (!empty($key->validuntil) and $key->validuntil < time()) {
2852
        throw new \moodle_exception('expiredkey');
2853
    }
2854
 
2855
    if ($key->iprestriction) {
2856
        $remoteaddr = getremoteaddr(null);
2857
        if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
2858
            throw new \moodle_exception('ipmismatch');
2859
        }
2860
    }
2861
    return $key;
2862
}
2863
 
2864
/**
2865
 * Require key login. Function terminates with error if key not found or incorrect.
2866
 *
2867
 * @uses NO_MOODLE_COOKIES
2868
 * @uses PARAM_ALPHANUM
2869
 * @param string $script unique script identifier
2870
 * @param int $instance optional instance id
2871
 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
2872
 * @return int Instance ID
2873
 */
1326 ariadna 2874
function require_user_key_login($script, $instance = null, $keyvalue = null)
2875
{
1 efrain 2876
    global $DB;
2877
 
2878
    if (!NO_MOODLE_COOKIES) {
2879
        throw new \moodle_exception('sessioncookiesdisable');
2880
    }
2881
 
2882
    // Extra safety.
2883
    \core\session\manager::write_close();
2884
 
2885
    if (null === $keyvalue) {
2886
        $keyvalue = required_param('key', PARAM_ALPHANUM);
2887
    }
2888
 
2889
    $key = validate_user_key($keyvalue, $script, $instance);
2890
 
2891
    if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
2892
        throw new \moodle_exception('invaliduserid');
2893
    }
2894
 
2895
    core_user::require_active_user($user, true, true);
2896
 
2897
    // Emulate normal session.
2898
    enrol_check_plugins($user, false);
2899
    \core\session\manager::set_user($user);
2900
 
2901
    // Note we are not using normal login.
2902
    if (!defined('USER_KEY_LOGIN')) {
2903
        define('USER_KEY_LOGIN', true);
2904
    }
2905
 
2906
    // Return instance id - it might be empty.
2907
    return $key->instance;
2908
}
2909
 
2910
/**
2911
 * Creates a new private user access key.
2912
 *
2913
 * @param string $script unique target identifier
2914
 * @param int $userid
2915
 * @param int $instance optional instance id
2916
 * @param string $iprestriction optional ip restricted access
2917
 * @param int $validuntil key valid only until given data
2918
 * @return string access key value
2919
 */
1326 ariadna 2920
function create_user_key($script, $userid, $instance = null, $iprestriction = null, $validuntil = null)
2921
{
1 efrain 2922
    global $DB;
2923
 
2924
    $key = new stdClass();
2925
    $key->script        = $script;
2926
    $key->userid        = $userid;
2927
    $key->instance      = $instance;
2928
    $key->iprestriction = $iprestriction;
2929
    $key->validuntil    = $validuntil;
2930
    $key->timecreated   = time();
2931
 
2932
    // Something long and unique.
1326 ariadna 2933
    $key->value         = md5($userid . '_' . time() . random_string(40));
1 efrain 2934
    while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
2935
        // Must be unique.
1326 ariadna 2936
        $key->value     = md5($userid . '_' . time() . random_string(40));
1 efrain 2937
    }
2938
    $DB->insert_record('user_private_key', $key);
2939
    return $key->value;
2940
}
2941
 
2942
/**
2943
 * Delete the user's new private user access keys for a particular script.
2944
 *
2945
 * @param string $script unique target identifier
2946
 * @param int $userid
2947
 * @return void
2948
 */
1326 ariadna 2949
function delete_user_key($script, $userid)
2950
{
1 efrain 2951
    global $DB;
2952
    $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
2953
}
2954
 
2955
/**
2956
 * Gets a private user access key (and creates one if one doesn't exist).
2957
 *
2958
 * @param string $script unique target identifier
2959
 * @param int $userid
2960
 * @param int $instance optional instance id
2961
 * @param string $iprestriction optional ip restricted access
2962
 * @param int $validuntil key valid only until given date
2963
 * @return string access key value
2964
 */
1326 ariadna 2965
function get_user_key($script, $userid, $instance = null, $iprestriction = null, $validuntil = null)
2966
{
1 efrain 2967
    global $DB;
2968
 
1326 ariadna 2969
    if ($key = $DB->get_record('user_private_key', array(
2970
        'script' => $script,
2971
        'userid' => $userid,
2972
        'instance' => $instance,
2973
        'iprestriction' => $iprestriction,
2974
        'validuntil' => $validuntil
2975
    ))) {
1 efrain 2976
        return $key->value;
2977
    } else {
2978
        return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
2979
    }
2980
}
2981
 
2982
 
2983
/**
2984
 * Modify the user table by setting the currently logged in user's last login to now.
2985
 *
2986
 * @return bool Always returns true
2987
 */
1326 ariadna 2988
function update_user_login_times()
2989
{
1 efrain 2990
    global $USER, $DB, $SESSION;
2991
 
2992
    if (isguestuser()) {
2993
        // Do not update guest access times/ips for performance.
2994
        return true;
2995
    }
2996
 
2997
    if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) {
2998
        // Do not update user login time when using user key login.
2999
        return true;
3000
    }
3001
 
3002
    $now = time();
3003
 
3004
    $user = new stdClass();
3005
    $user->id = $USER->id;
3006
 
3007
    // Make sure all users that logged in have some firstaccess.
3008
    if ($USER->firstaccess == 0) {
3009
        $USER->firstaccess = $user->firstaccess = $now;
3010
    }
3011
 
3012
    // Store the previous current as lastlogin.
3013
    $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3014
 
3015
    $USER->currentlogin = $user->currentlogin = $now;
3016
 
3017
    // Function user_accesstime_log() may not update immediately, better do it here.
3018
    $USER->lastaccess = $user->lastaccess = $now;
3019
    $SESSION->userpreviousip = $USER->lastip;
3020
    $USER->lastip = $user->lastip = getremoteaddr();
3021
 
3022
    // Note: do not call user_update_user() here because this is part of the login process,
3023
    //       the login event means that these fields were updated.
3024
    $DB->update_record('user', $user);
3025
    return true;
3026
}
3027
 
3028
/**
3029
 * Determines if a user has completed setting up their account.
3030
 *
3031
 * The lax mode (with $strict = false) has been introduced for special cases
3032
 * only where we want to skip certain checks intentionally. This is valid in
3033
 * certain mnet or ajax scenarios when the user cannot / should not be
3034
 * redirected to edit their profile. In most cases, you should perform the
3035
 * strict check.
3036
 *
3037
 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3038
 * @param bool $strict Be more strict and assert id and custom profile fields set, too
3039
 * @return bool
3040
 */
1326 ariadna 3041
function user_not_fully_set_up($user, $strict = true)
3042
{
1 efrain 3043
    global $CFG, $SESSION, $USER;
1326 ariadna 3044
    require_once($CFG->dirroot . '/user/profile/lib.php');
1 efrain 3045
 
3046
    // If the user is setup then store this in the session to avoid re-checking.
3047
    // Some edge cases are when the users email starts to bounce or the
3048
    // configuration for custom fields has changed while they are logged in so
3049
    // we re-check this fully every hour for the rare cases it has changed.
1326 ariadna 3050
    if (
3051
        isset($USER->id) && isset($user->id) && $USER->id === $user->id &&
3052
        isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS
3053
    ) {
1 efrain 3054
        return false;
3055
    }
3056
 
3057
    if (isguestuser($user)) {
3058
        return false;
3059
    }
3060
 
3061
    if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
3062
        return true;
3063
    }
3064
 
3065
    if ($strict) {
3066
        if (empty($user->id)) {
3067
            // Strict mode can be used with existing accounts only.
3068
            return true;
3069
        }
3070
        if (!profile_has_required_custom_fields_set($user->id)) {
3071
            return true;
3072
        }
3073
        if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) {
3074
            $SESSION->fullysetupstrict = time();
3075
        }
3076
    }
3077
 
3078
    return false;
3079
}
3080
 
3081
/**
3082
 * Check whether the user has exceeded the bounce threshold
3083
 *
3084
 * @param stdClass $user A {@link $USER} object
3085
 * @return bool true => User has exceeded bounce threshold
3086
 */
1326 ariadna 3087
function over_bounce_threshold($user)
3088
{
1 efrain 3089
    global $CFG, $DB;
3090
 
3091
    if (empty($CFG->handlebounces)) {
3092
        return false;
3093
    }
3094
 
3095
    if (empty($user->id)) {
3096
        // No real (DB) user, nothing to do here.
3097
        return false;
3098
    }
3099
 
3100
    // Set sensible defaults.
3101
    if (empty($CFG->minbounces)) {
3102
        $CFG->minbounces = 10;
3103
    }
3104
    if (empty($CFG->bounceratio)) {
3105
        $CFG->bounceratio = .20;
3106
    }
3107
    $bouncecount = 0;
3108
    $sendcount = 0;
1326 ariadna 3109
    if ($bounce = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
1 efrain 3110
        $bouncecount = $bounce->value;
3111
    }
3112
    if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3113
        $sendcount = $send->value;
3114
    }
1326 ariadna 3115
    return ($bouncecount >= $CFG->minbounces && $bouncecount / $sendcount >= $CFG->bounceratio);
1 efrain 3116
}
3117
 
3118
/**
3119
 * Used to increment or reset email sent count
3120
 *
3121
 * @param stdClass $user object containing an id
3122
 * @param bool $reset will reset the count to 0
3123
 * @return void
3124
 */
1326 ariadna 3125
function set_send_count($user, $reset = false)
3126
{
1 efrain 3127
    global $DB;
3128
 
3129
    if (empty($user->id)) {
3130
        // No real (DB) user, nothing to do here.
3131
        return;
3132
    }
3133
 
3134
    if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
1326 ariadna 3135
        $pref->value = (!empty($reset)) ? 0 : $pref->value + 1;
1 efrain 3136
        $DB->update_record('user_preferences', $pref);
3137
    } else if (!empty($reset)) {
3138
        // If it's not there and we're resetting, don't bother. Make a new one.
3139
        $pref = new stdClass();
3140
        $pref->name   = 'email_send_count';
3141
        $pref->value  = 1;
3142
        $pref->userid = $user->id;
3143
        $DB->insert_record('user_preferences', $pref, false);
3144
    }
3145
}
3146
 
3147
/**
3148
 * Increment or reset user's email bounce count
3149
 *
3150
 * @param stdClass $user object containing an id
3151
 * @param bool $reset will reset the count to 0
3152
 */
1326 ariadna 3153
function set_bounce_count($user, $reset = false)
3154
{
1 efrain 3155
    global $DB;
3156
 
3157
    if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
1326 ariadna 3158
        $pref->value = (!empty($reset)) ? 0 : $pref->value + 1;
1 efrain 3159
        $DB->update_record('user_preferences', $pref);
3160
    } else if (!empty($reset)) {
3161
        // If it's not there and we're resetting, don't bother. Make a new one.
3162
        $pref = new stdClass();
3163
        $pref->name   = 'email_bounce_count';
3164
        $pref->value  = 1;
3165
        $pref->userid = $user->id;
3166
        $DB->insert_record('user_preferences', $pref, false);
3167
    }
3168
}
3169
 
3170
/**
3171
 * Determines if the logged in user is currently moving an activity
3172
 *
3173
 * @param int $courseid The id of the course being tested
3174
 * @return bool
3175
 */
1326 ariadna 3176
function ismoving($courseid)
3177
{
1 efrain 3178
    global $USER;
3179
 
3180
    if (!empty($USER->activitycopy)) {
3181
        return ($USER->activitycopycourse == $courseid);
3182
    }
3183
    return false;
3184
}
3185
 
3186
/**
3187
 * Returns a persons full name
3188
 *
3189
 * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3190
 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3191
 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3192
 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3193
 *
3194
 * @param stdClass $user A {@link $USER} object to get full name of.
3195
 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3196
 * @return string
3197
 */
1326 ariadna 3198
function fullname($user, $override = false)
3199
{
1 efrain 3200
    // Note: We do not intend to deprecate this function any time soon as it is too widely used at this time.
3201
    // Uses of it should be updated to use the new API and pass updated arguments.
3202
 
3203
    // Return an empty string if there is no user.
3204
    if (empty($user)) {
3205
        return '';
3206
    }
3207
 
3208
    $options = ['override' => $override];
3209
    return core_user::get_fullname($user, null, $options);
3210
}
3211
 
3212
/**
3213
 * Reduces lines of duplicated code for getting user name fields.
3214
 *
3215
 * See also {@link user_picture::unalias()}
3216
 *
3217
 * @param object $addtoobject Object to add user name fields to.
3218
 * @param object $secondobject Object that contains user name field information.
3219
 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3220
 * @param array $additionalfields Additional fields to be matched with data in the second object.
3221
 * The key can be set to the user table field name.
3222
 * @return object User name fields.
3223
 */
1326 ariadna 3224
function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null)
3225
{
1 efrain 3226
    $fields = [];
3227
    foreach (\core_user\fields::get_name_fields() as $field) {
3228
        $fields[$field] = $prefix . $field;
3229
    }
3230
    if ($additionalfields) {
3231
        // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3232
        // the key is a number and then sets the key to the array value.
3233
        foreach ($additionalfields as $key => $value) {
3234
            if (is_numeric($key)) {
3235
                $additionalfields[$value] = $prefix . $value;
3236
                unset($additionalfields[$key]);
3237
            } else {
3238
                $additionalfields[$key] = $prefix . $value;
3239
            }
3240
        }
3241
        $fields = array_merge($fields, $additionalfields);
3242
    }
3243
    foreach ($fields as $key => $field) {
3244
        // Important that we have all of the user name fields present in the object that we are sending back.
3245
        $addtoobject->$key = '';
3246
        if (isset($secondobject->$field)) {
3247
            $addtoobject->$key = $secondobject->$field;
3248
        }
3249
    }
3250
    return $addtoobject;
3251
}
3252
 
3253
/**
3254
 * Returns an array of values in order of occurance in a provided string.
3255
 * The key in the result is the character postion in the string.
3256
 *
3257
 * @param array $values Values to be found in the string format
3258
 * @param string $stringformat The string which may contain values being searched for.
3259
 * @return array An array of values in order according to placement in the string format.
3260
 */
1326 ariadna 3261
function order_in_string($values, $stringformat)
3262
{
1 efrain 3263
    $valuearray = array();
3264
    foreach ($values as $value) {
3265
        $pattern = "/$value\b/";
3266
        // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3267
        if (preg_match($pattern, $stringformat)) {
3268
            $replacement = "thing";
3269
            // Replace the value with something more unique to ensure we get the right position when using strpos().
3270
            $newformat = preg_replace($pattern, $replacement, $stringformat);
3271
            $position = strpos($newformat, $replacement);
3272
            $valuearray[$position] = $value;
3273
        }
3274
    }
3275
    ksort($valuearray);
3276
    return $valuearray;
3277
}
3278
 
3279
/**
3280
 * Returns whether a given authentication plugin exists.
3281
 *
3282
 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3283
 * @return boolean Whether the plugin is available.
3284
 */
1326 ariadna 3285
function exists_auth_plugin($auth)
3286
{
1 efrain 3287
    global $CFG;
3288
 
3289
    if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3290
        return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3291
    }
3292
    return false;
3293
}
3294
 
3295
/**
3296
 * Checks if a given plugin is in the list of enabled authentication plugins.
3297
 *
3298
 * @param string $auth Authentication plugin.
3299
 * @return boolean Whether the plugin is enabled.
3300
 */
1326 ariadna 3301
function is_enabled_auth($auth)
3302
{
1 efrain 3303
    if (empty($auth)) {
3304
        return false;
3305
    }
3306
 
3307
    $enabled = get_enabled_auth_plugins();
3308
 
3309
    return in_array($auth, $enabled);
3310
}
3311
 
3312
/**
3313
 * Returns an authentication plugin instance.
3314
 *
3315
 * @param string $auth name of authentication plugin
3316
 * @return auth_plugin_base An instance of the required authentication plugin.
3317
 */
1326 ariadna 3318
function get_auth_plugin($auth)
3319
{
1 efrain 3320
    global $CFG;
3321
 
3322
    // Check the plugin exists first.
3323
    if (! exists_auth_plugin($auth)) {
3324
        throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth);
3325
    }
3326
 
3327
    // Return auth plugin instance.
3328
    require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3329
    $class = "auth_plugin_$auth";
3330
    return new $class;
3331
}
3332
 
3333
/**
3334
 * Returns array of active auth plugins.
3335
 *
3336
 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3337
 * @return array
3338
 */
1326 ariadna 3339
function get_enabled_auth_plugins($fix = false)
3340
{
1 efrain 3341
    global $CFG;
3342
 
3343
    $default = array('manual', 'nologin');
3344
 
3345
    if (empty($CFG->auth)) {
3346
        $auths = array();
3347
    } else {
3348
        $auths = explode(',', $CFG->auth);
3349
    }
3350
 
3351
    $auths = array_unique($auths);
3352
    $oldauthconfig = implode(',', $auths);
3353
    foreach ($auths as $k => $authname) {
3354
        if (in_array($authname, $default)) {
3355
            // The manual and nologin plugin never need to be stored.
3356
            unset($auths[$k]);
3357
        } else if (!exists_auth_plugin($authname)) {
3358
            debugging(get_string('authpluginnotfound', 'debug', $authname));
3359
            unset($auths[$k]);
3360
        }
3361
    }
3362
 
3363
    // Ideally only explicit interaction from a human admin should trigger a
3364
    // change in auth config, see MDL-70424 for details.
3365
    if ($fix) {
3366
        $newconfig = implode(',', $auths);
3367
        if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3368
            add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
3369
            set_config('auth', $newconfig);
3370
        }
3371
    }
3372
 
3373
    return (array_merge($default, $auths));
3374
}
3375
 
3376
/**
3377
 * Returns true if an internal authentication method is being used.
3378
 * if method not specified then, global default is assumed
3379
 *
3380
 * @param string $auth Form of authentication required
3381
 * @return bool
3382
 */
1326 ariadna 3383
function is_internal_auth($auth)
3384
{
1 efrain 3385
    // Throws error if bad $auth.
3386
    $authplugin = get_auth_plugin($auth);
3387
    return $authplugin->is_internal();
3388
}
3389
 
3390
/**
3391
 * Returns true if the user is a 'restored' one.
3392
 *
3393
 * Used in the login process to inform the user and allow him/her to reset the password
3394
 *
3395
 * @param string $username username to be checked
3396
 * @return bool
3397
 */
1326 ariadna 3398
function is_restored_user($username)
3399
{
1 efrain 3400
    global $CFG, $DB;
3401
 
3402
    return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3403
}
3404
 
3405
/**
3406
 * Returns an array of user fields
3407
 *
3408
 * @return array User field/column names
3409
 */
1326 ariadna 3410
function get_user_fieldnames()
3411
{
1 efrain 3412
    global $DB;
3413
 
3414
    $fieldarray = $DB->get_columns('user');
3415
    unset($fieldarray['id']);
3416
    $fieldarray = array_keys($fieldarray);
3417
 
3418
    return $fieldarray;
3419
}
3420
 
3421
/**
3422
 * Returns the string of the language for the new user.
3423
 *
3424
 * @return string language for the new user
3425
 */
1326 ariadna 3426
function get_newuser_language()
3427
{
1 efrain 3428
    global $CFG, $SESSION;
3429
    return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
3430
}
3431
 
3432
/**
3433
 * Creates a bare-bones user record
3434
 *
3435
 * @todo Outline auth types and provide code example
3436
 *
3437
 * @param string $username New user's username to add to record
3438
 * @param string $password New user's password to add to record
3439
 * @param string $auth Form of authentication required
3440
 * @return stdClass A complete user object
3441
 */
1326 ariadna 3442
function create_user_record($username, $password, $auth = 'manual')
3443
{
1 efrain 3444
    global $CFG, $DB, $SESSION;
1326 ariadna 3445
    require_once($CFG->dirroot . '/user/profile/lib.php');
3446
    require_once($CFG->dirroot . '/user/lib.php');
1 efrain 3447
 
3448
    // Just in case check text case.
3449
    $username = trim(core_text::strtolower($username));
3450
 
3451
    $authplugin = get_auth_plugin($auth);
3452
    $customfields = $authplugin->get_custom_user_profile_fields();
3453
    $newuser = new stdClass();
3454
    if ($newinfo = $authplugin->get_userinfo($username)) {
3455
        $newinfo = truncate_userinfo($newinfo);
3456
        foreach ($newinfo as $key => $value) {
3457
            if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3458
                $newuser->$key = $value;
3459
            }
3460
        }
3461
    }
3462
 
3463
    if (!empty($newuser->email)) {
3464
        if (email_is_not_allowed($newuser->email)) {
3465
            unset($newuser->email);
3466
        }
3467
    }
3468
 
3469
    $newuser->auth = $auth;
3470
    $newuser->username = $username;
3471
 
3472
    // Fix for MDL-8480
3473
    // user CFG lang for user if $newuser->lang is empty
3474
    // or $user->lang is not an installed language.
3475
    if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3476
        $newuser->lang = get_newuser_language();
3477
    }
3478
    $newuser->confirmed = 1;
3479
    $newuser->lastip = getremoteaddr();
3480
    $newuser->timecreated = time();
3481
    $newuser->timemodified = $newuser->timecreated;
3482
    $newuser->mnethostid = $CFG->mnet_localhost_id;
3483
 
3484
    $newuser->id = user_create_user($newuser, false, false);
3485
 
3486
    // Save user profile data.
3487
    profile_save_data($newuser);
3488
 
3489
    $user = get_complete_user_data('id', $newuser->id);
1326 ariadna 3490
    if (!empty($CFG->{'auth_' . $newuser->auth . '_forcechangepassword'})) {
1 efrain 3491
        set_user_preference('auth_forcepasswordchange', 1, $user);
3492
    }
3493
    // Set the password.
3494
    update_internal_user_password($user, $password);
3495
 
3496
    // Trigger event.
3497
    \core\event\user_created::create_from_userid($newuser->id)->trigger();
3498
 
3499
    return $user;
3500
}
3501
 
3502
/**
3503
 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3504
 *
3505
 * @param string $username user's username to update the record
3506
 * @return stdClass A complete user object
3507
 */
1326 ariadna 3508
function update_user_record($username)
3509
{
1 efrain 3510
    global $DB, $CFG;
3511
    // Just in case check text case.
3512
    $username = trim(core_text::strtolower($username));
3513
 
3514
    $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
3515
    return update_user_record_by_id($oldinfo->id);
3516
}
3517
 
3518
/**
3519
 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3520
 *
3521
 * @param int $id user id
3522
 * @return stdClass A complete user object
3523
 */
1326 ariadna 3524
function update_user_record_by_id($id)
3525
{
1 efrain 3526
    global $DB, $CFG;
1326 ariadna 3527
    require_once($CFG->dirroot . "/user/profile/lib.php");
3528
    require_once($CFG->dirroot . '/user/lib.php');
1 efrain 3529
 
3530
    $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
3531
    $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
3532
 
3533
    $newuser = array();
3534
    $userauth = get_auth_plugin($oldinfo->auth);
3535
 
3536
    if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
3537
        $newinfo = truncate_userinfo($newinfo);
3538
        $customfields = $userauth->get_custom_user_profile_fields();
3539
 
3540
        foreach ($newinfo as $key => $value) {
3541
            $iscustom = in_array($key, $customfields);
3542
            if (!$iscustom) {
3543
                $key = strtolower($key);
3544
            }
3545
            if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
1326 ariadna 3546
                or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted'
3547
            ) {
1 efrain 3548
                // Unknown or must not be changed.
3549
                continue;
3550
            }
3551
            if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
3552
                continue;
3553
            }
3554
            $confval = $userauth->config->{'field_updatelocal_' . $key};
3555
            $lockval = $userauth->config->{'field_lock_' . $key};
3556
            if ($confval === 'onlogin') {
3557
                // MDL-4207 Don't overwrite modified user profile values with
3558
                // empty LDAP values when 'unlocked if empty' is set. The purpose
3559
                // of the setting 'unlocked if empty' is to allow the user to fill
3560
                // in a value for the selected field _if LDAP is giving
3561
                // nothing_ for this field. Thus it makes sense to let this value
3562
                // stand in until LDAP is giving a value for this field.
3563
                if (!(empty($value) && $lockval === 'unlockedifempty')) {
3564
                    if ($iscustom || (in_array($key, $userauth->userfields) &&
1326 ariadna 3565
                        ((string)$oldinfo->$key !== (string)$value))) {
1 efrain 3566
                        $newuser[$key] = (string)$value;
3567
                    }
3568
                }
3569
            }
3570
        }
3571
        if ($newuser) {
3572
            $newuser['id'] = $oldinfo->id;
3573
            $newuser['timemodified'] = time();
3574
            user_update_user((object) $newuser, false, false);
3575
 
3576
            // Save user profile data.
3577
            profile_save_data((object) $newuser);
3578
 
3579
            // Trigger event.
3580
            \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
3581
        }
3582
    }
3583
 
3584
    return get_complete_user_data('id', $oldinfo->id);
3585
}
3586
 
3587
/**
3588
 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
3589
 *
3590
 * @param array $info Array of user properties to truncate if needed
3591
 * @return array The now truncated information that was passed in
3592
 */
1326 ariadna 3593
function truncate_userinfo(array $info)
3594
{
1 efrain 3595
    // Define the limits.
3596
    $limit = array(
3597
        'username'    => 100,
3598
        'idnumber'    => 255,
3599
        'firstname'   => 100,
3600
        'lastname'    => 100,
3601
        'email'       => 100,
3602
        'phone1'      =>  20,
3603
        'phone2'      =>  20,
3604
        'institution' => 255,
3605
        'department'  => 255,
3606
        'address'     => 255,
3607
        'city'        => 120,
3608
        'country'     =>   2,
3609
    );
3610
 
3611
    // Apply where needed.
3612
    foreach (array_keys($info) as $key) {
3613
        if (!empty($limit[$key])) {
3614
            $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
3615
        }
3616
    }
3617
 
3618
    return $info;
3619
}
3620
 
3621
/**
3622
 * Marks user deleted in internal user database and notifies the auth plugin.
3623
 * Also unenrols user from all roles and does other cleanup.
3624
 *
3625
 * Any plugin that needs to purge user data should register the 'user_deleted' event.
3626
 *
3627
 * @param stdClass $user full user object before delete
3628
 * @return boolean success
3629
 * @throws coding_exception if invalid $user parameter detected
3630
 */
1326 ariadna 3631
function delete_user(stdClass $user)
3632
{
1 efrain 3633
    global $CFG, $DB, $SESSION;
1326 ariadna 3634
    require_once($CFG->libdir . '/grouplib.php');
3635
    require_once($CFG->libdir . '/gradelib.php');
3636
    require_once($CFG->dirroot . '/message/lib.php');
3637
    require_once($CFG->dirroot . '/user/lib.php');
1 efrain 3638
 
3639
    // Make sure nobody sends bogus record type as parameter.
3640
    if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
3641
        throw new coding_exception('Invalid $user parameter in delete_user() detected');
3642
    }
3643
 
3644
    // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
3645
    if (!$user = $DB->get_record('user', array('id' => $user->id))) {
3646
        debugging('Attempt to delete unknown user account.');
3647
        return false;
3648
    }
3649
 
3650
    // There must be always exactly one guest record, originally the guest account was identified by username only,
3651
    // now we use $CFG->siteguest for performance reasons.
3652
    if ($user->username === 'guest' or isguestuser($user)) {
3653
        debugging('Guest user account can not be deleted.');
3654
        return false;
3655
    }
3656
 
3657
    // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
3658
    // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
3659
    if ($user->auth === 'manual' and is_siteadmin($user)) {
3660
        debugging('Local administrator accounts can not be deleted.');
3661
        return false;
3662
    }
3663
    // Allow plugins to use this user object before we completely delete it.
3664
    if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
3665
        foreach ($pluginsfunction as $plugintype => $plugins) {
3666
            foreach ($plugins as $pluginfunction) {
3667
                $pluginfunction($user);
3668
            }
3669
        }
3670
    }
3671
 
3672
    // Dispatch the hook for pre user update actions.
3673
    $hook = new \core_user\hook\before_user_deleted(
3674
        user: $user,
3675
    );
3676
    di::get(hook\manager::class)->dispatch($hook);
3677
 
3678
    // Keep user record before updating it, as we have to pass this to user_deleted event.
3679
    $olduser = clone $user;
3680
 
3681
    // Keep a copy of user context, we need it for event.
3682
    $usercontext = context_user::instance($user->id);
3683
 
3684
    // Delete all grades - backup is kept in grade_grades_history table.
3685
    grade_user_delete($user->id);
3686
 
3687
    // TODO: remove from cohorts using standard API here.
3688
 
3689
    // Remove user tags.
3690
    core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
3691
 
3692
    // Unconditionally unenrol from all courses.
3693
    enrol_user_delete($user);
3694
 
3695
    // Unenrol from all roles in all contexts.
3696
    // This might be slow but it is really needed - modules might do some extra cleanup!
3697
    role_unassign_all(array('userid' => $user->id));
3698
 
3699
    // Notify the competency subsystem.
3700
    \core_competency\api::hook_user_deleted($user->id);
3701
 
3702
    // Now do a brute force cleanup.
3703
 
3704
    // Delete all user events and subscription events.
3705
    $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
3706
 
3707
    // Now, delete all calendar subscription from the user.
3708
    $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
3709
 
3710
    // Remove from all cohorts.
3711
    $DB->delete_records('cohort_members', array('userid' => $user->id));
3712
 
3713
    // Remove from all groups.
3714
    $DB->delete_records('groups_members', array('userid' => $user->id));
3715
 
3716
    // Brute force unenrol from all courses.
3717
    $DB->delete_records('user_enrolments', array('userid' => $user->id));
3718
 
3719
    // Purge user preferences.
3720
    $DB->delete_records('user_preferences', array('userid' => $user->id));
3721
 
3722
    // Purge user extra profile info.
3723
    $DB->delete_records('user_info_data', array('userid' => $user->id));
3724
 
3725
    // Purge log of previous password hashes.
3726
    $DB->delete_records('user_password_history', array('userid' => $user->id));
3727
 
3728
    // Last course access not necessary either.
3729
    $DB->delete_records('user_lastaccess', array('userid' => $user->id));
3730
    // Remove all user tokens.
3731
    $DB->delete_records('external_tokens', array('userid' => $user->id));
3732
 
3733
    // Unauthorise the user for all services.
3734
    $DB->delete_records('external_services_users', array('userid' => $user->id));
3735
 
3736
    // Remove users private keys.
3737
    $DB->delete_records('user_private_key', array('userid' => $user->id));
3738
 
3739
    // Remove users customised pages.
3740
    $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
3741
 
3742
    // Remove user's oauth2 refresh tokens, if present.
3743
    $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
3744
 
3745
    // Delete user from $SESSION->bulk_users.
3746
    if (isset($SESSION->bulk_users[$user->id])) {
3747
        unset($SESSION->bulk_users[$user->id]);
3748
    }
3749
 
3750
    // Force logout - may fail if file based sessions used, sorry.
3751
    \core\session\manager::kill_user_sessions($user->id);
3752
 
3753
    // Generate username from email address, or a fake email.
3754
    $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
3755
 
3756
    $deltime = time();
3757
 
3758
    // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
3759
    $delnameprefix = clean_param($delemail, PARAM_USERNAME);
3760
    $delnamesuffix = $deltime;
3761
    $delnamesuffixlength = 10;
3762
    do {
3763
        // Workaround for bulk deletes of users with the same email address.
3764
        $delname = sprintf(
3765
            "%s.%10d",
3766
            core_text::substr(
3767
                $delnameprefix,
3768
                0,
3769
                // 100 Character maximum, with a '.' character, and a 10-digit timestamp.
3770
                100 - 1 - $delnamesuffixlength,
3771
            ),
3772
            $delnamesuffix,
3773
        );
3774
        $delnamesuffix++;
3775
 
3776
        // No need to use mnethostid here.
3777
    } while ($DB->record_exists('user', ['username' => $delname]));
3778
 
3779
    // Mark internal user record as "deleted".
3780
    $updateuser = new stdClass();
3781
    $updateuser->id           = $user->id;
3782
    $updateuser->deleted      = 1;
3783
    $updateuser->username     = $delname;            // Remember it just in case.
1326 ariadna 3784
    $updateuser->email        = md5($user->username); // Store hash of username, useful importing/restoring users.
1 efrain 3785
    $updateuser->idnumber     = '';                  // Clear this field to free it up.
3786
    $updateuser->picture      = 0;
3787
    $updateuser->timemodified = $deltime;
3788
 
3789
    // Don't trigger update event, as user is being deleted.
3790
    user_update_user($updateuser, false, false);
3791
 
3792
    // Delete all content associated with the user context, but not the context itself.
3793
    $usercontext->delete_content();
3794
 
3795
    // Delete any search data.
3796
    \core_search\manager::context_deleted($usercontext);
3797
 
3798
    // Any plugin that needs to cleanup should register this event.
3799
    // Trigger event.
3800
    $event = \core\event\user_deleted::create(
1326 ariadna 3801
        array(
3802
            'objectid' => $user->id,
3803
            'relateduserid' => $user->id,
3804
            'context' => $usercontext,
3805
            'other' => array(
3806
                'username' => $user->username,
3807
                'email' => $user->email,
3808
                'idnumber' => $user->idnumber,
3809
                'picture' => $user->picture,
3810
                'mnethostid' => $user->mnethostid
3811
            )
3812
        )
3813
    );
1 efrain 3814
    $event->add_record_snapshot('user', $olduser);
3815
    $event->trigger();
3816
 
3817
    // We will update the user's timemodified, as it will be passed to the user_deleted event, which
3818
    // should know about this updated property persisted to the user's table.
3819
    $user->timemodified = $updateuser->timemodified;
3820
 
3821
    // Notify auth plugin - do not block the delete even when plugin fails.
3822
    $authplugin = get_auth_plugin($user->auth);
3823
    $authplugin->user_delete($user);
3824
 
3825
    return true;
3826
}
3827
 
3828
/**
3829
 * Retrieve the guest user object.
3830
 *
3831
 * @return stdClass A {@link $USER} object
3832
 */
1326 ariadna 3833
function guest_user()
3834
{
1 efrain 3835
    global $CFG, $DB;
3836
 
3837
    if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
3838
        $newuser->confirmed = 1;
3839
        $newuser->lang = get_newuser_language();
3840
        $newuser->lastip = getremoteaddr();
3841
    }
3842
 
3843
    return $newuser;
3844
}
3845
 
3846
/**
3847
 * Authenticates a user against the chosen authentication mechanism
3848
 *
3849
 * Given a username and password, this function looks them
3850
 * up using the currently selected authentication mechanism,
3851
 * and if the authentication is successful, it returns a
3852
 * valid $user object from the 'user' table.
3853
 *
3854
 * Uses auth_ functions from the currently active auth module
3855
 *
3856
 * After authenticate_user_login() returns success, you will need to
3857
 * log that the user has logged in, and call complete_user_login() to set
3858
 * the session up.
3859
 *
3860
 * Note: this function works only with non-mnet accounts!
3861
 *
3862
 * @param string $username  User's username (or also email if $CFG->authloginviaemail enabled)
3863
 * @param string $password  User's password
3864
 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
3865
 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
3866
 * @param string|bool $logintoken If this is set to a string it is validated against the login token for the session.
3867
 * @param string|bool $loginrecaptcha If this is set to a string it is validated against Google reCaptcha.
3868
 * @return stdClass|false A {@link $USER} object or false if error
3869
 */
3870
function authenticate_user_login(
3871
    $username,
3872
    $password,
3873
    $ignorelockout = false,
3874
    &$failurereason = null,
3875
    $logintoken = false,
3876
    string|bool $loginrecaptcha = false,
3877
) {
3878
    global $CFG, $DB, $PAGE, $SESSION;
3879
    require_once("$CFG->libdir/authlib.php");
3880
 
3881
    if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
3882
        // we have found the user
3883
 
3884
    } else if (!empty($CFG->authloginviaemail)) {
3885
        if ($email = clean_param($username, PARAM_EMAIL)) {
3886
            $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
3887
            $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
3888
            $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
3889
            if (count($users) === 1) {
3890
                // Use email for login only if unique.
3891
                $user = reset($users);
3892
                $user = get_complete_user_data('id', $user->id);
3893
                $username = $user->username;
3894
            }
3895
            unset($users);
3896
        }
3897
    }
3898
 
3899
    // Make sure this request came from the login form.
3900
    if (!\core\session\manager::validate_login_token($logintoken)) {
3901
        $failurereason = AUTH_LOGIN_FAILED;
3902
 
3903
        // Trigger login failed event (specifying the ID of the found user, if available).
3904
        \core\event\user_login_failed::create([
3905
            'userid' => ($user->id ?? 0),
3906
            'other' => [
3907
                'username' => $username,
3908
                'reason' => $failurereason,
3909
            ],
3910
        ])->trigger();
3911
 
1326 ariadna 3912
        error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Invalid Login Token:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 3913
        return false;
3914
    }
3915
 
3916
    // Login reCaptcha.
3917
    if (login_captcha_enabled() && !validate_login_captcha($loginrecaptcha)) {
3918
        $failurereason = AUTH_LOGIN_FAILED_RECAPTCHA;
3919
        // Trigger login failed event (specifying the ID of the found user, if available).
3920
        \core\event\user_login_failed::create([
3921
            'userid' => ($user->id ?? 0),
3922
            'other' => [
3923
                'username' => $username,
3924
                'reason' => $failurereason,
3925
            ],
3926
        ])->trigger();
3927
        return false;
3928
    }
3929
 
3930
    $authsenabled = get_enabled_auth_plugins();
3931
 
3932
    if ($user) {
3933
        // Use manual if auth not set.
3934
        $auth = empty($user->auth) ? 'manual' : $user->auth;
3935
 
3936
        if (in_array($user->auth, $authsenabled)) {
3937
            $authplugin = get_auth_plugin($user->auth);
3938
            $authplugin->pre_user_login_hook($user);
3939
        }
3940
 
3941
        if (!empty($user->suspended)) {
3942
            $failurereason = AUTH_LOGIN_SUSPENDED;
3943
 
3944
            // Trigger login failed event.
1326 ariadna 3945
            $event = \core\event\user_login_failed::create(array(
3946
                'userid' => $user->id,
3947
                'other' => array('username' => $username, 'reason' => $failurereason)
3948
            ));
1 efrain 3949
            $event->trigger();
1326 ariadna 3950
            error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Suspended Login:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 3951
            return false;
3952
        }
1326 ariadna 3953
        if ($auth == 'nologin' or !is_enabled_auth($auth)) {
1 efrain 3954
            // Legacy way to suspend user.
3955
            $failurereason = AUTH_LOGIN_SUSPENDED;
3956
 
3957
            // Trigger login failed event.
1326 ariadna 3958
            $event = \core\event\user_login_failed::create(array(
3959
                'userid' => $user->id,
3960
                'other' => array('username' => $username, 'reason' => $failurereason)
3961
            ));
1 efrain 3962
            $event->trigger();
1326 ariadna 3963
            error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Disabled Login:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 3964
            return false;
3965
        }
3966
        $auths = array($auth);
3967
    } else {
3968
        // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
3969
        if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
3970
            $failurereason = AUTH_LOGIN_NOUSER;
3971
 
3972
            // Trigger login failed event.
1326 ariadna 3973
            $event = \core\event\user_login_failed::create(array('other' => array(
3974
                'username' => $username,
3975
                'reason' => $failurereason
3976
            )));
1 efrain 3977
            $event->trigger();
1326 ariadna 3978
            error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Deleted Login:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 3979
            return false;
3980
        }
3981
 
3982
        // User does not exist.
3983
        $auths = $authsenabled;
3984
        $user = new stdClass();
3985
        $user->id = 0;
3986
    }
3987
 
3988
    if ($ignorelockout) {
3989
        // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
3990
        // or this function is called from a SSO script.
3991
    } else if ($user->id) {
3992
        // Verify login lockout after other ways that may prevent user login.
3993
        if (login_is_lockedout($user)) {
3994
            $failurereason = AUTH_LOGIN_LOCKOUT;
3995
 
3996
            // Trigger login failed event.
1326 ariadna 3997
            $event = \core\event\user_login_failed::create(array(
3998
                'userid' => $user->id,
3999
                'other' => array('username' => $username, 'reason' => $failurereason)
4000
            ));
1 efrain 4001
            $event->trigger();
4002
 
1326 ariadna 4003
            error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Login lockout:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 4004
            $SESSION->loginerrormsg = get_string('accountlocked', 'admin');
4005
 
4006
            return false;
4007
        }
4008
    } else {
4009
        // We can not lockout non-existing accounts.
4010
    }
4011
 
4012
    foreach ($auths as $auth) {
4013
        $authplugin = get_auth_plugin($auth);
4014
 
4015
        // On auth fail fall through to the next plugin.
4016
        if (!$authplugin->user_login($username, $password)) {
4017
            continue;
4018
        }
4019
 
4020
        // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
4021
        if (!empty($CFG->passwordpolicycheckonlogin)) {
4022
            $errmsg = '';
4023
            $passed = check_password_policy($password, $errmsg, $user);
4024
            if (!$passed) {
4025
                // First trigger event for failure.
4026
                $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
4027
                $failedevent->trigger();
4028
 
4029
                // If able to change password, set flag and move on.
4030
                if ($authplugin->can_change_password()) {
4031
                    // Check if we are on internal change password page, or service is external, don't show notification.
4032
                    $internalchangeurl = new moodle_url('/login/change_password.php');
4033
                    if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
4034
                        \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
4035
                    }
4036
                    set_user_preference('auth_forcepasswordchange', 1, $user);
4037
                } else if ($authplugin->can_reset_password()) {
4038
                    // Else force a reset if possible.
4039
                    \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
4040
                    redirect(new moodle_url('/login/forgot_password.php'));
4041
                } else {
4042
                    $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
4043
                    // If support page is set, add link for help.
4044
                    if (!empty($CFG->supportpage)) {
4045
                        $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
4046
                        $link = \html_writer::tag('p', $link);
4047
                        $notifymsg .= $link;
4048
                    }
4049
 
4050
                    // If no change or reset is possible, add a notification for user.
4051
                    \core\notification::error($notifymsg);
4052
                }
4053
            }
4054
        }
4055
 
4056
        // Successful authentication.
4057
        if ($user->id) {
4058
            // User already exists in database.
4059
            if (empty($user->auth)) {
4060
                // For some reason auth isn't set yet.
4061
                $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
4062
                $user->auth = $auth;
4063
            }
4064
 
4065
            // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4066
            // the current hash algorithm while we have access to the user's password.
4067
            update_internal_user_password($user, $password);
4068
 
4069
            if ($authplugin->is_synchronised_with_external()) {
4070
                // Update user record from external DB.
4071
                $user = update_user_record_by_id($user->id);
4072
            }
4073
        } else {
4074
            // The user is authenticated but user creation may be disabled.
4075
            if (!empty($CFG->authpreventaccountcreation)) {
4076
                $failurereason = AUTH_LOGIN_UNAUTHORISED;
4077
 
4078
                // Trigger login failed event.
1326 ariadna 4079
                $event = \core\event\user_login_failed::create(array('other' => array(
4080
                    'username' => $username,
4081
                    'reason' => $failurereason
4082
                )));
1 efrain 4083
                $event->trigger();
4084
 
1326 ariadna 4085
                error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  " .
4086
                    $_SERVER['HTTP_USER_AGENT']);
1 efrain 4087
                return false;
4088
            } else {
4089
                $user = create_user_record($username, $password, $auth);
4090
            }
4091
        }
4092
 
4093
        $authplugin->sync_roles($user);
4094
 
4095
        foreach ($authsenabled as $hau) {
4096
            $hauth = get_auth_plugin($hau);
4097
            $hauth->user_authenticated_hook($user, $username, $password);
4098
        }
4099
 
4100
        if (empty($user->id)) {
4101
            $failurereason = AUTH_LOGIN_NOUSER;
4102
            // Trigger login failed event.
1326 ariadna 4103
            $event = \core\event\user_login_failed::create(array('other' => array(
4104
                'username' => $username,
4105
                'reason' => $failurereason
4106
            )));
1 efrain 4107
            $event->trigger();
4108
            return false;
4109
        }
4110
 
4111
        if (!empty($user->suspended)) {
4112
            // Just in case some auth plugin suspended account.
4113
            $failurereason = AUTH_LOGIN_SUSPENDED;
4114
            // Trigger login failed event.
1326 ariadna 4115
            $event = \core\event\user_login_failed::create(array(
4116
                'userid' => $user->id,
4117
                'other' => array('username' => $username, 'reason' => $failurereason)
4118
            ));
1 efrain 4119
            $event->trigger();
1326 ariadna 4120
            error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Suspended Login:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 4121
            return false;
4122
        }
4123
 
4124
        login_attempt_valid($user);
4125
        $failurereason = AUTH_LOGIN_OK;
4126
        return $user;
4127
    }
4128
 
4129
    // Failed if all the plugins have failed.
4130
    if (debugging('', DEBUG_ALL)) {
1326 ariadna 4131
        error_log('[client ' . getremoteaddr() . "]  $CFG->wwwroot  Failed Login:  $username  " . $_SERVER['HTTP_USER_AGENT']);
1 efrain 4132
    }
4133
 
4134
    if ($user->id) {
4135
        login_attempt_failed($user);
4136
        $failurereason = AUTH_LOGIN_FAILED;
4137
        // Trigger login failed event.
1326 ariadna 4138
        $event = \core\event\user_login_failed::create(array(
4139
            'userid' => $user->id,
4140
            'other' => array('username' => $username, 'reason' => $failurereason)
4141
        ));
1 efrain 4142
        $event->trigger();
4143
    } else {
4144
        $failurereason = AUTH_LOGIN_NOUSER;
4145
        // Trigger login failed event.
1326 ariadna 4146
        $event = \core\event\user_login_failed::create(array('other' => array(
4147
            'username' => $username,
4148
            'reason' => $failurereason
4149
        )));
1 efrain 4150
        $event->trigger();
4151
    }
4152
 
4153
    return false;
4154
}
4155
 
4156
/**
4157
 * Call to complete the user login process after authenticate_user_login()
4158
 * has succeeded. It will setup the $USER variable and other required bits
4159
 * and pieces.
4160
 *
4161
 * NOTE:
4162
 * - It will NOT log anything -- up to the caller to decide what to log.
4163
 * - this function does not set any cookies any more!
4164
 *
4165
 * @param stdClass $user
4166
 * @param array $extrauserinfo
4167
 * @return stdClass A {@link $USER} object - BC only, do not use
4168
 */
1326 ariadna 4169
function complete_user_login($user, array $extrauserinfo = [])
4170
{
1 efrain 4171
    global $CFG, $DB, $USER, $SESSION;
4172
 
4173
    \core\session\manager::login_user($user);
4174
 
4175
    // Reload preferences from DB.
4176
    unset($USER->preference);
4177
    check_user_preferences_loaded($USER);
4178
 
4179
    // Update login times.
4180
    update_user_login_times();
4181
 
4182
    // Extra session prefs init.
4183
    set_login_session_preferences();
4184
 
4185
    // Trigger login event.
4186
    $event = \core\event\user_loggedin::create(
4187
        array(
4188
            'userid' => $USER->id,
4189
            'objectid' => $USER->id,
4190
            'other' => [
4191
                'username' => $USER->username,
4192
                'extrauserinfo' => $extrauserinfo
4193
            ]
4194
        )
4195
    );
4196
    $event->trigger();
4197
 
4198
    // Allow plugins to callback as soon possible after user has completed login.
4199
    di::get(\core\hook\manager::class)->dispatch(new \core_user\hook\after_login_completed());
4200
 
4201
    // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case).
4202
    // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser).
4203
    // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself.
4204
    $loginip = getremoteaddr();
4205
    $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip;
4206
    $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST;
4207
 
4208
    if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) {
4209
 
4210
        $logintime = time();
4211
        $ismoodleapp = false;
4212
        $useragent = \core_useragent::get_user_agent_string();
4213
 
4214
        $sitepreferences = get_message_output_default_preferences();
4215
        // Check if new login notification is disabled at system level.
4216
        $newlogindisabled = $sitepreferences->moodle_newlogin_disable ?? 0;
4217
        // Check if message providers (web, email, mobile) are enabled at system level.
4218
        $msgproviderenabled = isset($sitepreferences->message_provider_moodle_newlogin_enabled);
4219
        // Get message providers enabled for a user.
4220
        $userpreferences = get_user_preferences('message_provider_moodle_newlogin_enabled');
4221
        // Check if notification processor plugins (web, email, mobile) are enabled at system level.
4222
        $msgprocessorsready = !empty(get_message_processors(true));
4223
        // If new login notification is enabled at system level then go for other conditions check.
4224
        $newloginenabled = $newlogindisabled ? 0 : ($userpreferences != 'none' && $msgproviderenabled);
4225
 
4226
        if ($newloginenabled && $msgprocessorsready) {
4227
            // Schedule adhoc task to send a login notification to the user.
4228
            $task = new \core\task\send_login_notifications();
4229
            $task->set_userid($USER->id);
4230
            $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime'));
4231
            $task->set_component('core');
4232
            \core\task\manager::queue_adhoc_task($task);
4233
        }
4234
    }
4235
 
4236
    // Queue migrating the messaging data, if we need to.
4237
    if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4238
        // Check if there are any legacy messages to migrate.
4239
        if (\core_message\helper::legacy_messages_exist($USER->id)) {
4240
            \core_message\task\migrate_message_data::queue_task($USER->id);
4241
        } else {
4242
            set_user_preference('core_message_migrate_data', true, $USER->id);
4243
        }
4244
    }
4245
 
4246
    if (isguestuser()) {
4247
        // No need to continue when user is THE guest.
4248
        return $USER;
4249
    }
4250
 
4251
    if (CLI_SCRIPT) {
4252
        // We can redirect to password change URL only in browser.
4253
        return $USER;
4254
    }
4255
 
4256
    // Select password change url.
4257
    $userauth = get_auth_plugin($USER->auth);
4258
 
4259
    // Check whether the user should be changing password.
4260
    if (get_user_preferences('auth_forcepasswordchange', false)) {
4261
        if ($userauth->can_change_password()) {
4262
            if ($changeurl = $userauth->change_password_url()) {
4263
                redirect($changeurl);
4264
            } else {
4265
                require_once($CFG->dirroot . '/login/lib.php');
4266
                $SESSION->wantsurl = core_login_get_return_url();
1326 ariadna 4267
                redirect($CFG->wwwroot . '/login/change_password.php');
1 efrain 4268
            }
4269
        } else {
4270
            throw new \moodle_exception('nopasswordchangeforced', 'auth');
4271
        }
4272
    }
4273
    return $USER;
4274
}
4275
 
4276
/**
4277
 * Check a password hash to see if it was hashed using the legacy hash algorithm (bcrypt).
4278
 *
4279
 * @param string $password String to check.
4280
 * @return bool True if the $password matches the format of a bcrypt hash.
4281
 */
1326 ariadna 4282
function password_is_legacy_hash(#[\SensitiveParameter] string $password): bool
4283
{
1 efrain 4284
    return (bool) preg_match('/^\$2y\$[\d]{2}\$[A-Za-z0-9\.\/]{53}$/', $password);
4285
}
4286
 
4287
/**
4288
 * Calculate the Shannon entropy of a string.
4289
 *
4290
 * @param string $pepper The pepper to calculate the entropy of.
4291
 * @return float The Shannon entropy of the string.
4292
 */
1326 ariadna 4293
function calculate_entropy(#[\SensitiveParameter] string $pepper): float
4294
{
1 efrain 4295
    // Initialize entropy.
4296
    $h = 0;
4297
 
4298
    // Calculate the length of the string.
4299
    $size = strlen($pepper);
4300
 
4301
    // For each unique character in the string.
4302
    foreach (count_chars($pepper, 1) as $v) {
4303
        // Calculate the probability of the character.
4304
        $p = $v / $size;
4305
 
4306
        // Add the character's contribution to the total entropy.
4307
        // This uses the formula for the entropy of a discrete random variable.
4308
        $h -= $p * log($p) / log(2);
4309
    }
4310
 
4311
    // Instead of returning the average entropy per symbol (Shannon entropy),
4312
    // we multiply by the length of the string to get total entropy.
4313
    return $h * $size;
4314
}
4315
 
4316
/**
4317
 * Get the available password peppers.
4318
 * The latest pepper is checked for minimum entropy as part of this function.
4319
 * We only calculate the entropy of the most recent pepper,
4320
 * because passwords are always updated to the latest pepper,
4321
 * and in the past we may have enforced a lower minimum entropy.
4322
 * Also, we allow the latest pepper to be empty, to allow admins to migrate off peppers.
4323
 *
4324
 * @return array The password peppers.
4325
 * @throws coding_exception If the entropy of the password pepper is less than the recommended minimum.
4326
 */
1326 ariadna 4327
function get_password_peppers(): array
4328
{
1 efrain 4329
    global $CFG;
4330
 
4331
    // Get all available peppers.
4332
    if (isset($CFG->passwordpeppers) && is_array($CFG->passwordpeppers)) {
4333
        // Sort the array in descending order of keys (numerical).
4334
        $peppers = $CFG->passwordpeppers;
4335
        krsort($peppers, SORT_NUMERIC);
4336
    } else {
4337
        $peppers = [];  // Set an empty array if no peppers are found.
4338
    }
4339
 
4340
    // Check if the entropy of the most recent pepper is less than the minimum.
4341
    // Also, we allow the most recent pepper to be empty, to allow admins to migrate off peppers.
4342
    $lastpepper = reset($peppers);
4343
    if (!empty($peppers) && $lastpepper !== '' && calculate_entropy($lastpepper) < PEPPER_ENTROPY) {
4344
        throw new coding_exception(
1326 ariadna 4345
            'password pepper below minimum',
4346
            'The entropy of the password pepper is less than the recommended minimum.'
4347
        );
1 efrain 4348
    }
4349
    return $peppers;
4350
}
4351
 
4352
/**
4353
 * Compare password against hash stored in user object to determine if it is valid.
4354
 *
4355
 * If necessary it also updates the stored hash to the current format.
4356
 *
4357
 * @param stdClass $user (Password property may be updated).
4358
 * @param string $password Plain text password.
4359
 * @return bool True if password is valid.
4360
 */
1326 ariadna 4361
function validate_internal_user_password(stdClass $user, #[\SensitiveParameter] string $password): bool
4362
{
1 efrain 4363
 
4364
    if (exceeds_password_length($password)) {
4365
        // Password cannot be more than MAX_PASSWORD_CHARACTERS characters.
4366
        return false;
4367
    }
4368
 
4369
    if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4370
        // Internal password is not used at all, it can not validate.
4371
        return false;
4372
    }
4373
 
4374
    $peppers = get_password_peppers(); // Get the array of available peppers.
4375
    $islegacy = password_is_legacy_hash($user->password); // Check if the password is a legacy bcrypt hash.
4376
 
4377
    // If the password is a legacy hash, no peppers were used, so verify and update directly.
4378
    if ($islegacy && password_verify($password, $user->password)) {
4379
        update_internal_user_password($user, $password);
4380
        return true;
4381
    }
4382
 
4383
    // If the password is not a legacy hash, iterate through the peppers.
4384
    $latestpepper = reset($peppers);
4385
    // Add an empty pepper to the beginning of the array. To make it easier to check if the password matches without any pepper.
4386
    $peppers = [-1 => ''] + $peppers;
4387
    foreach ($peppers as $pepper) {
4388
        $pepperedpassword = $password . $pepper;
4389
 
4390
        // If the peppered password is correct, update (if necessary) and return true.
4391
        if (password_verify($pepperedpassword, $user->password)) {
4392
            // If the pepper used is not the latest one, update the password.
4393
            if ($pepper !== $latestpepper) {
4394
                update_internal_user_password($user, $password);
4395
            }
4396
            return true;
4397
        }
4398
    }
4399
 
4400
    // If no peppered password was correct, the password is wrong.
4401
    return false;
4402
}
4403
 
4404
/**
4405
 * Calculate hash for a plain text password.
4406
 *
4407
 * @param string $password Plain text password to be hashed.
4408
 * @param bool $fasthash If true, use a low number of rounds when generating the hash
4409
 *                       This is faster to generate but makes the hash less secure.
4410
 *                       It is used when lots of hashes need to be generated quickly.
4411
 * @param int $pepperlength Lenght of the peppers
4412
 * @return string The hashed password.
4413
 *
4414
 * @throws moodle_exception If a problem occurs while generating the hash.
4415
 */
1326 ariadna 4416
function hash_internal_user_password(#[\SensitiveParameter] string $password, $fasthash = false, $pepperlength = 0): string
4417
{
1 efrain 4418
    if (exceeds_password_length($password, $pepperlength)) {
4419
        // Password cannot be more than MAX_PASSWORD_CHARACTERS.
4420
        throw new \moodle_exception(get_string("passwordexceeded", 'error', MAX_PASSWORD_CHARACTERS));
4421
    }
4422
 
4423
    // Set the cost factor to 5000 for fast hashing, otherwise use default cost.
4424
    $rounds = $fasthash ? 5000 : 10000;
4425
 
4426
    // First generate a cryptographically suitable salt.
4427
    $randombytes = random_bytes(16);
4428
    $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16);
4429
 
4430
    // Now construct the password string with the salt and number of rounds.
4431
    // The password string is in the format $algorithm$rounds$salt$hash. ($6 is the SHA512 algorithm).
4432
    $generatedhash = crypt($password, implode('$', [
4433
        '',
4434
        // The SHA512 Algorithm
4435
        '6',
4436
        "rounds={$rounds}",
4437
        $salt,
4438
        '',
4439
    ]));
4440
 
4441
    if ($generatedhash === false || $generatedhash === null) {
4442
        throw new moodle_exception('Failed to generate password hash.');
4443
    }
4444
 
4445
    return $generatedhash;
4446
}
4447
 
4448
/**
4449
 * Update password hash in user object (if necessary).
4450
 *
4451
 * The password is updated if:
4452
 * 1. The password has changed (the hash of $user->password is different
4453
 *    to the hash of $password).
4454
 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4455
 *    md5 algorithm).
4456
 *
4457
 * The password is peppered with the latest pepper before hashing,
4458
 * if peppers are available.
4459
 * Updating the password will modify the $user object and the database
4460
 * record to use the current hashing algorithm.
4461
 * It will remove Web Services user tokens too.
4462
 *
4463
 * @param stdClass $user User object (password property may be updated).
11 efrain 4464
 * @param string|null $password Plain text password.
1 efrain 4465
 * @param bool $fasthash If true, use a low cost factor when generating the hash
4466
 *                       This is much faster to generate but makes the hash
4467
 *                       less secure. It is used when lots of hashes need to
4468
 *                       be generated quickly.
4469
 * @return bool Always returns true.
4470
 */
4471
function update_internal_user_password(
1326 ariadna 4472
    stdClass $user,
4473
    #[\SensitiveParameter] ?string $password,
4474
    bool $fasthash = false
1 efrain 4475
): bool {
4476
    global $CFG, $DB;
4477
 
4478
    // Add the latest password pepper to the password before further processing.
4479
    $peppers = get_password_peppers();
4480
    if (!empty($peppers)) {
4481
        $password = $password . reset($peppers);
4482
    }
4483
 
4484
    // Figure out what the hashed password should be.
4485
    if (!isset($user->auth)) {
1326 ariadna 4486
        debugging(
4487
            'User record in update_internal_user_password() must include field auth',
4488
            DEBUG_DEVELOPER
4489
        );
1 efrain 4490
        $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4491
    }
4492
    $authplugin = get_auth_plugin($user->auth);
4493
    if ($authplugin->prevent_local_passwords()) {
4494
        $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4495
    } else {
4496
        $hashedpassword = hash_internal_user_password($password, $fasthash);
4497
    }
4498
 
4499
    $algorithmchanged = false;
4500
 
4501
    if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4502
        // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4503
        $passwordchanged = ($user->password !== $hashedpassword);
4504
    } else if (isset($user->password)) {
4505
        // If verification fails then it means the password has changed.
4506
        $passwordchanged = !password_verify($password, $user->password);
4507
        $algorithmchanged = password_is_legacy_hash($user->password);
4508
    } else {
4509
        // While creating new user, password in unset in $user object, to avoid
4510
        // saving it with user_create()
4511
        $passwordchanged = true;
4512
    }
4513
 
4514
    if ($passwordchanged || $algorithmchanged) {
4515
        $DB->set_field('user', 'password',  $hashedpassword, array('id' => $user->id));
4516
        $user->password = $hashedpassword;
4517
 
4518
        // Trigger event.
4519
        $user = $DB->get_record('user', array('id' => $user->id));
4520
        \core\event\user_password_updated::create_from_user($user)->trigger();
4521
 
4522
        // Remove WS user tokens.
4523
        if (!empty($CFG->passwordchangetokendeletion)) {
1326 ariadna 4524
            require_once($CFG->dirroot . '/webservice/lib.php');
1 efrain 4525
            webservice::delete_user_ws_tokens($user->id);
4526
        }
4527
    }
4528
 
4529
    return true;
4530
}
4531
 
4532
/**
4533
 * Get a complete user record, which includes all the info in the user record.
4534
 *
4535
 * Intended for setting as $USER session variable
4536
 *
4537
 * @param string $field The user field to be checked for a given value.
4538
 * @param string $value The value to match for $field.
4539
 * @param int $mnethostid
4540
 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4541
 *                              found. Otherwise, it will just return false.
4542
 * @return mixed False, or A {@link $USER} object.
4543
 */
1326 ariadna 4544
function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false)
4545
{
1 efrain 4546
    global $CFG, $DB;
4547
 
4548
    if (!$field || !$value) {
4549
        return false;
4550
    }
4551
 
4552
    // Change the field to lowercase.
4553
    $field = core_text::strtolower($field);
4554
 
4555
    // List of case insensitive fields.
4556
    $caseinsensitivefields = ['email'];
4557
 
4558
    // Username input is forced to lowercase and should be case sensitive.
4559
    if ($field == 'username') {
4560
        $value = core_text::strtolower($value);
4561
    }
4562
 
4563
    // Build the WHERE clause for an SQL query.
4564
    $params = array('fieldval' => $value);
4565
 
4566
    // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4567
    // such as MySQL by pre-filtering users with accent-insensitive subselect.
4568
    if (in_array($field, $caseinsensitivefields)) {
4569
        $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4570
        $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4571
        $params['fieldval2'] = $value;
4572
    } else {
4573
        $fieldselect = "$field = :fieldval";
4574
        $idsubselect = '';
4575
    }
4576
    $constraints = "$fieldselect AND deleted <> 1";
4577
 
4578
    // If we are loading user data based on anything other than id,
4579
    // we must also restrict our search based on mnet host.
4580
    if ($field != 'id') {
4581
        if (empty($mnethostid)) {
4582
            // If empty, we restrict to local users.
4583
            $mnethostid = $CFG->mnet_localhost_id;
4584
        }
4585
    }
4586
    if (!empty($mnethostid)) {
4587
        $params['mnethostid'] = $mnethostid;
4588
        $constraints .= " AND mnethostid = :mnethostid";
4589
    }
4590
 
4591
    if ($idsubselect) {
4592
        $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4593
    }
4594
 
4595
    // Get all the basic user data.
4596
    try {
4597
        // Make sure that there's only a single record that matches our query.
4598
        // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4599
        // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4600
        $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4601
    } catch (dml_exception $exception) {
4602
        if ($throwexception) {
4603
            throw $exception;
4604
        } else {
4605
            // Return false when no records or multiple records were found.
4606
            return false;
4607
        }
4608
    }
4609
 
4610
    // Get various settings and preferences.
4611
 
4612
    // Preload preference cache.
4613
    check_user_preferences_loaded($user);
4614
 
4615
    // Load course enrolment related stuff.
4616
    $user->lastcourseaccess    = array(); // During last session.
4617
    $user->currentcourseaccess = array(); // During current session.
4618
    if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4619
        foreach ($lastaccesses as $lastaccess) {
4620
            $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4621
        }
4622
    }
4623
 
4624
    // Add cohort theme.
4625
    if (!empty($CFG->allowcohortthemes)) {
4626
        require_once($CFG->dirroot . '/cohort/lib.php');
4627
        if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
4628
            $user->cohorttheme = $cohorttheme;
4629
        }
4630
    }
4631
 
4632
    // Add the custom profile fields to the user record.
4633
    $user->profile = array();
4634
    if (!isguestuser($user)) {
1326 ariadna 4635
        require_once($CFG->dirroot . '/user/profile/lib.php');
1 efrain 4636
        profile_load_custom_fields($user);
4637
    }
4638
 
4639
    // Rewrite some variables if necessary.
4640
    if (!empty($user->description)) {
4641
        // No need to cart all of it around.
4642
        $user->description = true;
4643
    }
4644
    if (isguestuser($user)) {
4645
        // Guest language always same as site.
4646
        $user->lang = get_newuser_language();
4647
        // Name always in current language.
4648
        $user->firstname = get_string('guestuser');
4649
        $user->lastname = ' ';
4650
    }
4651
 
4652
    return $user;
4653
}
4654
 
4655
/**
4656
 * Validate a password against the configured password policy
4657
 *
4658
 * @param string $password the password to be checked against the password policy
4659
 * @param string|null $errmsg the error message to display when the password doesn't comply with the policy.
4660
 * @param stdClass|null $user the user object to perform password validation against. Defaults to null if not provided.
4661
 *
4662
 * @return bool true if the password is valid according to the policy. false otherwise.
4663
 */
1326 ariadna 4664
function check_password_policy(string $password, ?string &$errmsg, ?stdClass $user = null)
4665
{
1 efrain 4666
    global $CFG;
4667
    if (!empty($CFG->passwordpolicy) && !isguestuser($user)) {
4668
        $errors = get_password_policy_errors($password, $user);
4669
 
4670
        foreach ($errors as $error) {
4671
            $errmsg .= '<div>' . $error . '</div>';
4672
        }
4673
    }
4674
 
4675
    return $errmsg == '';
4676
}
4677
 
4678
/**
4679
 * Validate a password against the configured password policy.
4680
 * Note: This function is unaffected by whether the password policy is enabled or not.
4681
 *
4682
 * @param string $password the password to be checked against the password policy
4683
 * @param stdClass|null $user the user object to perform password validation against. Defaults to null if not provided.
4684
 *
4685
 * @return string[] Array of error messages.
4686
 */
1326 ariadna 4687
function get_password_policy_errors(string $password, ?stdClass $user = null): array
4688
{
1 efrain 4689
    global $CFG;
4690
 
4691
    $errors = [];
4692
 
4693
    if (core_text::strlen($password) < $CFG->minpasswordlength) {
4694
        $errors[] = get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength);
4695
    }
4696
    if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4697
        $errors[] = get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits);
4698
    }
4699
    if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4700
        $errors[] = get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower);
4701
    }
4702
    if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4703
        $errors[] = get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper);
4704
    }
4705
    if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4706
        $errors[] = get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum);
4707
    }
4708
    if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4709
        $errors[] = get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars);
4710
    }
4711
 
4712
    // Fire any additional password policy functions from plugins.
4713
    // Plugin functions should output an error message string or empty string for success.
4714
    $pluginsfunction = get_plugins_with_function('check_password_policy');
4715
    foreach ($pluginsfunction as $plugintype => $plugins) {
4716
        foreach ($plugins as $pluginfunction) {
4717
            $pluginerr = $pluginfunction($password, $user);
4718
            if ($pluginerr) {
4719
                $errors[] = $pluginerr;
4720
            }
4721
        }
4722
    }
4723
 
4724
    return $errors;
4725
}
4726
 
4727
/**
4728
 * When logging in, this function is run to set certain preferences for the current SESSION.
4729
 */
1326 ariadna 4730
function set_login_session_preferences()
4731
{
1 efrain 4732
    global $SESSION;
4733
 
4734
    $SESSION->justloggedin = true;
4735
 
4736
    unset($SESSION->lang);
4737
    unset($SESSION->forcelang);
4738
    unset($SESSION->load_navigation_admin);
4739
}
4740
 
4741
 
4742
/**
4743
 * Delete a course, including all related data from the database, and any associated files.
4744
 *
4745
 * @param mixed $courseorid The id of the course or course object to delete.
4746
 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4747
 * @return bool true if all the removals succeeded. false if there were any failures. If this
4748
 *             method returns false, some of the removals will probably have succeeded, and others
4749
 *             failed, but you have no way of knowing which.
4750
 */
1326 ariadna 4751
function delete_course($courseorid, $showfeedback = true)
4752
{
1 efrain 4753
    global $DB, $CFG;
4754
 
4755
    if (is_object($courseorid)) {
4756
        $courseid = $courseorid->id;
4757
        $course   = $courseorid;
4758
    } else {
4759
        $courseid = $courseorid;
4760
        if (!$course = $DB->get_record('course', array('id' => $courseid))) {
4761
            return false;
4762
        }
4763
    }
4764
    $context = context_course::instance($courseid);
4765
 
4766
    // Frontpage course can not be deleted!!
4767
    if ($courseid == SITEID) {
4768
        return false;
4769
    }
4770
 
4771
    // Allow plugins to use this course before we completely delete it.
4772
    if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
4773
        foreach ($pluginsfunction as $plugintype => $plugins) {
4774
            foreach ($plugins as $pluginfunction) {
4775
                $pluginfunction($course);
4776
            }
4777
        }
4778
    }
4779
 
4780
    // Dispatch the hook for pre course delete actions.
4781
    $hook = new \core_course\hook\before_course_deleted(
4782
        course: $course,
4783
    );
4784
    \core\di::get(\core\hook\manager::class)->dispatch($hook);
4785
 
4786
    // Tell the search manager we are about to delete a course. This prevents us sending updates
4787
    // for each individual context being deleted.
4788
    \core_search\manager::course_deleting_start($courseid);
4789
 
4790
    $handler = core_course\customfield\course_handler::create();
4791
    $handler->delete_instance($courseid);
4792
 
4793
    // Make the course completely empty.
4794
    remove_course_contents($courseid, $showfeedback);
4795
 
4796
    // Delete the course and related context instance.
4797
    context_helper::delete_instance(CONTEXT_COURSE, $courseid);
4798
 
4799
    $DB->delete_records("course", array("id" => $courseid));
4800
    $DB->delete_records("course_format_options", array("courseid" => $courseid));
4801
 
4802
    // Reset all course related caches here.
4803
    core_courseformat\base::reset_course_cache($courseid);
4804
 
4805
    // Tell search that we have deleted the course so it can delete course data from the index.
4806
    \core_search\manager::course_deleting_finish($courseid);
4807
 
4808
    // Trigger a course deleted event.
4809
    $event = \core\event\course_deleted::create(array(
4810
        'objectid' => $course->id,
4811
        'context' => $context,
4812
        'other' => array(
4813
            'shortname' => $course->shortname,
4814
            'fullname' => $course->fullname,
4815
            'idnumber' => $course->idnumber
1326 ariadna 4816
        )
1 efrain 4817
    ));
4818
    $event->add_record_snapshot('course', $course);
4819
    $event->trigger();
4820
 
4821
    return true;
4822
}
4823
 
4824
/**
4825
 * Clear a course out completely, deleting all content but don't delete the course itself.
4826
 *
4827
 * This function does not verify any permissions.
4828
 *
4829
 * Please note this function also deletes all user enrolments,
4830
 * enrolment instances and role assignments by default.
4831
 *
4832
 * $options:
4833
 *  - 'keep_roles_and_enrolments' - false by default
4834
 *  - 'keep_groups_and_groupings' - false by default
4835
 *
4836
 * @param int $courseid The id of the course that is being deleted
4837
 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4838
 * @param array $options extra options
4839
 * @return bool true if all the removals succeeded. false if there were any failures. If this
4840
 *             method returns false, some of the removals will probably have succeeded, and others
4841
 *             failed, but you have no way of knowing which.
4842
 */
1326 ariadna 4843
function remove_course_contents($courseid, $showfeedback = true, array $options = null)
4844
{
1 efrain 4845
    global $CFG, $DB, $OUTPUT;
4846
 
1326 ariadna 4847
    require_once($CFG->libdir . '/badgeslib.php');
4848
    require_once($CFG->libdir . '/completionlib.php');
4849
    require_once($CFG->libdir . '/questionlib.php');
4850
    require_once($CFG->libdir . '/gradelib.php');
4851
    require_once($CFG->dirroot . '/group/lib.php');
4852
    require_once($CFG->dirroot . '/comment/lib.php');
4853
    require_once($CFG->dirroot . '/rating/lib.php');
4854
    require_once($CFG->dirroot . '/notes/lib.php');
1 efrain 4855
 
4856
    // Handle course badges.
4857
    badges_handle_course_deletion($courseid);
4858
 
4859
    // NOTE: these concatenated strings are suboptimal, but it is just extra info...
1326 ariadna 4860
    $strdeleted = get_string('deleted') . ' - ';
1 efrain 4861
 
4862
    // Some crazy wishlist of stuff we should skip during purging of course content.
4863
    $options = (array)$options;
4864
 
4865
    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
4866
    $coursecontext = context_course::instance($courseid);
4867
    $fs = get_file_storage();
4868
 
4869
    // Delete course completion information, this has to be done before grades and enrols.
4870
    $cc = new completion_info($course);
4871
    $cc->clear_criteria();
4872
    if ($showfeedback) {
1326 ariadna 4873
        echo $OUTPUT->notification($strdeleted . get_string('completion', 'completion'), 'notifysuccess');
1 efrain 4874
    }
4875
 
4876
    // Remove all data from gradebook - this needs to be done before course modules
4877
    // because while deleting this information, the system may need to reference
4878
    // the course modules that own the grades.
4879
    remove_course_grades($courseid, $showfeedback);
4880
    remove_grade_letters($coursecontext, $showfeedback);
4881
 
4882
    // Delete course blocks in any all child contexts,
4883
    // they may depend on modules so delete them first.
4884
    $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4885
    foreach ($childcontexts as $childcontext) {
4886
        blocks_delete_all_for_context($childcontext->id);
4887
    }
4888
    unset($childcontexts);
4889
    blocks_delete_all_for_context($coursecontext->id);
4890
    if ($showfeedback) {
1326 ariadna 4891
        echo $OUTPUT->notification($strdeleted . get_string('type_block_plural', 'plugin'), 'notifysuccess');
1 efrain 4892
    }
4893
 
4894
    $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
4895
    rebuild_course_cache($courseid, true);
4896
 
4897
    // Get the list of all modules that are properly installed.
4898
    $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
4899
 
4900
    // Delete every instance of every module,
4901
    // this has to be done before deleting of course level stuff.
4902
    $locations = core_component::get_plugin_list('mod');
4903
    foreach ($locations as $modname => $moddir) {
4904
        if ($modname === 'NEWMODULE') {
4905
            continue;
4906
        }
4907
        if (array_key_exists($modname, $allmodules)) {
4908
            $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
1326 ariadna 4909
              FROM {" . $modname . "} m
1 efrain 4910
                   LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
4911
             WHERE m.course = :courseid";
1326 ariadna 4912
            $instances = $DB->get_records_sql($sql, array(
4913
                'courseid' => $course->id,
4914
                'modulename' => $modname,
4915
                'moduleid' => $allmodules[$modname]
4916
            ));
1 efrain 4917
 
4918
            include_once("$moddir/lib.php");                 // Shows php warning only if plugin defective.
1326 ariadna 4919
            $moddelete = $modname . '_delete_instance';       // Delete everything connected to an instance.
1 efrain 4920
 
4921
            if ($instances) {
4922
                foreach ($instances as $cm) {
4923
                    if ($cm->id) {
4924
                        // Delete activity context questions and question categories.
4925
                        question_delete_activity($cm);
4926
                        // Notify the competency subsystem.
4927
                        \core_competency\api::hook_course_module_deleted($cm);
4928
 
4929
                        // Delete all tag instances associated with the instance of this module.
4930
                        core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id);
4931
                        core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
4932
                    }
4933
                    if (function_exists($moddelete)) {
4934
                        // This purges all module data in related tables, extra user prefs, settings, etc.
4935
                        $moddelete($cm->modinstance);
4936
                    } else {
4937
                        // NOTE: we should not allow installation of modules with missing delete support!
4938
                        debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4939
                        $DB->delete_records($modname, array('id' => $cm->modinstance));
4940
                    }
4941
 
4942
                    if ($cm->id) {
4943
                        // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
4944
                        context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4945
                        $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
4946
                        $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
4947
                        $DB->delete_records('course_modules', array('id' => $cm->id));
4948
                        rebuild_course_cache($cm->course, true);
4949
                    }
4950
                }
4951
            }
4952
            if ($instances and $showfeedback) {
1326 ariadna 4953
                echo $OUTPUT->notification($strdeleted . get_string('pluginname', $modname), 'notifysuccess');
1 efrain 4954
            }
4955
        } else {
4956
            // Ooops, this module is not properly installed, force-delete it in the next block.
4957
        }
4958
    }
4959
 
4960
    // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
4961
 
4962
    // Delete completion defaults.
4963
    $DB->delete_records("course_completion_defaults", array("course" => $courseid));
4964
 
4965
    // Remove all data from availability and completion tables that is associated
4966
    // with course-modules belonging to this course. Note this is done even if the
4967
    // features are not enabled now, in case they were enabled previously.
1326 ariadna 4968
    $DB->delete_records_subquery(
4969
        'course_modules_completion',
4970
        'coursemoduleid',
4971
        'id',
4972
        'SELECT id from {course_modules} WHERE course = ?',
4973
        [$courseid]
4974
    );
4975
    $DB->delete_records_subquery(
4976
        'course_modules_viewed',
4977
        'coursemoduleid',
4978
        'id',
4979
        'SELECT id from {course_modules} WHERE course = ?',
4980
        [$courseid]
4981
    );
1 efrain 4982
 
4983
    // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
4984
    $cms = $DB->get_records('course_modules', array('course' => $course->id));
4985
    $allmodulesbyid = array_flip($allmodules);
4986
    foreach ($cms as $cm) {
4987
        if (array_key_exists($cm->module, $allmodulesbyid)) {
4988
            try {
4989
                $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
4990
            } catch (Exception $e) {
4991
                // Ignore weird or missing table problems.
4992
            }
4993
        }
4994
        context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4995
        $DB->delete_records('course_modules', array('id' => $cm->id));
4996
        rebuild_course_cache($cm->course, true);
4997
    }
4998
 
4999
    if ($showfeedback) {
1326 ariadna 5000
        echo $OUTPUT->notification($strdeleted . get_string('type_mod_plural', 'plugin'), 'notifysuccess');
1 efrain 5001
    }
5002
 
5003
    // Delete questions and question categories.
5004
    question_delete_course($course);
5005
    if ($showfeedback) {
1326 ariadna 5006
        echo $OUTPUT->notification($strdeleted . get_string('questions', 'question'), 'notifysuccess');
1 efrain 5007
    }
5008
 
5009
    // Delete content bank contents.
5010
    $cb = new \core_contentbank\contentbank();
5011
    $cbdeleted = $cb->delete_contents($coursecontext);
5012
    if ($showfeedback && $cbdeleted) {
1326 ariadna 5013
        echo $OUTPUT->notification($strdeleted . get_string('contentbank', 'contentbank'), 'notifysuccess');
1 efrain 5014
    }
5015
 
5016
    // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
5017
    $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5018
    foreach ($childcontexts as $childcontext) {
5019
        $childcontext->delete();
5020
    }
5021
    unset($childcontexts);
5022
 
5023
    // Remove roles and enrolments by default.
5024
    if (empty($options['keep_roles_and_enrolments'])) {
5025
        // This hack is used in restore when deleting contents of existing course.
5026
        // During restore, we should remove only enrolment related data that the user performing the restore has a
5027
        // permission to remove.
5028
        $userid = $options['userid'] ?? null;
5029
        enrol_course_delete($course, $userid);
5030
        role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
5031
        if ($showfeedback) {
1326 ariadna 5032
            echo $OUTPUT->notification($strdeleted . get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
1 efrain 5033
        }
5034
    }
5035
 
5036
    // Delete any groups, removing members and grouping/course links first.
5037
    if (empty($options['keep_groups_and_groupings'])) {
5038
        groups_delete_groupings($course->id, $showfeedback);
5039
        groups_delete_groups($course->id, $showfeedback);
5040
    }
5041
 
5042
    // Filters be gone!
5043
    filter_delete_all_for_context($coursecontext->id);
5044
 
5045
    // Notes, you shall not pass!
5046
    note_delete_all($course->id);
5047
 
5048
    // Die comments!
5049
    comment::delete_comments($coursecontext->id);
5050
 
5051
    // Ratings are history too.
5052
    $delopt = new stdclass();
5053
    $delopt->contextid = $coursecontext->id;
5054
    $rm = new rating_manager();
5055
    $rm->delete_ratings($delopt);
5056
 
5057
    // Delete course tags.
5058
    core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
5059
 
5060
    // Give the course format the opportunity to remove its obscure data.
5061
    $format = course_get_format($course);
5062
    $format->delete_format_data();
5063
 
5064
    // Notify the competency subsystem.
5065
    \core_competency\api::hook_course_deleted($course);
5066
 
5067
    // Delete calendar events.
5068
    $DB->delete_records('event', array('courseid' => $course->id));
5069
    $fs->delete_area_files($coursecontext->id, 'calendar');
5070
 
5071
    // Delete all related records in other core tables that may have a courseid
5072
    // This array stores the tables that need to be cleared, as
5073
    // table_name => column_name that contains the course id.
5074
    $tablestoclear = array(
5075
        'backup_courses' => 'courseid',  // Scheduled backup stuff.
5076
        'user_lastaccess' => 'courseid', // User access info.
5077
    );
5078
    foreach ($tablestoclear as $table => $col) {
5079
        $DB->delete_records($table, array($col => $course->id));
5080
    }
5081
 
5082
    // Delete all course backup files.
5083
    $fs->delete_area_files($coursecontext->id, 'backup');
5084
 
5085
    // Cleanup course record - remove links to deleted stuff.
5086
    // Do not wipe cacherev, as this course might be reused and we need to ensure that it keeps
5087
    // increasing.
5088
    $oldcourse = new stdClass();
5089
    $oldcourse->id               = $course->id;
5090
    $oldcourse->summary          = '';
5091
    $oldcourse->legacyfiles      = 0;
5092
    if (!empty($options['keep_groups_and_groupings'])) {
5093
        $oldcourse->defaultgroupingid = 0;
5094
    }
5095
    $DB->update_record('course', $oldcourse);
5096
 
5097
    // Delete course sections.
5098
    $DB->delete_records('course_sections', array('course' => $course->id));
5099
 
5100
    // Delete legacy, section and any other course files.
5101
    $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
5102
 
5103
    // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5104
    if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5105
        // Easy, do not delete the context itself...
5106
        $coursecontext->delete_content();
5107
    } else {
5108
        // Hack alert!!!!
5109
        // We can not drop all context stuff because it would bork enrolments and roles,
5110
        // there might be also files used by enrol plugins...
5111
    }
5112
 
5113
    // Delete legacy files - just in case some files are still left there after conversion to new file api,
5114
    // also some non-standard unsupported plugins may try to store something there.
1326 ariadna 5115
    fulldelete($CFG->dataroot . '/' . $course->id);
1 efrain 5116
 
5117
    // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5118
    course_modinfo::purge_course_cache($courseid);
5119
 
5120
    // Trigger a course content deleted event.
5121
    $event = \core\event\course_content_deleted::create(array(
5122
        'objectid' => $course->id,
5123
        'context' => $coursecontext,
1326 ariadna 5124
        'other' => array(
5125
            'shortname' => $course->shortname,
5126
            'fullname' => $course->fullname,
5127
            'options' => $options
5128
        ) // Passing this for legacy reasons.
1 efrain 5129
    ));
5130
    $event->add_record_snapshot('course', $course);
5131
    $event->trigger();
5132
 
5133
    return true;
5134
}
5135
 
5136
/**
5137
 * Change dates in module - used from course reset.
5138
 *
5139
 * @param string $modname forum, assignment, etc
5140
 * @param array $fields array of date fields from mod table
5141
 * @param int $timeshift time difference
5142
 * @param int $courseid
5143
 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5144
 * @return bool success
5145
 */
1326 ariadna 5146
function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0)
5147
{
1 efrain 5148
    global $CFG, $DB;
1326 ariadna 5149
    include_once($CFG->dirroot . '/mod/' . $modname . '/lib.php');
1 efrain 5150
 
5151
    $return = true;
5152
    $params = array($timeshift, $courseid);
5153
    foreach ($fields as $field) {
1326 ariadna 5154
        $updatesql = "UPDATE {" . $modname . "}
1 efrain 5155
                          SET $field = $field + ?
5156
                        WHERE course=? AND $field<>0";
5157
        if ($modid) {
5158
            $updatesql .= ' AND id=?';
5159
            $params[] = $modid;
5160
        }
5161
        $return = $DB->execute($updatesql, $params) && $return;
5162
    }
5163
 
5164
    return $return;
5165
}
5166
 
5167
/**
5168
 * This function will empty a course of user data.
5169
 * It will retain the activities and the structure of the course.
5170
 *
5171
 * @param object $data an object containing all the settings including courseid (without magic quotes)
5172
 * @return array status array of array component, item, error
5173
 */
1326 ariadna 5174
function reset_course_userdata($data)
5175
{
1 efrain 5176
    global $CFG, $DB;
1326 ariadna 5177
    require_once($CFG->libdir . '/gradelib.php');
5178
    require_once($CFG->libdir . '/completionlib.php');
5179
    require_once($CFG->dirroot . '/completion/criteria/completion_criteria_date.php');
5180
    require_once($CFG->dirroot . '/group/lib.php');
1 efrain 5181
 
5182
    $data->courseid = $data->id;
5183
    $context = context_course::instance($data->courseid);
5184
 
5185
    $eventparams = array(
5186
        'context' => $context,
5187
        'courseid' => $data->id,
5188
        'other' => array(
5189
            'reset_options' => (array) $data
5190
        )
5191
    );
5192
    $event = \core\event\course_reset_started::create($eventparams);
5193
    $event->trigger();
5194
 
5195
    // Calculate the time shift of dates.
5196
    if (!empty($data->reset_start_date)) {
5197
        // Time part of course startdate should be zero.
5198
        $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5199
    } else {
5200
        $data->timeshift = 0;
5201
    }
5202
 
5203
    // Result array: component, item, error.
5204
    $status = array();
5205
 
5206
    // Start the resetting.
5207
    $componentstr = get_string('general');
5208
 
5209
    // Move the course start time.
5210
    if (!empty($data->reset_start_date) and $data->timeshift) {
5211
        // Change course start data.
5212
        $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5213
        // Update all course and group events - do not move activity events.
5214
        $updatesql = "UPDATE {event}
5215
                         SET timestart = timestart + ?
5216
                       WHERE courseid=? AND instance=0";
5217
        $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5218
 
5219
        // Update any date activity restrictions.
5220
        if ($CFG->enableavailability) {
5221
            \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5222
        }
5223
 
5224
        // Update completion expected dates.
5225
        if ($CFG->enablecompletion) {
5226
            $modinfo = get_fast_modinfo($data->courseid);
5227
            $changed = false;
5228
            foreach ($modinfo->get_cms() as $cm) {
5229
                if ($cm->completion && !empty($cm->completionexpected)) {
1326 ariadna 5230
                    $DB->set_field(
5231
                        'course_modules',
5232
                        'completionexpected',
5233
                        $cm->completionexpected + $data->timeshift,
5234
                        array('id' => $cm->id)
5235
                    );
1 efrain 5236
                    $changed = true;
5237
                }
5238
            }
5239
 
5240
            // Clear course cache if changes made.
5241
            if ($changed) {
5242
                rebuild_course_cache($data->courseid, true);
5243
            }
5244
 
5245
            // Update course date completion criteria.
5246
            \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5247
        }
5248
 
5249
        $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5250
    }
5251
 
5252
    if (!empty($data->reset_end_date)) {
5253
        // If the user set a end date value respect it.
5254
        $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5255
    } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5256
        // If there is a time shift apply it to the end date as well.
5257
        $enddate = $data->reset_end_date_old + $data->timeshift;
5258
        $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5259
    }
5260
 
5261
    if (!empty($data->reset_events)) {
5262
        $DB->delete_records('event', array('courseid' => $data->courseid));
5263
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5264
    }
5265
 
5266
    if (!empty($data->reset_notes)) {
1326 ariadna 5267
        require_once($CFG->dirroot . '/notes/lib.php');
1 efrain 5268
        note_delete_all($data->courseid);
5269
        $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5270
    }
5271
 
5272
    if (!empty($data->delete_blog_associations)) {
1326 ariadna 5273
        require_once($CFG->dirroot . '/blog/lib.php');
1 efrain 5274
        blog_remove_associations_for_course($data->courseid);
5275
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5276
    }
5277
 
5278
    if (!empty($data->reset_completion)) {
5279
        // Delete course and activity completion information.
5280
        $course = $DB->get_record('course', array('id' => $data->courseid));
5281
        $cc = new completion_info($course);
5282
        $cc->delete_all_completion_data();
1326 ariadna 5283
        $status[] = array(
5284
            'component' => $componentstr,
5285
            'item' => get_string('deletecompletiondata', 'completion'),
5286
            'error' => false
5287
        );
1 efrain 5288
    }
5289
 
5290
    if (!empty($data->reset_competency_ratings)) {
5291
        \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
1326 ariadna 5292
        $status[] = array(
5293
            'component' => $componentstr,
5294
            'item' => get_string('deletecompetencyratings', 'core_competency'),
5295
            'error' => false
5296
        );
1 efrain 5297
    }
5298
 
5299
    $componentstr = get_string('roles');
5300
 
5301
    if (!empty($data->reset_roles_overrides)) {
5302
        $children = $context->get_child_contexts();
5303
        foreach ($children as $child) {
5304
            $child->delete_capabilities();
5305
        }
5306
        $context->delete_capabilities();
5307
        $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5308
    }
5309
 
5310
    if (!empty($data->reset_roles_local)) {
5311
        $children = $context->get_child_contexts();
5312
        foreach ($children as $child) {
5313
            role_unassign_all(array('contextid' => $child->id));
5314
        }
5315
        $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5316
    }
5317
 
5318
    // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5319
    $data->unenrolled = array();
5320
    if (!empty($data->unenrol_users)) {
5321
        $plugins = enrol_get_plugins(true);
5322
        $instances = enrol_get_instances($data->courseid, true);
5323
        foreach ($instances as $key => $instance) {
5324
            if (!isset($plugins[$instance->enrol])) {
5325
                unset($instances[$key]);
5326
                continue;
5327
            }
5328
        }
5329
 
5330
        $usersroles = enrol_get_course_users_roles($data->courseid);
5331
        foreach ($data->unenrol_users as $withroleid) {
5332
            if ($withroleid) {
5333
                $sql = "SELECT ue.*
5334
                          FROM {user_enrolments} ue
5335
                          JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5336
                          JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5337
                          JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5338
                $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5339
            } else {
5340
                // Without any role assigned at course context.
5341
                $sql = "SELECT ue.*
5342
                          FROM {user_enrolments} ue
5343
                          JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5344
                          JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5345
                     LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5346
                         WHERE ra.id IS null";
5347
                $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5348
            }
5349
 
5350
            $rs = $DB->get_recordset_sql($sql, $params);
5351
            foreach ($rs as $ue) {
5352
                if (!isset($instances[$ue->enrolid])) {
5353
                    continue;
5354
                }
5355
                $instance = $instances[$ue->enrolid];
5356
                $plugin = $plugins[$instance->enrol];
5357
                if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5358
                    continue;
5359
                }
5360
 
5361
                if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5362
                    // If we don't remove all roles and user has more than one role, just remove this role.
5363
                    role_unassign($withroleid, $ue->userid, $context->id);
5364
 
5365
                    unset($usersroles[$ue->userid][$withroleid]);
5366
                } else {
5367
                    // If we remove all roles or user has only one role, unenrol user from course.
5368
                    $plugin->unenrol_user($instance, $ue->userid);
5369
                }
5370
                $data->unenrolled[$ue->userid] = $ue->userid;
5371
            }
5372
            $rs->close();
5373
        }
5374
    }
5375
    if (!empty($data->unenrolled)) {
5376
        $status[] = array(
5377
            'component' => $componentstr,
1326 ariadna 5378
            'item' => get_string('unenrol', 'enrol') . ' (' . count($data->unenrolled) . ')',
1 efrain 5379
            'error' => false
5380
        );
5381
    }
5382
 
5383
    $componentstr = get_string('groups');
5384
 
5385
    // Remove all group members.
5386
    if (!empty($data->reset_groups_members)) {
5387
        groups_delete_group_members($data->courseid);
5388
        $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5389
    }
5390
 
5391
    // Remove all groups.
5392
    if (!empty($data->reset_groups_remove)) {
5393
        groups_delete_groups($data->courseid, false);
5394
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5395
    }
5396
 
5397
    // Remove all grouping members.
5398
    if (!empty($data->reset_groupings_members)) {
5399
        groups_delete_groupings_groups($data->courseid, false);
5400
        $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5401
    }
5402
 
5403
    // Remove all groupings.
5404
    if (!empty($data->reset_groupings_remove)) {
5405
        groups_delete_groupings($data->courseid, false);
5406
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5407
    }
5408
 
5409
    // Look in every instance of every module for data to delete.
5410
    $unsupportedmods = array();
1326 ariadna 5411
    if ($allmods = $DB->get_records('modules')) {
1 efrain 5412
        foreach ($allmods as $mod) {
5413
            $modname = $mod->name;
1326 ariadna 5414
            $modfile = $CFG->dirroot . '/mod/' . $modname . '/lib.php';
5415
            $moddeleteuserdata = $modname . '_reset_userdata';   // Function to delete user data.
1 efrain 5416
            if (file_exists($modfile)) {
5417
                if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5418
                    continue; // Skip mods with no instances.
5419
                }
5420
                include_once($modfile);
5421
                if (function_exists($moddeleteuserdata)) {
5422
                    $modstatus = $moddeleteuserdata($data);
5423
                    if (is_array($modstatus)) {
5424
                        $status = array_merge($status, $modstatus);
5425
                    } else {
1326 ariadna 5426
                        debugging('Module ' . $modname . ' returned incorrect staus - must be an array!');
1 efrain 5427
                    }
5428
                } else {
5429
                    $unsupportedmods[] = $mod;
5430
                }
5431
            } else {
1326 ariadna 5432
                debugging('Missing lib.php in ' . $modname . ' module!');
1 efrain 5433
            }
5434
            // Update calendar events for all modules.
5435
            course_module_bulk_update_calendar_events($modname, $data->courseid);
5436
        }
5437
        // Purge the course cache after resetting course start date. MDL-76936
5438
        if ($data->timeshift) {
5439
            course_modinfo::purge_course_cache($data->courseid);
5440
        }
5441
    }
5442
 
5443
    // Mention unsupported mods.
5444
    if (!empty($unsupportedmods)) {
5445
        foreach ($unsupportedmods as $mod) {
5446
            $status[] = array(
5447
                'component' => get_string('modulenameplural', $mod->name),
5448
                'item' => '',
5449
                'error' => get_string('resetnotimplemented')
5450
            );
5451
        }
5452
    }
5453
 
5454
    $componentstr = get_string('gradebook', 'grades');
5455
    // Reset gradebook,.
5456
    if (!empty($data->reset_gradebook_items)) {
5457
        remove_course_grades($data->courseid, false);
5458
        grade_grab_course_grades($data->courseid);
5459
        grade_regrade_final_grades($data->courseid);
5460
        $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5461
    } else if (!empty($data->reset_gradebook_grades)) {
5462
        grade_course_reset($data->courseid);
5463
        $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5464
    }
5465
    // Reset comments.
5466
    if (!empty($data->reset_comments)) {
1326 ariadna 5467
        require_once($CFG->dirroot . '/comment/lib.php');
1 efrain 5468
        comment::reset_course_page_comments($context);
5469
    }
5470
 
5471
    $event = \core\event\course_reset_ended::create($eventparams);
5472
    $event->trigger();
5473
 
5474
    return $status;
5475
}
5476
 
5477
/**
5478
 * Generate an email processing address.
5479
 *
5480
 * @param int $modid
5481
 * @param string $modargs
5482
 * @return string Returns email processing address
5483
 */
1326 ariadna 5484
function generate_email_processing_address($modid, $modargs)
5485
{
1 efrain 5486
    global $CFG;
5487
 
1326 ariadna 5488
    $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2) . $modargs;
5489
    return $header . substr(md5($header . get_site_identifier()), 0, 16) . '@' . $CFG->maildomain;
1 efrain 5490
}
5491
 
5492
/**
5493
 * ?
5494
 *
5495
 * @todo Finish documenting this function
5496
 *
5497
 * @param string $modargs
5498
 * @param string $body Currently unused
5499
 */
1326 ariadna 5500
function moodle_process_email($modargs, $body)
5501
{
1 efrain 5502
    global $DB;
5503
 
5504
    // The first char should be an unencoded letter. We'll take this as an action.
5505
    switch ($modargs[0]) {
5506
        case 'B': { // Bounce.
1326 ariadna 5507
                list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5508
                if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5509
                    // Check the half md5 of their email.
5510
                    $md5check = substr(md5($user->email), 0, 16);
5511
                    if ($md5check == substr($modargs, -16)) {
5512
                        set_bounce_count($user);
5513
                    }
5514
                    // Else maybe they've already changed it?
1 efrain 5515
                }
5516
            }
1326 ariadna 5517
            break;
5518
            // Maybe more later?
1 efrain 5519
    }
5520
}
5521
 
5522
// CORRESPONDENCE.
5523
 
5524
/**
5525
 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5526
 *
5527
 * @param string $action 'get', 'buffer', 'close' or 'flush'
5528
 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5529
 */
1326 ariadna 5530
function get_mailer($action = 'get')
5531
{
1 efrain 5532
    global $CFG;
5533
 
5534
    /** @var moodle_phpmailer $mailer */
5535
    static $mailer  = null;
5536
    static $counter = 0;
5537
 
5538
    if (!isset($CFG->smtpmaxbulk)) {
5539
        $CFG->smtpmaxbulk = 1;
5540
    }
5541
 
5542
    if ($action == 'get') {
5543
        $prevkeepalive = false;
5544
 
5545
        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5546
            if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5547
                $counter++;
5548
                // Reset the mailer.
5549
                $mailer->Priority         = 3;
5550
                $mailer->CharSet          = 'UTF-8'; // Our default.
5551
                $mailer->ContentType      = "text/plain";
5552
                $mailer->Encoding         = "8bit";
5553
                $mailer->From             = "root@localhost";
5554
                $mailer->FromName         = "Root User";
5555
                $mailer->Sender           = "";
5556
                $mailer->Subject          = "";
5557
                $mailer->Body             = "";
5558
                $mailer->AltBody          = "";
5559
                $mailer->ConfirmReadingTo = "";
5560
 
5561
                $mailer->clearAllRecipients();
5562
                $mailer->clearReplyTos();
5563
                $mailer->clearAttachments();
5564
                $mailer->clearCustomHeaders();
5565
                return $mailer;
5566
            }
5567
 
5568
            $prevkeepalive = $mailer->SMTPKeepAlive;
5569
            get_mailer('flush');
5570
        }
5571
 
1326 ariadna 5572
        require_once($CFG->libdir . '/phpmailer/moodle_phpmailer.php');
1 efrain 5573
        $mailer = new moodle_phpmailer();
5574
 
5575
        $counter = 1;
5576
 
5577
        if ($CFG->smtphosts == 'qmail') {
5578
            // Use Qmail system.
5579
            $mailer->isQmail();
5580
        } else if (empty($CFG->smtphosts)) {
5581
            // Use PHP mail() = sendmail.
5582
            $mailer->isMail();
5583
        } else {
5584
            // Use SMTP directly.
5585
            $mailer->isSMTP();
5586
            if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5587
                $mailer->SMTPDebug = 3;
5588
            }
5589
            // Specify main and backup servers.
5590
            $mailer->Host          = $CFG->smtphosts;
5591
            // Specify secure connection protocol.
5592
            $mailer->SMTPSecure    = $CFG->smtpsecure;
5593
            // Use previous keepalive.
5594
            $mailer->SMTPKeepAlive = $prevkeepalive;
5595
 
5596
            if ($CFG->smtpuser) {
5597
                // Use SMTP authentication.
5598
                $mailer->SMTPAuth = true;
5599
                $mailer->Username = $CFG->smtpuser;
5600
                $mailer->Password = $CFG->smtppass;
5601
            }
5602
        }
5603
 
5604
        return $mailer;
5605
    }
5606
 
5607
    $nothing = null;
5608
 
5609
    // Keep smtp session open after sending.
5610
    if ($action == 'buffer') {
5611
        if (!empty($CFG->smtpmaxbulk)) {
5612
            get_mailer('flush');
5613
            $m = get_mailer();
5614
            if ($m->Mailer == 'smtp') {
5615
                $m->SMTPKeepAlive = true;
5616
            }
5617
        }
5618
        return $nothing;
5619
    }
5620
 
5621
    // Close smtp session, but continue buffering.
5622
    if ($action == 'flush') {
5623
        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5624
            if (!empty($mailer->SMTPDebug)) {
1326 ariadna 5625
                echo '<pre>' . "\n";
1 efrain 5626
            }
5627
            $mailer->SmtpClose();
5628
            if (!empty($mailer->SMTPDebug)) {
5629
                echo '</pre>';
5630
            }
5631
        }
5632
        return $nothing;
5633
    }
5634
 
5635
    // Close smtp session, do not buffer anymore.
5636
    if ($action == 'close') {
5637
        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5638
            get_mailer('flush');
5639
            $mailer->SMTPKeepAlive = false;
5640
        }
5641
        $mailer = null; // Better force new instance.
5642
        return $nothing;
5643
    }
5644
}
5645
 
5646
/**
5647
 * A helper function to test for email diversion
5648
 *
5649
 * @param string $email
5650
 * @return bool Returns true if the email should be diverted
5651
 */
1326 ariadna 5652
function email_should_be_diverted($email)
5653
{
1 efrain 5654
    global $CFG;
5655
 
5656
    if (empty($CFG->divertallemailsto)) {
5657
        return false;
5658
    }
5659
 
5660
    if (empty($CFG->divertallemailsexcept)) {
5661
        return true;
5662
    }
5663
 
5664
    $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY));
5665
    foreach ($patterns as $pattern) {
5666
        if (preg_match("/{$pattern}/i", $email)) {
5667
            return false;
5668
        }
5669
    }
5670
 
5671
    return true;
5672
}
5673
 
5674
/**
5675
 * Generate a unique email Message-ID using the moodle domain and install path
5676
 *
5677
 * @param string $localpart An optional unique message id prefix.
5678
 * @return string The formatted ID ready for appending to the email headers.
5679
 */
1326 ariadna 5680
function generate_email_messageid($localpart = null)
5681
{
1 efrain 5682
    global $CFG;
5683
 
5684
    $urlinfo = parse_url($CFG->wwwroot);
5685
    $base = '@' . $urlinfo['host'];
5686
 
5687
    // If multiple moodles are on the same domain we want to tell them
5688
    // apart so we add the install path to the local part. This means
5689
    // that the id local part should never contain a / character so
5690
    // we can correctly parse the id to reassemble the wwwroot.
5691
    if (isset($urlinfo['path'])) {
5692
        $base = $urlinfo['path'] . $base;
5693
    }
5694
 
5695
    if (empty($localpart)) {
5696
        $localpart = uniqid('', true);
5697
    }
5698
 
5699
    // Because we may have an option /installpath suffix to the local part
5700
    // of the id we need to escape any / chars which are in the $localpart.
5701
    $localpart = str_replace('/', '%2F', $localpart);
5702
 
5703
    return '<' . $localpart . $base . '>';
5704
}
5705
 
5706
/**
5707
 * Send an email to a specified user
5708
 *
5709
 * @param stdClass $user  A {@link $USER} object
5710
 * @param stdClass $from A {@link $USER} object
5711
 * @param string $subject plain text subject line of the email
5712
 * @param string $messagetext plain text version of the message
5713
 * @param string $messagehtml complete html version of the message (optional)
5714
 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
5715
 *          the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
5716
 * @param string $attachname the name of the file (extension indicates MIME)
5717
 * @param bool $usetrueaddress determines whether $from email address should
5718
 *          be sent out. Will be overruled by user profile setting for maildisplay
5719
 * @param string $replyto Email address to reply to
5720
 * @param string $replytoname Name of reply to recipient
5721
 * @param int $wordwrapwidth custom word wrap width, default 79
5722
 * @return bool Returns true if mail was sent OK and false if there was an error.
5723
 */
1326 ariadna 5724
function email_to_user(
5725
    $user,
5726
    $from,
5727
    $subject,
5728
    $messagetext,
5729
    $messagehtml = '',
5730
    $attachment = '',
5731
    $attachname = '',
5732
    $usetrueaddress = true,
5733
    $replyto = '',
5734
    $replytoname = '',
5735
    $wordwrapwidth = 79
5736
) {
1 efrain 5737
 
5738
    global $CFG, $PAGE, $SITE;
5739
 
5740
    if (empty($user) or empty($user->id)) {
5741
        debugging('Can not send email to null user', DEBUG_DEVELOPER);
5742
        return false;
5743
    }
5744
 
5745
    if (empty($user->email)) {
1326 ariadna 5746
        debugging('Can not send email to user without email: ' . $user->id, DEBUG_DEVELOPER);
1 efrain 5747
        return false;
5748
    }
5749
 
5750
    if (!empty($user->deleted)) {
1326 ariadna 5751
        debugging('Can not send email to deleted user: ' . $user->id, DEBUG_DEVELOPER);
1 efrain 5752
        return false;
5753
    }
5754
 
5755
    if (defined('BEHAT_SITE_RUNNING')) {
5756
        // Fake email sending in behat.
5757
        return true;
5758
    }
5759
 
5760
    if (!empty($CFG->noemailever)) {
5761
        // Hidden setting for development sites, set in config.php if needed.
5762
        debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
5763
        return true;
5764
    }
5765
 
5766
    if (email_should_be_diverted($user->email)) {
5767
        $subject = "[DIVERTED {$user->email}] $subject";
1326 ariadna 5768
        $user = clone ($user);
1 efrain 5769
        $user->email = $CFG->divertallemailsto;
5770
    }
5771
 
5772
    // Skip mail to suspended users.
1326 ariadna 5773
    if ((isset($user->auth) && $user->auth == 'nologin') or (isset($user->suspended) && $user->suspended)) {
1 efrain 5774
        return true;
5775
    }
5776
 
5777
    if (!validate_email($user->email)) {
5778
        // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
1326 ariadna 5779
        debugging("email_to_user: User $user->id (" . fullname($user) . ") email ($user->email) is invalid! Not sending.");
1 efrain 5780
        return false;
5781
    }
5782
 
5783
    if (over_bounce_threshold($user)) {
1326 ariadna 5784
        debugging("email_to_user: User $user->id (" . fullname($user) . ") is over bounce threshold! Not sending.");
1 efrain 5785
        return false;
5786
    }
5787
 
5788
    // TLD .invalid  is specifically reserved for invalid domain names.
5789
    // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
5790
    if (substr($user->email, -8) == '.invalid') {
1326 ariadna 5791
        debugging("email_to_user: User $user->id (" . fullname($user) . ") email domain ($user->email) is invalid! Not sending.");
1 efrain 5792
        return true; // This is not an error.
5793
    }
5794
 
5795
    // If the user is a remote mnet user, parse the email text for URL to the
5796
    // wwwroot and modify the url to direct the user's browser to login at their
5797
    // home site (identity provider - idp) before hitting the link itself.
5798
    if (is_mnet_remote_user($user)) {
1326 ariadna 5799
        require_once($CFG->dirroot . '/mnet/lib.php');
1 efrain 5800
 
5801
        $jumpurl = mnet_get_idp_jump_url($user);
5802
        $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5803
 
1326 ariadna 5804
        $messagetext = preg_replace_callback(
5805
            "%($CFG->wwwroot[^[:space:]]*)%",
5806
            $callback,
5807
            $messagetext
5808
        );
5809
        $messagehtml = preg_replace_callback(
5810
            "%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5811
            $callback,
5812
            $messagehtml
5813
        );
1 efrain 5814
    }
5815
    $mail = get_mailer();
5816
 
5817
    if (!empty($mail->SMTPDebug)) {
5818
        echo '<pre>' . "\n";
5819
    }
5820
 
5821
    $temprecipients = array();
5822
    $tempreplyto = array();
5823
 
5824
    // Make sure that we fall back onto some reasonable no-reply address.
5825
    $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
5826
    $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
5827
 
5828
    if (!validate_email($noreplyaddress)) {
1326 ariadna 5829
        debugging('email_to_user: Invalid noreply-email ' . s($noreplyaddress));
1 efrain 5830
        $noreplyaddress = $noreplyaddressdefault;
5831
    }
5832
 
5833
    // Make up an email address for handling bounces.
5834
    if (!empty($CFG->handlebounces)) {
1326 ariadna 5835
        $modargs = 'B' . base64_encode(pack('V', $user->id)) . substr(md5($user->email), 0, 16);
1 efrain 5836
        $mail->Sender = generate_email_processing_address(0, $modargs);
5837
    } else {
5838
        $mail->Sender = $noreplyaddress;
5839
    }
5840
 
5841
    // Make sure that the explicit replyto is valid, fall back to the implicit one.
5842
    if (!empty($replyto) && !validate_email($replyto)) {
1326 ariadna 5843
        debugging('email_to_user: Invalid replyto-email ' . s($replyto));
1 efrain 5844
        $replyto = $noreplyaddress;
5845
    }
5846
 
5847
    if (is_string($from)) { // So we can pass whatever we want if there is need.
5848
        $mail->From     = $noreplyaddress;
5849
        $mail->FromName = $from;
1326 ariadna 5850
        // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
5851
        // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
5852
        // in a course with the sender.
1 efrain 5853
    } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
5854
        if (!validate_email($from->email)) {
1326 ariadna 5855
            debugging('email_to_user: Invalid from-email ' . s($from->email) . ' - not sending');
1 efrain 5856
            // Better not to use $noreplyaddress in this case.
5857
            return false;
5858
        }
5859
        $mail->From = $from->email;
5860
        $fromdetails = new stdClass();
5861
        $fromdetails->name = fullname($from);
5862
        $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5863
        $fromdetails->siteshortname = format_string($SITE->shortname);
5864
        $fromstring = $fromdetails->name;
5865
        if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
5866
            $fromstring = get_string('emailvia', 'core', $fromdetails);
5867
        }
5868
        $mail->FromName = $fromstring;
5869
        if (empty($replyto)) {
5870
            $tempreplyto[] = array($from->email, fullname($from));
5871
        }
5872
    } else {
5873
        $mail->From = $noreplyaddress;
5874
        $fromdetails = new stdClass();
5875
        $fromdetails->name = fullname($from);
5876
        $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5877
        $fromdetails->siteshortname = format_string($SITE->shortname);
5878
        $fromstring = $fromdetails->name;
5879
        if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
5880
            $fromstring = get_string('emailvia', 'core', $fromdetails);
5881
        }
5882
        $mail->FromName = $fromstring;
5883
        if (empty($replyto)) {
5884
            $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
5885
        }
5886
    }
5887
 
5888
    if (!empty($replyto)) {
5889
        $tempreplyto[] = array($replyto, $replytoname);
5890
    }
5891
 
5892
    $temprecipients[] = array($user->email, fullname($user));
5893
 
5894
    // Set word wrap.
5895
    $mail->WordWrap = $wordwrapwidth;
5896
 
5897
    if (!empty($from->customheaders)) {
5898
        // Add custom headers.
5899
        if (is_array($from->customheaders)) {
5900
            foreach ($from->customheaders as $customheader) {
5901
                $mail->addCustomHeader($customheader);
5902
            }
5903
        } else {
5904
            $mail->addCustomHeader($from->customheaders);
5905
        }
5906
    }
5907
 
5908
    // If the X-PHP-Originating-Script email header is on then also add an additional
5909
    // header with details of where exactly in moodle the email was triggered from,
5910
    // either a call to message_send() or to email_to_user().
5911
    if (ini_get('mail.add_x_header')) {
5912
 
5913
        $stack = debug_backtrace(false);
5914
        $origin = $stack[0];
5915
 
5916
        foreach ($stack as $depth => $call) {
5917
            if ($call['function'] == 'message_send') {
5918
                $origin = $call;
5919
            }
5920
        }
5921
 
5922
        $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
1326 ariadna 5923
            . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
1 efrain 5924
        $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
5925
    }
5926
 
5927
    if (!empty($CFG->emailheaders)) {
5928
        $headers = array_map('trim', explode("\n", $CFG->emailheaders));
5929
        foreach ($headers as $header) {
5930
            if (!empty($header)) {
5931
                $mail->addCustomHeader($header);
5932
            }
5933
        }
5934
    }
5935
 
5936
    if (!empty($from->priority)) {
5937
        $mail->Priority = $from->priority;
5938
    }
5939
 
5940
    $renderer = $PAGE->get_renderer('core');
5941
    $context = array(
5942
        'sitefullname' => $SITE->fullname,
5943
        'siteshortname' => $SITE->shortname,
5944
        'sitewwwroot' => $CFG->wwwroot,
5945
        'subject' => $subject,
5946
        'prefix' => $CFG->emailsubjectprefix,
5947
        'to' => $user->email,
5948
        'toname' => fullname($user),
5949
        'from' => $mail->From,
5950
        'fromname' => $mail->FromName,
5951
    );
5952
    if (!empty($tempreplyto[0])) {
5953
        $context['replyto'] = $tempreplyto[0][0];
5954
        $context['replytoname'] = $tempreplyto[0][1];
5955
    }
5956
    if ($user->id > 0) {
5957
        $context['touserid'] = $user->id;
5958
        $context['tousername'] = $user->username;
5959
    }
5960
 
5961
    if (!empty($user->mailformat) && $user->mailformat == 1) {
5962
        // Only process html templates if the user preferences allow html email.
5963
 
5964
        if (!$messagehtml) {
5965
            // If no html has been given, BUT there is an html wrapping template then
5966
            // auto convert the text to html and then wrap it.
5967
            $messagehtml = trim(text_to_html($messagetext));
5968
        }
5969
        $context['body'] = $messagehtml;
5970
        $messagehtml = $renderer->render_from_template('core/email_html', $context);
5971
    }
5972
 
5973
    $context['body'] = html_to_text(nl2br($messagetext));
5974
    $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
5975
    $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
5976
    $messagetext = $renderer->render_from_template('core/email_text', $context);
5977
 
5978
    // Autogenerate a MessageID if it's missing.
5979
    if (empty($mail->MessageID)) {
5980
        $mail->MessageID = generate_email_messageid();
5981
    }
5982
 
5983
    if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
1333 ariadna 5984
        // Obtener las URLs correctamente
1330 ariadna 5985
        $themeconfig = \theme_config::load('universe_child');
1326 ariadna 5986
 
1333 ariadna 5987
        $header_image_url = $themeconfig->image_url('logo-horizontal-cesa', 'theme')->out(false);
5988
        $footer_image_url = $themeconfig->image_url('email-footer', 'theme')->out(false);
5989
 
5990
        // Armar HTML del header
1326 ariadna 5991
        $header_html = '<div style="text-align:center; margin-bottom:20px;">'
5992
            . '<img src="' . $header_image_url . '" alt="Header Image" style="max-width:100%; height:auto;">'
5993
            . '</div>';
5994
 
1333 ariadna 5995
        // Armar HTML del footer
1326 ariadna 5996
        $footer_html = '<div style="text-align:center; margin-top:40px;">'
5997
            . '<img src="' . $footer_image_url . '" alt="Footer Image" style="max-width:100%; height:auto;">'
5998
            . '</div>';
5999
 
1333 ariadna 6000
        // Armamos el nuevo body: header + contenido original + footer
6001
        $full_html_message = $header_html . $messagehtml . $footer_html;
6002
 
1327 ariadna 6003
        $mail->isHTML(true);
6004
        $mail->Encoding = 'quoted-printable';
1333 ariadna 6005
        $mail->Body = $full_html_message; // AQUÍ usamos el mensaje completo
6006
        $mail->AltBody = strip_tags($header_html) . "\n" . $messagetext . "\n" . strip_tags($footer_html); // Texto plano alternativo
1 efrain 6007
    } else {
6008
        $mail->IsHTML(false);
6009
        $mail->Body =  "\n$messagetext\n";
6010
    }
6011
 
6012
    if ($attachment && $attachname) {
1326 ariadna 6013
        if (preg_match("~\\.\\.~", $attachment)) {
1 efrain 6014
            // Security check for ".." in dir path.
6015
            $supportuser = core_user::get_support_user();
6016
            $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
6017
            $mail->addStringAttachment('Error in attachment.  User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
6018
        } else {
1326 ariadna 6019
            require_once($CFG->libdir . '/filelib.php');
1 efrain 6020
            $mimetype = mimeinfo('type', $attachname);
6021
 
6022
            // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
6023
            // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
6024
            $attachpath = str_replace('\\', '/', realpath($attachment));
6025
 
6026
            // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
1326 ariadna 6027
            $allowedpaths = array_map(function (string $path): string {
1 efrain 6028
                return str_replace('\\', '/', realpath($path));
6029
            }, [
6030
                $CFG->cachedir,
6031
                $CFG->dataroot,
6032
                $CFG->dirroot,
6033
                $CFG->localcachedir,
6034
                $CFG->tempdir,
6035
                $CFG->localrequestdir,
6036
            ]);
6037
 
6038
            // Set addpath to true.
6039
            $addpath = true;
6040
 
6041
            // Check if attachment includes one of the allowed paths.
6042
            foreach (array_filter($allowedpaths) as $allowedpath) {
6043
                // Set addpath to false if the attachment includes one of the allowed paths.
6044
                if (strpos($attachpath, $allowedpath) === 0) {
6045
                    $addpath = false;
6046
                    break;
6047
                }
6048
            }
6049
 
6050
            // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
6051
            // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
6052
            if ($addpath == true) {
6053
                $attachment = $CFG->dataroot . '/' . $attachment;
6054
            }
6055
 
6056
            $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
6057
        }
6058
    }
6059
 
6060
    // Check if the email should be sent in an other charset then the default UTF-8.
6061
    if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
6062
 
6063
        // Use the defined site mail charset or eventually the one preferred by the recipient.
6064
        $charset = $CFG->sitemailcharset;
6065
        if (!empty($CFG->allowusermailcharset)) {
6066
            if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
6067
                $charset = $useremailcharset;
6068
            }
6069
        }
6070
 
6071
        // Convert all the necessary strings if the charset is supported.
6072
        $charsets = get_list_of_charsets();
6073
        unset($charsets['UTF-8']);
6074
        if (in_array($charset, $charsets)) {
6075
            $mail->CharSet  = $charset;
6076
            $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
6077
            $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
6078
            $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
6079
            $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
6080
 
6081
            foreach ($temprecipients as $key => $values) {
6082
                $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6083
            }
6084
            foreach ($tempreplyto as $key => $values) {
6085
                $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6086
            }
6087
        }
6088
    }
6089
 
6090
    foreach ($temprecipients as $values) {
6091
        $mail->addAddress($values[0], $values[1]);
6092
    }
6093
    foreach ($tempreplyto as $values) {
6094
        $mail->addReplyTo($values[0], $values[1]);
6095
    }
6096
 
6097
    if (!empty($CFG->emaildkimselector)) {
6098
        $domain = substr(strrchr($mail->From, "@"), 1);
6099
        $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
6100
        if (file_exists($pempath)) {
6101
            $mail->DKIM_domain      = $domain;
6102
            $mail->DKIM_private     = $pempath;
6103
            $mail->DKIM_selector    = $CFG->emaildkimselector;
6104
            $mail->DKIM_identity    = $mail->From;
6105
        } else {
6106
            debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
6107
        }
6108
    }
6109
 
6110
    if ($mail->send()) {
6111
        set_send_count($user);
6112
        if (!empty($mail->SMTPDebug)) {
6113
            echo '</pre>';
6114
        }
6115
        return true;
6116
    } else {
6117
        // Trigger event for failing to send email.
6118
        $event = \core\event\email_failed::create(array(
6119
            'context' => context_system::instance(),
6120
            'userid' => $from->id,
6121
            'relateduserid' => $user->id,
6122
            'other' => array(
6123
                'subject' => $subject,
6124
                'message' => $messagetext,
6125
                'errorinfo' => $mail->ErrorInfo
6126
            )
6127
        ));
6128
        $event->trigger();
6129
        if (CLI_SCRIPT) {
1326 ariadna 6130
            mtrace('Error: lib/moodlelib.php email_to_user(): ' . $mail->ErrorInfo);
1 efrain 6131
        }
6132
        if (!empty($mail->SMTPDebug)) {
6133
            echo '</pre>';
6134
        }
6135
        return false;
6136
    }
6137
}
6138
 
6139
/**
6140
 * Check to see if a user's real email address should be used for the "From" field.
6141
 *
6142
 * @param  object $from The user object for the user we are sending the email from.
6143
 * @param  object $user The user object that we are sending the email to.
6144
 * @param  array $unused No longer used.
6145
 * @return bool Returns true if we can use the from user's email adress in the "From" field.
6146
 */
1326 ariadna 6147
function can_send_from_real_email_address($from, $user, $unused = null)
6148
{
1 efrain 6149
    global $CFG;
6150
    if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
6151
        return false;
6152
    }
6153
    $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
6154
    // Email is in the list of allowed domains for sending email,
6155
    // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6156
    // in a course with the sender.
1326 ariadna 6157
    if (
6158
        \core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
6159
        && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
6160
            || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
6161
                && enrol_get_shared_courses($user, $from, false, true)))
6162
    ) {
1 efrain 6163
        return true;
6164
    }
6165
    return false;
6166
}
6167
 
6168
/**
6169
 * Generate a signoff for emails based on support settings
6170
 *
6171
 * @return string
6172
 */
1326 ariadna 6173
function generate_email_signoff()
6174
{
1 efrain 6175
    global $CFG, $OUTPUT;
6176
 
6177
    $signoff = "\n";
6178
    if (!empty($CFG->supportname)) {
1326 ariadna 6179
        $signoff .= $CFG->supportname . "\n";
1 efrain 6180
    }
6181
 
6182
    $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']);
6183
 
6184
    if ($supportemail) {
6185
        $signoff .= "\n" . $supportemail . "\n";
6186
    }
6187
 
6188
    return $signoff;
6189
}
6190
 
6191
/**
6192
 * Sets specified user's password and send the new password to the user via email.
6193
 *
6194
 * @param stdClass $user A {@link $USER} object
6195
 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6196
 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6197
 */
1326 ariadna 6198
function setnew_password_and_mail($user, $fasthash = false)
6199
{
1 efrain 6200
    global $CFG, $DB;
6201
 
6202
    // We try to send the mail in language the user understands,
6203
    // unfortunately the filter_string() does not support alternative langs yet
6204
    // so multilang will not work properly for site->fullname.
6205
    $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6206
 
6207
    $site  = get_site();
6208
 
6209
    $supportuser = core_user::get_support_user();
6210
 
6211
    $newpassword = generate_password();
6212
 
6213
    update_internal_user_password($user, $newpassword, $fasthash);
6214
 
6215
    $a = new stdClass();
6216
    $a->firstname   = fullname($user, true);
6217
    $a->sitename    = format_string($site->fullname);
6218
    $a->username    = $user->username;
6219
    $a->newpassword = $newpassword;
1326 ariadna 6220
    $a->link        = $CFG->wwwroot . '/login/?lang=' . $lang;
1 efrain 6221
    $a->signoff     = generate_email_signoff();
6222
 
6223
    $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6224
 
1326 ariadna 6225
    $subject = format_string($site->fullname) . ': ' . (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
1 efrain 6226
 
6227
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6228
    return email_to_user($user, $supportuser, $subject, $message);
6229
}
6230
 
6231
/**
6232
 * Resets specified user's password and send the new password to the user via email.
6233
 *
6234
 * @param stdClass $user A {@link $USER} object
6235
 * @return bool Returns true if mail was sent OK and false if there was an error.
6236
 */
1326 ariadna 6237
function reset_password_and_mail($user)
6238
{
1 efrain 6239
    global $CFG;
6240
 
6241
    $site  = get_site();
6242
    $supportuser = core_user::get_support_user();
6243
 
6244
    $userauth = get_auth_plugin($user->auth);
6245
    if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6246
        trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6247
        return false;
6248
    }
6249
 
6250
    $newpassword = generate_password();
6251
 
6252
    if (!$userauth->user_update_password($user, $newpassword)) {
6253
        throw new \moodle_exception("cannotsetpassword");
6254
    }
6255
 
6256
    $a = new stdClass();
6257
    $a->firstname   = $user->firstname;
6258
    $a->lastname    = $user->lastname;
6259
    $a->sitename    = format_string($site->fullname);
6260
    $a->username    = $user->username;
6261
    $a->newpassword = $newpassword;
1326 ariadna 6262
    $a->link        = $CFG->wwwroot . '/login/change_password.php';
1 efrain 6263
    $a->signoff     = generate_email_signoff();
6264
 
6265
    $message = get_string('newpasswordtext', '', $a);
6266
 
1326 ariadna 6267
    $subject  = format_string($site->fullname) . ': ' . get_string('changedpassword');
1 efrain 6268
 
6269
    unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6270
 
6271
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6272
    return email_to_user($user, $supportuser, $subject, $message);
6273
}
6274
 
6275
/**
6276
 * Send email to specified user with confirmation text and activation link.
6277
 *
6278
 * @param stdClass $user A {@link $USER} object
6279
 * @param string $confirmationurl user confirmation URL
6280
 * @return bool Returns true if mail was sent OK and false if there was an error.
6281
 */
1326 ariadna 6282
function send_confirmation_email($user, $confirmationurl = null)
6283
{
1 efrain 6284
    global $CFG;
6285
 
6286
    $site = get_site();
6287
    $supportuser = core_user::get_support_user();
6288
 
6289
    $data = new stdClass();
6290
    $data->sitename  = format_string($site->fullname);
6291
    $data->admin     = generate_email_signoff();
6292
 
6293
    $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6294
 
6295
    if (empty($confirmationurl)) {
6296
        $confirmationurl = '/login/confirm.php';
6297
    }
6298
 
6299
    $confirmationurl = new moodle_url($confirmationurl);
6300
    // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6301
    $confirmationurl->remove_params('data');
6302
    $confirmationpath = $confirmationurl->out(false);
6303
 
6304
    // We need to custom encode the username to include trailing dots in the link.
6305
    // Because of this custom encoding we can't use moodle_url directly.
6306
    // Determine if a query string is present in the confirmation url.
6307
    $hasquerystring = strpos($confirmationpath, '?') !== false;
6308
    // Perform normal url encoding of the username first.
6309
    $username = urlencode($user->username);
6310
    // Prevent problems with trailing dots not being included as part of link in some mail clients.
6311
    $username = str_replace('.', '%2E', $username);
6312
 
1326 ariadna 6313
    $data->link = $confirmationpath . ($hasquerystring ? '&' : '?') . 'data=' . $user->secret . '/' . $username;
1 efrain 6314
 
6315
    $message     = get_string('emailconfirmation', '', $data);
6316
    $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6317
 
6318
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6319
    return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6320
}
6321
 
6322
/**
6323
 * Sends a password change confirmation email.
6324
 *
6325
 * @param stdClass $user A {@link $USER} object
6326
 * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6327
 * @return bool Returns true if mail was sent OK and false if there was an error.
6328
 */
1326 ariadna 6329
function send_password_change_confirmation_email($user, $resetrecord)
6330
{
1 efrain 6331
    global $CFG;
6332
 
6333
    $site = get_site();
6334
    $supportuser = core_user::get_support_user();
6335
    $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6336
 
6337
    $data = new stdClass();
6338
    $data->firstname = $user->firstname;
6339
    $data->lastname  = $user->lastname;
6340
    $data->username  = $user->username;
6341
    $data->sitename  = format_string($site->fullname);
1326 ariadna 6342
    $data->link      = $CFG->wwwroot . '/login/forgot_password.php?token=' . $resetrecord->token;
1 efrain 6343
    $data->admin     = generate_email_signoff();
6344
    $data->resetminutes = $pwresetmins;
6345
 
6346
    $message = get_string('emailresetconfirmation', '', $data);
6347
    $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6348
 
6349
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6350
    return email_to_user($user, $supportuser, $subject, $message);
6351
}
6352
 
6353
/**
6354
 * Sends an email containing information on how to change your password.
6355
 *
6356
 * @param stdClass $user A {@link $USER} object
6357
 * @return bool Returns true if mail was sent OK and false if there was an error.
6358
 */
1326 ariadna 6359
function send_password_change_info($user)
6360
{
1 efrain 6361
    $site = get_site();
6362
    $supportuser = core_user::get_support_user();
6363
 
6364
    $data = new stdClass();
6365
    $data->firstname = $user->firstname;
6366
    $data->lastname  = $user->lastname;
6367
    $data->username  = $user->username;
6368
    $data->sitename  = format_string($site->fullname);
6369
    $data->admin     = generate_email_signoff();
6370
 
6371
    if (!is_enabled_auth($user->auth)) {
6372
        $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6373
        $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6374
        // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6375
        return email_to_user($user, $supportuser, $subject, $message);
6376
    }
6377
 
6378
    $userauth = get_auth_plugin($user->auth);
6379
    ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6380
 
6381
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6382
    return email_to_user($user, $supportuser, $subject, $message);
6383
}
6384
 
6385
/**
6386
 * Check that an email is allowed.  It returns an error message if there was a problem.
6387
 *
6388
 * @param string $email Content of email
6389
 * @return string|false
6390
 */
1326 ariadna 6391
function email_is_not_allowed($email)
6392
{
1 efrain 6393
    global $CFG;
6394
 
6395
    // Comparing lowercase domains.
6396
    $email = strtolower($email);
6397
    if (!empty($CFG->allowemailaddresses)) {
6398
        $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6399
        foreach ($allowed as $allowedpattern) {
6400
            $allowedpattern = trim($allowedpattern);
6401
            if (!$allowedpattern) {
6402
                continue;
6403
            }
6404
            if (strpos($allowedpattern, '.') === 0) {
6405
                if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6406
                    // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6407
                    return false;
6408
                }
1326 ariadna 6409
            } else if (strpos(strrev($email), strrev('@' . $allowedpattern)) === 0) {
1 efrain 6410
                return false;
6411
            }
6412
        }
6413
        return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6414
    } else if (!empty($CFG->denyemailaddresses)) {
6415
        $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6416
        foreach ($denied as $deniedpattern) {
6417
            $deniedpattern = trim($deniedpattern);
6418
            if (!$deniedpattern) {
6419
                continue;
6420
            }
6421
            if (strpos($deniedpattern, '.') === 0) {
6422
                if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6423
                    // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6424
                    return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6425
                }
1326 ariadna 6426
            } else if (strpos(strrev($email), strrev('@' . $deniedpattern)) === 0) {
1 efrain 6427
                return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6428
            }
6429
        }
6430
    }
6431
 
6432
    return false;
6433
}
6434
 
6435
// FILE HANDLING.
6436
 
6437
/**
6438
 * Returns local file storage instance
6439
 *
6440
 * @return ?file_storage
6441
 */
1326 ariadna 6442
function get_file_storage($reset = false)
6443
{
1 efrain 6444
    global $CFG;
6445
 
6446
    static $fs = null;
6447
 
6448
    if ($reset) {
6449
        $fs = null;
6450
        return;
6451
    }
6452
 
6453
    if ($fs) {
6454
        return $fs;
6455
    }
6456
 
6457
    require_once("$CFG->libdir/filelib.php");
6458
 
6459
    $fs = new file_storage();
6460
 
6461
    return $fs;
6462
}
6463
 
6464
/**
6465
 * Returns local file storage instance
6466
 *
6467
 * @return file_browser
6468
 */
1326 ariadna 6469
function get_file_browser()
6470
{
1 efrain 6471
    global $CFG;
6472
 
6473
    static $fb = null;
6474
 
6475
    if ($fb) {
6476
        return $fb;
6477
    }
6478
 
6479
    require_once("$CFG->libdir/filelib.php");
6480
 
6481
    $fb = new file_browser();
6482
 
6483
    return $fb;
6484
}
6485
 
6486
/**
6487
 * Returns file packer
6488
 *
6489
 * @param string $mimetype default application/zip
6490
 * @return file_packer|false
6491
 */
1326 ariadna 6492
function get_file_packer($mimetype = 'application/zip')
6493
{
1 efrain 6494
    global $CFG;
6495
 
6496
    static $fp = array();
6497
 
6498
    if (isset($fp[$mimetype])) {
6499
        return $fp[$mimetype];
6500
    }
6501
 
6502
    switch ($mimetype) {
6503
        case 'application/zip':
6504
        case 'application/vnd.moodle.profiling':
6505
            $classname = 'zip_packer';
6506
            break;
6507
 
1326 ariadna 6508
        case 'application/x-gzip':
1 efrain 6509
            $classname = 'tgz_packer';
6510
            break;
6511
 
6512
        case 'application/vnd.moodle.backup':
6513
            $classname = 'mbz_packer';
6514
            break;
6515
 
6516
        default:
6517
            return false;
6518
    }
6519
 
6520
    require_once("$CFG->libdir/filestorage/$classname.php");
6521
    $fp[$mimetype] = new $classname();
6522
 
6523
    return $fp[$mimetype];
6524
}
6525
 
6526
/**
6527
 * Returns current name of file on disk if it exists.
6528
 *
6529
 * @param string $newfile File to be verified
6530
 * @return string Current name of file on disk if true
6531
 */
1326 ariadna 6532
function valid_uploaded_file($newfile)
6533
{
1 efrain 6534
    if (empty($newfile)) {
6535
        return '';
6536
    }
6537
    if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6538
        return $newfile['tmp_name'];
6539
    } else {
6540
        return '';
6541
    }
6542
}
6543
 
6544
/**
6545
 * Returns the maximum size for uploading files.
6546
 *
6547
 * There are seven possible upload limits:
6548
 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6549
 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6550
 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6551
 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6552
 * 5. by the Moodle admin in $CFG->maxbytes
6553
 * 6. by the teacher in the current course $course->maxbytes
6554
 * 7. by the teacher for the current module, eg $assignment->maxbytes
6555
 *
6556
 * These last two are passed to this function as arguments (in bytes).
6557
 * Anything defined as 0 is ignored.
6558
 * The smallest of all the non-zero numbers is returned.
6559
 *
6560
 * @todo Finish documenting this function
6561
 *
6562
 * @param int $sitebytes Set maximum size
6563
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6564
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6565
 * @param bool $unused This parameter has been deprecated and is not used any more.
6566
 * @return int The maximum size for uploading files.
6567
 */
1326 ariadna 6568
function get_max_upload_file_size($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $unused = false)
6569
{
1 efrain 6570
 
6571
    if (! $filesize = ini_get('upload_max_filesize')) {
6572
        $filesize = '5M';
6573
    }
6574
    $minimumsize = get_real_size($filesize);
6575
 
6576
    if ($postsize = ini_get('post_max_size')) {
6577
        $postsize = get_real_size($postsize);
6578
        if ($postsize < $minimumsize) {
6579
            $minimumsize = $postsize;
6580
        }
6581
    }
6582
 
6583
    if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6584
        $minimumsize = $sitebytes;
6585
    }
6586
 
6587
    if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6588
        $minimumsize = $coursebytes;
6589
    }
6590
 
6591
    if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6592
        $minimumsize = $modulebytes;
6593
    }
6594
 
6595
    return $minimumsize;
6596
}
6597
 
6598
/**
6599
 * Returns the maximum size for uploading files for the current user
6600
 *
6601
 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6602
 *
6603
 * @param context $context The context in which to check user capabilities
6604
 * @param int $sitebytes Set maximum size
6605
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6606
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6607
 * @param stdClass|int|null $user The user
6608
 * @param bool $unused This parameter has been deprecated and is not used any more.
6609
 * @return int The maximum size for uploading files.
6610
 */
1326 ariadna 6611
function get_user_max_upload_file_size(
6612
    $context,
6613
    $sitebytes = 0,
6614
    $coursebytes = 0,
6615
    $modulebytes = 0,
6616
    $user = null,
6617
    $unused = false
6618
) {
1 efrain 6619
    global $USER;
6620
 
6621
    if (empty($user)) {
6622
        $user = $USER;
6623
    }
6624
 
6625
    if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6626
        return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6627
    }
6628
 
6629
    return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6630
}
6631
 
6632
/**
6633
 * Returns an array of possible sizes in local language
6634
 *
6635
 * Related to {@link get_max_upload_file_size()} - this function returns an
6636
 * array of possible sizes in an array, translated to the
6637
 * local language.
6638
 *
6639
 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6640
 *
6641
 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6642
 * with the value set to 0. This option will be the first in the list.
6643
 *
6644
 * @uses SORT_NUMERIC
6645
 * @param int $sitebytes Set maximum size
6646
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6647
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6648
 * @param int|array $custombytes custom upload size/s which will be added to list,
6649
 *        Only value/s smaller then maxsize will be added to list.
6650
 * @return array
6651
 */
1326 ariadna 6652
function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null)
6653
{
1 efrain 6654
    global $CFG;
6655
 
6656
    if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6657
        return array();
6658
    }
6659
 
6660
    if ($sitebytes == 0) {
6661
        // Will get the minimum of upload_max_filesize or post_max_size.
6662
        $sitebytes = get_max_upload_file_size();
6663
    }
6664
 
6665
    $filesize = array();
1326 ariadna 6666
    $sizelist = array(
6667
        10240,
6668
        51200,
6669
        102400,
6670
        512000,
6671
        1048576,
6672
        2097152,
6673
        5242880,
6674
        10485760,
6675
        20971520,
6676
        52428800,
6677
        104857600,
6678
        262144000,
6679
        524288000,
6680
        786432000,
6681
        1073741824,
6682
        2147483648,
6683
        4294967296,
6684
        8589934592
6685
    );
1 efrain 6686
 
6687
    // If custombytes is given and is valid then add it to the list.
6688
    if (is_number($custombytes) and $custombytes > 0) {
6689
        $custombytes = (int)$custombytes;
6690
        if (!in_array($custombytes, $sizelist)) {
6691
            $sizelist[] = $custombytes;
6692
        }
6693
    } else if (is_array($custombytes)) {
6694
        $sizelist = array_unique(array_merge($sizelist, $custombytes));
6695
    }
6696
 
6697
    // Allow maxbytes to be selected if it falls outside the above boundaries.
6698
    if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6699
        // Note: get_real_size() is used in order to prevent problems with invalid values.
6700
        $sizelist[] = get_real_size($CFG->maxbytes);
6701
    }
6702
 
6703
    foreach ($sizelist as $sizebytes) {
6704
        if ($sizebytes < $maxsize && $sizebytes > 0) {
6705
            $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6706
        }
6707
    }
6708
 
6709
    $limitlevel = '';
6710
    $displaysize = '';
1326 ariadna 6711
    if (
6712
        $modulebytes &&
1 efrain 6713
        (($modulebytes < $coursebytes || $coursebytes == 0) &&
1326 ariadna 6714
            ($modulebytes < $sitebytes || $sitebytes == 0))
6715
    ) {
1 efrain 6716
        $limitlevel = get_string('activity', 'core');
6717
        $displaysize = display_size($modulebytes, 0);
6718
        $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6719
 
6720
    } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6721
        $limitlevel = get_string('course', 'core');
6722
        $displaysize = display_size($coursebytes, 0);
6723
        $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6724
 
6725
    } else if ($sitebytes) {
6726
        $limitlevel = get_string('site', 'core');
6727
        $displaysize = display_size($sitebytes, 0);
6728
        $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6729
    }
6730
 
6731
    krsort($filesize, SORT_NUMERIC);
6732
    if ($limitlevel) {
6733
        $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6734
        $filesize  = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6735
    }
6736
 
6737
    return $filesize;
6738
}
6739
 
6740
/**
6741
 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6742
 *
6743
 * If excludefiles is defined, then that file/directory is ignored
6744
 * If getdirs is true, then (sub)directories are included in the output
6745
 * If getfiles is true, then files are included in the output
6746
 * (at least one of these must be true!)
6747
 *
6748
 * @todo Finish documenting this function. Add examples of $excludefile usage.
6749
 *
6750
 * @param string $rootdir A given root directory to start from
6751
 * @param string|array $excludefiles If defined then the specified file/directory is ignored
6752
 * @param bool $descend If true then subdirectories are recursed as well
6753
 * @param bool $getdirs If true then (sub)directories are included in the output
6754
 * @param bool $getfiles  If true then files are included in the output
6755
 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6756
 */
1326 ariadna 6757
function get_directory_list($rootdir, $excludefiles = '', $descend = true, $getdirs = false, $getfiles = true)
6758
{
1 efrain 6759
 
6760
    $dirs = array();
6761
 
6762
    if (!$getdirs and !$getfiles) {   // Nothing to show.
6763
        return $dirs;
6764
    }
6765
 
6766
    if (!is_dir($rootdir)) {          // Must be a directory.
6767
        return $dirs;
6768
    }
6769
 
6770
    if (!$dir = opendir($rootdir)) {  // Can't open it for some reason.
6771
        return $dirs;
6772
    }
6773
 
6774
    if (!is_array($excludefiles)) {
6775
        $excludefiles = array($excludefiles);
6776
    }
6777
 
6778
    while (false !== ($file = readdir($dir))) {
6779
        $firstchar = substr($file, 0, 1);
6780
        if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6781
            continue;
6782
        }
1326 ariadna 6783
        $fullfile = $rootdir . '/' . $file;
1 efrain 6784
        if (filetype($fullfile) == 'dir') {
6785
            if ($getdirs) {
6786
                $dirs[] = $file;
6787
            }
6788
            if ($descend) {
6789
                $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6790
                foreach ($subdirs as $subdir) {
1326 ariadna 6791
                    $dirs[] = $file . '/' . $subdir;
1 efrain 6792
                }
6793
            }
6794
        } else if ($getfiles) {
6795
            $dirs[] = $file;
6796
        }
6797
    }
6798
    closedir($dir);
6799
 
6800
    asort($dirs);
6801
 
6802
    return $dirs;
6803
}
6804
 
6805
 
6806
/**
6807
 * Adds up all the files in a directory and works out the size.
6808
 *
6809
 * @param string $rootdir  The directory to start from
6810
 * @param string $excludefile A file to exclude when summing directory size
6811
 * @return int The summed size of all files and subfiles within the root directory
6812
 */
1326 ariadna 6813
function get_directory_size($rootdir, $excludefile = '')
6814
{
1 efrain 6815
    global $CFG;
6816
 
6817
    // Do it this way if we can, it's much faster.
6818
    if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
1326 ariadna 6819
        $command = trim($CFG->pathtodu) . ' -sk ' . escapeshellarg($rootdir);
1 efrain 6820
        $output = null;
6821
        $return = null;
6822
        exec($command, $output, $return);
6823
        if (is_array($output)) {
6824
            // We told it to return k.
1326 ariadna 6825
            return get_real_size(intval($output[0]) . 'k');
1 efrain 6826
        }
6827
    }
6828
 
6829
    if (!is_dir($rootdir)) {
6830
        // Must be a directory.
6831
        return 0;
6832
    }
6833
 
6834
    if (!$dir = @opendir($rootdir)) {
6835
        // Can't open it for some reason.
6836
        return 0;
6837
    }
6838
 
6839
    $size = 0;
6840
 
6841
    while (false !== ($file = readdir($dir))) {
6842
        $firstchar = substr($file, 0, 1);
6843
        if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6844
            continue;
6845
        }
1326 ariadna 6846
        $fullfile = $rootdir . '/' . $file;
1 efrain 6847
        if (filetype($fullfile) == 'dir') {
6848
            $size += get_directory_size($fullfile, $excludefile);
6849
        } else {
6850
            $size += filesize($fullfile);
6851
        }
6852
    }
6853
    closedir($dir);
6854
 
6855
    return $size;
6856
}
6857
 
6858
/**
6859
 * Converts bytes into display form
6860
 *
6861
 * @param int $size  The size to convert to human readable form
6862
 * @param int $decimalplaces If specified, uses fixed number of decimal places
6863
 * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
6864
 * @return string Display version of size
6865
 */
1326 ariadna 6866
function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string
6867
{
1 efrain 6868
 
6869
    static $units;
6870
 
6871
    if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6872
        return get_string('unlimited');
6873
    }
6874
 
6875
    if (empty($units)) {
6876
        $units[] = get_string('sizeb');
6877
        $units[] = get_string('sizekb');
6878
        $units[] = get_string('sizemb');
6879
        $units[] = get_string('sizegb');
6880
        $units[] = get_string('sizetb');
6881
        $units[] = get_string('sizepb');
6882
    }
6883
 
6884
    switch ($fixedunits) {
1326 ariadna 6885
        case 'PB':
1 efrain 6886
            $magnitude = 5;
6887
            break;
1326 ariadna 6888
        case 'TB':
1 efrain 6889
            $magnitude = 4;
6890
            break;
1326 ariadna 6891
        case 'GB':
1 efrain 6892
            $magnitude = 3;
6893
            break;
1326 ariadna 6894
        case 'MB':
1 efrain 6895
            $magnitude = 2;
6896
            break;
1326 ariadna 6897
        case 'KB':
1 efrain 6898
            $magnitude = 1;
6899
            break;
1326 ariadna 6900
        case 'B':
1 efrain 6901
            $magnitude = 0;
6902
            break;
6903
        case '':
6904
            $magnitude = floor(log($size, 1024));
6905
            $magnitude = max(0, min(5, $magnitude));
6906
            break;
6907
        default:
6908
            throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
6909
    }
6910
 
6911
    // Special case for magnitude 0 (bytes) - never use decimal places.
6912
    $nbsp = "\xc2\xa0";
6913
    if ($magnitude === 0) {
6914
        return round($size) . $nbsp . $units[$magnitude];
6915
    }
6916
 
6917
    // Convert to specified units.
6918
    $sizeinunit = $size / 1024 ** $magnitude;
6919
 
6920
    // Fixed decimal places.
6921
    return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
6922
}
6923
 
6924
/**
6925
 * Cleans a given filename by removing suspicious or troublesome characters
6926
 *
6927
 * @see clean_param()
6928
 * @param string $string file name
6929
 * @return string cleaned file name
6930
 */
1326 ariadna 6931
function clean_filename($string)
6932
{
1 efrain 6933
    return clean_param($string, PARAM_FILE);
6934
}
6935
 
6936
// STRING TRANSLATION.
6937
 
6938
/**
6939
 * Returns the code for the current language
6940
 *
6941
 * @category string
6942
 * @return string
6943
 */
1326 ariadna 6944
function current_language()
6945
{
1 efrain 6946
    global $CFG, $PAGE, $SESSION, $USER;
6947
 
6948
    if (!empty($SESSION->forcelang)) {
6949
        // Allows overriding course-forced language (useful for admins to check
6950
        // issues in courses whose language they don't understand).
6951
        // Also used by some code to temporarily get language-related information in a
6952
        // specific language (see force_current_language()).
6953
        $return = $SESSION->forcelang;
6954
    } else if (!empty($PAGE->cm->lang)) {
6955
        // Activity language, if set.
6956
        $return = $PAGE->cm->lang;
6957
    } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) {
6958
        // Course language can override all other settings for this page.
6959
        $return = $PAGE->course->lang;
6960
    } else if (!empty($SESSION->lang)) {
6961
        // Session language can override other settings.
6962
        $return = $SESSION->lang;
6963
    } else if (!empty($USER->lang)) {
6964
        $return = $USER->lang;
6965
    } else if (isset($CFG->lang)) {
6966
        $return = $CFG->lang;
6967
    } else {
6968
        $return = 'en';
6969
    }
6970
 
6971
    // Just in case this slipped in from somewhere by accident.
6972
    $return = str_replace('_utf8', '', $return);
6973
 
6974
    return $return;
6975
}
6976
 
6977
/**
6978
 * Fix the current language to the given language code.
6979
 *
6980
 * @param string $lang The language code to use.
6981
 * @return void
6982
 */
1326 ariadna 6983
function fix_current_language(string $lang): void
6984
{
1 efrain 6985
    global $CFG, $COURSE, $SESSION, $USER;
6986
 
6987
    if (!get_string_manager()->translation_exists($lang)) {
6988
        throw new coding_exception("The language pack for $lang is not available");
6989
    }
6990
 
6991
    $fixglobal = '';
6992
    $fixlang = 'lang';
6993
    if (!empty($SESSION->forcelang)) {
6994
        $fixglobal = $SESSION;
6995
        $fixlang = 'forcelang';
6996
    } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) {
6997
        $fixglobal = $COURSE;
6998
    } else if (!empty($SESSION->lang)) {
6999
        $fixglobal = $SESSION;
7000
    } else if (!empty($USER->lang)) {
7001
        $fixglobal = $USER;
7002
    } else if (isset($CFG->lang)) {
7003
        set_config('lang', $lang);
7004
    }
7005
 
7006
    if ($fixglobal) {
7007
        $fixglobal->$fixlang = $lang;
7008
    }
7009
}
7010
 
7011
/**
7012
 * Returns parent language of current active language if defined
7013
 *
7014
 * @category string
7015
 * @param string $lang null means current language
7016
 * @return string
7017
 */
1326 ariadna 7018
function get_parent_language($lang = null)
7019
{
1 efrain 7020
 
7021
    $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
7022
 
7023
    if ($parentlang === 'en') {
7024
        $parentlang = '';
7025
    }
7026
 
7027
    return $parentlang;
7028
}
7029
 
7030
/**
7031
 * Force the current language to get strings and dates localised in the given language.
7032
 *
7033
 * After calling this function, all strings will be provided in the given language
7034
 * until this function is called again, or equivalent code is run.
7035
 *
7036
 * @param string $language
7037
 * @return string previous $SESSION->forcelang value
7038
 */
1326 ariadna 7039
function force_current_language($language)
7040
{
1 efrain 7041
    global $SESSION;
7042
    $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
7043
    if ($language !== $sessionforcelang) {
7044
        // Setting forcelang to null or an empty string disables its effect.
7045
        if (empty($language) || get_string_manager()->translation_exists($language, false)) {
7046
            $SESSION->forcelang = $language;
7047
            moodle_setlocale();
7048
        }
7049
    }
7050
    return $sessionforcelang;
7051
}
7052
 
7053
/**
7054
 * Returns current string_manager instance.
7055
 *
7056
 * The param $forcereload is needed for CLI installer only where the string_manager instance
7057
 * must be replaced during the install.php script life time.
7058
 *
7059
 * @category string
7060
 * @param bool $forcereload shall the singleton be released and new instance created instead?
7061
 * @return core_string_manager
7062
 */
1326 ariadna 7063
function get_string_manager($forcereload = false)
7064
{
1 efrain 7065
    global $CFG;
7066
 
7067
    static $singleton = null;
7068
 
7069
    if ($forcereload) {
7070
        $singleton = null;
7071
    }
7072
    if ($singleton === null) {
7073
        if (empty($CFG->early_install_lang)) {
7074
 
7075
            $transaliases = array();
7076
            if (empty($CFG->langlist)) {
1326 ariadna 7077
                $translist = array();
1 efrain 7078
            } else {
7079
                $translist = explode(',', $CFG->langlist);
7080
                $translist = array_map('trim', $translist);
7081
                // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
7082
                foreach ($translist as $i => $value) {
7083
                    $parts = preg_split('/\s*\|\s*/', $value, 2);
7084
                    if (count($parts) == 2) {
7085
                        $transaliases[$parts[0]] = $parts[1];
7086
                        $translist[$i] = $parts[0];
7087
                    }
7088
                }
7089
            }
7090
 
7091
            if (!empty($CFG->config_php_settings['customstringmanager'])) {
7092
                $classname = $CFG->config_php_settings['customstringmanager'];
7093
 
7094
                if (class_exists($classname)) {
7095
                    $implements = class_implements($classname);
7096
 
7097
                    if (isset($implements['core_string_manager'])) {
7098
                        $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7099
                        return $singleton;
7100
                    } else {
1326 ariadna 7101
                        debugging('Unable to instantiate custom string manager: class ' . $classname .
1 efrain 7102
                            ' does not implement the core_string_manager interface.');
7103
                    }
7104
                } else {
1326 ariadna 7105
                    debugging('Unable to instantiate custom string manager: class ' . $classname . ' can not be found.');
1 efrain 7106
                }
7107
            }
7108
 
7109
            $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7110
        } else {
7111
            $singleton = new core_string_manager_install();
7112
        }
7113
    }
7114
 
7115
    return $singleton;
7116
}
7117
 
7118
/**
7119
 * Returns a localized string.
7120
 *
7121
 * Returns the translated string specified by $identifier as
7122
 * for $module.  Uses the same format files as STphp.
7123
 * $a is an object, string or number that can be used
7124
 * within translation strings
7125
 *
7126
 * eg 'hello {$a->firstname} {$a->lastname}'
7127
 * or 'hello {$a}'
7128
 *
7129
 * If you would like to directly echo the localized string use
7130
 * the function {@link print_string()}
7131
 *
7132
 * Example usage of this function involves finding the string you would
7133
 * like a local equivalent of and using its identifier and module information
7134
 * to retrieve it.<br/>
7135
 * If you open moodle/lang/en/moodle.php and look near line 278
7136
 * you will find a string to prompt a user for their word for 'course'
7137
 * <code>
7138
 * $string['course'] = 'Course';
7139
 * </code>
7140
 * So if you want to display the string 'Course'
7141
 * in any language that supports it on your site
7142
 * you just need to use the identifier 'course'
7143
 * <code>
7144
 * $mystring = '<strong>'. get_string('course') .'</strong>';
7145
 * or
7146
 * </code>
7147
 * If the string you want is in another file you'd take a slightly
7148
 * different approach. Looking in moodle/lang/en/calendar.php you find
7149
 * around line 75:
7150
 * <code>
7151
 * $string['typecourse'] = 'Course event';
7152
 * </code>
7153
 * If you want to display the string "Course event" in any language
7154
 * supported you would use the identifier 'typecourse' and the module 'calendar'
7155
 * (because it is in the file calendar.php):
7156
 * <code>
7157
 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7158
 * </code>
7159
 *
7160
 * As a last resort, should the identifier fail to map to a string
7161
 * the returned string will be [[ $identifier ]]
7162
 *
7163
 * In Moodle 2.3 there is a new argument to this function $lazyload.
7164
 * Setting $lazyload to true causes get_string to return a lang_string object
7165
 * rather than the string itself. The fetching of the string is then put off until
7166
 * the string object is first used. The object can be used by calling it's out
7167
 * method or by casting the object to a string, either directly e.g.
7168
 *     (string)$stringobject
7169
 * or indirectly by using the string within another string or echoing it out e.g.
7170
 *     echo $stringobject
7171
 *     return "<p>{$stringobject}</p>";
7172
 * It is worth noting that using $lazyload and attempting to use the string as an
7173
 * array key will cause a fatal error as objects cannot be used as array keys.
7174
 * But you should never do that anyway!
7175
 * For more information {@link lang_string}
7176
 *
7177
 * @category string
7178
 * @param string $identifier The key identifier for the localized string
7179
 * @param string $component The module where the key identifier is stored,
7180
 *      usually expressed as the filename in the language pack without the
7181
 *      .php on the end but can also be written as mod/forum or grade/export/xls.
7182
 *      If none is specified then moodle.php is used.
7183
 * @param string|object|array|int $a An object, string or number that can be used
7184
 *      within translation strings
7185
 * @param bool $lazyload If set to true a string object is returned instead of
7186
 *      the string itself. The string then isn't calculated until it is first used.
7187
 * @return string The localized string.
7188
 * @throws coding_exception
7189
 */
1326 ariadna 7190
function get_string($identifier, $component = '', $a = null, $lazyload = false)
7191
{
1 efrain 7192
    global $CFG;
7193
 
7194
    // If the lazy load argument has been supplied return a lang_string object
7195
    // instead.
7196
    // We need to make sure it is true (and a bool) as you will see below there
7197
    // used to be a forth argument at one point.
7198
    if ($lazyload === true) {
7199
        return new lang_string($identifier, $component, $a);
7200
    }
7201
 
7202
    if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
7203
        throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
7204
    }
7205
 
7206
    // There is now a forth argument again, this time it is a boolean however so
7207
    // we can still check for the old extralocations parameter.
7208
    if (!is_bool($lazyload) && !empty($lazyload)) {
7209
        debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7210
    }
7211
 
7212
    if (strpos((string)$component, '/') !== false) {
7213
        debugging('The module name you passed to get_string is the deprecated format ' .
1326 ariadna 7214
            'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.', DEBUG_DEVELOPER);
1 efrain 7215
        $componentpath = explode('/', $component);
7216
 
7217
        switch ($componentpath[0]) {
7218
            case 'mod':
7219
                $component = $componentpath[1];
7220
                break;
7221
            case 'blocks':
7222
            case 'block':
1326 ariadna 7223
                $component = 'block_' . $componentpath[1];
1 efrain 7224
                break;
7225
            case 'enrol':
1326 ariadna 7226
                $component = 'enrol_' . $componentpath[1];
1 efrain 7227
                break;
7228
            case 'format':
1326 ariadna 7229
                $component = 'format_' . $componentpath[1];
1 efrain 7230
                break;
7231
            case 'grade':
1326 ariadna 7232
                $component = 'grade' . $componentpath[1] . '_' . $componentpath[2];
1 efrain 7233
                break;
7234
        }
7235
    }
7236
 
7237
    $result = get_string_manager()->get_string($identifier, $component, $a);
7238
 
7239
    // Debugging feature lets you display string identifier and component.
7240
    if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7241
        $result .= ' {' . $identifier . '/' . $component . '}';
7242
    }
7243
    return $result;
7244
}
7245
 
7246
/**
7247
 * Converts an array of strings to their localized value.
7248
 *
7249
 * @param array $array An array of strings
7250
 * @param string $component The language module that these strings can be found in.
7251
 * @return stdClass translated strings.
7252
 */
1326 ariadna 7253
function get_strings($array, $component = '')
7254
{
1 efrain 7255
    $string = new stdClass;
7256
    foreach ($array as $item) {
7257
        $string->$item = get_string($item, $component);
7258
    }
7259
    return $string;
7260
}
7261
 
7262
/**
7263
 * Prints out a translated string.
7264
 *
7265
 * Prints out a translated string using the return value from the {@link get_string()} function.
7266
 *
7267
 * Example usage of this function when the string is in the moodle.php file:<br/>
7268
 * <code>
7269
 * echo '<strong>';
7270
 * print_string('course');
7271
 * echo '</strong>';
7272
 * </code>
7273
 *
7274
 * Example usage of this function when the string is not in the moodle.php file:<br/>
7275
 * <code>
7276
 * echo '<h1>';
7277
 * print_string('typecourse', 'calendar');
7278
 * echo '</h1>';
7279
 * </code>
7280
 *
7281
 * @category string
7282
 * @param string $identifier The key identifier for the localized string
7283
 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7284
 * @param string|object|array $a An object, string or number that can be used within translation strings
7285
 */
1326 ariadna 7286
function print_string($identifier, $component = '', $a = null)
7287
{
1 efrain 7288
    echo get_string($identifier, $component, $a);
7289
}
7290
 
7291
/**
7292
 * Returns a list of charset codes
7293
 *
7294
 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7295
 * (checking that such charset is supported by the texlib library!)
7296
 *
7297
 * @return array And associative array with contents in the form of charset => charset
7298
 */
1326 ariadna 7299
function get_list_of_charsets()
7300
{
1 efrain 7301
 
7302
    $charsets = array(
7303
        'EUC-JP'     => 'EUC-JP',
1326 ariadna 7304
        'ISO-2022-JP' => 'ISO-2022-JP',
1 efrain 7305
        'ISO-8859-1' => 'ISO-8859-1',
7306
        'SHIFT-JIS'  => 'SHIFT-JIS',
7307
        'GB2312'     => 'GB2312',
7308
        'GB18030'    => 'GB18030', // GB18030 not supported by typo and mbstring.
1326 ariadna 7309
        'UTF-8'      => 'UTF-8'
7310
    );
1 efrain 7311
 
7312
    asort($charsets);
7313
 
7314
    return $charsets;
7315
}
7316
 
7317
/**
7318
 * Returns a list of valid and compatible themes
7319
 *
7320
 * @return array
7321
 */
1326 ariadna 7322
function get_list_of_themes()
7323
{
1 efrain 7324
    global $CFG;
7325
 
7326
    $themes = array();
7327
 
7328
    if (!empty($CFG->themelist)) {       // Use admin's list of themes.
7329
        $themelist = explode(',', $CFG->themelist);
7330
    } else {
7331
        $themelist = array_keys(core_component::get_plugin_list("theme"));
7332
    }
7333
 
7334
    foreach ($themelist as $key => $themename) {
7335
        $theme = theme_config::load($themename);
7336
        $themes[$themename] = $theme;
7337
    }
7338
 
7339
    core_collator::asort_objects_by_method($themes, 'get_theme_name');
7340
 
7341
    return $themes;
7342
}
7343
 
7344
/**
7345
 * Factory function for emoticon_manager
7346
 *
7347
 * @return emoticon_manager singleton
7348
 */
1326 ariadna 7349
function get_emoticon_manager()
7350
{
1 efrain 7351
    static $singleton = null;
7352
 
7353
    if (is_null($singleton)) {
7354
        $singleton = new emoticon_manager();
7355
    }
7356
 
7357
    return $singleton;
7358
}
7359
 
7360
/**
7361
 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7362
 *
7363
 * Whenever this manager mentiones 'emoticon object', the following data
7364
 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7365
 * altidentifier and altcomponent
7366
 *
7367
 * @see admin_setting_emoticons
7368
 *
7369
 * @copyright 2010 David Mudrak
7370
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7371
 */
1326 ariadna 7372
class emoticon_manager
7373
{
1 efrain 7374
 
7375
    /**
7376
     * Returns the currently enabled emoticons
7377
     *
7378
     * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7379
     * @return array of emoticon objects
7380
     */
1326 ariadna 7381
    public function get_emoticons($selectable = false)
7382
    {
1 efrain 7383
        global $CFG;
7384
        $notselectable = ['martin', 'egg'];
7385
 
7386
        if (empty($CFG->emoticons)) {
7387
            return array();
7388
        }
7389
 
7390
        $emoticons = $this->decode_stored_config($CFG->emoticons);
7391
 
7392
        if (!is_array($emoticons)) {
7393
            // Something is wrong with the format of stored setting.
7394
            debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7395
            return array();
7396
        }
7397
        if ($selectable) {
7398
            foreach ($emoticons as $index => $emote) {
7399
                if (in_array($emote->altidentifier, $notselectable)) {
7400
                    // Skip this one.
7401
                    unset($emoticons[$index]);
7402
                }
7403
            }
7404
        }
7405
 
7406
        return $emoticons;
7407
    }
7408
 
7409
    /**
7410
     * Converts emoticon object into renderable pix_emoticon object
7411
     *
7412
     * @param stdClass $emoticon emoticon object
7413
     * @param array $attributes explicit HTML attributes to set
7414
     * @return pix_emoticon
7415
     */
1326 ariadna 7416
    public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array())
7417
    {
1 efrain 7418
        $stringmanager = get_string_manager();
7419
        if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7420
            $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7421
        } else {
7422
            $alt = s($emoticon->text);
7423
        }
7424
        return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7425
    }
7426
 
7427
    /**
7428
     * Encodes the array of emoticon objects into a string storable in config table
7429
     *
7430
     * @see self::decode_stored_config()
7431
     * @param array $emoticons array of emtocion objects
7432
     * @return string
7433
     */
1326 ariadna 7434
    public function encode_stored_config(array $emoticons)
7435
    {
1 efrain 7436
        return json_encode($emoticons);
7437
    }
7438
 
7439
    /**
7440
     * Decodes the string into an array of emoticon objects
7441
     *
7442
     * @see self::encode_stored_config()
7443
     * @param string $encoded
7444
     * @return array|null
7445
     */
1326 ariadna 7446
    public function decode_stored_config($encoded)
7447
    {
1 efrain 7448
        $decoded = json_decode($encoded);
7449
        if (!is_array($decoded)) {
7450
            return null;
7451
        }
7452
        return $decoded;
7453
    }
7454
 
7455
    /**
7456
     * Returns default set of emoticons supported by Moodle
7457
     *
7458
     * @return array of sdtClasses
7459
     */
1326 ariadna 7460
    public function default_emoticons()
7461
    {
1 efrain 7462
        return array(
7463
            $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7464
            $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7465
            $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7466
            $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7467
            $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7468
            $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7469
            $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7470
            $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7471
            $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7472
            $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7473
            $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7474
            $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7475
            $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7476
            $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7477
            $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7478
            $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7479
            $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7480
            $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7481
            $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7482
            $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7483
            $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7484
            $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7485
            $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7486
            $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7487
            $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7488
            $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7489
            $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7490
            $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7491
            $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7492
            $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7493
        );
7494
    }
7495
 
7496
    /**
7497
     * Helper method preparing the stdClass with the emoticon properties
7498
     *
7499
     * @param string|array $text or array of strings
7500
     * @param string $imagename to be used by {@link pix_emoticon}
7501
     * @param string $altidentifier alternative string identifier, null for no alt
7502
     * @param string $altcomponent where the alternative string is defined
7503
     * @param string $imagecomponent to be used by {@link pix_emoticon}
7504
     * @return stdClass
7505
     */
1326 ariadna 7506
    protected function prepare_emoticon_object(
7507
        $text,
7508
        $imagename,
7509
        $altidentifier = null,
7510
        $altcomponent = 'core_pix',
7511
        $imagecomponent = 'core'
7512
    ) {
1 efrain 7513
        return (object)array(
7514
            'text'           => $text,
7515
            'imagename'      => $imagename,
7516
            'imagecomponent' => $imagecomponent,
7517
            'altidentifier'  => $altidentifier,
7518
            'altcomponent'   => $altcomponent,
7519
        );
7520
    }
7521
}
7522
 
7523
// ENCRYPTION.
7524
 
7525
/**
7526
 * rc4encrypt
7527
 *
7528
 * @param string $data        Data to encrypt.
7529
 * @return string             The now encrypted data.
7530
 */
1326 ariadna 7531
function rc4encrypt($data)
7532
{
1 efrain 7533
    return endecrypt(get_site_identifier(), $data, '');
7534
}
7535
 
7536
/**
7537
 * rc4decrypt
7538
 *
7539
 * @param string $data        Data to decrypt.
7540
 * @return string             The now decrypted data.
7541
 */
1326 ariadna 7542
function rc4decrypt($data)
7543
{
1 efrain 7544
    return endecrypt(get_site_identifier(), $data, 'de');
7545
}
7546
 
7547
/**
7548
 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7549
 *
7550
 * @todo Finish documenting this function
7551
 *
7552
 * @param string $pwd The password to use when encrypting or decrypting
7553
 * @param string $data The data to be decrypted/encrypted
7554
 * @param string $case Either 'de' for decrypt or '' for encrypt
7555
 * @return string
7556
 */
1326 ariadna 7557
function endecrypt($pwd, $data, $case)
7558
{
1 efrain 7559
 
7560
    if ($case == 'de') {
7561
        $data = urldecode($data);
7562
    }
7563
 
7564
    $key[] = '';
7565
    $box[] = '';
7566
    $pwdlength = strlen($pwd);
7567
 
7568
    for ($i = 0; $i <= 255; $i++) {
7569
        $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7570
        $box[$i] = $i;
7571
    }
7572
 
7573
    $x = 0;
7574
 
7575
    for ($i = 0; $i <= 255; $i++) {
7576
        $x = ($x + $box[$i] + $key[$i]) % 256;
7577
        $tempswap = $box[$i];
7578
        $box[$i] = $box[$x];
7579
        $box[$x] = $tempswap;
7580
    }
7581
 
7582
    $cipher = '';
7583
 
7584
    $a = 0;
7585
    $j = 0;
7586
 
7587
    for ($i = 0; $i < strlen($data); $i++) {
7588
        $a = ($a + 1) % 256;
7589
        $j = ($j + $box[$a]) % 256;
7590
        $temp = $box[$a];
7591
        $box[$a] = $box[$j];
7592
        $box[$j] = $temp;
7593
        $k = $box[(($box[$a] + $box[$j]) % 256)];
7594
        $cipherby = ord(substr($data, $i, 1)) ^ $k;
7595
        $cipher .= chr($cipherby);
7596
    }
7597
 
7598
    if ($case == 'de') {
7599
        $cipher = urldecode(urlencode($cipher));
7600
    } else {
7601
        $cipher = urlencode($cipher);
7602
    }
7603
 
7604
    return $cipher;
7605
}
7606
 
7607
// ENVIRONMENT CHECKING.
7608
 
7609
/**
7610
 * This method validates a plug name. It is much faster than calling clean_param.
7611
 *
7612
 * @param string $name a string that might be a plugin name.
7613
 * @return bool if this string is a valid plugin name.
7614
 */
1326 ariadna 7615
function is_valid_plugin_name($name)
7616
{
1 efrain 7617
    // This does not work for 'mod', bad luck, use any other type.
7618
    return core_component::is_valid_plugin_name('tool', $name);
7619
}
7620
 
7621
/**
7622
 * Get a list of all the plugins of a given type that define a certain API function
7623
 * in a certain file. The plugin component names and function names are returned.
7624
 *
7625
 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7626
 * @param string $function the part of the name of the function after the
7627
 *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7628
 *      names like report_courselist_hook.
7629
 * @param string $file the name of file within the plugin that defines the
7630
 *      function. Defaults to lib.php.
7631
 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7632
 *      and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7633
 */
1326 ariadna 7634
function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php')
7635
{
1 efrain 7636
    global $CFG;
7637
 
7638
    // We don't include here as all plugin types files would be included.
7639
    $plugins = get_plugins_with_function($function, $file, false);
7640
 
7641
    if (empty($plugins[$plugintype])) {
7642
        return array();
7643
    }
7644
 
7645
    $allplugins = core_component::get_plugin_list($plugintype);
7646
 
7647
    // Reformat the array and include the files.
7648
    $pluginfunctions = array();
7649
    foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7650
 
7651
        // Check that it has not been removed and the file is still available.
7652
        if (!empty($allplugins[$pluginname])) {
7653
 
7654
            $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7655
            if (file_exists($filepath)) {
7656
                include_once($filepath);
7657
 
7658
                // Now that the file is loaded, we must verify the function still exists.
7659
                if (function_exists($functionname)) {
7660
                    $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7661
                } else {
7662
                    // Invalidate the cache for next run.
7663
                    \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7664
                }
7665
            }
7666
        }
7667
    }
7668
 
7669
    return $pluginfunctions;
7670
}
7671
 
7672
/**
7673
 * Get a list of all the plugins that define a certain API function in a certain file.
7674
 *
7675
 * @param string $function the part of the name of the function after the
7676
 *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7677
 *      names like report_courselist_hook.
7678
 * @param string $file the name of file within the plugin that defines the
7679
 *      function. Defaults to lib.php.
7680
 * @param bool $include Whether to include the files that contain the functions or not.
7681
 * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
7682
 * @return array with [plugintype][plugin] = functionname
7683
 */
1326 ariadna 7684
function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false)
7685
{
1 efrain 7686
    global $CFG;
7687
 
7688
    if (during_initial_install() || isset($CFG->upgraderunning)) {
7689
        // API functions _must not_ be called during an installation or upgrade.
7690
        return [];
7691
    }
7692
 
7693
    $plugincallback = $function;
1326 ariadna 7694
    $filtermigrated = function ($plugincallback, $pluginfunctions): array {
1 efrain 7695
        foreach ($pluginfunctions as $plugintype => $plugins) {
7696
            foreach ($plugins as $plugin => $unusedfunction) {
7697
                $component = $plugintype . '_' . $plugin;
7698
                if ($hooks = di::get(hook\manager::class)->get_hooks_deprecating_plugin_callback($plugincallback)) {
7699
                    if (di::get(hook\manager::class)->is_deprecating_hook_present($component, $plugincallback)) {
7700
                        // Ignore the old callback, it is there only for older Moodle versions.
7701
                        unset($pluginfunctions[$plugintype][$plugin]);
7702
                    } else {
7703
                        $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
7704
                        debugging(
7705
                            "Callback $plugincallback in $component component should be migrated to new " .
7706
                                "hook callback for $hookmessage",
7707
                            DEBUG_DEVELOPER
7708
                        );
7709
                    }
7710
                }
7711
            }
7712
        }
7713
        return $pluginfunctions;
7714
    };
7715
 
7716
    $cache = \cache::make('core', 'plugin_functions');
7717
 
7718
    // Including both although I doubt that we will find two functions definitions with the same name.
7719
    // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7720
    $pluginfunctions = false;
7721
    if (!empty($CFG->allversionshash)) {
7722
        $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA);
7723
        $pluginfunctions = $cache->get($key);
7724
    }
7725
    $dirty = false;
7726
 
7727
    // Use the plugin manager to check that plugins are currently installed.
7728
    $pluginmanager = \core_plugin_manager::instance();
7729
 
7730
    if ($pluginfunctions !== false) {
7731
 
7732
        // Checking that the files are still available.
7733
        foreach ($pluginfunctions as $plugintype => $plugins) {
7734
 
7735
            $allplugins = \core_component::get_plugin_list($plugintype);
7736
            $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7737
            foreach ($plugins as $plugin => $function) {
7738
                if (!isset($installedplugins[$plugin])) {
7739
                    // Plugin code is still present on disk but it is not installed.
7740
                    $dirty = true;
7741
                    break 2;
7742
                }
7743
 
7744
                // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7745
                if (empty($allplugins[$plugin])) {
7746
                    $dirty = true;
7747
                    break 2;
7748
                }
7749
 
7750
                $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7751
                if ($include && $fileexists) {
7752
                    // Include the files if it was requested.
7753
                    include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7754
                } else if (!$fileexists) {
7755
                    // If the file is not available any more it should not be returned.
7756
                    $dirty = true;
7757
                    break 2;
7758
                }
7759
 
7760
                // Check if the function still exists in the file.
7761
                if ($include && !function_exists($function)) {
7762
                    $dirty = true;
7763
                    break 2;
7764
                }
7765
            }
7766
        }
7767
 
7768
        // If the cache is dirty, we should fall through and let it rebuild.
7769
        if (!$dirty) {
7770
            if ($migratedtohook && $file === 'lib.php') {
7771
                $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7772
            }
7773
            return $pluginfunctions;
7774
        }
7775
    }
7776
 
7777
    $pluginfunctions = array();
7778
 
7779
    // To fill the cached. Also, everything should continue working with cache disabled.
7780
    $plugintypes = \core_component::get_plugin_types();
7781
    foreach ($plugintypes as $plugintype => $unused) {
7782
 
7783
        // We need to include files here.
7784
        $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7785
        $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7786
        foreach ($pluginswithfile as $plugin => $notused) {
7787
 
7788
            if (!isset($installedplugins[$plugin])) {
7789
                continue;
7790
            }
7791
 
7792
            $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7793
 
7794
            $pluginfunction = false;
7795
            if (function_exists($fullfunction)) {
7796
                // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7797
                $pluginfunction = $fullfunction;
7798
            } else if ($plugintype === 'mod') {
7799
                // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7800
                $shortfunction = $plugin . '_' . $function;
7801
                if (function_exists($shortfunction)) {
7802
                    $pluginfunction = $shortfunction;
7803
                }
7804
            }
7805
 
7806
            if ($pluginfunction) {
7807
                if (empty($pluginfunctions[$plugintype])) {
7808
                    $pluginfunctions[$plugintype] = array();
7809
                }
7810
                $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7811
            }
7812
        }
7813
    }
7814
    if (!empty($CFG->allversionshash)) {
7815
        $cache->set($key, $pluginfunctions);
7816
    }
7817
 
7818
    if ($migratedtohook && $file === 'lib.php') {
7819
        $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7820
    }
7821
 
7822
    return $pluginfunctions;
7823
}
7824
 
7825
/**
7826
 * Lists plugin-like directories within specified directory
7827
 *
7828
 * This function was originally used for standard Moodle plugins, please use
7829
 * new core_component::get_plugin_list() now.
7830
 *
7831
 * This function is used for general directory listing and backwards compatility.
7832
 *
7833
 * @param string $directory relative directory from root
7834
 * @param string $exclude dir name to exclude from the list (defaults to none)
7835
 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7836
 * @return array Sorted array of directory names found under the requested parameters
7837
 */
1326 ariadna 7838
function get_list_of_plugins($directory = 'mod', $exclude = '', $basedir = '')
7839
{
1 efrain 7840
    global $CFG;
7841
 
7842
    $plugins = array();
7843
 
7844
    if (empty($basedir)) {
1326 ariadna 7845
        $basedir = $CFG->dirroot . '/' . $directory;
1 efrain 7846
    } else {
1326 ariadna 7847
        $basedir = $basedir . '/' . $directory;
1 efrain 7848
    }
7849
 
7850
    if ($CFG->debugdeveloper and empty($exclude)) {
7851
        // Make sure devs do not use this to list normal plugins,
7852
        // this is intended for general directories that are not plugins!
7853
 
7854
        $subtypes = core_component::get_plugin_types();
7855
        if (in_array($basedir, $subtypes)) {
7856
            debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7857
        }
7858
        unset($subtypes);
7859
    }
7860
 
7861
    $ignorelist = array_flip(array_filter([
7862
        'CVS',
7863
        '_vti_cnf',
7864
        'amd',
7865
        'classes',
7866
        'simpletest',
7867
        'tests',
7868
        'templates',
7869
        'yui',
7870
        $exclude,
7871
    ]));
7872
 
7873
    if (file_exists($basedir) && filetype($basedir) == 'dir') {
7874
        if (!$dirhandle = opendir($basedir)) {
7875
            debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7876
            return array();
7877
        }
7878
        while (false !== ($dir = readdir($dirhandle))) {
7879
            if (strpos($dir, '.') === 0) {
7880
                // Ignore directories starting with .
7881
                // These are treated as hidden directories.
7882
                continue;
7883
            }
7884
            if (array_key_exists($dir, $ignorelist)) {
7885
                // This directory features on the ignore list.
7886
                continue;
7887
            }
1326 ariadna 7888
            if (filetype($basedir . '/' . $dir) != 'dir') {
1 efrain 7889
                continue;
7890
            }
7891
            $plugins[] = $dir;
7892
        }
7893
        closedir($dirhandle);
7894
    }
7895
    if ($plugins) {
7896
        asort($plugins);
7897
    }
7898
    return $plugins;
7899
}
7900
 
7901
/**
7902
 * Invoke plugin's callback functions
7903
 *
7904
 * @param string $type plugin type e.g. 'mod'
7905
 * @param string $name plugin name
7906
 * @param string $feature feature name
7907
 * @param string $action feature's action
7908
 * @param array $params parameters of callback function, should be an array
7909
 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7910
 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7911
 * @return mixed
7912
 *
7913
 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7914
 */
1326 ariadna 7915
function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false)
7916
{
1 efrain 7917
    return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
7918
}
7919
 
7920
/**
7921
 * Invoke component's callback functions
7922
 *
7923
 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7924
 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7925
 * @param array $params parameters of callback function
7926
 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7927
 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7928
 * @return mixed
7929
 */
1326 ariadna 7930
function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false)
7931
{
1 efrain 7932
    $functionname = component_callback_exists($component, $function);
7933
 
7934
    if ($functionname) {
7935
        if ($migratedtohook) {
7936
            $hookmanager = di::get(hook\manager::class);
7937
            if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($function)) {
7938
                if ($hookmanager->is_deprecating_hook_present($component, $function)) {
7939
                    // Do not call the old lib.php callback,
7940
                    // it is there for compatibility with older Moodle versions only.
7941
                    return null;
7942
                } else {
7943
                    $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
7944
                    debugging(
7945
                        "Callback $function in $component component should be migrated to new hook callback for $hookmessage",
1326 ariadna 7946
                        DEBUG_DEVELOPER
7947
                    );
1 efrain 7948
                }
7949
            }
7950
        }
7951
 
7952
        // Function exists, so just return function result.
7953
        $ret = call_user_func_array($functionname, $params);
7954
        if (is_null($ret)) {
7955
            return $default;
7956
        } else {
7957
            return $ret;
7958
        }
7959
    }
7960
    return $default;
7961
}
7962
 
7963
/**
7964
 * Determine if a component callback exists and return the function name to call. Note that this
7965
 * function will include the required library files so that the functioname returned can be
7966
 * called directly.
7967
 *
7968
 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7969
 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7970
 * @return mixed Complete function name to call if the callback exists or false if it doesn't.
7971
 * @throws coding_exception if invalid component specfied
7972
 */
1326 ariadna 7973
function component_callback_exists($component, $function)
7974
{
1 efrain 7975
    global $CFG; // This is needed for the inclusions.
7976
 
7977
    $cleancomponent = clean_param($component, PARAM_COMPONENT);
7978
    if (empty($cleancomponent)) {
7979
        throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7980
    }
7981
    $component = $cleancomponent;
7982
 
7983
    list($type, $name) = core_component::normalize_component($component);
7984
    $component = $type . '_' . $name;
7985
 
1326 ariadna 7986
    $oldfunction = $name . '_' . $function;
7987
    $function = $component . '_' . $function;
1 efrain 7988
 
7989
    $dir = core_component::get_component_directory($component);
7990
    if (empty($dir)) {
7991
        throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7992
    }
7993
 
7994
    // Load library and look for function.
1326 ariadna 7995
    if (file_exists($dir . '/lib.php')) {
7996
        require_once($dir . '/lib.php');
1 efrain 7997
    }
7998
 
7999
    if (!function_exists($function) and function_exists($oldfunction)) {
8000
        if ($type !== 'mod' and $type !== 'core') {
8001
            debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
8002
        }
8003
        $function = $oldfunction;
8004
    }
8005
 
8006
    if (function_exists($function)) {
8007
        return $function;
8008
    }
8009
    return false;
8010
}
8011
 
8012
/**
8013
 * Call the specified callback method on the provided class.
8014
 *
8015
 * If the callback returns null, then the default value is returned instead.
8016
 * If the class does not exist, then the default value is returned.
8017
 *
8018
 * @param   string      $classname The name of the class to call upon.
8019
 * @param   string      $methodname The name of the staticically defined method on the class.
8020
 * @param   array       $params The arguments to pass into the method.
8021
 * @param   mixed       $default The default value.
8022
 * @param   bool        $migratedtohook True if the callback has been migrated to a hook.
8023
 * @return  mixed       The return value.
8024
 */
1326 ariadna 8025
function component_class_callback($classname, $methodname, array $params, $default = null, bool $migratedtohook = false)
8026
{
1 efrain 8027
    if (!class_exists($classname)) {
8028
        return $default;
8029
    }
8030
 
8031
    if (!method_exists($classname, $methodname)) {
8032
        return $default;
8033
    }
8034
 
8035
    $fullfunction = $classname . '::' . $methodname;
8036
 
8037
    if ($migratedtohook) {
8038
        $functionparts = explode('\\', trim($fullfunction, '\\'));
8039
        $component = $functionparts[0];
8040
        $callback = end($functionparts);
8041
        $hookmanager = di::get(hook\manager::class);
8042
        if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($callback)) {
8043
            if ($hookmanager->is_deprecating_hook_present($component, $callback)) {
8044
                // Do not call the old class callback,
8045
                // it is there for compatibility with older Moodle versions only.
8046
                return null;
8047
            } else {
8048
                $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
1326 ariadna 8049
                debugging(
8050
                    "Callback $callback in $component component should be migrated to new hook callback for $hookmessage",
8051
                    DEBUG_DEVELOPER
8052
                );
1 efrain 8053
            }
8054
        }
8055
    }
8056
 
8057
    $result = call_user_func_array($fullfunction, $params);
8058
 
8059
    if (null === $result) {
8060
        return $default;
8061
    } else {
8062
        return $result;
8063
    }
8064
}
8065
 
8066
/**
8067
 * Checks whether a plugin supports a specified feature.
8068
 *
8069
 * @param string $type Plugin type e.g. 'mod'
8070
 * @param string $name Plugin name e.g. 'forum'
8071
 * @param string $feature Feature code (FEATURE_xx constant)
8072
 * @param mixed $default default value if feature support unknown
8073
 * @return mixed Feature result (false if not supported, null if feature is unknown,
8074
 *         otherwise usually true but may have other feature-specific value such as array)
8075
 * @throws coding_exception
8076
 */
1326 ariadna 8077
function plugin_supports($type, $name, $feature, $default = null)
8078
{
1 efrain 8079
    global $CFG;
8080
 
8081
    if ($type === 'mod' and $name === 'NEWMODULE') {
8082
        // Somebody forgot to rename the module template.
8083
        return false;
8084
    }
8085
 
8086
    $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8087
    if (empty($component)) {
8088
        throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8089
    }
8090
 
8091
    $function = null;
8092
 
8093
    if ($type === 'mod') {
8094
        // We need this special case because we support subplugins in modules,
8095
        // otherwise it would end up in infinite loop.
8096
        if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8097
            include_once("$CFG->dirroot/mod/$name/lib.php");
1326 ariadna 8098
            $function = $component . '_supports';
1 efrain 8099
            if (!function_exists($function)) {
8100
                // Legacy non-frankenstyle function name.
1326 ariadna 8101
                $function = $name . '_supports';
1 efrain 8102
            }
8103
        }
8104
    } else {
8105
        if (!$path = core_component::get_plugin_directory($type, $name)) {
8106
            // Non existent plugin type.
8107
            return false;
8108
        }
8109
        if (file_exists("$path/lib.php")) {
8110
            include_once("$path/lib.php");
1326 ariadna 8111
            $function = $component . '_supports';
1 efrain 8112
        }
8113
    }
8114
 
8115
    if ($function and function_exists($function)) {
8116
        $supports = $function($feature);
8117
        if (is_null($supports)) {
8118
            // Plugin does not know - use default.
8119
            return $default;
8120
        } else {
8121
            return $supports;
8122
        }
8123
    }
8124
 
8125
    // Plugin does not care, so use default.
8126
    return $default;
8127
}
8128
 
8129
/**
8130
 * Returns true if the current version of PHP is greater that the specified one.
8131
 *
8132
 * @todo Check PHP version being required here is it too low?
8133
 *
8134
 * @param string $version The version of php being tested.
8135
 * @return bool
8136
 */
1326 ariadna 8137
function check_php_version($version = '5.2.4')
8138
{
1 efrain 8139
    return (version_compare(phpversion(), $version) >= 0);
8140
}
8141
 
8142
/**
8143
 * Determine if moodle installation requires update.
8144
 *
8145
 * Checks version numbers of main code and all plugins to see
8146
 * if there are any mismatches.
8147
 *
8148
 * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
8149
 * @return bool
8150
 */
1326 ariadna 8151
function moodle_needs_upgrading($checkupgradeflag = true)
8152
{
1 efrain 8153
    global $CFG, $DB;
8154
 
8155
    // Say no if there is already an upgrade running.
8156
    if ($checkupgradeflag) {
8157
        $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']);
8158
        $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING);
8159
        // If we ARE locked, but this PHP process is NOT the process running the upgrade,
8160
        // We should always return false.
8161
        // This means the upgrade is running from CLI somewhere, or about to.
8162
        if (!empty($lock) && !$currentprocessrunningupgrade) {
8163
            return false;
8164
        }
8165
    }
8166
 
8167
    if (empty($CFG->version)) {
8168
        return true;
8169
    }
8170
 
8171
    // There is no need to purge plugininfo caches here because
8172
    // these caches are not used during upgrade and they are purged after
8173
    // every upgrade.
8174
 
8175
    if (empty($CFG->allversionshash)) {
8176
        return true;
8177
    }
8178
 
8179
    $hash = core_component::get_all_versions_hash();
8180
 
8181
    return ($hash !== $CFG->allversionshash);
8182
}
8183
 
8184
/**
8185
 * Returns the major version of this site
8186
 *
8187
 * Moodle version numbers consist of three numbers separated by a dot, for
8188
 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
8189
 * called major version. This function extracts the major version from either
8190
 * $CFG->release (default) or eventually from the $release variable defined in
8191
 * the main version.php.
8192
 *
8193
 * @param bool $fromdisk should the version if source code files be used
8194
 * @return string|false the major version like '2.3', false if could not be determined
8195
 */
1326 ariadna 8196
function moodle_major_version($fromdisk = false)
8197
{
1 efrain 8198
    global $CFG;
8199
 
8200
    if ($fromdisk) {
8201
        $release = null;
1326 ariadna 8202
        require($CFG->dirroot . '/version.php');
1 efrain 8203
        if (empty($release)) {
8204
            return false;
8205
        }
8206
    } else {
8207
        if (empty($CFG->release)) {
8208
            return false;
8209
        }
8210
        $release = $CFG->release;
8211
    }
8212
 
8213
    if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
8214
        return $matches[0];
8215
    } else {
8216
        return false;
8217
    }
8218
}
8219
 
8220
// MISCELLANEOUS.
8221
 
8222
/**
8223
 * Gets the system locale
8224
 *
8225
 * @return string Retuns the current locale.
8226
 */
1326 ariadna 8227
function moodle_getlocale()
8228
{
1 efrain 8229
    global $CFG;
8230
 
8231
    // Fetch the correct locale based on ostype.
8232
    if ($CFG->ostype == 'WINDOWS') {
8233
        $stringtofetch = 'localewin';
8234
    } else {
8235
        $stringtofetch = 'locale';
8236
    }
8237
 
8238
    if (!empty($CFG->locale)) { // Override locale for all language packs.
8239
        return $CFG->locale;
8240
    }
8241
 
8242
    return get_string($stringtofetch, 'langconfig');
8243
}
8244
 
8245
/**
8246
 * Sets the system locale
8247
 *
8248
 * @category string
8249
 * @param string $locale Can be used to force a locale
8250
 */
1326 ariadna 8251
function moodle_setlocale($locale = '')
8252
{
1 efrain 8253
    global $CFG;
8254
 
8255
    static $currentlocale = ''; // Last locale caching.
8256
 
8257
    $oldlocale = $currentlocale;
8258
 
8259
    // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8260
    if (!empty($locale)) {
8261
        $currentlocale = $locale;
8262
    } else {
8263
        $currentlocale = moodle_getlocale();
8264
    }
8265
 
8266
    // Do nothing if locale already set up.
8267
    if ($oldlocale == $currentlocale) {
8268
        return;
8269
    }
8270
 
8271
    // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8272
    // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8273
    // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8274
 
8275
    // Get current values.
1326 ariadna 8276
    $monetary = setlocale(LC_MONETARY, 0);
8277
    $numeric = setlocale(LC_NUMERIC, 0);
8278
    $ctype   = setlocale(LC_CTYPE, 0);
1 efrain 8279
    if ($CFG->ostype != 'WINDOWS') {
1326 ariadna 8280
        $messages = setlocale(LC_MESSAGES, 0);
1 efrain 8281
    }
8282
    // Set locale to all.
1326 ariadna 8283
    $result = setlocale(LC_ALL, $currentlocale);
1 efrain 8284
    // If setting of locale fails try the other utf8 or utf-8 variant,
8285
    // some operating systems support both (Debian), others just one (OSX).
8286
    if ($result === false) {
8287
        if (stripos($currentlocale, '.UTF-8') !== false) {
8288
            $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
1326 ariadna 8289
            setlocale(LC_ALL, $newlocale);
1 efrain 8290
        } else if (stripos($currentlocale, '.UTF8') !== false) {
8291
            $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
1326 ariadna 8292
            setlocale(LC_ALL, $newlocale);
1 efrain 8293
        }
8294
    }
8295
    // Set old values.
1326 ariadna 8296
    setlocale(LC_MONETARY, $monetary);
8297
    setlocale(LC_NUMERIC, $numeric);
1 efrain 8298
    if ($CFG->ostype != 'WINDOWS') {
1326 ariadna 8299
        setlocale(LC_MESSAGES, $messages);
1 efrain 8300
    }
8301
    if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8302
        // To workaround a well-known PHP problem with Turkish letter Ii.
1326 ariadna 8303
        setlocale(LC_CTYPE, $ctype);
1 efrain 8304
    }
8305
}
8306
 
8307
/**
8308
 * Count words in a string.
8309
 *
8310
 * Words are defined as things between whitespace.
8311
 *
8312
 * @category string
8313
 * @param string $string The text to be searched for words. May be HTML.
8314
 * @param int|null $format
8315
 * @return int The count of words in the specified string
8316
 */
1326 ariadna 8317
function count_words($string, $format = null)
8318
{
1 efrain 8319
    // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8320
    // Also, br is a special case because it definitely delimits a word, but has no close tag.
8321
    $string = preg_replace('~
8322
            (                                   # Capture the tag we match.
8323
                </                              # Start of close tag.
8324
                (?!                             # Do not match any of these specific close tag names.
8325
                    a> | b> | del> | em> | i> |
8326
                    ins> | s> | small> | span> |
8327
                    strong> | sub> | sup> | u>
8328
                )
8329
                \w+                             # But, apart from those execptions, match any tag name.
8330
                >                               # End of close tag.
8331
            |
8332
                <br> | <br\s*/>                 # Special cases that are not close tags.
8333
            )
8334
            ~x', '$1 ', $string); // Add a space after the close tag.
8335
    if ($format !== null && $format != FORMAT_PLAIN) {
8336
        // Match the usual text cleaning before display.
8337
        // Ideally we should apply multilang filter only here, other filters might add extra text.
8338
        $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8339
    }
8340
    // Now remove HTML tags.
8341
    $string = strip_tags($string);
8342
    // Decode HTML entities.
8343
    $string = html_entity_decode($string, ENT_COMPAT);
8344
 
8345
    // Now, the word count is the number of blocks of characters separated
8346
    // by any sort of space. That seems to be the definition used by all other systems.
8347
    // To be precise about what is considered to separate words:
8348
    // * Anything that Unicode considers a 'Separator'
8349
    // * Anything that Unicode considers a 'Control character'
8350
    // * An em- or en- dash.
8351
    return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8352
}
8353
 
8354
/**
8355
 * Count letters in a string.
8356
 *
8357
 * Letters are defined as chars not in tags and different from whitespace.
8358
 *
8359
 * @category string
8360
 * @param string $string The text to be searched for letters. May be HTML.
8361
 * @param int|null $format
8362
 * @return int The count of letters in the specified text.
8363
 */
1326 ariadna 8364
function count_letters($string, $format = null)
8365
{
1 efrain 8366
    if ($format !== null && $format != FORMAT_PLAIN) {
8367
        // Match the usual text cleaning before display.
8368
        // Ideally we should apply multilang filter only here, other filters might add extra text.
8369
        $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8370
    }
8371
    $string = strip_tags($string); // Tags are out now.
8372
    $string = html_entity_decode($string, ENT_COMPAT);
8373
    $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8374
 
8375
    return core_text::strlen($string);
8376
}
8377
 
8378
/**
8379
 * Generate and return a random string of the specified length.
8380
 *
8381
 * @param int $length The length of the string to be created.
8382
 * @return string
8383
 */
1326 ariadna 8384
function random_string($length = 15)
8385
{
1 efrain 8386
    $randombytes = random_bytes($length);
8387
    $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8388
    $pool .= 'abcdefghijklmnopqrstuvwxyz';
8389
    $pool .= '0123456789';
8390
    $poollen = strlen($pool);
8391
    $string = '';
8392
    for ($i = 0; $i < $length; $i++) {
8393
        $rand = ord($randombytes[$i]);
1326 ariadna 8394
        $string .= substr($pool, ($rand % ($poollen)), 1);
1 efrain 8395
    }
8396
    return $string;
8397
}
8398
 
8399
/**
8400
 * Generate a complex random string (useful for md5 salts)
8401
 *
8402
 * This function is based on the above {@link random_string()} however it uses a
8403
 * larger pool of characters and generates a string between 24 and 32 characters
8404
 *
8405
 * @param int $length Optional if set generates a string to exactly this length
8406
 * @return string
8407
 */
1326 ariadna 8408
function complex_random_string($length = null)
8409
{
1 efrain 8410
    $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8411
    $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8412
    $poollen = strlen($pool);
1326 ariadna 8413
    if ($length === null) {
1 efrain 8414
        $length = floor(rand(24, 32));
8415
    }
8416
    $randombytes = random_bytes($length);
8417
    $string = '';
8418
    for ($i = 0; $i < $length; $i++) {
8419
        $rand = ord($randombytes[$i]);
1326 ariadna 8420
        $string .= $pool[($rand % $poollen)];
1 efrain 8421
    }
8422
    return $string;
8423
}
8424
 
8425
/**
8426
 * Given some text (which may contain HTML) and an ideal length,
8427
 * this function truncates the text neatly on a word boundary if possible
8428
 *
8429
 * @category string
8430
 * @param string $text text to be shortened
8431
 * @param int $ideal ideal string length
8432
 * @param boolean $exact if false, $text will not be cut mid-word
8433
 * @param string $ending The string to append if the passed string is truncated
8434
 * @return string $truncate shortened string
8435
 */
1326 ariadna 8436
function shorten_text($text, $ideal = 30, $exact = false, $ending = '...')
8437
{
1 efrain 8438
    // If the plain text is shorter than the maximum length, return the whole text.
8439
    if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8440
        return $text;
8441
    }
8442
 
8443
    // Splits on HTML tags. Each open/close/empty tag will be the first thing
8444
    // and only tag in its 'line'.
8445
    preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8446
 
8447
    $totallength = core_text::strlen($ending);
8448
    $truncate = '';
8449
 
8450
    // This array stores information about open and close tags and their position
8451
    // in the truncated string. Each item in the array is an object with fields
8452
    // ->open (true if open), ->tag (tag name in lower case), and ->pos
8453
    // (byte position in truncated text).
8454
    $tagdetails = array();
8455
 
8456
    foreach ($lines as $linematchings) {
8457
        // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8458
        if (!empty($linematchings[1])) {
8459
            // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8460
            if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8461
                if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8462
                    // Record closing tag.
8463
                    $tagdetails[] = (object) array(
1326 ariadna 8464
                        'open' => false,
8465
                        'tag'  => core_text::strtolower($tagmatchings[1]),
8466
                        'pos'  => core_text::strlen($truncate),
8467
                    );
1 efrain 8468
                } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8469
                    // Record opening tag.
8470
                    $tagdetails[] = (object) array(
1326 ariadna 8471
                        'open' => true,
8472
                        'tag'  => core_text::strtolower($tagmatchings[1]),
8473
                        'pos'  => core_text::strlen($truncate),
8474
                    );
1 efrain 8475
                } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8476
                    $tagdetails[] = (object) array(
1326 ariadna 8477
                        'open' => true,
8478
                        'tag'  => core_text::strtolower('if'),
8479
                        'pos'  => core_text::strlen($truncate),
1 efrain 8480
                    );
8481
                } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8482
                    $tagdetails[] = (object) array(
1326 ariadna 8483
                        'open' => false,
8484
                        'tag'  => core_text::strtolower('if'),
8485
                        'pos'  => core_text::strlen($truncate),
1 efrain 8486
                    );
8487
                }
8488
            }
8489
            // Add html-tag to $truncate'd text.
8490
            $truncate .= $linematchings[1];
8491
        }
8492
 
8493
        // Calculate the length of the plain text part of the line; handle entities as one character.
8494
        $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8495
        if ($totallength + $contentlength > $ideal) {
8496
            // The number of characters which are left.
8497
            $left = $ideal - $totallength;
8498
            $entitieslength = 0;
8499
            // Search for html entities.
8500
            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)) {
8501
                // Calculate the real length of all entities in the legal range.
8502
                foreach ($entities[0] as $entity) {
1326 ariadna 8503
                    if ($entity[1] + 1 - $entitieslength <= $left) {
1 efrain 8504
                        $left--;
8505
                        $entitieslength += core_text::strlen($entity[0]);
8506
                    } else {
8507
                        // No more characters left.
8508
                        break;
8509
                    }
8510
                }
8511
            }
8512
            $breakpos = $left + $entitieslength;
8513
 
8514
            // If the words shouldn't be cut in the middle...
8515
            if (!$exact) {
8516
                // Search the last occurence of a space.
8517
                for (; $breakpos > 0; $breakpos--) {
8518
                    if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8519
                        if ($char === '.' or $char === ' ') {
8520
                            $breakpos += 1;
8521
                            break;
8522
                        } else if (strlen($char) > 2) {
8523
                            // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8524
                            $breakpos += 1;
8525
                            break;
8526
                        }
8527
                    }
8528
                }
8529
            }
8530
            if ($breakpos == 0) {
8531
                // This deals with the test_shorten_text_no_spaces case.
8532
                $breakpos = $left + $entitieslength;
8533
            } else if ($breakpos > $left + $entitieslength) {
8534
                // This deals with the previous for loop breaking on the first char.
8535
                $breakpos = $left + $entitieslength;
8536
            }
8537
 
8538
            $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8539
            // Maximum length is reached, so get off the loop.
8540
            break;
8541
        } else {
8542
            $truncate .= $linematchings[2];
8543
            $totallength += $contentlength;
8544
        }
8545
 
8546
        // If the maximum length is reached, get off the loop.
8547
        if ($totallength >= $ideal) {
8548
            break;
8549
        }
8550
    }
8551
 
8552
    // Add the defined ending to the text.
8553
    $truncate .= $ending;
8554
 
8555
    // Now calculate the list of open html tags based on the truncate position.
8556
    $opentags = array();
8557
    foreach ($tagdetails as $taginfo) {
8558
        if ($taginfo->open) {
8559
            // Add tag to the beginning of $opentags list.
8560
            array_unshift($opentags, $taginfo->tag);
8561
        } else {
8562
            // Can have multiple exact same open tags, close the last one.
8563
            $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8564
            if ($pos !== false) {
8565
                unset($opentags[$pos]);
8566
            }
8567
        }
8568
    }
8569
 
8570
    // Close all unclosed html-tags.
8571
    foreach ($opentags as $tag) {
8572
        if ($tag === 'if') {
8573
            $truncate .= '<!--<![endif]-->';
8574
        } else {
8575
            $truncate .= '</' . $tag . '>';
8576
        }
8577
    }
8578
 
8579
    return $truncate;
8580
}
8581
 
8582
/**
8583
 * Shortens a given filename by removing characters positioned after the ideal string length.
8584
 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8585
 * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8586
 *
8587
 * @param string $filename file name
8588
 * @param int $length ideal string length
8589
 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8590
 * @return string $shortened shortened file name
8591
 */
1326 ariadna 8592
function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false)
8593
{
1 efrain 8594
    $shortened = $filename;
8595
    // Extract a part of the filename if it's char size exceeds the ideal string length.
8596
    if (core_text::strlen($filename) > $length) {
8597
        // Exclude extension if present in filename.
8598
        $mimetypes = get_mimetypes_array();
8599
        $extension = pathinfo($filename, PATHINFO_EXTENSION);
8600
        if ($extension && !empty($mimetypes[$extension])) {
8601
            $basename = pathinfo($filename, PATHINFO_FILENAME);
8602
            $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8603
            $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8604
            $shortened .= '.' . $extension;
8605
        } else {
8606
            $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8607
            $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8608
        }
8609
    }
8610
    return $shortened;
8611
}
8612
 
8613
/**
8614
 * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8615
 *
8616
 * @param array $path The paths to reduce the length.
8617
 * @param int $length Ideal string length
8618
 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8619
 * @return array $result Shortened paths in array.
8620
 */
1326 ariadna 8621
function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false)
8622
{
1 efrain 8623
    $result = null;
8624
 
1326 ariadna 8625
    $result = array_reduce($path, function ($carry, $singlepath) use ($length, $includehash) {
1 efrain 8626
        $carry[] = shorten_filename($singlepath, $length, $includehash);
8627
        return $carry;
8628
    }, []);
8629
 
8630
    return $result;
8631
}
8632
 
8633
/**
8634
 * Given dates in seconds, how many weeks is the date from startdate
8635
 * The first week is 1, the second 2 etc ...
8636
 *
8637
 * @param int $startdate Timestamp for the start date
8638
 * @param int $thedate Timestamp for the end date
8639
 * @return string
8640
 */
1326 ariadna 8641
function getweek($startdate, $thedate)
8642
{
1 efrain 8643
    if ($thedate < $startdate) {
8644
        return 0;
8645
    }
8646
 
8647
    return floor(($thedate - $startdate) / WEEKSECS) + 1;
8648
}
8649
 
8650
/**
8651
 * Returns a randomly generated password of length $maxlen.  inspired by
8652
 *
8653
 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8654
 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8655
 *
8656
 * @param int $maxlen  The maximum size of the password being generated.
8657
 * @return string
8658
 */
1326 ariadna 8659
function generate_password($maxlen = 10)
8660
{
1 efrain 8661
    global $CFG;
8662
 
8663
    if (empty($CFG->passwordpolicy)) {
8664
        $fillers = PASSWORD_DIGITS;
8665
        $wordlist = file($CFG->wordlist);
8666
        $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8667
        $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8668
        $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8669
        $password = $word1 . $filler1 . $word2;
8670
    } else {
8671
        $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8672
        $digits = $CFG->minpassworddigits;
8673
        $lower = $CFG->minpasswordlower;
8674
        $upper = $CFG->minpasswordupper;
8675
        $nonalphanum = $CFG->minpasswordnonalphanum;
8676
        $total = $lower + $upper + $digits + $nonalphanum;
8677
        // Var minlength should be the greater one of the two ( $minlen and $total ).
8678
        $minlen = $minlen < $total ? $total : $minlen;
8679
        // Var maxlen can never be smaller than minlen.
8680
        $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8681
        $additional = $maxlen - $total;
8682
 
8683
        // Make sure we have enough characters to fulfill
8684
        // complexity requirements.
8685
        $passworddigits = PASSWORD_DIGITS;
8686
        while ($digits > strlen($passworddigits)) {
8687
            $passworddigits .= PASSWORD_DIGITS;
8688
        }
8689
        $passwordlower = PASSWORD_LOWER;
8690
        while ($lower > strlen($passwordlower)) {
8691
            $passwordlower .= PASSWORD_LOWER;
8692
        }
8693
        $passwordupper = PASSWORD_UPPER;
8694
        while ($upper > strlen($passwordupper)) {
8695
            $passwordupper .= PASSWORD_UPPER;
8696
        }
8697
        $passwordnonalphanum = PASSWORD_NONALPHANUM;
8698
        while ($nonalphanum > strlen($passwordnonalphanum)) {
8699
            $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8700
        }
8701
 
8702
        // Now mix and shuffle it all.
1326 ariadna 8703
        $password = str_shuffle(substr(str_shuffle($passwordlower), 0, $lower) .
8704
            substr(str_shuffle($passwordupper), 0, $upper) .
8705
            substr(str_shuffle($passworddigits), 0, $digits) .
8706
            substr(str_shuffle($passwordnonalphanum), 0, $nonalphanum) .
8707
            substr(str_shuffle($passwordlower .
8708
                $passwordupper .
8709
                $passworddigits .
8710
                $passwordnonalphanum), 0, $additional));
1 efrain 8711
    }
8712
 
1326 ariadna 8713
    return substr($password, 0, $maxlen);
1 efrain 8714
}
8715
 
8716
/**
8717
 * Given a float, prints it nicely.
8718
 * Localized floats must not be used in calculations!
8719
 *
8720
 * The stripzeros feature is intended for making numbers look nicer in small
8721
 * areas where it is not necessary to indicate the degree of accuracy by showing
8722
 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8723
 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8724
 *
8725
 * @param float $float The float to print
8726
 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8727
 * @param bool $localized use localized decimal separator
8728
 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8729
 *                         the decimal point are always striped if $decimalpoints is -1.
8730
 * @return string locale float
8731
 */
1326 ariadna 8732
function format_float($float, $decimalpoints = 1, $localized = true, $stripzeros = false)
8733
{
1 efrain 8734
    if (is_null($float)) {
8735
        return '';
8736
    }
8737
    if ($localized) {
8738
        $separator = get_string('decsep', 'langconfig');
8739
    } else {
8740
        $separator = '.';
8741
    }
8742
    if ($decimalpoints == -1) {
8743
        // The following counts the number of decimals.
8744
        // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8745
        $floatval = floatval($float);
8746
        for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8747
    }
8748
 
8749
    $result = number_format($float, $decimalpoints, $separator, '');
8750
    if ($stripzeros && $decimalpoints > 0) {
8751
        // Remove zeros and final dot if not needed.
8752
        // However, only do this if there is a decimal point!
8753
        $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8754
    }
8755
    return $result;
8756
}
8757
 
8758
/**
8759
 * Converts locale specific floating point/comma number back to standard PHP float value
8760
 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8761
 *
8762
 * @param string $localefloat locale aware float representation
8763
 * @param bool $strict If true, then check the input and return false if it is not a valid number.
8764
 * @return mixed float|bool - false or the parsed float.
8765
 */
1326 ariadna 8766
function unformat_float($localefloat, $strict = false)
8767
{
1 efrain 8768
    $localefloat = trim((string)$localefloat);
8769
 
8770
    if ($localefloat == '') {
8771
        return null;
8772
    }
8773
 
8774
    $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8775
    $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8776
 
8777
    if ($strict && !is_numeric($localefloat)) {
8778
        return false;
8779
    }
8780
 
8781
    return (float)$localefloat;
8782
}
8783
 
8784
/**
8785
 * Given a simple array, this shuffles it up just like shuffle()
8786
 * Unlike PHP's shuffle() this function works on any machine.
8787
 *
8788
 * @param array $array The array to be rearranged
8789
 * @return array
8790
 */
1326 ariadna 8791
function swapshuffle($array)
8792
{
1 efrain 8793
 
8794
    $last = count($array) - 1;
8795
    for ($i = 0; $i <= $last; $i++) {
8796
        $from = rand(0, $last);
8797
        $curr = $array[$i];
8798
        $array[$i] = $array[$from];
8799
        $array[$from] = $curr;
8800
    }
8801
    return $array;
8802
}
8803
 
8804
/**
8805
 * Like {@link swapshuffle()}, but works on associative arrays
8806
 *
8807
 * @param array $array The associative array to be rearranged
8808
 * @return array
8809
 */
1326 ariadna 8810
function swapshuffle_assoc($array)
8811
{
1 efrain 8812
 
8813
    $newarray = array();
8814
    $newkeys = swapshuffle(array_keys($array));
8815
 
8816
    foreach ($newkeys as $newkey) {
8817
        $newarray[$newkey] = $array[$newkey];
8818
    }
8819
    return $newarray;
8820
}
8821
 
8822
/**
8823
 * Given an arbitrary array, and a number of draws,
8824
 * this function returns an array with that amount
8825
 * of items.  The indexes are retained.
8826
 *
8827
 * @todo Finish documenting this function
8828
 *
8829
 * @param array $array
8830
 * @param int $draws
8831
 * @return array
8832
 */
1326 ariadna 8833
function draw_rand_array($array, $draws)
8834
{
1 efrain 8835
 
8836
    $return = array();
8837
 
8838
    $last = count($array);
8839
 
8840
    if ($draws > $last) {
8841
        $draws = $last;
8842
    }
8843
 
8844
    while ($draws > 0) {
8845
        $last--;
8846
 
8847
        $keys = array_keys($array);
8848
        $rand = rand(0, $last);
8849
 
8850
        $return[$keys[$rand]] = $array[$keys[$rand]];
8851
        unset($array[$keys[$rand]]);
8852
 
8853
        $draws--;
8854
    }
8855
 
8856
    return $return;
8857
}
8858
 
8859
/**
8860
 * Calculate the difference between two microtimes
8861
 *
8862
 * @param string $a The first Microtime
8863
 * @param string $b The second Microtime
8864
 * @return string
8865
 */
1326 ariadna 8866
function microtime_diff($a, $b)
8867
{
1 efrain 8868
    list($adec, $asec) = explode(' ', $a);
8869
    list($bdec, $bsec) = explode(' ', $b);
8870
    return $bsec - $asec + $bdec - $adec;
8871
}
8872
 
8873
/**
8874
 * Given a list (eg a,b,c,d,e) this function returns
8875
 * an array of 1->a, 2->b, 3->c etc
8876
 *
8877
 * @param string $list The string to explode into array bits
8878
 * @param string $separator The separator used within the list string
8879
 * @return array The now assembled array
8880
 */
1326 ariadna 8881
function make_menu_from_list($list, $separator = ',')
8882
{
1 efrain 8883
 
8884
    $array = array_reverse(explode($separator, $list), true);
8885
    foreach ($array as $key => $item) {
1326 ariadna 8886
        $outarray[$key + 1] = trim($item);
1 efrain 8887
    }
8888
    return $outarray;
8889
}
8890
 
8891
/**
8892
 * Creates an array that represents all the current grades that
8893
 * can be chosen using the given grading type.
8894
 *
8895
 * Negative numbers
8896
 * are scales, zero is no grade, and positive numbers are maximum
8897
 * grades.
8898
 *
8899
 * @todo Finish documenting this function or better deprecated this completely!
8900
 *
8901
 * @param int $gradingtype
8902
 * @return array
8903
 */
1326 ariadna 8904
function make_grades_menu($gradingtype)
8905
{
1 efrain 8906
    global $DB;
8907
 
8908
    $grades = array();
8909
    if ($gradingtype < 0) {
1326 ariadna 8910
        if ($scale = $DB->get_record('scale', array('id' => (-$gradingtype)))) {
1 efrain 8911
            return make_menu_from_list($scale->scale);
8912
        }
8913
    } else if ($gradingtype > 0) {
1326 ariadna 8914
        for ($i = $gradingtype; $i >= 0; $i--) {
8915
            $grades[$i] = $i . ' / ' . $gradingtype;
1 efrain 8916
        }
8917
        return $grades;
8918
    }
8919
    return $grades;
8920
}
8921
 
8922
/**
8923
 * make_unique_id_code
8924
 *
8925
 * @todo Finish documenting this function
8926
 *
8927
 * @uses $_SERVER
8928
 * @param string $extra Extra string to append to the end of the code
8929
 * @return string
8930
 */
1326 ariadna 8931
function make_unique_id_code($extra = '')
8932
{
1 efrain 8933
 
8934
    $hostname = 'unknownhost';
8935
    if (!empty($_SERVER['HTTP_HOST'])) {
8936
        $hostname = $_SERVER['HTTP_HOST'];
8937
    } else if (!empty($_ENV['HTTP_HOST'])) {
8938
        $hostname = $_ENV['HTTP_HOST'];
8939
    } else if (!empty($_SERVER['SERVER_NAME'])) {
8940
        $hostname = $_SERVER['SERVER_NAME'];
8941
    } else if (!empty($_ENV['SERVER_NAME'])) {
8942
        $hostname = $_ENV['SERVER_NAME'];
8943
    }
8944
 
8945
    $date = gmdate("ymdHis");
8946
 
8947
    $random =  random_string(6);
8948
 
8949
    if ($extra) {
1326 ariadna 8950
        return $hostname . '+' . $date . '+' . $random . '+' . $extra;
1 efrain 8951
    } else {
1326 ariadna 8952
        return $hostname . '+' . $date . '+' . $random;
1 efrain 8953
    }
8954
}
8955
 
8956
 
8957
/**
8958
 * Function to check the passed address is within the passed subnet
8959
 *
8960
 * The parameter is a comma separated string of subnet definitions.
8961
 * Subnet strings can be in one of three formats:
8962
 *   1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn          (number of bits in net mask)
8963
 *   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)
8964
 *   3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.                  (incomplete address, a bit non-technical ;-)
8965
 * Code for type 1 modified from user posted comments by mediator at
8966
 * {@link http://au.php.net/manual/en/function.ip2long.php}
8967
 *
8968
 * @param string $addr    The address you are checking
8969
 * @param string $subnetstr    The string of subnet addresses
8970
 * @param bool $checkallzeros    The state to whether check for 0.0.0.0
8971
 * @return bool
8972
 */
1326 ariadna 8973
function address_in_subnet($addr, $subnetstr, $checkallzeros = false)
8974
{
1 efrain 8975
 
8976
    if ($addr == '0.0.0.0' && !$checkallzeros) {
8977
        return false;
8978
    }
8979
    $subnets = explode(',', $subnetstr);
8980
    $found = false;
8981
    $addr = trim($addr);
8982
    $addr = cleanremoteaddr($addr, false); // Normalise.
8983
    if ($addr === null) {
8984
        return false;
8985
    }
8986
    $addrparts = explode(':', $addr);
8987
 
8988
    $ipv6 = strpos($addr, ':');
8989
 
8990
    foreach ($subnets as $subnet) {
8991
        $subnet = trim($subnet);
8992
        if ($subnet === '') {
8993
            continue;
8994
        }
8995
 
8996
        if (strpos($subnet, '/') !== false) {
8997
            // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
8998
            list($ip, $mask) = explode('/', $subnet);
8999
            $mask = trim($mask);
9000
            if (!is_number($mask)) {
9001
                continue; // Incorect mask number, eh?
9002
            }
9003
            $ip = cleanremoteaddr($ip, false); // Normalise.
9004
            if ($ip === null) {
9005
                continue;
9006
            }
9007
            if (strpos($ip, ':') !== false) {
9008
                // IPv6.
9009
                if (!$ipv6) {
9010
                    continue;
9011
                }
9012
                if ($mask > 128 or $mask < 0) {
9013
                    continue; // Nonsense.
9014
                }
9015
                if ($mask == 0) {
9016
                    return true; // Any address.
9017
                }
9018
                if ($mask == 128) {
9019
                    if ($ip === $addr) {
9020
                        return true;
9021
                    }
9022
                    continue;
9023
                }
9024
                $ipparts = explode(':', $ip);
9025
                $modulo  = $mask % 16;
1326 ariadna 9026
                $ipnet   = array_slice($ipparts, 0, ($mask - $modulo) / 16);
9027
                $addrnet = array_slice($addrparts, 0, ($mask - $modulo) / 16);
1 efrain 9028
                if (implode(':', $ipnet) === implode(':', $addrnet)) {
9029
                    if ($modulo == 0) {
9030
                        return true;
9031
                    }
1326 ariadna 9032
                    $pos     = ($mask - $modulo) / 16;
1 efrain 9033
                    $ipnet   = hexdec($ipparts[$pos]);
9034
                    $addrnet = hexdec($addrparts[$pos]);
9035
                    $mask    = 0xffff << (16 - $modulo);
9036
                    if (($addrnet & $mask) == ($ipnet & $mask)) {
9037
                        return true;
9038
                    }
9039
                }
9040
            } else {
9041
                // IPv4.
9042
                if ($ipv6) {
9043
                    continue;
9044
                }
9045
                if ($mask > 32 or $mask < 0) {
9046
                    continue; // Nonsense.
9047
                }
9048
                if ($mask == 0) {
9049
                    return true;
9050
                }
9051
                if ($mask == 32) {
9052
                    if ($ip === $addr) {
9053
                        return true;
9054
                    }
9055
                    continue;
9056
                }
9057
                $mask = 0xffffffff << (32 - $mask);
9058
                if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9059
                    return true;
9060
                }
9061
            }
9062
        } else if (strpos($subnet, '-') !== false) {
9063
            // 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.
9064
            $parts = explode('-', $subnet);
9065
            if (count($parts) != 2) {
9066
                continue;
9067
            }
9068
 
9069
            if (strpos($subnet, ':') !== false) {
9070
                // IPv6.
9071
                if (!$ipv6) {
9072
                    continue;
9073
                }
9074
                $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9075
                if ($ipstart === null) {
9076
                    continue;
9077
                }
9078
                $ipparts = explode(':', $ipstart);
9079
                $start = hexdec(array_pop($ipparts));
9080
                $ipparts[] = trim($parts[1]);
9081
                $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
9082
                if ($ipend === null) {
9083
                    continue;
9084
                }
9085
                $ipparts[7] = '';
9086
                $ipnet = implode(':', $ipparts);
9087
                if (strpos($addr, $ipnet) !== 0) {
9088
                    continue;
9089
                }
9090
                $ipparts = explode(':', $ipend);
9091
                $end = hexdec($ipparts[7]);
9092
 
9093
                $addrend = hexdec($addrparts[7]);
9094
 
9095
                if (($addrend >= $start) and ($addrend <= $end)) {
9096
                    return true;
9097
                }
9098
            } else {
9099
                // IPv4.
9100
                if ($ipv6) {
9101
                    continue;
9102
                }
9103
                $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9104
                if ($ipstart === null) {
9105
                    continue;
9106
                }
9107
                $ipparts = explode('.', $ipstart);
9108
                $ipparts[3] = trim($parts[1]);
9109
                $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
9110
                if ($ipend === null) {
9111
                    continue;
9112
                }
9113
 
9114
                if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9115
                    return true;
9116
                }
9117
            }
9118
        } else {
9119
            // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9120
            if (strpos($subnet, ':') !== false) {
9121
                // IPv6.
9122
                if (!$ipv6) {
9123
                    continue;
9124
                }
9125
                $parts = explode(':', $subnet);
9126
                $count = count($parts);
1326 ariadna 9127
                if ($parts[$count - 1] === '') {
9128
                    unset($parts[$count - 1]); // Trim trailing :'s.
1 efrain 9129
                    $count--;
9130
                    $subnet = implode('.', $parts);
9131
                }
9132
                $isip = cleanremoteaddr($subnet, false); // Normalise.
9133
                if ($isip !== null) {
9134
                    if ($isip === $addr) {
9135
                        return true;
9136
                    }
9137
                    continue;
9138
                } else if ($count > 8) {
9139
                    continue;
9140
                }
1326 ariadna 9141
                $zeros = array_fill(0, 8 - $count, '0');
9142
                $subnet = $subnet . ':' . implode(':', $zeros) . '/' . ($count * 16);
1 efrain 9143
                if (address_in_subnet($addr, $subnet)) {
9144
                    return true;
9145
                }
9146
            } else {
9147
                // IPv4.
9148
                if ($ipv6) {
9149
                    continue;
9150
                }
9151
                $parts = explode('.', $subnet);
9152
                $count = count($parts);
1326 ariadna 9153
                if ($parts[$count - 1] === '') {
9154
                    unset($parts[$count - 1]); // Trim trailing .
1 efrain 9155
                    $count--;
9156
                    $subnet = implode('.', $parts);
9157
                }
9158
                if ($count == 4) {
9159
                    $subnet = cleanremoteaddr($subnet, false); // Normalise.
9160
                    if ($subnet === $addr) {
9161
                        return true;
9162
                    }
9163
                    continue;
9164
                } else if ($count > 4) {
9165
                    continue;
9166
                }
1326 ariadna 9167
                $zeros = array_fill(0, 4 - $count, '0');
9168
                $subnet = $subnet . '.' . implode('.', $zeros) . '/' . ($count * 8);
1 efrain 9169
                if (address_in_subnet($addr, $subnet)) {
9170
                    return true;
9171
                }
9172
            }
9173
        }
9174
    }
9175
 
9176
    return false;
9177
}
9178
 
9179
/**
9180
 * For outputting debugging info
9181
 *
9182
 * @param string $string The string to write
9183
 * @param string $eol The end of line char(s) to use
9184
 * @param string $sleep Period to make the application sleep
9185
 *                      This ensures any messages have time to display before redirect
9186
 */
1326 ariadna 9187
function mtrace($string, $eol = "\n", $sleep = 0)
9188
{
1 efrain 9189
    global $CFG;
9190
 
9191
    if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
9192
        $fn = $CFG->mtrace_wrapper;
9193
        $fn($string, $eol);
9194
        return;
9195
    } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
9196
        // We must explicitly call the add_line function here.
9197
        // Uses of fwrite to STDOUT are not picked up by ob_start.
9198
        if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
9199
            fwrite(STDOUT, $output);
9200
        }
9201
    } else {
9202
        echo $string . $eol;
9203
    }
9204
 
9205
    // Flush again.
9206
    flush();
9207
 
9208
    // Delay to keep message on user's screen in case of subsequent redirect.
9209
    if ($sleep) {
9210
        sleep($sleep);
9211
    }
9212
}
9213
 
9214
/**
9215
 * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
9216
 *
9217
 * @param Throwable $e the error to ouptput.
9218
 */
1326 ariadna 9219
function mtrace_exception(Throwable $e): void
9220
{
1 efrain 9221
    $info = get_exception_info($e);
9222
 
9223
    $message = $info->message;
9224
    if ($info->debuginfo) {
9225
        $message .= "\n\n" . $info->debuginfo;
9226
    }
9227
    if ($info->backtrace) {
9228
        $message .= "\n\n" . format_backtrace($info->backtrace, true);
9229
    }
9230
 
9231
    mtrace($message);
9232
}
9233
 
9234
/**
9235
 * Replace 1 or more slashes or backslashes to 1 slash
9236
 *
9237
 * @param string $path The path to strip
9238
 * @return string the path with double slashes removed
9239
 */
1326 ariadna 9240
function cleardoubleslashes($path)
9241
{
1 efrain 9242
    return preg_replace('/(\/|\\\){1,}/', '/', $path);
9243
}
9244
 
9245
/**
9246
 * Is the current ip in a given list?
9247
 *
9248
 * @param string $list
9249
 * @return bool
9250
 */
1326 ariadna 9251
function remoteip_in_list($list)
9252
{
1 efrain 9253
    $clientip = getremoteaddr(null);
9254
 
9255
    if (!$clientip) {
9256
        // Ensure access on cli.
9257
        return true;
9258
    }
9259
    return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
9260
}
9261
 
9262
/**
9263
 * Returns most reliable client address
9264
 *
9265
 * @param string $default If an address can't be determined, then return this
9266
 * @return string The remote IP address
9267
 */
1326 ariadna 9268
function getremoteaddr($default = '0.0.0.0')
9269
{
1 efrain 9270
    global $CFG;
9271
 
9272
    if (!isset($CFG->getremoteaddrconf)) {
9273
        // This will happen, for example, before just after the upgrade, as the
9274
        // user is redirected to the admin screen.
9275
        $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
9276
    } else {
9277
        $variablestoskip = $CFG->getremoteaddrconf;
9278
    }
9279
    if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9280
        if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9281
            $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9282
            return $address ? $address : $default;
9283
        }
9284
    }
9285
    if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9286
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9287
            $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9288
 
1326 ariadna 9289
            $forwardedaddresses = array_filter($forwardedaddresses, function ($ip) {
1 efrain 9290
                global $CFG;
9291
                return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9292
            });
9293
 
9294
            // Multiple proxies can append values to this header including an
9295
            // untrusted original request header so we must only trust the last ip.
9296
            $address = end($forwardedaddresses);
9297
 
9298
            if (substr_count($address, ":") > 1) {
9299
                // Remove port and brackets from IPv6.
9300
                if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9301
                    $address = $matches[1];
9302
                }
9303
            } else {
9304
                // Remove port from IPv4.
9305
                if (substr_count($address, ":") == 1) {
9306
                    $parts = explode(":", $address);
9307
                    $address = $parts[0];
9308
                }
9309
            }
9310
 
9311
            $address = cleanremoteaddr($address);
9312
            return $address ? $address : $default;
9313
        }
9314
    }
9315
    if (!empty($_SERVER['REMOTE_ADDR'])) {
9316
        $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9317
        return $address ? $address : $default;
9318
    } else {
9319
        return $default;
9320
    }
9321
}
9322
 
9323
/**
9324
 * Cleans an ip address. Internal addresses are now allowed.
9325
 * (Originally local addresses were not allowed.)
9326
 *
9327
 * @param string $addr IPv4 or IPv6 address
9328
 * @param bool $compress use IPv6 address compression
9329
 * @return string normalised ip address string, null if error
9330
 */
1326 ariadna 9331
function cleanremoteaddr($addr, $compress = false)
9332
{
1 efrain 9333
    $addr = trim($addr);
9334
 
9335
    if (strpos($addr, ':') !== false) {
9336
        // Can be only IPv6.
9337
        $parts = explode(':', $addr);
9338
        $count = count($parts);
9339
 
1326 ariadna 9340
        if (strpos($parts[$count - 1], '.') !== false) {
1 efrain 9341
            // Legacy ipv4 notation.
9342
            $last = array_pop($parts);
9343
            $ipv4 = cleanremoteaddr($last, true);
9344
            if ($ipv4 === null) {
9345
                return null;
9346
            }
9347
            $bits = explode('.', $ipv4);
1326 ariadna 9348
            $parts[] = dechex($bits[0]) . dechex($bits[1]);
9349
            $parts[] = dechex($bits[2]) . dechex($bits[3]);
1 efrain 9350
            $count = count($parts);
9351
            $addr = implode(':', $parts);
9352
        }
9353
 
9354
        if ($count < 3 or $count > 8) {
9355
            return null; // Severly malformed.
9356
        }
9357
 
9358
        if ($count != 8) {
9359
            if (strpos($addr, '::') === false) {
9360
                return null; // Malformed.
9361
            }
9362
            // Uncompress.
9363
            $insertat = array_search('', $parts, true);
9364
            $missing = array_fill(0, 1 + 8 - $count, '0');
9365
            array_splice($parts, $insertat, 1, $missing);
9366
            foreach ($parts as $key => $part) {
9367
                if ($part === '') {
9368
                    $parts[$key] = '0';
9369
                }
9370
            }
9371
        }
9372
 
9373
        $adr = implode(':', $parts);
9374
        if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9375
            return null; // Incorrect format - sorry.
9376
        }
9377
 
9378
        // Normalise 0s and case.
9379
        $parts = array_map('hexdec', $parts);
9380
        $parts = array_map('dechex', $parts);
9381
 
9382
        $result = implode(':', $parts);
9383
 
9384
        if (!$compress) {
9385
            return $result;
9386
        }
9387
 
9388
        if ($result === '0:0:0:0:0:0:0:0') {
9389
            return '::'; // All addresses.
9390
        }
9391
 
9392
        $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9393
        if ($compressed !== $result) {
9394
            return $compressed;
9395
        }
9396
 
9397
        $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9398
        if ($compressed !== $result) {
9399
            return $compressed;
9400
        }
9401
 
9402
        $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9403
        if ($compressed !== $result) {
9404
            return $compressed;
9405
        }
9406
 
9407
        return $result;
9408
    }
9409
 
9410
    // First get all things that look like IPv4 addresses.
9411
    $parts = array();
9412
    if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9413
        return null;
9414
    }
9415
    unset($parts[0]);
9416
 
9417
    foreach ($parts as $key => $match) {
9418
        if ($match > 255) {
9419
            return null;
9420
        }
9421
        $parts[$key] = (int)$match; // Normalise 0s.
9422
    }
9423
 
9424
    return implode('.', $parts);
9425
}
9426
 
9427
 
9428
/**
9429
 * Is IP address a public address?
9430
 *
9431
 * @param string $ip The ip to check
9432
 * @return bool true if the ip is public
9433
 */
1326 ariadna 9434
function ip_is_public($ip)
9435
{
1 efrain 9436
    return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9437
}
9438
 
9439
/**
9440
 * This function will make a complete copy of anything it's given,
9441
 * regardless of whether it's an object or not.
9442
 *
9443
 * @param mixed $thing Something you want cloned
9444
 * @return mixed What ever it is you passed it
9445
 */
1326 ariadna 9446
function fullclone($thing)
9447
{
1 efrain 9448
    return unserialize(serialize($thing));
9449
}
9450
 
9451
/**
9452
 * Used to make sure that $min <= $value <= $max
9453
 *
9454
 * Make sure that value is between min, and max
9455
 *
9456
 * @param int $min The minimum value
9457
 * @param int $value The value to check
9458
 * @param int $max The maximum value
9459
 * @return int
9460
 */
1326 ariadna 9461
function bounded_number($min, $value, $max)
9462
{
1 efrain 9463
    if ($value < $min) {
9464
        return $min;
9465
    }
9466
    if ($value > $max) {
9467
        return $max;
9468
    }
9469
    return $value;
9470
}
9471
 
9472
/**
9473
 * Check if there is a nested array within the passed array
9474
 *
9475
 * @param array $array
9476
 * @return bool true if there is a nested array false otherwise
9477
 */
1326 ariadna 9478
function array_is_nested($array)
9479
{
1 efrain 9480
    foreach ($array as $value) {
9481
        if (is_array($value)) {
9482
            return true;
9483
        }
9484
    }
9485
    return false;
9486
}
9487
 
9488
/**
9489
 * get_performance_info() pairs up with init_performance_info()
9490
 * loaded in setup.php. Returns an array with 'html' and 'txt'
9491
 * values ready for use, and each of the individual stats provided
9492
 * separately as well.
9493
 *
9494
 * @return array
9495
 */
1326 ariadna 9496
function get_performance_info()
9497
{
1 efrain 9498
    global $CFG, $PERF, $DB, $PAGE;
9499
 
9500
    $info = array();
9501
    $info['txt']  = me() . ' '; // Holds log-friendly representation.
9502
 
9503
    $info['html'] = '';
9504
    if (!empty($CFG->themedesignermode)) {
9505
        // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9506
        $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9507
    }
9508
    $info['html'] .= '<ul class="list-unstyled row mx-md-0">';         // Holds userfriendly HTML representation.
9509
 
9510
    $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9511
 
1326 ariadna 9512
    $info['html'] .= '<li class="timeused col-sm-4">' . $info['realtime'] . ' secs</li> ';
9513
    $info['txt'] .= 'time: ' . $info['realtime'] . 's ';
1 efrain 9514
 
9515
    // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9516
    $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9517
 
9518
    if (function_exists('memory_get_usage')) {
9519
        $info['memory_total'] = memory_get_usage();
9520
        $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
1326 ariadna 9521
        $info['html'] .= '<li class="memoryused col-sm-4">RAM: ' . display_size($info['memory_total']) . '</li> ';
9522
        $info['txt']  .= 'memory_total: ' . $info['memory_total'] . 'B (' . display_size($info['memory_total']) . ') memory_growth: ' .
9523
            $info['memory_growth'] . 'B (' . display_size($info['memory_growth']) . ') ';
1 efrain 9524
    }
9525
 
9526
    if (function_exists('memory_get_peak_usage')) {
9527
        $info['memory_peak'] = memory_get_peak_usage();
1326 ariadna 9528
        $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: ' . display_size($info['memory_peak']) . '</li> ';
9529
        $info['txt']  .= 'memory_peak: ' . $info['memory_peak'] . 'B (' . display_size($info['memory_peak']) . ') ';
1 efrain 9530
    }
9531
 
9532
    $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9533
    $inc = get_included_files();
9534
    $info['includecount'] = count($inc);
1326 ariadna 9535
    $info['html'] .= '<li class="included col-sm-4">Included ' . $info['includecount'] . ' files</li> ';
9536
    $info['txt']  .= 'includecount: ' . $info['includecount'] . ' ';
1 efrain 9537
 
9538
    if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9539
        // We can not track more performance before installation or before PAGE init, sorry.
9540
        return $info;
9541
    }
9542
 
9543
    $filtermanager = filter_manager::instance();
9544
    if (method_exists($filtermanager, 'get_performance_summary')) {
9545
        list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9546
        $info = array_merge($filterinfo, $info);
9547
        foreach ($filterinfo as $key => $value) {
9548
            $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9549
            $info['txt'] .= "$key: $value ";
9550
        }
9551
    }
9552
 
9553
    $stringmanager = get_string_manager();
9554
    if (method_exists($stringmanager, 'get_performance_summary')) {
9555
        list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9556
        $info = array_merge($filterinfo, $info);
9557
        foreach ($filterinfo as $key => $value) {
9558
            $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9559
            $info['txt'] .= "$key: $value ";
9560
        }
9561
    }
9562
 
1326 ariadna 9563
    $info['dbqueries'] = $DB->perf_get_reads() . '/' . $DB->perf_get_writes();
9564
    $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: ' . $info['dbqueries'] . '</li> ';
9565
    $info['txt'] .= 'db reads/writes: ' . $info['dbqueries'] . ' ';
1 efrain 9566
 
9567
    if ($DB->want_read_slave()) {
9568
        $info['dbreads_slave'] = $DB->perf_get_reads_slave();
1326 ariadna 9569
        $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: ' . $info['dbreads_slave'] . '</li> ';
9570
        $info['txt'] .= 'db reads from slave: ' . $info['dbreads_slave'] . ' ';
1 efrain 9571
    }
9572
 
9573
    $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
1326 ariadna 9574
    $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: ' . $info['dbtime'] . ' secs</li> ';
1 efrain 9575
    $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9576
 
9577
    if (function_exists('posix_times')) {
9578
        $ptimes = posix_times();
9579
        if (is_array($ptimes)) {
9580
            foreach ($ptimes as $key => $val) {
9581
                $info[$key] = $ptimes[$key] -  $PERF->startposixtimes[$key];
9582
            }
9583
            $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9584
            $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9585
            $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9586
        }
9587
    }
9588
 
9589
    // Grab the load average for the last minute.
9590
    // /proc will only work under some linux configurations
9591
    // while uptime is there under MacOSX/Darwin and other unices.
9592
    if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9593
        list($serverload) = explode(' ', $loadavg[0]);
9594
        unset($loadavg);
1326 ariadna 9595
    } else if (function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime`) {
1 efrain 9596
        if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9597
            $serverload = $matches[1];
9598
        } else {
9599
            trigger_error('Could not parse uptime output!');
9600
        }
9601
    }
9602
    if (!empty($serverload)) {
9603
        $info['serverload'] = $serverload;
1326 ariadna 9604
        $info['html'] .= '<li class="serverload col-sm-4">Load average: ' . $info['serverload'] . '</li> ';
1 efrain 9605
        $info['txt'] .= "serverload: {$info['serverload']} ";
9606
    }
9607
 
9608
    // Display size of session if session started.
9609
    if ($si = \core\session\manager::get_performance_info()) {
9610
        $info['sessionsize'] = $si['size'];
9611
        $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9612
        $info['txt'] .= $si['txt'];
9613
    }
9614
 
9615
    // Display time waiting for session if applicable.
9616
    if (!empty($PERF->sessionlock['wait'])) {
9617
        $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs';
9618
        $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [
9619
            'class' => 'sessionwait col-sm-4'
9620
        ]);
9621
        $info['txt'] .= 'sessionwait: ' . $sessionwait . ' ';
9622
    }
9623
 
9624
    $info['html'] .= '</ul>';
9625
    $html = '';
9626
    if ($stats = cache_helper::get_stats()) {
9627
 
9628
        $table = new html_table();
9629
        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9630
        $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9631
        $table->data = [];
9632
        $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9633
 
9634
        $text = 'Caches used (hits/misses/sets): ';
9635
        $hits = 0;
9636
        $misses = 0;
9637
        $sets = 0;
9638
        $maxstores = 0;
9639
 
9640
        // We want to align static caches into their own column.
9641
        $hasstatic = false;
9642
        foreach ($stats as $definition => $details) {
9643
            $numstores = count($details['stores']);
9644
            $first = key($details['stores']);
9645
            if ($first !== cache_store::STATIC_ACCEL) {
9646
                $numstores++; // Add a blank space for the missing static store.
9647
            }
9648
            $maxstores = max($maxstores, $numstores);
9649
        }
9650
 
9651
        $storec = 0;
9652
 
9653
        while ($storec++ < ($maxstores - 2)) {
9654
            if ($storec == ($maxstores - 2)) {
9655
                $table->head[] = get_string('mappingfinal', 'cache');
9656
            } else {
9657
                $table->head[] = "Store $storec";
9658
            }
9659
            $table->align[] = 'left';
9660
            $table->align[] = 'right';
9661
            $table->align[] = 'right';
9662
            $table->align[] = 'right';
9663
            $table->align[] = 'right';
9664
            $table->head[] = 'H';
9665
            $table->head[] = 'M';
9666
            $table->head[] = 'S';
9667
            $table->head[] = 'I/O';
9668
        }
9669
 
9670
        ksort($stats);
9671
 
9672
        foreach ($stats as $definition => $details) {
9673
            switch ($details['mode']) {
9674
                case cache_store::MODE_APPLICATION:
9675
                    $modeclass = 'application';
9676
                    $mode = ' <span title="application cache">App</span>';
9677
                    break;
9678
                case cache_store::MODE_SESSION:
9679
                    $modeclass = 'session';
9680
                    $mode = ' <span title="session cache">Ses</span>';
9681
                    break;
9682
                case cache_store::MODE_REQUEST:
9683
                    $modeclass = 'request';
9684
                    $mode = ' <span title="request cache">Req</span>';
9685
                    break;
9686
            }
9687
            $row = [$mode, $definition];
9688
 
9689
            $text .= "$definition {";
9690
 
9691
            $storec = 0;
9692
            foreach ($details['stores'] as $store => $data) {
9693
 
9694
                if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9695
                    $row[] = '';
9696
                    $row[] = '';
9697
                    $row[] = '';
9698
                    $storec++;
9699
                }
9700
 
9701
                $hits   += $data['hits'];
9702
                $misses += $data['misses'];
9703
                $sets   += $data['sets'];
9704
                if ($data['hits'] == 0 and $data['misses'] > 0) {
9705
                    $cachestoreclass = 'nohits bg-danger';
9706
                } else if ($data['hits'] < $data['misses']) {
9707
                    $cachestoreclass = 'lowhits bg-warning text-dark';
9708
                } else {
9709
                    $cachestoreclass = 'hihits';
9710
                }
9711
                $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9712
                $cell = new html_table_cell($store);
9713
                $cell->attributes = ['class' => $cachestoreclass];
9714
                $row[] = $cell;
9715
                $cell = new html_table_cell($data['hits']);
9716
                $cell->attributes = ['class' => $cachestoreclass];
9717
                $row[] = $cell;
9718
                $cell = new html_table_cell($data['misses']);
9719
                $cell->attributes = ['class' => $cachestoreclass];
9720
                $row[] = $cell;
9721
 
9722
                if ($store !== cache_store::STATIC_ACCEL) {
9723
                    // The static cache is never set.
9724
                    $cell = new html_table_cell($data['sets']);
9725
                    $cell->attributes = ['class' => $cachestoreclass];
9726
                    $row[] = $cell;
9727
 
9728
                    if ($data['hits'] || $data['sets']) {
9729
                        if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
9730
                            $size = '-';
9731
                        } else {
9732
                            $size = display_size($data['iobytes'], 1, 'KB');
9733
                            if ($data['iobytes'] >= 10 * 1024) {
9734
                                $cachestoreclass = ' bg-warning text-dark';
9735
                            }
9736
                        }
9737
                    } else {
9738
                        $size = '';
9739
                    }
9740
                    $cell = new html_table_cell($size);
9741
                    $cell->attributes = ['class' => $cachestoreclass];
9742
                    $row[] = $cell;
9743
                }
9744
                $storec++;
9745
            }
9746
            while ($storec++ < $maxstores) {
9747
                $row[] = '';
9748
                $row[] = '';
9749
                $row[] = '';
9750
                $row[] = '';
9751
                $row[] = '';
9752
            }
9753
            $text .= '} ';
9754
 
9755
            $table->data[] = $row;
9756
        }
9757
 
9758
        $html .= html_writer::table($table);
9759
 
9760
        // Now lets also show sub totals for each cache store.
9761
        $storetotals = [];
9762
        $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9763
        foreach ($stats as $definition => $details) {
9764
            foreach ($details['stores'] as $store => $data) {
9765
                if (!array_key_exists($store, $storetotals)) {
9766
                    $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9767
                }
9768
                $storetotals[$store]['class']   = $data['class'];
9769
                $storetotals[$store]['hits']   += $data['hits'];
9770
                $storetotals[$store]['misses'] += $data['misses'];
9771
                $storetotals[$store]['sets']   += $data['sets'];
9772
                $storetotal['hits']   += $data['hits'];
9773
                $storetotal['misses'] += $data['misses'];
9774
                $storetotal['sets']   += $data['sets'];
9775
                if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) {
9776
                    $storetotals[$store]['iobytes'] += $data['iobytes'];
9777
                    $storetotal['iobytes'] += $data['iobytes'];
9778
                }
9779
            }
9780
        }
9781
 
9782
        $table = new html_table();
9783
        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9784
        $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9785
        $table->data = [];
9786
        $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
9787
 
9788
        ksort($storetotals);
9789
 
9790
        foreach ($storetotals as $store => $data) {
9791
            $row = [];
9792
            if ($data['hits'] == 0 and $data['misses'] > 0) {
9793
                $cachestoreclass = 'nohits bg-danger';
9794
            } else if ($data['hits'] < $data['misses']) {
9795
                $cachestoreclass = 'lowhits bg-warning text-dark';
9796
            } else {
9797
                $cachestoreclass = 'hihits';
9798
            }
9799
            $cell = new html_table_cell($store);
9800
            $cell->attributes = ['class' => $cachestoreclass];
9801
            $row[] = $cell;
9802
            $cell = new html_table_cell($data['class']);
9803
            $cell->attributes = ['class' => $cachestoreclass];
9804
            $row[] = $cell;
9805
            $cell = new html_table_cell($data['hits']);
9806
            $cell->attributes = ['class' => $cachestoreclass];
9807
            $row[] = $cell;
9808
            $cell = new html_table_cell($data['misses']);
9809
            $cell->attributes = ['class' => $cachestoreclass];
9810
            $row[] = $cell;
9811
            $cell = new html_table_cell($data['sets']);
9812
            $cell->attributes = ['class' => $cachestoreclass];
9813
            $row[] = $cell;
9814
            if ($data['hits'] || $data['sets']) {
9815
                if ($data['iobytes']) {
9816
                    $size = display_size($data['iobytes'], 1, 'KB');
9817
                } else {
9818
                    $size = '-';
9819
                }
9820
            } else {
9821
                $size = '';
9822
            }
9823
            $cell = new html_table_cell($size);
9824
            $cell->attributes = ['class' => $cachestoreclass];
9825
            $row[] = $cell;
9826
            $table->data[] = $row;
9827
        }
9828
        if (!empty($storetotal['iobytes'])) {
9829
            $size = display_size($storetotal['iobytes'], 1, 'KB');
9830
        } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) {
9831
            $size = '-';
9832
        } else {
9833
            $size = '';
9834
        }
9835
        $row = [
9836
            get_string('total'),
9837
            '',
9838
            $storetotal['hits'],
9839
            $storetotal['misses'],
9840
            $storetotal['sets'],
9841
            $size,
9842
        ];
9843
        $table->data[] = $row;
9844
 
9845
        $html .= html_writer::table($table);
9846
 
9847
        $info['cachesused'] = "$hits / $misses / $sets";
9848
        $info['html'] .= $html;
1326 ariadna 9849
        $info['txt'] .= $text . '. ';
1 efrain 9850
    } else {
9851
        $info['cachesused'] = '0 / 0 / 0';
9852
        $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9853
        $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9854
    }
9855
 
9856
    // Display lock information if any.
9857
    if (!empty($PERF->locks)) {
9858
        $table = new html_table();
9859
        $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered';
9860
        $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)'];
9861
        $table->align = ['left', 'right', 'center', 'right'];
9862
        $table->data = [];
9863
        $text = 'Locks (waited/obtained/held):';
9864
        foreach ($PERF->locks as $locktiming) {
9865
            $row = [];
9866
            $row[] = s($locktiming->type . '/' . $locktiming->resource);
9867
            $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' (';
9868
 
9869
            // The time we had to wait to get the lock.
9870
            $roundedtime = number_format($locktiming->wait, 1);
9871
            $cell = new html_table_cell($roundedtime);
9872
            if ($locktiming->wait > 0.5) {
9873
                $cell->attributes = ['class' => 'bg-warning text-dark'];
9874
            }
9875
            $row[] = $cell;
9876
            $text .= $roundedtime . '/';
9877
 
9878
            // Show a tick or cross for success.
9879
            $row[] = $locktiming->success ? '&#x2713;' : '&#x274c;';
9880
            $text .= ($locktiming->success ? 'y' : 'n') . '/';
9881
 
9882
            // If applicable, show how long we held the lock before releasing it.
9883
            if (property_exists($locktiming, 'held')) {
9884
                $roundedtime = number_format($locktiming->held, 1);
9885
                $cell = new html_table_cell($roundedtime);
9886
                if ($locktiming->held > 0.5) {
9887
                    $cell->attributes = ['class' => 'bg-warning text-dark'];
9888
                }
9889
                $row[] = $cell;
9890
                $text .= $roundedtime;
9891
            } else {
9892
                $row[] = '-';
9893
                $text .= '-';
9894
            }
9895
            $text .= ')';
9896
 
9897
            $table->data[] = $row;
9898
        }
9899
        $info['html'] .= html_writer::table($table);
9900
        $info['txt'] .= $text . '. ';
9901
    }
9902
 
1326 ariadna 9903
    $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">' . $info['html'] . '</div>';
1 efrain 9904
    return $info;
9905
}
9906
 
9907
/**
9908
 * Renames a file or directory to a unique name within the same directory.
9909
 *
9910
 * This function is designed to avoid any potential race conditions, and select an unused name.
9911
 *
9912
 * @param string $filepath Original filepath
9913
 * @param string $prefix Prefix to use for the temporary name
9914
 * @return string|bool New file path or false if failed
9915
 * @since Moodle 3.10
9916
 */
1326 ariadna 9917
function rename_to_unused_name(string $filepath, string $prefix = '_temp_')
9918
{
1 efrain 9919
    $dir = dirname($filepath);
9920
    $basename = $dir . '/' . $prefix;
9921
    $limit = 0;
9922
    while ($limit < 100) {
9923
        // Select a new name based on a random number.
9924
        $newfilepath = $basename . md5(mt_rand());
9925
 
9926
        // Attempt a rename to that new name.
9927
        if (@rename($filepath, $newfilepath)) {
9928
            return $newfilepath;
9929
        }
9930
 
9931
        // The first time, do some sanity checks, maybe it is failing for a good reason and there
9932
        // is no point trying 100 times if so.
9933
        if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
9934
            return false;
9935
        }
9936
        $limit++;
9937
    }
9938
    return false;
9939
}
9940
 
9941
/**
9942
 * Delete directory or only its content
9943
 *
9944
 * @param string $dir directory path
9945
 * @param bool $contentonly
9946
 * @return bool success, true also if dir does not exist
9947
 */
1326 ariadna 9948
function remove_dir($dir, $contentonly = false)
9949
{
1 efrain 9950
    if (!is_dir($dir)) {
9951
        // Nothing to do.
9952
        return true;
9953
    }
9954
 
9955
    if (!$contentonly) {
9956
        // Start by renaming the directory; this will guarantee that other processes don't write to it
9957
        // while it is in the process of being deleted.
9958
        $tempdir = rename_to_unused_name($dir);
9959
        if ($tempdir) {
9960
            // If the rename was successful then delete the $tempdir instead.
9961
            $dir = $tempdir;
9962
        }
9963
        // If the rename fails, we will continue through and attempt to delete the directory
9964
        // without renaming it since that is likely to at least delete most of the files.
9965
    }
9966
 
9967
    if (!$handle = opendir($dir)) {
9968
        return false;
9969
    }
9970
    $result = true;
1326 ariadna 9971
    while (false !== ($item = readdir($handle))) {
1 efrain 9972
        if ($item != '.' && $item != '..') {
1326 ariadna 9973
            if (is_dir($dir . '/' . $item)) {
9974
                $result = remove_dir($dir . '/' . $item) && $result;
1 efrain 9975
            } else {
1326 ariadna 9976
                $result = unlink($dir . '/' . $item) && $result;
1 efrain 9977
            }
9978
        }
9979
    }
9980
    closedir($handle);
9981
    if ($contentonly) {
9982
        clearstatcache(); // Make sure file stat cache is properly invalidated.
9983
        return $result;
9984
    }
9985
    $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9986
    clearstatcache(); // Make sure file stat cache is properly invalidated.
9987
    return $result;
9988
}
9989
 
9990
/**
9991
 * Detect if an object or a class contains a given property
9992
 * will take an actual object or the name of a class
9993
 *
9994
 * @param mixed $obj Name of class or real object to test
9995
 * @param string $property name of property to find
9996
 * @return bool true if property exists
9997
 */
1326 ariadna 9998
function object_property_exists($obj, $property)
9999
{
10000
    if (is_string($obj)) {
10001
        $properties = get_class_vars($obj);
1 efrain 10002
    } else {
1326 ariadna 10003
        $properties = get_object_vars($obj);
1 efrain 10004
    }
1326 ariadna 10005
    return array_key_exists($property, $properties);
1 efrain 10006
}
10007
 
10008
/**
10009
 * Converts an object into an associative array
10010
 *
10011
 * This function converts an object into an associative array by iterating
10012
 * over its public properties. Because this function uses the foreach
10013
 * construct, Iterators are respected. It works recursively on arrays of objects.
10014
 * Arrays and simple values are returned as is.
10015
 *
10016
 * If class has magic properties, it can implement IteratorAggregate
10017
 * and return all available properties in getIterator()
10018
 *
10019
 * @param mixed $var
10020
 * @return array
10021
 */
1326 ariadna 10022
function convert_to_array($var)
10023
{
1 efrain 10024
    $result = array();
10025
 
10026
    // Loop over elements/properties.
10027
    foreach ($var as $key => $value) {
10028
        // Recursively convert objects.
10029
        if (is_object($value) || is_array($value)) {
10030
            $result[$key] = convert_to_array($value);
10031
        } else {
10032
            // Simple values are untouched.
10033
            $result[$key] = $value;
10034
        }
10035
    }
10036
    return $result;
10037
}
10038
 
10039
/**
10040
 * Detect a custom script replacement in the data directory that will
10041
 * replace an existing moodle script
10042
 *
10043
 * @return string|bool full path name if a custom script exists, false if no custom script exists
10044
 */
1326 ariadna 10045
function custom_script_path()
10046
{
1 efrain 10047
    global $CFG, $SCRIPT;
10048
 
10049
    if ($SCRIPT === null) {
10050
        // Probably some weird external script.
10051
        return false;
10052
    }
10053
 
10054
    $scriptpath = $CFG->customscripts . $SCRIPT;
10055
 
10056
    // Check the custom script exists.
10057
    if (file_exists($scriptpath) and is_file($scriptpath)) {
10058
        return $scriptpath;
10059
    } else {
10060
        return false;
10061
    }
10062
}
10063
 
10064
/**
10065
 * Returns whether or not the user object is a remote MNET user. This function
10066
 * is in moodlelib because it does not rely on loading any of the MNET code.
10067
 *
10068
 * @param object $user A valid user object
10069
 * @return bool        True if the user is from a remote Moodle.
10070
 */
1326 ariadna 10071
function is_mnet_remote_user($user)
10072
{
1 efrain 10073
    global $CFG;
10074
 
10075
    if (!isset($CFG->mnet_localhost_id)) {
10076
        include_once($CFG->dirroot . '/mnet/lib.php');
10077
        $env = new mnet_environment();
10078
        $env->init();
10079
        unset($env);
10080
    }
10081
 
10082
    return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10083
}
10084
 
10085
/**
10086
 * This function will search for browser prefereed languages, setting Moodle
10087
 * to use the best one available if $SESSION->lang is undefined
10088
 */
1326 ariadna 10089
function setup_lang_from_browser()
10090
{
1 efrain 10091
    global $CFG, $SESSION, $USER;
10092
 
10093
    if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10094
        // Lang is defined in session or user profile, nothing to do.
10095
        return;
10096
    }
10097
 
10098
    if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
10099
        return;
10100
    }
10101
 
10102
    // Extract and clean langs from headers.
10103
    $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10104
    $rawlangs = str_replace('-', '_', $rawlangs);         // We are using underscores.
10105
    $rawlangs = explode(',', $rawlangs);                  // Convert to array.
10106
    $langs = array();
10107
 
10108
    $order = 1.0;
10109
    foreach ($rawlangs as $lang) {
10110
        if (strpos($lang, ';') === false) {
10111
            $langs[(string)$order] = $lang;
1326 ariadna 10112
            $order = $order - 0.01;
1 efrain 10113
        } else {
10114
            $parts = explode(';', $lang);
10115
            $pos = strpos($parts[1], '=');
1326 ariadna 10116
            $langs[substr($parts[1], $pos + 1)] = $parts[0];
1 efrain 10117
        }
10118
    }
10119
    krsort($langs, SORT_NUMERIC);
10120
 
10121
    // Look for such langs under standard locations.
10122
    foreach ($langs as $lang) {
10123
        // Clean it properly for include.
10124
        $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
10125
        if (get_string_manager()->translation_exists($lang, false)) {
10126
            // If the translation for this language exists then try to set it
10127
            // for the rest of the session, if this is a read only session then
10128
            // we can only set it temporarily in $CFG.
10129
            if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) {
10130
                $CFG->lang = $lang;
10131
            } else {
10132
                $SESSION->lang = $lang;
10133
            }
10134
            // We have finished. Go out.
10135
            break;
10136
        }
10137
    }
10138
    return;
10139
}
10140
 
10141
/**
10142
 * Check if $url matches anything in proxybypass list
10143
 *
10144
 * Any errors just result in the proxy being used (least bad)
10145
 *
10146
 * @param string $url url to check
10147
 * @return boolean true if we should bypass the proxy
10148
 */
1326 ariadna 10149
function is_proxybypass($url)
10150
{
1 efrain 10151
    global $CFG;
10152
 
10153
    // Sanity check.
10154
    if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10155
        return false;
10156
    }
10157
 
10158
    // Get the host part out of the url.
1326 ariadna 10159
    if (!$host = parse_url($url, PHP_URL_HOST)) {
1 efrain 10160
        return false;
10161
    }
10162
 
10163
    // Get the possible bypass hosts into an array.
1326 ariadna 10164
    $matches = explode(',', $CFG->proxybypass);
1 efrain 10165
 
10166
    // Check for a exact match on the IP or in the domains.
10167
    $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches);
10168
    $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ',');
10169
 
10170
    if ($isdomaininallowedlist || $isipinsubnetlist) {
10171
        return true;
10172
    }
10173
 
10174
    // Nothing matched.
10175
    return false;
10176
}
10177
 
10178
/**
10179
 * Check if the passed navigation is of the new style
10180
 *
10181
 * @param mixed $navigation
10182
 * @return bool true for yes false for no
10183
 */
1326 ariadna 10184
function is_newnav($navigation)
10185
{
1 efrain 10186
    if (is_array($navigation) && !empty($navigation['newnav'])) {
10187
        return true;
10188
    } else {
10189
        return false;
10190
    }
10191
}
10192
 
10193
/**
10194
 * Checks whether the given variable name is defined as a variable within the given object.
10195
 *
10196
 * This will NOT work with stdClass objects, which have no class variables.
10197
 *
10198
 * @param string $var The variable name
10199
 * @param object $object The object to check
10200
 * @return boolean
10201
 */
1326 ariadna 10202
function in_object_vars($var, $object)
10203
{
1 efrain 10204
    $classvars = get_class_vars(get_class($object));
10205
    $classvars = array_keys($classvars);
10206
    return in_array($var, $classvars);
10207
}
10208
 
10209
/**
10210
 * Returns an array without repeated objects.
10211
 * This function is similar to array_unique, but for arrays that have objects as values
10212
 *
10213
 * @param array $array
10214
 * @param bool $keepkeyassoc
10215
 * @return array
10216
 */
1326 ariadna 10217
function object_array_unique($array, $keepkeyassoc = true)
10218
{
1 efrain 10219
    $duplicatekeys = array();
10220
    $tmp         = array();
10221
 
10222
    foreach ($array as $key => $val) {
10223
        // Convert objects to arrays, in_array() does not support objects.
10224
        if (is_object($val)) {
10225
            $val = (array)$val;
10226
        }
10227
 
10228
        if (!in_array($val, $tmp)) {
10229
            $tmp[] = $val;
10230
        } else {
10231
            $duplicatekeys[] = $key;
10232
        }
10233
    }
10234
 
10235
    foreach ($duplicatekeys as $key) {
10236
        unset($array[$key]);
10237
    }
10238
 
10239
    return $keepkeyassoc ? $array : array_values($array);
10240
}
10241
 
10242
/**
10243
 * Is a userid the primary administrator?
10244
 *
10245
 * @param int $userid int id of user to check
10246
 * @return boolean
10247
 */
1326 ariadna 10248
function is_primary_admin($userid)
10249
{
1 efrain 10250
    $primaryadmin =  get_admin();
10251
 
10252
    if ($userid == $primaryadmin->id) {
10253
        return true;
10254
    } else {
10255
        return false;
10256
    }
10257
}
10258
 
10259
/**
10260
 * Returns the site identifier
10261
 *
10262
 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10263
 */
1326 ariadna 10264
function get_site_identifier()
10265
{
1 efrain 10266
    global $CFG;
10267
    // Check to see if it is missing. If so, initialise it.
10268
    if (empty($CFG->siteidentifier)) {
10269
        set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10270
    }
10271
    // Return it.
10272
    return $CFG->siteidentifier;
10273
}
10274
 
10275
/**
10276
 * Check whether the given password has no more than the specified
10277
 * number of consecutive identical characters.
10278
 *
10279
 * @param string $password   password to be checked against the password policy
10280
 * @param integer $maxchars  maximum number of consecutive identical characters
10281
 * @return bool
10282
 */
1326 ariadna 10283
function check_consecutive_identical_characters($password, $maxchars)
10284
{
1 efrain 10285
 
10286
    if ($maxchars < 1) {
10287
        return true; // Zero 0 is to disable this check.
10288
    }
10289
    if (strlen($password) <= $maxchars) {
10290
        return true; // Too short to fail this test.
10291
    }
10292
 
10293
    $previouschar = '';
10294
    $consecutivecount = 1;
10295
    foreach (str_split($password) as $char) {
10296
        if ($char != $previouschar) {
10297
            $consecutivecount = 1;
10298
        } else {
10299
            $consecutivecount++;
10300
            if ($consecutivecount > $maxchars) {
10301
                return false; // Check failed already.
10302
            }
10303
        }
10304
 
10305
        $previouschar = $char;
10306
    }
10307
 
10308
    return true;
10309
}
10310
 
10311
/**
10312
 * Helper function to do partial function binding.
10313
 * so we can use it for preg_replace_callback, for example
10314
 * this works with php functions, user functions, static methods and class methods
10315
 * it returns you a callback that you can pass on like so:
10316
 *
10317
 * $callback = partial('somefunction', $arg1, $arg2);
10318
 *     or
10319
 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10320
 *     or even
10321
 * $obj = new someclass();
10322
 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10323
 *
10324
 * and then the arguments that are passed through at calltime are appended to the argument list.
10325
 *
10326
 * @param mixed $function a php callback
10327
 * @param mixed $arg1,... $argv arguments to partially bind with
10328
 * @return array Array callback
10329
 */
1326 ariadna 10330
function partial()
10331
{
1 efrain 10332
    if (!class_exists('partial')) {
10333
        /**
10334
         * Used to manage function binding.
10335
         * @copyright  2009 Penny Leach
10336
         * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10337
         */
1326 ariadna 10338
        class partial
10339
        {
1 efrain 10340
            /** @var array */
10341
            public $values = array();
10342
            /** @var string The function to call as a callback. */
10343
            public $func;
10344
            /**
10345
             * Constructor
10346
             * @param string $func
10347
             * @param array $args
10348
             */
1326 ariadna 10349
            public function __construct($func, $args)
10350
            {
1 efrain 10351
                $this->values = $args;
10352
                $this->func = $func;
10353
            }
10354
            /**
10355
             * Calls the callback function.
10356
             * @return mixed
10357
             */
1326 ariadna 10358
            public function method()
10359
            {
1 efrain 10360
                $args = func_get_args();
10361
                return call_user_func_array($this->func, array_merge($this->values, $args));
10362
            }
10363
        }
10364
    }
10365
    $args = func_get_args();
10366
    $func = array_shift($args);
10367
    $p = new partial($func, $args);
10368
    return array($p, 'method');
10369
}
10370
 
10371
/**
10372
 * helper function to load up and initialise the mnet environment
10373
 * this must be called before you use mnet functions.
10374
 *
10375
 * @return mnet_environment the equivalent of old $MNET global
10376
 */
1326 ariadna 10377
function get_mnet_environment()
10378
{
1 efrain 10379
    global $CFG;
10380
    require_once($CFG->dirroot . '/mnet/lib.php');
10381
    static $instance = null;
10382
    if (empty($instance)) {
10383
        $instance = new mnet_environment();
10384
        $instance->init();
10385
    }
10386
    return $instance;
10387
}
10388
 
10389
/**
10390
 * during xmlrpc server code execution, any code wishing to access
10391
 * information about the remote peer must use this to get it.
10392
 *
10393
 * @return mnet_remote_client|false the equivalent of old $MNETREMOTE_CLIENT global
10394
 */
1326 ariadna 10395
function get_mnet_remote_client()
10396
{
1 efrain 10397
    if (!defined('MNET_SERVER')) {
10398
        debugging(get_string('notinxmlrpcserver', 'mnet'));
10399
        return false;
10400
    }
10401
    global $MNET_REMOTE_CLIENT;
10402
    if (isset($MNET_REMOTE_CLIENT)) {
10403
        return $MNET_REMOTE_CLIENT;
10404
    }
10405
    return false;
10406
}
10407
 
10408
/**
10409
 * during the xmlrpc server code execution, this will be called
10410
 * to setup the object returned by {@link get_mnet_remote_client}
10411
 *
10412
 * @param mnet_remote_client $client the client to set up
10413
 * @throws moodle_exception
10414
 */
1326 ariadna 10415
function set_mnet_remote_client($client)
10416
{
1 efrain 10417
    if (!defined('MNET_SERVER')) {
10418
        throw new moodle_exception('notinxmlrpcserver', 'mnet');
10419
    }
10420
    global $MNET_REMOTE_CLIENT;
10421
    $MNET_REMOTE_CLIENT = $client;
10422
}
10423
 
10424
/**
10425
 * return the jump url for a given remote user
10426
 * this is used for rewriting forum post links in emails, etc
10427
 *
10428
 * @param stdclass $user the user to get the idp url for
10429
 */
1326 ariadna 10430
function mnet_get_idp_jump_url($user)
10431
{
1 efrain 10432
    global $CFG;
10433
 
10434
    static $mnetjumps = array();
10435
    if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10436
        $idp = mnet_get_peer_host($user->mnethostid);
10437
        $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10438
        $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10439
    }
10440
    return $mnetjumps[$user->mnethostid];
10441
}
10442
 
10443
/**
10444
 * Gets the homepage to use for the current user
10445
 *
10446
 * @return int One of HOMEPAGE_*
10447
 */
1326 ariadna 10448
function get_home_page()
10449
{
1 efrain 10450
    global $CFG;
10451
 
10452
    if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10453
        // If dashboard is disabled, home will be set to default page.
10454
        $defaultpage = get_default_home_page();
10455
        if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10456
            if (!empty($CFG->enabledashboard)) {
10457
                return HOMEPAGE_MY;
10458
            } else {
10459
                return $defaultpage;
10460
            }
10461
        } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) {
10462
            return HOMEPAGE_MYCOURSES;
10463
        } else {
10464
            $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage);
10465
            if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) {
10466
                // If the user was using the dashboard but it's disabled, return the default home page.
10467
                $userhomepage = $defaultpage;
10468
            }
10469
            return $userhomepage;
10470
        }
10471
    }
10472
    return HOMEPAGE_SITE;
10473
}
10474
 
10475
/**
10476
 * Returns the default home page to display if current one is not defined or can't be applied.
10477
 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
10478
 *
10479
 * @return int The default home page.
10480
 */
1326 ariadna 10481
function get_default_home_page(): int
10482
{
1 efrain 10483
    global $CFG;
10484
 
10485
    return (!isset($CFG->enabledashboard) || $CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES;
10486
}
10487
 
10488
/**
10489
 * Gets the name of a course to be displayed when showing a list of courses.
10490
 * By default this is just $course->fullname but user can configure it. The
10491
 * result of this function should be passed through print_string.
10492
 * @param stdClass|core_course_list_element $course Moodle course object
10493
 * @return string Display name of course (either fullname or short + fullname)
10494
 */
1326 ariadna 10495
function get_course_display_name_for_list($course)
10496
{
1 efrain 10497
    global $CFG;
10498
    if (!empty($CFG->courselistshortnames)) {
10499
        if (!($course instanceof stdClass)) {
10500
            $course = (object)convert_to_array($course);
10501
        }
10502
        return get_string('courseextendednamedisplay', '', $course);
10503
    } else {
10504
        return $course->fullname;
10505
    }
10506
}
10507
 
10508
/**
10509
 * Safe analogue of unserialize() that can only parse arrays
10510
 *
10511
 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10512
 *
10513
 * @param string $expression
10514
 * @return array|bool either parsed array or false if parsing was impossible.
10515
 */
1326 ariadna 10516
function unserialize_array($expression)
10517
{
1 efrain 10518
 
10519
    // Check the expression is an array.
10520
    if (!preg_match('/^a:(\d+):/', $expression)) {
10521
        return false;
10522
    }
10523
 
10524
    $values = (array) unserialize_object($expression);
10525
 
10526
    // Callback that returns true if the given value is an unserialized object, executes recursively.
1326 ariadna 10527
    $invalidvaluecallback = static function ($value) use (&$invalidvaluecallback): bool {
1 efrain 10528
        if (is_array($value)) {
10529
            return (bool) array_filter($value, $invalidvaluecallback);
10530
        }
10531
        return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class);
10532
    };
10533
 
10534
    // Iterate over the result to ensure there are no stray objects.
10535
    if (array_filter($values, $invalidvaluecallback)) {
10536
        return false;
10537
    }
10538
 
10539
    return $values;
10540
}
10541
 
10542
/**
10543
 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10544
 *
10545
 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10546
 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10547
 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10548
 *
10549
 * @param string $input
10550
 * @return stdClass
10551
 */
1326 ariadna 10552
function unserialize_object(string $input): stdClass
10553
{
1 efrain 10554
    $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10555
    return (object) $instance;
10556
}
10557
 
10558
/**
10559
 * The lang_string class
10560
 *
10561
 * This special class is used to create an object representation of a string request.
10562
 * It is special because processing doesn't occur until the object is first used.
10563
 * The class was created especially to aid performance in areas where strings were
10564
 * required to be generated but were not necessarily used.
10565
 * As an example the admin tree when generated uses over 1500 strings, of which
10566
 * normally only 1/3 are ever actually printed at any time.
10567
 * The performance advantage is achieved by not actually processing strings that
10568
 * arn't being used, as such reducing the processing required for the page.
10569
 *
10570
 * How to use the lang_string class?
10571
 *     There are two methods of using the lang_string class, first through the
10572
 *     forth argument of the get_string function, and secondly directly.
10573
 *     The following are examples of both.
10574
 * 1. Through get_string calls e.g.
10575
 *     $string = get_string($identifier, $component, $a, true);
10576
 *     $string = get_string('yes', 'moodle', null, true);
10577
 * 2. Direct instantiation
10578
 *     $string = new lang_string($identifier, $component, $a, $lang);
10579
 *     $string = new lang_string('yes');
10580
 *
10581
 * How do I use a lang_string object?
10582
 *     The lang_string object makes use of a magic __toString method so that you
10583
 *     are able to use the object exactly as you would use a string in most cases.
10584
 *     This means you are able to collect it into a variable and then directly
10585
 *     echo it, or concatenate it into another string, or similar.
10586
 *     The other thing you can do is manually get the string by calling the
10587
 *     lang_strings out method e.g.
10588
 *         $string = new lang_string('yes');
10589
 *         $string->out();
10590
 *     Also worth noting is that the out method can take one argument, $lang which
10591
 *     allows the developer to change the language on the fly.
10592
 *
10593
 * When should I use a lang_string object?
10594
 *     The lang_string object is designed to be used in any situation where a
10595
 *     string may not be needed, but needs to be generated.
10596
 *     The admin tree is a good example of where lang_string objects should be
10597
 *     used.
10598
 *     A more practical example would be any class that requries strings that may
10599
 *     not be printed (after all classes get renderer by renderers and who knows
10600
 *     what they will do ;))
10601
 *
10602
 * When should I not use a lang_string object?
10603
 *     Don't use lang_strings when you are going to use a string immediately.
10604
 *     There is no need as it will be processed immediately and there will be no
10605
 *     advantage, and in fact perhaps a negative hit as a class has to be
10606
 *     instantiated for a lang_string object, however get_string won't require
10607
 *     that.
10608
 *
10609
 * Limitations:
10610
 * 1. You cannot use a lang_string object as an array offset. Doing so will
10611
 *     result in PHP throwing an error. (You can use it as an object property!)
10612
 *
10613
 * @package    core
10614
 * @category   string
10615
 * @copyright  2011 Sam Hemelryk
10616
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10617
 */
1326 ariadna 10618
class lang_string
10619
{
1 efrain 10620
 
10621
    /** @var string The strings identifier */
10622
    protected $identifier;
10623
    /** @var string The strings component. Default '' */
10624
    protected $component = '';
10625
    /** @var array|stdClass Any arguments required for the string. Default null */
10626
    protected $a = null;
10627
    /** @var string The language to use when processing the string. Default null */
10628
    protected $lang = null;
10629
 
10630
    /** @var string The processed string (once processed) */
10631
    protected $string = null;
10632
 
10633
    /**
10634
     * A special boolean. If set to true then the object has been woken up and
10635
     * cannot be regenerated. If this is set then $this->string MUST be used.
10636
     * @var bool
10637
     */
10638
    protected $forcedstring = false;
10639
 
10640
    /**
10641
     * Constructs a lang_string object
10642
     *
10643
     * This function should do as little processing as possible to ensure the best
10644
     * performance for strings that won't be used.
10645
     *
10646
     * @param string $identifier The strings identifier
10647
     * @param string $component The strings component
10648
     * @param stdClass|array|mixed $a Any arguments the string requires
10649
     * @param string $lang The language to use when processing the string.
10650
     * @throws coding_exception
10651
     */
1326 ariadna 10652
    public function __construct($identifier, $component = '', $a = null, $lang = null)
10653
    {
1 efrain 10654
        if (empty($component)) {
10655
            $component = 'moodle';
10656
        }
10657
 
10658
        $this->identifier = $identifier;
10659
        $this->component = $component;
10660
        $this->lang = $lang;
10661
 
10662
        // We MUST duplicate $a to ensure that it if it changes by reference those
10663
        // changes are not carried across.
10664
        // To do this we always ensure $a or its properties/values are strings
10665
        // and that any properties/values that arn't convertable are forgotten.
10666
        if ($a !== null) {
10667
            if (is_scalar($a)) {
10668
                $this->a = $a;
10669
            } else if ($a instanceof lang_string) {
10670
                $this->a = $a->out();
10671
            } else if (is_object($a) or is_array($a)) {
10672
                $a = (array)$a;
10673
                $this->a = array();
10674
                foreach ($a as $key => $value) {
10675
                    // Make sure conversion errors don't get displayed (results in '').
10676
                    if (is_array($value)) {
10677
                        $this->a[$key] = '';
10678
                    } else if (is_object($value)) {
10679
                        if (method_exists($value, '__toString')) {
10680
                            $this->a[$key] = $value->__toString();
10681
                        } else {
10682
                            $this->a[$key] = '';
10683
                        }
10684
                    } else {
10685
                        $this->a[$key] = (string)$value;
10686
                    }
10687
                }
10688
            }
10689
        }
10690
 
10691
        if (debugging(false, DEBUG_DEVELOPER)) {
10692
            if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10693
                throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10694
            }
10695
            if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10696
                throw new coding_exception('Invalid string compontent. Please check your string definition');
10697
            }
10698
            if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
1326 ariadna 10699
                debugging('String does not exist. Please check your string definition for ' . $this->identifier . '/' . $this->component, DEBUG_DEVELOPER);
1 efrain 10700
            }
10701
        }
10702
    }
10703
 
10704
    /**
10705
     * Processes the string.
10706
     *
10707
     * This function actually processes the string, stores it in the string property
10708
     * and then returns it.
10709
     * You will notice that this function is VERY similar to the get_string method.
10710
     * That is because it is pretty much doing the same thing.
10711
     * However as this function is an upgrade it isn't as tolerant to backwards
10712
     * compatibility.
10713
     *
10714
     * @return string
10715
     * @throws coding_exception
10716
     */
1326 ariadna 10717
    protected function get_string()
10718
    {
1 efrain 10719
        global $CFG;
10720
 
10721
        // Check if we need to process the string.
10722
        if ($this->string === null) {
10723
            // Check the quality of the identifier.
10724
            if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10725
                throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition', DEBUG_DEVELOPER);
10726
            }
10727
 
10728
            // Process the string.
10729
            $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10730
            // Debugging feature lets you display string identifier and component.
10731
            if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10732
                $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10733
            }
10734
        }
10735
        // Return the string.
10736
        return $this->string;
10737
    }
10738
 
10739
    /**
10740
     * Returns the string
10741
     *
10742
     * @param string $lang The langauge to use when processing the string
10743
     * @return string
10744
     */
1326 ariadna 10745
    public function out($lang = null)
10746
    {
1 efrain 10747
        if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10748
            if ($this->forcedstring) {
1326 ariadna 10749
                debugging('lang_string objects that have been used cannot be printed in another language. (' . $this->lang . ' used)', DEBUG_DEVELOPER);
1 efrain 10750
                return $this->get_string();
10751
            }
10752
            $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10753
            return $translatedstring->out();
10754
        }
10755
        return $this->get_string();
10756
    }
10757
 
10758
    /**
10759
     * Magic __toString method for printing a string
10760
     *
10761
     * @return string
10762
     */
1326 ariadna 10763
    public function __toString()
10764
    {
1 efrain 10765
        return $this->get_string();
10766
    }
10767
 
10768
    /**
10769
     * Magic __set_state method used for var_export
10770
     *
10771
     * @param array $array
10772
     * @return self
10773
     */
1326 ariadna 10774
    public static function __set_state(array $array): self
10775
    {
1 efrain 10776
        $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']);
10777
        $tmp->string = $array['string'];
10778
        $tmp->forcedstring = $array['forcedstring'];
10779
        return $tmp;
10780
    }
10781
 
10782
    /**
10783
     * Prepares the lang_string for sleep and stores only the forcedstring and
10784
     * string properties... the string cannot be regenerated so we need to ensure
10785
     * it is generated for this.
10786
     *
10787
     * @return array
10788
     */
1326 ariadna 10789
    public function __sleep()
10790
    {
1 efrain 10791
        $this->get_string();
10792
        $this->forcedstring = true;
10793
        return array('forcedstring', 'string', 'lang');
10794
    }
10795
 
10796
    /**
10797
     * Returns the identifier.
10798
     *
10799
     * @return string
10800
     */
1326 ariadna 10801
    public function get_identifier()
10802
    {
1 efrain 10803
        return $this->identifier;
10804
    }
10805
 
10806
    /**
10807
     * Returns the component.
10808
     *
10809
     * @return string
10810
     */
1326 ariadna 10811
    public function get_component()
10812
    {
1 efrain 10813
        return $this->component;
10814
    }
10815
}
10816
 
10817
/**
10818
 * Get human readable name describing the given callable.
10819
 *
10820
 * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10821
 * It does not check if the callable actually exists.
10822
 *
10823
 * @param callable|string|array $callable
10824
 * @return string|bool Human readable name of callable, or false if not a valid callable.
10825
 */
1326 ariadna 10826
function get_callable_name($callable)
10827
{
1 efrain 10828
 
10829
    if (!is_callable($callable, true, $name)) {
10830
        return false;
10831
    } else {
10832
        return $name;
10833
    }
10834
}
10835
 
10836
/**
10837
 * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10838
 * Never put your faith on this function and rely on its accuracy as there might be false positives.
10839
 * It just performs some simple checks, and mainly is used for places where we want to hide some options
10840
 * such as site registration when $CFG->wwwroot is not publicly accessible.
10841
 * Good thing is there is no false negative.
10842
 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10843
 *
10844
 * @return bool
10845
 */
1326 ariadna 10846
function site_is_public()
10847
{
1 efrain 10848
    global $CFG;
10849
 
10850
    // Return early if site admin has forced this setting.
10851
    if (isset($CFG->site_is_public)) {
10852
        return (bool)$CFG->site_is_public;
10853
    }
10854
 
10855
    $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10856
 
10857
    if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10858
        $ispublic = false;
10859
    } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10860
        $ispublic = false;
10861
    } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10862
        $ispublic = false;
10863
    } else {
10864
        $ispublic = true;
10865
    }
10866
 
10867
    return $ispublic;
10868
}
10869
 
10870
/**
10871
 * Validates user's password length.
10872
 *
10873
 * @param string $password
10874
 * @param int $pepperlength The length of the used peppers
10875
 * @return bool
10876
 */
1326 ariadna 10877
function exceeds_password_length(string $password, int $pepperlength = 0): bool
10878
{
1 efrain 10879
    return (strlen($password) > (MAX_PASSWORD_CHARACTERS + $pepperlength));
10880
}
10881
 
10882
/**
10883
 * A helper to replace PHP 8.3 usage of array_keys with two args.
10884
 *
10885
 * There is an indication that this will become a new method in PHP 8.4, but that has not happened yet.
10886
 * Therefore this non-polyfill has been created with a different naming convention.
10887
 * In the future it can be deprecated if a core PHP method is created.
10888
 *
10889
 * https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#array_keys
10890
 *
10891
 * @param array $array
10892
 * @param mixed $filter The value to filter on
10893
 * @param bool $strict Whether to apply a strit test with the filter
10894
 * @return array
10895
 */
1326 ariadna 10896
function moodle_array_keys_filter(array $array, mixed $filter, bool $strict = false): array
10897
{
1 efrain 10898
    return array_keys(array_filter(
10899
        $array,
1326 ariadna 10900
        function ($value, $key) use ($filter, $strict): bool {
1 efrain 10901
            if ($strict) {
10902
                return $value === $filter;
10903
            }
10904
            return $value == $filter;
10905
        },
10906
        ARRAY_FILTER_USE_BOTH,
10907
    ));
10908
}