diff options
Diffstat (limited to 'core/AssetManager.php')
-rw-r--r-- | core/AssetManager.php | 861 |
1 files changed, 423 insertions, 438 deletions
diff --git a/core/AssetManager.php b/core/AssetManager.php index 3ef46bf0d3..1091e518f0 100644 --- a/core/AssetManager.php +++ b/core/AssetManager.php @@ -24,7 +24,7 @@ require_once PIWIK_INCLUDE_PATH . '/libs/jsmin/jsmin.php'; * JavaScript and CSS files. * * It performs the following actions: - * - Identifies required assets + * - Identifies required assets * - Includes assets in the rendered HTML page * - Manages asset merging and minifying * - Manages server-side cache @@ -39,92 +39,89 @@ require_once PIWIK_INCLUDE_PATH . '/libs/jsmin/jsmin.php'; */ class Piwik_AssetManager { - const MERGED_CSS_FILE = "asset_manager_global_css.css"; - const MERGED_JS_FILE = "asset_manager_global_js.js"; - const CSS_IMPORT_EVENT = "AssetManager.getCssFiles"; - const JS_IMPORT_EVENT = "AssetManager.getJsFiles"; - const MERGED_FILE_DIR = "tmp/assets/"; - const CSS_IMPORT_DIRECTIVE = "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\" />\n"; - const JS_IMPORT_DIRECTIVE = "<script type=\"text/javascript\" src=\"%s\"></script>\n"; - const GET_CSS_MODULE_ACTION = "index.php?module=Proxy&action=getCss"; - const GET_JS_MODULE_ACTION = "index.php?module=Proxy&action=getJs"; - const MINIFIED_JS_RATIO = 100; - - /** - * Returns CSS file inclusion directive(s) using the markup <link> - * - * @return string - */ - public static function getCssAssets() - { - if ( self::getDisableMergedAssets() ) - { - // Individual includes mode - self::removeMergedAsset(self::MERGED_CSS_FILE); - return self::getIndividualCssIncludes(); - } - return sprintf ( self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION ); - } - - /** - * Returns JS file inclusion directive(s) using the markup <script> - * - * @return string - */ - public static function getJsAssets() - { - if ( self::getDisableMergedAssets() ) - { - // Individual includes mode - self::removeMergedAsset(self::MERGED_JS_FILE); - return self::getIndividualJsIncludes(); - } - return sprintf ( self::JS_IMPORT_DIRECTIVE, self::GET_JS_MODULE_ACTION ); - } - - /** - * Generate the merged css file. - * - * @throws Exception if a file can not be opened in write mode - */ - private static function generateMergedCssFile() - { - $mergedContent = ""; - - // absolute path to doc root - $rootDirectory = realpath(PIWIK_DOCUMENT_ROOT); - if($rootDirectory != '/' && substr_compare($rootDirectory, '/', -1)) - { - $rootDirectory .= '/'; - } - $rootDirectoryLen = strlen($rootDirectory); - - // Loop through each css file - $files = self::getCssFiles(); - foreach ($files as $file) { - - self::validateCssFile ( $file ); - - $fileLocation = self::getAbsoluteLocation($file); - $content = file_get_contents ($fileLocation); - - // Rewrite css url directives - // - assumes these are all relative paths - // - rewrite windows directory separator \\ to / - $baseDirectory = dirname($file); - $content = preg_replace_callback( - "/(url\(['\"]?)([^'\")]*)/", - create_function( - '$matches', - "return \$matches[1] . str_replace('\\\\', '/', substr(realpath(PIWIK_DOCUMENT_ROOT . '/$baseDirectory/' . \$matches[2]), $rootDirectoryLen));" - ), - $content - ); - $mergedContent = $mergedContent . $content; - } - - $mergedContent = cssmin::minify($mergedContent); - $mergedContent = str_replace("\n", "\r\n", $mergedContent); + const MERGED_CSS_FILE = "asset_manager_global_css.css"; + const MERGED_JS_FILE = "asset_manager_global_js.js"; + const CSS_IMPORT_EVENT = "AssetManager.getCssFiles"; + const JS_IMPORT_EVENT = "AssetManager.getJsFiles"; + const MERGED_FILE_DIR = "tmp/assets/"; + const CSS_IMPORT_DIRECTIVE = "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\" />\n"; + const JS_IMPORT_DIRECTIVE = "<script type=\"text/javascript\" src=\"%s\"></script>\n"; + const GET_CSS_MODULE_ACTION = "index.php?module=Proxy&action=getCss"; + const GET_JS_MODULE_ACTION = "index.php?module=Proxy&action=getJs"; + const MINIFIED_JS_RATIO = 100; + + /** + * Returns CSS file inclusion directive(s) using the markup <link> + * + * @return string + */ + public static function getCssAssets() + { + if (self::getDisableMergedAssets()) { + // Individual includes mode + self::removeMergedAsset(self::MERGED_CSS_FILE); + return self::getIndividualCssIncludes(); + } + return sprintf(self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION); + } + + /** + * Returns JS file inclusion directive(s) using the markup <script> + * + * @return string + */ + public static function getJsAssets() + { + if (self::getDisableMergedAssets()) { + // Individual includes mode + self::removeMergedAsset(self::MERGED_JS_FILE); + return self::getIndividualJsIncludes(); + } + return sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_JS_MODULE_ACTION); + } + + /** + * Generate the merged css file. + * + * @throws Exception if a file can not be opened in write mode + */ + private static function generateMergedCssFile() + { + $mergedContent = ""; + + // absolute path to doc root + $rootDirectory = realpath(PIWIK_DOCUMENT_ROOT); + if ($rootDirectory != '/' && substr_compare($rootDirectory, '/', -1)) { + $rootDirectory .= '/'; + } + $rootDirectoryLen = strlen($rootDirectory); + + // Loop through each css file + $files = self::getCssFiles(); + foreach ($files as $file) { + + self::validateCssFile($file); + + $fileLocation = self::getAbsoluteLocation($file); + $content = file_get_contents($fileLocation); + + // Rewrite css url directives + // - assumes these are all relative paths + // - rewrite windows directory separator \\ to / + $baseDirectory = dirname($file); + $content = preg_replace_callback( + "/(url\(['\"]?)([^'\")]*)/", + create_function( + '$matches', + "return \$matches[1] . str_replace('\\\\', '/', substr(realpath(PIWIK_DOCUMENT_ROOT . '/$baseDirectory/' . \$matches[2]), $rootDirectoryLen));" + ), + $content + ); + $mergedContent = $mergedContent . $content; + } + + $mergedContent = cssmin::minify($mergedContent); + $mergedContent = str_replace("\n", "\r\n", $mergedContent); Piwik_PostEvent('AssetManager.filterMergedCss', $mergedContent); @@ -150,359 +147,347 @@ class Piwik_AssetManager } /** - * Returns individual CSS file inclusion directive(s) using the markup <link> - * - * @return string - */ - private static function getIndividualCssIncludes() - { - $cssIncludeString = ''; - - $cssFiles = self::getCssFiles(); - - foreach ($cssFiles as $cssFile) { - - self::validateCssFile ( $cssFile ); - $cssIncludeString = $cssIncludeString . sprintf ( self::CSS_IMPORT_DIRECTIVE, $cssFile ); - } - - return $cssIncludeString; - } - - /** - * Returns required CSS files - * - * @return Array - */ - private static function getCssFiles() - { - $cssFiles = array(); - Piwik_PostEvent(self::CSS_IMPORT_EVENT, $cssFiles); - $cssFiles = self::sortCssFiles($cssFiles); - return $cssFiles; - } - - /** - * Ensure CSS stylesheets are loaded in a particular order regardless of the order that plugins are loaded. - * - * @param array $cssFiles Array of CSS stylesheet files - * @return array - */ - private static function sortCssFiles($cssFiles) - { - $priorityCssOrdered = array( - 'themes/default/common.css', - 'themes/default/', - 'libs/', - 'plugins/', - ); - - return self::prioritySort($priorityCssOrdered, $cssFiles); - } - - /** - * Check the validity of the css file - * - * @param string $cssFile CSS file name - * @return boolean - * @throws Exception if a file can not be opened in write mode - */ - private static function validateCssFile ( $cssFile ) - { - if(!self::assetIsReadable($cssFile)) - { - throw new Exception("The css asset with 'href' = " . $cssFile . " is not readable"); - } - } - - /** - * Generate the merged js file. - * - * @throws Exception if a file can not be opened in write mode - */ - private static function generateMergedJsFile() - { - $mergedContent = ""; - - // Loop through each js file - $files = self::getJsFiles(); - foreach ($files as $file) { - - self::validateJsFile ( $file ); - - $fileLocation = self::getAbsoluteLocation($file); - $content = file_get_contents ($fileLocation); - - if ( !self::isMinifiedJs($content) ) - { - $content = JSMin::minify($content); - } - - $mergedContent = $mergedContent . PHP_EOL . $content; - } - $mergedContent = str_replace("\n", "\r\n", $mergedContent); + * Returns individual CSS file inclusion directive(s) using the markup <link> + * + * @return string + */ + private static function getIndividualCssIncludes() + { + $cssIncludeString = ''; + + $cssFiles = self::getCssFiles(); + + foreach ($cssFiles as $cssFile) { + + self::validateCssFile($cssFile); + $cssIncludeString = $cssIncludeString . sprintf(self::CSS_IMPORT_DIRECTIVE, $cssFile); + } + + return $cssIncludeString; + } + + /** + * Returns required CSS files + * + * @return Array + */ + private static function getCssFiles() + { + $cssFiles = array(); + Piwik_PostEvent(self::CSS_IMPORT_EVENT, $cssFiles); + $cssFiles = self::sortCssFiles($cssFiles); + return $cssFiles; + } + + /** + * Ensure CSS stylesheets are loaded in a particular order regardless of the order that plugins are loaded. + * + * @param array $cssFiles Array of CSS stylesheet files + * @return array + */ + private static function sortCssFiles($cssFiles) + { + $priorityCssOrdered = array( + 'themes/default/common.css', + 'themes/default/', + 'libs/', + 'plugins/', + ); + + return self::prioritySort($priorityCssOrdered, $cssFiles); + } + + /** + * Check the validity of the css file + * + * @param string $cssFile CSS file name + * @return boolean + * @throws Exception if a file can not be opened in write mode + */ + private static function validateCssFile($cssFile) + { + if (!self::assetIsReadable($cssFile)) { + throw new Exception("The css asset with 'href' = " . $cssFile . " is not readable"); + } + } + + /** + * Generate the merged js file. + * + * @throws Exception if a file can not be opened in write mode + */ + private static function generateMergedJsFile() + { + $mergedContent = ""; + + // Loop through each js file + $files = self::getJsFiles(); + foreach ($files as $file) { + + self::validateJsFile($file); + + $fileLocation = self::getAbsoluteLocation($file); + $content = file_get_contents($fileLocation); + + if (!self::isMinifiedJs($content)) { + $content = JSMin::minify($content); + } + + $mergedContent = $mergedContent . PHP_EOL . $content; + } + $mergedContent = str_replace("\n", "\r\n", $mergedContent); Piwik_PostEvent('AssetManager.filterMergedJs', $mergedContent); self::writeAssetToFile($mergedContent, self::MERGED_JS_FILE); - } - - /** - * Returns individual JS file inclusion directive(s) using the markup <script> - * - * @return string - */ - private static function getIndividualJsIncludes() - { - $jsFiles = self::getJsFiles(); - $jsIncludeString = ''; - foreach ($jsFiles as $jsFile) - { - self::validateJsFile( $jsFile ); - $jsIncludeString = $jsIncludeString . sprintf ( self::JS_IMPORT_DIRECTIVE, $jsFile ); - } - return $jsIncludeString; - } - - /** - * Returns required JS files - * - * @return Array - */ - private static function getJsFiles() - { - $jsFiles = array(); - Piwik_PostEvent(self::JS_IMPORT_EVENT, $jsFiles); - $jsFiles = self::sortJsFiles($jsFiles); - return $jsFiles; - } - - /** - * Ensure core JS (jQuery etc.) are loaded in a particular order regardless of the order that plugins are loaded. - * - * @param array $jsFiles Arry of JavaScript files - * @return array - */ - private static function sortJsFiles($jsFiles) - { - $priorityJsOrdered = array( - 'libs/jquery/jquery.js', - 'libs/jquery/jquery-ui.js', - 'libs/', - 'themes/default/common.js', - 'themes/default/', - 'plugins/CoreHome/templates/broadcast.js', - 'plugins/', - ); - - return self::prioritySort($priorityJsOrdered, $jsFiles); - } - - /** - * Check the validity of the js file - * - * @param string $jsFile JavaScript file name - * @return boolean - * @throws Exception if js file is not valid - */ - private static function validateJsFile ( $jsFile ) - { - if(!self::assetIsReadable($jsFile)) - { - throw new Exception("The js asset with 'src' = " . $jsFile . " is not readable"); - } - } - - /** - * Returns the global option disable_merged_assets - * - * @return string - */ - private static function getDisableMergedAssets() - { - return Piwik_Config::getInstance()->Debug['disable_merged_assets']; - } - - /** - * Returns the css merged file absolute location. - * If there is none, the generation process will be triggered. - * - * @return string The absolute location of the css merged file - */ - public static function getMergedCssFileLocation() - { - $isGenerated = self::isGenerated(self::MERGED_CSS_FILE); - - if ( !$isGenerated ) - { - self::generateMergedCssFile(); - } - - return self::getAbsoluteMergedFileLocation(self::MERGED_CSS_FILE); - } - - /** - * Returns the js merged file absolute location. - * If there is none, the generation process will be triggered. - * - * @return string The absolute location of the js merged file - */ - public static function getMergedJsFileLocation() - { - $isGenerated = self::isGenerated(self::MERGED_JS_FILE); - - if ( !$isGenerated ) - { - self::generateMergedJsFile(); - } - - return self::getAbsoluteMergedFileLocation(self::MERGED_JS_FILE); - } - - /** - * Check if the provided merged file is generated - * - * @param string $filename filename of the merged asset - * @return boolean true is file exists and is readable, false otherwise - */ - private static function isGenerated($filename) - { - return is_readable (self::getAbsoluteMergedFileLocation($filename)); - } - - /** - * Removes the previous merged file if it exists. - * Also tries to remove compressed version of the merged file. - * - * @param string $filename filename of the merged asset - * @see Piwik::serveStaticFile() - * @throws Exception if the file couldn't be deleted - */ - private static function removeMergedAsset($filename) - { - $isGenerated = self::isGenerated($filename); - - if ( $isGenerated ) - { - if ( !unlink ( self::getAbsoluteMergedFileLocation($filename) ) ) - { - throw Exception ("Unable to delete merged file : " . $filename . ". Please delete the file and refresh"); - } - - // Tries to remove compressed version of the merged file. - // See Piwik::serveStaticFile() for more info on static file compression - $compressedFileLocation = PIWIK_USER_PATH . Piwik::COMPRESSED_FILE_LOCATION . $filename; - - @unlink ( $compressedFileLocation . ".deflate"); - @unlink ( $compressedFileLocation . ".gz"); - } - } - - /** - * Remove previous merged assets - */ - public static function removeMergedAssets() - { - self::removeMergedAsset(self::MERGED_CSS_FILE); - self::removeMergedAsset(self::MERGED_JS_FILE); - } - - /** - * Check if asset is readable - * - * @param string $relativePath Relative path to file - * @return boolean - */ - private static function assetIsReadable ($relativePath) - { - return is_readable(self::getAbsoluteLocation($relativePath)); - } - - /** - * Check if the merged file directory exists and is writable. - * - * @return string The directory location - * @throws Exception if directory is not writable. - */ - private static function getMergedFileDirectory () - { - $mergedFileDirectory = PIWIK_USER_PATH . '/' . self::MERGED_FILE_DIR; - - if (!is_dir($mergedFileDirectory)) - { - Piwik_Common::mkdir($mergedFileDirectory); - } - - if (!is_writable($mergedFileDirectory)) - { - throw new Exception("Directory " . $mergedFileDirectory . " has to be writable."); - } - - return $mergedFileDirectory; - } - - /** - * Builds the absolute location of the requested merged file - * - * @param string $mergedFile Name of the merge file - * @return string absolute location of the merged file - */ - private static function getAbsoluteMergedFileLocation( $mergedFile ) - { - return self::getMergedFileDirectory() . $mergedFile; - } - - /** - * Returns the full path of an asset file - * - * @param string $relativePath Relative path to file - * @return string - */ - private static function getAbsoluteLocation ($relativePath) - { - // served by web server directly, so must be a public path - return PIWIK_DOCUMENT_ROOT . "/" . $relativePath; - } - - /** - * Indicates if the provided JavaScript content has already been minified or not. - * The heuristic is based on a custom ratio : (size of file) / (number of lines). - * The threshold (100) has been found empirically on existing files : - * - the ratio never exceeds 50 for non-minified content and - * - it never goes under 150 for minified content. - * - * @param string $content Contents of the JavaScript file - * @return boolean - */ - private static function isMinifiedJs ( $content ) - { - $lineCount = substr_count($content, "\n"); - if ( $lineCount == 0 ) - { - return true; - } - - $contentSize = strlen($content); - - $ratio = $contentSize / $lineCount; - - return $ratio > self::MINIFIED_JS_RATIO; - } - - /** - * Sort files according to priority order. Duplicates are also removed. - * - * @param array $priorityOrder Ordered array of paths (first to last) serving as buckets - * @param array $files Unsorted array of files - * @return array - */ - public static function prioritySort($priorityOrder, $files) - { - $newFiles = array(); - foreach($priorityOrder as $filePattern) - { - $newFiles = array_merge($newFiles, preg_grep('~^' . $filePattern . '~', $files)); - } - return array_keys(array_flip($newFiles)); - } + } + + /** + * Returns individual JS file inclusion directive(s) using the markup <script> + * + * @return string + */ + private static function getIndividualJsIncludes() + { + $jsFiles = self::getJsFiles(); + $jsIncludeString = ''; + foreach ($jsFiles as $jsFile) { + self::validateJsFile($jsFile); + $jsIncludeString = $jsIncludeString . sprintf(self::JS_IMPORT_DIRECTIVE, $jsFile); + } + return $jsIncludeString; + } + + /** + * Returns required JS files + * + * @return Array + */ + private static function getJsFiles() + { + $jsFiles = array(); + Piwik_PostEvent(self::JS_IMPORT_EVENT, $jsFiles); + $jsFiles = self::sortJsFiles($jsFiles); + return $jsFiles; + } + + /** + * Ensure core JS (jQuery etc.) are loaded in a particular order regardless of the order that plugins are loaded. + * + * @param array $jsFiles Arry of JavaScript files + * @return array + */ + private static function sortJsFiles($jsFiles) + { + $priorityJsOrdered = array( + 'libs/jquery/jquery.js', + 'libs/jquery/jquery-ui.js', + 'libs/', + 'themes/default/common.js', + 'themes/default/', + 'plugins/CoreHome/templates/broadcast.js', + 'plugins/', + ); + + return self::prioritySort($priorityJsOrdered, $jsFiles); + } + + /** + * Check the validity of the js file + * + * @param string $jsFile JavaScript file name + * @return boolean + * @throws Exception if js file is not valid + */ + private static function validateJsFile($jsFile) + { + if (!self::assetIsReadable($jsFile)) { + throw new Exception("The js asset with 'src' = " . $jsFile . " is not readable"); + } + } + + /** + * Returns the global option disable_merged_assets + * + * @return string + */ + private static function getDisableMergedAssets() + { + return Piwik_Config::getInstance()->Debug['disable_merged_assets']; + } + + /** + * Returns the css merged file absolute location. + * If there is none, the generation process will be triggered. + * + * @return string The absolute location of the css merged file + */ + public static function getMergedCssFileLocation() + { + $isGenerated = self::isGenerated(self::MERGED_CSS_FILE); + + if (!$isGenerated) { + self::generateMergedCssFile(); + } + + return self::getAbsoluteMergedFileLocation(self::MERGED_CSS_FILE); + } + + /** + * Returns the js merged file absolute location. + * If there is none, the generation process will be triggered. + * + * @return string The absolute location of the js merged file + */ + public static function getMergedJsFileLocation() + { + $isGenerated = self::isGenerated(self::MERGED_JS_FILE); + + if (!$isGenerated) { + self::generateMergedJsFile(); + } + + return self::getAbsoluteMergedFileLocation(self::MERGED_JS_FILE); + } + + /** + * Check if the provided merged file is generated + * + * @param string $filename filename of the merged asset + * @return boolean true is file exists and is readable, false otherwise + */ + private static function isGenerated($filename) + { + return is_readable(self::getAbsoluteMergedFileLocation($filename)); + } + + /** + * Removes the previous merged file if it exists. + * Also tries to remove compressed version of the merged file. + * + * @param string $filename filename of the merged asset + * @see Piwik::serveStaticFile() + * @throws Exception if the file couldn't be deleted + */ + private static function removeMergedAsset($filename) + { + $isGenerated = self::isGenerated($filename); + + if ($isGenerated) { + if (!unlink(self::getAbsoluteMergedFileLocation($filename))) { + throw Exception("Unable to delete merged file : " . $filename . ". Please delete the file and refresh"); + } + + // Tries to remove compressed version of the merged file. + // See Piwik::serveStaticFile() for more info on static file compression + $compressedFileLocation = PIWIK_USER_PATH . Piwik::COMPRESSED_FILE_LOCATION . $filename; + + @unlink($compressedFileLocation . ".deflate"); + @unlink($compressedFileLocation . ".gz"); + } + } + + /** + * Remove previous merged assets + */ + public static function removeMergedAssets() + { + self::removeMergedAsset(self::MERGED_CSS_FILE); + self::removeMergedAsset(self::MERGED_JS_FILE); + } + + /** + * Check if asset is readable + * + * @param string $relativePath Relative path to file + * @return boolean + */ + private static function assetIsReadable($relativePath) + { + return is_readable(self::getAbsoluteLocation($relativePath)); + } + + /** + * Check if the merged file directory exists and is writable. + * + * @return string The directory location + * @throws Exception if directory is not writable. + */ + private static function getMergedFileDirectory() + { + $mergedFileDirectory = PIWIK_USER_PATH . '/' . self::MERGED_FILE_DIR; + + if (!is_dir($mergedFileDirectory)) { + Piwik_Common::mkdir($mergedFileDirectory); + } + + if (!is_writable($mergedFileDirectory)) { + throw new Exception("Directory " . $mergedFileDirectory . " has to be writable."); + } + + return $mergedFileDirectory; + } + + /** + * Builds the absolute location of the requested merged file + * + * @param string $mergedFile Name of the merge file + * @return string absolute location of the merged file + */ + private static function getAbsoluteMergedFileLocation($mergedFile) + { + return self::getMergedFileDirectory() . $mergedFile; + } + + /** + * Returns the full path of an asset file + * + * @param string $relativePath Relative path to file + * @return string + */ + private static function getAbsoluteLocation($relativePath) + { + // served by web server directly, so must be a public path + return PIWIK_DOCUMENT_ROOT . "/" . $relativePath; + } + + /** + * Indicates if the provided JavaScript content has already been minified or not. + * The heuristic is based on a custom ratio : (size of file) / (number of lines). + * The threshold (100) has been found empirically on existing files : + * - the ratio never exceeds 50 for non-minified content and + * - it never goes under 150 for minified content. + * + * @param string $content Contents of the JavaScript file + * @return boolean + */ + private static function isMinifiedJs($content) + { + $lineCount = substr_count($content, "\n"); + if ($lineCount == 0) { + return true; + } + + $contentSize = strlen($content); + + $ratio = $contentSize / $lineCount; + + return $ratio > self::MINIFIED_JS_RATIO; + } + + /** + * Sort files according to priority order. Duplicates are also removed. + * + * @param array $priorityOrder Ordered array of paths (first to last) serving as buckets + * @param array $files Unsorted array of files + * @return array + */ + public static function prioritySort($priorityOrder, $files) + { + $newFiles = array(); + foreach ($priorityOrder as $filePattern) { + $newFiles = array_merge($newFiles, preg_grep('~^' . $filePattern . '~', $files)); + } + return array_keys(array_flip($newFiles)); + } } |