Proyectos de Subversion Moodle

Rev

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