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