You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1240 lines
44 KiB

<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$
/**
* Add Javascript and CSS to output
* Javascript and CSS can be added:
* - as files (filename including relative path to tiki root)
* - as scripts (string)
* - 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')
* Note: there are 2 prefs to add additional cdns. one for http and one for https.
*
* To maintain the order of loading Javascript and to allow minifying, the following "ranks" are supported:
* '10dynamic': loaded first to allow minification of the other ranks. Usually module and plugin descriptions.
* '20cdn' : loaded after 'dynamic', no minification possible // main libs like jquery from jquery/google cdn (no user cdns)
* '30dependency': loaded after 'cdn', minification possible // main libs like jquery, codemirror
* '40external': loaded after 'dependency', minification possible // custom libs that require main libs
* '50standard': loaded after 'external', minification possible // standard js that might require main / custom libs
* '60late': loaded after 'standard', minification possible // page specific js
* Note: this rank is activated in tiki-setup.php to separate page specific JS from common JS
* So any JS loaded after tiki-setup.php that has no rank 'external' is put into 'late'.
* If minification is activated for late, any new combination of late files will be created automatically if needed.
* When using user specific CDNs AND minification for late is enabled, any possible minified file must be available via that CDN!
*
* The order of files within each section will be maintained. What adds first will be processed first.
*
* Note: cdns (google/jquery, not user cdns), files and scripts (strings) will be handled separately.
*
* To add JS, the following methods are available.
*
*
* Methods to add JS files:
* If $skip_minify == true, the file will not be processed for further minification.
* This could be used to avoid screwing up the JS file in the rare case minification on that particular file does not work.
* It will however be concatenated to a single JS file.
* add_jsfile_cdn($url) - add a JS File from a CDN
* add_jsfile_dependency($filename, $skip_minify) - add a JS File to the section dependency
* add_jsfile_external($filename, $skip_minify) - add a JS File to the section external
* add_jsfile($filename, $skip_minify) - add a JS File to the section standard
* add_jsfile_late($filename, $skip_minify) - add a JS File to the section late
*
* These functions allow to add JS as scripts/strings. No minification on them:
* add_js($script, $rank) - add JS as string
* add_jq_onready($script, $rank) - add JS as string and add to the onready event
* add_js_config($script, $rank) - add JS that usually represents a config object
*
* @TODO CSS handling
*
*/
class HeaderLib
{
public $title;
/**
* Array of js files arrays or js urls arrays to load
* key = rank, value = array of filenames with relative path or urls
* Some ranks have special meanings: (note ranks are array keys in the same array)
* @var array()
*/
public $jsfiles;
/**
* Array of js files that are already minified or should not be minified
* Filled when adding jsfiles and setting the $skip_minify param to true
* key = filename with relative path
* @var array
*/
public $skip_minify;
/**
* Array of JS scripts arrays as strings to load
* key = rank (load order), value = array of scripts.
* js[$rank][] = $script;
* @var array
*/
public $js;
/**
* Array of JS Scripts arrays as string that act as config
* Usually created dynamically
* js_config[$rank][] = $script;
* @var array
*/
public $js_config;
/**
* Array of JS Scripts arrays as string that should be called onReady().
* Key = rank (load order), value = array of scripts.
* jq_onready[$rank][] = $script;
* @var array
*/
public $jq_onready;
/**
* Array of JS Scripts arrays as string that should be embedded as modules
* Key = rank (load order), value = array of scripts.
* jq_modules[$rank][] = $script;
* @var array
*/
public $js_modules;
public $cssfiles;
public $css;
public $rssfeeds;
public $metatags;
public $linktags;
/* If set to true, any js added through add_jsfile() that has not rank 'external' will be put to rank 'late'
* Only set once in tiki-setup.php to separate wiki page specific js from common js.
* @var boolean
*/
public $forceJsRankLate;
public $jquery_version = '3.5.1';
public $jqueryui_version = '1.12.1';
public $jquerymigrate_version = '3.0.0';
public function __construct()
{
$smarty = TikiLib::lib('smarty');
$smarty->assign('headerlib', $this);
$this->title = '';
$this->jsfiles = [];
$this->skip_minify = [];
$this->js = [];
$this->js_config = [];
$this->jq_onready = [];
$this->js_modules = [];
$this->cssfiles = [];
$this->css = [];
$this->rssfeeds = [];
$this->metatags = [];
$this->rawhtml = '';
$this->forceJsRankLate = false;
}
/**
* user cdn and feature multi_cdn see r46854
* @param string $file
* @param string $rank
* @return string $file
*/
public function convert_cdn($file, $rank = null)
{
global $prefs, $tikiroot;
// using this method, also reverse proxy / ssl offloading will continue to work
$httpScheme = Tikilib::httpScheme();
$https_mode = ($httpScheme == 'https') ? true : false;
$cdn_ssl_uri = array_filter(preg_split('/\s+/', $prefs['tiki_cdn_ssl']));
$cdn_uri = array_filter(preg_split('/\s+/', $prefs['tiki_cdn']));
if ($https_mode && ! empty($cdn_ssl_uri)) {
$cdn_pref = &$cdn_ssl_uri;
} elseif (! empty($cdn_uri)) {
$cdn_pref = &$cdn_uri;
}
// feature multi_cdn see r46854 - quote from commit:
// filename hash is used to select/assign one CDN URI from the list.
// It ensure a same file will always point/use the same CDN and ensure proper caching.
if (! empty($cdn_pref) && 'http' != substr($file, 0, 4) && $rank !== 'dynamic') {
$index = hexdec(hash("crc32b", $file)) % count($cdn_pref);
$file = $cdn_pref[$index] . $tikiroot . $file;
}
return $file;
}
public function set_title($string)
{
$this->title = urlencode($string);
}
/**
* Add a js url from this tiki instance to top priority load order.
* These are usually dynamic created js scripts for configuration, module settings etc.
* Urls added here will not be further processed (like minified or put into a single file)
* @param string $url - relative url to this tiki instance
* @return HeaderLib Current object
*/
public function add_jsfile_dynamic($url)
{
$this->add_jsfile_by_rank($url, '10dynamic', true);
return $this;
}
/**
* Add a js url to top priority load order. That url must be loaded from an external source.
* These are usually libraries like jquery that are loaded from a cdn = content delivery network.
* Urls added here will not be further processed (like minified or put into a single file)
*
* N.B. skip_minify needs to be set to true here for when tiki_minify_late_js_files is active
* and cdn files are added after page setup by plugins etc
*
* @param string $url - absolute url including http/https
* @return HeaderLib Current object
*/
public function add_jsfile_cdn($url)
{
$this->add_jsfile_by_rank($url, '20cdn', true);
return $this;
}
/**
* Add a js file to top priority load order, right after cdns and dynamics. That file must not be loaded from an external source.
* Theses are usually libraries like jquery or codemirror, so files where other js file depend on.
* Depending on prefs, it could be minified and put into a single js file.
* @param string $file with path relative to tiki dir
* @param bool $skip_minify true if the file must not be minified, false if it can
* @return HeaderLib Current object
*/
public function add_jsfile_dependency($file, $skip_minify = false)
{
$this->add_jsfile_by_rank($file, '30dependency', $skip_minify);
return $this;
}
/**
* Add a js file to load after dependency . That file must not be loaded from an external source.
* Theses are usually custom libraries like raphael, gaffle etc.
* Depending on prefs, it could be minified and put into a single js file.
* @param string $filename with path relative to tiki dir
* @param bool $skip_minify true if the file must not be minified, false if it can
* @return HeaderLib Current object
*/
public function add_jsfile_external($file, $skip_minify = false)
{
$this->add_jsfile_by_rank($file, '40external', $skip_minify);
return $this;
}
/**
* Adds a js file to load after external. That file must not be loaded from an external source.
* Depending on prefs, it could be minified and also put into a single js file
* @param string $file - path relative to tiki dir
* @param bool $skip_minify true if the file must not be minified, false if it can
* @return HeaderLib Current object
*/
public function add_jsfile($file, $skip_minify = false)
{
$this->add_jsfile_by_rank($file, '50standard', $skip_minify);
return $this;
}
/**
* Add a js file to load after standard . That file must not be loaded from an external source.
* Use this method to add page specific js files. They will be minified separately.
* @see $this->forceJsRankLate()
* Depending on prefs, it could be minified and put into a single js file.
* @param string $filename with path relative to tiki dir
* @param bool $skip_minify true if the file must not be minified, false if it can
* @return HeaderLib Current object
*/
public function add_jsfile_late($file, $skip_minify = false)
{
$this->add_jsfile_by_rank($file, '60late', $skip_minify);
return $this;
}
/**
* Add a jf file by rank. Do not use this function directly!
* Only reason that it is public, is for access from lib/core/tiki/PageCache.php
* @param string $file
* @param string $rank
* @param bool $skip_minify true if the file must not be minified, false if it can
* @return HeaderLib Current object
*/
public function add_jsfile_by_rank($file, $rank, $skip_minify = false)
{
// if js is added after tiki-setup.php is run, add those js files to 'late'
// need to check whether this is really needed
if ($this->forceJsRankLate == true && $rank !== '40external') {
$rank = '60late';
}
if (empty($this->jsfiles[$rank]) or ! in_array($file, $this->jsfiles[$rank])) {
$this->jsfiles[$rank][] = $file;
if ($skip_minify) {
$this->skip_minify[$file] = $skip_minify;
}
}
return $this;
}
public function drop_jsfile($file)
{
$out = [];
foreach ($this->jsfiles as $rank => $data) {
foreach ($data as $f) {
if ($f != $file) {
$out[$rank][] = $f;
}
}
}
$this->jsfiles = $out;
return $this;
}
/**
* Add js that works as config. Usually created dynamically.
* @param string $script
* @param integer $rank - loadorder optional, default 0
* @return HeaderLib Current object
*/
public function add_js_config($script, $rank = 0)
{
if (empty($this->js_config[$rank]) or ! in_array($script, $this->js_config[$rank])) {
$this->js_config[$rank][] = $script;
}
return $this;
}
/**
* JS scripts to add as string
* @param string $script
* @param integer $rank loadorder optional, default = 0
* @return HeaderLib Current object
*/
public function add_js($script, $rank = 0)
{
if (empty($this->js[$rank]) or ! in_array($script, $this->js[$rank])) {
$this->js[$rank][] = $script;
}
return $this;
}
/**
* Adds lines or blocks of JQuery JavaScript to $(document).ready handler
* @param string $script - Script to execute
* @param number $rank - load order (default=0)
* @return HeaderLib Current object
*/
public function add_jq_onready($script, $rank = 0)
{
if (empty($this->jq_onready[$rank]) or ! in_array($script, $this->jq_onready[$rank])) {
$this->jq_onready[$rank][] = $script;
}
return $this;
}
/**
* Adds a javascript module
*
* @param string $script
* @param int $rank
*
* @return $this
*/
public function add_js_module($script, $rank = 0)
{
if (empty($this->js_modules[$rank]) or ! in_array($script, $this->js_modules[$rank])) {
$this->js_modules[$rank][] = $script;
}
return $this;
}
public function add_cssfile($file, $rank = 0)
{
if ((empty($this->cssfiles[$rank]) or ! in_array($file, $this->cssfiles[$rank])) && ! empty($file)) {
$this->cssfiles[$rank][] = $file;
}
return $this;
}
public function replace_cssfile($old, $new, $rank)
{
foreach ($this->cssfiles[$rank] as $i => $css) {
if ($css == $old) {
$this->cssfiles[$rank][$i] = $new;
break;
}
}
return $this;
}
public function drop_cssfile($file)
{
$out = [];
foreach ($this->cssfiles as $rank => $data) {
foreach ($data as $f) {
if ($f != $file) {
$out[$rank][] = $f;
}
}
}
$this->cssfiles = $out;
return $this;
}
public function add_css($rules, $rank = 0)
{
if (empty($this->css[$rank]) or ! in_array($rules, $this->css[$rank])) {
$this->css[$rank][] = $rules;
}
return $this;
}
public function add_rssfeed($href, $title, $rank = 0)
{
if (empty($this->rssfeeds[$rank]) or ! in_array($href, array_keys($this->rssfeeds[$rank]))) {
$this->rssfeeds[$rank][$href] = $title;
}
return $this;
}
public function add_meta($tag, $value)
{
$tag = addslashes($tag);
$this->metatags[$tag] = $value;
return $this;
}
public function add_rawhtml($tags)
{
$this->rawhtml = $tags;
return $this;
}
public function add_link($rel, $href, $sizes = '', $type = '', $color = '')
{
$this->linktags[$href]['href'] = $href;
$this->linktags[$href]['rel'] = $rel;
if ($sizes) {
$this->linktags[$href]['sizes'] = $sizes;
}
if ($type) {
$this->linktags[$href]['type'] = $type;
}
if ($color) {
$this->linktags[$href]['color'] = $color;
}
return $this;
}
public function output_headers()
{
$smarty = TikiLib::lib('smarty');
$smarty->loadPlugin('smarty_modifier_escape');
ksort($this->cssfiles);
ksort($this->css);
ksort($this->rssfeeds);
$back = "\n";
if ($this->title) {
$back = '<title>' . smarty_modifier_escape($this->title) . "</title>\n\n";
}
if ($this->rawhtml) {
$back .= $this->rawhtml;
}
if (count($this->metatags)) {
foreach ($this->metatags as $n => $m) {
// check if the meta name starts with OpenGraph protocol prefix and use property instead of name if true
$nameattrib = preg_match('/^og\:/', $n) ? 'property' : 'name';
$back .= '<meta ' . $nameattrib . '="' . smarty_modifier_escape($n) . '" content="' . smarty_modifier_escape($m) . "\">\n";
}
$back .= "\n";
}
if (! is_null($this->linktags) && count($this->linktags)) {
foreach ($this->linktags as $link) {
$back .= '<link rel="' . $link['rel'] . '" href="' . $link['href'] . '"';
if (isset($link['sizes'])) {
$back .= ' sizes="' . $link['sizes'] . '"' ;
}
if (isset($link['type'])) {
$back .= ' type="' . $link['type'] . '"' ;
}
if (isset($link['color'])) {
$back .= ' color="' . $link['color'] . '"' ;
}
$back .= ">\n";
}
}
$back .= $this->output_css_files();
if (count($this->css)) {
$back .= "<style type=\"text/css\"><!--\n";
foreach ($this->css as $x => $css) {
$back .= "/* css $x */\n";
foreach ($css as $c) {
$back .= "$c\n";
}
}
$back .= "-->\n</style>\n";
}
if (count($this->rssfeeds)) {
foreach ($this->rssfeeds as $x => $rssf) {
$back .= "<!-- rss $x -->\n";
foreach ($rssf as $rsstitle => $rssurl) {
$back .= "<link rel=\"alternate\" type=\"application/rss+xml\" title=\"" . smarty_modifier_escape($this->convert_cdn($rsstitle)) . "\" href=\"" . smarty_modifier_escape($rssurl) . "\">\n";
}
}
$back .= "\n";
}
return $back;
}
/**
* Force JS Files being added after tiki-setup.php is done to the rank/loadorder 'late' if rank is not 'external'.
* Used to separate page specific JS Files from the rest.
* @return HeaderLib Current object
*/
public function forceJsRankLate()
{
$this->forceJsRankLate = true;
return $this;
}
/**
* Gets included JavaScript files (for AJAX)
* Used in also lib/wiki/wikilib.php to rebuild the cache if activated
* @return array $jsFiles effectivly used jsfiles in scripttags considering minification / cdns if activated.
*/
public function getJsFilesWithScriptTags()
{
/*
// MISCONCEPTION: user cdns are supposed to work as entire tiki cdns - not user based additional url sources
// check for user defined cdns: prefs: tiki_cdn_ssl, tiki_cdn
// the current prefs ask for complete urls including the scheme-name (http / https)
$httpScheme = Tikilib::httpScheme();
$cdnType = ($httpScheme == 'http') ? 'tiki_cdn' : 'tiki_cdn_ssl';
if (isset($prefs[$cdnType])) {
$customCdns = array_filter(preg_split('/\s+/', $prefs[$cdnType]));
$rank = 'customCdn';
foreach ($customCdns as $entry) {
trim($entry);
if (!empty($entry)) {
$output[$rank] .= "<script type=\"text/javascript\" src=\"".smarty_modifier_escape($entry)."\"></script>\n";
}
}
}
*/
global $prefs;
if ($prefs['javascript_enabled'] == 'n') {
return [];
}
if (count($this->jsfiles) == 0) {
return [];
}
$smarty = TikiLib::lib('smarty');
$smarty->loadPlugin('smarty_modifier_escape');
ksort($this->jsfiles);
$jsfiles = $this->jsfiles;
// array that holds a sorted list for all JS files including script tags in the correct order
$output = [];
// output dynamic and cdn first - they cannot be minified anyway
$ranks = ['10dynamic', '20cdn'];
foreach ($ranks as $rank) {
if (isset($jsfiles[$rank])) {
foreach ($jsfiles[$rank] as $entry) {
$output[] = '<script type="text/javascript" src="' . smarty_modifier_escape($entry) . '"></script>';
}
}
}
// all other ranks could be minified - minification only happens if activated and if the file was not blocked by $skip_minify
// check whether we need to minify. minify also includes to put the minified files into one single file
$minifyActive = isset($prefs['tiki_minify_javascript']) && $prefs['tiki_minify_javascript'] == 'y' ? true : false;
if (! $minifyActive) {
$ranks = ['30dependency', '40external', '50standard', '60late'];
foreach ($ranks as $rank) {
if (isset($jsfiles[$rank])) {
foreach ($jsfiles[$rank] as $entry) {
$entry = $this->convert_cdn($entry, $rank);
$output[] = '<script type="text/javascript" src="' . smarty_modifier_escape($entry) . '"></script>';
}
}
}
} else {
// minify (each set of ranks will be compressed into one file).
// late stuff can vary by page. if we would include it in main, then we get multiple big js files.
// better to accept 2 js request: a big one which rarely changes and small ones that include (page specific) late stuff.
// at the end we could get rid of this pref though
$ranks = ['30dependency', '40external', '50standard'];
$entry = $this->minifyJSFiles($jsfiles, $ranks);
$output[] .= '<script type="text/javascript" src="' . smarty_modifier_escape($entry) . '"></script>';
$minifyLateActive = isset($prefs['tiki_minify_late_js_files']) && $prefs['tiki_minify_late_js_files'] == 'y' ? true : false;
$rank = '60late';
if ($minifyLateActive) {
foreach ($jsfiles[$rank] as $index => $file) {
if ($this->skip_minify[$file] === true) {
$output[] .= '<script type="text/javascript" src="' . smarty_modifier_escape($file) . '"></script>';
unset($jsfiles[$rank][$index]);
}
}
// handling of user defined cdn servers is done inside minifyJSFiles()
$entry = $this->minifyJSFiles($jsfiles, [$rank]);
$output[] .= '<script type="text/javascript" src="' . smarty_modifier_escape($entry) . '"></script>';
} else {
foreach ($jsfiles[$rank] as $entry) {
$output[] = '<script type="text/javascript" src="' . smarty_modifier_escape($entry) . '"></script>';
}
}
}
return $output;
}
/**
* Minify multiple JS files over multiple ranks into one single JS file.
* The file is identified by a hash over the given $jsfiles array and automatically created if needed.
* @param array $allJsfiles array of jsfiles ordered by ranks
* @param array $ranks simple array of ranks that needs to be processed.
* @return string $filename - name and relative path of the final js file.
*/
private function minifyJSFiles($allJsfiles, $ranks)
{
global $tikidomainslash, $prefs;
$cachelib = TikiLib::lib('cache');
$cacheType = 'js_minify_hash';
// build hash to identify minified file based on the _requested_ ranks, NOT on the entire jsfiles array
// $jsfiles contains only those keys defined in $ranks
$jsfiles = array_intersect_key($allJsfiles, array_flip($ranks));
$cacheName = md5(serialize($jsfiles));
// create the minified filename based on the contents of the files, and cache that hash as it's expensive to create
// browsers will automatically load new js if it has changed after the cache has been cleared, after an upgrade for instance
$hash = $cachelib->getCached($cacheName, $cacheType);
if (! $hash) {
$hash = $this->getFilesContentsHash($jsfiles);
$cachelib->cacheItem($cacheName, $hash, $cacheType);
}
$tempDir = 'temp/public/' . $tikidomainslash;
$file = $tempDir . "min_main_" . $hash . ".js";
$cdnFile = $this->convert_cdn($file);
// Check if we are on a user defined CDN and the file exists (if tiki_cdn_check is enabled).
// Note: cdn will only be used if the local minified file exists, this ensures that we run the minification at
// least once locally (covers the case where this instance is also cdn)
if (file_exists($file) && ($file != $cdnFile)) {
$cacheType = 'cdn_minify_check';
if ($prefs['tiki_cdn_check'] === 'y' && ! $cachelib->isCached($cdnFile, $cacheType)) {
$cdnHeaders = get_headers($cdnFile);
if (strpos(current($cdnHeaders), '200') !== false) { // check the file is really there
$cachelib->cacheItem($cdnFile, $cdnHeaders, $cacheType);
}
} else {
return $cdnFile;
}
}
if (file_exists($file)) {
return $file;
}
// file does not exist - create it
$minifiedAll = '';
// show all relevant messages about the JS files on top - will be prepended to the output
$topMsg = "/**** start overview of included js files *****/\n";
foreach ($ranks as $rank) {
// add list of minified js files to output
$topMsg .= "\n/* list of files for rank:$rank */\n";
$topMsg .= '/* ' . print_r($jsfiles[$rank], true) . ' */' . "\n";
foreach ($jsfiles[$rank] as $f) {
// important - some scripts like vendor_bundled/vendor/jquery/plugins/async/jquery.async.js do not terminate their last bits with a ';'
// this is bad practise and that causes issues when putting them all in one file!
$minified = ';';
$msg = '';
// if the name contains not 'min' and that file is not blacklisted for minification assume it is minified
// preferable is to set $skip_minify proper
if (! preg_match('/\bmin\./', $f) && $this->skip_minify[$f] !== true) {
set_time_limit(600);
try {
// remove cache-buster parameters from the end of the filename
$pos = strpos($f, '?');
if ($pos !== false) {
$f = substr($f, 0, $pos);
}
// to optimize processing time for changed js requirements, cache the minified version of each file
$hash = md5($f);
// filename without extension - makes it easier to identify the compressed files if needed.
$prefix = basename($f, '.js');
$minifyFile = $tempDir . "min_s_" . $prefix . "_" . $hash . ".js";
if (file_exists($minifyFile)) {
$temp = file_get_contents($minifyFile);
} else {
// if the file does not exist MatthiasMullie\Minify takes the input to be the file content
// which causes js errors and can break the whole site
if (! file_exists($f)) {
Feedback::error(tr('JavaScript file "%0" cannot be found so will not be minified.', $f));
throw new Exception('File not found');
}
$minifier = new MatthiasMullie\Minify\JS($f);
$temp = $minifier->minify($minifyFile);
chmod($minifyFile, 0644);
}
$msg .= "\n/* rank:$rank - minify:ok. $f */\n";
$topMsg .= $msg;
$minified .= $msg;
$minified .= $temp;
} catch (Exception $e) {
$content = file_get_contents($f);
$error = $e->getMessage();
$msg .= "\n/* rank:$rank - minify:error ($error) - adding raw file. $f */\n";
$topMsg .= $msg;
$minified .= $msg;
$minified .= $content;
}
} else {
$content = file_get_contents($f);
$msg .= "\n/* rank:$rank - minify:disabled - adding raw file. $f */\n";
$topMsg .= $msg;
$minified .= $msg;
$minified .= $content;
}
$minifiedAll .= $minified;
}
}
$topMsg .= "\n/**** end overview of included js files *****/\n";
file_put_contents($file, $topMsg . $minifiedAll);
chmod($file, 0644);
return $file;
}
/**
* Calculate a hash based on the contents of files recursively
*
* @param array $files multidimensional array of file paths to minify/hash
* @param string $hash
* @return string hash based on contents of the files
*/
private function getFilesContentsHash(array $files, &$hash = '')
{
foreach ($files as $file) {
if (is_array($file)) {
$hash .= $this->getFilesContentsHash($file, $hash);
} else {
$hash .= md5_file($file);
}
}
return md5($hash);
}
/**
* Output script tags for all javascript files being used.
* If minification is activated, file based JS (so not from a CDN) will be minified und put into one single file
* @return string $jsScriptTags
*/
public function output_js_files()
{
// we get one sorted array with script tags
$js_files = $this->getJsFilesWithScriptTags();
$output = '';
foreach ($js_files as $entry) {
$output .= "\n$entry";
}
return $output;
}
public function output_js_config($wrap = true)
{
global $prefs;
if ($prefs['javascript_enabled'] == 'n') {
return;
}
$back = null;
if (count($this->js_config)) {
ksort($this->js_config);
$back = "\n<!-- js_config before loading JSfile -->\n";
$b = "";
foreach ($this->js_config as $x => $js) {
$b .= "// js $x \n";
foreach ($js as $j) {
$b .= "$j\n";
}
}
if ($wrap === true) {
$back .= $this->wrap_js($b);
} else {
$back .= $b;
}
}
return $back;
}
public function clear_js($clear_js_files = false)
{
$this->js = [];
$this->jq_onready = [];
$this->js_modules = [];
if ($clear_js_files) {
$this->jsfiles = [];
}
return $this;
}
public function output_js($wrap = true)
{
// called in tiki.tpl - JS output at end of file now (pre 5.0)
global $prefs;
if ($prefs['javascript_enabled'] == 'n') {
return;
}
ksort($this->js);
ksort($this->jq_onready);
ksort($this->js_modules);
$back = "\n";
if (count($this->js_modules)) {
$b = '';
foreach ($this->js_modules as $x => $js) {
$b .= "// js $x \n";
foreach ($js as $j) {
$b .= "$j\n";
}
}
if ($wrap === true) {
$back .= $this->wrap_js($b, true);
} else {
$back .= $b;
}
}
if (count($this->js)) {
$b = '';
foreach ($this->js as $x => $js) {
$b .= "// js $x \n";
foreach ($js as $j) {
$b .= "$j\n";
}
}
if ($wrap === true) {
$back .= $this->wrap_js($b);
} else {
$back .= $b;
}
}
if (count($this->jq_onready)) {
$b = '$(document).ready(function(){' . "\n";
foreach ($this->jq_onready as $x => $js) {
$b .= "// jq_onready $x \n";
foreach ($js as $j) {
$b .= "$j\n";
}
}
$b .= "});\n";
if ($wrap === true) {
$back .= $this->wrap_js($b);
} else {
$back .= $b;
}
}
return $back;
}
/**
* Gets JavaScript and jQuery scripts as an array (for AJAX)
* @return array[strings]
*/
public function getJs()
{
ksort($this->js);
ksort($this->jq_onready);
$out = [];
if (count($this->js)) {
foreach ($this->js as $x => $js) {
foreach ($js as $j) {
$out[] = "$j\n";
}
}
}
if (count($this->jq_onready)) {
$b = '$(document).ready(function(){' . "\n";
foreach ($this->jq_onready as $x => $js) {
$b .= "// jq_onready $x \n";
foreach ($js as $j) {
$b .= "$j\n";
}
}
$b .= "}) /* end on ready */;\n";
$out[] = $b;
}
return $out;
}
public function wrap_js($inJs, $module = false)
{
if ($module) {
return "<script type=\"module\" name=\"App\">\n" . $inJs . "\n</script>\n";
} else {
return "<script type=\"text/javascript\">\n<!--//--><![CDATA[//><!--\n" . $inJs . "//--><!]]>\n</script>\n";
}
}
/**
* Get JavaScript tags from html source - used for AJAX responses and cached pages
*
* @param string $html - source to search for JavaScript
* @param bool $switch_fn_definition - if set converts 'function fName ()' to 'fName = function()' for AJAX
* @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.
*
* @return array of JavaScript strings
*/
public function getJsFromHTML($html, $switch_fn_definition = false, $isFiles = false)
{
$jsarr = [];
$js_script = [];
preg_match_all('/(?:<script.*type=[\'"]?text\/javascript[\'"]?.*>\s*?)(.*)(?:\s*<\/script>)/Umis', $html, $jsarr);
if ($isFiles == false) {
if (count($jsarr) > 1 && is_array($jsarr[1]) && count($jsarr[1]) > 0) {
$js = preg_replace('/\s*?<\!--\/\/--><\!\[CDATA\[\/\/><\!--\s*?/Umis', '', $jsarr[1]); // strip out CDATA XML wrapper if there
$js = preg_replace('/\s*?\/\/--><\!\]\]>\s*?/Umis', '', $js);
if ($switch_fn_definition) {
$js = preg_replace('/function (.*)\(/Umis', "$1 = function(", $js);
}
$js_script = array_merge($js_script, $js);
}
} else {
foreach ($jsarr[0] as $key => $tag) {
if (empty($jsarr[1][$key])) { //if there was no content in the script, it is a src file
//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
$js = simplexml_load_string($tag);
if (! empty($js['src'])) {
array_push($js_script, (string)$js['src']);
}
}
}
}
// this is very probably possible as a single regexp, maybe a preg_replace_callback
// but it was stopping the CDATA group being returned (and life's too short ;)
// the one below should work afaik but just doesn't! :(
// preg_match_all('/<script.*type=[\'"]?text\/javascript[\'"]?.*>(\s*<\!--\/\/--><\!\[CDATA\[\/\/><\!--)?\s*?(.*)(\s*\/\/--><\!\]\]>\s*)?<\/script>/imsU', $html, $js);
return array_filter($js_script);
}
public function removeJsFromHTML($html)
{
$html = preg_replace('/(?:<script.*type=[\'"]?text\/javascript[\'"]?.*>\s*?)(.*)(?:\s*<\/script>)/Umis', "", $html);
return $html;
}
public function get_all_css_content()
{
$files = $this->collect_css_files();
$minifier = new MatthiasMullie\Minify\CSS();
foreach (array_merge($files['screen'], $files['default']) as $file) {
$minifier->add($file);
}
$minified = $minifier->minify();
return $minified;
}
private function output_css_files()
{
$files = $this->collect_css_files();
$back = $this->output_css_files_list($files['default'], '');
$back .= $this->output_css_files_list($files['screen'], 'screen');
$back .= $this->output_css_files_list($files['print'], 'print');
return $back;
}
private function output_css_files_list($files, $media = '')
{
global $prefs;
$smarty = TikiLib::lib('smarty');
$smarty->loadPlugin('smarty_modifier_escape');
$back = '';
if ($prefs['tiki_minify_css'] == 'y' && ! empty($files)) {
if ($prefs['tiki_minify_css_single_file'] == 'y') {
$files = $this->get_minified_css_single($files);
} else {
$files = $this->get_minified_css($files);
}
}
foreach ($files as $file) {
$file = $this->convert_cdn($file);
$back .= "<link rel=\"stylesheet\" href=\"" . smarty_modifier_escape($file) . "\" type=\"text/css\"";
if (! empty($media)) {
$back .= " media=\"" . smarty_modifier_escape($media) . "\"";
}
$back .= ">\n";
}
return $back;
}
private function get_minified_css($files)
{
global $tikidomainslash;
$out = [];
$publicDirectory = 'temp/public/' . $tikidomainslash;
foreach ($files as $originalFile) {
/* This does not use the same cachelib-based caching strategy as get_minified_css_single() since I could not see any improvement.
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 */
$fileContentsHash = md5_file($originalFile);
$minimalFilePath = $publicDirectory . "minified_$fileContentsHash.css";
if (! file_exists($minimalFilePath)) {
(new MatthiasMullie\Minify\CSS($originalFile))->minify($minimalFilePath);
chmod($minimalFilePath, 0644);
}
$out[] = $minimalFilePath;
}
return $out;
}
private function get_minified_css_single($files)
{
global $tikidomainslash;
$cachelib = TikiLib::lib('cache');
$fileSetHash = md5(serialize($files));
/* The minimal file's name contains a hash based on the file contents, so that browsers will automatically load changes when files are modified.
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. */
$fileSetContentsHash = $cachelib->getCached($fileSetHash, 'minify_css_contents_by_paths');
if (! $fileSetContentsHash) {
$fileSetContentsHash = $this->getFilesContentsHash($files);
$cachelib->cacheItem($fileSetHash, $fileSetContentsHash, 'minify_css_contents_by_paths');
}
$minimalFilePath = 'temp/public/' . $tikidomainslash . "minified_$fileSetContentsHash.css";
if (! file_exists($minimalFilePath)) {
$minifier = new MatthiasMullie\Minify\CSS();
foreach ($files as $originalFile) {
// remove cache-buster parameters from the end of the filename
$pos = strpos($originalFile, '?');
if ($pos !== false) {
$originalFile = substr($originalFile, 0, $pos);
}
$minifier->add($originalFile);
}
$minifier->minify($minimalFilePath);
chmod($minimalFilePath, 0644);
}
return [ $minimalFilePath ];
}
public function minify_css($file)
{
$minifier = new MatthiasMullie\Minify\CSS($file);
return $minifier->minify();
}
private function collect_css_files()
{
global $tikipath;
$files = [
'default' => [],
'screen' => [],
'print' => [],
];
$pushFile = function ($section, $file) use (&$files) {
global $prefs;
$files[$section][] = $file;
if (Language::isRTL()) {
$rtl = str_replace('.css', '', $file) . '-rtl.css';
if (file_exists($rtl)) {
$files[$section][] = $rtl;
}
}
};
foreach ($this->cssfiles as $x => $cssf) {
foreach ($cssf as $cf) {
$cfprint = str_replace('.css', '', $cf) . '-print.css';
if (! file_exists($tikipath . $cfprint)) {
$pushFile('default', $cf);
} else {
$pushFile('screen', $cf);
$pushFile('print', $cfprint);
}
}
}
return $files;
}
public function get_css_files()
{
$files = $this->collect_css_files();
return array_merge($files['default'], $files['screen']);
}
// TODO compile_custom_scss function here
public function add_map()
{
global $prefs;
if ($prefs['geo_enabled'] != 'y') {
return;
}
$tikilib = TikiLib::lib('tiki');
$enabled = $tikilib->get_preference('geo_tilesets', ['openstreetmap'], true);
$google = array_intersect(['google_street', 'google_physical', 'google_satellite', 'google_hybrid'], $enabled);
if (count($google) > 0 || $prefs['geo_google_streetview'] == 'y') {
$args = [
'v' => '3',
];
if (! empty($prefs['gmap_key'])) {
$args['key'] = $prefs['gmap_key'];
}
$url = $tikilib->httpScheme() . '://maps.googleapis.com/maps/api/js?' . http_build_query($args, '', '&');
if (TikiLib::lib('access')->is_xml_http_request()) {
$this->add_js('function loadScript() {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "' . $url . '";
document.body.appendChild(script);
}
window.onload = loadScript;');
} else {
$this->add_jsfile_external($url, true);
}
}
/* Needs additional testing
$visual = array_intersect(array('visualearth_road', 'visualearth_aerial', 'visualearth_hybrid'), $enabled);
if (count($visual) > 0) {
$this->add_jsfile_cdn('http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.1');
}
*/
if ($prefs['geo_openlayers_version'] === 'ol3') {
$this->add_jsfile_external('vendor_bundled/vendor/openlayers/openlayers/ol.js', true)
->add_cssfile('vendor_bundled/vendor/openlayers/openlayers/ol.css')
->add_jsfile_external('vendor_bundled/vendor/walkermatt/ol-layerswitcher/dist/ol-layerswitcher.js')
->add_cssfile('vendor_bundled/vendor/walkermatt/ol-layerswitcher/src/ol-layerswitcher.css')
->add_js(
''
);
} else {
$this->add_jsfile_external('lib/openlayers/OpenLayers.js', true);
}
$this->add_js(
'$(".map-container:not(.done)")
.addClass("done")
.visible(function() {
$(this).createMap();
});'
);
return $this;
}
public function __toString()
{
return '';
}
}