diff options
author | Julien Moumné <julien@piwik.org> | 2013-12-17 20:52:36 +0400 |
---|---|---|
committer | Julien Moumné <julien@piwik.org> | 2013-12-17 20:52:36 +0400 |
commit | 6624e27e575d6056da7881bc05e9972c50ec1128 (patch) | |
tree | 06bec63f550d16ccc8a428d40a79604496ce0985 /core | |
parent | 5d44202fb79a9b2aa70b00ec88fd316f83a440b0 (diff) |
fixes #4373, #1640
Diffstat (limited to 'core')
23 files changed, 1520 insertions, 526 deletions
diff --git a/core/AssetManager.php b/core/AssetManager.php index ec850de0db..1104ed42d8 100644 --- a/core/AssetManager.php +++ b/core/AssetManager.php @@ -11,14 +11,19 @@ namespace Piwik; use Exception; -use JSMin; -use lessc; +use Piwik\AssetManager\UIAsset; +use Piwik\AssetManager\UIAsset\InMemoryUIAsset; +use Piwik\AssetManager\UIAsset\OnDiskUIAsset; +use Piwik\AssetManager\UIAssetCacheBuster; +use Piwik\AssetManager\UIAssetFetcher\JScriptUIAssetFetcher; +use Piwik\AssetManager\UIAssetFetcher\StaticUIAssetFetcher; +use Piwik\AssetManager\UIAssetFetcher\StylesheetUIAssetFetcher; +use Piwik\AssetManager\UIAssetFetcher; +use Piwik\AssetManager\UIAssetMerger\JScriptUIAssetMerger; +use Piwik\AssetManager\UIAssetMerger\StylesheetUIAssetMerger; +use Piwik\Plugin\Manager; use Piwik\Translate; - -/** - * @see libs/jsmin/jsmin.php - */ -require_once PIWIK_INCLUDE_PATH . '/libs/jsmin/jsmin.php'; +use Piwik\Config as PiwikConfig; /** * AssetManager is the class used to manage the inclusion of UI assets: @@ -36,678 +41,361 @@ require_once PIWIK_INCLUDE_PATH . '/libs/jsmin/jsmin.php'; * When set to 0, files will be included within a pair of files: 1 JavaScript * and 1 css file. * + * @method static \Piwik\AssetManager getInstance() * @package Piwik */ -class AssetManager +class AssetManager extends Singleton { const MERGED_CSS_FILE = "asset_manager_global_css.css"; - const MERGED_JS_FILE = "asset_manager_global_js.js"; - const STYLESHEET_IMPORT_EVENT = "AssetManager.getStylesheetFiles"; - const JAVASCRIPT_IMPORT_EVENT = "AssetManager.getJavaScriptFiles"; - const MERGED_FILE_DIR = "tmp/assets/"; - const COMPRESSED_FILE_LOCATION = "/tmp/assets/"; + const MERGED_CORE_JS_FILE = "asset_manager_core_js.js"; + const MERGED_NON_CORE_JS_FILE = "asset_manager_non_core_js.js"; 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; + const GET_CORE_JS_MODULE_ACTION = "index.php?module=Proxy&action=getCoreJs"; + const GET_NON_CORE_JS_MODULE_ACTION = "index.php?module=Proxy&action=getNonCoreJs"; /** - * @param $file - * @param $less - * @internal param $mergedContent - * @return string + * @var UIAssetCacheBuster */ - protected static function getCssContentFromFile($file, $less) - { - self::validateCssFile($file); - - $fileLocation = self::getAbsoluteLocation($file); - $less->addImportDir(dirname($fileLocation)); - - $content = file_get_contents($fileLocation); - $content = self::rewriteCssPathsDirectives($file, $content); - - return $content; - } + private $cacheBuster; /** - * Returns CSS file inclusion directive(s) using the markup <link> - * - * @return string + * @var UIAssetFetcher */ - public static function getCssAssets() - { - return sprintf(self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION); - } + private $minimalStylesheetFetcher; /** - * Returns JS file inclusion directive(s) using the markup <script> - * - * @return string + * @var Theme */ - public static function getJsAssets() - { - $result = "<script type=\"text/javascript\">\n" . Translate::getJavascriptTranslations() . "\n</script>"; + private $theme; - if (self::isMergedAssetsDisabled()) { - // Individual includes mode - self::removeMergedAsset(self::MERGED_JS_FILE); - $result .= self::getIndividualJsIncludes(); - } else { - $result .= sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_JS_MODULE_ACTION); - } + function __construct() + { + $this->cacheBuster = UIAssetCacheBuster::getInstance(); + $this->minimalStylesheetFetcher = new StaticUIAssetFetcher(array('plugins/Zeitgeist/stylesheets/base.less'), array(), $this->theme); - return $result; + if(Manager::getInstance()->getThemeEnabled() != null) + $this->theme = new Theme(); } /** - * Assets are cached in the browser and Piwik server returns 304 after initial download. - * when the Cache buster string changes, the assets will be re-generated - * - * @return string + * @param UIAssetCacheBuster $cacheBuster */ - public static function generateAssetsCacheBuster() + public function setCacheBuster($cacheBuster) { - $currentGitHash = @file_get_contents(PIWIK_INCLUDE_PATH . '/.git/refs/heads/master'); - $pluginList = md5(implode(",", \Piwik\Plugin\Manager::getInstance()->getLoadedPluginsName())); - $cacheBuster = md5(SettingsPiwik::getSalt() . $pluginList . PHP_VERSION . Version::VERSION . trim($currentGitHash)); - return $cacheBuster; + $this->cacheBuster = $cacheBuster; } /** - * Generate the merged css file. - * - * @throws Exception if a file can not be opened in write mode + * @param UIAssetFetcher $minimalStylesheetFetcher */ - private static function prepareMergedCssFile() + public function setMinimalStylesheetFetcher($minimalStylesheetFetcher) { - $mergedCssAlreadyGenerated = self::isGenerated(self::MERGED_CSS_FILE); - $isDevelopingPiwik = self::isMergedAssetsDisabled(); - - if ($mergedCssAlreadyGenerated && !$isDevelopingPiwik) { - return; - } - - $files = self::getStylesheetFiles(); - $less = self::makeLess(); - - // Loop through each css file - $mergedContent = ""; - foreach ($files as $file) { - $mergedContent .= self::getCssContentFromFile($file, $less, $mergedContent); - } - - $fileHash = md5($mergedContent); - $firstLineCompileHash = "/* compile_me_once=$fileHash */"; - - // Disable Merged Assets ==> Check on each request if file needs re-compiling - if ($mergedCssAlreadyGenerated - && !$isDevelopingPiwik - ) { - $mergedFile = self::MERGED_CSS_FILE; - $cacheIsValid = self::isFirstLineMatching($mergedFile, $firstLineCompileHash); - if($cacheIsValid) { - return; - } - // Some CSS file in the merge, has changed since last merged asset was generated - // Note: we do not detect changes in @import'ed LESS files - } - - $mergedContent = $less->compile($mergedContent); - - /** - * Triggered after all less stylesheets are compiled to CSS, minified and merged into - * one file, but before the generated CSS is written to disk. - * - * This event can be used to modify merged CSS. - * - * @param string &$mergedContent The merged and minified CSS. - */ - Piwik::postEvent('AssetManager.filterMergedStylesheets', array(&$mergedContent)); - - $theme = new Theme; - $mergedContent = $theme->rewriteAssetsPathToTheme($mergedContent); - - $mergedContent = - $firstLineCompileHash . "\n" - . "/* Piwik CSS file is compiled with Less. You may be interested in writing a custom Theme for Piwik! */\n" - . $mergedContent; - - self::writeAssetToFile($mergedContent, self::MERGED_CSS_FILE); + $this->minimalStylesheetFetcher = $minimalStylesheetFetcher; } - protected static function makeLess() + /** + * @param Theme $theme + */ + public function setTheme($theme) { - if (!class_exists("lessc")) { - throw new Exception("Less was added to composer during 2.0. ==> Execute this command to update composer packages: \$ php composer.phar install"); - } - $less = new lessc; - return $less; + $this->theme = $theme; } /** - * Returns the base.less compiled to css + * Return CSS file inclusion directive(s) using the markup <link> * * @return string */ - public static function getCompiledBaseCss() + public function getCssInclusionDirective() { - $file = '/plugins/Zeitgeist/stylesheets/base.less'; - $less = self::makeLess(); - $lessContent = self::getCssContentFromFile($file, $less); - $css = $less->compile($lessContent); - return $css; + return sprintf(self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION); } - /* - * Rewrite css url directives - * - rewrites relative paths - * - rewrite windows directory separator \\ to / + /** + * Return JS file inclusion directive(s) using the markup <script> + * + * @return string */ - protected static function rewriteCssPathsDirectives($relativePath, $content) + public function getJsInclusionDirective() { - static $rootDirectoryLength = null; - if (is_null($rootDirectoryLength)) { - $rootDirectoryLength = self::countDirectoriesInPathToRoot(); - } + $result = "<script type=\"text/javascript\">\n" . Translate::getJavascriptTranslations() . "\n</script>"; - $baseDirectory = dirname($relativePath); - $content = preg_replace_callback( - "/(url\(['\"]?)([^'\")]*)/", - function ($matches) use ($rootDirectoryLength, $baseDirectory) { - $absolutePath = substr(realpath(PIWIK_DOCUMENT_ROOT . "/$baseDirectory/" . $matches[2]), $rootDirectoryLength); - $rewritten = str_replace('\\', '/', $absolutePath); + if ($this->isMergedAssetsDisabled()) { - if (is_file($rewritten)) { // only rewrite the URL if transforming it points to a valid file - return $matches[1] . $rewritten; - } else { - return $matches[1] . $matches[2]; - } - }, - $content - ); - return $content; - } + $this->getMergedCoreJSAsset()->delete(); + $this->getMergedNonCoreJSAsset()->delete(); - protected static function countDirectoriesInPathToRoot() - { - $rootDirectory = realpath(PIWIK_DOCUMENT_ROOT); - if ($rootDirectory != '/' && substr_compare($rootDirectory, '/', -1)) { - $rootDirectory .= '/'; - } - $rootDirectoryLen = strlen($rootDirectory); - return $rootDirectoryLen; - } + $result .= $this->getIndividualJsIncludes(); - private static function writeAssetToFile($mergedContent, $name) - { - // Remove the previous file - self::removeMergedAsset($name); - - // Tries to open the new file - $newFilePath = self::getAbsoluteMergedFileLocation($name); - $newFile = @fopen($newFilePath, "w"); + } else { - if (!$newFile) { - throw new Exception ("The file : " . $newFile . " can not be opened in write mode."); + $result .= sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_CORE_JS_MODULE_ACTION); + $result .= sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_NON_CORE_JS_MODULE_ACTION); } - // Write the content in the new file - fwrite($newFile, $mergedContent); - fclose($newFile); + return $result; } /** - * Returns individual CSS file inclusion directive(s) using the markup <link> + * Return the base.less compiled to css * - * @return string + * @return UIAsset */ - private static function getIndividualCssIncludes() + public function getCompiledBaseCss() { - $cssIncludeString = ''; - - $stylesheets = self::getStylesheetFiles(); + $mergedAsset = new InMemoryUIAsset(); - foreach ($stylesheets as $cssFile) { + $assetMerger = new StylesheetUIAssetMerger($mergedAsset, $this->minimalStylesheetFetcher, $this->cacheBuster); - self::validateCssFile($cssFile); - $cssIncludeString = $cssIncludeString . sprintf(self::CSS_IMPORT_DIRECTIVE, $cssFile); - } + $assetMerger->generateFile(); - return $cssIncludeString; + return $mergedAsset; } /** - * Returns required CSS files + * Return the css merged file absolute location. + * If there is none, the generation process will be triggered. * - * @return Array + * @return UIAsset */ - private static function getStylesheetFiles() + public function getMergedStylesheet() { - $stylesheets = array(); - - /** - * Triggered when gathering the list of all stylesheets (CSS and LESS) needed by - * Piwik and its plugins. - * - * Plugins that have stylesheets should use this event to make those stylesheets - * load. - * - * Stylesheets should be placed within a **stylesheets** subdirectory in your plugin's - * root directory. - * - * _Note: While you are developing your plugin you should enable the config setting - * `[Debug] disable_merged_assets` so your stylesheets will be reloaded immediately - * after a change._ - * - * **Example** - * - * public function getStylesheetFiles(&$stylesheets) - * { - * $stylesheets[] = "plugins/MyPlugin/stylesheets/myfile.less"; - * $stylesheets[] = "plugins/MyPlugin/stylesheets/myotherfile.css"; - * } - * - * @param string[] &$stylesheets The list of stylesheet paths. - */ - Piwik::postEvent(self::STYLESHEET_IMPORT_EVENT, array(&$stylesheets)); - - $stylesheets = self::sortCssFiles($stylesheets); - - $theme = new Theme; - $themeStylesheet = $theme->getStylesheet(); - if($themeStylesheet) { - $stylesheets[] = $themeStylesheet; - } + $mergedAsset = $this->getMergedStylesheetAsset(); - return $stylesheets; - } + $assetFetcher = new StylesheetUIAssetFetcher(Manager::getInstance()->getLoadedPluginsName(), $this->theme); - /** - * Ensure CSS stylesheets are loaded in a particular order regardless of the order that plugins are loaded. - * - * @param array $stylesheets Array of CSS stylesheet files - * @return array - */ - private static function sortCssFiles($stylesheets) - { - $priorityCssOrdered = array( - 'libs/', - 'plugins/CoreHome/stylesheets/color_manager.css', // must be before other Piwik stylesheets - 'plugins/Zeitgeist/stylesheets/base.less', - 'plugins/Zeitgeist/stylesheets/', - 'plugins/', - 'plugins/Dashboard/stylesheets/dashboard.less', - 'tests/', - ); - - return self::prioritySort($priorityCssOrdered, $stylesheets); + $assetMerger = new StylesheetUIAssetMerger($mergedAsset, $assetFetcher, $this->cacheBuster); + + $assetMerger->generateFile(); + + return $mergedAsset; } /** - * Check the validity of the css file + * Return the core js merged file absolute location. + * If there is none, the generation process will be triggered. * - * @param string $cssFile CSS file name - * @return boolean - * @throws Exception if a file can not be opened in write mode + * @return UIAsset */ - private static function validateCssFile($cssFile) + public function getMergedCoreJavaScript() { - if (!self::assetIsReadable($cssFile)) { - throw new Exception("The css asset with 'href' = " . $cssFile . " is not readable"); - } + return $this->getMergedJavascript($this->getCoreJScriptFetcher(), $this->getMergedCoreJSAsset()); } /** - * Generate the merged js file. + * Return the non core js merged file absolute location. + * If there is none, the generation process will be triggered. * - * @throws Exception if a file can not be opened in write mode + * @return UIAsset */ - private static function generateMergedJsFile() + public function getMergedNonCoreJavaScript() { - $mergedContent = self::getFirstLineOfMergedJs(); - - $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); - - /** - * Triggered after all the JavaScript files Piwik uses are minified and merged into a - * single file, but before the merged JavaScript is written to disk. - * - * Plugins can use this event to modify merged JavaScript or do something else - * with it. - * - * @param string &$mergedContent The minified and merged JavaScript. - */ - Piwik::postEvent('AssetManager.filterMergedJavaScripts', array(&$mergedContent)); - - $theme = new Theme; - $mergedContent = $theme->rewriteAssetsPathToTheme($mergedContent); - - self::writeAssetToFile($mergedContent, self::MERGED_JS_FILE); + return $this->getMergedJavascript($this->getNonCoreJScriptFetcher(), $this->getMergedNonCoreJSAsset()); } /** - * Returns individual JS file inclusion directive(s) using the markup <script> - * - * @return string + * @param boolean $core + * @return string[] */ - private static function getIndividualJsIncludes() + public function getLoadedPlugins($core) { - $jsIncludeString = ''; + $loadedPlugins = array(); - $jsFiles = self::getJsFiles(); - foreach ($jsFiles as $jsFile) { - self::validateJsFile($jsFile); - $jsIncludeString = $jsIncludeString . sprintf(self::JS_IMPORT_DIRECTIVE, $jsFile); + foreach(Manager::getInstance()->getLoadedPluginsName() as $pluginName) { + + $pluginIsCore = Manager::getInstance()->isPluginBundledWithCore($pluginName); + + if(($pluginIsCore && $core) || (!$pluginIsCore && !$core)) + $loadedPlugins[] = $pluginName; } - return $jsIncludeString; + + return $loadedPlugins; } /** - * Returns required JS files - * - * @return Array + * Remove previous merged assets */ - private static function getJsFiles() + public function removeMergedAssets($pluginName = false) { - $jsFiles = array(); - - /** - * Triggered when gathering the list of all JavaScript files needed by Piwik - * and its plugins. - * - * Plugins that have their own JavaScript should use this event to make those - * files load in the browser. - * - * JavaScript files should be placed within a **javascripts** subdirectory in your - * plugin's root directory. - * - * _Note: While you are developing your plugin you should enable the config setting - * `[Debug] disable_merged_assets` so JavaScript files will be reloaded immediately - * after every change._ - * - * **Example** - * - * public function getJsFiles(&$jsFiles) - * { - * $jsFiles[] = "plugins/MyPlugin/javascripts/myfile.js"; - * $jsFiles[] = "plugins/MyPlugin/javascripts/anotherone.js"; - * } - * - * @param string[] $jsFiles The JavaScript files to load. - */ - Piwik::postEvent(self::JAVASCRIPT_IMPORT_EVENT, array(&$jsFiles)); - - $theme = new Theme; - $jsInThemes = $theme->getJavaScriptFiles(); - if(!empty($jsInThemes)) { - $jsFiles = array_merge($jsFiles, $jsInThemes); + $assetsToRemove = array($this->getMergedStylesheetAsset()); + + if($pluginName) { + + if($this->pluginContainsJScriptAssets($pluginName)) { + + PiwikConfig::getInstance()->init(); + if(Manager::getInstance()->isPluginBundledWithCore($pluginName)) { + + $assetsToRemove[] = $this->getMergedCoreJSAsset(); + + } else { + + $assetsToRemove[] = $this->getMergedNonCoreJSAsset(); + } + } + + } else { + + $assetsToRemove[] = $this->getMergedCoreJSAsset(); + $assetsToRemove[] = $this->getMergedNonCoreJSAsset(); } - $jsFiles = self::sortJsFiles($jsFiles); - return $jsFiles; + $this->removeAssets($assetsToRemove); } /** - * Ensure core JS (jQuery etc.) are loaded in a particular order regardless of the order that plugins are loaded. + * Check if the merged file directory exists and is writable. * - * @param array $jsFiles Arry of JavaScript files - * @return array + * @return string The directory location + * @throws Exception if directory is not writable. */ - private static function sortJsFiles($jsFiles) + public function getAssetDirectory() { - $priorityJsOrdered = array( - 'libs/jquery/jquery.js', - 'libs/jquery/jquery-ui.js', - 'libs/jquery/jquery.browser.js', - 'libs/', - 'plugins/Zeitgeist/javascripts/piwikHelper.js', - 'plugins/Zeitgeist/javascripts/', - 'plugins/CoreHome/javascripts/broadcast.js', - 'plugins/', - 'tests/', - ); - - return self::prioritySort($priorityJsOrdered, $jsFiles); - } + $mergedFileDirectory = PIWIK_USER_PATH . "/tmp/assets"; + $mergedFileDirectory = SettingsPiwik::rewriteTmpPathWithHostname($mergedFileDirectory); - /** - * 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"); + if (!is_dir($mergedFileDirectory)) { + Filesystem::mkdir($mergedFileDirectory); } - } - /** - * Returns the global option disable_merged_assets - * - * @return string - */ - public static function isMergedAssetsDisabled() - { - return Config::getInstance()->Debug['disable_merged_assets']; + if (!is_writable($mergedFileDirectory)) { + throw new Exception("Directory " . $mergedFileDirectory . " has to be writable."); + } + + return $mergedFileDirectory; } /** - * Returns the css merged file absolute location. - * If there is none, the generation process will be triggered. + * Return the global option disable_merged_assets * - * @return string The absolute location of the css merged file + * @return boolean */ - public static function getMergedCssFileLocation() + public function isMergedAssetsDisabled() { - self::prepareMergedCssFile(); - return self::getAbsoluteMergedFileLocation(self::MERGED_CSS_FILE); + return Config::getInstance()->Debug['disable_merged_assets']; } /** - * 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 + * @param UIAssetFetcher $assetFetcher + * @param UIAsset $mergedAsset + * @return UIAsset */ - public static function getMergedJsFileLocation() + private function getMergedJavascript($assetFetcher, $mergedAsset) { - $isGenerated = self::isGenerated(self::MERGED_JS_FILE); + $assetMerger = new JScriptUIAssetMerger($mergedAsset, $assetFetcher, $this->cacheBuster); - // Make sure the merged JS is re-generated if there are new commits - if($isGenerated) { - $expectedFirstLine = self::getFirstLineOfMergedJs(); - $isGenerated = self::isFirstLineMatching(self::MERGED_JS_FILE, $expectedFirstLine); - } - if (!$isGenerated) { - self::generateMergedJsFile(); - } + $assetMerger->generateFile(); - return self::getAbsoluteMergedFileLocation(self::MERGED_JS_FILE); + return $mergedAsset; } /** - * Check if the provided merged file is generated + * Return individual JS file inclusion directive(s) using the markup <script> * - * @param string $filename filename of the merged asset - * @return boolean true is file exists and is readable, false otherwise + * @return string */ - private static function isGenerated($filename) + private function getIndividualJsIncludes() { - return is_readable(self::getAbsoluteMergedFileLocation($filename)); + return + $this->getIndividualJsIncludesFromAssetFetcher($this->getCoreJScriptFetcher()) . + $this->getIndividualJsIncludesFromAssetFetcher($this->getNonCoreJScriptFetcher()); } /** - * 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 ProxyStaticFile::serveStaticFile(serveFile - * @throws Exception if the file couldn't be deleted + * @param UIAssetFetcher $assetFetcher + * @return string */ - private static function removeMergedAsset($filename) + private function getIndividualJsIncludesFromAssetFetcher($assetFetcher) { - $isGenerated = self::isGenerated($filename); - - if ($isGenerated) { - if (!unlink(self::getAbsoluteMergedFileLocation($filename))) { - throw new Exception("Unable to delete merged file : " . $filename . ". Please delete the file and refresh"); - } + $jsIncludeString = ''; - // Tries to remove compressed version of the merged file. - // See ProxyHttp::serverStaticFile() for more info on static file compression - $compressedFileLocation = PIWIK_USER_PATH . self::COMPRESSED_FILE_LOCATION . $filename; - $compressedFileLocation = SettingsPiwik::rewriteTmpPathWithHostname($compressedFileLocation); + foreach ($assetFetcher->getCatalog()->getAssets() as $jsFile) { - @unlink($compressedFileLocation . ".deflate"); - @unlink($compressedFileLocation . ".gz"); + $jsFile->validateFile(); + $jsIncludeString = $jsIncludeString . sprintf(self::JS_IMPORT_DIRECTIVE, $jsFile->getRelativeLocation()); } + + return $jsIncludeString; } - /** - * Remove previous merged assets - */ - public static function removeMergedAssets() + private function getCoreJScriptFetcher() { - self::removeMergedAsset(self::MERGED_CSS_FILE); - self::removeMergedAsset(self::MERGED_JS_FILE); + return new JScriptUIAssetFetcher($this->getLoadedPlugins(true), $this->theme); } - /** - * Check if asset is readable - * - * @param string $relativePath Relative path to file - * @return boolean - */ - private static function assetIsReadable($relativePath) + private function getNonCoreJScriptFetcher() { - return is_readable(self::getAbsoluteLocation($relativePath)); + return new JScriptUIAssetFetcher($this->getLoadedPlugins(false), $this->theme); } /** - * Check if the merged file directory exists and is writable. - * - * @return string The directory location - * @throws Exception if directory is not writable. + * @param string $pluginName + * @return boolean */ - private static function getMergedFileDirectory() + private function pluginContainsJScriptAssets($pluginName) { - $mergedFileDirectory = PIWIK_USER_PATH . '/' . self::MERGED_FILE_DIR; - $mergedFileDirectory = SettingsPiwik::rewriteTmpPathWithHostname($mergedFileDirectory); + $fetcher = new JScriptUIAssetFetcher(array($pluginName), $this->theme); - if (!is_dir($mergedFileDirectory)) { - Filesystem::mkdir($mergedFileDirectory); - } + $assets = $fetcher->getCatalog()->getAssets(); - if (!is_writable($mergedFileDirectory)) { - throw new Exception("Directory " . $mergedFileDirectory . " has to be writable."); - } + $plugin = Manager::getInstance()->getLoadedPlugin($pluginName); - return $mergedFileDirectory; - } + if($plugin->isTheme()) { - /** - * 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; + $theme = Manager::getInstance()->getTheme($pluginName); + + $javaScriptFiles = $theme->getJavaScriptFiles(); + + if(!empty($javaScriptFiles)) + $assets = array_merge($assets, $javaScriptFiles); + } + + return !empty($assets); } /** - * Returns the full path of an asset file - * - * @param string $relativePath Relative path to file - * @return string + * @param UIAsset[] $uiAssets */ - private static function getAbsoluteLocation($relativePath) + private function removeAssets($uiAssets) { - // served by web server directly, so must be a public path - return PIWIK_DOCUMENT_ROOT . "/" . $relativePath; + foreach($uiAssets as $uiAsset) { + $uiAsset->delete(); + } } /** - * 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 + * @return UIAsset */ - private static function isMinifiedJs($content) + private function getMergedStylesheetAsset() { - $lineCount = substr_count($content, "\n"); - if ($lineCount == 0) { - return true; - } - - $contentSize = strlen($content); - - $ratio = $contentSize / $lineCount; - - return $ratio > self::MINIFIED_JS_RATIO; + return $this->getMergedUIAsset(self::MERGED_CSS_FILE); } /** - * 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 + * @return UIAsset */ - public static function prioritySort($priorityOrder, $files) + private function getMergedCoreJSAsset() { - $newFiles = array(); - foreach ($priorityOrder as $filePattern) { - $newFiles = array_merge($newFiles, preg_grep('~^' . $filePattern . '~', $files)); - } - return array_keys(array_flip($newFiles)); + return $this->getMergedUIAsset(self::MERGED_CORE_JS_FILE); } /** - * @param $mergedFile - * @param $firstLineCompileHash - * @return bool + * @return UIAsset */ - private static function isFirstLineMatching($mergedFile, $firstLineCompileHash) + private function getMergedNonCoreJSAsset() { - $pathMerged = self::getAbsoluteMergedFileLocation($mergedFile); - $f = fopen($pathMerged, 'r'); - $firstLine = fgets($f); - fclose($f); - if (!empty($firstLine) - && trim($firstLine) == trim($firstLineCompileHash) - ) { - return true; - } - return false; + return $this->getMergedUIAsset(self::MERGED_NON_CORE_JS_FILE); } /** - * @return string + * @param string $fileName + * @return UIAsset */ - private static function getFirstLineOfMergedJs() + private function getMergedUIAsset($fileName) { - return "/* Piwik Javascript - cb=" . self::generateAssetsCacheBuster() . "*/\n"; + return new OnDiskUIAsset($this->getAssetDirectory(), $fileName); } }
\ No newline at end of file diff --git a/core/AssetManager/UIAsset.php b/core/AssetManager/UIAsset.php new file mode 100644 index 0000000000..00ba05cf74 --- /dev/null +++ b/core/AssetManager/UIAsset.php @@ -0,0 +1,63 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @package Piwik + */ +namespace Piwik\AssetManager; + +use Exception; + +abstract class UIAsset +{ + abstract public function validateFile(); + + /** + * @return string + */ + abstract public function getAbsoluteLocation(); + + /** + * @return string + */ + abstract public function getRelativeLocation(); + + /** + * @return string + */ + abstract public function getBaseDirectory(); + + /** + * Removes the previous file if it exists. + * Also tries to remove compressed version of the file. + * + * @see ProxyStaticFile::serveStaticFile(serveFile + * @throws Exception if the file couldn't be deleted + */ + abstract public function delete(); + + /** + * @param string $content + * @throws \Exception + */ + abstract public function writeContent($content); + + /** + * @return string + */ + abstract public function getContent(); + + /** + * @return boolean + */ + abstract public function exists(); + + /** + * @return int + */ + abstract public function getModificationDate(); +} diff --git a/core/AssetManager/UIAsset/InMemoryUIAsset.php b/core/AssetManager/UIAsset/InMemoryUIAsset.php new file mode 100644 index 0000000000..3b5e8488f5 --- /dev/null +++ b/core/AssetManager/UIAsset/InMemoryUIAsset.php @@ -0,0 +1,65 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @package Piwik + */ +namespace Piwik\AssetManager\UIAsset; + +use Exception; +use Piwik\AssetManager\UIAsset; + +class InMemoryUIAsset extends UIAsset +{ + private $content; + + public function validateFile() + { + return; + } + + public function getAbsoluteLocation() + { + throw new Exception('invalid operation'); + } + + public function getRelativeLocation() + { + throw new Exception('invalid operation'); + } + + public function getBaseDirectory() + { + throw new Exception('invalid operation'); + } + + public function delete() + { + $this->content = null; + } + + public function exists() + { + return false; + } + + + public function writeContent($content) + { + $this->content = $content; + } + + public function getContent() + { + return $this->content; + } + + public function getModificationDate() + { + throw new Exception('invalid operation'); + } +} diff --git a/core/AssetManager/UIAsset/OnDiskUIAsset.php b/core/AssetManager/UIAsset/OnDiskUIAsset.php new file mode 100644 index 0000000000..bdda1119c2 --- /dev/null +++ b/core/AssetManager/UIAsset/OnDiskUIAsset.php @@ -0,0 +1,115 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @package Piwik + */ +namespace Piwik\AssetManager\UIAsset; + +use Exception; +use Piwik\AssetManager\UIAsset; + +class OnDiskUIAsset extends UIAsset +{ + /** + * @var string + */ + private $baseDirectory; + + /** + * @var string + */ + private $relativeLocation; + + /** + * @param string $baseDirectory + * @param string $fileLocation + */ + function __construct($baseDirectory, $fileLocation) + { + $this->baseDirectory = $baseDirectory; + $this->relativeLocation = $fileLocation; + } + + public function getAbsoluteLocation() + { + return $this->baseDirectory . '/' . $this->relativeLocation; + } + + public function getRelativeLocation() + { + return $this->relativeLocation; + } + + public function getBaseDirectory() + { + return $this->baseDirectory; + } + + public function validateFile() + { + if (!$this->assetIsReadable()) + throw new Exception("The ui asset with 'href' = " . $this->getAbsoluteLocation() . " is not readable"); + } + + public function delete() + { + if ($this->exists()) { + + if (!unlink($this->getAbsoluteLocation())) + throw new Exception("Unable to delete merged file : " . $this->getAbsoluteLocation() . ". Please delete the file and refresh"); + + // try to remove compressed version of the merged file. + @unlink($this->getAbsoluteLocation() . ".deflate"); + @unlink($this->getAbsoluteLocation() . ".gz"); + } + } + + /** + * @param string $content + * @throws \Exception + */ + public function writeContent($content) + { + $this->delete(); + + $newFile = @fopen($this->getAbsoluteLocation(), "w"); + + if (!$newFile) + throw new Exception ("The file : " . $newFile . " can not be opened in write mode."); + + fwrite($newFile, $content); + + fclose($newFile); + } + + /** + * @return string + */ + public function getContent() + { + return file_get_contents($this->getAbsoluteLocation()); + } + + public function exists() + { + return $this->assetIsReadable(); + } + + /** + * @return boolean + */ + private function assetIsReadable() + { + return is_readable($this->getAbsoluteLocation()); + } + + public function getModificationDate() + { + return filemtime($this->getAbsoluteLocation()); + } +} diff --git a/core/AssetManager/UIAssetCacheBuster.php b/core/AssetManager/UIAssetCacheBuster.php new file mode 100644 index 0000000000..a8359442b3 --- /dev/null +++ b/core/AssetManager/UIAssetCacheBuster.php @@ -0,0 +1,47 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @method static \Piwik\AssetManager\UIAssetCacheBuster getInstance() + * @package Piwik + */ +namespace Piwik\AssetManager; + +use Piwik\Plugin\Manager; +use Piwik\SettingsPiwik; +use Piwik\Singleton; +use Piwik\Version; + +class UIAssetCacheBuster extends Singleton +{ + /** + * Cache buster based on + * - Piwik version + * - Loaded plugins + * - Super user salt + * - Latest + * + * @param string[] $pluginNames + * @return string + */ + public function piwikVersionBasedCacheBuster($pluginNames = false) + { + $currentGitHash = @file_get_contents(PIWIK_INCLUDE_PATH . '/.git/refs/heads/master'); + $pluginList = md5(implode(",", !$pluginNames ? Manager::getInstance()->getLoadedPluginsName() : $pluginNames)); + $cacheBuster = md5(SettingsPiwik::getSalt() . $pluginList . PHP_VERSION . Version::VERSION . trim($currentGitHash)); + return $cacheBuster; + } + + /** + * @param string $content + * @return string + */ + public function md5BasedCacheBuster($content) + { + return md5($content); + } +} diff --git a/core/AssetManager/UIAssetCatalog.php b/core/AssetManager/UIAssetCatalog.php new file mode 100644 index 0000000000..dc25553c8e --- /dev/null +++ b/core/AssetManager/UIAssetCatalog.php @@ -0,0 +1,83 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @package Piwik + */ +namespace Piwik\AssetManager; + +class UIAssetCatalog +{ + /** + * @var UIAsset[] + */ + private $uiAssets = array(); + + /** + * @var UIAssetCatalogSorter + */ + private $catalogSorter; + + /** + * @var string + */ + private $concatenatedAssets; + + /** + * @param UIAssetCatalogSorter $catalogSorter + */ + function __construct($catalogSorter) + { + $this->catalogSorter = $catalogSorter; + } + + /** + * @param UIAsset $uiAsset + */ + public function addUIAsset($uiAsset) + { + if(!$this->assetAlreadyInCatalog($uiAsset)) { + + $this->uiAssets[] = $uiAsset; + $this->resetConcatenatedAssets(); + } + } + + /** + * @return UIAsset[] + */ + public function getAssets() + { + return $this->uiAssets; + } + + /** + * @return UIAssetCatalog + */ + public function getSortedCatalog() + { + return $this->catalogSorter->sortUIAssetCatalog($this); + } + + private function resetConcatenatedAssets() + { + $this->concatenatedAssets = null; + } + + /** + * @param UIAsset $uiAsset + * @return boolean + */ + private function assetAlreadyInCatalog($uiAsset) + { + foreach($this->uiAssets as $existingAsset) + if($uiAsset->getAbsoluteLocation() == $existingAsset->getAbsoluteLocation()) + return true; + + return false; + } +} diff --git a/core/AssetManager/UIAssetCatalogSorter.php b/core/AssetManager/UIAssetCatalogSorter.php new file mode 100644 index 0000000000..1f93a398e1 --- /dev/null +++ b/core/AssetManager/UIAssetCatalogSorter.php @@ -0,0 +1,61 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @package Piwik + */ +namespace Piwik\AssetManager; + +class UIAssetCatalogSorter +{ + /** + * @var string[] + */ + private $priorityOrder; + + /** + * @param string[] $priorityOrder + */ + function __construct($priorityOrder) + { + $this->priorityOrder = $priorityOrder; + } + + /** + * @param UIAssetCatalog $uiAssetCatalog + * @return UIAssetCatalog + */ + public function sortUIAssetCatalog($uiAssetCatalog) + { + $sortedCatalog = new UIAssetCatalog($this); + foreach ($this->priorityOrder as $filePattern) { + + $assetsMatchingPattern = array_filter($uiAssetCatalog->getAssets(), function($uiAsset) use ($filePattern) { + return preg_match('~^' . $filePattern . '~', $uiAsset->getRelativeLocation()); + }); + + foreach($assetsMatchingPattern as $assetMatchingPattern) { + $sortedCatalog->addUIAsset($assetMatchingPattern); + } + } + + $this->addUnmatchedAssets($uiAssetCatalog, $sortedCatalog); + + return $sortedCatalog; + } + + /** + * @param UIAssetCatalog $uiAssetCatalog + * @param UIAssetCatalog $sortedCatalog + */ + private function addUnmatchedAssets($uiAssetCatalog, $sortedCatalog) + { + foreach ($uiAssetCatalog->getAssets() as $uiAsset) { + $sortedCatalog->addUIAsset($uiAsset); + } + } +} diff --git a/core/AssetManager/UIAssetFetcher.php b/core/AssetManager/UIAssetFetcher.php new file mode 100644 index 0000000000..21185014f7 --- /dev/null +++ b/core/AssetManager/UIAssetFetcher.php @@ -0,0 +1,121 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @package Piwik + */ +namespace Piwik\AssetManager; + +use Piwik\AssetManager\UIAsset\OnDiskUIAsset; +use Piwik\Theme; + +abstract class UIAssetFetcher +{ + /** + * @var UIAssetCatalog + */ + protected $catalog; + + /** + * @var string[] + */ + protected $fileLocations = array(); + + /** + * @var string[] + */ + protected $plugins; + + /** + * @var Theme + */ + private $theme; + + /** + * @param string[] $plugins + * @param Theme $theme + */ + function __construct($plugins, $theme) + { + $this->plugins = $plugins; + $this->theme = $theme; + } + + /** + * @return string[] + */ + public function getPlugins() + { + return $this->plugins; + } + + /** + * $return UIAssetCatalog + */ + public function getCatalog() + { + if($this->catalog == null) + $this->createCatalog(); + + return $this->catalog; + } + + abstract protected function retrieveFileLocations(); + + /** + * @return string[] + */ + abstract protected function getPriorityOrder(); + + private function createCatalog() + { + $this->retrieveFileLocations(); + + $this->initCatalog(); + + $this->populateCatalog(); + + $this->sortCatalog(); + } + + private function initCatalog() + { + $catalogSorter = new UIAssetCatalogSorter($this->getPriorityOrder()); + $this->catalog = new UIAssetCatalog($catalogSorter); + } + + private function populateCatalog() + { + foreach ($this->fileLocations as $fileLocation) { + + $newUIAsset = new OnDiskUIAsset($this->getBaseDirectory(), $fileLocation); + $this->catalog->addUIAsset($newUIAsset); + } + } + + private function sortCatalog() + { + $this->catalog = $this->catalog->getSortedCatalog(); + } + + /** + * @return string + */ + private function getBaseDirectory() + { + // served by web server directly, so must be a public path + return PIWIK_USER_PATH; + } + + /** + * @return Theme + */ + public function getTheme() + { + return $this->theme; + } +} diff --git a/core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php b/core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php new file mode 100644 index 0000000000..3382fdabc6 --- /dev/null +++ b/core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php @@ -0,0 +1,85 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @package Piwik + */ +namespace Piwik\AssetManager\UIAssetFetcher; + +use Piwik\AssetManager\UIAssetFetcher; +use Piwik\Piwik; +use string; + +class JScriptUIAssetFetcher extends UIAssetFetcher +{ + + protected function retrieveFileLocations() + { + + if(!empty($this->plugins)) { + + /** + * Triggered when gathering the list of all JavaScript files needed by Piwik + * and its plugins. + * + * Plugins that have their own JavaScript should use this event to make those + * files load in the browser. + * + * JavaScript files should be placed within a **javascripts** subdirectory in your + * plugin's root directory. + * + * _Note: While you are developing your plugin you should enable the config setting + * `[Debug] disable_merged_assets` so JavaScript files will be reloaded immediately + * after every change._ + * + * **Example** + * + * public function getJsFiles(&$jsFiles) + * { + * $jsFiles[] = "plugins/MyPlugin/javascripts/myfile.js"; + * $jsFiles[] = "plugins/MyPlugin/javascripts/anotherone.js"; + * } + * + * @param string[] $jsFiles The JavaScript files to load. + */ + Piwik::postEvent('AssetManager.getJavaScriptFiles', array(&$this->fileLocations), null, $this->plugins); + } + + $this->addThemeFiles(); + } + + protected function addThemeFiles() + { + if(in_array($this->getTheme()->getThemeName(), $this->plugins)) { + + $jsInThemes = $this->getTheme()->getJavaScriptFiles(); + + if(!empty($jsInThemes)) { + + foreach($jsInThemes as $jsFile) { + + $this->fileLocations[] = $jsFile; + } + } + } + } + + protected function getPriorityOrder() + { + return array( + 'libs/jquery/jquery.js', + 'libs/jquery/jquery-ui.js', + 'libs/jquery/jquery.browser.js', + 'libs/', + 'plugins/Zeitgeist/javascripts/piwikHelper.js', + 'plugins/Zeitgeist/javascripts/', + 'plugins/CoreHome/javascripts/broadcast.js', + 'plugins/', + 'tests/', + ); + } +} diff --git a/core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php b/core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php new file mode 100644 index 0000000000..981eac8204 --- /dev/null +++ b/core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php @@ -0,0 +1,39 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @package Piwik + */ +namespace Piwik\AssetManager\UIAssetFetcher; + +use Piwik\AssetManager\UIAssetFetcher; + +class StaticUIAssetFetcher extends UIAssetFetcher +{ + /** + * @var string[] + */ + private $priorityOrder; + + function __construct($fileLocations, $priorityOrder, $theme) + { + parent::__construct(array(), $theme); + + $this->fileLocations = $fileLocations; + $this->priorityOrder = $priorityOrder; + } + + protected function retrieveFileLocations() + { + + } + + protected function getPriorityOrder() + { + return $this->priorityOrder; + } +} diff --git a/core/AssetManager/UIAssetFetcher/StylesheetUIAssetFetcher.php b/core/AssetManager/UIAssetFetcher/StylesheetUIAssetFetcher.php new file mode 100644 index 0000000000..e5e6e7be0f --- /dev/null +++ b/core/AssetManager/UIAssetFetcher/StylesheetUIAssetFetcher.php @@ -0,0 +1,70 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @package Piwik + */ +namespace Piwik\AssetManager\UIAssetFetcher; + +use Piwik\AssetManager\UIAssetFetcher; +use Piwik\Piwik; + +class StylesheetUIAssetFetcher extends UIAssetFetcher +{ + protected function getPriorityOrder() + { + return array( + 'libs/', + 'plugins/CoreHome/stylesheets/color_manager.css', // must be before other Piwik stylesheets + 'plugins/Zeitgeist/stylesheets/base.less', + 'plugins/Zeitgeist/stylesheets/', + 'plugins/', + 'plugins/Dashboard/stylesheets/dashboard.less', + 'tests/', + ); + } + + protected function retrieveFileLocations() + { + /** + * Triggered when gathering the list of all stylesheets (CSS and LESS) needed by + * Piwik and its plugins. + * + * Plugins that have stylesheets should use this event to make those stylesheets + * load. + * + * Stylesheets should be placed within a **stylesheets** subdirectory in your plugin's + * root directory. + * + * _Note: While you are developing your plugin you should enable the config setting + * `[Debug] disable_merged_assets` so your stylesheets will be reloaded immediately + * after a change._ + * + * **Example** + * + * public function getStylesheetFiles(&$stylesheets) + * { + * $stylesheets[] = "plugins/MyPlugin/stylesheets/myfile.less"; + * $stylesheets[] = "plugins/MyPlugin/stylesheets/myotherfile.css"; + * } + * + * @param string[] &$stylesheets The list of stylesheet paths. + */ + Piwik::postEvent('AssetManager.getStylesheetFiles', array(&$this->fileLocations)); + + $this->addThemeFiles(); + } + + protected function addThemeFiles() + { + $themeStylesheet = $this->getTheme()->getStylesheet(); + + if($themeStylesheet) { + $this->fileLocations[] = $themeStylesheet; + } + } +} diff --git a/core/AssetManager/UIAssetMerger.php b/core/AssetManager/UIAssetMerger.php new file mode 100644 index 0000000000..8e1ededd47 --- /dev/null +++ b/core/AssetManager/UIAssetMerger.php @@ -0,0 +1,212 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @package Piwik + */ +namespace Piwik\AssetManager; + +use Piwik\AssetManager\PiwikLessCompiler; +use Piwik\AssetManager\UIAsset\StylesheetUIAsset; +use Piwik\AssetManager; + +abstract class UIAssetMerger +{ + /** + * @var UIAssetFetcher + */ + private $assetFetcher; + + /** + * @var UIAsset + */ + private $mergedAsset; + + /** + * @var string + */ + private $mergedContent; + + /** + * @var UIAssetCacheBuster + */ + protected $cacheBuster; + + /** + * @param UIAsset $mergedAsset + * @param UIAssetFetcher $assetFetcher + * @param UIAssetCacheBuster $cacheBuster + */ + function __construct($mergedAsset, $assetFetcher, $cacheBuster) + { + $this->mergedAsset = $mergedAsset; + $this->assetFetcher = $assetFetcher; + $this->cacheBuster = $cacheBuster; + } + + public function generateFile() + { + if(!$this->shouldGenerate()) + return; + + $this->mergedContent = $this->getMergedAssets(); + + $this->postEvent($this->mergedContent); + + $this->adjustPaths(); + + $this->addPreamble(); + + $this->writeContentToFile(); + } + + /** + * @return string + */ + abstract protected function getMergedAssets(); + + /** + * @return string + */ + abstract protected function generateCacheBuster(); + + /** + * @return string + */ + abstract protected function getPreamble(); + + /** + * @return string + */ + abstract protected function getFileSeparator(); + + /** + * @param UIAsset $uiAsset + * @return string + */ + abstract protected function processFileContent($uiAsset); + + /** + * @param string $mergedContent + */ + abstract protected function postEvent(&$mergedContent); + + protected function getConcatenatedAssets() + { + if(empty($this->mergedContent)) + $this->concatenateAssets(); + + return $this->mergedContent; + } + + private function concatenateAssets() + { + $mergedContent = ''; + + foreach ($this->getAssetCatalog()->getAssets() as $uiAsset) { + + $uiAsset->validateFile(); + $content = $this->processFileContent($uiAsset); + + $mergedContent .= $this->getFileSeparator() . $content; + } + + $this->mergedContent = $mergedContent; + } + + /** + * @return string[] + */ + protected function getPlugins() + { + return $this->assetFetcher->getPlugins(); + } + + /** + * @return UIAssetCatalog + */ + protected function getAssetCatalog() + { + return $this->assetFetcher->getCatalog(); + } + + /** + * @return boolean + */ + private function shouldGenerate() + { + if(!$this->mergedAsset->exists()) + return true; + + if($this->shouldCompareExistingVersion()) { + + return !$this->isFileUpToDate(); + } + + return false; + } + + /** + * @return boolean + */ + private function isFileUpToDate() + { + $f = fopen($this->mergedAsset->getAbsoluteLocation(), 'r'); + $firstLine = fgets($f); + fclose($f); + + if (!empty($firstLine) && trim($firstLine) == trim($this->getCacheBusterValue())) { + return true; + } + + // Some CSS file in the merge, has changed since last merged asset was generated + // Note: we do not detect changes in @import'ed LESS files + return false; + } + + /** + * @return boolean + */ + private function isMergedAssetsDisabled() + { + return AssetManager::getInstance()->isMergedAssetsDisabled(); + } + + private function adjustPaths() + { + $this->mergedContent = $this->assetFetcher->getTheme()->rewriteAssetsPathToTheme($this->mergedContent); + } + + private function writeContentToFile() + { + $this->mergedAsset->writeContent($this->mergedContent); + } + + /** + * @return string + */ + protected function getCacheBusterValue() + { + if(empty($this->cacheBusterValue)) + $this->cacheBusterValue = $this->generateCacheBuster(); + + return $this->cacheBusterValue; + } + + private function addPreamble() + { + $this->mergedContent = $this->getPreamble() . $this->mergedContent; + } + + /** + * @return boolean + */ + private function shouldCompareExistingVersion() + { + return $this->isMergedAssetsDisabled(); + } +} diff --git a/core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php b/core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php new file mode 100644 index 0000000000..f501280ab1 --- /dev/null +++ b/core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php @@ -0,0 +1,91 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @package Piwik + */ +namespace Piwik\AssetManager\UIAssetMerger; + +use Piwik\AssetManager\UIAsset; +use Piwik\AssetManager\UIAssetCacheBuster; +use Piwik\AssetManager\UIAssetFetcher\JScriptUIAssetFetcher; +use Piwik\AssetManager\UIAssetMerger; +use Piwik\AssetManager; +use Piwik\AssetManager\UIAssetMinifier; +use Piwik\Piwik; + +class JScriptUIAssetMerger extends UIAssetMerger +{ + /** + * @var UIAssetMinifier + */ + private $assetMinifier; + + /** + * @param UIAsset $mergedAsset + * @param JScriptUIAssetFetcher $assetFetcher + * @param UIAssetCacheBuster $cacheBuster + */ + function __construct($mergedAsset, $assetFetcher, $cacheBuster) + { + parent::__construct($mergedAsset, $assetFetcher, $cacheBuster); + + $this->assetMinifier = UIAssetMinifier::getInstance(); + } + + protected function getMergedAssets() + { + $concatenatedAssets = $this->getConcatenatedAssets(); + + return str_replace("\n", "\r\n", $concatenatedAssets); + } + + protected function generateCacheBuster() + { + $cacheBuster = $this->cacheBuster->piwikVersionBasedCacheBuster($this->getPlugins()); + return "/* Piwik Javascript - cb=" . $cacheBuster . "*/\r\n"; + } + + protected function getPreamble() + { + return $this->getCacheBusterValue(); + } + + protected function postEvent(&$mergedContent) + { + $plugins = $this->getPlugins(); + + if(!empty($plugins)) { + + /** + * Triggered after all the JavaScript files Piwik uses are minified and merged into a + * single file, but before the merged JavaScript is written to disk. + * + * Plugins can use this event to modify merged JavaScript or do something else + * with it. + * + * @param string $mergedContent The minified and merged JavaScript. + */ + Piwik::postEvent('AssetManager.filterMergedJavaScripts', array(&$mergedContent), null, $plugins); + } + } + + public function getFileSeparator() + { + return PHP_EOL; + } + + protected function processFileContent($uiAsset) + { + $content = $uiAsset->getContent(); + + if (!$this->assetMinifier->isMinifiedJs($content)) + $content = $this->assetMinifier->minifyJs($content); + + return $content; + } +} diff --git a/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php b/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php new file mode 100644 index 0000000000..ae02127448 --- /dev/null +++ b/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php @@ -0,0 +1,142 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @package Piwik + */ +namespace Piwik\AssetManager\UIAssetMerger; + +use Exception; +use Piwik\AssetManager\UIAsset; +use Piwik\AssetManager\UIAssetMerger; +use Piwik\Piwik; +use lessc; + +class StylesheetUIAssetMerger extends UIAssetMerger +{ + /** + * @var lessc + */ + private $lessCompiler; + + function __construct($mergedAsset, $assetFetcher, $cacheBuster) + { + parent::__construct($mergedAsset, $assetFetcher, $cacheBuster); + + $this->lessCompiler = self::getLessCompiler(); + } + + protected function getMergedAssets() + { + foreach($this->getAssetCatalog()->getAssets() as $uiAsset) { + $this->lessCompiler->addImportDir(dirname($uiAsset->getAbsoluteLocation())); + } + + return $this->lessCompiler->compile($this->getConcatenatedAssets()); + } + + /** + * @return lessc + * @throws Exception + */ + private static function getLessCompiler() + { + if (!class_exists("lessc")) { + throw new Exception("Less was added to composer during 2.0. ==> Execute this command to update composer packages: \$ php composer.phar install"); + } + $less = new lessc(); + return $less; + } + + protected function generateCacheBuster() + { + $fileHash = $this->cacheBuster->md5BasedCacheBuster($this->getConcatenatedAssets()); + return "/* compile_me_once=$fileHash */"; + } + + protected function getPreamble() + { + return $this->getCacheBusterValue() . "\n" + . "/* Piwik CSS file is compiled with Less. You may be interested in writing a custom Theme for Piwik! */\n"; + } + + protected function postEvent(&$mergedContent) + { + /** + * Triggered after all less stylesheets are compiled to CSS, minified and merged into + * one file, but before the generated CSS is written to disk. + * + * This event can be used to modify merged CSS. + * + * @param string $mergedContent The merged and minified CSS. + */ + Piwik::postEvent('AssetManager.filterMergedStylesheets', array(&$mergedContent)); + } + + public function getFileSeparator() + { + return ''; + } + + protected function processFileContent($uiAsset) + { + return $this->rewriteCssPathsDirectives($uiAsset); + } + + /** + * Rewrite css url directives + * - rewrites paths defined relatively to their css/less definition file + * - rewrite windows directory separator \\ to / + * + * @param UIAsset $uiAsset + * @return string + */ + private function rewriteCssPathsDirectives($uiAsset) + { + static $rootDirectoryLength = null; + if (is_null($rootDirectoryLength)) { + $rootDirectoryLength = self::countDirectoriesInPathToRoot($uiAsset); + } + + $baseDirectory = dirname($uiAsset->getRelativeLocation()); + $content = preg_replace_callback( + "/(url\(['\"]?)([^'\")]*)/", + function ($matches) use ($rootDirectoryLength, $baseDirectory) { + + $absolutePath = realpath(PIWIK_USER_PATH . "/$baseDirectory/" . $matches[2]); + + if($absolutePath) { + + $relativePath = substr($absolutePath, $rootDirectoryLength); + + $relativePath = str_replace('\\', '/', $relativePath); + + return $matches[1] . $relativePath; + + } else { + return $matches[1] . $matches[2]; + } + }, + $uiAsset->getContent() + ); + return $content; + } + + /** + * @param UIAsset $uiAsset + * @return int + */ + protected function countDirectoriesInPathToRoot($uiAsset) + { + $rootDirectory = realpath($uiAsset->getBaseDirectory()); + if ($rootDirectory != '/' && substr_compare($rootDirectory, '/', -1)) { + $rootDirectory .= '/'; + } + $rootDirectoryLen = strlen($rootDirectory); + return $rootDirectoryLen; + } +} diff --git a/core/AssetManager/UIAssetMinifier.php b/core/AssetManager/UIAssetMinifier.php new file mode 100644 index 0000000000..fe29f1ec61 --- /dev/null +++ b/core/AssetManager/UIAssetMinifier.php @@ -0,0 +1,67 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik + * @method static \Piwik\AssetManager\UIAssetMinifier getInstance() + * @package Piwik + */ +namespace Piwik\AssetManager; + +use Exception; +use Piwik\Singleton; +use JShrink\Minifier; + +class UIAssetMinifier extends Singleton +{ + const MINIFIED_JS_RATIO = 100; + + protected function __construct() + { + self::validateDependency(); + parent::__construct(); + } + + + /** + * 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 + */ + public 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; + } + + /** + * @param string $content + * @return string + */ + public function minifyJs($content) + { + return Minifier::minify($content); + } + + private static function validateDependency() + { + if (!class_exists("JShrink\Minifier")) + throw new Exception("JShrink dependency is managed using Composer."); + } +} diff --git a/core/Db/BatchInsert.php b/core/Db/BatchInsert.php index 1a9563be95..89a003490f 100644 --- a/core/Db/BatchInsert.php +++ b/core/Db/BatchInsert.php @@ -61,7 +61,7 @@ class BatchInsert */ public static function tableInsertBatch($tableName, $fields, $values, $throwException = false) { - $filePath = PIWIK_USER_PATH . '/' . AssetManager::MERGED_FILE_DIR . $tableName . '-' . Common::generateUniqId() . '.csv'; + $filePath = PIWIK_USER_PATH . '/tmp/assets/' . $tableName . '-' . Common::generateUniqId() . '.csv'; $filePath = SettingsPiwik::rewriteTmpPathWithHostname($filePath); if (Db::get()->hasBulkLoader()) { diff --git a/core/Filesystem.php b/core/Filesystem.php index aea8f5e1c8..37ea89e999 100644 --- a/core/Filesystem.php +++ b/core/Filesystem.php @@ -24,9 +24,9 @@ class Filesystem * Called on Core install, update, plugin enable/disable * Will clear all cache that could be affected by the change in configuration being made */ - public static function deleteAllCacheOnUpdate() + public static function deleteAllCacheOnUpdate($pluginName = false) { - AssetManager::removeMergedAssets(); + AssetManager::getInstance()->removeMergedAssets($pluginName); View::clearCompiledTemplates(); Cache::deleteTrackerCache(); } diff --git a/core/Plugin.php b/core/Plugin.php index 53fe0f8559..d2ce8d11dd 100644 --- a/core/Plugin.php +++ b/core/Plugin.php @@ -262,7 +262,7 @@ class Plugin * * @return bool */ - final public function isTheme() + public function isTheme() { $info = $this->getInformation(); return !empty($info['theme']) && (bool)$info['theme']; diff --git a/core/Plugin/Manager.php b/core/Plugin/Manager.php index 40ca5977ca..d067333b26 100644 --- a/core/Plugin/Manager.php +++ b/core/Plugin/Manager.php @@ -20,6 +20,7 @@ use Piwik\Plugin; use Piwik\Singleton; use Piwik\Translate; use Piwik\Updater; +use Piwik\Theme; require_once PIWIK_INCLUDE_PATH . '/core/EventDispatcher.php'; @@ -219,7 +220,7 @@ class Manager extends Singleton PiwikConfig::getInstance()->forceSave(); \Piwik\Settings\Manager::cleanupPluginSettings($pluginName); - Filesystem::deleteAllCacheOnUpdate(); + $this->clearCache($pluginName); self::deletePluginFromFilesystem($pluginName); if ($this->isPluginInFilesystem($pluginName)) { @@ -228,6 +229,14 @@ class Manager extends Singleton return true; } + /** + * @param string $pluginName + */ + private function clearCache($pluginName) + { + Filesystem::deleteAllCacheOnUpdate($pluginName); + } + public static function deletePluginFromFilesystem($plugin) { Filesystem::unlinkRecursive(PIWIK_INCLUDE_PATH . '/plugins/' . $plugin, $deleteRootToo = true); @@ -255,7 +264,7 @@ class Manager extends Singleton $this->removePluginFromTrackerConfig($pluginName); PiwikConfig::getInstance()->forceSave(); - Filesystem::deleteAllCacheOnUpdate(); + $this->clearCache($pluginName); return $plugins; } @@ -329,7 +338,7 @@ class Manager extends Singleton $this->updatePluginsConfig($plugins); PiwikConfig::getInstance()->forceSave(); - Filesystem::deleteAllCacheOnUpdate(); + $this->clearCache($pluginName); $this->pluginsToLoad[] = $pluginName; } @@ -369,6 +378,22 @@ class Manager extends Singleton return $theme; } + /** + * @param string $themeName + * @throws \Exception + * @return Theme + */ + public function getTheme($themeName) + { + $plugins = $this->getLoadedPlugins(); + + foreach ($plugins as $plugin) + if ($plugin->isTheme() && $plugin->getPluginName() == $themeName) + return new Theme($plugin); + + throw new \Exception('Theme not found : ' . $themeName); + } + public function getNumberOfActivatedPlugins() { $counter = 0; diff --git a/core/ProxyHttp.php b/core/ProxyHttp.php index b35d749342..692e8219bb 100644 --- a/core/ProxyHttp.php +++ b/core/ProxyHttp.php @@ -91,8 +91,7 @@ class ProxyHttp // optional compression $compressed = false; $encoding = ''; - $compressedFileLocation = PIWIK_USER_PATH . AssetManager::COMPRESSED_FILE_LOCATION . basename($file); - $compressedFileLocation = SettingsPiwik::rewriteTmpPathWithHostname($compressedFileLocation); + $compressedFileLocation = AssetManager::getInstance()->getAssetDirectory() . '/' . basename($file); $phpOutputCompressionEnabled = ProxyHttp::isPhpOutputCompressed(); if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && !$phpOutputCompressionEnabled) { diff --git a/core/Theme.php b/core/Theme.php index 699783ae46..785d37382f 100644 --- a/core/Theme.php +++ b/core/Theme.php @@ -10,6 +10,8 @@ */ namespace Piwik; +use Piwik\Plugin\Manager; + /** * This class contains logic to make Themes work beautifully. * @@ -23,10 +25,21 @@ class Theme /** @var \Piwik\Plugin */ private $theme; - public function __construct() + /** + * @var Plugin $plugin + */ + public function __construct($plugin = false) + { + $this->createThemeFromPlugin($plugin ? $plugin : Manager::getInstance()->getThemeEnabled()); + } + + /** + * @param Plugin $plugin + */ + private function createThemeFromPlugin($plugin) { - $this->theme = \Piwik\Plugin\Manager::getInstance()->getThemeEnabled(); - $this->themeName = $this->theme->getPluginName(); + $this->theme = $plugin; + $this->themeName = $plugin->getPluginName(); } public function getStylesheet() @@ -123,4 +136,11 @@ class Theme return $source; } + /** + * @return string + */ + public function getThemeName() + { + return $this->themeName; + } }
\ No newline at end of file diff --git a/core/Twig.php b/core/Twig.php index 69a6501d90..3f15148985 100644 --- a/core/Twig.php +++ b/core/Twig.php @@ -104,9 +104,9 @@ class Twig $assetType = strtolower($params['type']); switch ($assetType) { case 'css': - return AssetManager::getCssAssets(); + return AssetManager::getInstance()->getCssInclusionDirective(); case 'js': - return AssetManager::getJsAssets(); + return AssetManager::getInstance()->getJsInclusionDirective(); default: throw new Exception("The twig function includeAssets 'type' parameter needs to be either 'css' or 'js'."); } diff --git a/core/View.php b/core/View.php index 105434e00a..9a9e105da0 100644 --- a/core/View.php +++ b/core/View.php @@ -11,6 +11,7 @@ namespace Piwik; use Exception; +use Piwik\AssetManager\UIAssetCacheBuster; use Piwik\Plugins\SitesManager\API as APISitesManager; use Piwik\Plugins\UsersManager\API as APIUsersManager; use Piwik\View\ViewInterface; @@ -259,7 +260,7 @@ class View implements ViewInterface protected function applyFilter_cacheBuster($output) { - $cacheBuster = AssetManager::generateAssetsCacheBuster(); + $cacheBuster = UIAssetCacheBuster::getInstance()->piwikVersionBasedCacheBuster(); $tag = 'cb=' . $cacheBuster; $pattern = array( |