Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Moumné <julien@piwik.org>2013-12-17 20:52:36 +0400
committerJulien Moumné <julien@piwik.org>2013-12-17 20:52:36 +0400
commit6624e27e575d6056da7881bc05e9972c50ec1128 (patch)
tree06bec63f550d16ccc8a428d40a79604496ce0985
parent5d44202fb79a9b2aa70b00ec88fd316f83a440b0 (diff)
fixes #4373, #1640
-rw-r--r--LEGALNOTICE10
-rw-r--r--composer.json3
-rw-r--r--core/AssetManager.php710
-rw-r--r--core/AssetManager/UIAsset.php63
-rw-r--r--core/AssetManager/UIAsset/InMemoryUIAsset.php65
-rw-r--r--core/AssetManager/UIAsset/OnDiskUIAsset.php115
-rw-r--r--core/AssetManager/UIAssetCacheBuster.php47
-rw-r--r--core/AssetManager/UIAssetCatalog.php83
-rw-r--r--core/AssetManager/UIAssetCatalogSorter.php61
-rw-r--r--core/AssetManager/UIAssetFetcher.php121
-rw-r--r--core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php85
-rw-r--r--core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php39
-rw-r--r--core/AssetManager/UIAssetFetcher/StylesheetUIAssetFetcher.php70
-rw-r--r--core/AssetManager/UIAssetMerger.php212
-rw-r--r--core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php91
-rw-r--r--core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php142
-rw-r--r--core/AssetManager/UIAssetMinifier.php67
-rw-r--r--core/Db/BatchInsert.php2
-rw-r--r--core/Filesystem.php4
-rw-r--r--core/Plugin.php2
-rw-r--r--core/Plugin/Manager.php31
-rw-r--r--core/ProxyHttp.php3
-rw-r--r--core/Theme.php26
-rw-r--r--core/Twig.php4
-rw-r--r--core/View.php3
-rw-r--r--libs/cssmin/MIT-LICENSE.txt19
-rw-r--r--libs/cssmin/cssmin.php47
-rw-r--r--libs/jsmin/jsmin.php291
-rw-r--r--plugins/Installation/Controller.php2
-rw-r--r--plugins/Proxy/Controller.php33
-rw-r--r--tests/PHPUnit/Core/AssetManager/PluginManagerMock.php70
-rw-r--r--tests/PHPUnit/Core/AssetManager/PluginMock.php132
-rw-r--r--tests/PHPUnit/Core/AssetManager/ThemeMock.php76
-rw-r--r--tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterMock.php48
-rw-r--r--tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterTest.php31
-rw-r--r--tests/PHPUnit/Core/AssetManager/UIAssetCatalogSorterTest.php52
-rw-r--r--tests/PHPUnit/Core/AssetManager/UIAssetMinifierTest.php55
-rw-r--r--tests/PHPUnit/Core/AssetManager/configs/merged-assets-disabled.ini.php2
-rw-r--r--tests/PHPUnit/Core/AssetManager/configs/merged-assets-enabled.ini.php2
-rw-r--r--tests/PHPUnit/Core/AssetManager/configs/plugins.ini.php3
-rw-r--r--tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultCore.js6
-rw-r--r--tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultNonCore.js3
-rw-r--r--tests/PHPUnit/Core/AssetManager/scripts/SimpleAlert.js1
-rw-r--r--tests/PHPUnit/Core/AssetManager/scripts/SimpleArray.js1
-rw-r--r--tests/PHPUnit/Core/AssetManager/scripts/SimpleComments.js1
-rw-r--r--tests/PHPUnit/Core/AssetManager/scripts/SimpleObject.js18
-rw-r--r--tests/PHPUnit/Core/AssetManager/stylesheets/CssWithURLs.css12
-rw-r--r--tests/PHPUnit/Core/AssetManager/stylesheets/ExpectedMergeResult.css30
-rw-r--r--tests/PHPUnit/Core/AssetManager/stylesheets/ImportedLess.less12
-rw-r--r--tests/PHPUnit/Core/AssetManager/stylesheets/SimpleBody.css3
-rw-r--r--tests/PHPUnit/Core/AssetManager/stylesheets/SimpleLess.less17
-rw-r--r--tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.pngbin0 -> 290 bytes
-rw-r--r--tests/PHPUnit/Core/AssetManagerTest.php712
-rw-r--r--tests/PHPUnit/Core/ServeStaticFileTest.php3
m---------tests/PHPUnit/UI0
-rw-r--r--tests/PHPUnit/UITest.php2
56 files changed, 2817 insertions, 926 deletions
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 0000000000..da6e1bbe89
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png
Binary files differ
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
-Subproject cbf0922b352e6592076908ee1fab095cd8e0642
+Subproject 673d21c6a750d8280f4e9ab9d203072dd8d844d
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;