From 6624e27e575d6056da7881bc05e9972c50ec1128 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Julien=20Moumn=C3=A9?= <julien@piwik.org>
Date: Tue, 17 Dec 2013 17:52:36 +0100
Subject: [PATCH] fixes #4373, #1640

---
 LEGALNOTICE                                   |  10 -
 composer.json                                 |   3 +-
 core/AssetManager.php                         | 710 +++++------------
 core/AssetManager/UIAsset.php                 |  63 ++
 core/AssetManager/UIAsset/InMemoryUIAsset.php |  65 ++
 core/AssetManager/UIAsset/OnDiskUIAsset.php   | 115 +++
 core/AssetManager/UIAssetCacheBuster.php      |  47 ++
 core/AssetManager/UIAssetCatalog.php          |  83 ++
 core/AssetManager/UIAssetCatalogSorter.php    |  61 ++
 core/AssetManager/UIAssetFetcher.php          | 121 +++
 .../UIAssetFetcher/JScriptUIAssetFetcher.php  |  85 +++
 .../UIAssetFetcher/StaticUIAssetFetcher.php   |  39 +
 .../StylesheetUIAssetFetcher.php              |  70 ++
 core/AssetManager/UIAssetMerger.php           | 212 ++++++
 .../UIAssetMerger/JScriptUIAssetMerger.php    |  91 +++
 .../UIAssetMerger/StylesheetUIAssetMerger.php | 142 ++++
 core/AssetManager/UIAssetMinifier.php         |  67 ++
 core/Db/BatchInsert.php                       |   2 +-
 core/Filesystem.php                           |   4 +-
 core/Plugin.php                               |   2 +-
 core/Plugin/Manager.php                       |  31 +-
 core/ProxyHttp.php                            |   3 +-
 core/Theme.php                                |  26 +-
 core/Twig.php                                 |   4 +-
 core/View.php                                 |   3 +-
 libs/cssmin/MIT-LICENSE.txt                   |  19 -
 libs/cssmin/cssmin.php                        |  47 --
 libs/jsmin/jsmin.php                          | 291 -------
 plugins/Installation/Controller.php           |   2 +-
 plugins/Proxy/Controller.php                  |  33 +-
 .../Core/AssetManager/PluginManagerMock.php   |  70 ++
 .../PHPUnit/Core/AssetManager/PluginMock.php  | 132 ++++
 tests/PHPUnit/Core/AssetManager/ThemeMock.php |  76 ++
 .../AssetManager/UIAssetCacheBusterMock.php   |  48 ++
 .../AssetManager/UIAssetCacheBusterTest.php   |  31 +
 .../AssetManager/UIAssetCatalogSorterTest.php |  52 ++
 .../Core/AssetManager/UIAssetMinifierTest.php |  55 ++
 .../configs/merged-assets-disabled.ini.php    |   2 +
 .../configs/merged-assets-enabled.ini.php     |   2 +
 .../Core/AssetManager/configs/plugins.ini.php |   3 +
 .../scripts/ExpectedMergeResultCore.js        |   6 +
 .../scripts/ExpectedMergeResultNonCore.js     |   3 +
 .../Core/AssetManager/scripts/SimpleAlert.js  |   1 +
 .../Core/AssetManager/scripts/SimpleArray.js  |   1 +
 .../AssetManager/scripts/SimpleComments.js    |   1 +
 .../Core/AssetManager/scripts/SimpleObject.js |  18 +
 .../AssetManager/stylesheets/CssWithURLs.css  |  12 +
 .../stylesheets/ExpectedMergeResult.css       |  30 +
 .../stylesheets/ImportedLess.less             |  12 +
 .../AssetManager/stylesheets/SimpleBody.css   |   3 +
 .../AssetManager/stylesheets/SimpleLess.less  |  17 +
 .../stylesheets/images/test-image.png         | Bin 0 -> 290 bytes
 tests/PHPUnit/Core/AssetManagerTest.php       | 712 +++++++++++++++++-
 tests/PHPUnit/Core/ServeStaticFileTest.php    |   3 +-
 tests/PHPUnit/UI                              |   2 +-
 tests/PHPUnit/UITest.php                      |   2 +-
 56 files changed, 2818 insertions(+), 927 deletions(-)
 create mode 100644 core/AssetManager/UIAsset.php
 create mode 100644 core/AssetManager/UIAsset/InMemoryUIAsset.php
 create mode 100644 core/AssetManager/UIAsset/OnDiskUIAsset.php
 create mode 100644 core/AssetManager/UIAssetCacheBuster.php
 create mode 100644 core/AssetManager/UIAssetCatalog.php
 create mode 100644 core/AssetManager/UIAssetCatalogSorter.php
 create mode 100644 core/AssetManager/UIAssetFetcher.php
 create mode 100644 core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php
 create mode 100644 core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php
 create mode 100644 core/AssetManager/UIAssetFetcher/StylesheetUIAssetFetcher.php
 create mode 100644 core/AssetManager/UIAssetMerger.php
 create mode 100644 core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php
 create mode 100644 core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php
 create mode 100644 core/AssetManager/UIAssetMinifier.php
 delete mode 100644 libs/cssmin/MIT-LICENSE.txt
 delete mode 100644 libs/cssmin/cssmin.php
 delete mode 100644 libs/jsmin/jsmin.php
 create mode 100644 tests/PHPUnit/Core/AssetManager/PluginManagerMock.php
 create mode 100644 tests/PHPUnit/Core/AssetManager/PluginMock.php
 create mode 100644 tests/PHPUnit/Core/AssetManager/ThemeMock.php
 create mode 100644 tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterMock.php
 create mode 100644 tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterTest.php
 create mode 100644 tests/PHPUnit/Core/AssetManager/UIAssetCatalogSorterTest.php
 create mode 100644 tests/PHPUnit/Core/AssetManager/UIAssetMinifierTest.php
 create mode 100644 tests/PHPUnit/Core/AssetManager/configs/merged-assets-disabled.ini.php
 create mode 100644 tests/PHPUnit/Core/AssetManager/configs/merged-assets-enabled.ini.php
 create mode 100644 tests/PHPUnit/Core/AssetManager/configs/plugins.ini.php
 create mode 100644 tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultCore.js
 create mode 100644 tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultNonCore.js
 create mode 100644 tests/PHPUnit/Core/AssetManager/scripts/SimpleAlert.js
 create mode 100644 tests/PHPUnit/Core/AssetManager/scripts/SimpleArray.js
 create mode 100644 tests/PHPUnit/Core/AssetManager/scripts/SimpleComments.js
 create mode 100644 tests/PHPUnit/Core/AssetManager/scripts/SimpleObject.js
 create mode 100644 tests/PHPUnit/Core/AssetManager/stylesheets/CssWithURLs.css
 create mode 100644 tests/PHPUnit/Core/AssetManager/stylesheets/ExpectedMergeResult.css
 create mode 100644 tests/PHPUnit/Core/AssetManager/stylesheets/ImportedLess.less
 create mode 100644 tests/PHPUnit/Core/AssetManager/stylesheets/SimpleBody.css
 create mode 100644 tests/PHPUnit/Core/AssetManager/stylesheets/SimpleLess.less
 create mode 100644 tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png

diff --git a/LEGALNOTICE b/LEGALNOTICE
index 3863c68c12..f394df67e4 100644
--- a/LEGALNOTICE
+++ b/LEGALNOTICE
@@ -114,16 +114,6 @@ THIRD-PARTY COMPONENTS AND LIBRARIES
 	Notes:
 	- reference implementation
 
-	Name:  cssmin
-	Link:  http://code.google.com/p/cssmin/
-	License:  MIT
-
-	Name:  jsmin
-	Link:  http://code.google.com/p/jsmin-php/
-	License:  MIT
-	Notes:
-	- contains additional clause "The Software shall be used for Good, not Evil."
-
 	Name:  sparkline
 	Link:  https//sourceforge.net/projects/sparkline/
 	License:  Dual-licensed: New BSD or GPL v2
diff --git a/composer.json b/composer.json
index 190c5de277..039faa8a66 100644
--- a/composer.json
+++ b/composer.json
@@ -22,6 +22,7 @@
         "php": ">=5.3.0",
         "twig/twig": "1.*",
         "leafo/lessphp": "0.3.*",
-        "symfony/console": ">=v2.3.5"
+        "symfony/console": ">=v2.3.5",
+        "tedivm/jshrink": "v0.5.1"
     }
 }
diff --git a/core/AssetManager.php b/core/AssetManager.php
index ec850de0db..1104ed42d8 100644
--- a/core/AssetManager.php
+++ b/core/AssetManager.php
@@ -11,14 +11,19 @@
 namespace Piwik;
 
 use Exception;
-use JSMin;
-use lessc;
+use Piwik\AssetManager\UIAsset;
+use Piwik\AssetManager\UIAsset\InMemoryUIAsset;
+use Piwik\AssetManager\UIAsset\OnDiskUIAsset;
+use Piwik\AssetManager\UIAssetCacheBuster;
+use Piwik\AssetManager\UIAssetFetcher\JScriptUIAssetFetcher;
+use Piwik\AssetManager\UIAssetFetcher\StaticUIAssetFetcher;
+use Piwik\AssetManager\UIAssetFetcher\StylesheetUIAssetFetcher;
+use Piwik\AssetManager\UIAssetFetcher;
+use Piwik\AssetManager\UIAssetMerger\JScriptUIAssetMerger;
+use Piwik\AssetManager\UIAssetMerger\StylesheetUIAssetMerger;
+use Piwik\Plugin\Manager;
 use Piwik\Translate;
-
-/**
- * @see libs/jsmin/jsmin.php
- */
-require_once PIWIK_INCLUDE_PATH . '/libs/jsmin/jsmin.php';
+use Piwik\Config as PiwikConfig;
 
 /**
  * AssetManager is the class used to manage the inclusion of UI assets:
@@ -36,678 +41,361 @@ require_once PIWIK_INCLUDE_PATH . '/libs/jsmin/jsmin.php';
  * When set to 0, files will be included within a pair of files: 1 JavaScript
  * and 1 css file.
  *
+ * @method static \Piwik\AssetManager getInstance()
  * @package Piwik
  */
-class AssetManager
+class AssetManager extends Singleton
 {
     const MERGED_CSS_FILE = "asset_manager_global_css.css";
-    const MERGED_JS_FILE = "asset_manager_global_js.js";
-    const STYLESHEET_IMPORT_EVENT = "AssetManager.getStylesheetFiles";
-    const JAVASCRIPT_IMPORT_EVENT = "AssetManager.getJavaScriptFiles";
-    const MERGED_FILE_DIR = "tmp/assets/";
-    const COMPRESSED_FILE_LOCATION = "/tmp/assets/";
+    const MERGED_CORE_JS_FILE = "asset_manager_core_js.js";
+    const MERGED_NON_CORE_JS_FILE = "asset_manager_non_core_js.js";
 
     const CSS_IMPORT_DIRECTIVE = "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\" />\n";
     const JS_IMPORT_DIRECTIVE = "<script type=\"text/javascript\" src=\"%s\"></script>\n";
     const GET_CSS_MODULE_ACTION = "index.php?module=Proxy&action=getCss";
-    const GET_JS_MODULE_ACTION = "index.php?module=Proxy&action=getJs";
-    const MINIFIED_JS_RATIO = 100;
+    const GET_CORE_JS_MODULE_ACTION = "index.php?module=Proxy&action=getCoreJs";
+    const GET_NON_CORE_JS_MODULE_ACTION = "index.php?module=Proxy&action=getNonCoreJs";
 
     /**
-     * @param $file
-     * @param $less
-     * @internal param $mergedContent
-     * @return string
+     * @var UIAssetCacheBuster
      */
-    protected static function getCssContentFromFile($file, $less)
-    {
-        self::validateCssFile($file);
-
-        $fileLocation = self::getAbsoluteLocation($file);
-        $less->addImportDir(dirname($fileLocation));
-
-        $content = file_get_contents($fileLocation);
-        $content = self::rewriteCssPathsDirectives($file, $content);
-
-        return $content;
-    }
+    private $cacheBuster;
 
     /**
-     * Returns CSS file inclusion directive(s) using the markup <link>
-     *
-     * @return string
+     * @var UIAssetFetcher
      */
-    public static function getCssAssets()
-    {
-        return sprintf(self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION);
-    }
+    private $minimalStylesheetFetcher;
 
     /**
-     * Returns JS file inclusion directive(s) using the markup <script>
-     *
-     * @return string
+     * @var Theme
      */
-    public static function getJsAssets()
-    {
-        $result = "<script type=\"text/javascript\">\n" . Translate::getJavascriptTranslations() . "\n</script>";
+    private $theme;
 
-        if (self::isMergedAssetsDisabled()) {
-            // Individual includes mode
-            self::removeMergedAsset(self::MERGED_JS_FILE);
-            $result .= self::getIndividualJsIncludes();
-        } else {
-            $result .= sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_JS_MODULE_ACTION);
-        }
+    function __construct()
+    {
+        $this->cacheBuster = UIAssetCacheBuster::getInstance();
+        $this->minimalStylesheetFetcher =  new StaticUIAssetFetcher(array('plugins/Zeitgeist/stylesheets/base.less'), array(), $this->theme);
 
-        return $result;
+        if(Manager::getInstance()->getThemeEnabled() != null)
+            $this->theme = new Theme();
     }
 
     /**
-     * Assets are cached in the browser and Piwik server returns 304 after initial download.
-     * when the Cache buster string changes, the assets will be re-generated
-     *
-     * @return string
+     * @param UIAssetCacheBuster $cacheBuster
      */
-    public static function generateAssetsCacheBuster()
+    public function setCacheBuster($cacheBuster)
     {
-        $currentGitHash = @file_get_contents(PIWIK_INCLUDE_PATH . '/.git/refs/heads/master');
-        $pluginList = md5(implode(",", \Piwik\Plugin\Manager::getInstance()->getLoadedPluginsName()));
-        $cacheBuster = md5(SettingsPiwik::getSalt() . $pluginList . PHP_VERSION . Version::VERSION . trim($currentGitHash));
-        return $cacheBuster;
+        $this->cacheBuster = $cacheBuster;
     }
 
     /**
-     * Generate the merged css file.
-     *
-     * @throws Exception if a file can not be opened in write mode
+     * @param UIAssetFetcher $minimalStylesheetFetcher
      */
-    private static function prepareMergedCssFile()
+    public function setMinimalStylesheetFetcher($minimalStylesheetFetcher)
     {
-        $mergedCssAlreadyGenerated = self::isGenerated(self::MERGED_CSS_FILE);
-        $isDevelopingPiwik = self::isMergedAssetsDisabled();
-
-        if ($mergedCssAlreadyGenerated && !$isDevelopingPiwik) {
-            return;
-        }
-
-        $files = self::getStylesheetFiles();
-        $less = self::makeLess();
-
-        // Loop through each css file
-        $mergedContent = "";
-        foreach ($files as $file) {
-            $mergedContent .= self::getCssContentFromFile($file, $less, $mergedContent);
-        }
-
-        $fileHash = md5($mergedContent);
-        $firstLineCompileHash = "/* compile_me_once=$fileHash */";
-
-        // Disable Merged Assets ==> Check on each request if file needs re-compiling
-        if ($mergedCssAlreadyGenerated
-            && !$isDevelopingPiwik
-        ) {
-            $mergedFile = self::MERGED_CSS_FILE;
-            $cacheIsValid = self::isFirstLineMatching($mergedFile, $firstLineCompileHash);
-            if($cacheIsValid) {
-                return;
-            }
-            // Some CSS file in the merge, has changed since last merged asset was generated
-            // Note: we do not detect changes in @import'ed LESS files
-        }
-
-        $mergedContent = $less->compile($mergedContent);
-
-        /**
-         * Triggered after all less stylesheets are compiled to CSS, minified and merged into
-         * one file, but before the generated CSS is written to disk.
-         * 
-         * This event can be used to modify merged CSS.
-         * 
-         * @param string &$mergedContent The merged and minified CSS.
-         */
-        Piwik::postEvent('AssetManager.filterMergedStylesheets', array(&$mergedContent));
-
-        $theme = new Theme;
-        $mergedContent = $theme->rewriteAssetsPathToTheme($mergedContent);
-
-        $mergedContent =
-            $firstLineCompileHash . "\n"
-            . "/* Piwik CSS file is compiled with Less. You may be interested in writing a custom Theme for Piwik! */\n"
-            . $mergedContent;
-
-        self::writeAssetToFile($mergedContent, self::MERGED_CSS_FILE);
+        $this->minimalStylesheetFetcher = $minimalStylesheetFetcher;
     }
 
-    protected static function makeLess()
+    /**
+     * @param Theme $theme
+     */
+    public function setTheme($theme)
     {
-        if (!class_exists("lessc")) {
-            throw new Exception("Less was added to composer during 2.0. ==> Execute this command to update composer packages: \$ php composer.phar install");
-        }
-        $less = new lessc;
-        return $less;
+        $this->theme = $theme;
     }
 
     /**
-     * Returns the base.less compiled to css
+     * Return CSS file inclusion directive(s) using the markup <link>
      *
      * @return string
      */
-    public static function getCompiledBaseCss()
+    public function getCssInclusionDirective()
     {
-        $file = '/plugins/Zeitgeist/stylesheets/base.less';
-        $less = self::makeLess();
-        $lessContent = self::getCssContentFromFile($file, $less);
-        $css = $less->compile($lessContent);
-        return $css;
+        return sprintf(self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION);
     }
 
-    /*
-     * Rewrite css url directives
-     * - rewrites relative paths
-     * - rewrite windows directory separator \\ to /
+    /**
+     * Return JS file inclusion directive(s) using the markup <script>
+     *
+     * @return string
      */
-    protected static function rewriteCssPathsDirectives($relativePath, $content)
+    public function getJsInclusionDirective()
     {
-        static $rootDirectoryLength = null;
-        if (is_null($rootDirectoryLength)) {
-            $rootDirectoryLength = self::countDirectoriesInPathToRoot();
-        }
+        $result = "<script type=\"text/javascript\">\n" . Translate::getJavascriptTranslations() . "\n</script>";
 
-        $baseDirectory = dirname($relativePath);
-        $content = preg_replace_callback(
-            "/(url\(['\"]?)([^'\")]*)/",
-            function ($matches) use ($rootDirectoryLength, $baseDirectory) {
-                $absolutePath = substr(realpath(PIWIK_DOCUMENT_ROOT . "/$baseDirectory/" . $matches[2]), $rootDirectoryLength);
-                $rewritten = str_replace('\\', '/', $absolutePath);
+        if ($this->isMergedAssetsDisabled()) {
 
-                if (is_file($rewritten)) { // only rewrite the URL if transforming it points to a valid file
-                    return $matches[1] . $rewritten;
-                } else {
-                    return $matches[1] . $matches[2];
-                }
-            },
-            $content
-        );
-        return $content;
-    }
+            $this->getMergedCoreJSAsset()->delete();
+            $this->getMergedNonCoreJSAsset()->delete();
 
-    protected static function countDirectoriesInPathToRoot()
-    {
-        $rootDirectory = realpath(PIWIK_DOCUMENT_ROOT);
-        if ($rootDirectory != '/' && substr_compare($rootDirectory, '/', -1)) {
-            $rootDirectory .= '/';
-        }
-        $rootDirectoryLen = strlen($rootDirectory);
-        return $rootDirectoryLen;
-    }
+            $result .= $this->getIndividualJsIncludes();
 
-    private static function writeAssetToFile($mergedContent, $name)
-    {
-        // Remove the previous file
-        self::removeMergedAsset($name);
-
-        // Tries to open the new file
-        $newFilePath = self::getAbsoluteMergedFileLocation($name);
-        $newFile = @fopen($newFilePath, "w");
+        } else {
 
-        if (!$newFile) {
-            throw new Exception ("The file : " . $newFile . " can not be opened in write mode.");
+            $result .= sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_CORE_JS_MODULE_ACTION);
+            $result .= sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_NON_CORE_JS_MODULE_ACTION);
         }
 
-        // Write the content in the new file
-        fwrite($newFile, $mergedContent);
-        fclose($newFile);
+        return $result;
     }
 
     /**
-     * Returns individual CSS file inclusion directive(s) using the markup <link>
+     * Return the base.less compiled to css
      *
-     * @return string
+     * @return UIAsset
      */
