From 6624e27e575d6056da7881bc05e9972c50ec1128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Moumn=C3=A9?= <julien@piwik.org> Date: Tue, 17 Dec 2013 17:52:36 +0100 Subject: [PATCH] fixes #4373, #1640 --- LEGALNOTICE | 10 - composer.json | 3 +- core/AssetManager.php | 710 +++++------------ core/AssetManager/UIAsset.php | 63 ++ core/AssetManager/UIAsset/InMemoryUIAsset.php | 65 ++ core/AssetManager/UIAsset/OnDiskUIAsset.php | 115 +++ core/AssetManager/UIAssetCacheBuster.php | 47 ++ core/AssetManager/UIAssetCatalog.php | 83 ++ core/AssetManager/UIAssetCatalogSorter.php | 61 ++ core/AssetManager/UIAssetFetcher.php | 121 +++ .../UIAssetFetcher/JScriptUIAssetFetcher.php | 85 +++ .../UIAssetFetcher/StaticUIAssetFetcher.php | 39 + .../StylesheetUIAssetFetcher.php | 70 ++ core/AssetManager/UIAssetMerger.php | 212 ++++++ .../UIAssetMerger/JScriptUIAssetMerger.php | 91 +++ .../UIAssetMerger/StylesheetUIAssetMerger.php | 142 ++++ core/AssetManager/UIAssetMinifier.php | 67 ++ core/Db/BatchInsert.php | 2 +- core/Filesystem.php | 4 +- core/Plugin.php | 2 +- core/Plugin/Manager.php | 31 +- core/ProxyHttp.php | 3 +- core/Theme.php | 26 +- core/Twig.php | 4 +- core/View.php | 3 +- libs/cssmin/MIT-LICENSE.txt | 19 - libs/cssmin/cssmin.php | 47 -- libs/jsmin/jsmin.php | 291 ------- plugins/Installation/Controller.php | 2 +- plugins/Proxy/Controller.php | 33 +- .../Core/AssetManager/PluginManagerMock.php | 70 ++ .../PHPUnit/Core/AssetManager/PluginMock.php | 132 ++++ tests/PHPUnit/Core/AssetManager/ThemeMock.php | 76 ++ .../AssetManager/UIAssetCacheBusterMock.php | 48 ++ .../AssetManager/UIAssetCacheBusterTest.php | 31 + .../AssetManager/UIAssetCatalogSorterTest.php | 52 ++ .../Core/AssetManager/UIAssetMinifierTest.php | 55 ++ .../configs/merged-assets-disabled.ini.php | 2 + .../configs/merged-assets-enabled.ini.php | 2 + .../Core/AssetManager/configs/plugins.ini.php | 3 + .../scripts/ExpectedMergeResultCore.js | 6 + .../scripts/ExpectedMergeResultNonCore.js | 3 + .../Core/AssetManager/scripts/SimpleAlert.js | 1 + .../Core/AssetManager/scripts/SimpleArray.js | 1 + .../AssetManager/scripts/SimpleComments.js | 1 + .../Core/AssetManager/scripts/SimpleObject.js | 18 + .../AssetManager/stylesheets/CssWithURLs.css | 12 + .../stylesheets/ExpectedMergeResult.css | 30 + .../stylesheets/ImportedLess.less | 12 + .../AssetManager/stylesheets/SimpleBody.css | 3 + .../AssetManager/stylesheets/SimpleLess.less | 17 + .../stylesheets/images/test-image.png | Bin 0 -> 290 bytes tests/PHPUnit/Core/AssetManagerTest.php | 712 +++++++++++++++++- tests/PHPUnit/Core/ServeStaticFileTest.php | 3 +- tests/PHPUnit/UI | 2 +- tests/PHPUnit/UITest.php | 2 +- 56 files changed, 2818 insertions(+), 927 deletions(-) create mode 100644 core/AssetManager/UIAsset.php create mode 100644 core/AssetManager/UIAsset/InMemoryUIAsset.php create mode 100644 core/AssetManager/UIAsset/OnDiskUIAsset.php create mode 100644 core/AssetManager/UIAssetCacheBuster.php create mode 100644 core/AssetManager/UIAssetCatalog.php create mode 100644 core/AssetManager/UIAssetCatalogSorter.php create mode 100644 core/AssetManager/UIAssetFetcher.php create mode 100644 core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php create mode 100644 core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php create mode 100644 core/AssetManager/UIAssetFetcher/StylesheetUIAssetFetcher.php create mode 100644 core/AssetManager/UIAssetMerger.php create mode 100644 core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php create mode 100644 core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php create mode 100644 core/AssetManager/UIAssetMinifier.php delete mode 100644 libs/cssmin/MIT-LICENSE.txt delete mode 100644 libs/cssmin/cssmin.php delete mode 100644 libs/jsmin/jsmin.php create mode 100644 tests/PHPUnit/Core/AssetManager/PluginManagerMock.php create mode 100644 tests/PHPUnit/Core/AssetManager/PluginMock.php create mode 100644 tests/PHPUnit/Core/AssetManager/ThemeMock.php create mode 100644 tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterMock.php create mode 100644 tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterTest.php create mode 100644 tests/PHPUnit/Core/AssetManager/UIAssetCatalogSorterTest.php create mode 100644 tests/PHPUnit/Core/AssetManager/UIAssetMinifierTest.php create mode 100644 tests/PHPUnit/Core/AssetManager/configs/merged-assets-disabled.ini.php create mode 100644 tests/PHPUnit/Core/AssetManager/configs/merged-assets-enabled.ini.php create mode 100644 tests/PHPUnit/Core/AssetManager/configs/plugins.ini.php create mode 100644 tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultCore.js create mode 100644 tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultNonCore.js create mode 100644 tests/PHPUnit/Core/AssetManager/scripts/SimpleAlert.js create mode 100644 tests/PHPUnit/Core/AssetManager/scripts/SimpleArray.js create mode 100644 tests/PHPUnit/Core/AssetManager/scripts/SimpleComments.js create mode 100644 tests/PHPUnit/Core/AssetManager/scripts/SimpleObject.js create mode 100644 tests/PHPUnit/Core/AssetManager/stylesheets/CssWithURLs.css create mode 100644 tests/PHPUnit/Core/AssetManager/stylesheets/ExpectedMergeResult.css create mode 100644 tests/PHPUnit/Core/AssetManager/stylesheets/ImportedLess.less create mode 100644 tests/PHPUnit/Core/AssetManager/stylesheets/SimpleBody.css create mode 100644 tests/PHPUnit/Core/AssetManager/stylesheets/SimpleLess.less create mode 100644 tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png diff --git a/LEGALNOTICE b/LEGALNOTICE index 3863c68c12..f394df67e4 100644 --- a/LEGALNOTICE +++ b/LEGALNOTICE @@ -114,16 +114,6 @@ THIRD-PARTY COMPONENTS AND LIBRARIES Notes: - reference implementation - Name: cssmin - Link: http://code.google.com/p/cssmin/ - License: MIT - - Name: jsmin - Link: http://code.google.com/p/jsmin-php/ - License: MIT - Notes: - - contains additional clause "The Software shall be used for Good, not Evil." - Name: sparkline Link: https//sourceforge.net/projects/sparkline/ License: Dual-licensed: New BSD or GPL v2 diff --git a/composer.json b/composer.json index 190c5de277..039faa8a66 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "php": ">=5.3.0", "twig/twig": "1.*", "leafo/lessphp": "0.3.*", - "symfony/console": ">=v2.3.5" + "symfony/console": ">=v2.3.5", + "tedivm/jshrink": "v0.5.1" } } 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( diff --git a/libs/cssmin/MIT-LICENSE.txt b/libs/cssmin/MIT-LICENSE.txt deleted file mode 100644 index 974edcfdc1..0000000000 --- a/libs/cssmin/MIT-LICENSE.txt +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2008 Joe Scylla <joe.scylla@gmail.com> - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/libs/cssmin/cssmin.php b/libs/cssmin/cssmin.php deleted file mode 100644 index 5fa999bc54..0000000000 --- a/libs/cssmin/cssmin.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php -/** - * cssmin.php - A simple CSS minifier. - * -- - * - * <code> - * include("cssmin.php"); - * file_put_contents("path/to/target.css", cssmin::minify(file_get_contents("path/to/source.css"))); - * </code> - * -- - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING - * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * -- - * - * @package cssmin - * @author Joe Scylla <joe.scylla@gmail.com> - * @copyright 2008 Joe Scylla <joe.scylla@gmail.com> - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 1.0 (2008-01-31) - */ -class cssmin - { - /** - * Minifies stylesheet definitions - * - * @param string $v Stylesheet definitions as string - * @return string Minified stylesheet definitions - */ - public static function minify($v) - { - $v = trim($v); - $v = str_replace("\r\n", "\n", $v); - $search = array("/\/\*[\d\D]*?\*\/|\t+/", "/\s+/", "/\}\s+/"); - $replace = array(null, " ", "}\n"); - $v = preg_replace($search, $replace, $v); - $search = array("/\\;\s/", "/\s+\{\\s+/", "/\\:\s+\\#/", "/,\s+/i", "/\\:\s+\\\'/i", "/\\:\s+([0-9]+|[A-F]+)/i"); - $replace = array(";", "{", ":#", ",", ":\'", ":$1"); - $v = preg_replace($search, $replace, $v); - $v = str_replace("\n", null, $v); - return $v; - } - } -?> \ No newline at end of file diff --git a/libs/jsmin/jsmin.php b/libs/jsmin/jsmin.php deleted file mode 100644 index 3c2f859dfd..0000000000 --- a/libs/jsmin/jsmin.php +++ /dev/null @@ -1,291 +0,0 @@ -<?php -/** - * jsmin.php - PHP implementation of Douglas Crockford's JSMin. - * - * This is pretty much a direct port of jsmin.c to PHP with just a few - * PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and - * outputs to stdout, this library accepts a string as input and returns another - * string as output. - * - * PHP 5 or higher is required. - * - * Permission is hereby granted to use this version of the library under the - * same terms as jsmin.c, which has the following license: - * - * -- - * Copyright (c) 2002 Douglas Crockford (www.crockford.com) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies - * of the Software, and to permit persons to whom the Software is furnished to do - * so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * The Software shall be used for Good, not Evil. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * -- - * - * @package JSMin - * @author Ryan Grove <ryan@wonko.com> - * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c) - * @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port) - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 1.1.1 (2008-03-02) - * @link http://code.google.com/p/jsmin-php/ - */ - -class JSMin { - const ORD_LF = 10; - const ORD_SPACE = 32; - - protected $a = ''; - protected $b = ''; - protected $input = ''; - protected $inputIndex = 0; - protected $inputLength = 0; - protected $lookAhead = null; - protected $output = ''; - - // -- Public Static Methods -------------------------------------------------- - - public static function minify($js) { - $jsmin = new JSMin($js); - return $jsmin->min(); - } - - // -- Public Instance Methods ------------------------------------------------ - - public function __construct($input) { - $this->input = str_replace("\r\n", "\n", $input); - $this->inputLength = strlen($this->input); - } - - // -- Protected Instance Methods --------------------------------------------- - - protected function action($d) { - switch($d) { - case 1: - $this->output .= $this->a; - - case 2: - $this->a = $this->b; - - if ($this->a === "'" || $this->a === '"') { - for (;;) { - $this->output .= $this->a; - $this->a = $this->get(); - - if ($this->a === $this->b) { - break; - } - - if (ord($this->a) <= self::ORD_LF) { - throw new JSMinException('Unterminated string literal.'); - } - - if ($this->a === '\\') { - $this->output .= $this->a; - $this->a = $this->get(); - } - } - } - - case 3: - $this->b = $this->next(); - - if ($this->b === '/' && ( - $this->a === '(' || $this->a === ',' || $this->a === '=' || - $this->a === ':' || $this->a === '[' || $this->a === '!' || - $this->a === '&' || $this->a === '|' || $this->a === '?')) { - - $this->output .= $this->a . $this->b; - - for (;;) { - $this->a = $this->get(); - - if ($this->a === '/') { - break; - } elseif ($this->a === '\\') { - $this->output .= $this->a; - $this->a = $this->get(); - } elseif (ord($this->a) <= self::ORD_LF) { - throw new JSMinException('Unterminated regular expression '. - 'literal.'); - } - - $this->output .= $this->a; - } - - $this->b = $this->next(); - } - } - } - - protected function get() { - $c = $this->lookAhead; - $this->lookAhead = null; - - if ($c === null) { - if ($this->inputIndex < $this->inputLength) { - $c = substr($this->input, $this->inputIndex, 1); - $this->inputIndex += 1; - } else { - $c = null; - } - } - - if ($c === "\r") { - return "\n"; - } - - if ($c === null || $c === "\n" || ord($c) >= self::ORD_SPACE) { - return $c; - } - - return ' '; - } - - protected function isAlphaNum($c) { - return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1; - } - - protected function min() { - $this->a = "\n"; - $this->action(3); - - while ($this->a !== null) { - switch ($this->a) { - case ' ': - if ($this->isAlphaNum($this->b)) { - $this->action(1); - } else { - $this->action(2); - } - break; - - case "\n": - switch ($this->b) { - case '{': - case '[': - case '(': - case '+': - case '-': - $this->action(1); - break; - - case ' ': - $this->action(3); - break; - - default: - if ($this->isAlphaNum($this->b)) { - $this->action(1); - } - else { - $this->action(2); - } - } - break; - - default: - switch ($this->b) { - case ' ': - if ($this->isAlphaNum($this->a)) { - $this->action(1); - break; - } - - $this->action(3); - break; - - case "\n": - switch ($this->a) { - case '}': - case ']': - case ')': - case '+': - case '-': - case '"': - case "'": - $this->action(1); - break; - - default: - if ($this->isAlphaNum($this->a)) { - $this->action(1); - } - else { - $this->action(3); - } - } - break; - - default: - $this->action(1); - break; - } - } - } - - return $this->output; - } - - protected function next() { - $c = $this->get(); - - if ($c === '/') { - switch($this->peek()) { - case '/': - for (;;) { - $c = $this->get(); - - if (ord($c) <= self::ORD_LF) { - return $c; - } - } - - case '*': - $this->get(); - - for (;;) { - switch($this->get()) { - case '*': - if ($this->peek() === '/') { - $this->get(); - return ' '; - } - break; - - case null: - throw new JSMinException('Unterminated comment.'); - } - } - - default: - return $c; - } - } - - return $c; - } - - protected function peek() { - $this->lookAhead = $this->get(); - return $this->lookAhead; - } -} - -// -- Exceptions --------------------------------------------------------------- -class JSMinException extends Exception {} -?> \ No newline at end of file diff --git a/plugins/Installation/Controller.php b/plugins/Installation/Controller.php index 4001d99f05..f664eb12b0 100644 --- a/plugins/Installation/Controller.php +++ b/plugins/Installation/Controller.php @@ -598,7 +598,7 @@ class Controller extends \Piwik\Plugin\ControllerAdmin public function getBaseCss() { @header('Content-Type: text/css'); - return AssetManager::getCompiledBaseCss(); + return AssetManager::getInstance()->getCompiledBaseCss()->getContent(); } /** diff --git a/plugins/Proxy/Controller.php b/plugins/Proxy/Controller.php index 60b43cd21c..e703cc6df5 100644 --- a/plugins/Proxy/Controller.php +++ b/plugins/Proxy/Controller.php @@ -11,6 +11,7 @@ namespace Piwik\Plugins\Proxy; use Piwik\AssetManager; +use Piwik\AssetManager\UIAsset; use Piwik\Common; use Piwik\Piwik; use Piwik\ProxyHttp; @@ -35,20 +36,40 @@ class Controller extends \Piwik\Plugin\Controller */ public function getCss() { - $cssMergedFile = AssetManager::getMergedCssFileLocation(); - ProxyHttp::serverStaticFile($cssMergedFile, "text/css"); + $cssMergedFile = AssetManager::getInstance()->getMergedStylesheet(); + ProxyHttp::serverStaticFile($cssMergedFile->getAbsoluteLocation(), "text/css"); } /** - * Output the merged JavaScript file. + * Output the merged core JavaScript file. * This method is called when the asset manager is enabled. * * @see core/AssetManager.php */ - public function getJs() + public function getCoreJs() { - $jsMergedFile = AssetManager::getMergedJsFileLocation(); - ProxyHttp::serverStaticFile($jsMergedFile, self::JS_MIME_TYPE); + $jsMergedFile = AssetManager::getInstance()->getMergedCoreJavaScript(); + $this->serveJsFile($jsMergedFile); + } + + /** + * Output the merged non core JavaScript file. + * This method is called when the asset manager is enabled. + * + * @see core/AssetManager.php + */ + public function getNonCoreJs() + { + $jsMergedFile = AssetManager::getInstance()->getMergedNonCoreJavaScript(); + $this->serveJsFile($jsMergedFile); + } + + /** + * @param UIAsset $uiAsset + */ + private function serveJsFile($uiAsset) + { + ProxyHttp::serverStaticFile($uiAsset->getAbsoluteLocation(), self::JS_MIME_TYPE); } /** diff --git a/tests/PHPUnit/Core/AssetManager/PluginManagerMock.php b/tests/PHPUnit/Core/AssetManager/PluginManagerMock.php new file mode 100644 index 0000000000..de0a7153e3 --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/PluginManagerMock.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 + */ + +use Piwik\Plugin\Manager; +use Piwik\Plugin; +use Piwik\Theme; + +class PluginManagerMock extends Manager +{ + + /** + * @var Plugin[] + */ + private $plugins = array(); + + /** + * @var Theme + */ + private $loadedTheme; + + /** + * @param Plugin[] $plugins + */ + public function setPlugins($plugins) + { + $this->plugins = $plugins; + } + + public function getLoadedPlugin($name) + { + foreach($this->plugins as $plugin) + if($plugin->getPluginName() == $name) + return $plugin; + + return null; + } + + public function getLoadedPluginsName() + { + $pluginNames = array(); + + foreach($this->plugins as $plugin) + $pluginNames[] = $plugin->getPluginName(); + + return $pluginNames; + } + + public function getLoadedPlugins() + { + return $this->plugins; + } + + public function getTheme($themeName) + { + return $this->loadedTheme; + } + + /** + * @param Theme $loadedTheme + */ + public function setLoadedTheme($loadedTheme) + { + $this->loadedTheme = $loadedTheme; + } +} diff --git a/tests/PHPUnit/Core/AssetManager/PluginMock.php b/tests/PHPUnit/Core/AssetManager/PluginMock.php new file mode 100644 index 0000000000..3253124350 --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/PluginMock.php @@ -0,0 +1,132 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +use Piwik\Plugin; + +class PluginMock extends Plugin +{ + /** + * @var string[] + */ + private $jsFiles = array(); + + /** + * @var string[] + */ + private $stylesheetFiles = array(); + + /** + * @var string + */ + private $jsCustomization = ''; + + /** + * @var string + */ + private $cssCustomization = ''; + + /** + * @var boolean + */ + private $isTheme = false; + + /** + * @param string $name + */ + function __construct($name) + { + $this->pluginName = $name; + } + + public function getListHooksRegistered() + { + return array( + 'AssetManager.getJavaScriptFiles' => 'getJsFiles', + 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', + 'AssetManager.filterMergedJavaScripts' => 'filterMergedJavaScriptsHook', + 'AssetManager.filterMergedStylesheets' => 'filterMergedStylesheetsHook', + ); + } + + /** + * @param string[] $jsFiles + */ + public function getJsFiles(&$jsFiles) + { + $jsFiles = array_merge($jsFiles, $this->jsFiles); + } + + /** + * @param string[] $stylesheetFiles + */ + public function getStylesheetFiles(&$stylesheetFiles) + { + $stylesheetFiles = array_merge($stylesheetFiles, $this->stylesheetFiles); + } + + /** + * @param string $mergedContent + */ + public function filterMergedJavaScriptsHook(&$mergedContent) + { + $mergedContent .= $this->jsCustomization; + } + + /** + * @param string $mergedContent + */ + public function filterMergedStylesheetsHook(&$mergedContent) + { + $mergedContent .= $this->cssCustomization; + } + + /** + * @param string $cssCustomization + */ + public function setCssCustomization($cssCustomization) + { + $this->cssCustomization = $cssCustomization; + } + + /** + * @param string $jsCustomization + */ + public function setJsCustomization($jsCustomization) + { + $this->jsCustomization = $jsCustomization; + } + + /** + * @param string[] $jsFiles + */ + public function setJsFiles($jsFiles) + { + $this->jsFiles = $jsFiles; + } + + /** + * @param string[] $stylesheetFiles + */ + public function setStylesheetFiles($stylesheetFiles) + { + $this->stylesheetFiles = $stylesheetFiles; + } + + /** + * @param boolean $isTheme + */ + public function setIsTheme($isTheme) + { + $this->isTheme = $isTheme; + } + + public function isTheme() + { + return $this->isTheme; + } +} \ No newline at end of file diff --git a/tests/PHPUnit/Core/AssetManager/ThemeMock.php b/tests/PHPUnit/Core/AssetManager/ThemeMock.php new file mode 100644 index 0000000000..1d21edb60d --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/ThemeMock.php @@ -0,0 +1,76 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +use Piwik\Plugin; +use Piwik\Theme; + +class ThemeMock extends Theme +{ + + /** + * @var string[] + */ + private $jsFiles = array(); + + /** + * @var string + */ + private $stylesheet; + + /** + * @var Plugin + */ + private $plugin; + + /** + * @param Plugin $plugin + */ + function __construct($plugin) + { + $this->plugin = $plugin; + } + + public function getStylesheet() + { + return $this->stylesheet; + } + + public function getJavaScriptFiles() + { + return $this->jsFiles; + } + + /** + * @param string[] $jsFiles + */ + public function setJsFiles($jsFiles) + { + $this->jsFiles = $jsFiles; + } + + /** + * @param string $stylesheet + */ + public function setStylesheet($stylesheet) + { + $this->stylesheet = $stylesheet; + } + + /** + * @return Plugin + */ + public function getPlugin() + { + return $this->plugin; + } + + public function getThemeName() + { + return $this->plugin->getPluginName(); + } +} \ No newline at end of file diff --git a/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterMock.php b/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterMock.php new file mode 100644 index 0000000000..a14c20232f --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterMock.php @@ -0,0 +1,48 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +use Piwik\AssetManager\UIAssetCacheBuster; + +class UIAssetCacheBusterMock extends UIAssetCacheBuster +{ + /** + * @var string + */ + private $piwikVersionBasedCacheBuster; + + /** + * @var string + */ + private $md5BasedCacheBuster; + + public function piwikVersionBasedCacheBuster() + { + return $this->piwikVersionBasedCacheBuster; + } + + public function md5BasedCacheBuster($content) + { + return $this->md5BasedCacheBuster; + } + + /** + * @param string $md5BasedCacheBuster + */ + public function setMd5BasedCacheBuster($md5BasedCacheBuster) + { + $this->md5BasedCacheBuster = $md5BasedCacheBuster; + } + + /** + * @param string $piwikVersionBasedCacheBuster + */ + public function setPiwikVersionBasedCacheBuster($piwikVersionBasedCacheBuster) + { + $this->piwikVersionBasedCacheBuster = $piwikVersionBasedCacheBuster; + } +} diff --git a/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterTest.php b/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterTest.php new file mode 100644 index 0000000000..705000049c --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterTest.php @@ -0,0 +1,31 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +use Piwik\AssetManager\UIAssetCacheBuster; + +class UIAssetCacheBusterTest extends PHPUnit_Framework_TestCase +{ + + /** + * @var UIAssetCacheBuster + */ + private $cacheBuster; + + public function setUp() + { + $this->cacheBuster = UIAssetCacheBuster::getInstance(); + } + + /** + * @group Core + */ + public function test_md5BasedCacheBuster() + { + $this->assertEquals('098f6bcd4621d373cade4e832627b4f6', $this->cacheBuster->md5BasedCacheBuster('test')); + } +} diff --git a/tests/PHPUnit/Core/AssetManager/UIAssetCatalogSorterTest.php b/tests/PHPUnit/Core/AssetManager/UIAssetCatalogSorterTest.php new file mode 100644 index 0000000000..223be99b9c --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/UIAssetCatalogSorterTest.php @@ -0,0 +1,52 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +use Piwik\AssetManager\UIAsset\OnDiskUIAsset; +use Piwik\AssetManager\UIAssetCatalog; +use Piwik\AssetManager\UIAssetCatalogSorter; + +class UIAssetCatalogSorterTest extends PHPUnit_Framework_TestCase +{ + /** + * @group Core + */ + public function testPrioritySort() + { + $baseDirectory = '/var/www/piwik/'; + + $priorityPatterns = array( + 'libs/base.css', + 'libs/', + 'plugins/', + ); + + $catalogSorter = new UIAssetCatalogSorter($priorityPatterns); + + $unsortedCatalog = new UIAssetCatalog($catalogSorter); + $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'new_dir/new_file')); + $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'plugins/xyz')); + $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'plugins/abc')); + $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/xyz')); + $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/base.css')); + $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/abc')); + $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'plugins/xyz')); + $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/base.css')); + $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/xyz')); + + $expectedCatalog = new UIAssetCatalog($catalogSorter); + $expectedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/base.css')); + $expectedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/xyz')); + $expectedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/abc')); + $expectedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'plugins/xyz')); + $expectedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'plugins/abc')); + $expectedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'new_dir/new_file')); + + $sortedCatalog = $unsortedCatalog->getSortedCatalog(); + + $this->assertEquals($expectedCatalog, $sortedCatalog); + } +} diff --git a/tests/PHPUnit/Core/AssetManager/UIAssetMinifierTest.php b/tests/PHPUnit/Core/AssetManager/UIAssetMinifierTest.php new file mode 100644 index 0000000000..845055652c --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/UIAssetMinifierTest.php @@ -0,0 +1,55 @@ +<?php + +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +use Piwik\AssetManager\UIAsset\OnDiskUIAsset; +use Piwik\AssetManager\UIAssetMinifier; + +class UIAssetMinifierTest extends PHPUnit_Framework_TestCase +{ + + /** + * @var UIAssetMinifier + */ + private $assetMinifier; + + public function setUp() + { + $this->assetMinifier = UIAssetMinifier::getInstance(); + } + + public function provider_isMinifiedJs() + { + return array( + array('libs/jquery/jquery.js', true), + array('libs/jquery/jquery-ui.js', true), + array('libs/jquery/jquery.browser.js', true), + array('libs/jqplot/jqplot-custom.min.js', true), + array('plugins/TreemapVisualization/libs/Jit/jit-2.0.1-yc.js', true), + array('plugins/TreemapVisualization/javascripts/treemapViz.js', false), + array('plugins/UserCountryMap/javascripts/vendor/raphael.min.js', true), + array('plugins/UserCountryMap/javascripts/vendor/jquery.qtip.min.js', true), + array('plugins/UserCountryMap/javascripts/vendor/kartograph.min.js', true), + array('plugins/UserCountryMap/javascripts/vendor/jquery.qtip.min.js', true), + ); + } + + /** + * @group Core + * @dataProvider provider_isMinifiedJs + */ + public function test_isMinifiedJs($scriptFileName, $isMinified) + { + $scriptFile = new OnDiskUIAsset(PIWIK_USER_PATH, $scriptFileName); + + $this->assertEquals( + $isMinified, + $this->assetMinifier->isMinifiedJs($scriptFile->getContent()) + ); + } +} diff --git a/tests/PHPUnit/Core/AssetManager/configs/merged-assets-disabled.ini.php b/tests/PHPUnit/Core/AssetManager/configs/merged-assets-disabled.ini.php new file mode 100644 index 0000000000..00887b8c52 --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/configs/merged-assets-disabled.ini.php @@ -0,0 +1,2 @@ +[Debug] +disable_merged_assets = 1 diff --git a/tests/PHPUnit/Core/AssetManager/configs/merged-assets-enabled.ini.php b/tests/PHPUnit/Core/AssetManager/configs/merged-assets-enabled.ini.php new file mode 100644 index 0000000000..df10787203 --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/configs/merged-assets-enabled.ini.php @@ -0,0 +1,2 @@ +[Debug] +disable_merged_assets = 0 diff --git a/tests/PHPUnit/Core/AssetManager/configs/plugins.ini.php b/tests/PHPUnit/Core/AssetManager/configs/plugins.ini.php new file mode 100644 index 0000000000..72834098e0 --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/configs/plugins.ini.php @@ -0,0 +1,3 @@ +[Plugins] +Plugins[] = MockCorePlugin +Plugins[] = CoreThemePlugin \ No newline at end of file diff --git a/tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultCore.js b/tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultCore.js new file mode 100644 index 0000000000..823fc76cdb --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultCore.js @@ -0,0 +1,6 @@ +/* Piwik Javascript - cb={{{CACHE-BUSTER-JS}}}*/ + +if(typeof SimpleObject!=='object'){SimpleObject=(function(){var privateVar;function privateMethod(param){privateVar=param;} +return{publicMethod:function(){privateMethod('val');}}}());} +var simpleArray=['el1','el2']; +//This is a simple comment// customization via event \ No newline at end of file diff --git a/tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultNonCore.js b/tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultNonCore.js new file mode 100644 index 0000000000..f004dcc501 --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultNonCore.js @@ -0,0 +1,3 @@ +/* Piwik Javascript - cb={{{CACHE-BUSTER-JS}}}*/ + +alert('test'); \ No newline at end of file diff --git a/tests/PHPUnit/Core/AssetManager/scripts/SimpleAlert.js b/tests/PHPUnit/Core/AssetManager/scripts/SimpleAlert.js new file mode 100644 index 0000000000..003d113a36 --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/scripts/SimpleAlert.js @@ -0,0 +1 @@ +alert('test'); \ No newline at end of file diff --git a/tests/PHPUnit/Core/AssetManager/scripts/SimpleArray.js b/tests/PHPUnit/Core/AssetManager/scripts/SimpleArray.js new file mode 100644 index 0000000000..3176f85cff --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/scripts/SimpleArray.js @@ -0,0 +1 @@ +var simpleArray = ['el1', 'el2']; diff --git a/tests/PHPUnit/Core/AssetManager/scripts/SimpleComments.js b/tests/PHPUnit/Core/AssetManager/scripts/SimpleComments.js new file mode 100644 index 0000000000..5a7103256a --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/scripts/SimpleComments.js @@ -0,0 +1 @@ +//This is a simple comment \ No newline at end of file diff --git a/tests/PHPUnit/Core/AssetManager/scripts/SimpleObject.js b/tests/PHPUnit/Core/AssetManager/scripts/SimpleObject.js new file mode 100644 index 0000000000..aacdf5069c --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/scripts/SimpleObject.js @@ -0,0 +1,18 @@ +if (typeof SimpleObject !== 'object') { + + SimpleObject = (function () { + + var privateVar; + + function privateMethod(param) { + privateVar = param; + } + + return { + + publicMethod: function () { + privateMethod('val'); + } + } + }()); +} diff --git a/tests/PHPUnit/Core/AssetManager/stylesheets/CssWithURLs.css b/tests/PHPUnit/Core/AssetManager/stylesheets/CssWithURLs.css new file mode 100644 index 0000000000..75c919da98 --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/stylesheets/CssWithURLs.css @@ -0,0 +1,12 @@ +h1 { + color: orange; + text-align: center; + /* url relative to root: must not be rewritten*/ + background: url(tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png); +} + +p { + font-size: 20px; + /* url relative to file: must be rewritten*/ + background: url(images/test-image.png); +} \ No newline at end of file diff --git a/tests/PHPUnit/Core/AssetManager/stylesheets/ExpectedMergeResult.css b/tests/PHPUnit/Core/AssetManager/stylesheets/ExpectedMergeResult.css new file mode 100644 index 0000000000..3b86a91e52 --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/stylesheets/ExpectedMergeResult.css @@ -0,0 +1,30 @@ +/* compile_me_once={{{CACHE-BUSTER-JS}}} */ +/* Piwik CSS file is compiled with Less. You may be interested in writing a custom Theme for Piwik! */ +#page #header { + color: white; +} +#footer { + color: red; +} +.box { + color: #fe33ac; + border-color: #fdcdea; +} +.box div { + -webkit-box-shadow: 0 0 5px rgba(0,0,0,0.3); + -moz-box-shadow: 0 0 5px rgba(0,0,0,0.3); + box-shadow: 0 0 5px rgba(0,0,0,0.3); +} +h1 { + color: orange; + text-align: center; + background: url(tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png); +} +p { + font-size: 20px; + background: url(tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png); +} +body { + background-color: #b0c4de; +} +/* customization via event */ \ No newline at end of file diff --git a/tests/PHPUnit/Core/AssetManager/stylesheets/ImportedLess.less b/tests/PHPUnit/Core/AssetManager/stylesheets/ImportedLess.less new file mode 100644 index 0000000000..db566317b6 --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/stylesheets/ImportedLess.less @@ -0,0 +1,12 @@ +@var: red; + +#page { + @var: white; + #header { + color: @var; + } +} + +#footer { + color: @var; +} \ No newline at end of file diff --git a/tests/PHPUnit/Core/AssetManager/stylesheets/SimpleBody.css b/tests/PHPUnit/Core/AssetManager/stylesheets/SimpleBody.css new file mode 100644 index 0000000000..6e0e55777a --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/stylesheets/SimpleBody.css @@ -0,0 +1,3 @@ +body { + background-color:#b0c4de; +} \ No newline at end of file diff --git a/tests/PHPUnit/Core/AssetManager/stylesheets/SimpleLess.less b/tests/PHPUnit/Core/AssetManager/stylesheets/SimpleLess.less new file mode 100644 index 0000000000..fd7b33af31 --- /dev/null +++ b/tests/PHPUnit/Core/AssetManager/stylesheets/SimpleLess.less @@ -0,0 +1,17 @@ +@import "ImportedLess"; + +@base: #f938ab; + +.box-shadow(@style, @c) when (iscolor(@c)) { + -webkit-box-shadow: @style @c; + -moz-box-shadow: @style @c; + box-shadow: @style @c; +} +.box-shadow(@style, @alpha: 50%) when (isnumber(@alpha)) { + .box-shadow(@style, rgba(0, 0, 0, @alpha)); +} +.box { + color: saturate(@base, 5%); + border-color: lighten(@base, 30%); + div { .box-shadow(0 0 5px, 30%) } +} diff --git a/tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png b/tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png new file mode 100644 index 0000000000000000000000000000000000000000..da6e1bbe89dc52e9fdaae1119f1c7353ed997076 GIT binary patch literal 290 zcmV+-0p0$IP)<h;3K|Lk000e1NJLTq000mG000XJ0ssI2`GA^k0002#Nkl<ZD3NVZ zK@LSQ5LKC#orJyESXy!c?&1ngVI^#w!orbAkVq^?`=>3{|MOpyl9~7V=4IeG4u^Bc zb)9c;DyrKCat;K~^Qb5I<uEA|LS5!#VV-B;h`RBxC2v`l^E{&{ve}F=G65GrT$dh6 zv!`hq$I<uweczj=F#<gy@F##QsZ*9^Q558mWf@t8VQ2)12s)f8nfL0t?)%=b)y(X6 zcnaz$48xG7X`bg_8HA96zt1O0634O4HVNO6Y(d+$RaFH+U<>XHHrs5i>pEJFRbkZE of6`%H*LRnQ+H>2spEYa22TbujoNSKsqyPW_07*qoM6N<$f+WX)cmMzZ literal 0 HcmV?d00001 diff --git a/tests/PHPUnit/Core/AssetManagerTest.php b/tests/PHPUnit/Core/AssetManagerTest.php index 21c061c028..0ffc40771d 100644 --- a/tests/PHPUnit/Core/AssetManagerTest.php +++ b/tests/PHPUnit/Core/AssetManagerTest.php @@ -6,38 +6,706 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ use Piwik\AssetManager; +use Piwik\AssetManager\UIAsset\OnDiskUIAsset; +use Piwik\AssetManager\UIAsset; +use Piwik\AssetManager\UIAssetFetcher\StaticUIAssetFetcher; +use Piwik\Config; +use Piwik\EventDispatcher; +use Piwik\Plugin\Manager; +use Piwik\Plugin; +use Piwik\Theme; + +require_once PIWIK_INCLUDE_PATH . "/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterMock.php"; +require_once PIWIK_INCLUDE_PATH . "/tests/PHPUnit/Core/AssetManager/PluginManagerMock.php"; +require_once PIWIK_INCLUDE_PATH . "/tests/PHPUnit/Core/AssetManager/PluginMock.php"; +require_once PIWIK_INCLUDE_PATH . "/tests/PHPUnit/Core/AssetManager/ThemeMock.php"; class AssetManagerTest extends PHPUnit_Framework_TestCase { + // todo Theme->rewriteAssetPathIfOverridesFound is not tested + + const ASSET_MANAGER_TEST_DIR = 'tests/PHPUnit/Core/AssetManager/'; + + const FIRST_CACHE_BUSTER_JS = 'first-cache-buster-js'; + const SECOND_CACHE_BUSTER_JS = 'second-cache-buster-js'; + const FIRST_CACHE_BUSTER_SS = 'first-cache-buster-stylesheet'; + const SECOND_CACHE_BUSTER_SS = 'second-cache-buster-stylesheet'; + + const CORE_PLUGIN_NAME = 'MockCorePlugin'; + const CORE_PLUGIN_WITHOUT_ASSETS_NAME = 'MockCoreWithoutAssetPlugin'; + const NON_CORE_PLUGIN_NAME = 'MockNonCorePlugin'; + const CORE_THEME_PLUGIN_NAME = 'CoreThemePlugin'; + const NON_CORE_THEME_PLUGIN_NAME = 'NonCoreThemePlugin'; + /** - * @group Core + * @var AssetManager + */ + private $assetManager; + + /** + * @var UIAsset + */ + private $mergedAsset; + + /** + * @var UIAssetCacheBusterMock */ - public function testPrioritySort() + private $cacheBuster; + + /** + * @var PluginManagerMock + */ + private $pluginManager; + + public function setUp() + { + $this->activateMergedAssets(); + + $this->setUpCacheBuster(); + + $this->setUpAssetManager(); + + $this->setUpPluginManager(); + + $this->setUpTheme(); + + $this->setUpPlugins(); + } + + public function tearDown() + { + $this->assetManager->removeMergedAssets(); + } + + private function activateMergedAssets() + { + $this->setUpConfig('merged-assets-enabled.ini.php'); + } + + private function disableMergedAssets() + { + $this->setUpConfig('merged-assets-disabled.ini.php'); + } + + /** + * @param string $filename + */ + private function setUpConfig($filename) + { + $userFile = PIWIK_INCLUDE_PATH . '/' . self::ASSET_MANAGER_TEST_DIR . 'configs/' . $filename; + $globalFile = PIWIK_INCLUDE_PATH . '/' . self::ASSET_MANAGER_TEST_DIR . 'configs/plugins.ini.php'; + + $config = Config::getInstance(); + $config->setTestEnvironment($userFile, $globalFile); + $config->init(); + } + + private function setUpCacheBuster() + { + $this->cacheBuster = UIAssetCacheBusterMock::getInstance(); + } + + private function setUpAssetManager() + { + $this->assetManager = AssetManager::getInstance(); + + $this->assetManager->removeMergedAssets(); + + $this->assetManager->setCacheBuster($this->cacheBuster); + } + + private function setUpPluginManager() { - $buckets = array( - 'libs/base.css', - 'libs/', - 'plugins/', + $this->pluginManager = PluginManagerMock::getInstance(); + Manager::setSingletonInstance($this->pluginManager); + } + + private function setUpPlugins() + { + $this->pluginManager->setPlugins( + array( + $this->getCoreTheme()->getPlugin(), + $this->getNonCoreTheme()->getPlugin(), + $this->getCorePlugin(), + $this->getCorePluginWithoutUIAssets(), + $this->getNonCorePlugin() + ) ); - $data = array( - 'plugins/xyz', - 'plugins/abc', - 'libs/xyz', - 'libs/base.css', - 'libs/abc', - 'plugins/xyz', - 'libs/xyz', + $this->pluginManager->setLoadedTheme($this->getNonCoreTheme()); + } + + private function setUpCorePluginOnly() + { + $this->pluginManager->setPlugins( + array( + $this->getCorePlugin(), + ) ); + } + + /** + * @return Plugin + */ + private function getCorePlugin() + { + $corePlugin = new PluginMock(self::CORE_PLUGIN_NAME); - $expected = array( - 'libs/base.css', - 'libs/xyz', - 'libs/abc', - 'plugins/xyz', - 'plugins/abc', + $corePlugin->setJsFiles( + array( + self::ASSET_MANAGER_TEST_DIR . 'scripts/SimpleObject.js', + self::ASSET_MANAGER_TEST_DIR . 'scripts/SimpleArray.js', + ) ); - $this->assertEquals($expected, AssetManager::prioritySort($buckets, $data)); + $corePlugin->setStylesheetFiles($this->getCorePluginStylesheetFiles()); + $corePlugin->setJsCustomization('// customization via event'); + $corePlugin->setCssCustomization('/* customization via event */'); + + return $corePlugin; + } + + /** + * @return Plugin + */ + private function getCorePluginWithoutUIAssets() + { + return new PluginMock(self::CORE_PLUGIN_WITHOUT_ASSETS_NAME); + } + + /** + * @return Plugin + */ + private function getNonCorePlugin() + { + $nonCorePlugin = new PluginMock(self::NON_CORE_PLUGIN_NAME); + $nonCorePlugin->setJsFiles(array(self::ASSET_MANAGER_TEST_DIR . 'scripts/SimpleAlert.js')); + + return $nonCorePlugin; + } + + private function setUpTheme() + { + $this->assetManager->setTheme($this->getCoreTheme()); + } + + /** + * @return ThemeMock + */ + private function getCoreTheme() + { + return $this->createTheme(self::CORE_THEME_PLUGIN_NAME); + } + + /** + * @return ThemeMock + */ + private function getNonCoreTheme() + { + return $this->createTheme(self::NON_CORE_THEME_PLUGIN_NAME); + } + + /** + * @param string $themeName + * @return ThemeMock + */ + private function createTheme($themeName) + { + $coreThemePlugin = new PluginMock($themeName); + + $coreThemePlugin->setIsTheme(true); + + $coreTheme = new ThemeMock($coreThemePlugin); + + $coreTheme->setStylesheet($this->getCoreThemeStylesheet()); + $coreTheme->setJsFiles(array(self::ASSET_MANAGER_TEST_DIR . 'scripts/SimpleComments.js')); + + return $coreTheme; + } + + /** + * @return string[] + */ + public function getCorePluginStylesheetFiles() + { + return array( + self::ASSET_MANAGER_TEST_DIR . 'stylesheets/SimpleLess.less', + self::ASSET_MANAGER_TEST_DIR . 'stylesheets/CssWithURLs.css', + ); + } + + private function clearDateCache() + { + clearstatcache(); + } + + /** + * @return int + */ + private function waitAndGetModificationDate() + { + $this->clearDateCache(); + + sleep(1.5); + + $modificationDate = $this->mergedAsset->getModificationDate(); + + return $modificationDate; + } + + /** + * @param string $cacheBuster + */ + private function setJSCacheBuster($cacheBuster) + { + $this->cacheBuster->setPiwikVersionBasedCacheBuster($cacheBuster); + } + + /** + * @param string $cacheBuster + */ + private function setStylesheetCacheBuster($cacheBuster) + { + $this->cacheBuster->setMd5BasedCacheBuster($cacheBuster); + } + + private function triggerGetMergedCoreJavaScript() + { + $this->mergedAsset = $this->assetManager->getMergedCoreJavaScript(); + } + + private function triggerGetMergedNonCoreJavaScript() + { + $this->mergedAsset = $this->assetManager->getMergedNonCoreJavaScript(); + } + + private function triggerGetMergedStylesheet() + { + $this->mergedAsset = $this->assetManager->getMergedStylesheet(); + } + + private function validateMergedCoreJs() + { + $expectedContent = $this->getExpectedMergedCoreJs(); + + $this->validateExpectedContent($expectedContent); + } + + private function validateMergedNonCoreJs() + { + $expectedContent = $this->getExpectedMergedNonCoreJs(); + + $this->validateExpectedContent($expectedContent); + } + + private function validateMergedStylesheet() + { + $expectedContent = $this->getExpectedMergedStylesheet(); + + $this->validateExpectedContent($expectedContent); + } + + /** + * @param string $expectedContent + */ + private function validateExpectedContent($expectedContent) + { + $this->assertEquals($expectedContent, $this->mergedAsset->getContent()); + } + + /** + * @return string + */ + private function getExpectedMergedCoreJs() + { + return $this->getExpectedMergedJs('ExpectedMergeResultCore.js'); + } + + /** + * @return string + */ + private function getExpectedMergedNonCoreJs() + { + return $this->getExpectedMergedJs('ExpectedMergeResultNonCore.js'); + } + + /** + * @param string $filename + * @return string + */ + private function getExpectedMergedJs($filename) + { + $expectedMergeResult = new OnDiskUIAsset(PIWIK_USER_PATH, self::ASSET_MANAGER_TEST_DIR .'scripts/' . $filename); + + $expectedContent = $expectedMergeResult->getContent(); + + return $this->adjustExpectedJsContent($expectedContent); + } + + /** + * @param string $expectedJsContent + * @return string + */ + private function adjustExpectedJsContent($expectedJsContent) + { + $expectedJsContent = str_replace("\n", "\r\n", $expectedJsContent); + + $expectedJsContent = $this->specifyCacheBusterInExpectedContent($expectedJsContent, $this->cacheBuster->piwikVersionBasedCacheBuster()); + + return $expectedJsContent; + } + + /** + * @return string + */ + private function getExpectedMergedStylesheet() + { + $expectedMergeResult = new OnDiskUIAsset(PIWIK_USER_PATH, self::ASSET_MANAGER_TEST_DIR .'stylesheets/ExpectedMergeResult.css'); + + $expectedContent = $expectedMergeResult->getContent(); + + $expectedContent = $this->specifyCacheBusterInExpectedContent($expectedContent, $this->cacheBuster->md5BasedCacheBuster('')); + + return $expectedContent; + } + + /** + * @return string + */ + private function getCoreThemeStylesheet() + { + return self::ASSET_MANAGER_TEST_DIR . 'stylesheets/SimpleBody.css'; + } + + /** + * @param string $content + * @param string $cacheBuster + * @return string + */ + private function specifyCacheBusterInExpectedContent($content, $cacheBuster) + { + return str_replace('{{{CACHE-BUSTER-JS}}}', $cacheBuster, $content); + } + + /** + * @param int $previousDate + */ + private function validateDateDidNotChange($previousDate) + { + $this->clearDateCache(); + + $this->assertEquals($previousDate, $this->mergedAsset->getModificationDate()); + } + + /** + * @param int $previousDate + */ + private function validateDateIsMoreRecent($previousDate) + { + $this->clearDateCache(); + + $this->assertTrue($previousDate < $this->mergedAsset->getModificationDate()); + } + + /** + * @return string + */ + private function getJsTranslationScript() + { + return + '<script type="text/javascript">' . PHP_EOL . + 'var translations = [];' . PHP_EOL . + 'if(typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }for(var i in translations) { piwik_translations[i] = translations[i];} function _pk_translate(translationStringId) { if( typeof(piwik_translations[translationStringId]) != \'undefined\' ){ return piwik_translations[translationStringId]; }return "The string "+translationStringId+" was not loaded in javascript. Make sure it is added in the Translate.getClientSideTranslationKeys hook.";}' . PHP_EOL . + '</script>'; + } + + /** + * @return UIAsset[] + */ + private function generateAllMergedAssets() + { + $this->triggerGetMergedStylesheet(); + $stylesheetAsset = $this->mergedAsset; + + $this->triggerGetMergedCoreJavaScript(); + $coreJsAsset = $this->mergedAsset; + + $this->triggerGetMergedNonCoreJavaScript(); + $nonCoreJsAsset = $this->mergedAsset; + + $this->assertTrue($stylesheetAsset->exists()); + $this->assertTrue($coreJsAsset->exists()); + $this->assertTrue($nonCoreJsAsset->exists()); + + return array($stylesheetAsset, $coreJsAsset, $nonCoreJsAsset); + } + + /** + * @group Core + */ + public function test_getMergedCoreJavaScript_NotGenerated() + { + $this->setJSCacheBuster(self::FIRST_CACHE_BUSTER_JS); + + $this->triggerGetMergedCoreJavaScript(); + + $this->validateMergedCoreJs(); + } + + /** + * @group Core + */ + public function test_getMergedNonCoreJavaScript_NotGenerated() + { + $this->setJSCacheBuster(self::FIRST_CACHE_BUSTER_JS); + + $this->triggerGetMergedNonCoreJavaScript(); + + $this->validateMergedNonCoreJs(); + } + + /** + * @group Core + */ + public function test_getMergedNonCoreJavaScript_NotGenerated_NoNonCorePlugin() + { + $this->setUpCorePluginOnly(); + + $this->setJSCacheBuster(self::FIRST_CACHE_BUSTER_JS); + + $this->triggerGetMergedNonCoreJavaScript(); + + $expectedContent = $this->adjustExpectedJsContent('/* Piwik Javascript - cb={{{CACHE-BUSTER-JS}}}*/' . PHP_EOL); + + $this->validateExpectedContent($expectedContent); + } + + /** + * @group Core + */ + public function test_getMergedCoreJavaScript_AlreadyGenerated_MergedAssetsDisabled_UpToDate() + { + $this->disableMergedAssets(); + + $this->setJSCacheBuster(self::FIRST_CACHE_BUSTER_JS); + + $this->triggerGetMergedCoreJavaScript(); + + $modDateBeforeSecondRequest = $this->waitAndGetModificationDate(); + + $this->triggerGetMergedCoreJavaScript(); + + $this->validateDateDidNotChange($modDateBeforeSecondRequest); + } + + /** + * @group Core + */ + public function test_getMergedCoreJavaScript_AlreadyGenerated_MergedAssetsDisabled_Stale() + { + $this->disableMergedAssets(); + + $this->setJSCacheBuster(self::FIRST_CACHE_BUSTER_JS); + + $this->triggerGetMergedCoreJavaScript(); + + $modDateBeforeSecondRequest = $this->waitAndGetModificationDate(); + + $this->setJSCacheBuster(self::SECOND_CACHE_BUSTER_JS); + + $this->triggerGetMergedCoreJavaScript(); + + $this->validateDateIsMoreRecent($modDateBeforeSecondRequest); + + $this->validateMergedCoreJs(); + } + + /** + * @group Core + */ + public function test_getMergedStylesheet_NotGenerated() + { + $this->setStylesheetCacheBuster(self::FIRST_CACHE_BUSTER_SS); + + $this->triggerGetMergedStylesheet(); + + $this->validateMergedStylesheet(); + } + + /** + * @group Core + */ + public function test_getMergedStylesheet_Generated_MergedAssetsEnabled_Stale() + { + $this->setStylesheetCacheBuster(self::FIRST_CACHE_BUSTER_SS); + + $this->triggerGetMergedStylesheet(); + + $modDateBeforeSecondRequest = $this->waitAndGetModificationDate(); + + $this->setStylesheetCacheBuster(self::SECOND_CACHE_BUSTER_SS); + + $this->triggerGetMergedStylesheet(); + + $this->validateDateDidNotChange($modDateBeforeSecondRequest); + } + + /** + * @group Core + */ + public function test_getMergedStylesheet_Generated_MergedAssetsDisabled_Stale() + { + $this->disableMergedAssets(); + + $this->setStylesheetCacheBuster(self::FIRST_CACHE_BUSTER_SS); + + $this->triggerGetMergedStylesheet(); + + $modDateBeforeSecondRequest = $this->waitAndGetModificationDate(); + + $this->setStylesheetCacheBuster(self::SECOND_CACHE_BUSTER_SS); + + $this->triggerGetMergedStylesheet(); + + $this->validateDateIsMoreRecent($modDateBeforeSecondRequest); + + $this->validateMergedStylesheet(); + } + + + /** + * @group Core + */ + public function test_getMergedStylesheet_Generated_MergedAssetsDisabled_UpToDate() + { + $this->disableMergedAssets(); + + $this->setStylesheetCacheBuster(self::FIRST_CACHE_BUSTER_SS); + + $this->triggerGetMergedStylesheet(); + + $modDateBeforeSecondRequest = $this->waitAndGetModificationDate(); + + $this->triggerGetMergedStylesheet(); + + $this->validateDateDidNotChange($modDateBeforeSecondRequest); + } + + /** + * @group Core + */ + public function test_getCssInclusionDirective() + { + $expectedCssInclusionDirective = '<link rel="stylesheet" type="text/css" href="index.php?module=Proxy&action=getCss" />' . PHP_EOL; + + $this->assertEquals($expectedCssInclusionDirective, $this->assetManager->getCssInclusionDirective()); + } + + /** + * @group Core + */ + public function test_getJsInclusionDirective_MergedAssetsDisabled() + { + $this->disableMergedAssets(); + + $expectedJsInclusionDirective = + $this->getJsTranslationScript() . + '<script type="text/javascript" src="tests/PHPUnit/Core/AssetManager/scripts/SimpleObject.js"></script>' . PHP_EOL . + '<script type="text/javascript" src="tests/PHPUnit/Core/AssetManager/scripts/SimpleArray.js"></script>' . PHP_EOL . + '<script type="text/javascript" src="tests/PHPUnit/Core/AssetManager/scripts/SimpleComments.js"></script>' . PHP_EOL . + '<script type="text/javascript" src="tests/PHPUnit/Core/AssetManager/scripts/SimpleAlert.js"></script>' . PHP_EOL; + + $this->assertEquals($expectedJsInclusionDirective, $this->assetManager->getJsInclusionDirective()); + } + + /** + * @group Core + */ + public function test_getJsInclusionDirective_MergedAssetsEnabled() + { + $expectedJsInclusionDirective = + $this->getJsTranslationScript() . + '<script type="text/javascript" src="index.php?module=Proxy&action=getCoreJs"></script>' . PHP_EOL . + '<script type="text/javascript" src="index.php?module=Proxy&action=getNonCoreJs"></script>' . PHP_EOL; + + $this->assertEquals($expectedJsInclusionDirective, $this->assetManager->getJsInclusionDirective()); + } + + /** + * @group Core + */ + public function test_getCompiledBaseCss() + { + $this->setStylesheetCacheBuster(self::FIRST_CACHE_BUSTER_SS); + + $staticStylesheetList = array_merge($this->getCorePluginStylesheetFiles(), array($this->getCoreThemeStylesheet())); + + $minimalAssetFetcher = new StaticUIAssetFetcher( + array_reverse($staticStylesheetList), + $staticStylesheetList, + $this->getCoreTheme() + ); + + $this->assetManager->setMinimalStylesheetFetcher($minimalAssetFetcher); + + $this->mergedAsset = $this->assetManager->getCompiledBaseCss(); + + $this->validateMergedStylesheet(); + } + + /** + * @group Core + */ + public function test_removeMergedAssets() + { + list($stylesheetAsset, $coreJsAsset, $nonCoreJsAsset) = $this->generateAllMergedAssets(); + + $this->assetManager->removeMergedAssets(); + + $this->assertFalse($stylesheetAsset->exists()); + $this->assertFalse($coreJsAsset->exists()); + $this->assertFalse($nonCoreJsAsset->exists()); + } + + /** + * @group Core + */ + public function test_removeMergedAssets_PluginNameSpecified_PluginWithoutAssets() + { + list($stylesheetAsset, $coreJsAsset, $nonCoreJsAsset) = $this->generateAllMergedAssets(); + + $this->assetManager->removeMergedAssets(self::CORE_PLUGIN_WITHOUT_ASSETS_NAME); + + $this->assertFalse($stylesheetAsset->exists()); + $this->assertTrue($coreJsAsset->exists()); + $this->assertTrue($nonCoreJsAsset->exists()); + } + + /** + * @group Core + */ + public function test_removeMergedAssets_PluginNameSpecified_CorePlugin() + { + list($stylesheetAsset, $coreJsAsset, $nonCoreJsAsset) = $this->generateAllMergedAssets(); + + $this->assetManager->removeMergedAssets(self::CORE_PLUGIN_NAME); + + $this->assertFalse($stylesheetAsset->exists()); + $this->assertFalse($coreJsAsset->exists()); + $this->assertTrue($nonCoreJsAsset->exists()); + } + + /** + * @group Core + */ + public function test_removeMergedAssets_PluginNameSpecified_NonCoreThemeWithAssets() + { + list($stylesheetAsset, $coreJsAsset, $nonCoreJsAsset) = $this->generateAllMergedAssets(); + + $this->assetManager->removeMergedAssets(self::NON_CORE_THEME_PLUGIN_NAME); + + $this->assertFalse($stylesheetAsset->exists()); + $this->assertTrue($coreJsAsset->exists()); + $this->assertFalse($nonCoreJsAsset->exists()); } -} +} \ No newline at end of file diff --git a/tests/PHPUnit/Core/ServeStaticFileTest.php b/tests/PHPUnit/Core/ServeStaticFileTest.php index 9482227fea..d97fa4a6cf 100644 --- a/tests/PHPUnit/Core/ServeStaticFileTest.php +++ b/tests/PHPUnit/Core/ServeStaticFileTest.php @@ -477,8 +477,7 @@ class Test_Piwik_ServeStaticFile extends PHPUnit_Framework_TestCase private function getCompressedFileLocation() { - $path = PIWIK_PATH_TEST_TO_ROOT . \Piwik\AssetManager::COMPRESSED_FILE_LOCATION . basename(TEST_FILE_LOCATION); - return \Piwik\SettingsPiwik::rewriteTmpPathWithHostname($path); + return \Piwik\AssetManager::getInstance()->getAssetDirectory() . '/' . basename(TEST_FILE_LOCATION); } private function removeCompressedFiles() diff --git a/tests/PHPUnit/UI b/tests/PHPUnit/UI index cbf0922b35..673d21c6a7 160000 --- a/tests/PHPUnit/UI +++ b/tests/PHPUnit/UI @@ -1 +1 @@ -Subproject commit cbf0922b352e6592076908ee1fab095cd8e0642c +Subproject commit 673d21c6a750d8280f4e9ab9d203072dd8d844d2 diff --git a/tests/PHPUnit/UITest.php b/tests/PHPUnit/UITest.php index 5d7c555a32..cd2b86833c 100644 --- a/tests/PHPUnit/UITest.php +++ b/tests/PHPUnit/UITest.php @@ -49,7 +49,7 @@ abstract class UITest extends IntegrationTestCase DbHelper::createAnonymousUser(); - AssetManager::removeMergedAssets(); + AssetManager::getInstance()->removeMergedAssets(); // launch archiving so tests don't run out of time Rules::$purgeDisabledByTests = true; -- GitLab