Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 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
namespace core\output\requirements;
18
 
19
use core_component;
20
use core\context\course as context_course;
21
use core\exception\coding_exception;
22
use core\output\core_renderer;
23
use core\output\js_writer;
24
use core\output\html_writer;
25
use core\output\renderer_base;
26
use lang_string;
27
use moodle_page;
28
use moodle_url;
29
use stdClass;
30
 
31
/**
32
 * This class tracks all the things that are needed by the current page.
33
 *
34
 * Normally, the only instance of this  class you will need to work with is the
35
 * one accessible via $PAGE->requires.
36
 *
37
 * Typical usage would be
38
 * <pre>
39
 *     $PAGE->requires->js_call_amd('mod_forum/view', 'init');
40
 * </pre>
41
 *
42
 * It also supports obsoleted coding style with/without YUI3 modules.
43
 * <pre>
44
 *     $PAGE->requires->js_init_call('M.mod_forum.init_view');
45
 *     $PAGE->requires->css('/mod/mymod/userstyles.php?id='.$id); // not overridable via themes!
46
 *     $PAGE->requires->js('/mod/mymod/script.js');
47
 *     $PAGE->requires->js('/mod/mymod/small_but_urgent.js', true);
48
 *     $PAGE->requires->js_function_call('init_mymod', array($data), true);
49
 * </pre>
50
 *
51
 * There are some natural restrictions on some methods. For example, {@see css()}
52
 * can only be called before the <head> tag is output. See the comments on the
53
 * individual methods for details.
54
 *
55
 * @copyright 2009 Tim Hunt, 2010 Petr Skoda
56
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
57
 * @since Moodle 2.0
58
 * @package core
59
 * @category output
60
 */