-    private static function getIndividualCssIncludes()
+    public function getCompiledBaseCss()
     {
-        $cssIncludeString = '';
-
-        $stylesheets = self::getStylesheetFiles();
+        $mergedAsset = new InMemoryUIAsset();
 
-        foreach ($stylesheets as $cssFile) {
+        $assetMerger = new StylesheetUIAssetMerger($mergedAsset, $this->minimalStylesheetFetcher, $this->cacheBuster);
 
-            self::validateCssFile($cssFile);
-            $cssIncludeString = $cssIncludeString . sprintf(self::CSS_IMPORT_DIRECTIVE, $cssFile);
-        }
+        $assetMerger->generateFile();
 
-        return $cssIncludeString;
+        return $mergedAsset;
     }
 
     /**
-     * Returns required CSS files
+     * Return the css merged file absolute location.
+     * If there is none, the generation process will be triggered.
      *
-     * @return Array
+     * @return UIAsset
      */
-    private static function getStylesheetFiles()
+    public function getMergedStylesheet()
     {
-        $stylesheets = array();
-
-        /**
-         * Triggered when gathering the list of all stylesheets (CSS and LESS) needed by
-         * Piwik and its plugins.
-         * 
-         * Plugins that have stylesheets should use this event to make those stylesheets
-         * load.
-         * 
-         * Stylesheets should be placed within a **stylesheets** subdirectory in your plugin's
-         * root directory.
-         * 
-         * _Note: While you are developing your plugin you should enable the config setting
-         * `[Debug] disable_merged_assets` so your stylesheets will be reloaded immediately
-         * after a change._
-         *
-         * **Example**
-         * 
-         *     public function getStylesheetFiles(&$stylesheets)
-         *     {
-         *         $stylesheets[] = "plugins/MyPlugin/stylesheets/myfile.less";
-         *         $stylesheets[] = "plugins/MyPlugin/stylesheets/myotherfile.css";
-         *     }
-         * 
-         * @param string[] &$stylesheets The list of stylesheet paths.
-         */
-        Piwik::postEvent(self::STYLESHEET_IMPORT_EVENT, array(&$stylesheets));
-
-        $stylesheets = self::sortCssFiles($stylesheets);
-
-        $theme = new Theme;
-        $themeStylesheet = $theme->getStylesheet();
-        if($themeStylesheet) {
-            $stylesheets[] = $themeStylesheet;
-        }
+        $mergedAsset = $this->getMergedStylesheetAsset();
 
-        return $stylesheets;
-    }
+        $assetFetcher = new StylesheetUIAssetFetcher(Manager::getInstance()->getLoadedPluginsName(), $this->theme);
 
-    /**
-     * Ensure CSS stylesheets are loaded in a particular order regardless of the order that plugins are loaded.
-     *
-     * @param array $stylesheets Array of CSS stylesheet files
-     * @return array
-     */
-    private static function sortCssFiles($stylesheets)
-    {
-        $priorityCssOrdered = array(
-            'libs/',
-            'plugins/CoreHome/stylesheets/color_manager.css', // must be before other Piwik stylesheets
-            'plugins/Zeitgeist/stylesheets/base.less',
-            'plugins/Zeitgeist/stylesheets/',
-            'plugins/',
-            'plugins/Dashboard/stylesheets/dashboard.less',
-            'tests/',
-        );
-
-        return self::prioritySort($priorityCssOrdered, $stylesheets);
+        $assetMerger = new StylesheetUIAssetMerger($mergedAsset, $assetFetcher, $this->cacheBuster);
+
+        $assetMerger->generateFile();
+
+        return $mergedAsset;
     }
 
     /**
-     * Check the validity of the css file
+     * Return the core js merged file absolute location.
+     * If there is none, the generation process will be triggered.
      *
-     * @param string $cssFile CSS file name
-     * @return boolean
-     * @throws Exception if a file can not be opened in write mode
+     * @return UIAsset
      */
-    private static function validateCssFile($cssFile)
+    public function getMergedCoreJavaScript()
     {
-        if (!self::assetIsReadable($cssFile)) {
-            throw new Exception("The css asset with 'href' = " . $cssFile . " is not readable");
-        }
+        return $this->getMergedJavascript($this->getCoreJScriptFetcher(), $this->getMergedCoreJSAsset());
     }
 
     /**
-     * Generate the merged js file.
+     * Return the non core js merged file absolute location.
+     * If there is none, the generation process will be triggered.
      *
-     * @throws Exception if a file can not be opened in write mode
+     * @return UIAsset
      */
-    private static function generateMergedJsFile()
+    public function getMergedNonCoreJavaScript()
     {
-        $mergedContent = self::getFirstLineOfMergedJs();
-
-        $files = self::getJsFiles();
-        foreach ($files as $file) {
-            self::validateJsFile($file);
-            $fileLocation = self::getAbsoluteLocation($file);
-            $content = file_get_contents($fileLocation);
-            if (!self::isMinifiedJs($content)) {
-                $content = JSMin::minify($content);
-            }
-            $mergedContent = $mergedContent . PHP_EOL . $content;
-        }
-        $mergedContent = str_replace("\n", "\r\n", $mergedContent);
-
-        /**
-         * Triggered after all the JavaScript files Piwik uses are minified and merged into a
-         * single file, but before the merged JavaScript is written to disk.
-         * 
-         * Plugins can use this event to modify merged JavaScript or do something else
-         * with it.
-         * 
-         * @param string &$mergedContent The minified and merged JavaScript.
-         */
-        Piwik::postEvent('AssetManager.filterMergedJavaScripts', array(&$mergedContent));
-
-        $theme = new Theme;
-        $mergedContent = $theme->rewriteAssetsPathToTheme($mergedContent);
-
-        self::writeAssetToFile($mergedContent, self::MERGED_JS_FILE);
+        return $this->getMergedJavascript($this->getNonCoreJScriptFetcher(), $this->getMergedNonCoreJSAsset());
     }
 
     /**
-     * Returns individual JS file inclusion directive(s) using the markup <script>
-     *
-     * @return string
+     * @param boolean $core
+     * @return string[]
      */
