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