61
class page_requirements_manager {
62
    /**
63
     * @var array List of string available from JS
64
     */
65
    protected $stringsforjs = [];
66
 
67
    /**
68
     * @var array List of get_string $a parameters - used for validation only.
69
     */
70
    protected $stringsforjs_as = []; // phpcs:ignore moodle.NamingConventions.ValidVariableName.MemberNameUnderscore
71
 
72
    /**
73
     * @var array List of JS variables to be initialised
74
     */
75
    protected $jsinitvariables = ['head' => [], 'footer' => []];
76
 
77
    /**
78
     * @var array Included JS scripts
79
     */
80
    protected $jsincludes = ['head' => [], 'footer' => []];
81
 
82
    /**
83
     * @var array Inline scripts using RequireJS module loading.
84
     */
85
    protected $amdjscode = [''];
86
 
87
    /**
88
     * @var array List of needed function calls
89
     */
90
    protected $jscalls = ['normal' => [], 'ondomready' => []];
91
 
92
    /**
93
     * @var array List of skip links, those are needed for accessibility reasons
94
     */
95
    protected $skiplinks = [];
96
 
97
    /**
98
     * @var array Javascript code used for initialisation of page, it should
99
     * be relatively small
100
     */
101
    protected $jsinitcode = [];
102
 
103
    /**
104
     * @var array of moodle_url Theme sheets, initialised only from core_renderer
105
     */
106
    protected $cssthemeurls = [];
107
 
108
    /**
109
     * @var array of moodle_url List of custom theme sheets, these are strongly discouraged!
110
     * Useful mostly only for CSS submitted by teachers that is not part of the theme.
111
     */
112
    protected $cssurls = [];
113
 
114
    /**
115
     * @var array List of requested event handlers
116
     */
117
    protected $eventhandlers = [];
118
 
119
    /**
120
     * @var array Extra modules
121
     */
122
    protected $extramodules = [];
123
 
124
    /**
125
     * @var array trackes the names of bits of HTML that are only required once
126
     * per page. See {@see has_one_time_item_been_created()},
127
     * {@see set_one_time_item_created()} and {@see should_create_one_time_item_now()}.
128
     */
129
    protected $onetimeitemsoutput = [];
130
 
131
    /**
132
     * @var bool Flag indicated head stuff already printed
133
     */
134
    protected $headdone = false;
135
 
136
    /**
137
     * @var bool Flag indicating top of body already printed
138
     */
139
    protected $topofbodydone = false;
140
 
141
    /**
142
     * @var stdClass YUI PHPLoader instance responsible for YUI3 loading from PHP only
143
     */
144
    protected $yui3loader;
145
 
146
    /**
147
     * @var yui default YUI loader configuration
148
     */
149
    protected $YUI_config; // phpcs:ignore moodle.NamingConventions.ValidVariableName.MemberNameUnderscore
150
 
151
    /**
152
     * @var array $yuicssmodules
153
     */
154
    protected $yuicssmodules = [];
155
 
156
    /**
157
     * @var array Some config vars exposed in JS, please no secret stuff there
158
     */
159
    protected $M_cfg; // phpcs:ignore moodle.NamingConventions.ValidVariableName.MemberNameUnderscore
160
 
161
    /**
162
     * @var array list of requested jQuery plugins
163
     */
164
    protected $jqueryplugins = [];
165
 
166
    /**
167
     * @var array list of jQuery plugin overrides
168
     */
169
    protected $jquerypluginoverrides = [];
170
 
171
    /**
172
     * Page requirements constructor.
173
     */
174
    public function __construct() {
175
        global $CFG;
176
 
177
        // You may need to set up URL rewrite rule because oversized URLs might not be allowed by web server.
178
        $sep = empty($CFG->yuislasharguments) ? '?' : '/';
179
 
180
        $this->yui3loader = new stdClass();
181
        $this->YUI_config = new yui();
182
 
183
        // Set up some loader options.
184
        $this->yui3loader->local_base = $CFG->wwwroot . '/lib/yuilib/' . $CFG->yui3version . '/';
185
        $this->yui3loader->local_comboBase = $CFG->wwwroot . '/theme/yui_combo.php' . $sep;
186
 
187
        $this->yui3loader->base = $this->yui3loader->local_base;
188
        $this->yui3loader->comboBase = $this->yui3loader->local_comboBase;
189
 
190
        // Enable combo loader? This significantly helps with caching and performance!
191
        $this->yui3loader->combine = !empty($CFG->yuicomboloading);
192
 
193
        $jsrev = $this->get_jsrev();
194
 
195
        // Set up JS YUI loader helper object.
196
        $this->YUI_config->base         = $this->yui3loader->base;
197
        $this->YUI_config->comboBase    = $this->yui3loader->comboBase;
198
        $this->YUI_config->combine      = $this->yui3loader->combine;
199
 
200
        // If we've had to patch any YUI modules between releases, we must override the YUI configuration to include them.
201
        if (!empty($CFG->yuipatchedmodules) && !empty($CFG->yuipatchlevel)) {
202
            $this->YUI_config->define_patched_core_modules(
203
                $this->yui3loader->local_comboBase,
204
                $CFG->yui3version,
205
                $CFG->yuipatchlevel,
206
                $CFG->yuipatchedmodules
207
            );
208
        }
209
 
210
        $configname = $this->YUI_config->set_config_source('lib/yui/config/yui2.js');
211
        $this->YUI_config->add_group('yui2', [
212
            // Loader configuration for our 2in3.
213
            'base' => $CFG->wwwroot . '/lib/yuilib/2in3/' . $CFG->yui2version . '/build/',
214
            'comboBase' => $CFG->wwwroot . '/theme/yui_combo.php' . $sep,
215
            'combine' => $this->yui3loader->combine,
216
            'ext' => false,
217
            'root' => '2in3/' . $CFG->yui2version . '/build/',
218
            'patterns' => [
219
                'yui2-' => [
220
                    'group' => 'yui2',
221
                    'configFn' => $configname,
222
                ],
223
            ],
224
        ]);
225
        $configname = $this->YUI_config->set_config_source('lib/yui/config/moodle.js');
226
        $this->YUI_config->add_group('moodle', [
227
            'name' => 'moodle',
228
            'base' => $CFG->wwwroot . '/theme/yui_combo.php' . $sep . 'm/' . $jsrev . '/',
229
            'combine' => $this->yui3loader->combine,
230
            'comboBase' => $CFG->wwwroot . '/theme/yui_combo.php' . $sep,
231
            'ext' => false,
232
            'root' => 'm/' . $jsrev . '/', // Add the rev to the root path so that we can control caching.
233
            'patterns' => [
234
                'moodle-' => [
235
                    'group' => 'moodle',
236
                    'configFn' => $configname,
237
                ],
238
            ],
239
        ]);
240
 
241
        $this->YUI_config->add_group('gallery', [
242
            'name' => 'gallery',
243
            'base' => $CFG->wwwroot . '/lib/yuilib/gallery/',
244
            'combine' => $this->yui3loader->combine,
245
            'comboBase' => $CFG->wwwroot . '/theme/yui_combo.php' . $sep,
246
            'ext' => false,
247
            'root' => 'gallery/' . $jsrev . '/',
248
            'patterns' => [
249
                'gallery-' => [
250
                    'group' => 'gallery',
251
                ],
252
            ],
253
        ]);
254
 
255
        // Set some more loader options applying to groups too.
256
        if ($CFG->debugdeveloper) {
257
            // When debugging is enabled, we want to load the non-minified (RAW) versions of YUI library modules rather
258
            // than the DEBUG versions as these generally generate too much logging for our purposes.
259
            // However we do want the DEBUG versions of our Moodle-specific modules.
260
            // To debug a YUI-specific issue, change the yui3loader->filter value to DEBUG.
261
            $this->YUI_config->filter = 'RAW';
262
            $this->YUI_config->groups['moodle']['filter'] = 'DEBUG';
263
 
264
            // We use the yui3loader->filter setting when writing the YUI3 seed scripts into the header.
265
            $this->yui3loader->filter = $this->YUI_config->filter;
266
            $this->YUI_config->debug = true;
267
        } else {
268
            $this->yui3loader->filter = null;
269
            $this->YUI_config->groups['moodle']['filter'] = null;
270
            $this->YUI_config->debug = false;
271
        }
272
 
273
        // Include the YUI config log filters.
274
        if (!empty($CFG->yuilogexclude) && is_array($CFG->yuilogexclude)) {
275
            $this->YUI_config->logExclude = $CFG->yuilogexclude;
276
        }
277
        if (!empty($CFG->yuiloginclude) && is_array($CFG->yuiloginclude)) {
278
            $this->YUI_config->logInclude = $CFG->yuiloginclude;
279
        }
280
        if (!empty($CFG->yuiloglevel)) {
281
            $this->YUI_config->logLevel = $CFG->yuiloglevel;
282
        }
283
 
284
        // Add the moodle group's module data.
285
        $this->YUI_config->add_moodle_metadata();
286
 
287
        // Every page should include definition of following modules.
288
        $this->js_module($this->find_module('core_filepicker'));
289
        $this->js_module($this->find_module('core_comment'));
290
    }
291
 
292
    /**
293
     * Return the safe config values that get set for javascript in "M.cfg".
294
     *
295
     * @since 2.9
296
     * @param moodle_page $page The page to add JS to
297
     * @param renderer_base $renderer The renderer to use
298
     * @return array List of safe config values that are available to javascript.
299
     */
300
    public function get_config_for_javascript(moodle_page $page, renderer_base $renderer) {
301
        global $CFG, $USER;
302
 
303
        if (empty($this->M_cfg)) {
304
            $iconsystem = \core\output\icon_system::instance();
305
 
306
            // It is possible that the $page->context is null, so we can't use $page->context->id.
307
            $contextid = null;
308
            $contextinstanceid = null;
309
            if (!is_null($page->context)) {
310
                $contextid = $page->context->id;
311
                $contextinstanceid = $page->context->instanceid;
312
                $courseid = $page->course->id;
313
                $coursecontext = context_course::instance($courseid);
314
            }
315
 
316
            $this->M_cfg = [
317
                'wwwroot'               => $CFG->wwwroot,
318
                'apibase'               => $this->get_api_base(),
319
                'homeurl'               => $page->navigation->action,
320
                'sesskey'               => sesskey(),
321
                'sessiontimeout'        => $CFG->sessiontimeout,
322
                'sessiontimeoutwarning' => $CFG->sessiontimeoutwarning,
323
                'themerev'              => theme_get_revision(),
324
                'slasharguments'        => (int)(!empty($CFG->slasharguments)),
325
                'theme'                 => $page->theme->name,
326
                'iconsystemmodule'      => $iconsystem->get_amd_name(),
327
                'jsrev'                 => $this->get_jsrev(),
328
                'admin'                 => $CFG->admin,
329
                'svgicons'              => $page->theme->use_svg_icons(),
330
                'usertimezone'          => usertimezone(),
331
                'language'              => current_language(),
332
                'courseId'              => isset($courseid) ? (int) $courseid : 0,
333
                'courseContextId'       => isset($coursecontext) ? $coursecontext->id : 0,
334
                'contextid'             => $contextid,
335
                'contextInstanceId'     => (int) $contextinstanceid,
336
                'langrev'               => get_string_manager()->get_revision(),
337
                'templaterev'           => $this->get_templaterev(),
338
                'siteId'                => (int) SITEID,
339
                'userId'                => (int) $USER->id,
340
            ];
341
            if ($CFG->debugdeveloper) {
342
                $this->M_cfg['developerdebug'] = true;
343
            }
344
            if (defined('BEHAT_SITE_RUNNING')) {
345
                $this->M_cfg['behatsiterunning'] = true;
346
            }
347
        }
348
        return $this->M_cfg;
349
    }
350
 
351
    /**
352
     * Return the base URL for the API.
353
     *
354
     * If the router has been fully configured on the web server then we can use the shortened route, otherwise the r.php.
355
     *
356
     * @return string
357
     */
358
    protected function get_api_base(): string {
359
        global $CFG;
360
 
361
        if (!empty($CFG->routerconfigured)) {
362
            return sprintf(
363
                "%s/api",
364
                $CFG->wwwroot,
365
            );
366
        }
367
 
368
        return sprintf(
369
            "%s/r.php/api",
370
            $CFG->wwwroot,
371
        );
372
    }
373
 
374
    /**
375
     * Initialise with the bits of JavaScript that every Moodle page should have.
376
     *
377
     * @param moodle_page $page
378
     * @param core_renderer $renderer
379
     */
380
    protected function init_requirements_data(moodle_page $page, core_renderer $renderer) {
381
        global $CFG;
382
 
383
        // Init the js config.
384
        $this->get_config_for_javascript($page, $renderer);
385
 
386
        // Accessibility stuff.
387
        $this->skip_link_to('maincontent', get_string('tocontent', 'access'));
388
 
389
        // Add strings used on many pages.
390
        $this->string_for_js('confirmation', 'admin');
391
        $this->string_for_js('cancel', 'moodle');
392
        $this->string_for_js('yes', 'moodle');
393
 
394
        // Alter links in top frame to break out of frames.
395
        if ($page->pagelayout === 'frametop') {
396
            $this->js_init_call('M.util.init_frametop');
397
        }
398
 
399
        // Include block drag/drop if editing is on.
400
        if ($page->user_is_editing()) {
401
            $params = [
402
                'regions' => $page->blocks->get_regions(),
403
                'pagehash' => $page->get_edited_page_hash(),
404
            ];
405
            if (!empty($page->cm->id)) {
406
                $params['cmid'] = $page->cm->id;
407
            }
408
            // Strings for drag and drop.
409
            $this->strings_for_js(
410
                [
411
                    'movecontent',
412
                    'tocontent',
413
                    'emptydragdropregion',
414
                ],
415
                'moodle',
416
            );
417
            $page->requires->yui_module('moodle-core-blocks', 'M.core_blocks.init_dragdrop', [$params], null, true);
418
            $page->requires->js_call_amd('core_block/edit', 'init', ['pagehash' => $page->get_edited_page_hash()]);
419
        }
420
 
421
        // Include the YUI CSS Modules.
422
        $page->requires->set_yuicssmodules($page->theme->yuicssmodules);
423
    }
424
 
425
    /**
426
     * Determine the correct JS Revision to use for this load.
427
     *
428
     * @return int the jsrev to use.
429
     */
430
    public function get_jsrev() {
431
        global $CFG;
432
 
433
        if (empty($CFG->cachejs)) {
434
            $jsrev = -1;
435
        } else if (empty($CFG->jsrev)) {
436
            $jsrev = 1;
437
        } else {
438
            $jsrev = $CFG->jsrev;
439
        }
440
 
441
        return $jsrev;
442
    }
443
 
444
    /**
445
     * Determine the correct Template revision to use for this load.
446
     *
447
     * @return int the templaterev to use.
448
     */
449
    protected function get_templaterev() {
450
        global $CFG;
451
 
452
        if (empty($CFG->cachetemplates)) {
453
            $templaterev = -1;
454
        } else if (empty($CFG->templaterev)) {
455
            $templaterev = 1;
456
        } else {
457
            $templaterev = $CFG->templaterev;
458
        }
459
 
460
        return $templaterev;
461
    }
462
 
463
    /**
464
     * Ensure that the specified JavaScript file is linked to from this page.
465
     *
466
     * NOTE: This function is to be used in RARE CASES ONLY, please store your JS in module.js file
467
     * and use $PAGE->requires->js_init_call() instead or use /yui/ subdirectories for YUI modules.
468
     *
469
     * By default the link is put at the end of the page, since this gives best page-load performance.
470
     *
471
     * Even if a particular script is requested more than once, it will only be linked
472
     * to once.
473
     *
474
     * @param string|moodle_url $url The path to the .js file, relative to $CFG->dirroot / $CFG->wwwroot.
475
     *      For example '/mod/mymod/customscripts.js'; use moodle_url for external scripts
476
     * @param bool $inhead initialise in head
477
     */
478
    public function js($url, $inhead = false) {
479
        if ($url == '/question/qengine.js') {
480
            debugging('The question/qengine.js has been deprecated. ' .
481
                'Please use core_question/question_engine', DEBUG_DEVELOPER);
482
        }
483
        $url = $this->js_fix_url($url);
484
        $where = $inhead ? 'head' : 'footer';
485
        $this->jsincludes[$where][$url->out()] = $url;
486
    }
487
 
488
    /**
489
     * Request inclusion of jQuery library in the page.
490
     *
491
     * NOTE: this should not be used in official Moodle distribution!
492
     *
493
     * @link https://moodledev.io/docs/guides/javascript/jquery
494
     */
495
    public function jquery() {
496
        $this->jquery_plugin('jquery');
497
    }
498
 
499
    /**
500
     * Request inclusion of jQuery plugin.
501
     *
502
     * NOTE: this should not be used in official Moodle distribution!
503
     *
504
     * jQuery plugins are located in plugin/jquery/* subdirectory,
505
     * plugin/jquery/plugins.php lists all available plugins.
506
     *
507
     * Included core plugins:
508
     *   - jQuery UI
509
     *
510
     * Add-ons may include extra jQuery plugins in jquery/ directory,
511
     * plugins.php file defines the mapping between plugin names and
512
     * necessary page includes.
513
     *
514
     * Examples:
515
     * <code>
516
     *   // file: mod/xxx/view.php
517
     *   $PAGE->requires->jquery();
518
     *   $PAGE->requires->jquery_plugin('ui');
519
     *   $PAGE->requires->jquery_plugin('ui-css');
520
     * </code>
521
     *
522
     * <code>
523
     *   // file: theme/yyy/lib.php
524
     *   function theme_yyy_page_init(moodle_page $page) {
525
     *       $page->requires->jquery();
526
     *       $page->requires->jquery_plugin('ui');
527
     *       $page->requires->jquery_plugin('ui-css');
528
     *   }
529
     * </code>
530
     *
531
     * <code>
532
     *   // file: blocks/zzz/block_zzz.php
533
     *   public function get_required_javascript() {
534
     *       parent::get_required_javascript();
535
     *       $this->page->requires->jquery();
536
     *       $page->requires->jquery_plugin('ui');
537
     *       $page->requires->jquery_plugin('ui-css');
538
     *   }
539
     * </code>
540
     *
541
     * {@link https://moodledev.io/docs/guides/javascript/jquery}
542
     *
543
     * @param string $plugin name of the jQuery plugin as defined in jquery/plugins.php
544
     * @param string $component name of the component
545
     * @return bool success
546
     */
547
    public function jquery_plugin($plugin, $component = 'core') {
548
        global $CFG;
549
 
550
        if ($this->headdone) {
551
            debugging('Can not add jQuery plugins after starting page output!');
552
            return false;
553
        }
554
 
555
        if ($component !== 'core' && in_array($plugin, ['jquery', 'ui', 'ui-css'])) {
556
            debugging(
557
                "jQuery plugin '$plugin' is included in Moodle core, other components can not use the same name.",
558
                DEBUG_DEVELOPER,
559
            );
560
            $component = 'core';
561
        } else if ($component !== 'core' && strpos($component, '_') === false) {
562
            // Let's normalise the legacy activity names, Frankenstyle rulez!
563
            $component = 'mod_' . $component;
564
        }
565
 
566
        if (empty($this->jqueryplugins) && ($component !== 'core' || $plugin !== 'jquery')) {
567
            // Make sure the jQuery itself is always loaded first,
568
            // the order of all other plugins depends on order of $PAGE_>requires->.
569
            $this->jquery_plugin('jquery', 'core');
570
        }
571
 
572
        if (isset($this->jqueryplugins[$plugin])) {
573
            // No problem, we already have something, first Moodle plugin to register the jQuery plugin wins.
574
            return true;
575
        }
576
 
577
        $componentdir = core_component::get_component_directory($component);
578
        if (!file_exists($componentdir) || !file_exists("$componentdir/jquery/plugins.php")) {
579
            debugging("Can not load jQuery plugin '$plugin', missing plugins.php in component '$component'.", DEBUG_DEVELOPER);
580
            return false;
581
        }
582
 
583
        $plugins = [];
584
        require("$componentdir/jquery/plugins.php");
585
 
586
        if (!isset($plugins[$plugin])) {
587
            debugging("jQuery plugin '$plugin' can not be found in component '$component'.", DEBUG_DEVELOPER);
588
            return false;
589
        }
590
 
591
        $this->jqueryplugins[$plugin] = new stdClass();
592
        $this->jqueryplugins[$plugin]->plugin    = $plugin;
593
        $this->jqueryplugins[$plugin]->component = $component;
594
        $this->jqueryplugins[$plugin]->urls      = [];
595
 
596
        foreach ($plugins[$plugin]['files'] as $file) {
597
            if ($CFG->debugdeveloper) {
598
                if (!file_exists("$componentdir/jquery/$file")) {
599
                    debugging("Invalid file '$file' specified in jQuery plugin '$plugin' in component '$component'");
600
                    continue;
601
                }
602
                $file = str_replace('.min.css', '.css', $file);
603
                $file = str_replace('.min.js', '.js', $file);
604
            }
605
            if (!file_exists("$componentdir/jquery/$file")) {
606
                debugging("Invalid file '$file' specified in jQuery plugin '$plugin' in component '$component'");
607
                continue;
608
            }
609
            if (!empty($CFG->slasharguments)) {
610
                $url = new moodle_url("/theme/jquery.php");
611
                $url->set_slashargument("/$component/$file");
612
            } else {
613
                // This is not really good, we need slasharguments for relative links, this means no caching...
614
                $path = realpath("$componentdir/jquery/$file");
615
                if (strpos($path, $CFG->dirroot) === 0) {
616
                    $url = $CFG->wwwroot . preg_replace('/^' . preg_quote($CFG->dirroot, '/') . '/', '', $path);
617
                    // Replace all occurences of backslashes characters in url to forward slashes.
618
                    $url = str_replace('\\', '/', $url);
619
                    $url = new moodle_url($url);
620
                } else {
621
                    // Bad luck, fix your server!
622
                    debugging("Moodle jQuery integration requires 'slasharguments' setting to be enabled.");
623
                    continue;
624
                }
625
            }
626
            $this->jqueryplugins[$plugin]->urls[] = $url;
627
        }
628
 
629
        return true;
630
    }
631
 
632
    /**
633
     * Request replacement of one jQuery plugin by another.
634
     *
635
     * This is useful when themes want to replace the jQuery UI theme,
636
     * the problem is that theme can not prevent others from including the core ui-css plugin.
637
     *
638
     * Example:
639
     *  1/ generate new jQuery UI theme and place it into theme/yourtheme/jquery/
640
     *  2/ write theme/yourtheme/jquery/plugins.php
641
     *  3/ init jQuery from theme
642
     *
643
     * <code>
644
     *   // file theme/yourtheme/lib.php
645
     *   function theme_yourtheme_page_init($page) {
646
     *       $page->requires->jquery_plugin('yourtheme-ui-css', 'theme_yourtheme');
647
     *       $page->requires->jquery_override_plugin('ui-css', 'yourtheme-ui-css');
648
     *   }
649
     * </code>
650
     *
651
     * This code prevents loading of standard 'ui-css' which my be requested by other plugins,
652
     * the 'yourtheme-ui-css' gets loaded only if some other code requires jquery.
653
     *
654
     * @link https://moodledev.io/docs/guides/javascript/jquery
655
     *
656
     * @param string $oldplugin original plugin
657
     * @param string $newplugin the replacement
658
     */
659
    public function jquery_override_plugin($oldplugin, $newplugin) {
660
        if ($this->headdone) {
661
            debugging('Can not override jQuery plugins after starting page output!');
662
            return;
663
        }
664
        $this->jquerypluginoverrides[$oldplugin] = $newplugin;
665
    }
666
 
667
    /**
668
     * Return jQuery related markup for page start.
669
     * @return string
670
     */
671
    protected function get_jquery_headcode() {
672
        if (empty($this->jqueryplugins['jquery'])) {
673
            // If nobody requested jQuery then do not bother to load anything.
674
            // This may be useful for themes that want to override 'ui-css' only if requested by something else.
675
            return '';
676
        }
677
 
678
        $included = [];
679
        $urls = [];
680
 
681
        foreach ($this->jqueryplugins as $name => $unused) {
682
            if (isset($included[$name])) {
683
                continue;
684
            }
685
            if (array_key_exists($name, $this->jquerypluginoverrides)) {
686
                // The following loop tries to resolve the replacements,
687
                // use max 100 iterations to prevent infinite loop resulting
688
                // in blank page.
689
                $cyclic = true;
690
                $oldname = $name;
691
                for ($i = 0; $i < 100; $i++) {
692
                    $name = $this->jquerypluginoverrides[$name];
693
                    if (!array_key_exists($name, $this->jquerypluginoverrides)) {
694
                        $cyclic = false;
695
                        break;
696
                    }
697
                }
698
                if ($cyclic) {
699
                    // We can not do much with cyclic references here, let's use the old plugin.
700
                    $name = $oldname;
701
                    debugging("Cyclic overrides detected for jQuery plugin '$name'");
702
                } else if (empty($name)) {
703
                    // Developer requested removal of the plugin.
704
                    continue;
705
                } else if (!isset($this->jqueryplugins[$name])) {
706
                    debugging("Unknown jQuery override plugin '$name' detected");
707
                    $name = $oldname;
708
                } else if (isset($included[$name])) {
709
                    // The plugin was already included, easy.
710
                    continue;
711
                }
712
            }
713
 
714
            $plugin = $this->jqueryplugins[$name];
715
            $urls = array_merge($urls, $plugin->urls);
716
            $included[$name] = true;
717
        }
718
 
719
        $output = '';
720
        $attributes = ['rel' => 'stylesheet', 'type' => 'text/css'];
721
        foreach ($urls as $url) {
722
            if (preg_match('/\.js$/', $url)) {
723
                $output .= html_writer::script('', $url);
724
            } else if (preg_match('/\.css$/', $url)) {
725
                $attributes['href'] = $url;
726
                $output .= html_writer::empty_tag('link', $attributes) . "\n";
727
            }
728
        }
729
 
730
        return $output;
731
    }
732
 
733
    /**
734
     * Returns the actual url through which a JavaScript file is served.
735
     *
736
     * @param moodle_url|string $url full moodle url, or shortened path to script.
737
     * @throws coding_exception if the given $url isn't a shortened url starting with / or a moodle_url instance.
738
     * @return moodle_url
739
     */
740
    protected function js_fix_url($url) {
741
        global $CFG;
742
 
743
        if ($url instanceof moodle_url) {
744
            // If the URL is external to Moodle, it won't be handled by Moodle (!).
745
            if ($url->is_local_url()) {
746
                $localurl = $url->out_as_local_url();
747
                // Check if the URL points to a Moodle PHP resource.
748
                if (strpos($localurl, '.php') !== false) {
749
                    // It's a Moodle PHP resource e.g. a resource already served by the proper Moodle Handler.
750
                    return $url;
751
                }
752
                // It's a local resource: we need to further examine it.
753
                return $this->js_fix_url($url->out_as_local_url(false));
754
            }
755
            // The URL is not a Moodle resource.
756
            return $url;
757
        } else if (null !== $url && strpos($url, '/') === 0) {
758
            // Fix the admin links if needed.
759
            if ($CFG->admin !== 'admin') {
760
                if (strpos($url, "/admin/") === 0) {
761
                    $url = preg_replace("|^/admin/|", "/$CFG->admin/", $url);
762
                }
763
            }
764
            if (debugging()) {
765
                // Check file existence only when in debug mode.
766
                if (!file_exists($CFG->dirroot . strtok($url, '?'))) {
767
                    throw new coding_exception('Attempt to require a JavaScript file that does not exist.', $url);
768
                }
769
            }
770
            if (substr($url, -3) === '.js') {
771
                $jsrev = $this->get_jsrev();
772
                if (empty($CFG->slasharguments)) {
773
                    return new moodle_url('/lib/javascript.php', ['rev' => $jsrev, 'jsfile' => $url]);
774
                } else {
775
                    $returnurl = new moodle_url('/lib/javascript.php');
776
                    $returnurl->set_slashargument('/' . $jsrev . $url);
777
                    return $returnurl;
778
                }
779
            } else {
780
                return new moodle_url($url);
781
            }
782
        } else {
783
            throw new coding_exception('Invalid JS url, it has to be shortened url starting with / or moodle_url instance.', $url);
784
        }
785
    }
786
 
787
    /**
788
     * Find out if JS module present and return details.
789
     *
790
     * @param string $component name of component in frankenstyle, ex: core_group, mod_forum
791
     * @return array description of module or null if not found
792
     */
793
    protected function find_module($component) {
794
        global $CFG, $PAGE;
795
 
796
        $module = null;
797
 
798
        if (strpos($component, 'core_') === 0) {
799
            // Must be some core stuff - list here is not complete, this is just the stuff used from multiple places
800
            // so that we do nto have to repeat the definition of these modules over and over again.
801
            switch ($component) {
802
                case 'core_filepicker':
803
                    $module = [
804
                        'name' => 'core_filepicker',
805
                        'fullpath' => '/repository/filepicker.js',
806
                        'requires' => [
807
                            'base', 'node', 'node-event-simulate', 'json', 'async-queue', 'io-base', 'io-upload-iframe', 'io-form',
808
                            'yui2-treeview', 'panel', 'cookie', 'datatable', 'datatable-sort', 'resize-plugin', 'dd-plugin',
809
                            'escape', 'moodle-core_filepicker', 'moodle-core-notification-dialogue',
810
                        ],
811
                        'strings'  => [
812
                            ['lastmodified', 'moodle'],
813
                            ['name', 'moodle'],
814
                            ['type', 'repository'],
815
                            ['size', 'repository'],
816
                            ['invalidjson', 'repository'],
817
                            ['error', 'moodle'],
818
                            ['info', 'moodle'],
819
                            ['nofilesattached', 'repository'],
820
                            ['filepicker', 'repository'],
821
                            ['logout', 'repository'],
822
                            ['nofilesavailable', 'repository'],
823
                            ['norepositoriesavailable', 'repository'],
824
                            ['fileexistsdialogheader', 'repository'],
825
                            ['fileexistsdialog_editor', 'repository'],
826
                            ['fileexistsdialog_filemanager', 'repository'],
827
                            ['renameto', 'repository'],
828
                            ['referencesexist', 'repository'],
829
                            ['select', 'repository'],
830
                        ],
831
                    ];
832
                    break;
833
                case 'core_comment':
834
                    $module = [
835
                        'name' => 'core_comment',
836
                        'fullpath' => '/comment/comment.js',
837
                        'requires' => ['base', 'io-base', 'node', 'json', 'yui2-animation', 'overlay', 'escape'],
838
                        'strings' => [['confirmdeletecomments', 'admin'], ['yes', 'moodle'], ['no', 'moodle']],
839
                    ];
840
                    break;
841
                case 'core_role':
842
                    $module = [
843
                        'name' => 'core_role',
844
                        'fullpath' => '/admin/roles/module.js',
845
                        'requires' => ['node', 'cookie'],
846
                    ];
847
                    break;
848
                case 'core_completion':
849
                    break;
850
                case 'core_message':
851
                    $module = [
852
                        'name' => 'core_message',
853
                        'requires' => ['base', 'node', 'event', 'node-event-simulate'],
854
                        'fullpath' => '/message/module.js',
855
                    ];
856
                    break;
857
                case 'core_group':
858
                    $module = [
859
                        'name' => 'core_group',
860
                        'fullpath' => '/group/module.js',
861
                        'requires' => ['node', 'overlay', 'event-mouseenter'],
862
                    ];
863
                    break;
864
                case 'core_question_engine':
865
                    $module = [
866
                        'name' => 'core_question_engine',
867
                        'fullpath' => '/question/qengine.js',
868
                        'requires' => ['node', 'event'],
869
                    ];
870
                    break;
871
                case 'core_rating':
872
                    $module = [
873
                        'name' => 'core_rating',
874
                        'fullpath' => '/rating/module.js',
875
                        'requires' => ['node', 'event', 'overlay', 'io-base', 'json'],
876
                    ];
877
                    break;
878
                case 'core_dndupload':
879
                    $module = [
880
                        'name' => 'core_dndupload',
881
                        'fullpath' => '/lib/form/dndupload.js',
882
                        'requires' => ['node', 'event', 'json', 'core_filepicker'],
883
                        'strings'  => [
884
                            ['uploadformlimit', 'moodle'], ['droptoupload', 'moodle'], ['maxfilesreached', 'moodle'],
885
                            ['dndenabled_inbox', 'moodle'], ['fileexists', 'moodle'], ['maxbytesfile', 'error'],
886
                            ['sizegb', 'moodle'], ['sizemb', 'moodle'], ['sizekb', 'moodle'], ['sizeb', 'moodle'],
887
                            ['maxareabytesreached', 'moodle'], ['serverconnection', 'error'],
888
                            ['changesmadereallygoaway', 'moodle'], ['complete', 'moodle'],
889
                        ],
890
                    ];
891
                    break;
892
            }
893
        } else {
894
            if ($dir = core_component::get_component_directory($component)) {
895
                if (file_exists("$dir/module.js")) {
896
                    if (strpos($dir, $CFG->dirroot . '/') === 0) {
897
                        $dir = substr($dir, strlen($CFG->dirroot));
898
                        $module = ['name' => $component, 'fullpath' => "$dir/module.js", 'requires' => []];
899
                    }
900
                }
901
            }
902
        }
903
 
904
        return $module;
905
    }
906
 
907
    /**
908
     * Append YUI3 module to default YUI3 JS loader.
909
     * The structure of module array is described at {@link http://developer.yahoo.com/yui/3/yui/}
910
     *
911
     * @param string|array $module name of module (details are autodetected), or full module specification as array
912
     * @return void
913
     */
914
    public function js_module($module) {
915
        global $CFG;
916
 
917
        if (empty($module)) {
918
            throw new coding_exception('Missing YUI3 module name or full description.');
919
        }
920
 
921
        if (is_string($module)) {
922
            $module = $this->find_module($module);
923
        }
924
 
925
        if (empty($module) || empty($module['name']) || empty($module['fullpath'])) {
926
            throw new coding_exception('Missing YUI3 module details.');
927
        }
928
 
929
        $module['fullpath'] = $this->js_fix_url($module['fullpath'])->out(false);
930
        // Add all needed strings.
931
        if (!empty($module['strings'])) {
932
            foreach ($module['strings'] as $string) {
933
                $identifier = $string[0];
934
                $component = isset($string[1]) ? $string[1] : 'moodle';
935
                $a = isset($string[2]) ? $string[2] : null;
936
                $this->string_for_js($identifier, $component, $a);
937
            }
938
        }
939
        unset($module['strings']);
940
 
941
        // Process module requirements and attempt to load each. This allows
942
        // moodle modules to require each other.
943
        if (!empty($module['requires'])) {
944
            foreach ($module['requires'] as $requirement) {
945
                $rmodule = $this->find_module($requirement);
946
                if (is_array($rmodule)) {
947
                    $this->js_module($rmodule);
948
                }
949
            }
950
        }
951
 
952
        if ($this->headdone) {
953
            $this->extramodules[$module['name']] = $module;
954
        } else {
955
            $this->YUI_config->add_module_config($module['name'], $module);
956
        }
957
    }
958
 
959
    /**
960
     * Returns true if the module has already been loaded.
961
     *
962
     * @param string|array $module
963
     * @return bool True if the module has already been loaded
964
     */
965
    protected function js_module_loaded($module) {
966
        if (is_string($module)) {
967
            $modulename = $module;
968
        } else {
969
            $modulename = $module['name'];
970
        }
971
        return array_key_exists($modulename, $this->YUI_config->modules) ||
972
               array_key_exists($modulename, $this->extramodules);
973
    }
974
 
975
    /**
976
     * Ensure that the specified CSS file is linked to from this page.
977
     *
978
     * Because stylesheet links must go in the <head> part of the HTML, you must call
979
     * this function before {@see get_head_code()} is called. That normally means before
980
     * the call to print_header. If you call it when it is too late, an exception
981
     * will be thrown.
982
     *
983
     * Even if a particular style sheet is requested more than once, it will only
984
     * be linked to once.
985
     *
986
     * Please note use of this feature is strongly discouraged,
987
     * it is suitable only for places where CSS is submitted directly by teachers.
988
     * (Students must not be allowed to submit any external CSS because it may
989
     * contain embedded javascript!). Example of correct use is mod/data.
990
     *
991
     * @param string $stylesheet The path to the .css file, relative to $CFG->wwwroot.
992
     *   For example:
993
     *      $PAGE->requires->css('mod/data/css.php?d='.$data->id);
994
     */
995
    public function css($stylesheet) {
996
        global $CFG;
997
 
998
        if ($this->headdone) {
999
            throw new coding_exception('Cannot require a CSS file after &lt;head> has been printed.', $stylesheet);
1000
        }
1001
 
1002
        // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
1003
        if ($stylesheet instanceof moodle_url) {
1004
            // Ok.
1005
        } else if (strpos($stylesheet, '/') === 0) {
1006
            $stylesheet = new moodle_url($stylesheet);
1007
        } else {
1008
            throw new coding_exception('Invalid stylesheet parameter.', $stylesheet);
1009
        }
1010
 
1011
        $this->cssurls[$stylesheet->out()] = $stylesheet;
1012
    }
1013
 
1014
    /**
1015
     * Add theme stylesheet to page - do not use from plugin code,
1016
     * this should be called only from the core renderer!
1017
     *
1018
     * @param moodle_url $stylesheet
1019
     * @return void
1020
     */
1021
    public function css_theme(moodle_url $stylesheet) {
1022
        $this->cssthemeurls[] = $stylesheet;
1023
    }
1024
 
1025
    /**
1026
     * Ensure that a skip link to a given target is printed at the top of the <body>.
1027
     *
1028
     * You must call this function before {@see get_top_of_body_code()}, (if not, an exception
1029
     * will be thrown). That normally means you must call this before the call to print_header.
1030
     *
1031
     * If you ask for a particular skip link to be printed, it is then your responsibility
1032
     * to ensure that the appropriate <a name="..."> tag is printed in the body of the
1033
     * page, so that the skip link goes somewhere.
1034
     *
1035
     * Even if a particular skip link is requested more than once, only one copy of it will be output.
1036
     *
1037
     * @param string $target the name of anchor this link should go to. For example 'maincontent'.
1038
     * @param string $linktext The text to use for the skip link. Normally get_string('skipto', 'access', ...);
1039
     */
1040
    public function skip_link_to($target, $linktext) {
1041
        if ($this->topofbodydone) {
1042
            debugging('Page header already printed, can not add skip links any more, code needs to be fixed.');
1043
            return;
1044
        }
1045
        $this->skiplinks[$target] = $linktext;
1046
    }
1047
 
1048
    /**
1049
     * !!!DEPRECATED!!! please use js_init_call() if possible
1050
     * Ensure that the specified JavaScript function is called from an inline script
1051
     * somewhere on this page.
1052
     *
1053
     * By default the call will be put in a script tag at the
1054
     * end of the page after initialising Y instance, since this gives best page-load
1055
     * performance and allows you to use YUI3 library.
1056
     *
1057
     * If you request that a particular function is called several times, then
1058
     * that is what will happen (unlike linking to a CSS or JS file, where only
1059
     * one link will be output).
1060
     *
1061
     * The main benefit of the method is the automatic encoding of all function parameters.
1062
     *
1063
     * @deprecated
1064
     *
1065
     * @param string $function the name of the JavaScritp function to call. Can
1066
     *      be a compound name like 'Y.Event.purgeElement'. Can also be
1067
     *      used to create and object by using a 'function name' like 'new user_selector'.
1068
     * @param null|array $arguments and array of arguments to be passed to the function.
1069
     *      When generating the function call, this will be escaped using json_encode,
1070
     *      so passing objects and arrays should work.
1071
     * @param bool $ondomready If tru the function is only called when the dom is
1072
     *      ready for manipulation.
1073
     * @param int $delay The delay before the function is called.
1074
     */
1075
    public function js_function_call(
1076
        $function,
1077
        ?array $arguments = null,
1078
        $ondomready = false,
1079
        $delay = 0,
1080
    ) {
1081
        $where = $ondomready ? 'ondomready' : 'normal';
1082
        $this->jscalls[$where][] = [$function, $arguments, $delay];
1083
    }
1084
 
1085
    /**
1086
     * This function appends a block of code to the AMD specific javascript block executed
1087
     * in the page footer, just after loading the requirejs library.
1088
     *
1089
     * The code passed here can rely on AMD module loading, e.g. require('jquery', function($) {...});
1090
     *
1091
     * @param string $code The JS code to append.
1092
     */
1093
    public function js_amd_inline($code) {
1094
        $this->amdjscode[] = $code;
1095
    }
1096
 
1097
    /**
1098
     * Load an AMD module and eventually call its method.
1099
     *
1100
     * This function creates a minimal inline JS snippet that requires an AMD module and eventually calls a single
1101
     * function from the module with given arguments. If it is called multiple times, it will be create multiple
1102
     * snippets.
1103
     *
1104
     * @param string $fullmodule The name of the AMD module to load, formatted as <component name>/<module name>.
1105
     * @param string $func Optional function from the module to call, defaults to just loading the AMD module.
1106
     * @param array $params The params to pass to the function (will be serialized into JSON).
1107
     */
1108
    public function js_call_amd($fullmodule, $func = null, $params = []) {
1109
        global $CFG;
1110
 
1111
        $modulepath = explode('/', $fullmodule);
1112
 
1113
        $modname = clean_param(array_shift($modulepath), PARAM_COMPONENT);
1114
        foreach ($modulepath as $module) {
1115
            $modname .= '/' . clean_param($module, PARAM_ALPHANUMEXT);
1116
        }
1117
 
1118
        $functioncode = [];
1119
        if ($func !== null) {
1120
            $func = clean_param($func, PARAM_ALPHANUMEXT);
1121
 
1122
            $jsonparams = [];
1123
            foreach ($params as $param) {
1124
                $jsonparams[] = json_encode($param);
1125
            }
1126
            $strparams = implode(', ', $jsonparams);
1127
            if ($CFG->debugdeveloper) {
1128
                $toomanyparamslimit = 1024;
1129
                if (strlen($strparams) > $toomanyparamslimit) {
1130
                    debugging('Too much data passed as arguments to js_call_amd("' . $fullmodule . '", "' . $func .
1131
                        '"). Generally there are better ways to pass lots of data from PHP to JavaScript, for example via Ajax, ' .
1132
                        'data attributes, ... . This warning is triggered if the argument string becomes longer than ' .
1133
                        $toomanyparamslimit . ' characters.', DEBUG_DEVELOPER);
1134
                }
1135
            }
1136
 
1137
            $functioncode[] = "amd.{$func}({$strparams});";
1138
        }
1139
 
1140
        $functioncode[] = "M.util.js_complete('{$modname}');";
1141
 
1142
        $initcode = implode(' ', $functioncode);
1143
        $js = "M.util.js_pending('{$modname}'); require(['{$modname}'], function(amd) {{$initcode}});";
1144
 
1145
        $this->js_amd_inline($js);
1146
    }
1147
 
1148
    /**
1149
     * Creates a JavaScript function call that requires one or more modules to be loaded.
1150
     *
1151
     * This function can be used to include all of the standard YUI module types within JavaScript:
1152
     *     - YUI3 modules    [node, event, io]
1153
     *     - YUI2 modules    [yui2-*]
1154
     *     - Moodle modules  [moodle-*]
1155
     *     - Gallery modules [gallery-*]
1156
     *
1157
     * Before writing new code that makes extensive use of YUI, you should consider it's replacement AMD/JQuery.
1158
     * @see js_call_amd()
1159
     *
1160
     * @param array|string $modules One or more modules
1161
     * @param string $function The function to call once modules have been loaded
1162
     * @param null|array $arguments An array of arguments to pass to the function
1163
     * @param null|string $galleryversion Deprecated: The gallery version to use
1164
     * @param bool $ondomready
1165
     */
1166
    public function yui_module(
1167
        $modules,
1168
        $function,
1169
        ?array $arguments = null,
1170
        $galleryversion = null,
1171
        $ondomready = false,
1172
    ) {
1173
        if (!is_array($modules)) {
1174
            $modules = [$modules];
1175
        }
1176
 
1177
        if ($galleryversion != null) {
1178
            debugging('The galleryversion parameter to yui_module has been deprecated since Moodle 2.3.');
1179
        }
1180
 
1181
        $jscode = 'Y.use(' .
1182
            join(
1183
                ',',
1184
                array_map(
1185
                    'json_encode',
1186
                    convert_to_array($modules)
1187
                )
1188
            ) .
1189
            ',function() {' . js_writer::function_call($function, $arguments) . '});';
1190
        if ($ondomready) {
1191
            $jscode = "Y.on('domready', function() { $jscode });";
1192
        }
1193
        $this->jsinitcode[] = $jscode;
1194
    }
1195
 
1196
    /**
1197
     * Set the CSS Modules to be included from YUI.
1198
     *
1199
     * @param array $modules The list of YUI CSS Modules to include.
1200
     */
1201
    public function set_yuicssmodules(array $modules = []) {
1202
        $this->yuicssmodules = $modules;
1203
    }
1204
 
1205
    /**
1206
     * Ensure that the specified JavaScript function is called from an inline script
1207
     * from page footer.
1208
     *
1209
     * @param string $function the name of the JavaScritp function to with init code,
1210
     *      usually something like 'M.mod_mymodule.init'
1211
     * @param null|array $extraarguments and array of arguments to be passed to the function.
1212
     *      The first argument is always the YUI3 Y instance with all required dependencies
1213
     *      already loaded.
1214
     * @param bool $ondomready wait for dom ready (helps with some IE problems when modifying DOM)
1215
     * @param null|array $module JS module specification array
1216
     */
1217
    public function js_init_call(
1218
        $function,
1219
        ?array $extraarguments = null,
1220
        $ondomready = false,
1221
        ?array $module = null,
1222
    ) {
1223
        $jscode = js_writer::function_call_with_Y($function, $extraarguments);
1224
        if (!$module) {
1225
            // Detect module automatically.
1226
            if (preg_match('/M\.([a-z0-9]+_[^\.]+)/', $function, $matches)) {
1227
                $module = $this->find_module($matches[1]);
1228
            }
1229
        }
1230
 
1231
        $this->js_init_code($jscode, $ondomready, $module);
1232
    }
1233
 
1234
    /**
1235
     * Add short static javascript code fragment to page footer.
1236
     * This is intended primarily for loading of js modules and initialising page layout.
1237
     * Ideally the JS code fragment should be stored in plugin renderer so that themes
1238
     * may override it.
1239
     *
1240
     * @param string $jscode
1241
     * @param bool $ondomready wait for dom ready (helps with some IE problems when modifying DOM)
1242
     * @param null|array $module JS module specification array
1243
     */
1244
    public function js_init_code(
1245
        $jscode,
1246
        $ondomready = false,
1247
        ?array $module = null,
1248
    ) {
1249
        $jscode = trim($jscode, " ;\n") . ';';
1250
 
1251
        $uniqid = html_writer::random_id();
1252
        $startjs = " M.util.js_pending('" . $uniqid . "');";
1253
        $endjs = " M.util.js_complete('" . $uniqid . "');";
1254
 
1255
        if ($module) {
1256
            $this->js_module($module);
1257
            $modulename = $module['name'];
1258
            $jscode = "$startjs Y.use('$modulename', function(Y) { $jscode $endjs });";
1259
        }
1260
 
1261
        if ($ondomready) {
1262
            $jscode = "$startjs Y.on('domready', function() { $jscode $endjs });";
1263
        }
1264
 
1265
        $this->jsinitcode[] = $jscode;
1266
    }
1267
 
1268
    /**
1269
     * Make a language string available to JavaScript.
1270
     *
1271
     * All the strings will be available in a M.str object in the global namespace.
1272
     * So, for example, after a call to $PAGE->requires->string_for_js('course', 'moodle');
1273
     * then the JavaScript variable M.str.moodle.course will be 'Course', or the
1274
     * equivalent in the current language.
1275
     *
1276
     * The arguments to this function are just like the arguments to get_string
1277
     * except that $component is not optional, and there are some aspects to consider
1278
     * when the string contains {$a} placeholder.
1279
     *
1280
     * If the string does not contain any {$a} placeholder, you can simply use
1281
     * M.str.component.identifier to obtain it. If you prefer, you can call
1282
     * M.util.get_string(identifier, component) to get the same result.
1283
     *
1284
     * If you need to use {$a} placeholders, there are two options. Either the
1285
     * placeholder should be substituted in PHP on server side or it should
1286
     * be substituted in Javascript at client side.
1287
     *
1288
     * To substitute the placeholder at server side, just provide the required
1289
     * value for the placeholder when you require the string. Because each string
1290
     * is only stored once in the JavaScript (based on $identifier and $module)
1291
     * you cannot get the same string with two different values of $a. If you try,
1292
     * an exception will be thrown. Once the placeholder is substituted, you can
1293
     * use M.str or M.util.get_string() as shown above:
1294
     *
1295
     *     // Require the string in PHP and replace the placeholder.
1296
     *     $PAGE->requires->string_for_js('fullnamedisplay', 'moodle', $USER);
1297
     *     // Use the result of the substitution in Javascript.
1298
     *     alert(M.str.moodle.fullnamedisplay);
1299
     *
1300
     * To substitute the placeholder at client side, use M.util.get_string()
1301
     * function. It implements the same logic as {@see get_string()}:
1302
     *
1303
     *     // Require the string in PHP but keep {$a} as it is.
1304
     *     $PAGE->requires->string_for_js('fullnamedisplay', 'moodle');
1305
     *     // Provide the values on the fly in Javascript.
1306
     *     user = { firstname : 'Harry', lastname : 'Potter' }
1307
     *     alert(M.util.get_string('fullnamedisplay', 'moodle', user);
1308
     *
1309
     * If you do need the same string expanded with different $a values in PHP
1310
     * on server side, then the solution is to put them in your own data structure
1311
     * (e.g. and array) that you pass to JavaScript with {@see data_for_js()}.
1312
     *
1313
     * @param string $identifier the desired string.
1314
     * @param string $component the language file to look in.
1315
     * @param mixed $a any extra data to add into the string (optional).
1316
     */
1317
    public function string_for_js($identifier, $component, $a = null) {
1318
        if (!$component) {
1319
            throw new coding_exception('The $component parameter is required for page_requirements_manager::string_for_js().');
1320
        }
1321
        if (isset($this->stringsforjs_as[$component][$identifier]) && $this->stringsforjs_as[$component][$identifier] !== $a) {
1322
            throw new coding_exception(
1323
                "Attempt to re-define already required string '$identifier' " .
1324
                    "from lang file '$component' with different \$a parameter?",
1325
            );
1326
        }
1327
        if (!isset($this->stringsforjs[$component][$identifier])) {
1328
            $this->stringsforjs[$component][$identifier] = new lang_string($identifier, $component, $a);
1329
            $this->stringsforjs_as[$component][$identifier] = $a;
1330
        }
1331
    }
1332
 
1333
    /**
1334
     * Make an array of language strings available for JS.
1335
     *
1336
     * This function calls the above function {@see string_for_js()} for each requested
1337
     * string in the $identifiers array that is passed to the argument for a single module
1338
     * passed in $module.
1339
     *
1340
     * <code>
1341
     * $PAGE->requires->strings_for_js(array('one', 'two', 'three'), 'mymod', array('a', null, 3));
1342
     *
1343
     * // The above is identical to calling:
1344
     *
1345
     * $PAGE->requires->string_for_js('one', 'mymod', 'a');
1346
     * $PAGE->requires->string_for_js('two', 'mymod');
1347
     * $PAGE->requires->string_for_js('three', 'mymod', 3);
1348
     * </code>
1349
     *
1350
     * @param array $identifiers An array of desired strings
1351
     * @param string $component The module to load for
1352
     * @param mixed $a This can either be a single variable that gets passed as extra
1353
     *         information for every string or it can be an array of mixed data where the
1354
     *         key for the data matches that of the identifier it is meant for.
1355
     *
1356
     */
1357
    public function strings_for_js($identifiers, $component, $a = null) {
1358
        foreach ($identifiers as $key => $identifier) {
1359
            if (is_array($a) && array_key_exists($key, $a)) {
1360
                $extra = $a[$key];
1361
            } else {
1362
                $extra = $a;
1363
            }
1364
            $this->string_for_js($identifier, $component, $extra);
1365
        }
1366
    }
1367
 
1368
    /**
1369
     * !!!!!!DEPRECATED!!!!!! please use js_init_call() for everything now.
1370
     *
1371
     * Make some data from PHP available to JavaScript code.
1372
     *
1373
     * For example, if you call
1374
     * <pre>
1375
     *      $PAGE->requires->data_for_js('mydata', array('name' => 'Moodle'));
1376
     * </pre>
1377
     * then in JavsScript mydata.name will be 'Moodle'.
1378
     *
1379
     * @deprecated
1380
     * @param string $variable the the name of the JavaScript variable to assign the data to.
1381
     *      Will probably work if you use a compound name like 'mybuttons.button[1]', but this
1382
     *      should be considered an experimental feature.
1383
     * @param mixed $data The data to pass to JavaScript. This will be escaped using json_encode,
1384
     *      so passing objects and arrays should work.
1385
     * @param bool $inhead initialise in head
1386
     * @return void
1387
     */
1388
    public function data_for_js($variable, $data, $inhead = false) {
1389
        $where = $inhead ? 'head' : 'footer';
1390
        $this->jsinitvariables[$where][] = [$variable, $data];
1391
    }
1392
 
1393
    /**
1394
     * Creates a YUI event handler.
1395
     *
1396
     * @param mixed $selector standard YUI selector for elements, may be array or string, element id is in the form "#idvalue"
1397
     * @param string $event A valid DOM event (click, mousedown, change etc.)
1398
     * @param string $function The name of the function to call
1399
     * @param null|array $arguments An optional array of argument parameters to pass to the function
1400
     */
1401
    public function event_handler(
1402
        $selector,
1403
        $event,
1404
        $function,
1405
        ?array $arguments = null,
1406
    ) {
1407
        $this->eventhandlers[] = ['selector' => $selector, 'event' => $event, 'function' => $function, 'arguments' => $arguments];
1408
    }
1409
 
1410
    /**
1411
     * Returns code needed for registering of event handlers.
1412
     * @return string JS code
1413
     */
1414
    protected function get_event_handler_code() {
1415
        $output = '';
1416
        foreach ($this->eventhandlers as $h) {
1417
            $output .= js_writer::event_handler($h['selector'], $h['event'], $h['function'], $h['arguments']);
1418
        }
1419
        return $output;
1420
    }
1421
 
1422
    /**
1423
     * Get the inline JavaScript code that need to appear in a particular place.
1424
     * @param bool $ondomready
1425
     * @return string
1426
     */
1427
    protected function get_javascript_code($ondomready) {
1428
        $where = $ondomready ? 'ondomready' : 'normal';
1429
        $output = '';
1430
        if ($this->jscalls[$where]) {
1431
            foreach ($this->jscalls[$where] as $data) {
1432
                $output .= js_writer::function_call($data[0], $data[1], $data[2]);
1433
            }
1434
            if (!empty($ondomready)) {
1435
                $output = "    Y.on('domready', function() {\n$output\n});";
1436
            }
1437
        }
1438
        return $output;
1439
    }
1440
 
1441
    /**
1442
     * Returns js code to be executed when Y is available.
1443
     * @return string
1444
     */
1445
    protected function get_javascript_init_code() {
1446
        if (count($this->jsinitcode)) {
1447
            return implode("\n", $this->jsinitcode) . "\n";
1448
        }
1449
        return '';
1450
    }
1451
 
1452
    /**
1453
     * Returns js code to load amd module loader, then insert inline script tags
1454
     * that contain require() calls using RequireJS.
1455
     * @return string
1456
     */
1457
    protected function get_amd_footercode() {
1458
        global $CFG;
1459
        $output = '';
1460
 
1461
        // We will cache JS if cachejs is not set, or it is true.
1462
        $cachejs = !isset($CFG->cachejs) || $CFG->cachejs;
1463
        $jsrev = $this->get_jsrev();
1464
 
1465
        $jsloader = new moodle_url('/lib/javascript.php');
1466
        $jsloader->set_slashargument('/' . $jsrev . '/');
1467
        $requirejsloader = new moodle_url('/lib/requirejs.php');
1468
        $requirejsloader->set_slashargument('/' . $jsrev . '/');
1469
 
1470
        $requirejsconfig = file_get_contents($CFG->dirroot . '/lib/requirejs/moodle-config.js');
1471
 
1472
        // No extension required unless slash args is disabled.
1473
        $jsextension = '.js';
1474
        if (!empty($CFG->slasharguments)) {
1475
            $jsextension = '';
1476
        }
1477
 
1478
        $minextension = '.min';
1479
        if (!$cachejs) {
1480
            $minextension = '';
1481
        }
1482
 
1483
        $requirejsconfig = str_replace('[BASEURL]', $requirejsloader, $requirejsconfig);
1484
        $requirejsconfig = str_replace('[JSURL]', $jsloader, $requirejsconfig);
1485
        $requirejsconfig = str_replace('[JSMIN]', $minextension, $requirejsconfig);
1486
        $requirejsconfig = str_replace('[JSEXT]', $jsextension, $requirejsconfig);
1487
 
1488
        $output .= html_writer::script($requirejsconfig);
1489
        if ($cachejs) {
1490
            $output .= html_writer::script('', $this->js_fix_url('/lib/requirejs/require.min.js'));
1491
        } else {
1492
            $output .= html_writer::script('', $this->js_fix_url('/lib/requirejs/require.js'));
1493
        }
1494
 
1495
        // First include must be to a module with no dependencies, this prevents multiple requests.
1496
        $prefix = <<<EOF
1497
M.util.js_pending("core/first");
1498
require(['core/first'], function() {
1499
 
1500
EOF;
1501
 
1502
        if (during_initial_install()) {
1503
            // Do not run a prefetch during initial install as the DB is not available to service WS calls.
1504
            $prefetch = '';
1505
        } else {
1506
            $prefetch = "require(['core/prefetch'])\n";
1507
        }
1508
 
1509
        $suffix = <<<EOF
1510
 
1511
    M.util.js_complete("core/first");
1512
});
1513
EOF;
1514
 
1515
        $output .= html_writer::script($prefix . $prefetch . implode(";\n", $this->amdjscode) . $suffix);
1516
        return $output;
1517
    }
1518
 
1519
    /**
1520
     * Returns basic YUI3 CSS code.
1521
     *
1522
     * @return string
1523
     */
1524
    protected function get_yui3lib_headcss() {
1525
        global $CFG;
1526
 
1527
        $yuiformat = '-min';
1528
        if ($this->yui3loader->filter === 'RAW') {
1529
            $yuiformat = '';
1530
        }
1531
 
1532
        $code = '';
1533
        if ($this->yui3loader->combine) {
1534
            if (!empty($this->yuicssmodules)) {
1535
                $modules = [];
1536
                foreach ($this->yuicssmodules as $module) {
1537
                    $modules[] = "$CFG->yui3version/$module/$module-min.css";
1538
                }
1539
                $code .= '<link rel="stylesheet" type="text/css" href="' .
1540
                    $this->yui3loader->comboBase . implode('&amp;', $modules) . '" />';
1541
            }
1542
            $code .= '<link rel="stylesheet" type="text/css" href="' .
1543
                $this->yui3loader->local_comboBase . 'rollup/' . $CFG->yui3version . '/yui-moodlesimple' . $yuiformat .
1544
                '.css" />';
1545
        } else {
1546
            if (!empty($this->yuicssmodules)) {
1547
                foreach ($this->yuicssmodules as $module) {
1548
                    $code .= '<link rel="stylesheet" type="text/css"href="' .
1549
                        $this->yui3loader->base . $module . '/' . $module .
1550
                        '-min.css" />';
1551
                }
1552
            }
1553
            $code .= '<link rel="stylesheet" type="text/css" href="' .
1554
                $this->yui3loader->local_comboBase . 'rollup/' . $CFG->yui3version . '/yui-moodlesimple' . $yuiformat .
1555
                '.css" />';
1556
        }
1557
 
1558
        if ($this->yui3loader->filter === 'RAW') {
1559
            $code = str_replace('-min.css', '.css', $code);
1560
        } else if ($this->yui3loader->filter === 'DEBUG') {
1561
            $code = str_replace('-min.css', '.css', $code);
1562
        }
1563
        return $code;
1564
    }
1565
 
1566
    /**
1567
     * Returns basic YUI3 JS loading code.
1568
     *
1569
     * @return string
1570
     */
1571
    protected function get_yui3lib_headcode() {
1572
        global $CFG;
1573
 
1574
        $jsrev = $this->get_jsrev();
1575
 
1576
        $yuiformat = '-min';
1577
        if ($this->yui3loader->filter === 'RAW') {
1578
            $yuiformat = '';
1579
        }
1580
 
1581
        $format = '-min';
1582
        if ($this->YUI_config->groups['moodle']['filter'] === 'DEBUG') {
1583
            $format = '-debug';
1584
        }
1585
 
1586
        $rollupversion = $CFG->yui3version;
1587
        if (!empty($CFG->yuipatchlevel)) {
1588
            $rollupversion .= '_' . $CFG->yuipatchlevel;
1589
        }
1590
 
1591
        $baserollups = [
1592
            'rollup/' . $rollupversion . "/yui-moodlesimple{$yuiformat}.js",
1593
        ];
1594
 
1595
        if ($this->yui3loader->combine) {
1596
            return '<script src="' .
1597
                    $this->yui3loader->local_comboBase .
1598
                    implode('&amp;', $baserollups) .
1599
                    '"></script>';
1600
        } else {
1601
            $code = '';
1602
            foreach ($baserollups as $rollup) {
1603
                $code .= '<script src="' . $this->yui3loader->local_comboBase . $rollup . '"></script>';
1604
            }
1605
            return $code;
1606
        }
1607
    }
1608
 
1609
    /**
1610
     * Returns html tags needed for inclusion of theme CSS.
1611
     *
1612
     * @return string
1613
     */
1614
    protected function get_css_code() {
1615
        // First of all the theme CSS, then any custom CSS
1616
        // Please note custom CSS is strongly discouraged,
1617
        // because it can not be overridden by themes!
1618
        // It is suitable only for things like mod/data which accepts CSS from teachers.
1619
        $attributes = ['rel' => 'stylesheet', 'type' => 'text/css'];
1620
 
1621
        // Add the YUI code first. We want this to be overridden by any Moodle CSS.
1622
        $code = $this->get_yui3lib_headcss();
1623
 
1624
        // This line of code may look funny but it is currently required in order
1625
        // to avoid MASSIVE display issues in Internet Explorer.
1626
        // As of IE8 + YUI3.1.1 the reference stylesheet (firstthemesheet) gets
1627
        // ignored whenever another resource is added until such time as a redraw
1628
        // is forced, usually by moving the mouse over the affected element.
1629
        $code .= html_writer::tag(
1630
            'script',
1631
            '/** Required in order to fix style inclusion problems in IE with YUI **/',
1632
            ['id' => 'firstthemesheet', 'type' => 'text/css'],
1633
        );
1634
 
1635
        $urls = $this->cssthemeurls + $this->cssurls;
1636
        foreach ($urls as $url) {
1637
            $attributes['href'] = $url;
1638
            $code .= html_writer::empty_tag('link', $attributes) . "\n";
1639
            // This id is needed in first sheet only so that theme may override YUI sheets loaded on the fly.
1640
            unset($attributes['id']);
1641
        }
1642
 
1643
        return $code;
1644
    }
1645
 
1646
    /**
1647
     * Adds extra modules specified after printing of page header.
1648
     *
1649
     * @return string
1650
     */
1651
    protected function get_extra_modules_code() {
1652
        if (empty($this->extramodules)) {
1653
            return '';
1654
        }
1655
        return html_writer::script(js_writer::function_call('M.yui.add_module', [$this->extramodules]));
1656
    }
1657
 
1658
    /**
1659
     * Generate any HTML that needs to go inside the <head> tag.
1660
     *
1661
     * Normally, this method is called automatically by the code that prints the
1662
     * <head> tag. You should not normally need to call it in your own code.
1663
     *
1664
     * @param moodle_page $page
1665
     * @param core_renderer $renderer
1666
     * @return string the HTML code to to inside the <head> tag.
1667
     */
1668
    public function get_head_code(moodle_page $page, core_renderer $renderer) {
1669
        global $CFG;
1670
 
1671
        // Note: the $page and $output are not stored here because it would
1672
        // create circular references in memory which prevents garbage collection.
1673
        $this->init_requirements_data($page, $renderer);
1674
 
1675
        $output = '';
1676
 
1677
        // Add all standard CSS for this page.
1678
        $output .= $this->get_css_code();
1679
 
1680
        // Set up the M namespace.
1681
        $js = "var M = {}; M.yui = {};\n";
1682
 
1683
        // Capture the time now ASAP during page load. This minimises the lag when
1684
        // we try to relate times on the server to times in the browser.
1685
        // An example of where this is used is the quiz countdown timer.
1686
        $js .= "M.pageloadstarttime = new Date();\n";
1687
 
1688
        // Add a subset of Moodle configuration to the M namespace.
1689
        $js .= js_writer::set_variable('M.cfg', $this->M_cfg, false);
1690
 
1691
        // Set up global YUI3 loader object - this should contain all code needed by plugins.
1692
        // Note: in JavaScript just use "YUI().use('overlay', function(Y) { .... });",
1693
        // this needs to be done before including any other script.
1694
        $js .= $this->YUI_config->get_config_functions();
1695
        $js .= js_writer::set_variable('YUI_config', $this->YUI_config, false) . "\n";
1696
        $js .= "M.yui.loader = {modules: {}};\n"; // Backwards compatibility only, not used any more.
1697
        $js = $this->YUI_config->update_header_js($js);
1698
 
1699
        $output .= html_writer::script($js);
1700
 
1701
        // Add variables.
1702
        if ($this->jsinitvariables['head']) {
1703
            $js = '';
1704
            foreach ($this->jsinitvariables['head'] as $data) {
1705
                [$var, $value] = $data;
1706
                $js .= js_writer::set_variable($var, $value, true);
1707
            }
1708
            $output .= html_writer::script($js);
1709
        }
1710
 
1711
        // Mark head sending done, it is not possible to anything there.
1712
        $this->headdone = true;
1713
 
1714
        return $output;
1715
    }
1716
 
1717
    /**
1718
     * Generate any HTML that needs to go at the start of the <body> tag.
1719
     *
1720
     * Normally, this method is called automatically by the code that prints the
1721
     * <head> tag. You should not normally need to call it in your own code.
1722
     *
1723
     * @param renderer_base $renderer
1724
     * @return string the HTML code to go at the start of the <body> tag.
1725
     */
1726
    public function get_top_of_body_code(renderer_base $renderer) {
1727
        global $CFG;
1728
 
1729
        // First the skip links.
1730
        $output = $renderer->render_skip_links($this->skiplinks);
1731
 
1732
        // Include the Polyfills.
1733
        $output .= html_writer::script('', $this->js_fix_url('/lib/polyfills/polyfill.js'));
1734
 
1735
        // YUI3 JS needs to be loaded early in the body. It should be cached well by the browser.
1736
        $output .= $this->get_yui3lib_headcode();
1737
 
1738
        // Add hacked jQuery support, it is not intended for standard Moodle distribution!
1739
        $output .= $this->get_jquery_headcode();
1740
 
1741
        // Link our main JS file, all core stuff should be there.
1742
        $output .= html_writer::script('', $this->js_fix_url('/lib/javascript-static.js'));
1743
 
1744
        // All the other linked things from HEAD - there should be as few as possible.
1745
        if ($this->jsincludes['head']) {
1746
            foreach ($this->jsincludes['head'] as $url) {
1747
                $output .= html_writer::script('', $url);
1748
            }
1749
        }
1750
 
1751
        // Then the clever trick for hiding of things not needed when JS works.
1752
        $output .= html_writer::script("document.body.className += ' jsenabled';") . "\n";
1753
        $this->topofbodydone = true;
1754
        return $output;
1755
    }
1756
 
1757
    /**
1758
     * Generate any HTML that needs to go at the end of the page.
1759
     *
1760
     * Normally, this method is called automatically by the code that prints the
1761
     * page footer. You should not normally need to call it in your own code.
1762
     *
1763
     * @return string the HTML code to to at the end of the page.
1764
     */
1765
    public function get_end_code() {
1766
        global $CFG, $USER;
1767
        $output = '';
1768
 
1769
        // Set the log level for the JS logging.
1770
        $logconfig = new stdClass();
1771
        $logconfig->level = 'warn';
1772
        if ($CFG->debugdeveloper) {
1773
            $logconfig->level = 'trace';
1774
        }
1775
        $this->js_call_amd('core/log', 'setConfig', [$logconfig]);
1776
        // Add any global JS that needs to run on all pages.
1777
        $this->js_call_amd('core/page_global', 'init');
1778
        $this->js_call_amd('core/utility');
1779
        $this->js_call_amd('core/storage_validation', 'init', [
1780
            !empty($USER->currentlogin) ? (int) $USER->currentlogin : null
1781
        ]);
1782
 
1783
        // Call amd init functions.
1784
        $output .= $this->get_amd_footercode();
1785
 
1786
        // Add other requested modules.
1787
        $output .= $this->get_extra_modules_code();
1788
 
1789
        $this->js_init_code('M.util.js_complete("init");', true);
1790
 
1791
        // All the other linked scripts - there should be as few as possible.
1792
        if ($this->jsincludes['footer']) {
1793
            foreach ($this->jsincludes['footer'] as $url) {
1794
                $output .= html_writer::script('', $url);
1795
            }
1796
        }
1797
 
1798
        // Add all needed strings.
1799
        // First add core strings required for some dialogues.
1800
        $this->strings_for_js([
1801
            'confirm',
1802
            'yes',
1803
            'no',
1804
            'areyousure',
1805
            'closebuttontitle',
1806
            'unknownerror',
1807
            'error',
1808
            'file',
1809
            'url',
1810
            // TODO MDL-70830 shortforms should preload the collapseall/expandall strings properly.
1811
            'collapseall',
1812
            'expandall',
1813
        ], 'moodle');
1814
        $this->strings_for_js([
1815
            'debuginfo',
1816
            'line',
1817
            'stacktrace',
1818
        ], 'debug');
1819
        $this->string_for_js('labelsep', 'langconfig');
1820
        if (!empty($this->stringsforjs)) {
1821
            $strings = [];
1822
            foreach ($this->stringsforjs as $component => $v) {
1823
                foreach ($v as $indentifier => $langstring) {
1824
                    $strings[$component][$indentifier] = $langstring->out();
1825
                }
1826
            }
1827
            $output .= html_writer::script(js_writer::set_variable('M.str', $strings));
1828
        }
1829
 
1830
        // Add variables.
1831
        if ($this->jsinitvariables['footer']) {
1832
            $js = '';
1833
            foreach ($this->jsinitvariables['footer'] as $data) {
1834
                [$var, $value] = $data;
1835
                $js .= js_writer::set_variable($var, $value, true);
1836
            }
1837
            $output .= html_writer::script($js);
1838
        }
1839
 
1840
        $inyuijs = $this->get_javascript_code(false);
1841
        $ondomreadyjs = $this->get_javascript_code(true);
1842
        $jsinit = $this->get_javascript_init_code();
1843
        $handlersjs = $this->get_event_handler_code();
1844
 
1845
        // There is a global Y, make sure it is available in your scope.
1846
        $js = "(function() {{$inyuijs}{$ondomreadyjs}{$jsinit}{$handlersjs}})();";
1847
 
1848
        $output .= html_writer::script($js);
1849
 
1850
        return $output;
1851
    }
1852
 
1853
    /**
1854
     * Have we already output the code in the <head> tag?
1855
     *
1856
     * @return bool
1857
     */
1858
    public function is_head_done() {
1859
        return $this->headdone;
1860
    }
1861
 
1862
    /**
1863
     * Have we already output the code at the start of the <body> tag?
1864
     *
1865
     * @return bool
1866
     */
1867
    public function is_top_of_body_done() {
1868
        return $this->topofbodydone;
1869
    }
1870
 
1871
    /**
1872
     * Should we generate a bit of content HTML that is only required once  on
1873
     * this page (e.g. the contents of the modchooser), now? Basically, we call
1874
     * {@see has_one_time_item_been_created()}, and if the thing has not already
1875
     * been output, we return true to tell the caller to generate it, and also
1876
     * call {@see set_one_time_item_created()} to record the fact that it is
1877
     * about to be generated.
1878
     *
1879
     * That is, a typical usage pattern (in a renderer method) is:
1880
     * <pre>
1881
     * if (!$this->page->requires->should_create_one_time_item_now($thing)) {
1882
     *     return '';
1883
     * }
1884
     * // Else generate it.
1885
     * </pre>
1886
     *
1887
     * @param string $thing identifier for the bit of content. Should be of the form
1888
     *      frankenstyle_things, e.g. core_course_modchooser.
1889
     * @return bool if true, the caller should generate that bit of output now, otherwise don't.
1890
     */
1891
    public function should_create_one_time_item_now($thing) {
1892
        if ($this->has_one_time_item_been_created($thing)) {
1893
            return false;
1894
        }
1895
 
1896
        $this->set_one_time_item_created($thing);
1897
        return true;
1898
    }
1899
 
1900
    /**
1901
     * Has a particular bit of HTML that is only required once  on this page
1902
     * (e.g. the contents of the modchooser) already been generated?
1903
     *
1904
     * Normally, you can use the {@see should_create_one_time_item_now()} helper
1905
     * method rather than calling this method directly.
1906
     *
1907
     * @param string $thing identifier for the bit of content. Should be of the form
1908
     *      frankenstyle_things, e.g. core_course_modchooser.
1909
     * @return bool whether that bit of output has been created.
1910
     */
1911
    public function has_one_time_item_been_created($thing) {
1912
        return isset($this->onetimeitemsoutput[$thing]);
1913
    }
1914
 
1915
    /**
1916
     * Indicate that a particular bit of HTML that is only required once on this
1917
     * page (e.g. the contents of the modchooser) has been generated (or is about to be)?
1918
     *
1919
     * Normally, you can use the {@see should_create_one_time_item_now()} helper
1920
     * method rather than calling this method directly.
1921
     *
1922
     * @param string $thing identifier for the bit of content. Should be of the form
1923
     *      frankenstyle_things, e.g. core_course_modchooser.
1924
     */
1925
    public function set_one_time_item_created($thing) {
1926
        if ($this->has_one_time_item_been_created($thing)) {
1927
            throw new coding_exception($thing . ' is only supposed to be ouput ' .
1928
                    'once per page, but it seems to be being output again.');
1929
        }
1930
        return $this->onetimeitemsoutput[$thing] = true;
1931
    }
1932
}
1933
 
1934
// Alias this class to the old name.
1935
// This file will be autoloaded by the legacyclasses autoload system.
1936
// In future all uses of this class will be corrected and the legacy references will be removed.
1937
class_alias(page_requirements_manager::class, \page_requirements_manager::class);