-    private static function getIndividualJsIncludes()
+    public function getLoadedPlugins($core)
     {
-        $jsIncludeString = '';
+        $loadedPlugins = array();
 
-        $jsFiles = self::getJsFiles();
-        foreach ($jsFiles as $jsFile) {
-            self::validateJsFile($jsFile);
-            $jsIncludeString = $jsIncludeString . sprintf(self::JS_IMPORT_DIRECTIVE, $jsFile);
+        foreach(Manager::getInstance()->getLoadedPluginsName() as $pluginName) {
+
+            $pluginIsCore = Manager::getInstance()->isPluginBundledWithCore($pluginName);
+
+            if(($pluginIsCore && $core) || (!$pluginIsCore && !$core))
+                $loadedPlugins[] = $pluginName;
         }
-        return $jsIncludeString;
+
+        return $loadedPlugins;
     }
 
     /**
-     * Returns required JS files
-     *
-     * @return Array
+     * Remove previous merged assets
      */
-    private static function getJsFiles()
+    public function removeMergedAssets($pluginName = false)
     {
-        $jsFiles = array();
-
-        /**
-         * Triggered when gathering the list of all JavaScript files needed by Piwik
-         * and its plugins.
-         * 
-         * Plugins that have their own JavaScript should use this event to make those
-         * files load in the browser.
-         * 
-         * JavaScript files should be placed within a **javascripts** subdirectory in your
-         * plugin's root directory.
-         * 
-         * _Note: While you are developing your plugin you should enable the config setting
-         * `[Debug] disable_merged_assets` so JavaScript files will be reloaded immediately
-         * after every change._
-         *
-         * **Example**
-         * 
-         *     public function getJsFiles(&$jsFiles)
-         *     {
-         *         $jsFiles[] = "plugins/MyPlugin/javascripts/myfile.js";
-         *         $jsFiles[] = "plugins/MyPlugin/javascripts/anotherone.js";
-         *     }
-         * 
-         * @param string[] $jsFiles The JavaScript files to load.
-         */
-        Piwik::postEvent(self::JAVASCRIPT_IMPORT_EVENT, array(&$jsFiles));
-
-        $theme = new Theme;
-        $jsInThemes = $theme->getJavaScriptFiles();
-        if(!empty($jsInThemes)) {
-            $jsFiles = array_merge($jsFiles, $jsInThemes);
+        $assetsToRemove = array($this->getMergedStylesheetAsset());
+
+        if($pluginName) {
+
+            if($this->pluginContainsJScriptAssets($pluginName)) {
+
+                PiwikConfig::getInstance()->init();
+                if(Manager::getInstance()->isPluginBundledWithCore($pluginName)) {
+
+                    $assetsToRemove[] = $this->getMergedCoreJSAsset();
+
+                } else {
+
+                    $assetsToRemove[] = $this->getMergedNonCoreJSAsset();
+                }
+            }
+
+        } else {
+
+            $assetsToRemove[] = $this->getMergedCoreJSAsset();
+            $assetsToRemove[] = $this->getMergedNonCoreJSAsset();
         }
 
-        $jsFiles = self::sortJsFiles($jsFiles);
-        return $jsFiles;
+        $this->removeAssets($assetsToRemove);
     }
 
     /**
-     * Ensure core JS (jQuery etc.) are loaded in a particular order regardless of the order that plugins are loaded.
+     * Check if the merged file directory exists and is writable.
      *
-     * @param array $jsFiles Arry of JavaScript files
-     * @return array
+     * @return string The directory location
+     * @throws Exception if directory is not writable.
      */
-    private static function sortJsFiles($jsFiles)
+    public function getAssetDirectory()
     {
-        $priorityJsOrdered = array(
-            'libs/jquery/jquery.js',
-            'libs/jquery/jquery-ui.js',
-            'libs/jquery/jquery.browser.js',
-            'libs/',
-            'plugins/Zeitgeist/javascripts/piwikHelper.js',
-            'plugins/Zeitgeist/javascripts/',
-            'plugins/CoreHome/javascripts/broadcast.js',
-            'plugins/',
-            'tests/',
-        );
-
-        return self::prioritySort($priorityJsOrdered, $jsFiles);
-    }
+        $mergedFileDirectory = PIWIK_USER_PATH . "/tmp/assets";
+        $mergedFileDirectory = SettingsPiwik::rewriteTmpPathWithHostname($mergedFileDirectory);
 
-    /**
-     * Check the validity of the js file
-     *
-     * @param string $jsFile JavaScript file name
-     * @return boolean
-     * @throws Exception if js file is not valid
-     */
-    private static function validateJsFile($jsFile)
-    {
-        if (!self::assetIsReadable($jsFile)) {
-            throw new Exception("The js asset with 'src' = " . $jsFile . " is not readable");
+        if (!is_dir($mergedFileDirectory)) {
+            Filesystem::mkdir($mergedFileDirectory);
         }
-    }
 
-    /**
-     * Returns the global option disable_merged_assets
-     *
-     * @return string
-     */
-    public static function isMergedAssetsDisabled()
-    {
-        return Config::getInstance()->Debug['disable_merged_assets'];
+        if (!is_writable($mergedFileDirectory)) {
+            throw new Exception("Directory " . $mergedFileDirectory . " has to be writable.");
+        }
+
+        return $mergedFileDirectory;
     }
 
     /**
-     * Returns the css merged file absolute location.
-     * If there is none, the generation process will be triggered.
+     * Return the global option disable_merged_assets
      *
-     * @return string The absolute location of the css merged file
+     * @return boolean
      */
-    public static function getMergedCssFileLocation()
+    public function isMergedAssetsDisabled()
     {
-        self::prepareMergedCssFile();
-        return self::getAbsoluteMergedFileLocation(self::MERGED_CSS_FILE);
+        return Config::getInstance()->Debug['disable_merged_assets'];
     }
 
     /**
-     * Returns the js merged file absolute location.
-     * If there is none, the generation process will be triggered.
-     *
-     * @return string The absolute location of the js merged file
+     * @param UIAssetFetcher $assetFetcher
+     * @param UIAsset $mergedAsset
+     * @return UIAsset
      */
-    public static function getMergedJsFileLocation()
+    private function getMergedJavascript($assetFetcher, $mergedAsset)
     {
-        $isGenerated = self::isGenerated(self::MERGED_JS_FILE);
+        $assetMerger = new JScriptUIAssetMerger($mergedAsset, $assetFetcher, $this->cacheBuster);
 
-        // Make sure the merged JS is re-generated if there are new commits
-        if($isGenerated) {
-            $expectedFirstLine = self::getFirstLineOfMergedJs();
-            $isGenerated = self::isFirstLineMatching(self::MERGED_JS_FILE, $expectedFirstLine);
-        }
-        if (!$isGenerated) {
-            self::generateMergedJsFile();
-        }
+        $assetMerger->generateFile();
 
-        return self::getAbsoluteMergedFileLocation(self::MERGED_JS_FILE);
+        return $mergedAsset;
     }
 
     /**
-     * Check if the provided merged file is generated
+     * Return individual JS file inclusion directive(s) using the markup <script>
      *
-     * @param string $filename filename of the merged asset
-     * @return boolean true is file exists and is readable, false otherwise
+     * @return string
      */
-    private static function isGenerated($filename)
+    private function getIndividualJsIncludes()
     {
-        return is_readable(self::getAbsoluteMergedFileLocation($filename));
+        return
+            $this->getIndividualJsIncludesFromAssetFetcher($this->getCoreJScriptFetcher()) .
+            $this->getIndividualJsIncludesFromAssetFetcher($this->getNonCoreJScriptFetcher());
     }
 
     /**
-     * Removes the previous merged file if it exists.
-     * Also tries to remove compressed version of the merged file.
-     *
-     * @param string $filename filename of the merged asset
-     * @see ProxyStaticFile::serveStaticFile(serveFile
-     * @throws Exception if the file couldn't be deleted
+     * @param UIAssetFetcher $assetFetcher
+     * @return string
      */
-    private static function removeMergedAsset($filename)
+    private function getIndividualJsIncludesFromAssetFetcher($assetFetcher)
     {
-        $isGenerated = self::isGenerated($filename);
-
-        if ($isGenerated) {
-            if (!unlink(self::getAbsoluteMergedFileLocation($filename))) {
-                throw new Exception("Unable to delete merged file : " . $filename . ". Please delete the file and refresh");
-            }
+        $jsIncludeString = '';
 
-            // Tries to remove compressed version of the merged file.
-            // See ProxyHttp::serverStaticFile() for more info on static file compression
-            $compressedFileLocation = PIWIK_USER_PATH . self::COMPRESSED_FILE_LOCATION . $filename;
-            $compressedFileLocation = SettingsPiwik::rewriteTmpPathWithHostname($compressedFileLocation);
+        foreach ($assetFetcher->getCatalog()->getAssets() as $jsFile) {
 
-            @unlink($compressedFileLocation . ".deflate");
-            @unlink($compressedFileLocation . ".gz");
+            $jsFile->validateFile();
+            $jsIncludeString = $jsIncludeString . sprintf(self::JS_IMPORT_DIRECTIVE, $jsFile->getRelativeLocation());
         }
+
+        return $jsIncludeString;
     }
 
-    /**
-     * Remove previous merged assets
-     */
-    public static function removeMergedAssets()
+    private function getCoreJScriptFetcher()
     {
-        self::removeMergedAsset(self::MERGED_CSS_FILE);
-        self::removeMergedAsset(self::MERGED_JS_FILE);
+        return new JScriptUIAssetFetcher($this->getLoadedPlugins(true), $this->theme);
     }
 
-    /**
-     * Check if asset is readable
-     *
-     * @param string $relativePath Relative path to file
-     * @return boolean
-     */
-    private static function assetIsReadable($relativePath)
+    private function getNonCoreJScriptFetcher()
     {
-        return is_readable(self::getAbsoluteLocation($relativePath));
+        return new JScriptUIAssetFetcher($this->getLoadedPlugins(false), $this->theme);
     }
 
     /**
-     * Check if the merged file directory exists and is writable.
-     *
-     * @return string The directory location
-     * @throws Exception if directory is not writable.
+     * @param string $pluginName
+     * @return boolean
      */
-    private static function getMergedFileDirectory()
+    private function pluginContainsJScriptAssets($pluginName)
     {
-        $mergedFileDirectory = PIWIK_USER_PATH . '/' . self::MERGED_FILE_DIR;
-        $mergedFileDirectory = SettingsPiwik::rewriteTmpPathWithHostname($mergedFileDirectory);
+        $fetcher = new JScriptUIAssetFetcher(array($pluginName), $this->theme);
 
-        if (!is_dir($mergedFileDirectory)) {
-            Filesystem::mkdir($mergedFileDirectory);
-        }
+        $assets = $fetcher->getCatalog()->getAssets();
 
-        if (!is_writable($mergedFileDirectory)) {
-            throw new Exception("Directory " . $mergedFileDirectory . " has to be writable.");
-        }
+        $plugin = Manager::getInstance()->getLoadedPlugin($pluginName);
 
-        return $mergedFileDirectory;
-    }
+        if($plugin->isTheme()) {
 
-    /**
-     * Builds the absolute location of the requested merged file
-     *
-     * @param string $mergedFile Name of the merge file
-     * @return string absolute location of the merged file
-     */
-    private static function getAbsoluteMergedFileLocation($mergedFile)
-    {
-        return self::getMergedFileDirectory() . $mergedFile;
+            $theme = Manager::getInstance()->getTheme($pluginName);
+
+            $javaScriptFiles = $theme->getJavaScriptFiles();
+
+            if(!empty($javaScriptFiles))
+                $assets = array_merge($assets, $javaScriptFiles);
+        }
+
+        return !empty($assets);
     }
 
     /**
-     * Returns the full path of an asset file
-     *
-     * @param string $relativePath Relative path to file
-     * @return string
+     * @param UIAsset[] $uiAssets
      */
-    private static function getAbsoluteLocation($relativePath)
+    private function removeAssets($uiAssets)
     {
-        // served by web server directly, so must be a public path
-        return PIWIK_DOCUMENT_ROOT . "/" . $relativePath;
+        foreach($uiAssets as $uiAsset) {
+            $uiAsset->delete();
+        }
     }
 
     /**
-     * Indicates if the provided JavaScript content has already been minified or not.
-     * The heuristic is based on a custom ratio : (size of file) / (number of lines).
-     * The threshold (100) has been found empirically on existing files :
-     * - the ratio never exceeds 50 for non-minified content and
-     * - it never goes under 150 for minified content.
-     *
-     * @param string $content Contents of the JavaScript file
-     * @return boolean
+     * @return UIAsset
      */
-    private static function isMinifiedJs($content)
+    private function getMergedStylesheetAsset()
     {
-        $lineCount = substr_count($content, "\n");
-        if ($lineCount == 0) {
-            return true;
-        }
-
-        $contentSize = strlen($content);
-
-        $ratio = $contentSize / $lineCount;
-
-        return $ratio > self::MINIFIED_JS_RATIO;
+        return $this->getMergedUIAsset(self::MERGED_CSS_FILE);
     }
 
     /**
-     * Sort files according to priority order. Duplicates are also removed.
-     *
-     * @param array $priorityOrder Ordered array of paths (first to last) serving as buckets
-     * @param array $files Unsorted array of files
-     * @return array
+     * @return UIAsset
      */
-    public static function prioritySort($priorityOrder, $files)
+    private function getMergedCoreJSAsset()
     {
-        $newFiles = array();
-        foreach ($priorityOrder as $filePattern) {
-            $newFiles = array_merge($newFiles, preg_grep('~^' . $filePattern . '~', $files));
-        }
-        return array_keys(array_flip($newFiles));
+        return $this->getMergedUIAsset(self::MERGED_CORE_JS_FILE);
     }
 
     /**
-     * @param $mergedFile
-     * @param $firstLineCompileHash
-     * @return bool
+     * @return UIAsset
      */
-    private static function isFirstLineMatching($mergedFile, $firstLineCompileHash)
+    private function getMergedNonCoreJSAsset()
     {
-        $pathMerged = self::getAbsoluteMergedFileLocation($mergedFile);
-        $f = fopen($pathMerged, 'r');
-        $firstLine = fgets($f);
-        fclose($f);
-        if (!empty($firstLine)
-            && trim($firstLine) == trim($firstLineCompileHash)
-        ) {
-            return true;
-        }
-        return false;
+        return $this->getMergedUIAsset(self::MERGED_NON_CORE_JS_FILE);
     }
 
     /**
-     * @return string
+     * @param string $fileName
+     * @return UIAsset
      */
-    private static function getFirstLineOfMergedJs()
+    private function getMergedUIAsset($fileName)
     {
-        return "/* Piwik Javascript - cb=" . self::generateAssetsCacheBuster() . "*/\n";
+        return new OnDiskUIAsset($this->getAssetDirectory(), $fileName);
     }
 }
\ No newline at end of file
diff --git a/core/AssetManager/UIAsset.php b/core/AssetManager/UIAsset.php
new file mode 100644
index 0000000000..00ba05cf74
--- /dev/null
+++ b/core/AssetManager/UIAsset.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+namespace Piwik\AssetManager;
+
+use Exception;
+
+abstract class UIAsset
+{
+    abstract public function validateFile();
+
+    /**
+     * @return string
+     */
+    abstract public function getAbsoluteLocation();
+
+    /**
+     * @return string
+     */
+    abstract public function getRelativeLocation();
+
+    /**
+     * @return string
+     */
+    abstract public function getBaseDirectory();
+
+    /**
+     * Removes the previous file if it exists.
+     * Also tries to remove compressed version of the file.
+     *
+     * @see ProxyStaticFile::serveStaticFile(serveFile
+     * @throws Exception if the file couldn't be deleted
+     */
+    abstract public function delete();
+
+    /**
+     * @param string $content
+     * @throws \Exception
+     */
+    abstract public function writeContent($content);
+
+    /**
+     * @return string
+     */
+    abstract public function getContent();
+
+    /**
+     * @return boolean
+     */
+    abstract public function exists();
+
+    /**
+     * @return int
+     */
+    abstract public function getModificationDate();
+}
diff --git a/core/AssetManager/UIAsset/InMemoryUIAsset.php b/core/AssetManager/UIAsset/InMemoryUIAsset.php
new file mode 100644
index 0000000000..3b5e8488f5
--- /dev/null
+++ b/core/AssetManager/UIAsset/InMemoryUIAsset.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+namespace Piwik\AssetManager\UIAsset;
+
+use Exception;
+use Piwik\AssetManager\UIAsset;
+
+class InMemoryUIAsset extends UIAsset
+{
+    private $content;
+
+    public function validateFile()
+    {
+        return;
+    }
+
+    public function getAbsoluteLocation()
+    {
+        throw new Exception('invalid operation');
+    }
+
+    public function getRelativeLocation()
+    {
+        throw new Exception('invalid operation');
+    }
+
+    public function getBaseDirectory()
+    {
+        throw new Exception('invalid operation');
+    }
+
+    public function delete()
+    {
+        $this->content = null;
+    }
+
+    public function exists()
+    {
+        return false;
+    }
+
+
+    public function writeContent($content)
+    {
+        $this->content = $content;
+    }
+
+    public function getContent()
+    {
+        return $this->content;
+    }
+
+    public function getModificationDate()
+    {
+        throw new Exception('invalid operation');
+    }
+}
diff --git a/core/AssetManager/UIAsset/OnDiskUIAsset.php b/core/AssetManager/UIAsset/OnDiskUIAsset.php
new file mode 100644
index 0000000000..bdda1119c2
--- /dev/null
+++ b/core/AssetManager/UIAsset/OnDiskUIAsset.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+namespace Piwik\AssetManager\UIAsset;
+
+use Exception;
+use Piwik\AssetManager\UIAsset;
+
+class OnDiskUIAsset extends UIAsset
+{
+    /**
+     * @var string
+     */
+    private $baseDirectory;
+
+    /**
+     * @var string
+     */
+    private $relativeLocation;
+
+    /**
+     * @param string $baseDirectory
+     * @param string $fileLocation
+     */
+    function __construct($baseDirectory, $fileLocation)
+    {
+        $this->baseDirectory = $baseDirectory;
+        $this->relativeLocation = $fileLocation;
+    }
+
+    public function getAbsoluteLocation()
+    {
+        return $this->baseDirectory . '/' . $this->relativeLocation;
+    }
+
+    public function getRelativeLocation()
+    {
+        return $this->relativeLocation;
+    }
+
+    public function getBaseDirectory()
+    {
+        return $this->baseDirectory;
+    }
+
+    public function validateFile()
+    {
+        if (!$this->assetIsReadable())
+            throw new Exception("The ui asset with 'href' = " . $this->getAbsoluteLocation() . " is not readable");
+    }
+
+    public function delete()
+    {
+        if ($this->exists()) {
+
+            if (!unlink($this->getAbsoluteLocation()))
+                throw new Exception("Unable to delete merged file : " . $this->getAbsoluteLocation() . ". Please delete the file and refresh");
+
+            // try to remove compressed version of the merged file.
+            @unlink($this->getAbsoluteLocation() . ".deflate");
+            @unlink($this->getAbsoluteLocation() . ".gz");
+        }
+    }
+
+    /**
+     * @param string $content
+     * @throws \Exception
+     */
+    public function writeContent($content)
+    {
+        $this->delete();
+
+        $newFile = @fopen($this->getAbsoluteLocation(), "w");
+
+        if (!$newFile)
+            throw new Exception ("The file : " . $newFile . " can not be opened in write mode.");
+
+        fwrite($newFile, $content);
+
+        fclose($newFile);
+    }
+
+    /**
+     * @return string
+     */
+    public function getContent()
+    {
+        return file_get_contents($this->getAbsoluteLocation());
+    }
+
+    public function exists()
+    {
+        return $this->assetIsReadable();
+    }
+
+    /**
+     * @return boolean
+     */
+    private function assetIsReadable()
+    {
+        return is_readable($this->getAbsoluteLocation());
+    }
+
+    public function getModificationDate()
+    {
+        return filemtime($this->getAbsoluteLocation());
+    }
+}
diff --git a/core/AssetManager/UIAssetCacheBuster.php b/core/AssetManager/UIAssetCacheBuster.php
new file mode 100644
index 0000000000..a8359442b3
--- /dev/null
+++ b/core/AssetManager/UIAssetCacheBuster.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @method static \Piwik\AssetManager\UIAssetCacheBuster getInstance()
+ * @package Piwik
+ */
+namespace Piwik\AssetManager;
+
+use Piwik\Plugin\Manager;
+use Piwik\SettingsPiwik;
+use Piwik\Singleton;
+use Piwik\Version;
+
+class UIAssetCacheBuster extends Singleton
+{
+    /**
+     * Cache buster based on
+     *  - Piwik version
+     *  - Loaded plugins
+     *  - Super user salt
+     *  - Latest
+     *
+     * @param string[] $pluginNames
+     * @return string
+     */
+    public function piwikVersionBasedCacheBuster($pluginNames = false)
+    {
+        $currentGitHash = @file_get_contents(PIWIK_INCLUDE_PATH . '/.git/refs/heads/master');
+        $pluginList = md5(implode(",", !$pluginNames ? Manager::getInstance()->getLoadedPluginsName() : $pluginNames));
+        $cacheBuster = md5(SettingsPiwik::getSalt() . $pluginList . PHP_VERSION . Version::VERSION . trim($currentGitHash));
+        return $cacheBuster;
+    }
+
+    /**
+     * @param string $content
+     * @return string
+     */
+    public function md5BasedCacheBuster($content)
+    {
+        return md5($content);
+    }
+}
diff --git a/core/AssetManager/UIAssetCatalog.php b/core/AssetManager/UIAssetCatalog.php
new file mode 100644
index 0000000000..dc25553c8e
--- /dev/null
+++ b/core/AssetManager/UIAssetCatalog.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+namespace Piwik\AssetManager;
+
+class UIAssetCatalog
+{
+    /**
+     * @var UIAsset[]
+     */
+    private $uiAssets = array();
+
+    /**
+     * @var UIAssetCatalogSorter
+     */
+    private $catalogSorter;
+
+    /**
+     * @var string
+     */
+    private $concatenatedAssets;
+
+    /**
+     * @param UIAssetCatalogSorter $catalogSorter
+     */
+    function __construct($catalogSorter)
+    {
+        $this->catalogSorter = $catalogSorter;
+    }
+
+    /**
+     * @param UIAsset $uiAsset
+     */
+    public function addUIAsset($uiAsset)
+    {
+        if(!$this->assetAlreadyInCatalog($uiAsset)) {
+
+            $this->uiAssets[] = $uiAsset;
+            $this->resetConcatenatedAssets();
+        }
+    }
+
+    /**
+     * @return UIAsset[]
+     */
+    public function getAssets()
+    {
+        return $this->uiAssets;
+    }
+
+    /**
+     * @return UIAssetCatalog
+     */
+    public function getSortedCatalog()
+    {
+        return $this->catalogSorter->sortUIAssetCatalog($this);
+    }
+
+    private function resetConcatenatedAssets()
+    {
+        $this->concatenatedAssets = null;
+    }
+
+    /**
+     * @param UIAsset $uiAsset
+     * @return boolean
+     */
+    private function assetAlreadyInCatalog($uiAsset)
+    {
+        foreach($this->uiAssets as $existingAsset)
+            if($uiAsset->getAbsoluteLocation() == $existingAsset->getAbsoluteLocation())
+                return true;
+
+        return false;
+    }
+}
diff --git a/core/AssetManager/UIAssetCatalogSorter.php b/core/AssetManager/UIAssetCatalogSorter.php
new file mode 100644
index 0000000000..1f93a398e1
--- /dev/null
+++ b/core/AssetManager/UIAssetCatalogSorter.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+namespace Piwik\AssetManager;
+
+class UIAssetCatalogSorter
+{
+    /**
+     * @var string[]
+     */
+    private $priorityOrder;
+
+    /**
+     * @param string[] $priorityOrder
+     */
+    function __construct($priorityOrder)
+    {
+        $this->priorityOrder = $priorityOrder;
+    }
+
+    /**
+     * @param UIAssetCatalog $uiAssetCatalog
+     * @return UIAssetCatalog
+     */
+    public function sortUIAssetCatalog($uiAssetCatalog)
+    {
+        $sortedCatalog = new UIAssetCatalog($this);
+        foreach ($this->priorityOrder as $filePattern) {
+
+            $assetsMatchingPattern = array_filter($uiAssetCatalog->getAssets(), function($uiAsset) use ($filePattern)  {
+                return preg_match('~^' . $filePattern . '~', $uiAsset->getRelativeLocation());
+            });
+
+            foreach($assetsMatchingPattern as $assetMatchingPattern) {
+                $sortedCatalog->addUIAsset($assetMatchingPattern);
+            }
+        }
+
+        $this->addUnmatchedAssets($uiAssetCatalog, $sortedCatalog);
+
+        return $sortedCatalog;
+    }
+
+    /**
+     * @param UIAssetCatalog $uiAssetCatalog
+     * @param UIAssetCatalog $sortedCatalog
+     */
+    private function addUnmatchedAssets($uiAssetCatalog, $sortedCatalog)
+    {
+        foreach ($uiAssetCatalog->getAssets() as $uiAsset) {
+            $sortedCatalog->addUIAsset($uiAsset);
+        }
+    }
+}
diff --git a/core/AssetManager/UIAssetFetcher.php b/core/AssetManager/UIAssetFetcher.php
new file mode 100644
index 0000000000..21185014f7
--- /dev/null
+++ b/core/AssetManager/UIAssetFetcher.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+namespace Piwik\AssetManager;
+
+use Piwik\AssetManager\UIAsset\OnDiskUIAsset;
+use Piwik\Theme;
+
+abstract class UIAssetFetcher
+{
+    /**
+     * @var UIAssetCatalog
+     */
+    protected $catalog;
+
+    /**
+     * @var string[]
+     */
+    protected $fileLocations = array();
+
+    /**
+     * @var string[]
+     */
+    protected $plugins;
+
+    /**
+     * @var Theme
+     */
+    private $theme;
+
+    /**
+     * @param string[] $plugins
+     * @param Theme $theme
+     */
+    function __construct($plugins, $theme)
+    {
+        $this->plugins = $plugins;
+        $this->theme = $theme;
+    }
+
+    /**
+     * @return string[]
+     */
+    public function getPlugins()
+    {
+        return $this->plugins;
+    }
+
+    /**
+     * $return UIAssetCatalog
+     */
+    public function getCatalog()
+    {
+        if($this->catalog == null)
+            $this->createCatalog();
+
+        return $this->catalog;
+    }
+
+    abstract protected function retrieveFileLocations();
+
+    /**
+     * @return string[]
+     */
+    abstract protected function getPriorityOrder();
+
+    private function createCatalog()
+    {
+        $this->retrieveFileLocations();
+
+        $this->initCatalog();
+
+        $this->populateCatalog();
+
+        $this->sortCatalog();
+    }
+
+    private function initCatalog()
+    {
+        $catalogSorter = new UIAssetCatalogSorter($this->getPriorityOrder());
+        $this->catalog = new UIAssetCatalog($catalogSorter);
+    }
+
+    private function populateCatalog()
+    {
+        foreach ($this->fileLocations as $fileLocation) {
+
+            $newUIAsset = new OnDiskUIAsset($this->getBaseDirectory(), $fileLocation);
+            $this->catalog->addUIAsset($newUIAsset);
+        }
+    }
+
+    private function sortCatalog()
+    {
+        $this->catalog = $this->catalog->getSortedCatalog();
+    }
+
+    /**
+     * @return string
+     */
+    private function getBaseDirectory()
+    {
+        // served by web server directly, so must be a public path
+        return PIWIK_USER_PATH;
+    }
+
+    /**
+     * @return Theme
+     */
+    public function getTheme()
+    {
+        return $this->theme;
+    }
+}
diff --git a/core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php b/core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php
new file mode 100644
index 0000000000..3382fdabc6
--- /dev/null
+++ b/core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+namespace Piwik\AssetManager\UIAssetFetcher;
+
+use Piwik\AssetManager\UIAssetFetcher;
+use Piwik\Piwik;
+use string;
+
+class JScriptUIAssetFetcher extends UIAssetFetcher
+{
+
+    protected function retrieveFileLocations()
+    {
+
+        if(!empty($this->plugins)) {
+
+            /**
+             * Triggered when gathering the list of all JavaScript files needed by Piwik
+             * and its plugins.
+             *
+             * Plugins that have their own JavaScript should use this event to make those
+             * files load in the browser.
+             *
+             * JavaScript files should be placed within a **javascripts** subdirectory in your
+             * plugin's root directory.
+             *
+             * _Note: While you are developing your plugin you should enable the config setting
+             * `[Debug] disable_merged_assets` so JavaScript files will be reloaded immediately
+             * after every change._
+             *
+             * **Example**
+             *
+             *     public function getJsFiles(&$jsFiles)
+             *     {
+             *         $jsFiles[] = "plugins/MyPlugin/javascripts/myfile.js";
+             *         $jsFiles[] = "plugins/MyPlugin/javascripts/anotherone.js";
+             *     }
+             *
+             * @param string[] $jsFiles The JavaScript files to load.
+             */
+             Piwik::postEvent('AssetManager.getJavaScriptFiles', array(&$this->fileLocations), null, $this->plugins);
+        }
+
+        $this->addThemeFiles();
+    }
+
+    protected function addThemeFiles()
+    {
+        if(in_array($this->getTheme()->getThemeName(), $this->plugins)) {
+
+            $jsInThemes = $this->getTheme()->getJavaScriptFiles();
+
+            if(!empty($jsInThemes)) {
+
+                foreach($jsInThemes as $jsFile) {
+
+                    $this->fileLocations[] = $jsFile;
+                }
+            }
+        }
+    }
+
+    protected function getPriorityOrder()
+    {
+        return array(
+            'libs/jquery/jquery.js',
+            'libs/jquery/jquery-ui.js',
+            'libs/jquery/jquery.browser.js',
+            'libs/',
+            'plugins/Zeitgeist/javascripts/piwikHelper.js',
+            'plugins/Zeitgeist/javascripts/',
+            'plugins/CoreHome/javascripts/broadcast.js',
+            'plugins/',
+            'tests/',
+        );
+    }
+}
diff --git a/core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php b/core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php
new file mode 100644
index 0000000000..981eac8204
--- /dev/null
+++ b/core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+namespace Piwik\AssetManager\UIAssetFetcher;
+
+use Piwik\AssetManager\UIAssetFetcher;
+
+class StaticUIAssetFetcher extends UIAssetFetcher
+{
+    /**
+     * @var string[]
+     */
+    private $priorityOrder;
+
+    function __construct($fileLocations, $priorityOrder, $theme)
+    {
+        parent::__construct(array(), $theme);
+
+        $this->fileLocations = $fileLocations;
+        $this->priorityOrder = $priorityOrder;
+    }
+
+    protected function retrieveFileLocations()
+    {
+
+    }
+
+    protected function getPriorityOrder()
+    {
+        return $this->priorityOrder;
+    }
+}
diff --git a/core/AssetManager/UIAssetFetcher/StylesheetUIAssetFetcher.php b/core/AssetManager/UIAssetFetcher/StylesheetUIAssetFetcher.php
new file mode 100644
index 0000000000..e5e6e7be0f
--- /dev/null
+++ b/core/AssetManager/UIAssetFetcher/StylesheetUIAssetFetcher.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+namespace Piwik\AssetManager\UIAssetFetcher;
+
+use Piwik\AssetManager\UIAssetFetcher;
+use Piwik\Piwik;
+
+class StylesheetUIAssetFetcher extends UIAssetFetcher
+{
+    protected function getPriorityOrder()
+    {
+        return array(
+            'libs/',
+            'plugins/CoreHome/stylesheets/color_manager.css', // must be before other Piwik stylesheets
+            'plugins/Zeitgeist/stylesheets/base.less',
+            'plugins/Zeitgeist/stylesheets/',
+            'plugins/',
+            'plugins/Dashboard/stylesheets/dashboard.less',
+            'tests/',
+        );
+    }
+
+    protected function retrieveFileLocations()
+    {
+        /**
+         * Triggered when gathering the list of all stylesheets (CSS and LESS) needed by
+         * Piwik and its plugins.
+         *
+         * Plugins that have stylesheets should use this event to make those stylesheets
+         * load.
+         *
+         * Stylesheets should be placed within a **stylesheets** subdirectory in your plugin's
+         * root directory.
+         *
+         * _Note: While you are developing your plugin you should enable the config setting
+         * `[Debug] disable_merged_assets` so your stylesheets will be reloaded immediately
+         * after a change._
+         *
+         * **Example**
+         *
+         *     public function getStylesheetFiles(&$stylesheets)
+         *     {
+         *         $stylesheets[] = "plugins/MyPlugin/stylesheets/myfile.less";
+         *         $stylesheets[] = "plugins/MyPlugin/stylesheets/myotherfile.css";
+         *     }
+         *
+         * @param string[] &$stylesheets The list of stylesheet paths.
+         */
+        Piwik::postEvent('AssetManager.getStylesheetFiles', array(&$this->fileLocations));
+
+        $this->addThemeFiles();
+    }
+
+    protected function addThemeFiles()
+    {
+        $themeStylesheet = $this->getTheme()->getStylesheet();
+
+        if($themeStylesheet) {
+            $this->fileLocations[] = $themeStylesheet;
+        }
+    }
+}
diff --git a/core/AssetManager/UIAssetMerger.php b/core/AssetManager/UIAssetMerger.php
new file mode 100644
index 0000000000..8e1ededd47
--- /dev/null
+++ b/core/AssetManager/UIAssetMerger.php
@@ -0,0 +1,212 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+namespace Piwik\AssetManager;
+
+use Piwik\AssetManager\PiwikLessCompiler;
+use Piwik\AssetManager\UIAsset\StylesheetUIAsset;
+use Piwik\AssetManager;
+
+abstract class UIAssetMerger
+{
+    /**
+     * @var UIAssetFetcher
+     */
+    private $assetFetcher;
+
+    /**
+     * @var UIAsset
+     */
+    private $mergedAsset;
+
+    /**
+     * @var string
+     */
+    private $mergedContent;
+
+    /**
+     * @var UIAssetCacheBuster
+     */
+    protected $cacheBuster;
+
+    /**
+     * @param UIAsset $mergedAsset
+     * @param UIAssetFetcher $assetFetcher
+     * @param UIAssetCacheBuster $cacheBuster
+     */
+    function __construct($mergedAsset, $assetFetcher, $cacheBuster)
+    {
+        $this->mergedAsset = $mergedAsset;
+        $this->assetFetcher = $assetFetcher;
+        $this->cacheBuster = $cacheBuster;
+    }
+
+    public function generateFile()
+    {
+        if(!$this->shouldGenerate())
+            return;
+
+        $this->mergedContent = $this->getMergedAssets();
+
+        $this->postEvent($this->mergedContent);
+
+        $this->adjustPaths();
+
+        $this->addPreamble();
+
+        $this->writeContentToFile();
+    }
+
+    /**
+     * @return string
+     */
+    abstract protected function getMergedAssets();
+
+    /**
+     * @return string
+     */
+    abstract protected function generateCacheBuster();
+
+    /**
+     * @return string
+     */
+    abstract protected function getPreamble();
+
+    /**
+     * @return string
+     */
+    abstract protected function getFileSeparator();
+
+    /**
+     * @param UIAsset $uiAsset
+     * @return string
+     */
+    abstract protected function processFileContent($uiAsset);
+
+    /**
+     * @param string $mergedContent
+     */
+    abstract protected function postEvent(&$mergedContent);
+
+    protected function getConcatenatedAssets()
+    {
+        if(empty($this->mergedContent))
+            $this->concatenateAssets();
+
+        return $this->mergedContent;
+    }
+
+    private function concatenateAssets()
+    {
+        $mergedContent = '';
+
+        foreach ($this->getAssetCatalog()->getAssets() as $uiAsset) {
+
+            $uiAsset->validateFile();
+            $content = $this->processFileContent($uiAsset);
+
+            $mergedContent .= $this->getFileSeparator() . $content;
+        }
+
+        $this->mergedContent = $mergedContent;
+    }
+
+    /**
+     * @return string[]
+     */
+    protected function getPlugins()
+    {
+        return $this->assetFetcher->getPlugins();
+    }
+
+    /**
+     * @return UIAssetCatalog
+     */
+    protected function getAssetCatalog()
+    {
+        return $this->assetFetcher->getCatalog();
+    }
+
+    /**
+     * @return boolean
+     */
+    private function shouldGenerate()
+    {
+        if(!$this->mergedAsset->exists())
+            return true;
+
+        if($this->shouldCompareExistingVersion()) {
+
+            return !$this->isFileUpToDate();
+        }
+
+        return false;
+    }
+
+    /**
+     * @return boolean
+     */
+    private function isFileUpToDate()
+    {
+        $f = fopen($this->mergedAsset->getAbsoluteLocation(), 'r');
+        $firstLine = fgets($f);
+        fclose($f);
+
+        if (!empty($firstLine) && trim($firstLine) == trim($this->getCacheBusterValue())) {
+            return true;
+        }
+
+        // Some CSS file in the merge, has changed since last merged asset was generated
+        // Note: we do not detect changes in @import'ed LESS files
+        return false;
+    }
+
+    /**
+     * @return boolean
+     */
+    private function isMergedAssetsDisabled()
+    {
+        return AssetManager::getInstance()->isMergedAssetsDisabled();
+    }
+
+    private function adjustPaths()
+    {
+        $this->mergedContent = $this->assetFetcher->getTheme()->rewriteAssetsPathToTheme($this->mergedContent);
+    }
+
+    private function writeContentToFile()
+    {
+        $this->mergedAsset->writeContent($this->mergedContent);
+    }
+
+    /**
+     * @return string
+     */
+    protected function getCacheBusterValue()
+    {
+        if(empty($this->cacheBusterValue))
+            $this->cacheBusterValue = $this->generateCacheBuster();
+
+        return $this->cacheBusterValue;
+    }
+
+    private function addPreamble()
+    {
+        $this->mergedContent = $this->getPreamble() . $this->mergedContent;
+    }
+
+    /**
+     * @return boolean
+     */
+    private function shouldCompareExistingVersion()
+    {
+        return $this->isMergedAssetsDisabled();
+    }
+}
diff --git a/core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php b/core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php
new file mode 100644
index 0000000000..f501280ab1
--- /dev/null
+++ b/core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+namespace Piwik\AssetManager\UIAssetMerger;
+
+use Piwik\AssetManager\UIAsset;
+use Piwik\AssetManager\UIAssetCacheBuster;
+use Piwik\AssetManager\UIAssetFetcher\JScriptUIAssetFetcher;
+use Piwik\AssetManager\UIAssetMerger;
+use Piwik\AssetManager;
+use Piwik\AssetManager\UIAssetMinifier;
+use Piwik\Piwik;
+
+class JScriptUIAssetMerger extends UIAssetMerger
+{
+    /**
+     * @var UIAssetMinifier
+     */
+    private $assetMinifier;
+
+    /**
+     * @param UIAsset $mergedAsset
+     * @param JScriptUIAssetFetcher $assetFetcher
+     * @param UIAssetCacheBuster $cacheBuster
+     */
+    function __construct($mergedAsset, $assetFetcher, $cacheBuster)
+    {
+        parent::__construct($mergedAsset, $assetFetcher, $cacheBuster);
+
+        $this->assetMinifier = UIAssetMinifier::getInstance();
+    }
+
+    protected function getMergedAssets()
+    {
+        $concatenatedAssets = $this->getConcatenatedAssets();
+
+        return str_replace("\n", "\r\n", $concatenatedAssets);
+    }
+
+    protected function generateCacheBuster()
+    {
+        $cacheBuster = $this->cacheBuster->piwikVersionBasedCacheBuster($this->getPlugins());
+        return "/* Piwik Javascript - cb=" . $cacheBuster . "*/\r\n";
+    }
+
+    protected function getPreamble()
+    {
+        return $this->getCacheBusterValue();
+    }
+
+    protected function postEvent(&$mergedContent)
+    {
+        $plugins = $this->getPlugins();
+
+        if(!empty($plugins)) {
+
+            /**
+             * Triggered after all the JavaScript files Piwik uses are minified and merged into a
+             * single file, but before the merged JavaScript is written to disk.
+             *
+             * Plugins can use this event to modify merged JavaScript or do something else
+             * with it.
+             *
+             * @param string $mergedContent The minified and merged JavaScript.
+             */
+            Piwik::postEvent('AssetManager.filterMergedJavaScripts', array(&$mergedContent), null, $plugins);
+        }
+    }
+
+    public function getFileSeparator()
+    {
+        return PHP_EOL;
+    }
+
+    protected function processFileContent($uiAsset)
+    {
+        $content = $uiAsset->getContent();
+
+        if (!$this->assetMinifier->isMinifiedJs($content))
+            $content = $this->assetMinifier->minifyJs($content);
+
+        return $content;
+    }
+}
diff --git a/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php b/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php
new file mode 100644
index 0000000000..ae02127448
--- /dev/null
+++ b/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php
@@ -0,0 +1,142 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+namespace Piwik\AssetManager\UIAssetMerger;
+
+use Exception;
+use Piwik\AssetManager\UIAsset;
+use Piwik\AssetManager\UIAssetMerger;
+use Piwik\Piwik;
+use lessc;
+
+class StylesheetUIAssetMerger extends UIAssetMerger
+{
+    /**
+     * @var lessc
+     */
+    private $lessCompiler;
+
+    function __construct($mergedAsset, $assetFetcher, $cacheBuster)
+    {
+        parent::__construct($mergedAsset, $assetFetcher, $cacheBuster);
+
+        $this->lessCompiler = self::getLessCompiler();
+    }
+
+    protected function getMergedAssets()
+    {
+        foreach($this->getAssetCatalog()->getAssets() as $uiAsset) {
+            $this->lessCompiler->addImportDir(dirname($uiAsset->getAbsoluteLocation()));
+        }
+
+        return $this->lessCompiler->compile($this->getConcatenatedAssets());
+    }
+
+    /**
+     * @return lessc
+     * @throws Exception
+     */
+    private static function getLessCompiler()
+    {
+        if (!class_exists("lessc")) {
+            throw new Exception("Less was added to composer during 2.0. ==> Execute this command to update composer packages: \$ php composer.phar install");
+        }
+        $less = new lessc();
+        return $less;
+    }
+
+    protected function generateCacheBuster()
+    {
+        $fileHash = $this->cacheBuster->md5BasedCacheBuster($this->getConcatenatedAssets());
+        return "/* compile_me_once=$fileHash */";
+    }
+
+    protected function getPreamble()
+    {
+        return $this->getCacheBusterValue() . "\n"
+        . "/* Piwik CSS file is compiled with Less. You may be interested in writing a custom Theme for Piwik! */\n";
+    }
+
+    protected function postEvent(&$mergedContent)
+    {
+        /**
+         * Triggered after all less stylesheets are compiled to CSS, minified and merged into
+         * one file, but before the generated CSS is written to disk.
+         *
+         * This event can be used to modify merged CSS.
+         *
+         * @param string $mergedContent The merged and minified CSS.
+         */
+        Piwik::postEvent('AssetManager.filterMergedStylesheets', array(&$mergedContent));
+    }
+
+    public function getFileSeparator()
+    {
+        return '';
+    }
+
+    protected function processFileContent($uiAsset)
+    {
+        return $this->rewriteCssPathsDirectives($uiAsset);
+    }
+
+    /**
+     * Rewrite css url directives
+     * - rewrites paths defined relatively to their css/less definition file
+     * - rewrite windows directory separator \\ to /
+     *
+     * @param UIAsset $uiAsset
+     * @return string
+     */
+    private function rewriteCssPathsDirectives($uiAsset)
+    {
+        static $rootDirectoryLength = null;
+        if (is_null($rootDirectoryLength)) {
+            $rootDirectoryLength = self::countDirectoriesInPathToRoot($uiAsset);
+        }
+
+        $baseDirectory = dirname($uiAsset->getRelativeLocation());
+        $content = preg_replace_callback(
+            "/(url\(['\"]?)([^'\")]*)/",
+            function ($matches) use ($rootDirectoryLength, $baseDirectory) {
+
+                $absolutePath = realpath(PIWIK_USER_PATH . "/$baseDirectory/" . $matches[2]);
+
+                if($absolutePath) {
+
+                    $relativePath = substr($absolutePath, $rootDirectoryLength);
+
+                    $relativePath = str_replace('\\', '/', $relativePath);
+
+                    return $matches[1] . $relativePath;
+
+                } else {
+                    return $matches[1] . $matches[2];
+                }
+            },
+            $uiAsset->getContent()
+        );
+        return $content;
+    }
+
+    /**
+     * @param UIAsset $uiAsset
+     * @return int
+     */
+    protected function countDirectoriesInPathToRoot($uiAsset)
+    {
+        $rootDirectory = realpath($uiAsset->getBaseDirectory());
+        if ($rootDirectory != '/' && substr_compare($rootDirectory, '/', -1)) {
+            $rootDirectory .= '/';
+        }
+        $rootDirectoryLen = strlen($rootDirectory);
+        return $rootDirectoryLen;
+    }
+}
diff --git a/core/AssetManager/UIAssetMinifier.php b/core/AssetManager/UIAssetMinifier.php
new file mode 100644
index 0000000000..fe29f1ec61
--- /dev/null
+++ b/core/AssetManager/UIAssetMinifier.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik
+ * @method static \Piwik\AssetManager\UIAssetMinifier getInstance()
+ * @package Piwik
+ */
+namespace Piwik\AssetManager;
+
+use Exception;
+use Piwik\Singleton;
+use JShrink\Minifier;
+
+class UIAssetMinifier extends Singleton
+{
+    const MINIFIED_JS_RATIO = 100;
+
+    protected function __construct()
+    {
+        self::validateDependency();
+        parent::__construct();
+    }
+
+
+    /**
+     * Indicates if the provided JavaScript content has already been minified or not.
+     * The heuristic is based on a custom ratio : (size of file) / (number of lines).
+     * The threshold (100) has been found empirically on existing files :
+     * - the ratio never exceeds 50 for non-minified content and
+     * - it never goes under 150 for minified content.
+     *
+     * @param string $content Contents of the JavaScript file
+     * @return boolean
+     */
+    public function isMinifiedJs($content)
+    {
+        $lineCount = substr_count($content, "\n");
+        if ($lineCount == 0) {
+            return true;
+        }
+
+        $contentSize = strlen($content);
+
+        $ratio = $contentSize / $lineCount;
+
+        return $ratio > self::MINIFIED_JS_RATIO;
+    }
+
+    /**
+     * @param string $content
+     * @return string
+     */
+    public function minifyJs($content)
+    {
+        return Minifier::minify($content);
+    }
+
+    private static function validateDependency()
+    {
+        if (!class_exists("JShrink\Minifier"))
+            throw new Exception("JShrink dependency is managed using Composer.");
+    }
+}
diff --git a/core/Db/BatchInsert.php b/core/Db/BatchInsert.php
index 1a9563be95..89a003490f 100644
--- a/core/Db/BatchInsert.php
+++ b/core/Db/BatchInsert.php
@@ -61,7 +61,7 @@ class BatchInsert
      */
     public static function tableInsertBatch($tableName, $fields, $values, $throwException = false)
     {
-        $filePath = PIWIK_USER_PATH . '/' . AssetManager::MERGED_FILE_DIR . $tableName . '-' . Common::generateUniqId() . '.csv';
+        $filePath = PIWIK_USER_PATH . '/tmp/assets/' . $tableName . '-' . Common::generateUniqId() . '.csv';
         $filePath = SettingsPiwik::rewriteTmpPathWithHostname($filePath);
 
         if (Db::get()->hasBulkLoader()) {
diff --git a/core/Filesystem.php b/core/Filesystem.php
index aea8f5e1c8..37ea89e999 100644
--- a/core/Filesystem.php
+++ b/core/Filesystem.php
@@ -24,9 +24,9 @@ class Filesystem
      * Called on Core install, update, plugin enable/disable
      * Will clear all cache that could be affected by the change in configuration being made
      */
-    public static function deleteAllCacheOnUpdate()
+    public static function deleteAllCacheOnUpdate($pluginName = false)
     {
-        AssetManager::removeMergedAssets();
+        AssetManager::getInstance()->removeMergedAssets($pluginName);
         View::clearCompiledTemplates();
         Cache::deleteTrackerCache();
     }
diff --git a/core/Plugin.php b/core/Plugin.php
index 53fe0f8559..d2ce8d11dd 100644
--- a/core/Plugin.php
+++ b/core/Plugin.php
@@ -262,7 +262,7 @@ class Plugin
      *
      * @return bool
      */
-    final public function isTheme()
+    public function isTheme()
     {
         $info = $this->getInformation();
         return !empty($info['theme']) && (bool)$info['theme'];
diff --git a/core/Plugin/Manager.php b/core/Plugin/Manager.php
index 40ca5977ca..d067333b26 100644
--- a/core/Plugin/Manager.php
+++ b/core/Plugin/Manager.php
@@ -20,6 +20,7 @@ use Piwik\Plugin;
 use Piwik\Singleton;
 use Piwik\Translate;
 use Piwik\Updater;
+use Piwik\Theme;
 
 require_once PIWIK_INCLUDE_PATH . '/core/EventDispatcher.php';
 
@@ -219,7 +220,7 @@ class Manager extends Singleton
         PiwikConfig::getInstance()->forceSave();
         \Piwik\Settings\Manager::cleanupPluginSettings($pluginName);
 
-        Filesystem::deleteAllCacheOnUpdate();
+        $this->clearCache($pluginName);
 
         self::deletePluginFromFilesystem($pluginName);
         if ($this->isPluginInFilesystem($pluginName)) {
@@ -228,6 +229,14 @@ class Manager extends Singleton
         return true;
     }
 
+    /**
+     * @param string $pluginName
+     */
+    private function clearCache($pluginName)
+    {
+        Filesystem::deleteAllCacheOnUpdate($pluginName);
+    }
+
     public static function deletePluginFromFilesystem($plugin)
     {
         Filesystem::unlinkRecursive(PIWIK_INCLUDE_PATH . '/plugins/' . $plugin, $deleteRootToo = true);
@@ -255,7 +264,7 @@ class Manager extends Singleton
         $this->removePluginFromTrackerConfig($pluginName);
 
         PiwikConfig::getInstance()->forceSave();
-        Filesystem::deleteAllCacheOnUpdate();
+        $this->clearCache($pluginName);
 
         return $plugins;
     }
@@ -329,7 +338,7 @@ class Manager extends Singleton
         $this->updatePluginsConfig($plugins);
         PiwikConfig::getInstance()->forceSave();
 
-        Filesystem::deleteAllCacheOnUpdate();
+        $this->clearCache($pluginName);
 
         $this->pluginsToLoad[] = $pluginName;
     }
@@ -369,6 +378,22 @@ class Manager extends Singleton
         return $theme;
     }
 
+    /**
+     * @param string $themeName
+     * @throws \Exception
+     * @return Theme
+     */
+    public function getTheme($themeName)
+    {
+        $plugins = $this->getLoadedPlugins();
+
+        foreach ($plugins as $plugin)
+            if ($plugin->isTheme() && $plugin->getPluginName() == $themeName)
+                return new Theme($plugin);
+
+        throw new \Exception('Theme not found : ' . $themeName);
+    }
+
     public function getNumberOfActivatedPlugins()
     {
         $counter = 0;
diff --git a/core/ProxyHttp.php b/core/ProxyHttp.php
index b35d749342..692e8219bb 100644
--- a/core/ProxyHttp.php
+++ b/core/ProxyHttp.php
@@ -91,8 +91,7 @@ class ProxyHttp
                 // optional compression
                 $compressed = false;
                 $encoding = '';
-                $compressedFileLocation = PIWIK_USER_PATH . AssetManager::COMPRESSED_FILE_LOCATION . basename($file);
-                $compressedFileLocation = SettingsPiwik::rewriteTmpPathWithHostname($compressedFileLocation);
+                $compressedFileLocation = AssetManager::getInstance()->getAssetDirectory() . '/' . basename($file);
 
                 $phpOutputCompressionEnabled = ProxyHttp::isPhpOutputCompressed();
                 if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && !$phpOutputCompressionEnabled) {
diff --git a/core/Theme.php b/core/Theme.php
index 699783ae46..785d37382f 100644
--- a/core/Theme.php
+++ b/core/Theme.php
@@ -10,6 +10,8 @@
  */
 namespace Piwik;
 
+use Piwik\Plugin\Manager;
+
 /**
  * This class contains logic to make Themes work beautifully.
  *
@@ -23,10 +25,21 @@ class Theme
     /** @var \Piwik\Plugin  */
     private $theme;
 
-    public function __construct()
+    /**
+     * @var Plugin $plugin
+     */
+    public function __construct($plugin = false)
+    {
+        $this->createThemeFromPlugin($plugin ? $plugin : Manager::getInstance()->getThemeEnabled());
+    }
+
+    /**
+     * @param Plugin $plugin
+     */
+    private function createThemeFromPlugin($plugin)
     {
-        $this->theme = \Piwik\Plugin\Manager::getInstance()->getThemeEnabled();
-        $this->themeName = $this->theme->getPluginName();
+        $this->theme = $plugin;
+        $this->themeName = $plugin->getPluginName();
     }
 
     public function getStylesheet()
@@ -123,4 +136,11 @@ class Theme
         return $source;
     }
 
+    /**
+     * @return string
+     */
+    public function getThemeName()
+    {
+        return $this->themeName;
+    }
 }
\ No newline at end of file
diff --git a/core/Twig.php b/core/Twig.php
index 69a6501d90..3f15148985 100644
--- a/core/Twig.php
+++ b/core/Twig.php
@@ -104,9 +104,9 @@ class Twig
             $assetType = strtolower($params['type']);
             switch ($assetType) {
                 case 'css':
-                    return AssetManager::getCssAssets();
+                    return AssetManager::getInstance()->getCssInclusionDirective();
                 case 'js':
-                    return AssetManager::getJsAssets();
+                    return AssetManager::getInstance()->getJsInclusionDirective();
                 default:
                     throw new Exception("The twig function includeAssets 'type' parameter needs to be either 'css' or 'js'.");
             }
diff --git a/core/View.php b/core/View.php
index 105434e00a..9a9e105da0 100644
--- a/core/View.php
+++ b/core/View.php
@@ -11,6 +11,7 @@
 namespace Piwik;
 
 use Exception;
+use Piwik\AssetManager\UIAssetCacheBuster;
 use Piwik\Plugins\SitesManager\API as APISitesManager;
 use Piwik\Plugins\UsersManager\API as APIUsersManager;
 use Piwik\View\ViewInterface;
@@ -259,7 +260,7 @@ class View implements ViewInterface
 
     protected function applyFilter_cacheBuster($output)
     {
-        $cacheBuster = AssetManager::generateAssetsCacheBuster();
+        $cacheBuster = UIAssetCacheBuster::getInstance()->piwikVersionBasedCacheBuster();
         $tag = 'cb=' . $cacheBuster;
 
         $pattern = array(
diff --git a/libs/cssmin/MIT-LICENSE.txt b/libs/cssmin/MIT-LICENSE.txt
deleted file mode 100644
index 974edcfdc1..0000000000
--- a/libs/cssmin/MIT-LICENSE.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-Copyright (c) 2008 Joe Scylla <joe.scylla@gmail.com>
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/libs/cssmin/cssmin.php b/libs/cssmin/cssmin.php
deleted file mode 100644
index 5fa999bc54..0000000000
--- a/libs/cssmin/cssmin.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-/**
- * cssmin.php - A simple CSS minifier.
- * --
- * 
- * <code>
- * include("cssmin.php");
- * file_put_contents("path/to/target.css", cssmin::minify(file_get_contents("path/to/source.css")));
- * </code>
- * --
- * 
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 
- * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
- * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
- * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- * --
- *
- * @package 	cssmin
- * @author 		Joe Scylla <joe.scylla@gmail.com>
- * @copyright 	2008 Joe Scylla <joe.scylla@gmail.com>
- * @license 	http://opensource.org/licenses/mit-license.php MIT License
- * @version 	1.0 (2008-01-31)
- */
-class cssmin
-	{
-	/**
-	 * Minifies stylesheet definitions
-	 *
-	 * @param 	string	$v	Stylesheet definitions as string
-	 * @return 	string		Minified stylesheet definitions
-	 */
-	public static function minify($v) 
-		{
-		$v = trim($v);
-		$v = str_replace("\r\n", "\n", $v);
-        $search = array("/\/\*[\d\D]*?\*\/|\t+/", "/\s+/", "/\}\s+/");
-        $replace = array(null, " ", "}\n");
-		$v = preg_replace($search, $replace, $v);
-		$search = array("/\\;\s/", "/\s+\{\\s+/", "/\\:\s+\\#/", "/,\s+/i", "/\\:\s+\\\'/i", "/\\:\s+([0-9]+|[A-F]+)/i");
-        $replace = array(";", "{", ":#", ",", ":\'", ":$1");
-        $v = preg_replace($search, $replace, $v);
-        $v = str_replace("\n", null, $v);
-    	return $v;	
-  		}
-	}
-?>
\ No newline at end of file
diff --git a/libs/jsmin/jsmin.php b/libs/jsmin/jsmin.php
deleted file mode 100644
index 3c2f859dfd..0000000000
--- a/libs/jsmin/jsmin.php
+++ /dev/null
@@ -1,291 +0,0 @@
-<?php
-/**
- * jsmin.php - PHP implementation of Douglas Crockford's JSMin.
- *
- * This is pretty much a direct port of jsmin.c to PHP with just a few
- * PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and
- * outputs to stdout, this library accepts a string as input and returns another
- * string as output.
- *
- * PHP 5 or higher is required.
- *
- * Permission is hereby granted to use this version of the library under the
- * same terms as jsmin.c, which has the following license:
- *
- * --
- * Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of
- * this software and associated documentation files (the "Software"), to deal in
- * the Software without restriction, including without limitation the rights to
- * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
- * of the Software, and to permit persons to whom the Software is furnished to do
- * so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * The Software shall be used for Good, not Evil.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- * --
- *
- * @package JSMin
- * @author Ryan Grove <ryan@wonko.com>
- * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
- * @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port)
- * @license http://opensource.org/licenses/mit-license.php MIT License
- * @version 1.1.1 (2008-03-02)
- * @link http://code.google.com/p/jsmin-php/
- */
-
-class JSMin {
-  const ORD_LF    = 10;
-  const ORD_SPACE = 32;
-
-  protected $a           = '';
-  protected $b           = '';
-  protected $input       = '';
-  protected $inputIndex  = 0;
-  protected $inputLength = 0;
-  protected $lookAhead   = null;
-  protected $output      = '';
-
-  // -- Public Static Methods --------------------------------------------------
-
-  public static function minify($js) {
-    $jsmin = new JSMin($js);
-    return $jsmin->min();
-  }
-
-  // -- Public Instance Methods ------------------------------------------------
-
-  public function __construct($input) {
-    $this->input       = str_replace("\r\n", "\n", $input);
-    $this->inputLength = strlen($this->input);
-  }
-
-  // -- Protected Instance Methods ---------------------------------------------
-
-  protected function action($d) {
-    switch($d) {
-      case 1:
-        $this->output .= $this->a;
-
-      case 2:
-        $this->a = $this->b;
-
-        if ($this->a === "'" || $this->a === '"') {
-          for (;;) {
-            $this->output .= $this->a;
-            $this->a       = $this->get();
-
-            if ($this->a === $this->b) {
-              break;
-            }
-
-            if (ord($this->a) <= self::ORD_LF) {
-              throw new JSMinException('Unterminated string literal.');
-            }
-
-            if ($this->a === '\\') {
-              $this->output .= $this->a;
-              $this->a       = $this->get();
-            }
-          }
-        }
-
-      case 3:
-        $this->b = $this->next();
-
-        if ($this->b === '/' && (
-            $this->a === '(' || $this->a === ',' || $this->a === '=' ||
-            $this->a === ':' || $this->a === '[' || $this->a === '!' ||
-            $this->a === '&' || $this->a === '|' || $this->a === '?')) {
-
-          $this->output .= $this->a . $this->b;
-
-          for (;;) {
-            $this->a = $this->get();
-
-            if ($this->a === '/') {
-              break;
-            } elseif ($this->a === '\\') {
-              $this->output .= $this->a;
-              $this->a       = $this->get();
-            } elseif (ord($this->a) <= self::ORD_LF) {
-              throw new JSMinException('Unterminated regular expression '.
-                  'literal.');
-            }
-
-            $this->output .= $this->a;
-          }
-
-          $this->b = $this->next();
-        }
-    }
-  }
-
-  protected function get() {
-    $c = $this->lookAhead;
-    $this->lookAhead = null;
-
-    if ($c === null) {
-      if ($this->inputIndex < $this->inputLength) {
-        $c = substr($this->input, $this->inputIndex, 1);
-        $this->inputIndex += 1;
-      } else {
-        $c = null;
-      }
-    }
-
-    if ($c === "\r") {
-      return "\n";
-    }
-
-    if ($c === null || $c === "\n" || ord($c) >= self::ORD_SPACE) {
-      return $c;
-    }
-
-    return ' ';
-  }
-
-  protected function isAlphaNum($c) {
-    return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1;
-  }
-
-  protected function min() {
-    $this->a = "\n";
-    $this->action(3);
-
-    while ($this->a !== null) {
-      switch ($this->a) {
-        case ' ':
-          if ($this->isAlphaNum($this->b)) {
-            $this->action(1);
-          } else {
-            $this->action(2);
-          }
-          break;
-
-        case "\n":
-          switch ($this->b) {
-            case '{':
-            case '[':
-            case '(':
-            case '+':
-            case '-':
-              $this->action(1);
-              break;
-
-            case ' ':
-              $this->action(3);
-              break;
-
-            default:
-              if ($this->isAlphaNum($this->b)) {
-                $this->action(1);
-              }
-              else {
-                $this->action(2);
-              }
-          }
-          break;
-
-        default:
-          switch ($this->b) {
-            case ' ':
-              if ($this->isAlphaNum($this->a)) {
-                $this->action(1);
-                break;
-              }
-
-              $this->action(3);
-              break;
-
-            case "\n":
-              switch ($this->a) {
-                case '}':
-                case ']':
-                case ')':
-                case '+':
-                case '-':
-                case '"':
-                case "'":
-                  $this->action(1);
-                  break;
-
-                default:
-                  if ($this->isAlphaNum($this->a)) {
-                    $this->action(1);
-                  }
-                  else {
-                    $this->action(3);
-                  }
-              }
-              break;
-
-            default:
-              $this->action(1);
-              break;
-          }
-      }
-    }
-
-    return $this->output;
-  }
-
-  protected function next() {
-    $c = $this->get();
-
-    if ($c === '/') {
-      switch($this->peek()) {
-        case '/':
-          for (;;) {
-            $c = $this->get();
-
-            if (ord($c) <= self::ORD_LF) {
-              return $c;
-            }
-          }
-
-        case '*':
-          $this->get();
-
-          for (;;) {
-            switch($this->get()) {
-              case '*':
-                if ($this->peek() === '/') {
-                  $this->get();
-                  return ' ';
-                }
-                break;
-
-              case null:
-                throw new JSMinException('Unterminated comment.');
-            }
-          }
-
-        default:
-          return $c;
-      }
-    }
-
-    return $c;
-  }
-
-  protected function peek() {
-    $this->lookAhead = $this->get();
-    return $this->lookAhead;
-  }
-}
-
-// -- Exceptions ---------------------------------------------------------------
-class JSMinException extends Exception {}
-?>
\ No newline at end of file
diff --git a/plugins/Installation/Controller.php b/plugins/Installation/Controller.php
index 4001d99f05..f664eb12b0 100644
--- a/plugins/Installation/Controller.php
+++ b/plugins/Installation/Controller.php
@@ -598,7 +598,7 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
     public function getBaseCss()
     {
         @header('Content-Type: text/css');
-        return AssetManager::getCompiledBaseCss();
+        return AssetManager::getInstance()->getCompiledBaseCss()->getContent();
     }
 
     /**
diff --git a/plugins/Proxy/Controller.php b/plugins/Proxy/Controller.php
index 60b43cd21c..e703cc6df5 100644
--- a/plugins/Proxy/Controller.php
+++ b/plugins/Proxy/Controller.php
@@ -11,6 +11,7 @@
 namespace Piwik\Plugins\Proxy;
 
 use Piwik\AssetManager;
+use Piwik\AssetManager\UIAsset;
 use Piwik\Common;
 use Piwik\Piwik;
 use Piwik\ProxyHttp;
@@ -35,20 +36,40 @@ class Controller extends \Piwik\Plugin\Controller
      */
     public function getCss()
     {
-        $cssMergedFile = AssetManager::getMergedCssFileLocation();
-        ProxyHttp::serverStaticFile($cssMergedFile, "text/css");
+        $cssMergedFile = AssetManager::getInstance()->getMergedStylesheet();
+        ProxyHttp::serverStaticFile($cssMergedFile->getAbsoluteLocation(), "text/css");
     }
 
     /**
-     * Output the merged JavaScript file.
+     * Output the merged core JavaScript file.
      * This method is called when the asset manager is enabled.
      *
      * @see core/AssetManager.php
      */
-    public function getJs()
+    public function getCoreJs()
     {
-        $jsMergedFile = AssetManager::getMergedJsFileLocation();
-        ProxyHttp::serverStaticFile($jsMergedFile, self::JS_MIME_TYPE);
+        $jsMergedFile = AssetManager::getInstance()->getMergedCoreJavaScript();
+        $this->serveJsFile($jsMergedFile);
+    }
+
+    /**
+     * Output the merged non core JavaScript file.
+     * This method is called when the asset manager is enabled.
+     *
+     * @see core/AssetManager.php
+     */
+    public function getNonCoreJs()
+    {
+        $jsMergedFile = AssetManager::getInstance()->getMergedNonCoreJavaScript();
+        $this->serveJsFile($jsMergedFile);
+    }
+
+    /**
+     * @param UIAsset $uiAsset
+     */
+    private function serveJsFile($uiAsset)
+    {
+        ProxyHttp::serverStaticFile($uiAsset->getAbsoluteLocation(), self::JS_MIME_TYPE);
     }
 
     /**
diff --git a/tests/PHPUnit/Core/AssetManager/PluginManagerMock.php b/tests/PHPUnit/Core/AssetManager/PluginManagerMock.php
new file mode 100644
index 0000000000..de0a7153e3
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/PluginManagerMock.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+use Piwik\Plugin\Manager;
+use Piwik\Plugin;
+use Piwik\Theme;
+
+class PluginManagerMock extends Manager
+{
+
+    /**
+     * @var Plugin[]
+     */
+    private $plugins = array();
+
+    /**
+     * @var Theme
+     */
+    private $loadedTheme;
+
+    /**
+     * @param Plugin[] $plugins
+     */
+    public function setPlugins($plugins)
+    {
+        $this->plugins = $plugins;
+    }
+
+    public function getLoadedPlugin($name)
+    {
+        foreach($this->plugins as $plugin)
+            if($plugin->getPluginName() == $name)
+                return $plugin;
+
+        return null;
+    }
+
+    public function getLoadedPluginsName()
+    {
+        $pluginNames = array();
+
+        foreach($this->plugins as $plugin)
+            $pluginNames[] = $plugin->getPluginName();
+
+        return $pluginNames;
+    }
+
+    public function getLoadedPlugins()
+    {
+        return $this->plugins;
+    }
+
+    public function getTheme($themeName)
+    {
+        return $this->loadedTheme;
+    }
+
+    /**
+     * @param Theme $loadedTheme
+     */
+    public function setLoadedTheme($loadedTheme)
+    {
+        $this->loadedTheme = $loadedTheme;
+    }
+}
diff --git a/tests/PHPUnit/Core/AssetManager/PluginMock.php b/tests/PHPUnit/Core/AssetManager/PluginMock.php
new file mode 100644
index 0000000000..3253124350
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/PluginMock.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+use Piwik\Plugin;
+
+class PluginMock extends Plugin
+{
+    /**
+     * @var string[]
+     */
+    private $jsFiles = array();
+
+    /**
+     * @var string[]
+     */
+    private $stylesheetFiles = array();
+
+    /**
+     * @var string
+     */
+    private $jsCustomization = '';
+
+    /**
+     * @var string
+     */
+    private $cssCustomization = '';
+
+    /**
+     * @var boolean
+     */
+    private $isTheme = false;
+
+    /**
+     * @param string $name
+     */
+    function __construct($name)
+    {
+        $this->pluginName = $name;
+    }
+
+    public function getListHooksRegistered()
+    {
+        return array(
+            'AssetManager.getJavaScriptFiles' => 'getJsFiles',
+            'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
+            'AssetManager.filterMergedJavaScripts' => 'filterMergedJavaScriptsHook',
+            'AssetManager.filterMergedStylesheets' => 'filterMergedStylesheetsHook',
+        );
+    }
+
+    /**
+     * @param string[] $jsFiles
+     */
+    public function getJsFiles(&$jsFiles)
+    {
+        $jsFiles = array_merge($jsFiles, $this->jsFiles);
+    }
+
+    /**
+     * @param string[] $stylesheetFiles
+     */
+    public function getStylesheetFiles(&$stylesheetFiles)
+    {
+        $stylesheetFiles = array_merge($stylesheetFiles, $this->stylesheetFiles);
+    }
+
+    /**
+     * @param string $mergedContent
+     */
+    public function filterMergedJavaScriptsHook(&$mergedContent)
+    {
+        $mergedContent .= $this->jsCustomization;
+    }
+
+    /**
+     * @param string $mergedContent
+     */
+    public function filterMergedStylesheetsHook(&$mergedContent)
+    {
+        $mergedContent .= $this->cssCustomization;
+    }
+
+    /**
+     * @param string $cssCustomization
+     */
+    public function setCssCustomization($cssCustomization)
+    {
+        $this->cssCustomization = $cssCustomization;
+    }
+
+    /**
+     * @param string $jsCustomization
+     */
+    public function setJsCustomization($jsCustomization)
+    {
+        $this->jsCustomization = $jsCustomization;
+    }
+
+    /**
+     * @param string[] $jsFiles
+     */
+    public function setJsFiles($jsFiles)
+    {
+        $this->jsFiles = $jsFiles;
+    }
+
+    /**
+     * @param string[] $stylesheetFiles
+     */
+    public function setStylesheetFiles($stylesheetFiles)
+    {
+        $this->stylesheetFiles = $stylesheetFiles;
+    }
+
+    /**
+     * @param boolean $isTheme
+     */
+    public function setIsTheme($isTheme)
+    {
+        $this->isTheme = $isTheme;
+    }
+
+    public function isTheme()
+    {
+        return $this->isTheme;
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/AssetManager/ThemeMock.php b/tests/PHPUnit/Core/AssetManager/ThemeMock.php
new file mode 100644
index 0000000000..1d21edb60d
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/ThemeMock.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+use Piwik\Plugin;
+use Piwik\Theme;
+
+class ThemeMock extends Theme
+{
+
+    /**
+     * @var string[]
+     */
+    private $jsFiles = array();
+
+    /**
+     * @var string
+     */
+    private $stylesheet;
+
+    /**
+     * @var Plugin
+     */
+    private $plugin;
+
+    /**
+     * @param Plugin $plugin
+     */
+    function __construct($plugin)
+    {
+        $this->plugin = $plugin;
+    }
+
+    public function getStylesheet()
+    {
+        return $this->stylesheet;
+    }
+
+    public function getJavaScriptFiles()
+    {
+        return $this->jsFiles;
+    }
+
+    /**
+     * @param string[] $jsFiles
+     */
+    public function setJsFiles($jsFiles)
+    {
+        $this->jsFiles = $jsFiles;
+    }
+
+    /**
+     * @param string $stylesheet
+     */
+    public function setStylesheet($stylesheet)
+    {
+        $this->stylesheet = $stylesheet;
+    }
+
+    /**
+     * @return Plugin
+     */
+    public function getPlugin()
+    {
+        return $this->plugin;
+    }
+
+    public function getThemeName()
+    {
+        return $this->plugin->getPluginName();
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterMock.php b/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterMock.php
new file mode 100644
index 0000000000..a14c20232f
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterMock.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+use Piwik\AssetManager\UIAssetCacheBuster;
+
+class UIAssetCacheBusterMock extends UIAssetCacheBuster
+{
+    /**
+     * @var string
+     */
+    private $piwikVersionBasedCacheBuster;
+
+    /**
+     * @var string
+     */
+    private $md5BasedCacheBuster;
+
+    public function piwikVersionBasedCacheBuster()
+    {
+        return $this->piwikVersionBasedCacheBuster;
+    }
+
+    public function md5BasedCacheBuster($content)
+    {
+        return $this->md5BasedCacheBuster;
+    }
+
+    /**
+     * @param string $md5BasedCacheBuster
+     */
+    public function setMd5BasedCacheBuster($md5BasedCacheBuster)
+    {
+        $this->md5BasedCacheBuster = $md5BasedCacheBuster;
+    }
+
+    /**
+     * @param string $piwikVersionBasedCacheBuster
+     */
+    public function setPiwikVersionBasedCacheBuster($piwikVersionBasedCacheBuster)
+    {
+        $this->piwikVersionBasedCacheBuster = $piwikVersionBasedCacheBuster;
+    }
+}
diff --git a/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterTest.php b/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterTest.php
new file mode 100644
index 0000000000..705000049c
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterTest.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+use Piwik\AssetManager\UIAssetCacheBuster;
+
+class UIAssetCacheBusterTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * @var UIAssetCacheBuster
+     */
+    private $cacheBuster;
+
+    public function setUp()
+    {
+        $this->cacheBuster = UIAssetCacheBuster::getInstance();
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_md5BasedCacheBuster()
+    {
+        $this->assertEquals('098f6bcd4621d373cade4e832627b4f6', $this->cacheBuster->md5BasedCacheBuster('test'));
+    }
+}
diff --git a/tests/PHPUnit/Core/AssetManager/UIAssetCatalogSorterTest.php b/tests/PHPUnit/Core/AssetManager/UIAssetCatalogSorterTest.php
new file mode 100644
index 0000000000..223be99b9c
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/UIAssetCatalogSorterTest.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+use Piwik\AssetManager\UIAsset\OnDiskUIAsset;
+use Piwik\AssetManager\UIAssetCatalog;
+use Piwik\AssetManager\UIAssetCatalogSorter;
+
+class UIAssetCatalogSorterTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @group Core
+     */
+    public function testPrioritySort()
+    {
+        $baseDirectory = '/var/www/piwik/';
+
+        $priorityPatterns = array(
+            'libs/base.css',
+            'libs/',
+            'plugins/',
+        );
+
+        $catalogSorter = new UIAssetCatalogSorter($priorityPatterns);
+
+        $unsortedCatalog = new UIAssetCatalog($catalogSorter);
+        $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'new_dir/new_file'));
+        $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'plugins/xyz'));
+        $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'plugins/abc'));
+        $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/xyz'));
+        $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/base.css'));
+        $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/abc'));
+        $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'plugins/xyz'));
+        $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/base.css'));
+        $unsortedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/xyz'));
+
+        $expectedCatalog = new UIAssetCatalog($catalogSorter);
+        $expectedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/base.css'));
+        $expectedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/xyz'));
+        $expectedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'libs/abc'));
+        $expectedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'plugins/xyz'));
+        $expectedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'plugins/abc'));
+        $expectedCatalog->addUIAsset(new OnDiskUIAsset($baseDirectory, 'new_dir/new_file'));
+
+        $sortedCatalog = $unsortedCatalog->getSortedCatalog();
+
+        $this->assertEquals($expectedCatalog, $sortedCatalog);
+    }
+}
diff --git a/tests/PHPUnit/Core/AssetManager/UIAssetMinifierTest.php b/tests/PHPUnit/Core/AssetManager/UIAssetMinifierTest.php
new file mode 100644
index 0000000000..845055652c
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/UIAssetMinifierTest.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+use Piwik\AssetManager\UIAsset\OnDiskUIAsset;
+use Piwik\AssetManager\UIAssetMinifier;
+
+class UIAssetMinifierTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * @var UIAssetMinifier
+     */
+    private $assetMinifier;
+
+    public function setUp()
+    {
+        $this->assetMinifier = UIAssetMinifier::getInstance();
+    }
+
+    public function provider_isMinifiedJs()
+    {
+        return array(
+            array('libs/jquery/jquery.js', true),
+            array('libs/jquery/jquery-ui.js', true),
+            array('libs/jquery/jquery.browser.js', true),
+            array('libs/jqplot/jqplot-custom.min.js', true),
+            array('plugins/TreemapVisualization/libs/Jit/jit-2.0.1-yc.js', true),
+            array('plugins/TreemapVisualization/javascripts/treemapViz.js', false),
+            array('plugins/UserCountryMap/javascripts/vendor/raphael.min.js', true),
+            array('plugins/UserCountryMap/javascripts/vendor/jquery.qtip.min.js', true),
+            array('plugins/UserCountryMap/javascripts/vendor/kartograph.min.js', true),
+            array('plugins/UserCountryMap/javascripts/vendor/jquery.qtip.min.js', true),
+        );
+    }
+
+    /**
+     * @group Core
+     * @dataProvider provider_isMinifiedJs
+     */
+    public function test_isMinifiedJs($scriptFileName, $isMinified)
+    {
+        $scriptFile = new OnDiskUIAsset(PIWIK_USER_PATH, $scriptFileName);
+
+        $this->assertEquals(
+            $isMinified,
+            $this->assetMinifier->isMinifiedJs($scriptFile->getContent())
+        );
+    }
+}
diff --git a/tests/PHPUnit/Core/AssetManager/configs/merged-assets-disabled.ini.php b/tests/PHPUnit/Core/AssetManager/configs/merged-assets-disabled.ini.php
new file mode 100644
index 0000000000..00887b8c52
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/configs/merged-assets-disabled.ini.php
@@ -0,0 +1,2 @@
+[Debug]
+disable_merged_assets = 1
diff --git a/tests/PHPUnit/Core/AssetManager/configs/merged-assets-enabled.ini.php b/tests/PHPUnit/Core/AssetManager/configs/merged-assets-enabled.ini.php
new file mode 100644
index 0000000000..df10787203
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/configs/merged-assets-enabled.ini.php
@@ -0,0 +1,2 @@
+[Debug]
+disable_merged_assets = 0
diff --git a/tests/PHPUnit/Core/AssetManager/configs/plugins.ini.php b/tests/PHPUnit/Core/AssetManager/configs/plugins.ini.php
new file mode 100644
index 0000000000..72834098e0
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/configs/plugins.ini.php
@@ -0,0 +1,3 @@
+[Plugins]
+Plugins[] = MockCorePlugin
+Plugins[] = CoreThemePlugin
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultCore.js b/tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultCore.js
new file mode 100644
index 0000000000..823fc76cdb
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultCore.js
@@ -0,0 +1,6 @@
+/* Piwik Javascript - cb={{{CACHE-BUSTER-JS}}}*/
+
+if(typeof SimpleObject!=='object'){SimpleObject=(function(){var privateVar;function privateMethod(param){privateVar=param;}
+return{publicMethod:function(){privateMethod('val');}}}());}
+var simpleArray=['el1','el2'];
+//This is a simple comment// customization via event
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultNonCore.js b/tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultNonCore.js
new file mode 100644
index 0000000000..f004dcc501
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/scripts/ExpectedMergeResultNonCore.js
@@ -0,0 +1,3 @@
+/* Piwik Javascript - cb={{{CACHE-BUSTER-JS}}}*/
+
+alert('test');
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/AssetManager/scripts/SimpleAlert.js b/tests/PHPUnit/Core/AssetManager/scripts/SimpleAlert.js
new file mode 100644
index 0000000000..003d113a36
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/scripts/SimpleAlert.js
@@ -0,0 +1 @@
+alert('test');
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/AssetManager/scripts/SimpleArray.js b/tests/PHPUnit/Core/AssetManager/scripts/SimpleArray.js
new file mode 100644
index 0000000000..3176f85cff
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/scripts/SimpleArray.js
@@ -0,0 +1 @@
+var simpleArray = ['el1', 'el2'];
diff --git a/tests/PHPUnit/Core/AssetManager/scripts/SimpleComments.js b/tests/PHPUnit/Core/AssetManager/scripts/SimpleComments.js
new file mode 100644
index 0000000000..5a7103256a
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/scripts/SimpleComments.js
@@ -0,0 +1 @@
+//This is a simple comment
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/AssetManager/scripts/SimpleObject.js b/tests/PHPUnit/Core/AssetManager/scripts/SimpleObject.js
new file mode 100644
index 0000000000..aacdf5069c
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/scripts/SimpleObject.js
@@ -0,0 +1,18 @@
+if (typeof SimpleObject !== 'object') {
+
+    SimpleObject = (function () {
+
+        var privateVar;
+
+        function privateMethod(param) {
+            privateVar = param;
+        }
+
+        return {
+
+            publicMethod: function () {
+                privateMethod('val');
+            }
+        }
+    }());
+}
diff --git a/tests/PHPUnit/Core/AssetManager/stylesheets/CssWithURLs.css b/tests/PHPUnit/Core/AssetManager/stylesheets/CssWithURLs.css
new file mode 100644
index 0000000000..75c919da98
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/stylesheets/CssWithURLs.css
@@ -0,0 +1,12 @@
+h1 {
+    color: orange;
+    text-align: center;
+    /* url relative to root: must not be rewritten*/
+    background: url(tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png);
+}
+
+p {
+    font-size: 20px;
+    /* url relative to file: must be rewritten*/
+    background: url(images/test-image.png);
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/AssetManager/stylesheets/ExpectedMergeResult.css b/tests/PHPUnit/Core/AssetManager/stylesheets/ExpectedMergeResult.css
new file mode 100644
index 0000000000..3b86a91e52
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/stylesheets/ExpectedMergeResult.css
@@ -0,0 +1,30 @@
+/* compile_me_once={{{CACHE-BUSTER-JS}}} */
+/* Piwik CSS file is compiled with Less. You may be interested in writing a custom Theme for Piwik! */
+#page #header {
+  color: white;
+}
+#footer {
+  color: red;
+}
+.box {
+  color: #fe33ac;
+  border-color: #fdcdea;
+}
+.box div {
+  -webkit-box-shadow: 0 0 5px rgba(0,0,0,0.3);
+  -moz-box-shadow: 0 0 5px rgba(0,0,0,0.3);
+  box-shadow: 0 0 5px rgba(0,0,0,0.3);
+}
+h1 {
+  color: orange;
+  text-align: center;
+  background: url(tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png);
+}
+p {
+  font-size: 20px;
+  background: url(tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png);
+}
+body {
+  background-color: #b0c4de;
+}
+/* customization via event */
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/AssetManager/stylesheets/ImportedLess.less b/tests/PHPUnit/Core/AssetManager/stylesheets/ImportedLess.less
new file mode 100644
index 0000000000..db566317b6
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/stylesheets/ImportedLess.less
@@ -0,0 +1,12 @@
+@var: red;
+
+#page {
+  @var: white;
+  #header {
+    color: @var;
+  }
+}
+
+#footer {
+  color: @var;
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/AssetManager/stylesheets/SimpleBody.css b/tests/PHPUnit/Core/AssetManager/stylesheets/SimpleBody.css
new file mode 100644
index 0000000000..6e0e55777a
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/stylesheets/SimpleBody.css
@@ -0,0 +1,3 @@
+body {
+    background-color:#b0c4de;
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/AssetManager/stylesheets/SimpleLess.less b/tests/PHPUnit/Core/AssetManager/stylesheets/SimpleLess.less
new file mode 100644
index 0000000000..fd7b33af31
--- /dev/null
+++ b/tests/PHPUnit/Core/AssetManager/stylesheets/SimpleLess.less
@@ -0,0 +1,17 @@
+@import "ImportedLess";
+
+@base: #f938ab;
+
+.box-shadow(@style, @c) when (iscolor(@c)) {
+  -webkit-box-shadow: @style @c;
+  -moz-box-shadow:    @style @c;
+  box-shadow:         @style @c;
+}
+.box-shadow(@style, @alpha: 50%) when (isnumber(@alpha)) {
+  .box-shadow(@style, rgba(0, 0, 0, @alpha));
+}
+.box {
+  color: saturate(@base, 5%);
+  border-color: lighten(@base, 30%);
+  div { .box-shadow(0 0 5px, 30%) }
+}
diff --git a/tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png b/tests/PHPUnit/Core/AssetManager/stylesheets/images/test-image.png
new file mode 100644
index 0000000000000000000000000000000000000000..da6e1bbe89dc52e9fdaae1119f1c7353ed997076
GIT binary patch
literal 290
zcmV+-0p0$IP)<h;3K|Lk000e1NJLTq000mG000XJ0ssI2`GA^k0002#Nkl<ZD3NVZ
zK@LSQ5LKC#orJyESXy!c?&1ngVI^#w!orbAkVq^?`=>3{|MOpyl9~7V=4IeG4u^Bc
zb)9c;DyrKCat;K~^Qb5I<uEA|LS5!#VV-B;h`RBxC2v`l^E{&{ve}F=G65GrT$dh6
zv!`hq$I<uweczj=F#<gy@F##QsZ*9^Q558mWf@t8VQ2)12s)f8nfL0t?)%=b)y(X6
zcnaz$48xG7X`bg_8HA96zt1O0634O4HVNO6Y(d+$RaFH+U<>XHHrs5i>pEJFRbkZE
of6`%H*LRnQ+H>2spEYa22TbujoNSKsqyPW_07*qoM6N<$f+WX)cmMzZ

literal 0
HcmV?d00001

diff --git a/tests/PHPUnit/Core/AssetManagerTest.php b/tests/PHPUnit/Core/AssetManagerTest.php
index 21c061c028..0ffc40771d 100644
--- a/tests/PHPUnit/Core/AssetManagerTest.php
+++ b/tests/PHPUnit/Core/AssetManagerTest.php
@@ -6,38 +6,706 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 use Piwik\AssetManager;
+use Piwik\AssetManager\UIAsset\OnDiskUIAsset;
+use Piwik\AssetManager\UIAsset;
+use Piwik\AssetManager\UIAssetFetcher\StaticUIAssetFetcher;
+use Piwik\Config;
+use Piwik\EventDispatcher;
+use Piwik\Plugin\Manager;
+use Piwik\Plugin;
+use Piwik\Theme;
+
+require_once PIWIK_INCLUDE_PATH . "/tests/PHPUnit/Core/AssetManager/UIAssetCacheBusterMock.php";
+require_once PIWIK_INCLUDE_PATH . "/tests/PHPUnit/Core/AssetManager/PluginManagerMock.php";
+require_once PIWIK_INCLUDE_PATH . "/tests/PHPUnit/Core/AssetManager/PluginMock.php";
+require_once PIWIK_INCLUDE_PATH . "/tests/PHPUnit/Core/AssetManager/ThemeMock.php";
 
 class AssetManagerTest extends PHPUnit_Framework_TestCase
 {
+    // todo Theme->rewriteAssetPathIfOverridesFound is not tested
+
+    const ASSET_MANAGER_TEST_DIR = 'tests/PHPUnit/Core/AssetManager/';
+
+    const FIRST_CACHE_BUSTER_JS = 'first-cache-buster-js';
+    const SECOND_CACHE_BUSTER_JS = 'second-cache-buster-js';
+    const FIRST_CACHE_BUSTER_SS = 'first-cache-buster-stylesheet';
+    const SECOND_CACHE_BUSTER_SS = 'second-cache-buster-stylesheet';
+
+    const CORE_PLUGIN_NAME = 'MockCorePlugin';
+    const CORE_PLUGIN_WITHOUT_ASSETS_NAME = 'MockCoreWithoutAssetPlugin';
+    const NON_CORE_PLUGIN_NAME = 'MockNonCorePlugin';
+    const CORE_THEME_PLUGIN_NAME = 'CoreThemePlugin';
+    const NON_CORE_THEME_PLUGIN_NAME = 'NonCoreThemePlugin';
+
     /**
-     * @group Core
+     * @var AssetManager
+     */
+    private $assetManager;
+
+    /**
+     * @var UIAsset
+     */
+    private $mergedAsset;
+
+    /**
+     * @var UIAssetCacheBusterMock
      */
-    public function testPrioritySort()
+    private $cacheBuster;
+
+    /**
+     * @var PluginManagerMock
+     */
+    private $pluginManager;
+
+    public function setUp()
+    {
+        $this->activateMergedAssets();
+
+        $this->setUpCacheBuster();
+
+        $this->setUpAssetManager();
+
+        $this->setUpPluginManager();
+
+        $this->setUpTheme();
+
+        $this->setUpPlugins();
+    }
+
+    public function tearDown()
+    {
+        $this->assetManager->removeMergedAssets();
+    }
+
+    private function activateMergedAssets()
+    {
+        $this->setUpConfig('merged-assets-enabled.ini.php');
+    }
+
+    private function disableMergedAssets()
+    {
+        $this->setUpConfig('merged-assets-disabled.ini.php');
+    }
+
+    /**
+     * @param string $filename
+     */
+    private function setUpConfig($filename)
+    {
+        $userFile = PIWIK_INCLUDE_PATH . '/' . self::ASSET_MANAGER_TEST_DIR . 'configs/' . $filename;
+        $globalFile = PIWIK_INCLUDE_PATH . '/' . self::ASSET_MANAGER_TEST_DIR . 'configs/plugins.ini.php';
+
+        $config = Config::getInstance();
+        $config->setTestEnvironment($userFile, $globalFile);
+        $config->init();
+    }
+
+    private function setUpCacheBuster()
+    {
+        $this->cacheBuster = UIAssetCacheBusterMock::getInstance();
+    }
+
+    private function setUpAssetManager()
+    {
+        $this->assetManager = AssetManager::getInstance();
+
+        $this->assetManager->removeMergedAssets();
+
+        $this->assetManager->setCacheBuster($this->cacheBuster);
+    }
+
+    private function setUpPluginManager()
     {
-        $buckets = array(
-            'libs/base.css',
-            'libs/',
-            'plugins/',
+        $this->pluginManager = PluginManagerMock::getInstance();
+        Manager::setSingletonInstance($this->pluginManager);
+    }
+
+    private function setUpPlugins()
+    {
+        $this->pluginManager->setPlugins(
+            array(
+                 $this->getCoreTheme()->getPlugin(),
+                 $this->getNonCoreTheme()->getPlugin(),
+                 $this->getCorePlugin(),
+                 $this->getCorePluginWithoutUIAssets(),
+                 $this->getNonCorePlugin()
+            )
         );
 
-        $data = array(
-            'plugins/xyz',
-            'plugins/abc',
-            'libs/xyz',
-            'libs/base.css',
-            'libs/abc',
-            'plugins/xyz',
-            'libs/xyz',
+        $this->pluginManager->setLoadedTheme($this->getNonCoreTheme());
+    }
+
+    private function setUpCorePluginOnly()
+    {
+        $this->pluginManager->setPlugins(
+            array(
+                 $this->getCorePlugin(),
+            )
         );
+    }
+
+    /**
+     * @return Plugin
+     */
+    private function getCorePlugin()
+    {
+        $corePlugin = new PluginMock(self::CORE_PLUGIN_NAME);
 
-        $expected = array(
-            'libs/base.css',
-            'libs/xyz',
-            'libs/abc',
-            'plugins/xyz',
-            'plugins/abc',
+        $corePlugin->setJsFiles(
+            array(
+                 self::ASSET_MANAGER_TEST_DIR . 'scripts/SimpleObject.js',
+                 self::ASSET_MANAGER_TEST_DIR . 'scripts/SimpleArray.js',
+            )
         );
 
-        $this->assertEquals($expected, AssetManager::prioritySort($buckets, $data));
+        $corePlugin->setStylesheetFiles($this->getCorePluginStylesheetFiles());
+        $corePlugin->setJsCustomization('// customization via event');
+        $corePlugin->setCssCustomization('/* customization via event */');
+
+        return $corePlugin;
+    }
+
+    /**
+     * @return Plugin
+     */
+    private function getCorePluginWithoutUIAssets()
+    {
+        return new PluginMock(self::CORE_PLUGIN_WITHOUT_ASSETS_NAME);
+    }
+
+    /**
+     * @return Plugin
+     */
+    private function getNonCorePlugin()
+    {
+        $nonCorePlugin = new PluginMock(self::NON_CORE_PLUGIN_NAME);
+        $nonCorePlugin->setJsFiles(array(self::ASSET_MANAGER_TEST_DIR . 'scripts/SimpleAlert.js'));
+
+        return $nonCorePlugin;
+    }
+
+    private function setUpTheme()
+    {
+        $this->assetManager->setTheme($this->getCoreTheme());
+    }
+
+    /**
+     * @return ThemeMock
+     */
+    private function getCoreTheme()
+    {
+        return $this->createTheme(self::CORE_THEME_PLUGIN_NAME);
+    }
+
+    /**
+     * @return ThemeMock
+     */
+    private function getNonCoreTheme()
+    {
+        return $this->createTheme(self::NON_CORE_THEME_PLUGIN_NAME);
+    }
+
+    /**
+     * @param string $themeName
+     * @return ThemeMock
+     */
+    private function createTheme($themeName)
+    {
+        $coreThemePlugin = new PluginMock($themeName);
+
+        $coreThemePlugin->setIsTheme(true);
+
+        $coreTheme = new ThemeMock($coreThemePlugin);
+
+        $coreTheme->setStylesheet($this->getCoreThemeStylesheet());
+        $coreTheme->setJsFiles(array(self::ASSET_MANAGER_TEST_DIR . 'scripts/SimpleComments.js'));
+
+        return $coreTheme;
+    }
+
+    /**
+     * @return string[]
+     */
+    public function getCorePluginStylesheetFiles()
+    {
+        return array(
+            self::ASSET_MANAGER_TEST_DIR . 'stylesheets/SimpleLess.less',
+            self::ASSET_MANAGER_TEST_DIR . 'stylesheets/CssWithURLs.css',
+        );
+    }
+
+    private function clearDateCache()
+    {
+        clearstatcache();
+    }
+
+    /**
+     * @return int
+     */
+    private function waitAndGetModificationDate()
+    {
+        $this->clearDateCache();
+
+        sleep(1.5);
+
+        $modificationDate = $this->mergedAsset->getModificationDate();
+
+        return $modificationDate;
+    }
+
+    /**
+     * @param string $cacheBuster
+     */
+    private function setJSCacheBuster($cacheBuster)
+    {
+        $this->cacheBuster->setPiwikVersionBasedCacheBuster($cacheBuster);
+    }
+
+    /**
+     * @param string $cacheBuster
+     */
+    private function setStylesheetCacheBuster($cacheBuster)
+    {
+        $this->cacheBuster->setMd5BasedCacheBuster($cacheBuster);
+    }
+
+    private function triggerGetMergedCoreJavaScript()
+    {
+        $this->mergedAsset = $this->assetManager->getMergedCoreJavaScript();
+    }
+
+    private function triggerGetMergedNonCoreJavaScript()
+    {
+        $this->mergedAsset = $this->assetManager->getMergedNonCoreJavaScript();
+    }
+
+    private function triggerGetMergedStylesheet()
+    {
+        $this->mergedAsset = $this->assetManager->getMergedStylesheet();
+    }
+
+    private function validateMergedCoreJs()
+    {
+        $expectedContent = $this->getExpectedMergedCoreJs();
+
+        $this->validateExpectedContent($expectedContent);
+    }
+
+    private function validateMergedNonCoreJs()
+    {
+        $expectedContent = $this->getExpectedMergedNonCoreJs();
+
+        $this->validateExpectedContent($expectedContent);
+    }
+
+    private function validateMergedStylesheet()
+    {
+        $expectedContent = $this->getExpectedMergedStylesheet();
+
+        $this->validateExpectedContent($expectedContent);
+    }
+
+    /**
+     * @param string $expectedContent
+     */
+    private function validateExpectedContent($expectedContent)
+    {
+        $this->assertEquals($expectedContent, $this->mergedAsset->getContent());
+    }
+
+    /**
+     * @return string
+     */
+    private function getExpectedMergedCoreJs()
+    {
+        return $this->getExpectedMergedJs('ExpectedMergeResultCore.js');
+    }
+
+    /**
+     * @return string
+     */
+    private function getExpectedMergedNonCoreJs()
+    {
+        return $this->getExpectedMergedJs('ExpectedMergeResultNonCore.js');
+    }
+
+    /**
+     * @param string $filename
+     * @return string
+     */
+    private function getExpectedMergedJs($filename)
+    {
+        $expectedMergeResult = new OnDiskUIAsset(PIWIK_USER_PATH, self::ASSET_MANAGER_TEST_DIR .'scripts/' . $filename);
+
+        $expectedContent = $expectedMergeResult->getContent();
+
+        return $this->adjustExpectedJsContent($expectedContent);
+    }
+
+    /**
+     * @param string $expectedJsContent
+     * @return string
+     */
+    private function adjustExpectedJsContent($expectedJsContent)
+    {
+        $expectedJsContent = str_replace("\n", "\r\n", $expectedJsContent);
+
+        $expectedJsContent = $this->specifyCacheBusterInExpectedContent($expectedJsContent, $this->cacheBuster->piwikVersionBasedCacheBuster());
+
+        return $expectedJsContent;
+    }
+
+    /**
+     * @return string
+     */
+    private function getExpectedMergedStylesheet()
+    {
+        $expectedMergeResult = new OnDiskUIAsset(PIWIK_USER_PATH, self::ASSET_MANAGER_TEST_DIR .'stylesheets/ExpectedMergeResult.css');
+
+        $expectedContent = $expectedMergeResult->getContent();
+
+        $expectedContent = $this->specifyCacheBusterInExpectedContent($expectedContent, $this->cacheBuster->md5BasedCacheBuster(''));
+
+        return $expectedContent;
+    }
+
+    /**
+     * @return string
+     */
+    private function getCoreThemeStylesheet()
+    {
+        return self::ASSET_MANAGER_TEST_DIR . 'stylesheets/SimpleBody.css';
+    }
+
+    /**
+     * @param string $content
+     * @param string $cacheBuster
+     * @return string
+     */
+    private function specifyCacheBusterInExpectedContent($content, $cacheBuster)
+    {
+        return str_replace('{{{CACHE-BUSTER-JS}}}', $cacheBuster, $content);
+    }
+
+    /**
+     * @param int $previousDate
+     */
+    private function validateDateDidNotChange($previousDate)
+    {
+        $this->clearDateCache();
+
+        $this->assertEquals($previousDate, $this->mergedAsset->getModificationDate());
+    }
+
+    /**
+     * @param int $previousDate
+     */
+    private function validateDateIsMoreRecent($previousDate)
+    {
+        $this->clearDateCache();
+
+        $this->assertTrue($previousDate < $this->mergedAsset->getModificationDate());
+    }
+
+    /**
+     * @return string
+     */
+    private function getJsTranslationScript()
+    {
+        return
+            '<script type="text/javascript">' . PHP_EOL .
+            'var translations = [];' . PHP_EOL .
+            'if(typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }for(var i in translations) { piwik_translations[i] = translations[i];} function _pk_translate(translationStringId) { if( typeof(piwik_translations[translationStringId]) != \'undefined\' ){  return piwik_translations[translationStringId]; }return "The string "+translationStringId+" was not loaded in javascript. Make sure it is added in the Translate.getClientSideTranslationKeys hook.";}' . PHP_EOL .
+            '</script>';
+    }
+
+    /**
+     * @return UIAsset[]
+     */
+    private function generateAllMergedAssets()
+    {
+        $this->triggerGetMergedStylesheet();
+        $stylesheetAsset = $this->mergedAsset;
+
+        $this->triggerGetMergedCoreJavaScript();
+        $coreJsAsset = $this->mergedAsset;
+
+        $this->triggerGetMergedNonCoreJavaScript();
+        $nonCoreJsAsset = $this->mergedAsset;
+
+        $this->assertTrue($stylesheetAsset->exists());
+        $this->assertTrue($coreJsAsset->exists());
+        $this->assertTrue($nonCoreJsAsset->exists());
+
+        return array($stylesheetAsset, $coreJsAsset, $nonCoreJsAsset);
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_getMergedCoreJavaScript_NotGenerated()
+    {
+        $this->setJSCacheBuster(self::FIRST_CACHE_BUSTER_JS);
+
+        $this->triggerGetMergedCoreJavaScript();
+
+        $this->validateMergedCoreJs();
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_getMergedNonCoreJavaScript_NotGenerated()
+    {
+        $this->setJSCacheBuster(self::FIRST_CACHE_BUSTER_JS);
+
+        $this->triggerGetMergedNonCoreJavaScript();
+
+        $this->validateMergedNonCoreJs();
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_getMergedNonCoreJavaScript_NotGenerated_NoNonCorePlugin()
+    {
+        $this->setUpCorePluginOnly();
+
+        $this->setJSCacheBuster(self::FIRST_CACHE_BUSTER_JS);
+
+        $this->triggerGetMergedNonCoreJavaScript();
+
+        $expectedContent = $this->adjustExpectedJsContent('/* Piwik Javascript - cb={{{CACHE-BUSTER-JS}}}*/' . PHP_EOL);
+
+        $this->validateExpectedContent($expectedContent);
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_getMergedCoreJavaScript_AlreadyGenerated_MergedAssetsDisabled_UpToDate()
+    {
+        $this->disableMergedAssets();
+
+        $this->setJSCacheBuster(self::FIRST_CACHE_BUSTER_JS);
+
+        $this->triggerGetMergedCoreJavaScript();
+
+        $modDateBeforeSecondRequest = $this->waitAndGetModificationDate();
+
+        $this->triggerGetMergedCoreJavaScript();
+
+        $this->validateDateDidNotChange($modDateBeforeSecondRequest);
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_getMergedCoreJavaScript_AlreadyGenerated_MergedAssetsDisabled_Stale()
+    {
+        $this->disableMergedAssets();
+
+        $this->setJSCacheBuster(self::FIRST_CACHE_BUSTER_JS);
+
+        $this->triggerGetMergedCoreJavaScript();
+
+        $modDateBeforeSecondRequest = $this->waitAndGetModificationDate();
+
+        $this->setJSCacheBuster(self::SECOND_CACHE_BUSTER_JS);
+
+        $this->triggerGetMergedCoreJavaScript();
+
+        $this->validateDateIsMoreRecent($modDateBeforeSecondRequest);
+
+        $this->validateMergedCoreJs();
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_getMergedStylesheet_NotGenerated()
+    {
+        $this->setStylesheetCacheBuster(self::FIRST_CACHE_BUSTER_SS);
+
+        $this->triggerGetMergedStylesheet();
+
+        $this->validateMergedStylesheet();
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_getMergedStylesheet_Generated_MergedAssetsEnabled_Stale()
+    {
+        $this->setStylesheetCacheBuster(self::FIRST_CACHE_BUSTER_SS);
+
+        $this->triggerGetMergedStylesheet();
+
+        $modDateBeforeSecondRequest = $this->waitAndGetModificationDate();
+
+        $this->setStylesheetCacheBuster(self::SECOND_CACHE_BUSTER_SS);
+
+        $this->triggerGetMergedStylesheet();
+
+        $this->validateDateDidNotChange($modDateBeforeSecondRequest);
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_getMergedStylesheet_Generated_MergedAssetsDisabled_Stale()
+    {
+        $this->disableMergedAssets();
+
+        $this->setStylesheetCacheBuster(self::FIRST_CACHE_BUSTER_SS);
+
+        $this->triggerGetMergedStylesheet();
+
+        $modDateBeforeSecondRequest = $this->waitAndGetModificationDate();
+
+        $this->setStylesheetCacheBuster(self::SECOND_CACHE_BUSTER_SS);
+
+        $this->triggerGetMergedStylesheet();
+
+        $this->validateDateIsMoreRecent($modDateBeforeSecondRequest);
+
+        $this->validateMergedStylesheet();
+    }
+
+
+    /**
+     * @group Core
+     */
+    public function test_getMergedStylesheet_Generated_MergedAssetsDisabled_UpToDate()
+    {
+        $this->disableMergedAssets();
+
+        $this->setStylesheetCacheBuster(self::FIRST_CACHE_BUSTER_SS);
+
+        $this->triggerGetMergedStylesheet();
+
+        $modDateBeforeSecondRequest = $this->waitAndGetModificationDate();
+
+        $this->triggerGetMergedStylesheet();
+
+        $this->validateDateDidNotChange($modDateBeforeSecondRequest);
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_getCssInclusionDirective()
+    {
+        $expectedCssInclusionDirective = '<link rel="stylesheet" type="text/css" href="index.php?module=Proxy&action=getCss" />' . PHP_EOL;
+
+        $this->assertEquals($expectedCssInclusionDirective, $this->assetManager->getCssInclusionDirective());
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_getJsInclusionDirective_MergedAssetsDisabled()
+    {
+        $this->disableMergedAssets();
+
+        $expectedJsInclusionDirective =
+            $this->getJsTranslationScript() .
+            '<script type="text/javascript" src="tests/PHPUnit/Core/AssetManager/scripts/SimpleObject.js"></script>' . PHP_EOL .
+            '<script type="text/javascript" src="tests/PHPUnit/Core/AssetManager/scripts/SimpleArray.js"></script>' . PHP_EOL .
+            '<script type="text/javascript" src="tests/PHPUnit/Core/AssetManager/scripts/SimpleComments.js"></script>' . PHP_EOL .
+            '<script type="text/javascript" src="tests/PHPUnit/Core/AssetManager/scripts/SimpleAlert.js"></script>' . PHP_EOL;
+
+        $this->assertEquals($expectedJsInclusionDirective, $this->assetManager->getJsInclusionDirective());
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_getJsInclusionDirective_MergedAssetsEnabled()
+    {
+        $expectedJsInclusionDirective =
+            $this->getJsTranslationScript() .
+            '<script type="text/javascript" src="index.php?module=Proxy&action=getCoreJs"></script>' . PHP_EOL .
+            '<script type="text/javascript" src="index.php?module=Proxy&action=getNonCoreJs"></script>' . PHP_EOL;
+
+        $this->assertEquals($expectedJsInclusionDirective, $this->assetManager->getJsInclusionDirective());
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_getCompiledBaseCss()
+    {
+        $this->setStylesheetCacheBuster(self::FIRST_CACHE_BUSTER_SS);
+
+        $staticStylesheetList = array_merge($this->getCorePluginStylesheetFiles(), array($this->getCoreThemeStylesheet()));
+
+        $minimalAssetFetcher = new StaticUIAssetFetcher(
+            array_reverse($staticStylesheetList),
+            $staticStylesheetList,
+            $this->getCoreTheme()
+        );
+
+        $this->assetManager->setMinimalStylesheetFetcher($minimalAssetFetcher);
+
+        $this->mergedAsset = $this->assetManager->getCompiledBaseCss();
+
+        $this->validateMergedStylesheet();
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_removeMergedAssets()
+    {
+        list($stylesheetAsset, $coreJsAsset, $nonCoreJsAsset) = $this->generateAllMergedAssets();
+
+        $this->assetManager->removeMergedAssets();
+
+        $this->assertFalse($stylesheetAsset->exists());
+        $this->assertFalse($coreJsAsset->exists());
+        $this->assertFalse($nonCoreJsAsset->exists());
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_removeMergedAssets_PluginNameSpecified_PluginWithoutAssets()
+    {
+        list($stylesheetAsset, $coreJsAsset, $nonCoreJsAsset) = $this->generateAllMergedAssets();
+
+        $this->assetManager->removeMergedAssets(self::CORE_PLUGIN_WITHOUT_ASSETS_NAME);
+
+        $this->assertFalse($stylesheetAsset->exists());
+        $this->assertTrue($coreJsAsset->exists());
+        $this->assertTrue($nonCoreJsAsset->exists());
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_removeMergedAssets_PluginNameSpecified_CorePlugin()
+    {
+        list($stylesheetAsset, $coreJsAsset, $nonCoreJsAsset) = $this->generateAllMergedAssets();
+
+        $this->assetManager->removeMergedAssets(self::CORE_PLUGIN_NAME);
+
+        $this->assertFalse($stylesheetAsset->exists());
+        $this->assertFalse($coreJsAsset->exists());
+        $this->assertTrue($nonCoreJsAsset->exists());
+    }
+
+    /**
+     * @group Core
+     */
+    public function test_removeMergedAssets_PluginNameSpecified_NonCoreThemeWithAssets()
+    {
+        list($stylesheetAsset, $coreJsAsset, $nonCoreJsAsset) = $this->generateAllMergedAssets();
+
+        $this->assetManager->removeMergedAssets(self::NON_CORE_THEME_PLUGIN_NAME);
+
+        $this->assertFalse($stylesheetAsset->exists());
+        $this->assertTrue($coreJsAsset->exists());
+        $this->assertFalse($nonCoreJsAsset->exists());
     }
-}
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/ServeStaticFileTest.php b/tests/PHPUnit/Core/ServeStaticFileTest.php
index 9482227fea..d97fa4a6cf 100644
--- a/tests/PHPUnit/Core/ServeStaticFileTest.php
+++ b/tests/PHPUnit/Core/ServeStaticFileTest.php
@@ -477,8 +477,7 @@ class Test_Piwik_ServeStaticFile extends PHPUnit_Framework_TestCase
 
     private function getCompressedFileLocation()
     {
-        $path = PIWIK_PATH_TEST_TO_ROOT . \Piwik\AssetManager::COMPRESSED_FILE_LOCATION . basename(TEST_FILE_LOCATION);
-        return \Piwik\SettingsPiwik::rewriteTmpPathWithHostname($path);
+        return \Piwik\AssetManager::getInstance()->getAssetDirectory() . '/' . basename(TEST_FILE_LOCATION);
     }
 
     private function removeCompressedFiles()
diff --git a/tests/PHPUnit/UI b/tests/PHPUnit/UI
index cbf0922b35..673d21c6a7 160000
--- a/tests/PHPUnit/UI
+++ b/tests/PHPUnit/UI
@@ -1 +1 @@
-Subproject commit cbf0922b352e6592076908ee1fab095cd8e0642c
+Subproject commit 673d21c6a750d8280f4e9ab9d203072dd8d844d2
diff --git a/tests/PHPUnit/UITest.php b/tests/PHPUnit/UITest.php
index 5d7c555a32..cd2b86833c 100644
--- a/tests/PHPUnit/UITest.php
+++ b/tests/PHPUnit/UITest.php
@@ -49,7 +49,7 @@ abstract class UITest extends IntegrationTestCase
 
         DbHelper::createAnonymousUser();
         
-        AssetManager::removeMergedAssets();
+        AssetManager::getInstance()->removeMergedAssets();
         
         // launch archiving so tests don't run out of time
         Rules::$purgeDisabledByTests = true;
-- 
GitLab