Current Path : /var/www/www-root/data/www.catalog.monolith-realty.ru/bitrix/modules/main/lib/page/ |
Current File : /var/www/www-root/data/www.catalog.monolith-realty.ru/bitrix/modules/main/lib/page/asset.php |
<?php namespace Bitrix\Main\Page; use Bitrix\Main; use Bitrix\Main\IO; use Bitrix\Main\Config\Option; class Asset { private static $instance; /** @var array Contains target list */ private $targetList; /** @var array pointer to current target */ private $target; /** @var array of css files */ private $css = []; /** @var array of js files */ private $js = []; /** @var array of inline string */ private $strings = [ AssetLocation::BEFORE_CSS => [], AssetLocation::AFTER_CSS => [], AssetLocation::AFTER_JS_KERNEL => [], AssetLocation::AFTER_JS => [], ]; /** @var array Information about kernel modules */ private $moduleInfo = ['CSS' => [], 'JS' => []]; private $kernelAsset = ['CSS' => [], 'JS' => []]; private $assetList = ['CSS' => [], 'SOURCE_CSS' => [], 'JS' => [], 'SOURCE_JS' => []]; private $fileList = ['CSS' => [], 'JS' => []]; private $mode = AssetMode::STANDARD; private $ajax; private $xhtmlStyle = '/'; private $optimizeCss = true; private $optimizeJs = true; private $headString = false; private $headScript = false; private $bodyScript = false; private $moveJsToBody = null; private $templateExists = false; private $siteTemplateID = ''; private $templatePath = ''; private $documentRoot = ''; private $dbType = 'MYSQL'; private $assetCSSCnt = 0; private $assetJSCnt = 0; const SOURCE_MAP_TAG = "\n//# sourceMappingURL="; const HEADER_START_TAG = "; /* Start:\""; const HEADER_END_TAG = "\"*/"; const version = 1; private function __construct() { //use self::getInstance() $this->targetList['KERNEL'] = [ 'NAME' => 'KERNEL', 'START' => true, 'CSS_RES' => [], 'JS_RES' => [], 'CSS_LIST' => [], 'JS_LIST' => [], 'STRING_LIST' => [], 'UNIQUE' => true, 'PREFIX' => 'kernel', 'BODY' => false, 'MODE' => AssetMode::ALL ]; $this->targetList['BODY'] = $this->targetList['TEMPLATE'] = $this->targetList['PAGE'] = $this->targetList['KERNEL']; $this->targetList['PAGE']['NAME'] = 'PAGE'; $this->targetList['PAGE']['UNIQUE'] = false; $this->targetList['PAGE']['PREFIX'] = 'page'; $this->targetList['TEMPLATE']['NAME'] = 'TEMPLATE'; $this->targetList['TEMPLATE']['UNIQUE'] = false; $this->targetList['TEMPLATE']['PREFIX'] = 'template'; $this->targetList['BODY']['NAME'] = 'BODY'; $this->targetList['BODY']['UNIQUE'] = false; $this->targetList['BODY']['PREFIX'] = 'body'; /** fix current order of kernel modules */ $this->targetList['KERNEL']['CSS_LIST']['KERNEL_main'] = []; $this->targetList['KERNEL']['JS_LIST']['KERNEL_main'] = []; $this->target = &$this->targetList['TEMPLATE']; $this->documentRoot = Main\Loader::getDocumentRoot(); } /** * Can`t clone this object * @return void */ private function __clone() { //you can't clone it } /** * Singleton instance. * * @return Asset */ public static function getInstance() { if (is_null(self::$instance)) { self::$instance = new Asset(); } return self::$instance; } /** * Set mode for current target. * @param int $mode Set current composite mode. * @return void */ public function setMode($mode = AssetMode::STANDARD) { $this->mode = $mode; } /** * Returns gzip enabled or not. * @return bool|null */ public static function gzipEnabled() { static $gzip = null; if ($gzip === null) { $gzip = ( Option::get('main','compres_css_js_files', 'N') == 'Y' && extension_loaded('zlib') && function_exists('gzopen') ); } return $gzip; } /** * Start optimizing css * @return void */ public function enableOptimizeCss() { $this->optimizeCss = true; } /** * Stop optimizing css * @return void */ public function disableOptimizeCss() { $this->optimizeCss = false; } /** * Start optimizing js * @return void */ public function enableOptimizeJs() { $this->optimizeJs = true; } /** * Stop optimizing js * @return void */ public function disableOptimizeJs() { $this->optimizeJs = false; } /** * @param boolean $value Use xhtml html style. * @return void */ public function setXhtml($value) { $this->xhtmlStyle = ($value === true ? '/':''); } /** * @param integer $value Count of css files showed inline fore IE. * @deprecated * @return void */ public function setMaxCss($value) { } /** * Set ShowHeadString in page or not. * @param boolean $value Set ShowHeadSting is set on page. * @return void */ public function setShowHeadString($value = true) { $this->headString = $value; } /** * Return true if ShowHeadString exist in page. * @return boolean */ public function getShowHeadString() { return $this->headString; } /** * Set ShowHeadScript in page or not. * @param boolean $value Set ShowHeadScript is set on page. * @return void */ public function setShowHeadScript($value = true) { $this->headScript = $value; } /** * Return true if ShowHeadBodyScript exist in page. * @param boolean $value Set ShowHeadBodyScript is set on page. * @return void */ public function setShowBodyScript($value = true) { $this->bodyScript = $value; } /** * Set Ajax mode and restart instance * @return Asset */ public function setAjax() { $newInstance = self::$instance = new Asset(); $newInstance->ajax = true; return $newInstance; } /** * @return string - Return current set name */ public function getTargetName() { return $this->target['NAME']; } /** * @return mixed Return current set */ public function getTarget() { return $this->target; } /** * Temporary fix for update system. Need to delete later. * @param string $id Target ID. * @param integer $mode Composite Mode. * @return boolean */ public function startSet($id = '', $mode = AssetMode::ALL) { return $this->startTarget($id, $mode); } /** * Start new target for asset. * @param string $id Target ID. * @param integer $mode Composite mode. * @return boolean */ public function startTarget($id = '', $mode = AssetMode::ALL) { $id = strtoupper(trim($id)); if ($id == '') { return false; } if ($id == 'TEMPLATE') { $this->templateExists = true; } if ( ($this->target['NAME'] == 'TEMPLATE' || $this->target['NAME'] == 'PAGE') && ($id == 'TEMPLATE' || $id == 'PAGE') ) { $this->target['START'] = false; $this->targetList[$id]['START'] = true; $this->target = &$this->targetList[$id]; } elseif ($id != 'TEMPLATE' && $id != 'PAGE') { if (isset($this->targetList[$id])) { return false; } $this->stopTarget(); $this->targetList[$id] = [ 'NAME' => $id, 'START' => true, 'JS_RES' => [], 'CSS_RES' => [], 'JS_LIST' => [], 'CSS_LIST' => [], 'STRING_LIST' => [], 'BODY' => false, 'UNIQUE' => false, 'MODE' => $mode ]; $this->target = &$this->targetList[$id]; } return true; } /** * Stop current target. * @param string $id Target ID. * @return bool */ public function stopTarget($id = '') { $id = strtoupper(trim($id)); if ($id == 'TEMPLATE') { if($this->target['NAME'] == 'TEMPLATE') { $this->target['START'] = false; $this->target = &$this->targetList['PAGE']; } else { $this->targetList['TEMPLATE']['START'] = false; } } else { if ($this->target['NAME'] == 'TEMPLATE') { return false; } elseif ($this->targetList['TEMPLATE']['START']) { $this->target['START'] = false; $this->target = &$this->targetList['TEMPLATE']; } else { $this->target['START'] = false; $this->target = &$this->targetList['PAGE']; } } return true; } /** * Return information about target assets. * @param string $id Asset ID. * @param mixed $mode Composite mode. * @return array */ public function getAssetInfo($id, $mode) { $id = strtoupper(trim($id)); $emptyData = ['JS' => [], 'BUNDLE_JS' => [], 'CSS' => [], 'BUNDLE_CSS' => [], 'STRINGS' => []]; if (!isset($this->targetList[$id])) { return $emptyData; } static $cacheInfo = [ AssetMode::STANDARD => null, AssetMode::COMPOSITE => null, AssetMode::ALL => null, AssetMode::SPECIAL => null ]; if ($cacheInfo[$mode] === null) { $cacheInfo[$mode] = $emptyData; foreach ($this->strings as $location) { foreach ($location as $item) { if ($mode == $item['MODE']) { $cacheInfo[$mode]['STRINGS'][$item['TARGET'][0]][] = $item['CONTENT']; } } } foreach (['JS', 'CSS'] as $type) { foreach ($this->getTargetList($type) as $set) { $cache = &$cacheInfo[$mode][$type][$set['NAME']]; $cacheFull = &$cacheInfo[$mode]['BUNDLE_'.$type][$set['NAME']]; if (!is_array($cache)) { $cache = []; } if (!is_array($cacheFull)) { $cacheFull = []; } $fileList = $this->fileList[$type][$set['NAME']] ?? []; $targetList = $this->targetList['KERNEL'][$type.'_LIST'][$set['NAME']] ?? []; $items = []; if ($mode === $set['MODE'] && isset($fileList['FILES'])) { $items = $fileList['FILES']; } elseif (isset($fileList['UP_NEW_FILES'])) { $items = $fileList['UP_NEW_FILES']; } if (empty($items)) { continue; } foreach ($items as $item) { $cache[] = $item; if (isset($fileList['FULL_FILES'][$item])) { $cacheFull = array_merge($cacheFull, $fileList['FULL_FILES'][$item]); } if ($set['PARENT_NAME'] == 'KERNEL') { foreach ($targetList['WHERE_USED'] as $target => $tmp) { $cacheInfo[$mode][$type][$target][] = $item; if (isset($fileList['FULL_FILES'][$item])) { if (!isset($cacheInfo[$mode]['BUNDLE_'.$type][$target])) { $cacheInfo[$mode]['BUNDLE_'.$type][$target] = []; } $cacheInfo[$mode]['BUNDLE_'.$type][$target] = array_merge( $cacheInfo[$mode]['BUNDLE_'.$type][$target], $fileList['FULL_FILES'][$item] ); } } } } } } } return [ 'JS' => $cacheInfo[$mode]['JS'][$id] ?? [], 'BUNDLE_JS' => $cacheInfo[$mode]['BUNDLE_JS'][$id] ?? [], 'CSS' => $cacheInfo[$mode]['CSS'][$id] ?? [], 'BUNDLE_CSS' => $cacheInfo[$mode]['BUNDLE_CSS'][$id] ?? [], 'STRINGS' => $cacheInfo[$mode]['STRINGS'][$id] ?? [] ]; } /** * Set composite mode for set. * @param string $id Target ID. * @return boolean */ public function compositeTarget($id = '') { $id = strtoupper(trim($id)); if ($id == '' || !isset($this->targetList[$id])) { return false; } else { $this->targetList[$id]['MODE'] = AssetMode::COMPOSITE; } return true; } /** * Return list of all targets on the page. * @param string $type Target type CSS or JS. * @return array Return set list with subsets. */ public function getTargetList($type = 'CSS') { static $res = ['CSS_LIST' => null, 'JS_LIST' => null]; $key = ($type == 'CSS' ? 'CSS_LIST' : 'JS_LIST'); if ($res[$key] === null) { foreach ($this->targetList as $targetName => $targetInfo) { $res[$key][] = [ 'NAME' => $targetName, 'PARENT_NAME' => $targetName, 'UNIQUE' => $targetInfo['UNIQUE'], 'PREFIX' => ($targetInfo['PREFIX'] ?? ''), 'MODE' => $targetInfo['MODE'], 'MODULE_NAME' => ($targetInfo['MODULE_NAME'] ?? ''), ]; if (!empty($targetInfo[$key])) { foreach ($targetInfo[$key] as $subSetName => $val) { $res[$key][] = [ 'NAME' => $subSetName, 'PARENT_NAME' => $targetName, 'UNIQUE' => ($val['UNIQUE'] ?? ''), 'PREFIX' => ($val['PREFIX'] ?? ''), 'MODE' => ($val['MODE'] ?? 0), 'MODULE_NAME' => ($val['MODULE_NAME'] ?? ''), ]; } } } } return $res[$key]; } /** * Add string asset. * @param string $str Added string. * @param bool $unique Check string for unique. * @param string $location Where string wheel be showed. * @param null $mode Composite mode. * @return boolean */ function addString($str, $unique = false, $location = AssetLocation::AFTER_JS_KERNEL, $mode = null) { if ($str == '') { return false; } if ($unique) { $chkSum = md5($str); $this->strings[$location][$chkSum]['CONTENT'] = $str; $this->strings[$location][$chkSum]['TARGET'][] = $this->getTargetName(); $this->strings[$location][$chkSum]['MODE'] = $mode; } else { $this->strings[$location][] = ['CONTENT' => $str, 'MODE' => $mode, 'TARGET' => [$this->getTargetName()]]; } return true; } /** * Return strings assets. * @param string $location Location. * @return string */ public function getStrings($location = AssetLocation::AFTER_JS_KERNEL) { static $firstExec = true; if ($firstExec) { $this->prepareString(); $firstExec = false; } $res = ''; if ($location == AssetLocation::AFTER_CSS && \CJSCore::IsCoreLoaded()) { $res = "<script>if(!window.BX)window.BX={};if(!window.BX.message)window.BX.message=function(mess){if(typeof mess==='object'){for(let i in mess) {BX.message[i]=mess[i];} return true;}};</script>\n"; } if (isset($this->strings[$location])) { foreach ($this->strings[$location] as $item) { if ($this->mode & $item['MODE']) { $res .= $item['CONTENT']."\n"; } } } return ($res == '') ? '' : $res."\n"; } /** * Add some css to asset. * @param string $path Path to css file. * @param boolean $additional Is additional file. * @return boolean */ public function addCss($path, $additional = false) { if ($path == '') { return false; } $css = $this->getAssetPath($path); $this->css[$css]['TARGET'][] = $this->getTargetName(); $this->css[$css]['ADDITIONAL'] = (isset($this->css[$css]['ADDITIONAL']) && $this->css[$css]['ADDITIONAL'] ? true : $additional); return true; } /** * Add some js to asset. * @param string $path Path to js file. * @param boolean $additional Is additional file. * @return boolean */ public function addJs($path, $additional = false) { if ($path == '') { return false; } $js = $this->getAssetPath($path); $this->js[$js]['TARGET'][] = $this->getTargetName(); $this->js[$js]['ADDITIONAL'] = (isset($this->js[$js]['ADDITIONAL']) && $this->js[$js]['ADDITIONAL'] ? true : $additional); return true; } /** * Replace path to includes in css. * @param string $content Content for replacing path. * @param string $path Path to correct. * @return mixed */ public static function fixCssIncludes($content, $path) { $path = IO\Path::getDirectory($path); $content = preg_replace_callback( '#([;\s:]*(?:url|@import)\s*\(\s*)(\'|"|)(.+?)(\2)\s*\)#si', function ($matches) use ($path) { return $matches[1].Asset::replaceUrlCSS($matches[3], $matches[2], addslashes($path)).")"; }, $content ); $content = preg_replace_callback( '#(\s*@import\s*)([\'"])([^\'"]+)(\2)#si', function ($matches) use ($path) { return $matches[1].Asset::replaceUrlCSS($matches[3], $matches[2], addslashes($path)); }, $content ); return $content; } /** * Group some js modules. * @param string $from Module name for packing. * @param string $to Module name for pack. * @return void */ public function groupJs($from = '', $to = '') { if (empty($from) || empty($to)) { return; } $to = $this->movedJsTo($to); if (array_key_exists($from, $this->moduleInfo['JS'])) { $this->moduleInfo['JS'][$from]['MODULE_ID'] = $to; } else { $this->moduleInfo['JS'][$from] = ['MODULE_ID' => $to, 'FILES_INFO' => false, 'BODY' => false]; } foreach ($this->moduleInfo['JS'] as $moduleID => $moduleInfo) { if ($moduleInfo['MODULE_ID'] == $from) { $this->moduleInfo['JS'][$moduleID]["MODULE_ID"] = $to; } } } /** * Group some css modules. * @param string $from Module name for packing. * @param string $to Module name for pack. * @return void */ public function groupCss($from = '', $to = '') { if (empty($from) || empty($to)) { return; } $to = $this->movedCssTo($to); if (array_key_exists($from, $this->moduleInfo['CSS'])) { $this->moduleInfo['CSS'][$from]['MODULE_ID'] = $to; } else { $this->moduleInfo['CSS'][$from] = ['MODULE_ID' => $to, 'FILES_INFO' => false]; } foreach ($this->moduleInfo['CSS'] as $moduleID => $moduleInfo) { if($moduleInfo['MODULE_ID'] == $from) { $this->moduleInfo['CSS'][$moduleID]["MODULE_ID"] = $to; } } } /** * @param string $to Module name. * @return string Return module name. */ private function movedJsTo($to) { if (isset($this->moduleInfo['JS'][$to]['MODULE_ID']) && $this->moduleInfo['JS'][$to]['MODULE_ID'] != $to) { $to = $this->movedJsTo($this->moduleInfo['JS'][$to]['MODULE_ID']); } return $to; } /** * @param string $to Module name. * @return string Return module name */ private function movedCssTo($to) { if (isset($this->moduleInfo['CSS'][$to]['MODULE_ID']) && $this->moduleInfo['CSS'][$to]['MODULE_ID'] != $to) { $to = $this->movedCssTo($this->moduleInfo['JS'][$to]['MODULE_ID']); } return $to; } /** * Move js kernel module to BODY. * @param string $module Module name. * @return void */ public function moveJs($module = '') { if (empty($module) || $module === "main") { return; } if (array_key_exists($module, $this->moduleInfo['JS'])) { $this->moduleInfo['JS'][$module]['BODY'] = true; } else { $this->moduleInfo['JS'][$module] = ['MODULE_ID' => $module, 'FILES_INFO' => false, 'BODY' => true]; } } /** * Enables or disables the moving of all scripts to the body. * @param bool $flag True or False. * @return void */ public function setJsToBody($flag) { $this->moveJsToBody = (bool)$flag; } /** * @return bool|null */ protected function getJsToBody() { if ($this->moveJsToBody === null) { $this->moveJsToBody = Option::get("main", "move_js_to_body") === "Y" && (!defined("ADMIN_SECTION") || ADMIN_SECTION !== true); } return $this->moveJsToBody; } /** * Moves all scripts in front of </body>. * @param string &$content Page content. * @internal * @return void */ public function moveJsToBody(&$content) { if (!$this->getJsToBody()) { return; } $js = ""; $offset = 0; $newContent = ""; $areas = $this->getScriptAreas($content); foreach ($areas as $area) { if (str_contains($area->attrs, "data-skip-moving") || !self::isValidScriptType($area->attrs)) { continue; } $js .= substr($content, $area->openTagStart, $area->closingTagEnd - $area->openTagStart); $newContent .= substr($content, $offset, $area->openTagStart - $offset); $offset = $area->closingTagEnd; } if ($js === "") { return; } $newContent .= substr($content, $offset); $bodyEnd = strripos($newContent, "</body>"); if ($bodyEnd === false) { $content = $newContent.$js; } else { $content = substr_replace($newContent, $js, $bodyEnd, 0); } } /** * Returns positions of <script>...</script> elements. * @param string $content Page content. * @return array */ private function getScriptAreas($content) { $openTag = "<script"; $closingTag = "</script"; $ending = ">"; $offset = 0; $areas = []; $content = strtolower($content); while (($openTagStart = strpos($content, $openTag, $offset)) !== false) { $endingPos = strpos($content, $ending, $openTagStart); if ($endingPos === false) { break; } $attrsStart = $openTagStart + strlen($openTag); $attrs = substr($content, $attrsStart, $endingPos - $attrsStart); $openTagEnd = $endingPos + strlen($ending); $realClosingTag = $closingTag.$ending; $closingTagStart = strpos($content, $realClosingTag, $openTagEnd); if ($closingTagStart === false) { $offset = $openTagEnd; continue; } $closingTagEnd = $closingTagStart + strlen($realClosingTag); while (isset($content[$closingTagEnd]) && $content[$closingTagEnd] === "\n") { $closingTagEnd++; } $area = new \stdClass(); $area->attrs = $attrs; $area->openTagStart = $openTagStart; $area->openTagEnd = $openTagEnd; $area->closingTagStart = $closingTagStart; $area->closingTagEnd = $closingTagEnd; $areas[] = $area; $offset = $closingTagEnd; } return $areas; } /** * @return bool */ public function canMoveJsToBody() { return $this->getJsToBody() && !Main\Application::getInstance()->getContext()->getRequest()->isAjaxRequest() && !defined("BX_BUFFER_SHUTDOWN"); } /** * * Returns true if <script> has valid mime type. * @param string $attrs Script attributes. * @return bool */ private static function isValidScriptType($attrs) { if ($attrs === "" || !preg_match("/type\\s*=\\s*(['\"]?)(.*?)\\1/i", $attrs, $match)) { return true; } $type = mb_strtolower($match[2]); return $type === "" || $type === "text/javascript" || $type === "application/javascript"; } /** * Replace path to includes in line. * @param string $url Url of css files. * @param string $quote Quote. * @param string $path Path to css. * @return string replaced. */ public static function replaceUrlCss($url, $quote, $path) { if ( str_contains($url, "://") || str_contains($url, "data:") || str_starts_with($url, "#") ) { return $quote.$url.$quote; } $url = trim(stripslashes($url), "'\" \r\n\t"); if (mb_substr($url, 0, 1) == "/") { return $quote.$url.$quote; } return $quote.$path.'/'.$url.$quote; } /** * Remove from file path any parametrs. * @param string $src Path to asset file. * @return string path whithout ?xxx. */ public static function getAssetPath($src) { if (($p = mb_strpos($src, "?")) > 0 && !\CMain::IsExternalLink($src)) { $src = mb_substr($src, 0, $p); } return $src; } /** * @return bool */ public function optimizeCss() { $optimize = $this->optimizeCss && (!defined("ADMIN_SECTION") || ADMIN_SECTION !== true) && Option::get('main', 'optimize_css_files', 'N') == 'Y' && !$this->ajax; return $optimize; } /** * @return bool */ public function optimizeJs() { $optimize = $this->optimizeJs && (!defined("ADMIN_SECTION") || ADMIN_SECTION !== true) && Option::get('main', 'optimize_js_files', 'N') == 'Y' && !$this->ajax; return $optimize; } /** * @return bool|null */ public static function canUseMinifiedAssets() { static $canLoad = null; if ($canLoad === null) { $canLoad = Option::get("main","use_minified_assets", "Y") == "Y"; } return $canLoad; } /** * @return boolean */ public function sliceKernel() { return (!defined("ADMIN_SECTION") || ADMIN_SECTION !== true); } /** * Insert inline css. * @param string $css Content or file name. * @param mixed $label Additional info. * @param boolean $inline Show inline. * @return string */ public function insertCss($css, $label = false, $inline = false) { if ($label === true) { $label = ' data-template-style="true" '; } elseif ($label === false) { $label = ''; } if ($inline) { return "<style type=\"text/css\" {$label}>\n{$css}\n</style>\n"; } else { return "<link href=\"{$css}\" type=\"text/css\" {$label} rel=\"stylesheet\" {$this->xhtmlStyle}>\n"; } } /** * insert inline js. * @param string $js Contet or file path. * @param mixed $label Additional info. * @param boolean $inline Show inline. * @return string */ public function insertJs($js, $label = '', $inline = false) { if ($inline) { return "<script {$label}>\n{$js}\n</script>\n"; } else { return "<script {$label} src=\"$js\"></script>\n"; } } /** * Sets templateID and template path * @return void */ private function setTemplateID() { static $firstExec = true; if ($firstExec && !$this->ajax && (!defined("ADMIN_SECTION") || ADMIN_SECTION !== true)) { if (defined("SITE_TEMPLATE_PREVIEW_MODE")) { $this->templatePath = BX_PERSONAL_ROOT.'/tmp/templates/__bx_preview'; } elseif (defined('SITE_TEMPLATE_ID')) { $this->siteTemplateID = SITE_TEMPLATE_ID; $this->templatePath = SITE_TEMPLATE_PATH; } else { $this->siteTemplateID = '.default'; $this->templatePath = BX_PERSONAL_ROOT."/templates/.default"; } $firstExec = false; } } /** * Add template css to asset * @return void */ private function addTemplateCss() { if ( !$this->ajax && (!defined("ADMIN_SECTION") || ADMIN_SECTION !== true) && $this->templateExists ) { $this->css[$this->templatePath . '/styles.css']['TARGET'][] = 'TEMPLATE'; $this->css[$this->templatePath . '/styles.css']['ADDITIONAL'] = false; $this->css[$this->templatePath . '/template_styles.css']['TARGET'][] = 'TEMPLATE'; $this->css[$this->templatePath . '/template_styles.css']['ADDITIONAL'] = false; } } /** * Prepare string assets. * @return void */ private function prepareString() { foreach ($this->strings as $location => $stringLocation) { foreach ($stringLocation as $key => $item) { /** @var $assetTID - get first target where added asset */ $this->strings[$location][$key]['MODE'] = ($item['MODE'] === null ? $this->targetList[$item['TARGET'][0]]['MODE'] : $item['MODE']); } } } /** * Returns asset's paths. * @param string $assetPath Peth to asset. * @return null|array * @throws Main\ArgumentNullException * @throws Main\ArgumentOutOfRangeException */ private function getAssetPaths($assetPath) { $paths = [$assetPath]; if (self::canUseMinifiedAssets() && preg_match("/(.+)\\.(js|css)$/i", $assetPath, $matches)) { array_unshift($paths, $matches[1].".min.".$matches[2]); } $result = null; $maxMtime = 0; foreach ($paths as $path) { $filePath = $this->documentRoot.$path; if (file_exists($filePath) && ($mtime = filemtime($filePath)) > $maxMtime && filesize($filePath) > 0) { $maxMtime = $mtime; $result = [ "PATH" => $path, "FILE_PATH" => $filePath, "FULL_PATH" => \CUtil::GetAdditionalFileURL($path, true), ]; } } return $result; } /** * Gets asset path. * if allowed use minified assets * @param $sourcePath * @return string|null * @throws Main\ArgumentNullException * @throws Main\ArgumentOutOfRangeException */ public function getFullAssetPath($sourcePath) { $result = $this->getAssetPaths($sourcePath); if (is_array($result)) { return $result["FULL_PATH"]; } if (\CMain::IsExternalLink($sourcePath)) { return $sourcePath; } return null; } /** * Prepare css asset to optimize. * @throws Main\ArgumentNullException * @throws Main\ArgumentOutOfRangeException * @return void */ private function prepareCss() : void { $additional = []; foreach ($this->css as $css => $set) { /** @var $assetTID - get first target where added asset */ $assetTID = $set['ADDITIONAL'] ? 'TEMPLATE' : $set['TARGET'][0]; $cssInfo = [ 'PATH' => $css, 'FULL_PATH' => false, 'FILE_PATH' => false, 'SKIP' => false, 'TARGET' => $assetTID, 'EXTERNAL' => \CMain::IsExternalLink($css), 'ADDITIONAL' => $set['ADDITIONAL'] ]; if ($cssInfo['EXTERNAL']) { if ($set['ADDITIONAL']) { $tmpKey = 'TEMPLATE'; $tmpPrefix = 'template'; } else { $tmpKey = 'KERNEL'; $tmpPrefix = 'kernel'; } $cssInfo['MODULE_ID'] = $this->assetCSSCnt; $cssInfo['TARGET'] = $tmpKey.'_'.$this->assetCSSCnt; $cssInfo['PREFIX'] = $tmpPrefix.'_'.$this->assetCSSCnt; $cssInfo['FULL_PATH'] = $cssInfo['PATH']; $cssInfo['SKIP'] = true; $this->assetCSSCnt++; $this->targetList[$tmpKey]['CSS_LIST'][$cssInfo['TARGET']] = [ 'TARGET' => $cssInfo['TARGET'], 'PREFIX' => $cssInfo['PREFIX'], 'MODE' => $this->targetList[$assetTID]['MODE'], 'UNIQUE' => false, 'WHERE_USED' => [] ]; } else { if (($paths = $this->getAssetPaths($css)) !== null) { $cssInfo["PATH"] = $css; $cssInfo["FILE_PATH"] = $paths["FILE_PATH"]; $cssInfo["FULL_PATH"] = $paths["FULL_PATH"]; } else { unset($this->css[$css]); continue; } $moduleInfo = $this->isKernelCSS($cssInfo['PATH']); if ($moduleInfo) { if ($this->sliceKernel() && $this->optimizeCss() && is_array($moduleInfo)) { $cssInfo['MODULE_ID'] = $moduleInfo['MODULE_ID']; $cssInfo['TARGET'] = 'KERNEL_'.$moduleInfo['MODULE_ID']; $cssInfo['PREFIX'] = 'kernel_'.$moduleInfo['MODULE_ID']; $cssInfo['SKIP'] = $moduleInfo['SKIP'] ?? false; } else { $cssInfo['MODULE_ID'] = $this->assetCSSCnt; $cssInfo['TARGET'] = 'KERNEL_'.$this->assetCSSCnt; $cssInfo['PREFIX'] = 'kernel_'.$this->assetCSSCnt; $cssInfo['SKIP'] = true; $this->assetCSSCnt++; } if (isset($this->targetList['KERNEL']['CSS_LIST'][$cssInfo['TARGET']]['MODE'])) { $this->targetList['KERNEL']['CSS_LIST'][$cssInfo['TARGET']]['MODE'] |= $this->targetList[$assetTID]['MODE']; } else { $this->targetList['KERNEL']['CSS_LIST'][$cssInfo['TARGET']] = [ 'TARGET' => $cssInfo['TARGET'], 'PREFIX' => $cssInfo['PREFIX'], 'MODE' => $set['ADDITIONAL'] ? $this->targetList[$set['TARGET'][0]]['MODE'] : $this->targetList[$assetTID]['MODE'], 'UNIQUE' => true, 'WHERE_USED' => [] ]; } if (is_array($moduleInfo)) { $this->targetList['KERNEL']['CSS_LIST'][$cssInfo['TARGET']]['MODULE_NAME'] = $moduleInfo['MODULE_ID']; } // Add information about sets where used foreach ($set['TARGET'] as $setID) { $this->targetList['KERNEL']['CSS_LIST'][$cssInfo['TARGET']]['WHERE_USED'][$setID] = true; } } elseif (strncmp($cssInfo['PATH'], '/bitrix/js/', 11) != 0 /*||*/ ) { $cssInfo['SKIP'] = !( strncmp($cssInfo['PATH'], '/bitrix/panel/', 14) != 0 && strncmp($cssInfo['PATH'], '/bitrix/themes/', 15) != 0 && strncmp($cssInfo['PATH'], '/bitrix/modules/', 16) != 0 ); } } if ($cssInfo['ADDITIONAL']) { $additional[] = $cssInfo; } else { $this->css[$cssInfo['TARGET']][] = $cssInfo; } unset($this->css[$css]); } foreach ($additional as $cssInfo) { $this->css[$cssInfo['TARGET']][] = $cssInfo; } } /** * Prepare js asset to optimize * @throws Main\ArgumentNullException * @throws Main\ArgumentOutOfRangeException * @return void */ private function prepareJs() { $additional = []; foreach ($this->js as $js => $set) { /** @var $assetTID - get first target where added asset */ $assetTID = $set['ADDITIONAL'] ? 'TEMPLATE' : $set['TARGET'][0]; $jsInfo = [ 'PATH' => $js, 'FULL_PATH' => false, 'FILE_PATH' => false, 'SKIP' => false, 'TARGET' => $assetTID, 'EXTERNAL' => \CMain::IsExternalLink($js), 'BODY' => false, 'ADDITIONAL' => $set['ADDITIONAL'] ]; if ($jsInfo['EXTERNAL']) { if ($set['ADDITIONAL']) { $tmpKey = 'TEMPLATE'; $tmpPrefix = 'template'; } else { $tmpKey = 'KERNEL'; $tmpPrefix = 'kernel'; } $jsInfo['MODULE_ID'] = $this->assetJSCnt; $jsInfo['TARGET'] = $tmpKey.'_'.$this->assetJSCnt; $jsInfo['PREFIX'] = $tmpPrefix.'_'.$this->assetJSCnt; $jsInfo['FULL_PATH'] = $jsInfo['PATH']; $jsInfo['SKIP'] = true; $this->assetJSCnt++; $this->targetList[$tmpKey]['JS_LIST'][$jsInfo['TARGET']] = [ 'TARGET' => $jsInfo['TARGET'], 'PREFIX' => $jsInfo['PREFIX'], 'MODE' => $this->targetList[$assetTID]['MODE'], 'UNIQUE' => false, 'WHERE_USED' => [] ]; } else { if (($paths = $this->getAssetPaths($js)) !== null) { $jsInfo["PATH"] = $js; $jsInfo["FILE_PATH"] = $paths["FILE_PATH"]; $jsInfo["FULL_PATH"] = $paths["FULL_PATH"]; } else { unset($this->js[$js]); continue; } if ($moduleInfo = $this->isKernelJS($jsInfo['PATH'])) { if ($this->sliceKernel() && $this->optimizeJs()) { $jsInfo['MODULE_ID'] = $moduleInfo['MODULE_ID']; $jsInfo['TARGET'] = 'KERNEL_'.$moduleInfo['MODULE_ID']; $jsInfo['PREFIX'] = 'kernel_'.$moduleInfo['MODULE_ID']; $jsInfo['SKIP'] = $moduleInfo['SKIP'] ?? false; $jsInfo['BODY'] = $moduleInfo['BODY']; } else { $jsInfo['MODULE_ID'] = $this->assetJSCnt; $jsInfo['TARGET'] = 'KERNEL_'.$this->assetJSCnt; $jsInfo['PREFIX'] = 'kernel_'.$this->assetJSCnt; $jsInfo['SKIP'] = true; $this->assetJSCnt++; } if ($jsInfo['BODY']) { $this->targetList['BODY']['JS_LIST'][$jsInfo['TARGET']] = [ 'TARGET' => $jsInfo['TARGET'], 'PREFIX' => $jsInfo['PREFIX'], 'MODE' => $this->targetList[$assetTID]['MODE'], 'UNIQUE' => true, 'WHERE_USED' => [] ]; } else { if (isset($this->targetList['KERNEL']['JS_LIST'][$jsInfo['TARGET']]['MODE'])) { $this->targetList['KERNEL']['JS_LIST'][$jsInfo['TARGET']]['MODE'] |= $this->targetList[$assetTID]['MODE']; } else { $this->targetList['KERNEL']['JS_LIST'][$jsInfo['TARGET']] = [ 'TARGET' => $jsInfo['TARGET'], 'PREFIX' => $jsInfo['PREFIX'], 'MODE' => $set['ADDITIONAL'] ? $this->targetList[$set['TARGET'][0]]['MODE'] : $this->targetList[$assetTID]['MODE'], 'UNIQUE' => true, 'WHERE_USED' => [] ]; } } // Add information about sets where used foreach ($set['TARGET'] as $setID) { $this->targetList['KERNEL']['JS_LIST'][$jsInfo['TARGET']]['WHERE_USED'][$setID] = true; } } elseif (strncmp($jsInfo['PATH'], '/bitrix/js/', 11) != 0) { $jsInfo['SKIP'] = !( strncmp($jsInfo['PATH'], '/bitrix/panel/', 14) != 0 && strncmp($jsInfo['PATH'], '/bitrix/themes/', 15) != 0 && strncmp($jsInfo['PATH'], '/bitrix/modules/', 16) != 0 ); } } if ($jsInfo['ADDITIONAL']) { $additional[] = $jsInfo; } else { $this->js[$jsInfo['TARGET']][] = $jsInfo; } unset($this->js[$js]); } // Clean body scripts foreach ($this->targetList['BODY']['JS_LIST'] as $item) { unset($this->targetList['KERNEL']['JS_LIST'][$item['TARGET']]); } foreach ($additional as $jsInfo) { $this->js[$jsInfo['TARGET']][] = $jsInfo; } } /** * Return css or page. * @param int $type Target type. * @return string * @throws Main\ArgumentNullException * @throws Main\ArgumentOutOfRangeException */ public function getCss($type = AssetShowTargetType::ALL) { $res = ''; $additional = []; static $setList = []; static $ajaxList = []; if (empty($setList)) { $this->setTemplateID(); $this->addTemplateCss(); $this->prepareCss(); $setList = $this->getTargetList(); $optimizeCss = $this->optimizeCss(); foreach ($setList as $setInfo) { if (!isset($this->css[$setInfo['NAME']])) { continue; } $data = ''; if (!empty($this->moduleInfo['CSS'][$setInfo['MODULE_NAME']]['DATA'])) { $data = $this->moduleInfo['CSS'][$setInfo['MODULE_NAME']]['DATA']; } $location = ''; if (!empty($this->moduleInfo['CSS'][$setInfo['MODULE_NAME']]['LOCATION'])) { $location = $this->moduleInfo['CSS'][$setInfo['MODULE_NAME']]['LOCATION']; } $resCss = ''; $listAsset = []; $showLabel = ($setInfo['NAME'] == 'TEMPLATE'); foreach ($this->css[$setInfo['NAME']] as $cssFile) { $css = $cssFile['FULL_PATH']; if ($this->ajax) { $this->assetList['CSS'][] = $cssFile['PATH']; $ajaxList[] = $css; } elseif ($cssFile['EXTERNAL']) { $resCss .= $this->insertCss($css, $showLabel); $this->fileList['CSS'][$setInfo['NAME']]['FILES'][] = $css; } elseif ($optimizeCss) { if ($cssFile['SKIP']) { $resCss .= $this->insertCss($css, $showLabel); $this->fileList['CSS'][$setInfo['NAME']]['FILES'][] = $css; } else { $listAsset[] = $cssFile; } } else { $resCss .= $this->insertCss($css, $showLabel); $this->fileList['CSS'][$setInfo['NAME']]['FILES'][] = $css; } } $optimizedAsset = $this->optimizeAsset($listAsset, $setInfo['UNIQUE'], $setInfo['PREFIX'], $setInfo['NAME'], 'css', $data); $resCss = $optimizedAsset['RESULT'].$resCss; if ($location == AssetLocation::AFTER_CSS) { $additional[] = [ 'FILES' => $optimizedAsset['FILES'], 'SOURCE_FILES' => $optimizedAsset['SOURCE_FILES'], 'RES' => $resCss ]; } else { $this->assetList['CSS'][$setInfo['PARENT_NAME']][$setInfo['NAME']] = $optimizedAsset['FILES']; $this->assetList['SOURCE_CSS'][$setInfo['PARENT_NAME']][$setInfo['NAME']] = ($optimizedAsset['SOURCE_FILES'] ?? []); $this->targetList[$setInfo['PARENT_NAME']]['CSS_RES'][$setInfo['NAME']][] = $resCss; } } foreach ($additional as $bundle) { $templateFiles = $this->assetList['CSS']['TEMPLATE']['TEMPLATE'] ?? []; $this->assetList['CSS']['TEMPLATE']['TEMPLATE'] = array_merge($templateFiles, $bundle['FILES']); $this->assetList['SOURCE_CSS']['TEMPLATE']['TEMPLATE'] = array_merge($templateFiles, $bundle['SOURCE_FILES']); $this->targetList['TEMPLATE']['CSS_RES']['TEMPLATE'][] = $bundle['RES']; } unset($additional, $templateFiles, $bundle); } if ($this->ajax && !empty($ajaxList)) { $res .= '<script>'."BX.loadCSS(['".implode("','", $ajaxList)."']);".'</script>'; } if ($type == AssetShowTargetType::KERNEL) { $res .= $this->showAsset($setList, 'css', 'KERNEL'); } elseif ($type == AssetShowTargetType::TEMPLATE_PAGE) { foreach ($this->targetList as $setName => $set) { if ($setName != 'TEMPLATE' && $setName != 'KERNEL') { $res .= $this->showAsset($setList, 'css', $setName); } } $res .= $this->showAsset($setList, 'css', 'TEMPLATE'); } else { foreach ($this->targetList as $setName => $set) { if ($setName != 'TEMPLATE') { $res .= $this->showAsset($setList, 'css', $setName); } } $res .= $this->showAsset($setList, 'css', 'TEMPLATE'); } return $res; } /** * Return JS page assets. * @param int $type Target type. * @return string * @throws Main\ArgumentNullException * @throws Main\ArgumentOutOfRangeException */ function getJs($type = AssetShowTargetType::ALL) { static $setList = []; $res = ''; $type = (int) $type; $type = (($type == AssetShowTargetType::KERNEL && $this->headString && !$this->headScript) ? AssetShowTargetType::ALL : $type); $optimize = $this->optimizeJs(); if (empty($setList)) { $this->prepareJs(); $setList = $this->getTargetList('JS'); foreach ($setList as $setInfo) { if (!isset($this->js[$setInfo['NAME']])) { continue; } $data = ''; if (!empty($this->moduleInfo['JS'][$setInfo['MODULE_NAME']]['DATA'])) { $data = $this->moduleInfo['JS'][$setInfo['MODULE_NAME']]['DATA']; } $resJs = ''; $listAsset = []; foreach ($this->js[$setInfo['NAME']] as $jsFile) { $js = $jsFile['FULL_PATH']; if ($optimize) { if ($jsFile['SKIP']) { $this->fileList['JS'][$setInfo['NAME']]['FILES'][] = $js; $resJs .= "<script src=\"{$js}\"></script>\n"; } else { $listAsset[] = $jsFile; } } else { $this->fileList['JS'][$setInfo['NAME']]['FILES'][] = $js; $resJs .= "<script src=\"{$js}\"></script>\n"; } } $optAsset = $this->optimizeAsset($listAsset, $setInfo['UNIQUE'], $setInfo['PREFIX'], $setInfo['NAME'], 'js', $data); $this->assetList['JS'][$setInfo['PARENT_NAME']][$setInfo['NAME']] = $optAsset['FILES']; $this->assetList['SOURCE_JS'][$setInfo['PARENT_NAME']][$setInfo['NAME']] = ($optAsset['SOURCE_FILES'] ?? []); $this->targetList[$setInfo['PARENT_NAME']]['JS_RES'][$setInfo['NAME']][] = $optAsset['RESULT'].$resJs; } unset($optAsset, $resJs, $listAsset); } if ($type == AssetShowTargetType::KERNEL && ($this->mode & $this->targetList['KERNEL']['MODE'])) { $setName = 'KERNEL'; $res .= $this->getStrings(AssetLocation::AFTER_CSS); $res .= $this->showAsset($setList,'js', $setName); $res .= $this->showFilesList(); $res .= $this->getStrings(AssetLocation::AFTER_JS_KERNEL); if (!$this->bodyScript) { $res .= $this->getStrings(AssetLocation::BODY_END); $res .= $this->showAsset($setList,'js', 'BODY'); } } elseif ($type == AssetShowTargetType::TEMPLATE_PAGE) { foreach ($this->targetList as $setName => $set) { if ($setName != 'KERNEL' && $setName != 'BODY') { $setName = $this->fixJsSetOrder($setName); $res .= $this->showAsset($setList,'js', $setName); } } $res .= $this->getStrings(AssetLocation::AFTER_JS); } elseif ($type == AssetShowTargetType::BODY && ($this->mode & $this->targetList['BODY']['MODE'])) { $setName = 'BODY'; $res .= $this->getStrings(AssetLocation::BODY_END); $res .= $this->showAsset($setList,'js', $setName); } else { foreach ($this->targetList as $setName => $set) { if ($this->mode & $set['MODE']) { $setName = $this->fixJsSetOrder($setName); if ($setName == 'KERNEL') { $res .= $this->getStrings(AssetLocation::AFTER_CSS); $res .= $this->showAsset($setList, 'js', $setName); $res .= $this->showFilesList(); $res .= $this->getStrings(AssetLocation::AFTER_JS_KERNEL); if (!$this->bodyScript) { $res .= $this->getStrings(AssetLocation::BODY_END); $res .= $this->showAsset($setList,'js', 'BODY'); } } elseif ($setName != 'BODY') { $res .= $this->showAsset($setList, 'js', $setName); } } } $res .= $this->getStrings(AssetLocation::AFTER_JS); } return (trim($res) == '' ? $res : $res."\n"); } /** * Convert location for new format. * @param mixed $location AssetLocation. * @return AssetLocation */ public static function getLocationByName($location) { if ($location === false || $location === 'DEFAULT') { $location = AssetLocation::AFTER_JS_KERNEL; } elseif ($location === true) { $location = AssetLocation::AFTER_CSS; } return $location; } /** * Insert JS code to sets assets included in page. * @return string */ public function showFilesList() { $res = ''; if (!\CJSCore::IsCoreLoaded()) { return $res; } if (!empty($this->assetList['JS'])) { $assets = []; foreach ($this->getTargetList('JS') as $set) { if ($this->mode & $set['MODE'] && isset($this->assetList['SOURCE_JS'][$set['PARENT_NAME']][$set['NAME']]) && is_array($this->assetList['SOURCE_JS'][$set['PARENT_NAME']][$set['NAME']])) { $assets = array_merge($assets, $this->assetList['SOURCE_JS'][$set['PARENT_NAME']][$set['NAME']]); } } if (!empty($assets)) { $res .= '<script>BX.setJSList('.\CUtil::phpToJSObject($assets).');</script>'; $res .= "\n"; } } if (!empty($this->assetList['CSS'])) { $assets = []; foreach ($this->getTargetList('CSS') as $set) { if ($this->mode & $set['MODE'] && isset($this->assetList['SOURCE_CSS'][$set['PARENT_NAME']][$set['NAME']]) && is_array($this->assetList['SOURCE_CSS'][$set['PARENT_NAME']][$set['NAME']]) ) { $assets = array_merge($assets, $this->assetList['SOURCE_CSS'][$set['PARENT_NAME']][$set['NAME']]); } } if (!empty($assets)) { $res .= '<script>BX.setCSSList('.\CUtil::phpToJSObject($assets).');</script>'; $res .= "\n"; } } return $res; } /** * Add information about kernel module css. * @param string $module Module name. * @param array $css Css files. * @param array $settings Settings. * @return void */ function addCssKernelInfo($module = '', $css = [], $settings = []) { if (empty($module) || empty($css)) { return; } if (!array_key_exists($module, $this->moduleInfo['CSS'])) { $this->moduleInfo['CSS'][$module] = [ 'MODULE_ID' => $module, 'BODY' => false, 'FILES_INFO' => true, 'IS_KERNEL' => true, 'DATA' => '', 'SKIP' => false ]; } foreach ($css as $key) { $key = self::getAssetPath($key); $this->kernelAsset['CSS'][$key] = $module; } $this->moduleInfo['CSS'][$module]['FILES_INFO'] = true; if (!empty($settings['DATA'])) { $this->moduleInfo['CSS'][$module]['DATA'] = $settings['DATA']; } if (!empty($settings['LOCATION'])) { $this->moduleInfo['CSS'][$module]['LOCATION'] = $settings['LOCATION']; } } /** * Add information about kernel js modules. * @param string $module Module name. * @param array $js Js files. * @param array $settings Settings. * @return void */ function addJsKernelInfo($module = '', $js = [], $settings = []) { if (empty($module) || empty($js)) { return; } if (!array_key_exists($module, $this->moduleInfo['JS'])) { $this->moduleInfo['JS'][$module] = [ 'MODULE_ID' => $module, 'BODY' => false, 'FILES_INFO' => true, 'IS_KERNEL' => true, 'DATA' => '', 'SKIP' => false ]; } foreach ($js as $key) { $key = self::getAssetPath($key); $this->kernelAsset['JS'][$key] = $module; } $this->moduleInfo['JS'][$module]['FILES_INFO'] = true; if (!empty($settings['DATA'])) { $this->moduleInfo['JS'][$module]['DATA'] = $settings['DATA']; } } /** * Return information about file and check is it in kernel pack. * @param string $css File path. * @return array|bool */ function isKernelCSS($css) { /** If optimisation off */ if (!($this->sliceKernel() && $this->optimizeCss())) { return ((strncmp($css, '/bitrix/js/', 11) == 0) || (strncmp($css, '/bitrix/css/', 12) == 0)); } /** If optimization on */ if (array_key_exists($css, $this->kernelAsset['CSS'])) { return $this->moduleInfo['CSS'][$this->kernelAsset['CSS'][$css]]; } elseif ((strncmp($css, '/bitrix/js/', 11) == 0) || (strncmp($css, '/bitrix/css/', 12) == 0)) { $tmp = explode('/', $css); $moduleID = $tmp['3']; unset($tmp); if (empty($moduleID)) { return false; } return [ 'MODULE_ID' => $moduleID.'_'.$this->assetCSSCnt++, 'BODY' => false, 'FILES_INFO' => false, 'IS_KERNEL' => true, 'DATA' => '', 'SKIP' => true ]; } return false; } /** * Return information about file and check is it in kernel pack. * @param string $js File path. * @return array|bool */ function isKernelJS($js) { /** If optimisation off */ if (!($this->sliceKernel() && $this->optimizeJs())) { return (strncmp($js, '/bitrix/js/', 11) == 0); } /** If optimization on */ if (array_key_exists($js, $this->kernelAsset['JS'])) { return $this->moduleInfo['JS'][$this->kernelAsset['JS'][$js]]; } elseif (strncmp($js, '/bitrix/js/', 11) == 0) { $tmp = explode('/', $js); $moduleID = $tmp['3']; unset($tmp); if (empty($moduleID)) { return false; } return [ 'MODULE_ID' => $moduleID.'_'.$this->assetJSCnt++, 'BODY' => false, 'FILES_INFO' => false, 'IS_KERNEL' => true, 'DATA' => '', 'SKIP' => true ]; } return false; } /** * Sets unique mode for set. * @param string $setID Target ID. * @param string $uniqueID Unique type. * @return bool */ public function setUnique($setID = '', $uniqueID = '') { $setID = preg_replace('#[^a-z0-9_]#i', '', $setID); $uniqueID = preg_replace('#[^a-z0-9_]#i', '', $uniqueID); if (!(empty($setID) || empty($uniqueID)) && isset($this->targetList[$setID])) { $this->targetList[$setID]['UNIQUE'] = true; $this->targetList[$setID]['PREFIX'] .= ($uniqueID == '' ? '' : '_'.$uniqueID); return true; } return false; } /** * Show asset resource. * @param array $setList Set list. * @param string $type Asset type css or js. * @param string $setName Parent set name. * @return string */ private function showAsset($setList = [], $type = 'css', $setName = '') { $res = ''; $type = ($type == 'css' ? 'CSS_RES' : 'JS_RES'); $skipCheck = ($setName == ''); foreach ($setList as $setInfo) { if ( ($skipCheck || $setName == $setInfo['PARENT_NAME']) && $this->mode & $setInfo['MODE'] && isset($this->targetList[$setInfo['PARENT_NAME']][$type][$setInfo['NAME']])) { $res .= implode("\n", $this->targetList[$setInfo['PARENT_NAME']][$type][$setInfo['NAME']]); } } return $res; } /** * Fix current set order for js. * @param string $setName Set name. * @return string */ private function fixJsSetOrder($setName = '') { if ($setName == 'PAGE') { $setName = 'TEMPLATE'; } elseif ($setName == 'TEMPLATE') { $setName = 'PAGE'; } return $setName; } /** * Get time for current asset. * @param string $file File path. * @return bool|string */ public static function getAssetTime($file = '') { $qpos = mb_strpos($file, '?'); if ($qpos === false) { return false; } $qpos++; return mb_substr($file, $qpos); } /** * Return md5 for asset. * @param array $assetList Asset list. * @return string */ private function getAssetChecksum($assetList = []) { $result = []; foreach ($assetList as $asset) { $result[$asset['PATH']] = $asset['FULL_PATH']; } ksort($result); return md5(implode('_', $result)); } /** * Check assets and return action and files. * @param array $assetList Asset list. * @param string $infoFile Path to metadata file. * @param string $optimFile Path to packed file. * @param bool $unique Unique type. * @return array */ private function isAssetChanged($assetList = [], $infoFile = '', $optimFile = '', $unique = false) { $result = [ 'FILE' => [], 'ACTION' => 'NO', 'FILE_EXIST' => false, 'FILES_INFO' => [] ]; if (file_exists($infoFile) && file_exists($optimFile)) { /** @noinspection PhpIncludeInspection */ include($infoFile); /** @var array $filesInfo - information about files in set */ $result['FILES_INFO'] = is_array($filesInfo) ? $filesInfo : []; $result['FILE_EXIST'] = true; if ($unique) { if (is_array($filesInfo)) { foreach ($assetList as $asset) { if (isset($filesInfo[$asset['PATH']])) { if ($this->getAssetTime($asset['FULL_PATH']) != $filesInfo[$asset['PATH']]) { $result = [ 'FILE' => $assetList, 'ACTION' => 'NEW', 'FILES_INFO' => [] ]; break; } } else { $result['FILE'][] = $asset; $result['ACTION'] = 'UP'; } } } else { $result = [ 'FILE' => $assetList, 'ACTION' => 'NEW', 'FILES_INFO' => [] ]; } } } else { $result['FILE'] = $assetList; $result['ACTION'] = 'NEW'; } return $result; } /** * @param array $files Files for optimisation. * @param bool $unique Unique type. * @param string $prefix Prefix for packed file. * @param string $setName Set name. * @param string $type Asset type css or js. * @param string $data Additional info. * @return array * @throws Main\ArgumentNullException * @throws Main\ArgumentOutOfRangeException */ private function optimizeAsset($files = [], $unique = false, $prefix = 'default', $setName = '', $type = 'css', $data = '') { if ((!is_array($files) || empty($files))) { return ['RESULT' => '', 'FILES' => []]; } $this->setTemplateID(); $res = $comments = $contents = ''; $prefix = trim($prefix); $prefix = mb_strlen($prefix) < 1 ? 'default' : $prefix; $add2End = (strncmp($prefix, 'kernel', 6) == 0); $type = ($type == 'js' ? 'js' : 'css'); // when we can't write files $noCheckOnly = !defined('BX_HEADFILES_CACHE_CHECK_ONLY'); $prefix = ($unique ? $prefix : $prefix.'_'.$this->getAssetChecksum($files)); $optimPath = BX_PERSONAL_ROOT.'/cache/'.$type.'/'.SITE_ID.'/'.$this->siteTemplateID.'/'.$prefix.'/'; $infoFile = $this->documentRoot.BX_PERSONAL_ROOT.'/managed_cache/'.$this->dbType.'/'.$type.'/'.SITE_ID.'/'.$this->siteTemplateID.'/'.$prefix.'/info_v'.self::version.'.php'; $optimFile = $optimPath.$prefix.'_v'.self::version.($type == 'css' ? '.css' : '.js'); $optimFName = $this->documentRoot.$optimFile; $tmpInfo = $this->isAssetChanged($files, $infoFile, $optimFName, $unique); $filesInfo = $tmpInfo['FILES_INFO']; $action = $tmpInfo['ACTION']; $files = $tmpInfo['FILE']; $optimFileExist = $tmpInfo['FILE_EXIST'] ?? false; $writeResult = ($action != 'NEW'); $currentFileList = &$this->fileList[strtoupper($type)][$setName]; if ($action != 'NO') { foreach ($tmpInfo['FILE'] as $newFile) { $currentFileList['UP_NEW_FILES'][] = $newFile['FULL_PATH']; } if ($action == 'UP') { if ($noCheckOnly) { $contents .= file_get_contents($optimFName); } else { $writeResult = false; } } $needWrite = false; if ($noCheckOnly) { $newContent = ''; $mapNeeded = false; foreach ($files as $file) { $assetContent = file_get_contents($file['FILE_PATH']); if ($type == 'css') { $comments .= "/* ".$file['FULL_PATH']." */\n"; $assetContent = $this->fixCSSIncludes($assetContent, $file['PATH']); $assetContent = "\n/* Start:".$file['FULL_PATH']."*/\n".$assetContent."\n/* End */\n"; $newContent .= "\n".$assetContent; } else { $info = [ "full" => $file['FULL_PATH'], "source" => $file['PATH'], "min" => "", "map" => "", ]; if (preg_match("/\\.min\\.js$/i", $file['FILE_PATH'])) { $sourceMap = self::cutSourceMap($assetContent); if ($sourceMap <> '') { $dirPath = IO\Path::getDirectory($file['PATH']); $info["map"] = $dirPath."/".$sourceMap; $info["min"] = self::getAssetPath($file['FULL_PATH']); $mapNeeded = true; } } $comments .= "; /* ".$file['FULL_PATH']."*/\n"; $newContent .= "\n".self::HEADER_START_TAG.serialize($info).self::HEADER_END_TAG."\n".$assetContent."\n/* End */\n;"; } $filesInfo[$file['PATH']] = $this->getAssetTime($file['FULL_PATH']); $needWrite = true; } if ($needWrite) { $sourceMap = self::cutSourceMap($contents); $mapNeeded = $mapNeeded || $sourceMap <> ''; // Write packed files and meta information $contents = ($add2End ? $comments.$contents.$newContent : $newContent.$contents.$comments); if ($mapNeeded) { $contents .= self::SOURCE_MAP_TAG.$prefix.".map.js"; } if ($writeResult = $this->write($optimFName, $contents)) { $cacheInfo = '<?php $filesInfo = ['; foreach ($filesInfo as $key => $hash) { $cacheInfo .= '"'.EscapePHPString($key).'" => "'.$hash.'",'; } $cacheInfo .= "]; ?>"; $this->write($infoFile, $cacheInfo, false); if ($mapNeeded) { $this->write($this->documentRoot.$optimPath.$prefix.".map.js", self::generateSourceMap($prefix.".js", $contents), false); } } } elseif ($optimFileExist) { $writeResult = true; } unset($contents); } } $label = (($type == 'css') && ($prefix == 'template' || mb_substr($prefix, 0, 9) == 'template_') ? ' data-template-style="true" ' : ''); $bundleFile = ''; $extendData = ($data != '' ? ' '.trim($data) : ''); $extendData .= ($label != '' ? ' '.trim($label) : ''); if ($writeResult || $unique && $action == 'UP') { $bundleFile = \CUtil::GetAdditionalFileURL($optimFile); $currentFileList['FILES'][] = $bundleFile; if ($type == 'css') { $res .= $this->insertCss($bundleFile, $extendData); } else { $res .= $this->insertJs($bundleFile, $extendData); } } if (!$writeResult) { foreach ($files as $file) { $currentFileList['FILES'][] = $file['FULL_PATH']; if ($type == 'css') { $res .= $this->insertCss($file['FULL_PATH'], $extendData); } else { $res .= $this->insertJs($file['FULL_PATH'], $extendData); } } } $resultFiles = []; if (is_array($filesInfo)) { foreach ($filesInfo as $key => $hash) { $resultFiles[] = $key.'?'.$hash; } } unset($files); if ($bundleFile != '') { $currentFileList['FULL_FILES'][$bundleFile] = $resultFiles; } return ['RESULT' => $res, 'FILES' => $resultFiles, 'SOURCE_FILES' => array_keys($filesInfo)]; } /** * Cuts and returns source map comment. * @param string &$content Asset content. * @return string */ private static function cutSourceMap(&$content) { $sourceMapName = ""; $length = strlen($content); $position = $length > 512 ? $length - 512 : 0; $lastLine = strpos($content, self::SOURCE_MAP_TAG, $position); if ($lastLine !== false) { $nameStart = $lastLine + strlen(self::SOURCE_MAP_TAG); if (($newLinePos = strpos($content, "\n", $nameStart)) !== false) { $sourceMapName = substr($content, $nameStart, $newLinePos - $nameStart); } else { $sourceMapName = substr($content, $nameStart); } $sourceMapName = trim($sourceMapName); $content = substr($content, 0, $lastLine); } return $sourceMapName; } /** * Returns array of file data. * @param string $content Content. * @return array */ private static function getFilesInfo($content) { $offset = 0; $line = 0; $result = []; while (($newLinePos = strpos($content, "\n", $offset)) !== false) { $line++; $offset = $newLinePos + 1; if (substr($content, $offset, strlen(self::HEADER_START_TAG)) === self::HEADER_START_TAG) { $endingPos = strpos($content, self::HEADER_END_TAG, $offset); if ($endingPos === false) { break; } $startData = $offset + strlen(self::HEADER_START_TAG); $data = unserialize(substr($content, $startData, $endingPos - $startData), ['allowed_classes' => false]); if (is_array($data)) { $data["line"] = $line + 1; $result[] = $data; } $offset = $endingPos; } } return $result; } /** * Generates source map content. * @param string $fileName File name. * @param string $content Content. * @return string */ private static function generateSourceMap($fileName, $content) { $files = self::getFilesInfo($content); $sections = ""; foreach ($files as $file) { if (!isset($file["map"]) || mb_strlen($file["map"]) < 1) { continue; } $filePath = Main\Loader::getDocumentRoot().$file["map"]; if (file_exists($filePath) && ($content = file_get_contents($filePath)) !== false) { if ($sections !== "") { $sections .= ","; } $dirPath = IO\Path::getDirectory($file["source"]); $sourceName = IO\Path::getName($file["source"]); $minName = IO\Path::getName($file["min"]); $sourceMap = str_replace( [$sourceName, $minName], [$dirPath."/".$sourceName, $dirPath."/".$minName], $content ); $sections .= '{"offset": { "line": '.$file["line"].', "column": 0 }, "map": '.$sourceMap.'}'; } } return '{"version":3, "file":"'.$fileName.'", "sections": ['.$sections.']}'; } /** * Write optimized css, js files or info file. * @param string $filePath Path for optimized css, js or info file. * @param string $content File contents. * @param bool $gzip For disabled gzip. * @return bool */ function write($filePath, $content, $gzip = true) { $fnTmp = $filePath.'.tmp'; if (!CheckDirPath($filePath) || !$fh = fopen($fnTmp, "wb")) { return false; } $written = fwrite($fh, $content); $len = strlen($content); fclose($fh); if (file_exists($filePath)) { @unlink($filePath); } $result = false; if ($written === $len) { $result = true; rename($fnTmp, $filePath); @chmod($filePath, BX_FILE_PERMISSIONS); if ($gzip && self::gzipEnabled()) { $fnTmpGz = $filePath.'.tmp.gz'; $fnGz = $filePath.'.gz'; if ($gz = gzopen($fnTmpGz, 'wb9f')) { $writtenGz = @gzwrite ($gz, $content); gzclose($gz); if (file_exists($fnGz)) { @unlink($fnGz); } if ($writtenGz === $len) { rename($fnTmpGz, $fnGz); @chmod($fnGz, BX_FILE_PERMISSIONS); } if (file_exists($fnTmpGz)) { @unlink($fnTmpGz); } } } } if (file_exists($fnTmp)) { @unlink($fnTmp); } return $result; } }