1<?php 2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project 3// 4// All Rights Reserved. See copyright.txt for details and a complete list of authors. 5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. 6// $Id$ 7 8if (strpos($_SERVER["SCRIPT_NAME"], basename(__FILE__)) !== false) { 9 header("location: index.php"); 10 exit; 11} 12 13 14/** 15 * Add Javascript and CSS to output 16 * Javascript and CSS can be added: 17 * - as files (filename including relative path to tiki root) 18 * - as scripts (string) 19 * - as a url to load from a cdn (Note: use $tikilib->httpScheme() to build the url. It considers reverse proxies and returns correctly 'http' or 'https') 20 * Note: there are 2 prefs to add additional cdns. one for http and one for https. 21 * 22 * To maintain the order of loading Javascript and to allow minifying, the following "ranks" are supported: 23 * '10dynamic': loaded first to allow minification of the other ranks. Usually module and plugin descriptions. 24 * '20cdn' : loaded after 'dynamic', no minification possible // main libs like jquery from jquery/google cdn (no user cdns) 25 * '30dependancy': loaded after 'cdn', minification possible // main libs like jquery, codemirror 26 * '40external': loaded after 'dependancy', minification possible // custom libs that require main libs 27 * '50standard': loaded after 'external', minification possible // standard js that might require main / custom libs 28 * '60late': loaded after 'standard', minification possible // page specific js 29 * Note: this rank is activated in tiki-setup.php to separate page specific JS from common JS 30 * So any JS loaded after tiki-setup.php that has no rank 'external' is put into 'late'. 31 * If minification is activated for late, any new combination of late files will be created automatically if needed. 32 * When using user specific CDNs AND minification for late is enabled, any possible minified file must be available via that CDN! 33 * 34 * The order of files within each section will be maintained. What adds first will be processed first. 35 * 36 * Note: cdns (google/jquery, not user cdns), files and scripts (strings) will be handled separately. 37 * 38 * To add JS, the following methods are available. 39 * 40 * 41 * Methods to add JS files: 42 * If $skip_minify == true, the file will not be processed for further minification. 43 * This could be used to avoid screwing up the JS file in the rare case minification on that particular file does not work. 44 * It will however be concatenated to a single JS file. 45 46 * add_jsfile_cdn($url) - add a JS File from a CDN 47 * add_jsfile_dependancy($filename, $skip_minify) - add a JS File to the section dependancy 48 * add_jsfile_external($filename, $skip_minify) - add a JS File to the section external 49 * add_jsfile($filename, $skip_minify) - add a JS File to the section standard 50 * add_jsfile_late($filename, $skip_minify) - add a JS File to the section late 51 52 * 53 * These functions allow to add JS as scripts/strings. No minification on them: 54 * add_js($script, $rank) - add JS as string 55 * add_jq_onready($script, $rank) - add JS as string and add to the onready event 56 * add_js_config($script, $rank) - add JS that usually represents a config object 57 * 58 * @TODO CSS handling 59 * 60 */ 61class HeaderLib 62{ 63 public $title; 64 65 /** 66 * Array of js files arrays or js urls arrays to load 67 * key = rank, value = array of filenames with relative path or urls 68 * Some ranks have special meanings: (note ranks are array keys in the same array) 69 * @var array() 70 */ 71 public $jsfiles; 72 73 74 /** 75 * Array of js files that are already minified or should not be minified 76 * Filled when adding jsfiles and setting the $skip_minify param to true 77 * key = filename with relative path 78 * @var array 79 */ 80 public $skip_minify; 81 82 83 /** 84 * Array of JS scripts arrays as strings to load 85 * key = rank (load order), value = array of scripts. 86 * js[$rank][] = $script; 87 * @var array 88 */ 89 public $js; 90 91 92 /** 93 * Array of JS Scripts arrays as string that act as config 94 * Usually created dynamically 95 * js_config[$rank][] = $script; 96 * @var array 97 */ 98 public $js_config; 99 100 101 /** 102 * Array of JS Scripts arrays as string that should be called onReady(). 103 * Key = rank (load order), value = array of scripts. 104 * jq_onready[$rank][] = $script; 105 * @var array 106 */ 107 public $jq_onready; 108 109 /** 110 * Array of JS Scripts arrays as string that should be embedded as modules 111 * Key = rank (load order), value = array of scripts. 112 * jq_modules[$rank][] = $script; 113 * @var array 114 */ 115 public $js_modules; 116 117 public $cssfiles; 118 public $css; 119 public $rssfeeds; 120 public $metatags; 121 public $linktags; 122 123 /* If set to true, any js added through add_jsfile() that has not rank 'external' will be put to rank 'late' 124 * Only set once in tiki-setup.php to separate wiki page specific js from common js. 125 * @var boolean 126 */ 127 public $forceJsRankLate; 128 129 130 public $jquery_version = '3.2.1'; 131 public $jqueryui_version = '1.12.1'; 132 public $jquerymigrate_version = '3.0.0'; 133 134 135 function __construct() 136 { 137 $smarty = TikiLib::lib('smarty'); 138 $smarty->assign('headerlib', $this); 139 140 $this->title = ''; 141 $this->jsfiles = []; 142 $this->skip_minify = []; 143 $this->js = []; 144 $this->js_config = []; 145 $this->jq_onready = []; 146 $this->js_modules = []; 147 $this->cssfiles = []; 148 $this->css = []; 149 $this->rssfeeds = []; 150 $this->metatags = []; 151 $this->rawhtml = ''; 152 153 $this->forceJsRankLate = false; 154 } 155 156 157 /** 158 * user cdn and feature multi_cdn see r46854 159 * @param string $file 160 * @param string $rank 161 * @return string $file 162 */ 163 function convert_cdn($file, $rank = null) 164 { 165 global $prefs, $tikiroot; 166 167 // using this method, also reverse proxy / ssl offloading will continue to work 168 $httpScheme = Tikilib::httpScheme(); 169 $https_mode = ($httpScheme == 'https') ? true : false; 170 171 $cdn_ssl_uri = array_filter(preg_split('/\s+/', $prefs['tiki_cdn_ssl'])); 172 $cdn_uri = array_filter(preg_split('/\s+/', $prefs['tiki_cdn'])); 173 174 if ($https_mode && ! empty($cdn_ssl_uri)) { 175 $cdn_pref = &$cdn_ssl_uri; 176 } elseif (! empty($cdn_uri)) { 177 $cdn_pref = &$cdn_uri; 178 } 179 180 // feature multi_cdn see r46854 - quote from commit: 181 // filename hash is used to select/assign one CDN URI from the list. 182 // It ensure a same file will always point/use the same CDN and ensure proper caching. 183 if (! empty($cdn_pref) && 'http' != substr($file, 0, 4) && $rank !== 'dynamic') { 184 $index = hexdec(hash("crc32b", $file)) % count($cdn_pref); 185 $file = $cdn_pref[$index] . $tikiroot . $file; 186 } 187 188 return $file; 189 } 190 191 192 function set_title($string) 193 { 194 $this->title = urlencode($string); 195 } 196 197 /** 198 * Add a js url from this tiki instance to top priority load order. 199 * These are usually dynamic created js scripts for configuration, module settings etc. 200 * Urls added here will not be further processed (like minified or put into a single file) 201 * @param string $url - relative url to this tiki instance 202 * @return HeaderLib Current object 203 */ 204 function add_jsfile_dynamic($url) 205 { 206 $this->add_jsfile_by_rank($url, '10dynamic', true); 207 return $this; 208 } 209 210 211 /** 212 * Add a js url to top priority load order. That url must be loaded from an external source. 213 * These are usually libraries like jquery that are loaded from a cdn = content delivery network. 214 * Urls added here will not be further processed (like minified or put into a single file) 215 * 216 * N.B. skip_minify needs to be set to true here for when tiki_minify_late_js_files is active 217 * and cdn files are added after page setup by plugins etc 218 * 219 * @param string $url - absolute url including http/https 220 * @return HeaderLib Current object 221 */ 222 function add_jsfile_cdn($url) 223 { 224 $this->add_jsfile_by_rank($url, '20cdn', true); 225 return $this; 226 } 227 228 229 /** 230 * Add a js file to top priority load order, right after cdns and dynamics. That file must not be loaded from an external source. 231 * Theses are usually libraries like jquery or codemirror, so files where other js file depend on. 232 * Depending on prefs, it could be minified and put into a single js file. 233 * @param string $file with path relative to tiki dir 234 * @param bool $skip_minify true if the file must not be minified, false if it can 235 * @return HeaderLib Current object 236 */ 237 function add_jsfile_dependancy($file, $skip_minify = false) 238 { 239 $this->add_jsfile_by_rank($file, '30dependancy', $skip_minify); 240 return $this; 241 } 242 243 244 /** 245 * Add a js file to load after dependancy . That file must not be loaded from an external source. 246 * Theses are usually custom libraries like raphael, gaffle etc. 247 * Depending on prefs, it could be minified and put into a single js file. 248 * @param string $filename with path relative to tiki dir 249 * @param bool $skip_minify true if the file must not be minified, false if it can 250 * @return HeaderLib Current object 251 */ 252 function add_jsfile_external($file, $skip_minify = false) 253 { 254 $this->add_jsfile_by_rank($file, '40external', $skip_minify); 255 return $this; 256 } 257 258 259 /** 260 * Adds a js file to load after external. That file must not be loaded from an external source. 261 * Depending on prefs, it could be minified and also put into a single js file 262 * @param string $file - path relative to tiki dir 263 * @param bool $skip_minify true if the file must not be minified, false if it can 264 * @return HeaderLib Current object 265 */ 266 function add_jsfile($file, $skip_minify = false) 267 { 268 $this->add_jsfile_by_rank($file, '50standard', $skip_minify); 269 return $this; 270 } 271 272 273 /** 274 * Add a js file to load after standard . That file must not be loaded from an external source. 275 * Use this method to add page specific js files. They will be minified separately. 276 * @see $this->forceJsRankLate() 277 * Depending on prefs, it could be minified and put into a single js file. 278 * @param string $filename with path relative to tiki dir 279 * @param bool $skip_minify true if the file must not be minified, false if it can 280 * @return HeaderLib Current object 281 */ 282 function add_jsfile_late($file, $skip_minify = false) 283 { 284 $this->add_jsfile_by_rank($file, '60late', $skip_minify); 285 return $this; 286 } 287 288 289 /** 290 * Add a jf file by rank. Do not use this function directly! 291 * Only reason that it is public, is for access from lib/core/tiki/PageCache.php 292 * @param string $file 293 * @param string $rank 294 * @param bool $skip_minify true if the file must not be minified, false if it can 295 * @return HeaderLib Current object 296 */ 297 function add_jsfile_by_rank($file, $rank, $skip_minify = false) 298 { 299 // if js is added after tiki-setup.php is run, add those js files to 'late' 300 // need to check whether this is really needed 301 if ($this->forceJsRankLate == true && $rank !== '40external') { 302 $rank = '60late'; 303 } 304 305 if (empty($this->jsfiles[$rank]) or ! in_array($file, $this->jsfiles[$rank])) { 306 $this->jsfiles[$rank][] = $file; 307 if ($skip_minify) { 308 $this->skip_minify[$file] = $skip_minify; 309 } 310 } 311 return $this; 312 } 313 314 function drop_jsfile($file) 315 { 316 $out = []; 317 foreach ($this->jsfiles as $rank => $data) { 318 foreach ($data as $f) { 319 if ($f != $file) { 320 $out[$rank][] = $f; 321 } 322 } 323 } 324 $this->jsfiles = $out; 325 return $this; 326 } 327 328 329 /** 330 * Add js that works as config. Usually created dynamically. 331 * @param string $script 332 * @param integer $rank - loadorder optional, default 0 333 * @return HeaderLib Current object 334 */ 335 function add_js_config($script, $rank = 0) 336 { 337 if (empty($this->js_config[$rank]) or ! in_array($script, $this->js_config[$rank])) { 338 $this->js_config[$rank][] = $script; 339 } 340 return $this; 341 } 342 343 344 /** 345 * JS scripts to add as string 346 * @param string $script 347 * @param integer $rank loadorder optional, default = 0 348 * @return HeaderLib Current object 349 */ 350 function add_js($script, $rank = 0) 351 { 352 if (empty($this->js[$rank]) or ! in_array($script, $this->js[$rank])) { 353 $this->js[$rank][] = $script; 354 } 355 return $this; 356 } 357 358 /** 359 * Adds lines or blocks of JQuery JavaScript to $(document).ready handler 360 * @param string $script - Script to execute 361 * @param number $rank - load order (default=0) 362 * @return HeaderLib Current object 363 */ 364 function add_jq_onready($script, $rank = 0) 365 { 366 if (empty($this->jq_onready[$rank]) or ! in_array($script, $this->jq_onready[$rank])) { 367 $this->jq_onready[$rank][] = $script; 368 } 369 return $this; 370 } 371 372 /** 373 * Adds a javascript module 374 * 375 * @param string $script 376 * @param int $rank 377 * 378 * @return $this 379 */ 380 function add_js_module($script, $rank = 0) 381 { 382 if (empty($this->js_modules[$rank]) or ! in_array($script, $this->js_modules[$rank])) { 383 $this->js_modules[$rank][] = $script; 384 } 385 return $this; 386 } 387 388 function add_cssfile($file, $rank = 0) 389 { 390 if ((empty($this->cssfiles[$rank]) or ! in_array($file, $this->cssfiles[$rank])) && ! empty($file)) { 391 $this->cssfiles[$rank][] = $file; 392 } 393 return $this; 394 } 395 396 function replace_cssfile($old, $new, $rank) 397 { 398 foreach ($this->cssfiles[$rank] as $i => $css) { 399 if ($css == $old) { 400 $this->cssfiles[$rank][$i] = $new; 401 break; 402 } 403 } 404 return $this; 405 } 406 407 function drop_cssfile($file) 408 { 409 $out = []; 410 foreach ($this->cssfiles as $rank => $data) { 411 foreach ($data as $f) { 412 if ($f != $file) { 413 $out[$rank][] = $f; 414 } 415 } 416 } 417 $this->cssfiles = $out; 418 return $this; 419 } 420 421 function add_css($rules, $rank = 0) 422 { 423 if (empty($this->css[$rank]) or ! in_array($rules, $this->css[$rank])) { 424 $this->css[$rank][] = $rules; 425 } 426 return $this; 427 } 428 429 function add_rssfeed($href, $title, $rank = 0) 430 { 431 if (empty($this->rssfeeds[$rank]) or ! in_array($href, array_keys($this->rssfeeds[$rank]))) { 432 $this->rssfeeds[$rank][$href] = $title; 433 } 434 return $this; 435 } 436 437 function add_meta($tag, $value) 438 { 439 $tag = addslashes($tag); 440 $this->metatags[$tag] = $value; 441 return $this; 442 } 443 444 function add_rawhtml($tags) 445 { 446 $this->rawhtml = $tags; 447 return $this; 448 } 449 450 function add_link($rel, $href, $sizes = '', $type = '', $color = '') 451 { 452 $this->linktags[$href]['href'] = $href; 453 $this->linktags[$href]['rel'] = $rel; 454 if ($sizes) { 455 $this->linktags[$href]['sizes'] = $sizes; 456 } 457 if ($type) { 458 $this->linktags[$href]['type'] = $type; 459 } 460 if ($color) { 461 $this->linktags[$href]['color'] = $color; 462 } 463 return $this; 464 } 465 466 function output_headers() 467 { 468 $smarty = TikiLib::lib('smarty'); 469 $smarty->loadPlugin('smarty_modifier_escape'); 470 471 ksort($this->cssfiles); 472 ksort($this->css); 473 ksort($this->rssfeeds); 474 475 $back = "\n"; 476 if ($this->title) { 477 $back = '<title>' . smarty_modifier_escape($this->title) . "</title>\n\n"; 478 } 479 480 if ($this->rawhtml) { 481 $back .= $this->rawhtml; 482 } 483 484 if (count($this->metatags)) { 485 foreach ($this->metatags as $n => $m) { 486 // check if the meta name starts with OpenGraph protocol prefix and use property instead of name if true 487 $nameattrib = preg_match('/^og\:/', $n) ? 'property' : 'name'; 488 $back .= '<meta ' . $nameattrib . '="' . smarty_modifier_escape($n) . '" content="' . smarty_modifier_escape($m) . "\">\n"; 489 } 490 $back .= "\n"; 491 } 492 if (count($this->linktags)) { 493 foreach ($this->linktags as $link) { 494 $back .= '<link rel="' . $link['rel'] . '" href="' . $link['href'] . '"'; 495 if (isset($link['sizes'])) { 496 $back .= ' sizes="' . $link['sizes'] . '"' ; 497 } 498 if (isset($link['type'])) { 499 $back .= ' type="' . $link['type'] . '"' ; 500 } 501 if (isset($link['color'])) { 502 $back .= ' color="' . $link['color'] . '"' ; 503 } 504 $back .= ">\n"; 505 } 506 } 507 508 509 $back .= $this->output_css_files(); 510 511 if (count($this->css)) { 512 $back .= "<style type=\"text/css\"><!--\n"; 513 foreach ($this->css as $x => $css) { 514 $back .= "/* css $x */\n"; 515 foreach ($css as $c) { 516 $back .= "$c\n"; 517 } 518 } 519 $back .= "-->\n</style>\n"; 520 } 521 522 523 if (count($this->rssfeeds)) { 524 foreach ($this->rssfeeds as $x => $rssf) { 525 $back .= "<!-- rss $x -->\n"; 526 foreach ($rssf as $rsstitle => $rssurl) { 527 $back .= "<link rel=\"alternate\" type=\"application/rss+xml\" title=\"" . smarty_modifier_escape($this->convert_cdn($rsstitle)) . "\" href=\"" . smarty_modifier_escape($rssurl) . "\">\n"; 528 } 529 } 530 $back .= "\n"; 531 } 532 return $back; 533 } 534 535 536 /** 537 * Force JS Files being added after tiki-setup.php is done to the rank/loadorder 'late' if rank is not 'external'. 538 * Used to separate page specific JS Files from the rest. 539 * @return HeaderLib Current object 540 */ 541 public function forceJsRankLate() 542 { 543 $this->forceJsRankLate = true; 544 return $this; 545 } 546 547 548 /** 549 * Gets included JavaScript files (for AJAX) 550 * Used in also lib/wiki/wikilib.php to rebuild the cache if activated 551 * @return array $jsFiles effectivly used jsfiles in scripttags considering minification / cdns if activated. 552 */ 553 function getJsFilesWithScriptTags() 554 { 555 /* 556 // MISCONCEPTION: user cdns are supposed to work as entire tiki cdns - not user based additional url sources 557 558 // check for user defined cdns: prefs: tiki_cdn_ssl, tiki_cdn 559 // the current prefs ask for complete urls including the scheme-name (http / https) 560 561 $httpScheme = Tikilib::httpScheme(); 562 $cdnType = ($httpScheme == 'http') ? 'tiki_cdn' : 'tiki_cdn_ssl'; 563 if (isset($prefs[$cdnType])) { 564 565 $customCdns = array_filter(preg_split('/\s+/', $prefs[$cdnType])); 566 $rank = 'customCdn'; 567 foreach ($customCdns as $entry) { 568 trim($entry); 569 if (!empty($entry)) { 570 $output[$rank] .= "<script type=\"text/javascript\" src=\"".smarty_modifier_escape($entry)."\"></script>\n"; 571 } 572 } 573 } 574 */ 575 576 577 global $prefs; 578 if ($prefs['javascript_enabled'] == 'n') { 579 return []; 580 } 581 582 if (count($this->jsfiles) == 0) { 583 return []; 584 } 585 586 $smarty = TikiLib::lib('smarty'); 587 $smarty->loadPlugin('smarty_modifier_escape'); 588 589 ksort($this->jsfiles); 590 $jsfiles = $this->jsfiles; 591 592 593 // array that holds a sorted list for all JS files including script tags in the correct order 594 $output = []; 595 596 // output dynamic and cdn first - they cannot be minified anyway 597 $ranks = ['10dynamic', '20cdn']; 598 foreach ($ranks as $rank) { 599 if (isset($jsfiles[$rank])) { 600 foreach ($jsfiles[$rank] as $entry) { 601 $output[] = '<script type="text/javascript" src="' . smarty_modifier_escape($entry) . '"></script>'; 602 } 603 } 604 } 605 606 // all other ranks could be minified - minification only happens if activated and if the file was not blocked by $skip_minify 607 608 // check whether we need to minify. minify also includes to put the minified files into one single file 609 $minifyActive = isset($prefs['tiki_minify_javascript']) && $prefs['tiki_minify_javascript'] == 'y' ? true : false; 610 611 if (! $minifyActive) { 612 $ranks = ['30dependancy', '40external', '50standard', '60late']; 613 foreach ($ranks as $rank) { 614 if (isset($jsfiles[$rank])) { 615 foreach ($jsfiles[$rank] as $entry) { 616 $entry = $this->convert_cdn($entry, $rank); 617 $output[] = '<script type="text/javascript" src="' . smarty_modifier_escape($entry) . '"></script>'; 618 } 619 } 620 } 621 } else { 622 // minify (each set of ranks will be compressed into one file). 623 624 // late stuff can vary by page. if we would include it in main, then we get multiple big js files. 625 // better to accept 2 js request: a big one which rarely changes and small ones that include (page specific) late stuff. 626 // at the end we could get rid of this pref though 627 628 $ranks = ['30dependancy', '40external', '50standard']; 629 $entry = $this->minifyJSFiles($jsfiles, $ranks); 630 $output[] .= '<script type="text/javascript" src="' . smarty_modifier_escape($entry) . '"></script>'; 631 632 $minifyLateActive = isset($prefs['tiki_minify_late_js_files']) && $prefs['tiki_minify_late_js_files'] == 'y' ? true : false; 633 $rank = '60late'; 634 if ($minifyLateActive) { 635 foreach ($jsfiles[$rank] as $index => $file) { 636 if ($this->skip_minify[$file] === true) { 637 $output[] .= '<script type="text/javascript" src="' . smarty_modifier_escape($file) . '"></script>'; 638 unset($jsfiles[$rank][$index]); 639 } 640 } 641 // handling of user defined cdn servers is done inside minifyJSFiles() 642 $entry = $this->minifyJSFiles($jsfiles, [$rank]); 643 $output[] .= '<script type="text/javascript" src="' . smarty_modifier_escape($entry) . '"></script>'; 644 } else { 645 foreach ($jsfiles[$rank] as $entry) { 646 $output[] = '<script type="text/javascript" src="' . smarty_modifier_escape($entry) . '"></script>'; 647 } 648 } 649 } 650 651 return $output; 652 } 653 654 655 /** 656 * Minify multiple JS files over multiple ranks into one single JS file. 657 * The file is identified by a hash over the given $jsfiles array and automatically created if needed. 658 * @param array $allJsfiles array of jsfiles ordered by ranks 659 * @param array $ranks simple array of ranks that needs to be processed. 660 * @return string $filename - name and relative path of the final js file. 661 */ 662 private function minifyJSFiles($allJsfiles, $ranks) 663 { 664 global $tikidomainslash, $prefs; 665 666 $cachelib = TikiLib::lib('cache'); 667 $cacheType = 'js_minify_hash'; 668 669 // build hash to identify minified file based on the _requested_ ranks, NOT on the entire jsfiles array 670 // $jsfiles contains only those keys defined in $ranks 671 $jsfiles = array_intersect_key($allJsfiles, array_flip($ranks)); 672 $cacheName = md5(serialize($jsfiles)); 673 674 // create the minified filename based on the contents of the files, and cache that hash as it's expensive to create 675 // browsers will automatically load new js if it has changed after the cache has been cleared, after an upgrade for instance 676 $hash = $cachelib->getCached($cacheName, $cacheType); 677 if (! $hash) { 678 $hash = $this->getFilesContentsHash($jsfiles); 679 $cachelib->cacheItem($cacheName, $hash, $cacheType); 680 } 681 $tempDir = 'temp/public/' . $tikidomainslash; 682 $file = $tempDir . "min_main_" . $hash . ".js"; 683 $cdnFile = $this->convert_cdn($file); 684 685 // Check if we are on a user defined CDN and the file exists (if tiki_cdn_check is enabled). 686 // Note: cdn will only be used if the local minified file exists, this ensures that we run the minification at 687 // least once locally (covers the case where this instance is also cdn) 688 if (file_exists($file) && ($file != $cdnFile)) { 689 $cacheType = 'cdn_minify_check'; 690 if ($prefs['tiki_cdn_check'] === 'y' && ! $cachelib->isCached($cdnFile, $cacheType)) { 691 $cdnHeaders = get_headers($cdnFile); 692 if (strpos(current($cdnHeaders), '200') !== false) { // check the file is really there 693 $cachelib->cacheItem($cdnFile, $cdnHeaders, $cacheType); 694 } 695 } else { 696 return $cdnFile; 697 } 698 } 699 700 if (file_exists($file)) { 701 return $file; 702 } 703 704 // file does not exist - create it 705 $minifiedAll = ''; 706 // show all relevant messages about the JS files on top - will be prepended to the output 707 $topMsg = "/**** start overview of included js files *****/\n"; 708 foreach ($ranks as $rank) { 709 // add list of minified js files to output 710 $topMsg .= "\n/* list of files for rank:$rank */\n"; 711 $topMsg .= '/* ' . print_r($jsfiles[$rank], true) . ' */' . "\n"; 712 foreach ($jsfiles[$rank] as $f) { 713 // important - some scripts like vendor_bundled/vendor/jquery/plugins/async/jquery.async.js do not terminate their last bits with a ';' 714 // this is bad practise and that causes issues when putting them all in one file! 715 $minified = ';'; 716 $msg = ''; 717 // if the name contains not 'min' and that file is not blacklisted for minification assume it is minified 718 // preferable is to set $skip_minify proper 719 if (! preg_match('/\bmin\./', $f) && $this->skip_minify[$f] !== true) { 720 set_time_limit(600); 721 try { 722 // to optimize processing time for changed js requirements, cache the minified version of each file 723 $hash = md5($f); 724 // filename without extension - makes it easier to identify the compressed files if needed. 725 $prefix = basename($f, '.js'); 726 $minifyFile = $tempDir . "min_s_" . $prefix . "_" . $hash . ".js"; 727 if (file_exists($minifyFile)) { 728 $temp = file_get_contents($minifyFile); 729 } else { 730 // if the file does not exist MatthiasMullie\Minify takes the input to be the file content 731 // which causes js errors and can break the whole site 732 if (! file_exists($f)) { 733 Feedback::error(tr('JavaScript file "%0" cannot be found so will not be minified.', $f)); 734 throw new Exception('File not found'); 735 } 736 $minifier = new MatthiasMullie\Minify\JS($f); 737 $temp = $minifier->minify($minifyFile); 738 chmod($minifyFile, 0644); 739 } 740 $msg .= "\n/* rank:$rank - minify:ok. $f */\n"; 741 $topMsg .= $msg; 742 $minified .= $msg; 743 $minified .= $temp; 744 } catch (Exception $e) { 745 $content = file_get_contents($f); 746 $error = $e->getMessage(); 747 $msg .= "\n/* rank:$rank - minify:error ($error) - adding raw file. $f */\n"; 748 $topMsg .= $msg; 749 $minified .= $msg; 750 $minified .= $content; 751 } 752 } else { 753 $content = file_get_contents($f); 754 $msg .= "\n/* rank:$rank - minify:disabled - adding raw file. $f */\n"; 755 $topMsg .= $msg; 756 $minified .= $msg; 757 $minified .= $content; 758 } 759 760 $minifiedAll .= $minified; 761 } 762 } 763 764 $topMsg .= "\n/**** end overview of included js files *****/\n"; 765 file_put_contents($file, $topMsg . $minifiedAll); 766 chmod($file, 0644); 767 return $file; 768 } 769 770 /** 771 * Calculate a hash based on the contents of files recursively 772 * 773 * @param array $files multidimensional array of file paths to minify/hash 774 * @param string $hash 775 * @return string hash based on contents of the files 776 */ 777 private function getFilesContentsHash(array $files, & $hash = '') 778 { 779 foreach ($files as $file) { 780 if (is_array($file)) { 781 $hash .= $this->getFilesContentsHash($file, $hash); 782 } else { 783 $hash .= md5_file($file); 784 } 785 } 786 return md5($hash); 787 } 788 789 790 /** 791 * Output script tags for all javascript files being used. 792 * If minification is activated, file based JS (so not from a CDN) will be minified und put into one single file 793 * @return string $jsScriptTags 794 */ 795 function output_js_files() 796 { 797 798 // we get one sorted array with script tags 799 $js_files = $this->getJsFilesWithScriptTags(); 800 $output = ''; 801 802 foreach ($js_files as $entry) { 803 $output .= "\n$entry"; 804 } 805 806 return $output; 807 } 808 809 810 811 function output_js_config($wrap = true) 812 { 813 global $prefs; 814 815 if ($prefs['javascript_enabled'] == 'n') { 816 return; 817 } 818 819 $back = null; 820 if (count($this->js_config)) { 821 ksort($this->js_config); 822 $back = "\n<!-- js_config before loading JSfile -->\n"; 823 $b = ""; 824 foreach ($this->js_config as $x => $js) { 825 $b .= "// js $x \n"; 826 foreach ($js as $j) { 827 $b .= "$j\n"; 828 } 829 } 830 if ($wrap === true) { 831 $back .= $this->wrap_js($b); 832 } else { 833 $back .= $b; 834 } 835 } 836 837 return $back; 838 } 839 840 function clear_js($clear_js_files = false) 841 { 842 $this->js = []; 843 $this->jq_onready = []; 844 $this->js_modules = []; 845 if ($clear_js_files) { 846 $this->jsfiles = []; 847 } 848 return $this; 849 } 850 851 function output_js($wrap = true) 852 { 853 // called in tiki.tpl - JS output at end of file now (pre 5.0) 854 global $prefs; 855 856 if ($prefs['javascript_enabled'] == 'n') { 857 return; 858 } 859 860 ksort($this->js); 861 ksort($this->jq_onready); 862 ksort($this->js_modules); 863 864 $back = "\n"; 865 866 if (count($this->js_modules)) { 867 $b = ''; 868 foreach ($this->js_modules as $x => $js) { 869 $b .= "// js $x \n"; 870 foreach ($js as $j) { 871 $b .= "$j\n"; 872 } 873 } 874 if ($wrap === true) { 875 $back .= $this->wrap_js($b, true); 876 } else { 877 $back .= $b; 878 } 879 } 880 881 if (count($this->js)) { 882 $b = ''; 883 foreach ($this->js as $x => $js) { 884 $b .= "// js $x \n"; 885 foreach ($js as $j) { 886 $b .= "$j\n"; 887 } 888 } 889 if ($wrap === true) { 890 $back .= $this->wrap_js($b); 891 } else { 892 $back .= $b; 893 } 894 } 895 896 if (count($this->jq_onready)) { 897 $b = '$(document).ready(function(){' . "\n"; 898 foreach ($this->jq_onready as $x => $js) { 899 $b .= "// jq_onready $x \n"; 900 foreach ($js as $j) { 901 $b .= "$j\n"; 902 } 903 } 904 $b .= "});\n"; 905 if ($wrap === true) { 906 $back .= $this->wrap_js($b); 907 } else { 908 $back .= $b; 909 } 910 } 911 912 return $back; 913 } 914 915 /** 916 * Gets JavaScript and jQuery scripts as an array (for AJAX) 917 * @return array[strings] 918 */ 919 function getJs() 920 { 921 922 ksort($this->js); 923 ksort($this->jq_onready); 924 $out = []; 925 926 if (count($this->js)) { 927 foreach ($this->js as $x => $js) { 928 foreach ($js as $j) { 929 $out[] = "$j\n"; 930 } 931 } 932 } 933 if (count($this->jq_onready)) { 934 $b = '$(document).ready(function(){' . "\n"; 935 foreach ($this->jq_onready as $x => $js) { 936 $b .= "// jq_onready $x \n"; 937 foreach ($js as $j) { 938 $b .= "$j\n"; 939 } 940 } 941 $b .= "}) /* end on ready */;\n"; 942 $out[] = $b; 943 } 944 return $out; 945 } 946 947 948 function wrap_js($inJs, $module = false) 949 { 950 if ($module) { 951 return "<script type=\"module\" name=\"App\">\n" . $inJs . "\n</script>\n"; 952 } else { 953 return "<script type=\"text/javascript\">\n<!--//--><![CDATA[//><!--\n" . $inJs . "//--><!]]>\n</script>\n"; 954 } 955 956 } 957 958 /** 959 * Get JavaScript tags from html source - used for AJAX responses and cached pages 960 * 961 * @param string $html - source to search for JavaScript 962 * @param bool $switch_fn_definition - if set converts 'function fName ()' to 'fName = function()' for AJAX 963 * @param bool $isFiles - if set true, get external scripts. If set to false, get inline scripts. If true, the external script tags's src attributes are returned as an array. 964 * 965 * @return array of JavaScript strings 966 */ 967 function getJsFromHTML($html, $switch_fn_definition = false, $isFiles = false) 968 { 969 $jsarr = []; 970 $js_script = []; 971 972 preg_match_all('/(?:<script.*type=[\'"]?text\/javascript[\'"]?.*>\s*?)(.*)(?:\s*<\/script>)/Umis', $html, $jsarr); 973 if ($isFiles == false) { 974 if (count($jsarr) > 1 && is_array($jsarr[1]) && count($jsarr[1]) > 0) { 975 $js = preg_replace('/\s*?<\!--\/\/--><\!\[CDATA\[\/\/><\!--\s*?/Umis', '', $jsarr[1]); // strip out CDATA XML wrapper if there 976 $js = preg_replace('/\s*?\/\/--><\!\]\]>\s*?/Umis', '', $js); 977 978 if ($switch_fn_definition) { 979 $js = preg_replace('/function (.*)\(/Umis', "$1 = function(", $js); 980 } 981 982 $js_script = array_merge($js_script, $js); 983 } 984 } else { 985 foreach ($jsarr[0] as $key => $tag) { 986 if (empty($jsarr[1][$key])) { //if there was no content in the script, it is a src file 987 //we load the js as a xml element, then look to see if it has a "src" tag, if it does, we push it to array for end back 988 $js = simplexml_load_string($tag); 989 if (! empty($js['src'])) { 990 array_push($js_script, (string)$js['src']); 991 } 992 } 993 } 994 } 995 // this is very probably possible as a single regexp, maybe a preg_replace_callback 996 // but it was stopping the CDATA group being returned (and life's too short ;) 997 // the one below should work afaik but just doesn't! :( 998 // preg_match_all('/<script.*type=[\'"]?text\/javascript[\'"]?.*>(\s*<\!--\/\/--><\!\[CDATA\[\/\/><\!--)?\s*?(.*)(\s*\/\/--><\!\]\]>\s*)?<\/script>/imsU', $html, $js); 999 1000 return array_filter($js_script); 1001 } 1002 1003 function removeJsFromHTML($html) 1004 { 1005 $html = preg_replace('/(?:<script.*type=[\'"]?text\/javascript[\'"]?.*>\s*?)(.*)(?:\s*<\/script>)/Umis', "", $html); 1006 return $html; 1007 } 1008 1009 public function get_all_css_content() 1010 { 1011 $files = $this->collect_css_files(); 1012 $minifier = new MatthiasMullie\Minify\CSS(); 1013 1014 foreach (array_merge($files['screen'], $files['default']) as $file) { 1015 $minifier->add($file); 1016 } 1017 1018 $minified = $minifier->minify(); 1019 1020 return $minified; 1021 } 1022 1023 private function output_css_files() 1024 { 1025 $files = $this->collect_css_files(); 1026 1027 $back = $this->output_css_files_list($files['default'], ''); 1028 $back .= $this->output_css_files_list($files['screen'], 'screen'); 1029 $back .= $this->output_css_files_list($files['print'], 'print'); 1030 return $back; 1031 } 1032 1033 private function output_css_files_list($files, $media = '') 1034 { 1035 global $prefs; 1036 $smarty = TikiLib::lib('smarty'); 1037 $smarty->loadPlugin('smarty_modifier_escape'); 1038 1039 $back = ''; 1040 1041 if ($prefs['tiki_minify_css'] == 'y' && ! empty($files)) { 1042 if ($prefs['tiki_minify_css_single_file'] == 'y') { 1043 $files = $this->get_minified_css_single($files); 1044 } else { 1045 $files = $this->get_minified_css($files); 1046 } 1047 } 1048 1049 foreach ($files as $file) { 1050 $file = $this->convert_cdn($file); 1051 $back .= "<link rel=\"stylesheet\" href=\"" . smarty_modifier_escape($file) . "\" type=\"text/css\""; 1052 if (! empty($media)) { 1053 $back .= " media=\"" . smarty_modifier_escape($media) . "\""; 1054 } 1055 $back .= ">\n"; 1056 } 1057 1058 return $back; 1059 } 1060 1061 private function get_minified_css($files) 1062 { 1063 global $tikidomainslash; 1064 $out = []; 1065 $publicDirectory = 'temp/public/' . $tikidomainslash; 1066 foreach ($files as $originalFile) { 1067 /* This does not use the same cachelib-based caching strategy as get_minified_css_single() since I could not see any improvement. 1068 I tested on Windows 8 with an HDD and a filesystem-based CacheLib. CacheLibFileSystem::getCached() may be inefficient. The strategy may still improve performance for other setups, such as those using CacheLibMemcache. Chealer 2018-08-31 */ 1069 $fileContentsHash = md5_file($originalFile); 1070 $minimalFilePath = $publicDirectory . "minified_$fileContentsHash.css"; 1071 if (! file_exists($minimalFilePath)) { 1072 (new MatthiasMullie\Minify\CSS($originalFile))->minify($minimalFilePath); 1073 chmod($minimalFilePath, 0644); 1074 } 1075 1076 $out[] = $minimalFilePath; 1077 } 1078 1079 return $out; 1080 } 1081 1082 private function get_minified_css_single($files) 1083 { 1084 global $tikidomainslash; 1085 $cachelib = TikiLib::lib('cache'); 1086 1087 $fileSetHash = md5(serialize($files)); 1088 1089 /* The minimal file's name contains a hash based on the file contents, so that browsers will automatically load changes when files are modified. 1090 However, since that hash is itself costly to create, it is cached server-side. Therefore, client caches will be refreshed when the server-side cache is cleared, after an upgrade for instance. */ 1091 $fileSetContentsHash = $cachelib->getCached($fileSetHash, 'minify_css_contents_by_paths'); 1092 if (! $fileSetContentsHash) { 1093 $fileSetContentsHash = $this->getFilesContentsHash($files); 1094 $cachelib->cacheItem($fileSetHash, $fileSetContentsHash, 'minify_css_contents_by_paths'); 1095 } 1096 $minimalFilePath = 'temp/public/' . $tikidomainslash . "minified_$fileSetContentsHash.css"; 1097 1098 if (! file_exists($minimalFilePath)) { 1099 $minifier = new MatthiasMullie\Minify\CSS(); 1100 1101 foreach ($files as $originalFile) { 1102 $minifier->add($originalFile); 1103 } 1104 1105 $minifier->minify($minimalFilePath); 1106 chmod($minimalFilePath, 0644); 1107 } 1108 1109 return [ $minimalFilePath ]; 1110 } 1111 1112 public function minify_css($file) 1113 { 1114 $minifier = new MatthiasMullie\Minify\CSS($file); 1115 return $minifier->minify(); 1116 } 1117 1118 private function collect_css_files() 1119 { 1120 global $tikipath; 1121 1122 $files = [ 1123 'default' => [], 1124 'screen' => [], 1125 'print' => [], 1126 ]; 1127 1128 $pushFile = function ($section, $file) use (& $files) { 1129 global $prefs; 1130 $files[$section][] = $file; 1131 1132 if ($prefs['feature_bidi'] == 'y') { 1133 $rtl = str_replace('.css', '', $file) . '-rtl.css'; 1134 1135 if (file_exists($rtl)) { 1136 $files[$section][] = $rtl; 1137 } 1138 } 1139 }; 1140 1141 foreach ($this->cssfiles as $x => $cssf) { 1142 foreach ($cssf as $cf) { 1143 $cfprint = str_replace('.css', '', $cf) . '-print.css'; 1144 if (! file_exists($tikipath . $cfprint)) { 1145 $pushFile('default', $cf); 1146 } else { 1147 $pushFile('screen', $cf); 1148 $pushFile('print', $cfprint); 1149 } 1150 } 1151 } 1152 return $files; 1153 } 1154 1155 function get_css_files() 1156 { 1157 $files = $this->collect_css_files(); 1158 1159 return array_merge($files['default'], $files['screen']); 1160 } 1161 1162 // TODO compile_custom_scss function here 1163 1164 function add_map() 1165 { 1166 global $prefs; 1167 1168 if ($prefs['geo_enabled'] != 'y') { 1169 return; 1170 } 1171 1172 $tikilib = TikiLib::lib('tiki'); 1173 $enabled = $tikilib->get_preference('geo_tilesets', ['openstreetmap'], true); 1174 1175 $google = array_intersect(['google_street', 'google_physical', 'google_satellite', 'google_hybrid'], $enabled); 1176 if (count($google) > 0 || $prefs['geo_google_streetview'] == 'y') { 1177 $args = [ 1178 'v' => '3', 1179 ]; 1180 1181 if (! empty($prefs['gmap_key'])) { 1182 $args['key'] = $prefs['gmap_key']; 1183 } 1184 1185 $url = $tikilib->httpScheme() . '://maps.googleapis.com/maps/api/js?' . http_build_query($args, '', '&'); 1186 1187 if (TikiLib::lib('access')->is_xml_http_request()) { 1188 $this->add_js('function loadScript() { 1189var script = document.createElement("script"); 1190 script.type = "text/javascript"; 1191 script.src = "' . $url . '"; 1192 document.body.appendChild(script); 1193} 1194 1195window.onload = loadScript;'); 1196 } else { 1197 $this->add_jsfile_external($url, true); 1198 } 1199 } 1200 1201 /* Needs additional testing 1202 $visual = array_intersect(array('visualearth_road', 'visualearth_aerial', 'visualearth_hybrid'), $enabled); 1203 if (count($visual) > 0) { 1204 $this->add_jsfile_cdn('http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.1'); 1205 } 1206 */ 1207 1208 if ($prefs['geo_openlayers_version'] === 'ol3') { 1209 $this->add_jsfile_external('vendor_bundled/vendor/openlayers/openlayers/ol.js', true) 1210 ->add_cssfile('vendor_bundled/vendor/openlayers/openlayers/ol.css') 1211 ->add_jsfile_external('vendor_bundled/vendor/walkermatt/ol-layerswitcher/dist/ol-layerswitcher.js') 1212 ->add_cssfile('vendor_bundled/vendor/walkermatt/ol-layerswitcher/src/ol-layerswitcher.css') 1213 ->add_js( 1214 '' 1215 ); 1216 } else { 1217 $this->add_jsfile_external('lib/openlayers/OpenLayers.js', true); 1218 } 1219 1220 $this->add_js( 1221 '$(".map-container:not(.done)") 1222 .addClass("done") 1223 .visible(function() { 1224 $(this).createMap(); 1225 });' 1226 ); 1227 1228 return $this; 1229 } 1230 1231 1232 function __toString() 1233 { 1234 return ''; 1235 } 1236} 1237