diff --git a/.gitignore b/.gitignore
index 344d651bf5c9d5382e220f570251713b85f9f088..7a8311d0be4a946a79f43a5709fa90d5549a1131 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,8 +22,11 @@ php_errors.log
 /plugins/ImageGraph/fonts/unifont.ttf
 /plugins/ImageGraph/fonts/unifont.ttf.zip
 /plugins/*/tests/System/processed
+/plugins/*/Test/System/processed
 /plugins/*/tests/UI/processed-ui-screenshots
+/plugins/*/Test/UI/processed-ui-screenshots
 /plugins/*/tests/UI/screenshot-diffs
+/plugins/*/Test/UI/screenshot-diffs
 /robots.txt
 /tmp/
 /vendor/
diff --git a/core/ArchiveProcessor.php b/core/ArchiveProcessor.php
index 58282e72d19b65d25380318ffef5909c3c6ee73d..cb24d833678a97d15e431b4b6e0c4b7f3d3ae1fa 100644
--- a/core/ArchiveProcessor.php
+++ b/core/ArchiveProcessor.php
@@ -344,7 +344,10 @@ class ArchiveProcessor
         // By default we shall aggregate all sub-tables.
         $dataTable = $this->getArchive()->getDataTableExpanded($name, $idSubTable = null, $depth = null, $addMetadataSubtableId = false);
 
+        $columnsRenamed = false;
+
         if ($dataTable instanceof Map) {
+            $columnsRenamed = true;
             // see https://github.com/piwik/piwik/issues/4377
             $self = $this;
             $dataTable->filter(function ($table) use ($self, $columnsToRenameAfterAggregation) {
@@ -353,7 +356,11 @@ class ArchiveProcessor
         }
 
         $dataTable = $this->getAggregatedDataTableMap($dataTable, $columnsAggregationOperation);
-        $this->renameColumnsAfterAggregation($dataTable, $columnsToRenameAfterAggregation);
+
+        if (!$columnsRenamed) {
+            $this->renameColumnsAfterAggregation($dataTable, $columnsToRenameAfterAggregation);
+        }
+        
         return $dataTable;
     }
 
diff --git a/core/Container/ContainerFactory.php b/core/Container/ContainerFactory.php
index cbbc36a00a7d6243bd00a41f601dbff0dfae1283..af2a09b695d4f260a3875e93ac7413725d42f6c5 100644
--- a/core/Container/ContainerFactory.php
+++ b/core/Container/ContainerFactory.php
@@ -27,12 +27,18 @@ class ContainerFactory
      */
     private $environment;
 
+    /**
+     * @var array
+     */
+    private $definitions;
+
     /**
      * @param string|null $environment Optional environment config to load.
      */
-    public function __construct($environment = null)
+    public function __construct($environment = null, array $definitions = array())
     {
         $this->environment = $environment;
+        $this->definitions = $definitions;
     }
 
     /**
@@ -69,6 +75,10 @@ class ContainerFactory
         // Environment config
         $this->addEnvironmentConfig($builder);
 
+        if (!empty($this->definitions)) {
+            $builder->addDefinitions($this->definitions);
+        }
+
         return $builder->build();
     }
 
diff --git a/core/Container/StaticContainer.php b/core/Container/StaticContainer.php
index 874851acdee7d268c819f91503138a6171f3e0f7..8468dce2aeda4585eb68381c3fabe9c17e3b81b4 100644
--- a/core/Container/StaticContainer.php
+++ b/core/Container/StaticContainer.php
@@ -31,6 +31,13 @@ class StaticContainer
      */
     private static $environment;
 
+    /**
+     * Definitions to register in the container.
+     *
+     * @var array
+     */
+    private static $definitions = array();
+
     /**
      * @return Container
      */
@@ -63,7 +70,7 @@ class StaticContainer
      */
     private static function createContainer()
     {
-        $containerFactory = new ContainerFactory(self::$environment);
+        $containerFactory = new ContainerFactory(self::$environment, self::$definitions);
         return $containerFactory->create();
     }
 
@@ -77,6 +84,11 @@ class StaticContainer
         self::$environment = $environment;
     }
 
+    public static function addDefinitions(array $definitions)
+    {
+        self::$definitions = $definitions;
+    }
+
     /**
      * Proxy to Container::get()
      *
diff --git a/core/Plugin.php b/core/Plugin.php
index af564a002b8e3eaaa6ab89be037888a2685181f3..bb361b5bb5e596b4c9038ce2ad7b5985d77745af 100644
--- a/core/Plugin.php
+++ b/core/Plugin.php
@@ -8,6 +8,7 @@
  */
 namespace Piwik;
 
+use Piwik\Container\StaticContainer;
 use Piwik\Plugin\Dependency;
 use Piwik\Plugin\MetadataLoader;
 
@@ -359,7 +360,7 @@ class Plugin
             $this->cache->save($cacheId, $classname);
         }
 
-        return new $classname;
+        return StaticContainer::get($classname);
     }
 
     public function findMultipleComponents($directoryWithinPlugin, $expectedSubclass)
diff --git a/core/Plugin/Menu.php b/core/Plugin/Menu.php
index d2c54645f1c3e4afbbf389681877c9afbb17a10d..e354728821c7d259a7dea145fa5cc486e01e79ee 100644
--- a/core/Plugin/Menu.php
+++ b/core/Plugin/Menu.php
@@ -30,16 +30,6 @@ use Piwik\Plugins\UsersManager\UserPreferences;
  */
 class Menu
 {
-    protected $module = '';
-
-    /**
-     * @ignore
-     */
-    public function __construct()
-    {
-        $this->module = $this->getModule();
-    }
-
     private function getModule()
     {
         $className = get_class($this);
@@ -69,7 +59,7 @@ class Menu
     {
         $params = (array) $additionalParams;
         $params['action'] = '';
-        $params['module'] = $this->module;
+        $params['module'] = $this->getModule();
 
         return $params;
     }
@@ -88,11 +78,12 @@ class Menu
      */
     protected function urlForAction($controllerAction, $additionalParams = array())
     {
-        $this->checkisValidCallable($this->module, $controllerAction);
+        $module = $this->getModule();
+        $this->checkisValidCallable($module, $controllerAction);
 
         $params = (array) $additionalParams;
         $params['action'] = $controllerAction;
-        $params['module'] = $this->module;
+        $params['module'] = $module;
 
         return $params;
     }
diff --git a/core/Plugin/Widgets.php b/core/Plugin/Widgets.php
index 36081fe2d29e86ae09677cbbf0dcc923c4b4733c..5a0cad6fde1f5131b40b5e9cec9d80f626655f3b 100644
--- a/core/Plugin/Widgets.php
+++ b/core/Plugin/Widgets.php
@@ -25,16 +25,6 @@ class Widgets
     protected $category = '';
     protected $widgets  = array();
 
-    private $module = '';
-
-    /**
-     * @ignore
-     */
-    public function __construct()
-    {
-        $this->module = $this->getModule();
-    }
-
     /**
      * @ignore
      */
@@ -77,7 +67,7 @@ class Widgets
                                  'name'     => $name,
                                  'params'   => $parameters,
                                  'method'   => $method,
-                                 'module'   => $this->module);
+                                 'module'   => $this->getModule());
     }
 
     /**
@@ -182,7 +172,7 @@ class Widgets
             return;
         }
 
-        $controllerClass = '\\Piwik\\Plugins\\' . $this->module . '\\Controller';
+        $controllerClass = 'Piwik\\Plugins\\' . $this->getModule() . '\\Controller';
 
         if (!Development::methodExists($this, $method) &&
             !Development::methodExists($controllerClass, $method)) {
diff --git a/core/Version.php b/core/Version.php
index 3474cbf56ba1f592e90109fd964e71f6076bc626..f9c367e4e4c31bcae1c148b5f51b82f2ceb26b28 100644
--- a/core/Version.php
+++ b/core/Version.php
@@ -20,7 +20,7 @@ final class Version
      * The current Piwik version.
      * @var string
      */
-    const VERSION = '2.12.0-b3';
+    const VERSION = '2.12.0-b5';
 
     public function isStableVersion($version)
     {
diff --git a/core/View/OneClickDone.php b/core/View/OneClickDone.php
index e07c2c3bddb46b3e90e7770c44f9d0aa3ce0d320..1d8c8b809f99b7bfbb669b32b59d572077340828 100644
--- a/core/View/OneClickDone.php
+++ b/core/View/OneClickDone.php
@@ -30,13 +30,20 @@ class OneClickDone
     /**
      * @var string
      */
-    public $coreError;
+    public $error;
 
     /**
      * @var array
      */
     public $feedbackMessages;
 
+    /**
+     * Did the download over HTTPS fail?
+     *
+     * @var bool
+     */
+    public $httpsFail = false;
+
     public function __construct($tokenAuth)
     {
         $this->tokenAuth = $tokenAuth;
@@ -56,9 +63,10 @@ class OneClickDone
         @header('Cache-Control: must-revalidate');
         @header('X-Frame-Options: deny');
 
-        $error = htmlspecialchars($this->coreError, ENT_QUOTES, 'UTF-8');
+        $error = htmlspecialchars($this->error, ENT_QUOTES, 'UTF-8');
         $messages = htmlspecialchars(serialize($this->feedbackMessages), ENT_QUOTES, 'UTF-8');
         $tokenAuth = $this->tokenAuth;
+        $httpsFail = (int) $this->httpsFail;
 
         // use a heredoc instead of an external file
         echo <<<END_OF_TEMPLATE
@@ -73,6 +81,7 @@ class OneClickDone
    <input type="hidden" name="token_auth" value="$tokenAuth" />
    <input type="hidden" name="error" value="$error" />
    <input type="hidden" name="messages" value="$messages" />
+   <input type="hidden" name="httpsFail" value="$httpsFail" />
    <noscript>
     <button type="submit">Continue</button>
    </noscript>
diff --git a/plugins/CoreConsole/Commands/DevelopmentSyncUITestScreenshots.php b/plugins/CoreConsole/Commands/DevelopmentSyncUITestScreenshots.php
index be1bedd05d07d05714e2ed320cac263fef1fb973..a8fa9a7a6c197d083ef7f7f61aea8d53db880014 100644
--- a/plugins/CoreConsole/Commands/DevelopmentSyncUITestScreenshots.php
+++ b/plugins/CoreConsole/Commands/DevelopmentSyncUITestScreenshots.php
@@ -14,9 +14,13 @@ use Piwik\Http;
 use Piwik\Plugin\ConsoleCommand;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\Output;
 use Symfony\Component\Console\Output\OutputInterface;
 
 /**
+ * Tool for core developers to help making UI screenshot builds green.
+ *
  */
 class DevelopmentSyncUITestScreenshots extends ConsoleCommand
 {
@@ -29,40 +33,44 @@ class DevelopmentSyncUITestScreenshots extends ConsoleCommand
     {
         $this->setName('development:sync-ui-test-screenshots');
         $this->setDescription('For Piwik core devs. Copies screenshots '
-                            . 'from travis artifacts to tests/UI/expected-ui-screenshots/');
+                            . 'from travis artifacts to the tests/UI/expected-ui-screenshots/ folder');
         $this->addArgument('buildnumber', InputArgument::REQUIRED, 'Travis build number you want to sync.');
         $this->addArgument('screenshotsRegex', InputArgument::OPTIONAL,
             'A regex to use when selecting screenshots to copy. If not supplied all screenshots are copied.', '.*');
+        $this->addOption('plugin', 'p', InputOption::VALUE_OPTIONAL, 'Plugin name you want to sync screenshots for.');
+        $this->addOption('http-user', '', InputOption::VALUE_OPTIONAL, 'the HTTP AUTH username (for premium plugins where artifacts are protected)');
+        $this->addOption('http-password', '', InputOption::VALUE_OPTIONAL, 'the HTTP AUTH password (for premium plugins where artifacts are protected)');
     }
 
     protected function execute(InputInterface $input, OutputInterface $output)
     {
         $buildNumber = $input->getArgument('buildnumber');
-        $screenshotsRegex = $input->getArgument('screenshotsRegex');
-
         if (empty($buildNumber)) {
             throw new \InvalidArgumentException('Missing build number.');
         }
 
-        $urlBase = sprintf('http://builds-artifacts.piwik.org/ui-tests.master/%s', $buildNumber);
-        $diffviewer = Http::sendHttpRequest($urlBase . "/screenshot-diffs/diffviewer.html", $timeout = 60);
-        $diffviewer = str_replace('&', '&amp;', $diffviewer);
+        $screenshotsRegex = $input->getArgument('screenshotsRegex');
+
+        $plugin = $input->getOption('plugin');
+
+        $httpUser = $input->getOption('http-user');
+        $httpPassword = $input->getOption('http-password');
+
+        $urlBase = $this->getUrlBase($plugin, $buildNumber);
+        $diffviewer = $this->getDiffviewerContent($output, $urlBase, $httpUser, $httpPassword);
+
+        if(empty($diffviewer)) {
+            throw new \Exception("Screenshot tests artifacts were not found for this build.");
+        }
+
 
         $dom = new \DOMDocument();
         $dom->loadHTML($diffviewer);
         foreach ($dom->getElementsByTagName("tr") as $row) {
             $columns = $row->getElementsByTagName("td");
 
-            $nameColumn = $columns->item(0);
             $processedColumn = $columns->item(3);
 
-            $testPlugin = null;
-            if ($nameColumn
-                && preg_match("/\(for ([a-zA-Z_]+) plugin\)/", $dom->saveXml($nameColumn), $matches)
-            ) {
-                $testPlugin = $matches[1];
-            }
-
             $file = null;
             if ($processedColumn
                 && preg_match("/href=\".*\/(.*)\"/", $dom->saveXml($processedColumn), $matches)
@@ -73,42 +81,152 @@ class DevelopmentSyncUITestScreenshots extends ConsoleCommand
             if ($file !== null
                 && preg_match("/" . $screenshotsRegex . "/", $file)
             ) {
-                if ($testPlugin == null) {
-                    $downloadTo = "tests/UI/expected-ui-screenshots/$file";
-                } else {
-                    $downloadTo = "plugins/$testPlugin/tests/UI/expected-ui-screenshots/$file";
-                }
-
-                $output->write("<info>Downloading $file to  $downloadTo...</info>\n");
-                Http::sendHttpRequest("$urlBase/processed-ui-screenshots/$file", $timeout = 60, $userAgent = null,
-                    PIWIK_DOCUMENT_ROOT . "/" . $downloadTo);
+                $this->downloadProcessedScreenshot($output, $urlBase, $file, $plugin, $httpUser, $httpPassword);
             }
         }
 
-        $this->displayGitInstructions($output);
+        $this->displayGitInstructions($output, $plugin);
 
     }
 
-    /**
-     * @param OutputInterface $output
-     */
-    protected function displayGitInstructions(OutputInterface $output)
+    protected function displayGitInstructions(OutputInterface $output, $plugin)
     {
         $output->writeln('');
         $output->writeln('--------------');
         $output->writeln('');
         $output->writeln("If all downloaded screenshots are valid you may push them with these commands:");
-        $output->writeln('');
-        $commands = "cd tests/UI/expected-ui-screenshots
+        $downloadToPath = $this->getDownloadToPath($plugin);
+        $commands = "
+cd $downloadToPath
 git pull
 git add .
-git commit -m '' # WRITE A COMMIT MESSAGE
-git push
+git commit -m '' # Write a good commit message, eg. 'Fixed UI test failure caused by change introduced in <core or plugin commit> which caused failure by <explanation of failure>'
+git push";
+
+        if(empty($plugin)) {
+            $commands .= "
 cd ..
 git pull
-git add expected-ui-screenshots
-git commit -m '' #WRITE A COMMIT MESSAGE
-git push";
+git add expected-ui-screenshots/
+git commit -m '' # Copy paste the good commit message
+git push
+cd ../../\n\n";
+        } else {
+            $commands .= "
+cd ../../../../../\n\n";
+        }
         $output->writeln($commands);
     }
+
+    protected function getUrlBase($plugin, $buildNumber)
+    {
+        if ($plugin) {
+            return sprintf('http://builds-artifacts.piwik.org/ui-tests.master.%s/%s', $plugin, $buildNumber);
+        }
+        return sprintf('http://builds-artifacts.piwik.org/ui-tests.master/%s', $buildNumber);
+    }
+
+    protected function getDownloadToPath($plugin)
+    {
+        if (empty($plugin)) {
+            return PIWIK_DOCUMENT_ROOT . "/tests/UI/expected-ui-screenshots/";
+        }
+
+        $downloadTo = PIWIK_DOCUMENT_ROOT . "/plugins/$plugin/tests/UI/expected-ui-screenshots/";
+        if(is_dir($downloadTo)) {
+            return $downloadTo;
+        }
+
+        // Maybe the plugin is using folder "Test/" instead of "tests/"
+        $downloadTo = str_replace("tests/", "Test/", $downloadTo);
+        if(is_dir($downloadTo)) {
+            return $downloadTo;
+        }
+        throw new \Exception("Download to path could not be found: $downloadTo");
+    }
+
+    protected function getDiffviewerContent(OutputInterface $output, $urlBase, $httpUser = false, $httpPassword = false)
+    {
+        $diffviewerUrl = $this->getDiffviewerUrl($urlBase);
+
+        try {
+            return $this->downloadDiffviewer($output, $diffviewerUrl);
+        } catch(\Exception $e) {
+
+            // Maybe this is a Premium Piwik PRO plugin...
+            return $this->getDiffviewContentForPrivatePlugin($output, $urlBase, $httpUser, $httpPassword);
+        }
+    }
+
+    protected function getDiffviewContentForPrivatePlugin(OutputInterface $output, $urlBase, $httpUser, $httpPassword)
+    {
+        if (empty($httpUser) || empty($httpPassword)) {
+            $output->writeln("<info>--http-user and --http-password was not specified, skip download of private plugins screenshots.</info>");
+            return;
+        }
+
+        // Attempt to download from protected/ artifacts...
+        $urlBase = str_replace("builds-artifacts.piwik.org/", "builds-artifacts.piwik.org/protected/", $urlBase);
+        $diffviewerUrl = $this->getDiffviewerUrl($urlBase);
+
+        return $this->downloadDiffviewer($output, $diffviewerUrl, $httpUser, $httpPassword);
+    }
+
+    /**
+     * @return string
+     */
+    protected function getDiffviewerUrl($urlBase)
+    {
+        return $urlBase . "/screenshot-diffs/diffviewer.html";
+    }
+
+    protected function downloadDiffviewer(OutputInterface $output, $urlDiffviewer, $httpUsername = false, $httpPassword = false)
+    {
+        $responseExtended = Http::sendHttpRequest(
+            $urlDiffviewer,
+            $timeout = 60,
+            $userAgent = null,
+            $destinationPath = null,
+            $followDepth = 0,
+            $acceptLanguage = false,
+            $byteRange = false,
+            $getExtendedInfo = true,
+            $httpMethod = 'GET',
+            $httpUsername,
+            $httpPassword
+        );
+        $httpStatus = $responseExtended['status'];
+        if ($httpStatus == '200') {
+            $output->writeln("Found diffviewer at: " . $urlDiffviewer);
+            $diffviewer = str_replace('&', '&amp;', $responseExtended['data']);
+            return $diffviewer;
+        }
+
+        if($httpStatus == '401') {
+            $output->writeln("<error>HTTP AUTH username and password are not valid.</error>");
+        }
+        throw new \Exception ("Failed downloading diffviewer from $urlDiffviewer - Got HTTP status " . $httpStatus);
+    }
+
+
+    protected function downloadProcessedScreenshot(OutputInterface $output, $urlBase, $file, $plugin, $httpUser, $httpPassword)
+    {
+        $downloadTo = $this->getDownloadToPath($plugin) . $file;
+
+        $output->write("<info>Downloading $file to  $downloadTo...</info>\n");
+        $urlProcessedScreenshot = $urlBase . "/processed-ui-screenshots/$file";
+
+        Http::sendHttpRequest($urlProcessedScreenshot,
+            $timeout = 60,
+            $userAgent = null,
+            $downloadTo,
+            $followDepth = 0,
+            $acceptLanguage = false,
+            $byteRange = false,
+            $getExtendedInfo = true,
+            $httpMethod = 'GET',
+            $httpUser,
+            $httpPassword);
+    }
+
 }
diff --git a/plugins/CoreHome/Widgets.php b/plugins/CoreHome/Widgets.php
index 68812f07494005d2f53960b493a595752a842294..17d888bd35c43c6226af07a4cc0740047a27d6a4 100644
--- a/plugins/CoreHome/Widgets.php
+++ b/plugins/CoreHome/Widgets.php
@@ -10,12 +10,23 @@ namespace Piwik\Plugins\CoreHome;
 
 use Piwik\Common;
 use Piwik\Piwik;
+use Piwik\Translation\Translator;
 use Piwik\View;
 
 class Widgets extends \Piwik\Plugin\Widgets
 {
     protected $category = 'Example Widgets';
 
+    /**
+     * @var Translator
+     */
+    private $translator;
+
+    public function __construct(Translator $translator)
+    {
+        $this->translator = $translator;
+    }
+
     protected function init()
     {
         $this->addWidget('CoreHome_SupportPiwik', 'getDonateForm');
@@ -31,7 +42,7 @@ class Widgets extends \Piwik\Plugin\Widgets
 
         if (Common::getRequestVar('widget', false)
             && Piwik::hasUserSuperUserAccess()) {
-            $view->footerMessage = Piwik::translate('CoreHome_OnlyForSuperUserAccess');
+            $view->footerMessage = $this->translator->translate('CoreHome_OnlyForSuperUserAccess');
         }
 
         return $view->render();
@@ -43,8 +54,8 @@ class Widgets extends \Piwik\Plugin\Widgets
     public function getPromoVideo()
     {
         $view = new View('@CoreHome/getPromoVideo');
-        $view->shareText     = Piwik::translate('CoreHome_SharePiwikShort');
-        $view->shareTextLong = Piwik::translate('CoreHome_SharePiwikLong');
+        $view->shareText     = $this->translator->translate('CoreHome_SharePiwikShort');
+        $view->shareTextLong = $this->translator->translate('CoreHome_SharePiwikLong');
         $view->promoVideoUrl = 'https://www.youtube.com/watch?v=OslfF_EH81g';
 
         return $view->render();
diff --git a/plugins/CoreUpdater/ArchiveDownloadException.php b/plugins/CoreUpdater/ArchiveDownloadException.php
new file mode 100644
index 0000000000000000000000000000000000000000..0b68f30c180deb74087b437a8d33c3b835388f9f
--- /dev/null
+++ b/plugins/CoreUpdater/ArchiveDownloadException.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreUpdater;
+
+use Exception;
+
+/**
+ * Error while downloading the archive.
+ */
+class ArchiveDownloadException extends UpdaterException
+{
+    public function __construct(Exception $exception)
+    {
+        parent::__construct($exception, array());
+    }
+}
diff --git a/plugins/CoreUpdater/Controller.php b/plugins/CoreUpdater/Controller.php
index 998ef3ece76ccc6dc0bd5df23e8bac53ae270746..d15dc231e6178337233078d85a97d3f367e82b4b 100644
--- a/plugins/CoreUpdater/Controller.php
+++ b/plugins/CoreUpdater/Controller.php
@@ -9,10 +9,8 @@
 namespace Piwik\Plugins\CoreUpdater;
 
 use Exception;
-use Piwik\ArchiveProcessor\Rules;
 use Piwik\Common;
 use Piwik\Config;
-use Piwik\Container\StaticContainer;
 use Piwik\DbHelper;
 use Piwik\Filechecks;
 use Piwik\Filesystem;
@@ -24,51 +22,34 @@ use Piwik\Plugin;
 use Piwik\Plugins\CorePluginsAdmin\Marketplace;
 use Piwik\Plugins\LanguagesManager\LanguagesManager;
 use Piwik\SettingsServer;
-use Piwik\Unzip;
-use Piwik\UpdateCheck;
-use Piwik\Updater;
+use Piwik\Updater as DbUpdater;
 use Piwik\Version;
 use Piwik\View\OneClickDone;
 use Piwik\View;
 
-/**
- *
- */
 class Controller extends \Piwik\Plugin\Controller
 {
-    const PATH_TO_EXTRACT_LATEST_VERSION = '/latest/';
-    const LATEST_VERSION_URL = '://builds.piwik.org/piwik.zip';
-    const LATEST_BETA_VERSION_URL = '://builds.piwik.org/piwik-%s.zip';
-
     private $coreError = false;
     private $warningMessages = array();
     private $errorMessages = array();
     private $deactivatedPlugins = array();
-    private $pathPiwikZip = false;
-    private $newVersion;
 
-    protected static function getLatestZipUrl($newVersion)
-    {
-        if (@Config::getInstance()->Debug['allow_upgrades_to_beta']) {
-            $url = sprintf(self::LATEST_BETA_VERSION_URL, $newVersion);
-        } else {
-            $url = self::LATEST_VERSION_URL;
-        }
+    /**
+     * @var Updater
+     */
+    private $updater;
 
-        if (self::isUpdatingOverHttps()) {
-            $url = 'https' . $url;
-        } else {
-            $url = 'http' . $url;
-        }
-
-        return $url;
+    public function __construct(Updater $updater)
+    {
+        $this->updater = $updater;
     }
 
     public function newVersionAvailable()
     {
         Piwik::checkUserHasSuperUserAccess();
+        $this->checkNewVersionIsAvailableOrDie();
 
-        $newVersion = $this->checkNewVersionIsAvailableOrDie();
+        $newVersion = $this->updater->getLatestVersion();
 
         $view = new View('@CoreUpdater/newVersionAvailable');
         $this->addCustomLogoInfo($view);
@@ -88,7 +69,7 @@ class Controller extends \Piwik\Plugin\Controller
 
         $view->marketplacePlugins = $marketplacePlugins;
         $view->incompatiblePlugins = $incompatiblePlugins;
-        $view->piwik_latest_version_url = self::getLatestZipUrl($newVersion);
+        $view->piwik_latest_version_url = $this->updater->getArchiveUrl($newVersion);
         $view->can_auto_update  = Filechecks::canAutoUpdate();
         $view->makeWritableCommands = Filechecks::getAutoUpdateMakeWritableMessage();
 
@@ -98,61 +79,38 @@ class Controller extends \Piwik\Plugin\Controller
     public function oneClickUpdate()
     {
         Piwik::checkUserHasSuperUserAccess();
-        $this->newVersion = $this->checkNewVersionIsAvailableOrDie();
-
-        SettingsServer::setMaxExecutionTime(0);
 
-        $url = self::getLatestZipUrl($this->newVersion);
-        $steps = array(
-            array('oneClick_Download', Piwik::translate('CoreUpdater_DownloadingUpdateFromX', $url)),
-            array('oneClick_Unpack', Piwik::translate('CoreUpdater_UnpackingTheUpdate')),
-            array('oneClick_Verify', Piwik::translate('CoreUpdater_VerifyingUnpackedFiles')),
-        );
-        $incompatiblePlugins = $this->getIncompatiblePlugins($this->newVersion);
-        if (!empty($incompatiblePlugins)) {
-            $namesToDisable = array();
-            foreach ($incompatiblePlugins as $incompatiblePlugin) {
-                $namesToDisable[] = $incompatiblePlugin->getPluginName();
-            }
-            $steps[] = array('oneClick_DisableIncompatiblePlugins', Piwik::translate('CoreUpdater_DisablingIncompatiblePlugins', implode(', ', $namesToDisable)));
-        }
+        $view = new OneClickDone(Piwik::getCurrentUserTokenAuth());
 
-        $steps[] = array('oneClick_Copy', Piwik::translate('CoreUpdater_InstallingTheLatestVersion'));
-        $steps[] = array('oneClick_Finished', Piwik::translate('CoreUpdater_PiwikUpdatedSuccessfully'));
+        $useHttps = Common::getRequestVar('https', 1, 'int');
 
-        $errorMessage = false;
-        $messages = array();
-        foreach ($steps as $step) {
-            try {
-                $method = $step[0];
-                $message = $step[1];
-                $this->$method();
-                $messages[] = $message;
-            } catch (Exception $e) {
-                $errorMessage = $e->getMessage();
-                break;
-            }
+        try {
+            $messages = $this->updater->updatePiwik($useHttps);
+        } catch (ArchiveDownloadException $e) {
+            $view->httpsFail = $useHttps;
+            $view->error = $e->getMessage();
+            $messages = $e->getUpdateLogMessages();
+        } catch (UpdaterException $e) {
+            $view->error = $e->getMessage();
+            $messages = $e->getUpdateLogMessages();
         }
 
-        $view = new OneClickDone(Piwik::getCurrentUserTokenAuth());
-        $view->coreError = $errorMessage;
         $view->feedbackMessages = $messages;
-
         $this->addCustomLogoInfo($view);
-
         return $view->render();
     }
 
     public function oneClickResults()
     {
         $view = new View('@CoreUpdater/oneClickResults');
-        $view->coreError = Common::getRequestVar('error', '', 'string', $_POST);
+        $view->error = Common::getRequestVar('error', '', 'string', $_POST);
         $view->feedbackMessages = safe_unserialize(Common::unsanitizeInputValue(Common::getRequestVar('messages', '', 'string', $_POST)));
+        $view->httpsFail = (bool) Common::getRequestVar('httpsFail', 0, 'int', $_POST);
         $this->addCustomLogoInfo($view);
         return $view->render();
     }
 
-    protected function redirectToDashboardWhenNoError($updater)
+    protected function redirectToDashboardWhenNoError(DbUpdater $updater)
     {
         if (count($updater->getSqlQueriesToExecute()) == 1
             && !$this->coreError
@@ -166,130 +124,9 @@ class Controller extends \Piwik\Plugin\Controller
 
     private function checkNewVersionIsAvailableOrDie()
     {
-        $newVersion = UpdateCheck::isNewestVersionAvailable();
-        if (!$newVersion) {
+        if (!$this->updater->isNewVersionAvailable()) {
             throw new Exception(Piwik::translate('CoreUpdater_ExceptionAlreadyLatestVersion', Version::VERSION));
         }
-        return $newVersion;
-    }
-
-    private function oneClick_Download()
-    {
-        $path = StaticContainer::get('path.tmp') . self::PATH_TO_EXTRACT_LATEST_VERSION;
-        $this->pathPiwikZip = $path . 'latest.zip';
-
-        Filechecks::dieIfDirectoriesNotWritable(array($path));
-
-        // we catch exceptions in the caller (i.e., oneClickUpdate)
-        $url = self::getLatestZipUrl($this->newVersion) . '?cb=' . $this->newVersion;
-
-        Http::fetchRemoteFile($url, $this->pathPiwikZip, 0, 120);
-    }
-
-    private function oneClick_Unpack()
-    {
-        $pathExtracted = StaticContainer::get('path.tmp') . self::PATH_TO_EXTRACT_LATEST_VERSION;
-
-        $this->pathRootExtractedPiwik = $pathExtracted . 'piwik';
-
-        if (file_exists($this->pathRootExtractedPiwik)) {
-            Filesystem::unlinkRecursive($this->pathRootExtractedPiwik, true);
-        }
-
-        $archive = Unzip::factory('PclZip', $this->pathPiwikZip);
-
-        if (0 == ($archive_files = $archive->extract($pathExtracted))) {
-            throw new Exception(Piwik::translate('CoreUpdater_ExceptionArchiveIncompatible', $archive->errorInfo()));
-        }
-
-        if (0 == count($archive_files)) {
-            throw new Exception(Piwik::translate('CoreUpdater_ExceptionArchiveEmpty'));
-        }
-        unlink($this->pathPiwikZip);
-    }
-
-    private function oneClick_Verify()
-    {
-        $someExpectedFiles = array(
-            '/config/global.ini.php',
-            '/index.php',
-            '/core/Piwik.php',
-            '/piwik.php',
-            '/plugins/API/API.php'
-        );
-        foreach ($someExpectedFiles as $file) {
-            if (!is_file($this->pathRootExtractedPiwik . $file)) {
-                throw new Exception(Piwik::translate('CoreUpdater_ExceptionArchiveIncomplete', $file));
-            }
-        }
-    }
-
-    private function oneClick_DisableIncompatiblePlugins()
-    {
-        $plugins = $this->getIncompatiblePlugins($this->newVersion);
-
-        foreach ($plugins as $plugin) {
-            PluginManager::getInstance()->deactivatePlugin($plugin->getPluginName());
-        }
-    }
-
-    private function oneClick_Copy()
-    {
-        /*
-         * Make sure the execute bit is set for this shell script
-         */
-        if (!Rules::isBrowserTriggerEnabled()) {
-            @chmod($this->pathRootExtractedPiwik . '/misc/cron/archive.sh', 0755);
-        }
-
-        $model = new Model();
-
-        /*
-         * Copy all files to PIWIK_INCLUDE_PATH.
-         * These files are accessed through the dispatcher.
-         */
-        Filesystem::copyRecursive($this->pathRootExtractedPiwik, PIWIK_INCLUDE_PATH);
-        $model->removeGoneFiles($this->pathRootExtractedPiwik, PIWIK_INCLUDE_PATH);
-
-        /*
-         * These files are visible in the web root and are generally
-         * served directly by the web server.  May be shared.
-         */
-        if (PIWIK_INCLUDE_PATH !== PIWIK_DOCUMENT_ROOT) {
-            /*
-             * Copy PHP files that expect to be in the document root
-             */
-            $specialCases = array(
-                '/index.php',
-                '/piwik.php',
-                '/js/index.php',
-            );
-
-            foreach ($specialCases as $file) {
-                Filesystem::copy($this->pathRootExtractedPiwik . $file, PIWIK_DOCUMENT_ROOT . $file);
-            }
-
-            /*
-             * Copy the non-PHP files (e.g., images, css, javascript)
-             */
-            Filesystem::copyRecursive($this->pathRootExtractedPiwik, PIWIK_DOCUMENT_ROOT, true);
-            $model->removeGoneFiles($this->pathRootExtractedPiwik, PIWIK_DOCUMENT_ROOT);
-        }
-
-        /*
-         * Config files may be user (account) specific
-         */
-        if (PIWIK_INCLUDE_PATH !== PIWIK_USER_PATH) {
-            Filesystem::copyRecursive($this->pathRootExtractedPiwik . '/config', PIWIK_USER_PATH . '/config');
-        }
-
-        Filesystem::unlinkRecursive($this->pathRootExtractedPiwik, true);
-
-        Filesystem::clearPhpCaches();
-    }
-
-    private function oneClick_Finished()
-    {
     }
 
     public function index()
@@ -308,7 +145,7 @@ class Controller extends \Piwik\Plugin\Controller
 
     public function runUpdaterAndExit($doDryRun = null)
     {
-        $updater = new Updater();
+        $updater = new DbUpdater();
         $componentsWithUpdateFile = $updater->getComponentUpdates();
         if (empty($componentsWithUpdateFile)) {
             throw new NoUpdatesFoundException("Everything is already up to date.");
@@ -402,7 +239,7 @@ class Controller extends \Piwik\Plugin\Controller
         $view->coreToUpdate = $coreToUpdate;
     }
 
-    private function doExecuteUpdates($view, Updater $updater, $componentsWithUpdateFile)
+    private function doExecuteUpdates($view, DbUpdater $updater, $componentsWithUpdateFile)
     {
         $result = $updater->updateComponents($componentsWithUpdateFile);
 
diff --git a/tests/UI/Fixtures/UpdaterTestFixture.php b/plugins/CoreUpdater/Test/Fixtures/DbUpdaterTestFixture.php
similarity index 77%
rename from tests/UI/Fixtures/UpdaterTestFixture.php
rename to plugins/CoreUpdater/Test/Fixtures/DbUpdaterTestFixture.php
index e2b3a0024e706d6be9cbffd709409df3268402ed..89a29126f21d63a9206c82087056a7948d6d8b85 100644
--- a/tests/UI/Fixtures/UpdaterTestFixture.php
+++ b/plugins/CoreUpdater/Test/Fixtures/DbUpdaterTestFixture.php
@@ -6,9 +6,11 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-namespace Piwik\Tests\Fixtures;
+namespace Piwik\Plugins\CoreUpdater\Test\Fixtures;
 
-class UpdaterTestFixture extends SqlDump
+use Piwik\Tests\Fixtures\SqlDump;
+
+class DbUpdaterTestFixture extends SqlDump
 {
     public function performSetUp($setupEnvironmentOnly = false)
     {
@@ -18,4 +20,4 @@ class UpdaterTestFixture extends SqlDump
 
         parent::performSetUp($setupEnvironmentOnly);
     }
-}
\ No newline at end of file
+}
diff --git a/plugins/CoreUpdater/Test/Fixtures/FailUpdateHttpsFixture.php b/plugins/CoreUpdater/Test/Fixtures/FailUpdateHttpsFixture.php
new file mode 100644
index 0000000000000000000000000000000000000000..5060c993202d78f4efe0c35866dffe162a5d4928
--- /dev/null
+++ b/plugins/CoreUpdater/Test/Fixtures/FailUpdateHttpsFixture.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreUpdater\Test\Fixtures;
+
+use Piwik\Tests\Framework\Fixture;
+
+/**
+ * Fixture that makes the update over HTTPS fail to be able to test that users can still update over HTTP.
+ */
+class FailUpdateHttpsFixture extends Fixture
+{
+    public function provideContainerConfig()
+    {
+        return array(
+            'Piwik\Plugins\CoreUpdater\Updater' => \DI\object('Piwik\Plugins\CoreUpdater\Test\Mock\UpdaterMock'),
+        );
+    }
+}
diff --git a/plugins/CoreUpdater/tests/Integration/UpdateCommunicationTest.php b/plugins/CoreUpdater/Test/Integration/UpdateCommunicationTest.php
similarity index 99%
rename from plugins/CoreUpdater/tests/Integration/UpdateCommunicationTest.php
rename to plugins/CoreUpdater/Test/Integration/UpdateCommunicationTest.php
index 91b6e2c2a1d2b9923c00cf7862bf625d7e34766e..6ec5fdaf834baf2934350eccfe40c5a05654facc 100644
--- a/plugins/CoreUpdater/tests/Integration/UpdateCommunicationTest.php
+++ b/plugins/CoreUpdater/Test/Integration/UpdateCommunicationTest.php
@@ -6,7 +6,7 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-namespace Piwik\Plugins\CoreUpdater\tests;
+namespace Piwik\Plugins\CoreUpdater\Test;
 
 use Piwik\Config;
 use Piwik\Option;
diff --git a/plugins/CoreUpdater/Test/Mock/UpdaterMock.php b/plugins/CoreUpdater/Test/Mock/UpdaterMock.php
new file mode 100644
index 0000000000000000000000000000000000000000..7a73129c5bafd44ef23150daddc86acd20725456
--- /dev/null
+++ b/plugins/CoreUpdater/Test/Mock/UpdaterMock.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreUpdater\Test\Mock;
+
+use Piwik\Plugins\CoreUpdater\ArchiveDownloadException;
+use Piwik\Plugins\CoreUpdater\Updater;
+use Piwik\Translation\Translator;
+
+class UpdaterMock extends Updater
+{
+    /**
+     * @var Translator
+     */
+    private $translator;
+
+    public function __construct(Translator $translator)
+    {
+        $this->translator = $translator;
+    }
+
+    public function getLatestVersion()
+    {
+        return '4.0.0';
+    }
+
+    public function isNewVersionAvailable()
+    {
+        return true;
+    }
+
+    public function updatePiwik($https = true)
+    {
+        // Simulate that the update over HTTPS fails
+        if ($https) {
+            // The actual error message depends on the OS, the HTTP method etc.
+            // This is what I get on my machine, but it doesn't really matter
+            throw new ArchiveDownloadException(new \Exception('curl_exec: SSL certificate problem: Invalid certificate chain. Hostname requested was: piwik.org'), array());
+        }
+
+        // Simulate that the update over HTTP succeeds
+        return array(
+            $this->translator->translate('CoreUpdater_DownloadingUpdateFromX', ''),
+            $this->translator->translate('CoreUpdater_UnpackingTheUpdate'),
+            $this->translator->translate('CoreUpdater_VerifyingUnpackedFiles'),
+            $this->translator->translate('CoreUpdater_InstallingTheLatestVersion'),
+        );
+    }
+}
diff --git a/plugins/CoreUpdater/tests/Unit/ModelTest.php b/plugins/CoreUpdater/Test/Unit/ModelTest.php
similarity index 97%
rename from plugins/CoreUpdater/tests/Unit/ModelTest.php
rename to plugins/CoreUpdater/Test/Unit/ModelTest.php
index 3098595b7740fcfecf298ed857049a95b75297b3..521aa8fc351bbaecb4847b5d8a9bc12cdc3f5729 100644
--- a/plugins/CoreUpdater/tests/Unit/ModelTest.php
+++ b/plugins/CoreUpdater/Test/Unit/ModelTest.php
@@ -6,7 +6,8 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-namespace Piwik\Plugins\CoreUpdater\tests;
+namespace Piwik\Plugins\CoreUpdater\Test\Unit;
+
 use Piwik\Plugins\CoreUpdater\Model;
 
 /**
diff --git a/plugins/CoreUpdater/Updater.php b/plugins/CoreUpdater/Updater.php
new file mode 100644
index 0000000000000000000000000000000000000000..1adcbc6cc0669fc1dfd7f9807b9bae3963028ad7
--- /dev/null
+++ b/plugins/CoreUpdater/Updater.php
@@ -0,0 +1,272 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreUpdater;
+
+use Exception;
+use Piwik\ArchiveProcessor\Rules;
+use Piwik\Config;
+use Piwik\Filechecks;
+use Piwik\Filesystem;
+use Piwik\Http;
+use Piwik\Option;
+use Piwik\Plugin\Manager as PluginManager;
+use Piwik\SettingsServer;
+use Piwik\Translation\Translator;
+use Piwik\Unzip;
+use Piwik\Version;
+
+class Updater
+{
+    const OPTION_LATEST_VERSION = 'UpdateCheck_LatestVersion';
+    const PATH_TO_EXTRACT_LATEST_VERSION = '/latest/';
+    const LATEST_VERSION_URL = '://builds.piwik.org/piwik.zip';
+    const LATEST_BETA_VERSION_URL = '://builds.piwik.org/piwik-%s.zip';
+    const DOWNLOAD_TIMEOUT = 120;
+
+    /**
+     * @var Translator
+     */
+    private $translator;
+
+    /**
+     * @var string
+     */
+    private $tmpPath;
+
+    public function __construct(Translator $translator, $tmpPath)
+    {
+        $this->translator = $translator;
+        $this->tmpPath = $tmpPath;
+    }
+
+    /**
+     * Returns the latest available version number. Does not perform a check whether a later version is available.
+     *
+     * @return false|string
+     */
+    public function getLatestVersion()
+    {
+        return Option::get(self::OPTION_LATEST_VERSION);
+    }
+
+    /**
+     * @return bool
+     */
+    public function isNewVersionAvailable()
+    {
+        $latestVersion = self::getLatestVersion();
+        return $latestVersion && version_compare(Version::VERSION, $latestVersion) === -1;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isUpdatingOverHttps()
+    {
+        $openSslEnabled = extension_loaded('openssl');
+        $usingMethodSupportingHttps = (Http::getTransportMethod() !== 'socket');
+
+        return $openSslEnabled && $usingMethodSupportingHttps;
+    }
+
+    /**
+     * Update Piwik codebase by downloading and installing the latest version.
+     *
+     * @param bool $https Whether to use HTTPS if supported of not. If false, will use HTTP.
+     * @return string[] Return an array of messages for the user.
+     * @throws ArchiveDownloadException
+     * @throws UpdaterException
+     * @throws Exception
+     */
+    public function updatePiwik($https = true)
+    {
+        if (!$this->isNewVersionAvailable()) {
+            throw new Exception($this->translator->translate('CoreUpdater_ExceptionAlreadyLatestVersion', Version::VERSION));
+        }
+
+        SettingsServer::setMaxExecutionTime(0);
+
+        $newVersion = $this->getLatestVersion();
+        $url = $this->getArchiveUrl($newVersion, $https);
+        $messages = array();
+
+        try {
+            $archiveFile = $this->downloadArchive($newVersion, $url);
+            $messages[] = $this->translator->translate('CoreUpdater_DownloadingUpdateFromX', $url);
+
+            $extractedArchiveDirectory = $this->decompressArchive($archiveFile);
+            $messages[] = $this->translator->translate('CoreUpdater_UnpackingTheUpdate');
+
+            $this->verifyDecompressedArchive($extractedArchiveDirectory);
+            $messages[] = $this->translator->translate('CoreUpdater_VerifyingUnpackedFiles');
+
+            $disabledPluginNames = $this->disableIncompatiblePlugins($newVersion);
+            if (!empty($disabledPluginNames)) {
+                $messages[] = $this->translator->translate('CoreUpdater_DisablingIncompatiblePlugins', implode(', ', $disabledPluginNames));
+            }
+
+            $this->installNewFiles($extractedArchiveDirectory);
+            $messages[] = $this->translator->translate('CoreUpdater_InstallingTheLatestVersion');
+        } catch (Exception $e) {
+            throw new UpdaterException($e, $messages);
+        }
+
+        return $messages;
+    }
+
+    private function downloadArchive($version, $url)
+    {
+        $path = $this->tmpPath . self::PATH_TO_EXTRACT_LATEST_VERSION;
+        $archiveFile = $path . 'latest.zip';
+
+        Filechecks::dieIfDirectoriesNotWritable(array($path));
+
+        $url .= '?cb=' . $version;
+
+        try {
+            Http::fetchRemoteFile($url, $archiveFile, 0, self::DOWNLOAD_TIMEOUT);
+        } catch (Exception $e) {
+            // We throw a specific exception allowing to offer HTTP download if HTTPS failed
+            throw new ArchiveDownloadException($e);
+        }
+
+        return $archiveFile;
+    }
+
+    private function decompressArchive($archiveFile)
+    {
+        $extractionPath = $this->tmpPath . self::PATH_TO_EXTRACT_LATEST_VERSION;
+
+        $extractedArchiveDirectory = $extractionPath . 'piwik';
+
+        // Remove previous decompressed archive
+        if (file_exists($extractedArchiveDirectory)) {
+            Filesystem::unlinkRecursive($extractedArchiveDirectory, true);
+        }
+
+        $archive = Unzip::factory('PclZip', $archiveFile);
+        $archiveFiles = $archive->extract($extractionPath);
+
+        if (0 == $archiveFiles) {
+            throw new Exception($this->translator->translate('CoreUpdater_ExceptionArchiveIncompatible', $archive->errorInfo()));
+        }
+
+        if (0 == count($archiveFiles)) {
+            throw new Exception($this->translator->translate('CoreUpdater_ExceptionArchiveEmpty'));
+        }
+
+        unlink($archiveFile);
+
+        return $extractedArchiveDirectory;
+    }
+
+    private function verifyDecompressedArchive($extractedArchiveDirectory)
+    {
+        $someExpectedFiles = array(
+            '/config/global.ini.php',
+            '/index.php',
+            '/core/Piwik.php',
+            '/piwik.php',
+            '/plugins/API/API.php'
+        );
+        foreach ($someExpectedFiles as $file) {
+            if (!is_file($extractedArchiveDirectory . $file)) {
+                throw new Exception($this->translator->translate('CoreUpdater_ExceptionArchiveIncomplete', $file));
+            }
+        }
+    }
+
+    private function disableIncompatiblePlugins($version)
+    {
+        $incompatiblePlugins = $this->getIncompatiblePlugins($version);
+        $disabledPluginNames = array();
+
+        foreach ($incompatiblePlugins as $plugin) {
+            $name = $plugin->getPluginName();
+            PluginManager::getInstance()->deactivatePlugin($name);
+            $disabledPluginNames[] = $name;
+        }
+
+        return $disabledPluginNames;
+    }
+
+    private function installNewFiles($extractedArchiveDirectory)
+    {
+        // Make sure the execute bit is set for this shell script
+        if (!Rules::isBrowserTriggerEnabled()) {
+            @chmod($extractedArchiveDirectory . '/misc/cron/archive.sh', 0755);
+        }
+
+        $model = new Model();
+
+        /*
+         * Copy all files to PIWIK_INCLUDE_PATH.
+         * These files are accessed through the dispatcher.
+         */
+        Filesystem::copyRecursive($extractedArchiveDirectory, PIWIK_INCLUDE_PATH);
+        $model->removeGoneFiles($extractedArchiveDirectory, PIWIK_INCLUDE_PATH);
+
+        /*
+         * These files are visible in the web root and are generally
+         * served directly by the web server.  May be shared.
+         */
+        if (PIWIK_INCLUDE_PATH !== PIWIK_DOCUMENT_ROOT) {
+            // Copy PHP files that expect to be in the document root
+            $specialCases = array(
+                '/index.php',
+                '/piwik.php',
+                '/js/index.php',
+            );
+
+            foreach ($specialCases as $file) {
+                Filesystem::copy($extractedArchiveDirectory . $file, PIWIK_DOCUMENT_ROOT . $file);
+            }
+
+            // Copy the non-PHP files (e.g., images, css, javascript)
+            Filesystem::copyRecursive($extractedArchiveDirectory, PIWIK_DOCUMENT_ROOT, true);
+            $model->removeGoneFiles($extractedArchiveDirectory, PIWIK_DOCUMENT_ROOT);
+        }
+
+        // Config files may be user (account) specific
+        if (PIWIK_INCLUDE_PATH !== PIWIK_USER_PATH) {
+            Filesystem::copyRecursive($extractedArchiveDirectory . '/config', PIWIK_USER_PATH . '/config');
+        }
+
+        Filesystem::unlinkRecursive($extractedArchiveDirectory, true);
+
+        Filesystem::clearPhpCaches();
+    }
+
+    /**
+     * @param string $version
+     * @param bool $https Whether to use HTTPS if supported of not. If false, will use HTTP.
+     * @return string
+     */
+    public function getArchiveUrl($version, $https = true)
+    {
+        if (@Config::getInstance()->Debug['allow_upgrades_to_beta']) {
+            $url = sprintf(self::LATEST_BETA_VERSION_URL, $version);
+        } else {
+            $url = self::LATEST_VERSION_URL;
+        }
+
+        if ($this->isUpdatingOverHttps() && $https) {
+            $url = 'https' . $url;
+        } else {
+            $url = 'http' . $url;
+        }
+
+        return $url;
+    }
+
+    private function getIncompatiblePlugins($piwikVersion)
+    {
+        return PluginManager::getInstance()->getIncompatiblePlugins($piwikVersion);
+    }
+}
diff --git a/plugins/CoreUpdater/UpdaterException.php b/plugins/CoreUpdater/UpdaterException.php
new file mode 100644
index 0000000000000000000000000000000000000000..bd4599b111ff0bb61755c548142dfdb693cea004
--- /dev/null
+++ b/plugins/CoreUpdater/UpdaterException.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreUpdater;
+
+use Exception;
+
+/**
+ * Exception during the updating of Piwik to a new version.
+ */
+class UpdaterException extends Exception
+{
+    /**
+     * @var string[]
+     */
+    private $updateLogMessages;
+
+    public function __construct(Exception $exception, array $updateLogMessages)
+    {
+        parent::__construct($exception->getMessage(), 0, $exception);
+
+        $this->updateLogMessages = $updateLogMessages;
+    }
+
+    /**
+     * @return string[]
+     */
+    public function getUpdateLogMessages()
+    {
+        return $this->updateLogMessages;
+    }
+}
diff --git a/plugins/CoreUpdater/config/config.php b/plugins/CoreUpdater/config/config.php
new file mode 100644
index 0000000000000000000000000000000000000000..419e43cc7befa12bfe265e17767609b75923b778
--- /dev/null
+++ b/plugins/CoreUpdater/config/config.php
@@ -0,0 +1,6 @@
+<?php
+
+return array(
+    'Piwik\Plugins\CoreUpdater\Updater' => DI\object()
+        ->constructorParameter('tmpPath', DI\link('path.tmp')),
+);
diff --git a/plugins/CoreUpdater/lang/en.json b/plugins/CoreUpdater/lang/en.json
index 67bc20f27ba5ab12454a6e590d5cbe3fc577f3fb..2c3ffb9be2810563d69f9c32a1f8d2bf0b922c15 100644
--- a/plugins/CoreUpdater/lang/en.json
+++ b/plugins/CoreUpdater/lang/en.json
@@ -47,6 +47,10 @@
         "UpdateAutomatically": "Update Automatically",
         "UpdateHasBeenCancelledExplanation": "Piwik One Click Update has been cancelled. If you can't fix the above error message, it is recommended that you manually update Piwik. %1$s Please check out the %2$sUpdate documentation%3$s to get started!",
         "UpdateTitle": "Update",
+        "UpdateUsingHttpsFailed": "Downloading the latest Piwik version over secure HTTPS connection did not succeed, because of the following error:",
+        "UpdateUsingHttpsFailedHelp": "Please note that downloading the latest Piwik version (over secure HTTPS connection) can fail for various reasons, for example in case of network error, slow network speed, or wrong system configuration.",
+        "UpdateUsingHttpsFailedHelpWhatToDo": "You may continue the update via the non-secure standard HTTP connection by clicking on the button '%s'.",
+        "UpdateUsingHttpMessage": "Update Piwik automatically (over the non secure HTTP connection)",
         "UpgradeComplete": "Upgrade complete!",
         "UpgradePiwik": "Upgrade Piwik",
         "VerifyingUnpackedFiles": "Verifying the unpacked files",
diff --git a/plugins/CoreUpdater/templates/newVersionAvailable.twig b/plugins/CoreUpdater/templates/newVersionAvailable.twig
index cf87a949e639c620aeb580834ae562b73dc06ccf..8691d975952a6822186edcb146dac97b0dc1ba30 100644
--- a/plugins/CoreUpdater/templates/newVersionAvailable.twig
+++ b/plugins/CoreUpdater/templates/newVersionAvailable.twig
@@ -29,7 +29,7 @@
 <form id="oneclickupdate" action="index.php">
     <input type="hidden" name="module" value="CoreUpdater"/>
     <input type="hidden" name="action" value="oneClickUpdate"/>
-    <input type="submit" class="submit" value="{{ 'CoreUpdater_UpdateAutomatically'|translate }}"/>
+    <input id="updateAutomatically" type="submit" class="submit" value="{{ 'CoreUpdater_UpdateAutomatically'|translate }}"/>
     {% endif %}
     <a style="margin-left:50px;" class="submit button"
        href="{{ piwik_latest_version_url }}?cb={{ piwik_new_version }}">{{ 'CoreUpdater_DownloadX'|translate(piwik_new_version) }}</a><br/>
diff --git a/plugins/CoreUpdater/templates/oneClickResults.twig b/plugins/CoreUpdater/templates/oneClickResults.twig
index d08378d6c30e7f0ddd79828d2a2ea5cbaebd5167..8ded5fd6045a2a2fe6c6a721ad884f9bfad3bb93 100644
--- a/plugins/CoreUpdater/templates/oneClickResults.twig
+++ b/plugins/CoreUpdater/templates/oneClickResults.twig
@@ -3,13 +3,37 @@
 {% block content %}
 <br/>
 {% for message in feedbackMessages %}
-<p>&#10003; {{ message }}</p>
+    <p>&#10003; {{ message }}</p>
 {% endfor %}
 
-{% if coreError %}
+{% if httpsFail %}
     <br/>
     <br/>
-    <div class="error"><img src="plugins/Morpheus/images/error_medium.png"/> {{ coreError }}</div>
+    <div class="warning">
+        <img src="plugins/Morpheus/images/warning_medium.png"/>
+        {{ 'CoreUpdater_UpdateUsingHttpsFailed'|translate }}<br/>
+        "{{ error }}"
+    </div>
+    <p>{{ 'CoreUpdater_UpdateUsingHttpsFailedHelp'|translate }}</p>
+    <p>{{ 'CoreUpdater_UpdateUsingHttpsFailedHelpWhatToDo'|translate('CoreUpdater_UpdateAutomatically'|translate) }}</p>
+    <div class="warning">
+        {{ 'CoreUpdater_UpdateUsingHttpMessage'|translate }}
+        <form id="oneclickupdate" action="index.php">
+            <input type="hidden" name="module" value="CoreUpdater"/>
+            <input type="hidden" name="action" value="oneClickUpdate"/>
+            <input type="hidden" name="https" value="0"/>
+            <input id="updateUsingHttp" type="submit" class="submit" value="{{ 'CoreUpdater_UpdateAutomatically'|translate }}"/>
+        </form>
+    </div>
+    <br/>
+    <br/>
+{% elseif error %}
+    <br/>
+    <br/>
+    <div class="error">
+        <img src="plugins/Morpheus/images/error_medium.png"/>
+        {{ error }}
+    </div>
     <br/>
     <br/>
     <div class="warning">
diff --git a/plugins/Goals/templates/_addEditGoal.twig b/plugins/Goals/templates/_addEditGoal.twig
index 94db74df414d920f8acf5082bbf4a15459ce4e0b..dc217c9a66fdc69c561056bbfa513c8c2fdd955a 100644
--- a/plugins/Goals/templates/_addEditGoal.twig
+++ b/plugins/Goals/templates/_addEditGoal.twig
@@ -40,20 +40,21 @@
 		<br />{{ 'General_ForExampleShort'|translate }} {{ 'Goals_IsExactly'|translate("'click'") }} \
 		<br />{{ 'General_ForExampleShort'|translate }} {{ 'Goals_MatchesExpression'|translate("'(.*)_banner'") }}"
     };
-    bindGoalForm();
-
-    {% if onlyShowAddNewGoal is not defined %}
-        piwik.goals = {{ goalsJSON|raw }};
-        bindListGoalEdit();
-
-        {% if idGoal %}
-            editGoal({{ idGoal|e('js') }});
+    $(document).ready(function () {
+        bindGoalForm();
+
+        {% if onlyShowAddNewGoal is not defined %}
+            piwik.goals = {{ goalsJSON|raw }};
+            bindListGoalEdit();
+
+            {% if idGoal %}
+                editGoal({{ idGoal|e('js') }});
+            {% else %}
+                showEditGoals();
+            {% endif %}
         {% else %}
-            showEditGoals();
+            initAndShowAddGoalForm();
         {% endif %}
-    {% else %}
-        initAndShowAddGoalForm();
-    {% endif %}
-
+    });
 
 </script>
diff --git a/plugins/Login/Controller.php b/plugins/Login/Controller.php
index 270b7883800cd195508bd20b1f611dc84563e5c7..b9660a221df9ef959436756a5a1fb4ea932fdbdd 100644
--- a/plugins/Login/Controller.php
+++ b/plugins/Login/Controller.php
@@ -93,6 +93,7 @@ class Controller extends \Piwik\Plugin\Controller
     function login($messageNoAccess = null, $infoMessage = false)
     {
         $form = new FormLogin();
+        $form->removeAttribute('action'); // remove action attribute, otherwise hash part will be lost
         if ($form->validate()) {
             $nonce = $form->getSubmitValue('form_nonce');
             if (Nonce::verifyNonce('Login.login', $nonce)) {
diff --git a/tests/PHPUnit/Framework/Fixture.php b/tests/PHPUnit/Framework/Fixture.php
index 71cce331a82e50b1d75754adf413b731a10b2cf8..9d2fb8a2d7db19c6e61b0d5527c06779583a6e55 100644
--- a/tests/PHPUnit/Framework/Fixture.php
+++ b/tests/PHPUnit/Framework/Fixture.php
@@ -882,4 +882,14 @@ class Fixture extends \PHPUnit_Framework_Assert
 
         return $result;
     }
+
+    /**
+     * Use this method to return custom container configuration that you want to apply for the tests.
+     *
+     * @return array
+     */
+    public function provideContainerConfig()
+    {
+        return array();
+    }
 }
diff --git a/tests/PHPUnit/TestingEnvironment.php b/tests/PHPUnit/TestingEnvironment.php
index 97d98dbff8e6bca04e3244f436cfb09f7b4a683d..3acc202a911b9af8ea672c7569b97df618f387d1 100644
--- a/tests/PHPUnit/TestingEnvironment.php
+++ b/tests/PHPUnit/TestingEnvironment.php
@@ -2,10 +2,12 @@
 
 use Piwik\Common;
 use Piwik\Config;
+use Piwik\Container\StaticContainer;
 use Piwik\Piwik;
 use Piwik\Option;
 use Piwik\Plugin\Manager as PluginManager;
 use Piwik\DbHelper;
+use Piwik\Tests\Framework\Fixture;
 
 require_once PIWIK_INCLUDE_PATH . "/core/Config.php";
 
@@ -148,6 +150,19 @@ class Piwik_TestingEnvironment
             $testingEnvironment->configFileGlobal, $testingEnvironment->configFileLocal, $testingEnvironment->configFileCommon
         ));
 
+        // Apply DI config from the fixture
+        if ($testingEnvironment->fixtureClass) {
+            $fixtureClass = $testingEnvironment->fixtureClass;
+            if (class_exists($fixtureClass)) {
+                /** @var Fixture $fixture */
+                $fixture = new $fixtureClass;
+                $diConfig = $fixture->provideContainerConfig();
+                if (!empty($diConfig)) {
+                    StaticContainer::addDefinitions($diConfig);
+                }
+            }
+        }
+
         \Piwik\Cache\Backend\File::$invalidateOpCacheBeforeRead = true;
 
         Piwik::addAction('Access.createAccessSingleton', function($access) use ($testingEnvironment) {
diff --git a/tests/UI/expected-ui-screenshots b/tests/UI/expected-ui-screenshots
index 560c44710748f4f27ceaee0955f59aa1336ee4a9..8426ae2a418fc251f0408977622b560e33f31f2f 160000
--- a/tests/UI/expected-ui-screenshots
+++ b/tests/UI/expected-ui-screenshots
@@ -1 +1 @@
-Subproject commit 560c44710748f4f27ceaee0955f59aa1336ee4a9
+Subproject commit 8426ae2a418fc251f0408977622b560e33f31f2f
diff --git a/tests/UI/screenshot-diffs/comparator.js b/tests/UI/screenshot-diffs/comparator.js
new file mode 100644
index 0000000000000000000000000000000000000000..75b2e025c134a14e7de90a15205acc4a2b42d737
--- /dev/null
+++ b/tests/UI/screenshot-diffs/comparator.js
@@ -0,0 +1,86 @@
+jQuery(document).ready(function($){
+    //check if the .cd-image-container is in the viewport
+    //if yes, animate it
+    checkPosition($('.cd-image-container'));
+    $(window).on('scroll', function(){
+        checkPosition($('.cd-image-container'));
+    });
+
+    //make the .cd-handle element draggable and modify .cd-resize-img width according to its position
+    $('.cd-image-container').each(function(){
+        var actual = $(this);
+        drags(actual.find('.cd-handle'), actual.find('.cd-resize-img'), actual, actual.find('.cd-image-label[data-type="original"]'), actual.find('.cd-image-label[data-type="modified"]'));
+    });
+
+    //upadate images label visibility
+    $(window).on('resize', function(){
+        $('.cd-image-container').each(function(){
+            var actual = $(this);
+            updateLabel(actual.find('.cd-image-label[data-type="modified"]'), actual.find('.cd-resize-img'), 'left');
+            updateLabel(actual.find('.cd-image-label[data-type="original"]'), actual.find('.cd-resize-img'), 'right');
+        });
+    });
+});
+
+function checkPosition(container) {
+    container.each(function(){
+        var actualContainer = $(this);
+        if( $(window).scrollTop() + $(window).height()*0.5 > actualContainer.offset().top) {
+            actualContainer.addClass('is-visible');
+        }
+    });
+}
+
+//draggable funtionality - credits to http://css-tricks.com/snippets/jquery/draggable-without-jquery-ui/
+function drags(dragElement, resizeElement, container, labelContainer, labelResizeElement) {
+    dragElement.on("mousedown vmousedown", function(e) {
+        dragElement.addClass('draggable');
+        resizeElement.addClass('resizable');
+
+        var dragWidth = dragElement.outerWidth(),
+            xPosition = dragElement.offset().left + dragWidth - e.pageX,
+            containerOffset = container.offset().left,
+            containerWidth = container.outerWidth(),
+            minLeft = containerOffset + 10,
+            maxLeft = containerOffset + containerWidth - dragWidth - 10;
+
+        dragElement.parents().on("mousemove vmousemove", function(e) {
+            leftValue = e.pageX + xPosition - dragWidth;
+
+            //constrain the draggable element to move inside his container
+            if(leftValue < minLeft ) {
+                leftValue = minLeft;
+            } else if ( leftValue > maxLeft) {
+                leftValue = maxLeft;
+            }
+
+            widthValue = (leftValue + dragWidth/2 - containerOffset)*100/containerWidth+'%';
+
+            $('.draggable').css('left', widthValue).on("mouseup vmouseup", function() {
+                $(this).removeClass('draggable');
+                resizeElement.removeClass('resizable');
+            });
+
+            $('.resizable').css('width', widthValue);
+
+            updateLabel(labelResizeElement, resizeElement, 'left');
+            updateLabel(labelContainer, resizeElement, 'right');
+
+        }).on("mouseup vmouseup", function(e){
+            dragElement.removeClass('draggable');
+            resizeElement.removeClass('resizable');
+        });
+        e.preventDefault();
+    }).on("mouseup vmouseup", function(e) {
+        dragElement.removeClass('draggable');
+        resizeElement.removeClass('resizable');
+    });
+}
+
+function updateLabel(label, resizeElement, position) {
+    if(position == 'left') {
+        ( label.offset().left + label.outerWidth() < resizeElement.offset().left + resizeElement.outerWidth() ) ? label.removeClass('is-hidden') : label.addClass('is-hidden') ;
+    } else {
+        ( label.offset().left > resizeElement.offset().left + resizeElement.outerWidth() ) ? label.removeClass('is-hidden') : label.addClass('is-hidden') ;
+    }
+}
diff --git a/tests/UI/screenshot-diffs/diffgenerator.js b/tests/UI/screenshot-diffs/diffgenerator.js
index 78063a667432598b4fd0f8fe08a969bbc65134f2..c2de59f9eb302c7684c015357a33076b685ee3ba 100644
--- a/tests/UI/screenshot-diffs/diffgenerator.js
+++ b/tests/UI/screenshot-diffs/diffgenerator.js
@@ -20,7 +20,7 @@ resemble.outputSettings({
 
 function compareImages(expected, expectedGithub, processed)
 {
-    resemble(processed).compareTo(expected).onComplete(function(data){
+    var resembleControl = resemble(processed).compareTo(expected).onComplete(function(data){
 
         var info = 'Mismatch percentage: ' + data.misMatchPercentage + '%';
 
@@ -35,6 +35,8 @@ function compareImages(expected, expectedGithub, processed)
     $('.processed').attr('src', encodeURI(processed));
     $('.expected').attr('src', encodeURI(expected));
     $('.expectedGithub').attr('src', 'https://raw.githubusercontent.com/piwik/piwik-ui-tests/master/' + encodeURI(expectedGithub));
+
+    return resembleControl;
 }
 
 function getUrlQueryParam(sParam) {
@@ -54,5 +56,12 @@ $(function () {
     var processed = getUrlQueryParam('processed');
     var expected  = getUrlQueryParam('expected');
     var github    = getUrlQueryParam('github');
-    compareImages(expected, github, processed);
+    var resembleControl = compareImages(expected, github, processed);
+
+    $('#toggleAliasing').click(function () {
+        resembleControl.ignoreAntialiasing();
+    });
+
+    $('#original').attr('src', expected);
+    $('#modified').attr('src', processed);
 });
\ No newline at end of file
diff --git a/tests/UI/screenshot-diffs/singlediff.html b/tests/UI/screenshot-diffs/singlediff.html
index a91ffa7a404faa10c6ff9779cc76aeabf9d232a0..b80c764957990796dfcb6bf0b590c638929b4264 100644
--- a/tests/UI/screenshot-diffs/singlediff.html
+++ b/tests/UI/screenshot-diffs/singlediff.html
@@ -1,12 +1,14 @@
 <html>
 <head>
+    <link rel="stylesheet" href="style.css"/>
     <!-- loaded when on build-artifacts -->
     <script src='jquery.js'></script>
     <script src='resemble.js'></script>
     <!-- loaded when viewing within piwik -->
-    <script src='../../../../libs/bower_components/jquery/dist/jquery.min.js'></script>
-    <script src='../../../../tests/lib/resemblejs/resemble.js'></script>
+    <script src='../../../libs/bower_components/jquery/dist/jquery.min.js'></script>
+    <script src='../../../tests/lib/resemblejs/resemble.js'></script>
     <script src='diffgenerator.js'></script>
+    <script src='comparator.js'></script>
 </head>
 <body>
 
@@ -15,6 +17,26 @@
 <br />
 <img class="diff">
 
+<br/><br/>
+
+<div id="controls">
+    <button id="toggleAliasing">Ignore aliasing</button>
+</div>
+
+<br/><br/>
+
+<figure class="cd-image-container">
+    <img id="original">
+    <span class="cd-image-label" data-type="original">Original</span>
+
+    <div class="cd-resize-img"> <!-- the resizable image on top -->
+        <img id="modified">
+        <span class="cd-image-label" data-type="modified">Modified</span>
+    </div>
+
+    <span class="cd-handle"></span>
+</figure>
+
 <h2>Processed</h2>
 <img class="processed">
 
@@ -24,4 +46,5 @@
 <h2>Expected GitHub</h2>
 <img class="expectedGithub">
 
-</body></html>
\ No newline at end of file
+</body>
+</html>
\ No newline at end of file
diff --git a/tests/UI/screenshot-diffs/style.css b/tests/UI/screenshot-diffs/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..367b01f0f876e7eb2bda2ca64384352289b2a584
--- /dev/null
+++ b/tests/UI/screenshot-diffs/style.css
@@ -0,0 +1,156 @@
+img {
+    max-width: 100%;
+    display: block;
+    margin: 0 auto;
+}
+
+#controls {
+    margin: 0 auto;
+    width: 100px;
+}
+
+.cd-image-container {
+    position: relative;
+    width: 90%;
+    max-width: 768px;
+    margin: 0em auto;
+}
+.cd-image-container img {
+    display: block;
+}
+
+.cd-image-label {
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    color: #ffffff;
+    padding: 1em;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    opacity: 0;
+    -webkit-transform: translateY(20px);
+    -moz-transform: translateY(20px);
+    -ms-transform: translateY(20px);
+    -o-transform: translateY(20px);
+    transform: translateY(20px);
+    -webkit-transition: -webkit-transform 0.3s 0.7s, opacity 0.3s 0.7s;
+    -moz-transition: -moz-transform 0.3s 0.7s, opacity 0.3s 0.7s;
+    transition: transform 0.3s 0.7s, opacity 0.3s 0.7s;
+}
+.cd-image-label.is-hidden {
+    visibility: hidden;
+}
+.is-visible .cd-image-label {
+    opacity: 1;
+    -webkit-transform: translateY(0);
+    -moz-transform: translateY(0);
+    -ms-transform: translateY(0);
+    -o-transform: translateY(0);
+    transform: translateY(0);
+}
+
+.cd-resize-img {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 0;
+    height: 100%;
+    overflow: hidden;
+    /* Force Hardware Acceleration in WebKit */
+    -webkit-transform: translateZ(0);
+    -moz-transform: translateZ(0);
+    -ms-transform: translateZ(0);
+    -o-transform: translateZ(0);
+    transform: translateZ(0);
+    -webkit-backface-visibility: hidden;
+    backface-visibility: hidden;
+}
+.cd-resize-img img {
+    position: absolute;
+    left: 0;
+    top: 0;
+    display: block;
+    height: 100%;
+    width: auto;
+    max-width: none;
+}
+.cd-resize-img .cd-image-label {
+    right: auto;
+    left: 0;
+}
+.is-visible .cd-resize-img {
+    width: 50%;
+    /* bounce in animation of the modified image */
+    -webkit-animation: cd-bounce-in 0.7s;
+    -moz-animation: cd-bounce-in 0.7s;
+    animation: cd-bounce-in 0.7s;
+}
+
+@-webkit-keyframes cd-bounce-in {
+    0% {
+        width: 0;
+    }
+    60% {
+        width: 55%;
+    }
+    100% {
+        width: 50%;
+    }
+}
+@-moz-keyframes cd-bounce-in {
+    0% {
+        width: 0;
+    }
+    60% {
+        width: 55%;
+    }
+    100% {
+        width: 50%;
+    }
+}
+@keyframes cd-bounce-in {
+    0% {
+        width: 0;
+    }
+    60% {
+        width: 55%;
+    }
+    100% {
+        width: 50%;
+    }
+}
+.cd-handle {
+    position: absolute;
+    height: 44px;
+    width: 44px;
+    /* center the element */
+    left: 50%;
+    top: 50%;
+    margin-left: -22px;
+    margin-top: -22px;
+    border-radius: 50%;
+    background: #dc717d no-repeat center center;
+    cursor: move;
+    box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.2), 0 0 10px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.3);
+    opacity: 0;
+    -webkit-transform: translate3d(0, 0, 0) scale(0);
+    -moz-transform: translate3d(0, 0, 0) scale(0);
+    -ms-transform: translate3d(0, 0, 0) scale(0);
+    -o-transform: translate3d(0, 0, 0) scale(0);
+    transform: translate3d(0, 0, 0) scale(0);
+}
+.cd-handle.draggable {
+    /* change background color when element is active */
+    background-color: #445b7c;
+}
+.is-visible .cd-handle {
+    opacity: 1;
+    -webkit-transform: translate3d(0, 0, 0) scale(1);
+    -moz-transform: translate3d(0, 0, 0) scale(1);
+    -ms-transform: translate3d(0, 0, 0) scale(1);
+    -o-transform: translate3d(0, 0, 0) scale(1);
+    transform: translate3d(0, 0, 0) scale(1);
+    -webkit-transition: -webkit-transform 0.3s 0.7s, opacity 0s 0.7s;
+    -moz-transition: -moz-transform 0.3s 0.7s, opacity 0s 0.7s;
+    transition: transform 0.3s 0.7s, opacity 0s 0.7s;
+}
diff --git a/tests/UI/specs/CoreUpdaterCode_spec.js b/tests/UI/specs/CoreUpdaterCode_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..807810869b587ff35b27ad25e50efa5a8232b2e3
--- /dev/null
+++ b/tests/UI/specs/CoreUpdaterCode_spec.js
@@ -0,0 +1,34 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * Installation screenshot tests.
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("CoreUpdaterCode", function () {
+    this.timeout(0);
+
+    this.fixture = "Piwik\\Plugins\\CoreUpdater\\Test\\Fixtures\\FailUpdateHttpsFixture";
+
+    var url = "?module=CoreUpdater&action=newVersionAvailable";
+
+    it("should show a new version is available", function (done) {
+        expect.screenshot("newVersion").to.be.capture(function (page) {
+            page.load(url);
+        }, done);
+    });
+
+    it("should offer to update over http when updating over https fails", function (done) {
+        expect.screenshot("httpsUpdateFail").to.be.capture(function (page) {
+            page.click('#updateAutomatically');
+        }, done);
+    });
+
+    it("should show the update steps when updating over http succeeds", function (done) {
+        expect.screenshot("httpUpdateSuccess").to.be.capture(function (page) {
+            page.click('#updateUsingHttp');
+        }, done);
+    });
+});
diff --git a/tests/UI/specs/Updater_spec.js b/tests/UI/specs/CoreUpdaterDb_spec.js
similarity index 88%
rename from tests/UI/specs/Updater_spec.js
rename to tests/UI/specs/CoreUpdaterDb_spec.js
index 0ddc8b67cb734fb71d7e078ed9c7efcfe2cd2e7a..f77776a4ca493709b453ac7153b88d137d450140 100644
--- a/tests/UI/specs/Updater_spec.js
+++ b/tests/UI/specs/CoreUpdaterDb_spec.js
@@ -7,10 +7,10 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-describe("Updater", function () {
+describe("CoreUpdaterDb", function () {
     this.timeout(0);
 
-    this.fixture = "Piwik\\Tests\\Fixtures\\UpdaterTestFixture";
+    this.fixture = "Piwik\\Plugins\\CoreUpdater\\Test\\Fixtures\\DbUpdaterTestFixture";
 
     before(function () {
         testEnvironment.tablesPrefix = 'piwik_';
@@ -34,4 +34,4 @@ describe("Updater", function () {
             page.click('.submit');
         }, done);
     });
-});
\ No newline at end of file
+});
diff --git a/tests/lib/resemblejs/resemble.js b/tests/lib/resemblejs/resemble.js
old mode 100644
new mode 100755
index dbb927a7cdba68cbc67e4763b972fd20cbaaf0a8..bdd375634ad6631278d775f4e2a844b0b990417b
--- a/tests/lib/resemblejs/resemble.js
+++ b/tests/lib/resemblejs/resemble.js
@@ -1,600 +1,659 @@
 /*
- James Cryer / Huddle 2014
- URL: https://github.com/Huddle/Resemble.js
- */
+James Cryer / Huddle 2014
+URL: https://github.com/Huddle/Resemble.js
+*/
 
 (function(_this){
-    'use strict';
-
-    var pixelTransparency = 1;
-
-    var errorPixelColor = { // Color for Error Pixels. Between 0 and 255.
-        red: 255,
-        green: 0,
-        blue: 255,
-        alpha: 255
-    };
-
-    var errorPixelTransform = {
-        flat : function (d1, d2){
-            return {
-                r: errorPixelColor.red,
-                g: errorPixelColor.green,
-                b: errorPixelColor.blue,
-                a: errorPixelColor.alpha
-            }
-        },
-        movement: function (d1, d2){
-            return {
-                r: ((d2.r*(errorPixelColor.red/255)) + errorPixelColor.red)/2,
-                g: ((d2.g*(errorPixelColor.green/255)) + errorPixelColor.green)/2,
-                b: ((d2.b*(errorPixelColor.blue/255)) + errorPixelColor.blue)/2,
-                a: d2.a
-            }
-        }
-    };
-
-    var errorPixelTransformer = errorPixelTransform.flat;
-
-    _this['resemble'] = function( fileData ){
-
-        var data = {};
-        var images = [];
-        var updateCallbackArray = [];
-
-        var tolerance = { // between 0 and 255
-            red: 16,
-            green: 16,
-            blue: 16,
-            alpha: 16,
-            minBrightness: 16,
-            maxBrightness: 240
-        };
-
-        var ignoreAntialiasing = false;
-        var ignoreColors = false;
-
-        function triggerDataUpdate(){
-            var len = updateCallbackArray.length;
-            var i;
-            for(i=0;i<len;i++){
-                if (typeof updateCallbackArray[i] === 'function'){
-                    updateCallbackArray[i](data);
-                }
-            }
-        }
-
-        function loop(x, y, callback){
-            var i,j;
-
-            for (i=0;i<x;i++){
-                for (j=0;j<y;j++){
-                    callback(i, j);
-                }
-            }
-        }
-
-        function parseImage(sourceImageData, width, height){
-
-            var pixleCount = 0;
-            var redTotal = 0;
-            var greenTotal = 0;
-            var blueTotal = 0;
-            var brightnessTotal = 0;
-
-            loop(height, width, function(verticalPos, horizontalPos){
-                var offset = (verticalPos*width + horizontalPos) * 4;
-                var red = sourceImageData[offset];
-                var green = sourceImageData[offset + 1];
-                var blue = sourceImageData[offset + 2];
-                var brightness = getBrightness(red,green,blue);
-
-                pixleCount++;
-
-                redTotal += red / 255 * 100;
-                greenTotal += green / 255 * 100;
-                blueTotal += blue / 255 * 100;
-                brightnessTotal += brightness / 255 * 100;
-            });
-
-            data.red = Math.floor(redTotal / pixleCount);
-            data.green = Math.floor(greenTotal / pixleCount);
-            data.blue = Math.floor(blueTotal / pixleCount);
-            data.brightness = Math.floor(brightnessTotal / pixleCount);
-
-            triggerDataUpdate();
-        }
-
-        function loadImageData( fileData, callback ){
-            var fileReader;
-            var hiddenImage = new Image();
-
-            hiddenImage.onload = function() {
-
-                var hiddenCanvas =  document.createElement('canvas');
-                var imageData;
-                var width = hiddenImage.width;
-                var height = hiddenImage.height;
-
-                hiddenCanvas.width = width;
-                hiddenCanvas.height = height;
-                hiddenCanvas.getContext('2d').drawImage(hiddenImage, 0, 0, width, height);
-                imageData = hiddenCanvas.getContext('2d').getImageData(0, 0, width, height);
-
-                images.push(imageData);
-
-                callback(imageData, width, height);
-            };
-
-            if (typeof fileData === 'string') {
-                hiddenImage.src = fileData;
-            } else {
-                fileReader = new FileReader();
-                fileReader.onload = function (event) {
-                    hiddenImage.src = event.target.result;
-                };
-                fileReader.readAsDataURL(fileData);
-            }
-        }
-
-        function isColorSimilar(a, b, color){
-
-            var absDiff = Math.abs(a - b);
-
-            if(typeof a === 'undefined'){
-                return false;
-            }
-            if(typeof b === 'undefined'){
-                return false;
-            }
-
-            if(a === b){
-                return true;
-            } else if ( absDiff < tolerance[color] ) {
-                return true;
-            } else {
-                return false;
-            }
-        }
-
-        function isNumber(n) {
-            return !isNaN(parseFloat(n));
-        }
-
-        function isPixelBrightnessSimilar(d1, d2){
-            var alpha = isColorSimilar(d1.a, d2.a, 'alpha');
-            var brightness = isColorSimilar(d1.brightness, d2.brightness, 'minBrightness');
-            return brightness && alpha;
-        }
-
-        function getBrightness(r,g,b){
-            return 0.3*r + 0.59*g + 0.11*b;
-        }
-
-        function isRGBSame(d1,d2){
-            var red = d1.r === d2.r;
-            var green = d1.g === d2.g;
-            var blue = d1.b === d2.b;
-            return red && green && blue;
-        }
-
-        function isRGBSimilar(d1, d2){
-            var red = isColorSimilar(d1.r,d2.r,'red');
-            var green = isColorSimilar(d1.g,d2.g,'green');
-            var blue = isColorSimilar(d1.b,d2.b,'blue');
-            var alpha = isColorSimilar(d1.a, d2.a, 'alpha');
-
-            return red && green && blue && alpha;
-        }
-
-        function isContrasting(d1, d2){
-            return Math.abs(d1.brightness - d2.brightness) > tolerance.maxBrightness;
-        }
-
-        function getHue(r,g,b){
-
-            r = r / 255;
-            g = g / 255;
-            b = b / 255;
-            var max = Math.max(r, g, b), min = Math.min(r, g, b);
-            var h;
-            var d;
-
-            if (max == min){
-                h = 0; // achromatic
-            } else{
-                d = max - min;
-                switch(max){
-                    case r: h = (g - b) / d + (g < b ? 6 : 0); break;
-                    case g: h = (b - r) / d + 2; break;
-                    case b: h = (r - g) / d + 4; break;
-                }
-                h /= 6;
-            }
-
-            return h;
-        }
-
-        function isAntialiased(sourcePix, data, cacheSet, verticalPos, horizontalPos, width){
-            var offset;
-            var targetPix;
-            var distance = 1;
-            var i;
-            var j;
-            var hasHighContrastSibling = 0;
-            var hasSiblingWithDifferentHue = 0;
-            var hasEquivilantSibling = 0;
-
-            addHueInfo(sourcePix);
-
-            for (i = distance*-1; i <= distance; i++){
-                for (j = distance*-1; j <= distance; j++){
-
-                    if(i===0 && j===0){
-                        // ignore source pixel
-                    } else {
-
-                        offset = ((verticalPos+j)*width + (horizontalPos+i)) * 4;
-                        targetPix = getPixelInfo(data, offset, cacheSet);
-
-                        if(targetPix === null){
-                            continue;
-                        }
-
-                        addBrightnessInfo(targetPix);
-                        addHueInfo(targetPix);
-
-                        if( isContrasting(sourcePix, targetPix) ){
-                            hasHighContrastSibling++;
-                        }
-
-                        if( isRGBSame(sourcePix,targetPix) ){
-                            hasEquivilantSibling++;
-                        }
-
-                        if( Math.abs(targetPix.h - sourcePix.h) > 0.3 ){
-                            hasSiblingWithDifferentHue++;
-                        }
-
-                        if( hasSiblingWithDifferentHue > 1 || hasHighContrastSibling > 1){
-                            return true;
-                        }
-                    }
-                }
-            }
-
-            if(hasEquivilantSibling < 2){
-                return true;
-            }
-
-            return false;
-        }
-
-        function errorPixel(px, offset, data1, data2){
-            var data = errorPixelTransformer(data1, data2);
-            px[offset] = data.r;
-            px[offset + 1] = data.g;
-            px[offset + 2] = data.b;
-            px[offset + 3] = data.a;
-        }
-
-        function copyPixel(px, offset, data){
-            px[offset] = data.r; //r
-            px[offset + 1] = data.g; //g
-            px[offset + 2] = data.b; //b
-            px[offset + 3] = data.a * pixelTransparency; //a
-        }
-
-        function copyGrayScalePixel(px, offset, data){
-            px[offset] = data.brightness; //r
-            px[offset + 1] = data.brightness; //g
-            px[offset + 2] = data.brightness; //b
-            px[offset + 3] = data.a * pixelTransparency; //a
-        }
-
-        function getPixelInfo(data, offset, cacheSet){
-            var r;
-            var g;
-            var b;
-            var d;
-            var a;
-
-            r = data[offset];
-
-            if(typeof r !== 'undefined'){
-                g = data[offset+1];
-                b = data[offset+2];
-                a = data[offset+3];
-                d = {
-                    r: r,
-                    g: g,
-                    b: b,
-                    a: a
-                };
-
-                return d;
-            } else {
-                return null;
-            }
-        }
-
-        function addBrightnessInfo(data){
-            data.brightness = getBrightness(data.r,data.g,data.b); // 'corrected' lightness
-        }
-
-        function addHueInfo(data){
-            data.h = getHue(data.r,data.g,data.b);
-        }
-
-        function analyseImages(img1, img2, width, height){
-
-            var hiddenCanvas = document.createElement('canvas');
-
-            var data1 = img1.data;
-            var data2 = img2.data;
-
-            hiddenCanvas.width = width;
-            hiddenCanvas.height = height;
-
-            var context = hiddenCanvas.getContext('2d');
-            var imgd = context.createImageData(width,height);
-            var targetPix = imgd.data;
-
-            var mismatchCount = 0;
-
-            var time = Date.now();
-
-            var skip;
-
-            if( (width > 1200 || height > 1200) && ignoreAntialiasing){
-                skip = 6;
-            }
-
-            loop(height, width, function(verticalPos, horizontalPos){
-
-                if(skip){ // only skip if the image isn't small
-                    if(verticalPos % skip === 0 || horizontalPos % skip === 0){
-                        return;
-                    }
-                }
-
-                var offset = (verticalPos*width + horizontalPos) * 4;
-                var pixel1 = getPixelInfo(data1, offset, 1);
-                var pixel2 = getPixelInfo(data2, offset, 2);
-
-                if(pixel1 === null || pixel2 === null){
-                    return;
-                }
-
-                if (ignoreColors){
-
-                    addBrightnessInfo(pixel1);
-                    addBrightnessInfo(pixel2);
-
-                    if( isPixelBrightnessSimilar(pixel1, pixel2) ){
-                        copyGrayScalePixel(targetPix, offset, pixel2);
-                    } else {
-                        errorPixel(targetPix, offset, pixel1, pixel2);
-                        mismatchCount++;
-                    }
-                    return;
-                }
-
-                if( isRGBSimilar(pixel1, pixel2) ){
-                    copyPixel(targetPix, offset, pixel1, pixel2);
-
-                } else if( ignoreAntialiasing && (
-                    addBrightnessInfo(pixel1), // jit pixel info augmentation looks a little weird, sorry.
-                        addBrightnessInfo(pixel2),
-                        isAntialiased(pixel1, data1, 1, verticalPos, horizontalPos, width) ||
-                            isAntialiased(pixel2, data2, 2, verticalPos, horizontalPos, width)
-                    )){
+	'use strict';
+
+	var pixelTransparency = 1;
+
+	var errorPixelColor = { // Color for Error Pixels. Between 0 and 255.
+		red: 255,
+		green: 0,
+		blue: 255,
+		alpha: 255
+	};
+
+	function colorsDistance(c1, c2){
+		return (Math.abs(c1.r - c2.r) + Math.abs(c1.g - c2.g) + Math.abs(c1.b - c2.b))/3;
+	}
+
+	var errorPixelTransform = {
+		flat : function (d1, d2){
+			return {
+				r: errorPixelColor.red,
+				g: errorPixelColor.green,
+				b: errorPixelColor.blue,
+				a: errorPixelColor.alpha
+			}
+		},
+		movement: function (d1, d2){
+			return {
+				r: ((d2.r*(errorPixelColor.red/255)) + errorPixelColor.red)/2,
+				g: ((d2.g*(errorPixelColor.green/255)) + errorPixelColor.green)/2,
+				b: ((d2.b*(errorPixelColor.blue/255)) + errorPixelColor.blue)/2,
+				a: d2.a
+			}
+		},
+		flatDifferenceIntensity: function (d1, d2){
+			return {
+				r: errorPixelColor.red,
+				g: errorPixelColor.green,
+				b: errorPixelColor.blue,
+				a: colorsDistance(d1, d2)
+			}
+		},
+		movementDifferenceIntensity: function (d1, d2){
+			var ratio = colorsDistance(d1, d2)/255 * 0.8;
+			return {
+				r: ((1-ratio)*(d2.r*(errorPixelColor.red/255)) + ratio*errorPixelColor.red),
+				g: ((1-ratio)*(d2.g*(errorPixelColor.green/255)) + ratio*errorPixelColor.green),
+				b: ((1-ratio)*(d2.b*(errorPixelColor.blue/255)) + ratio*errorPixelColor.blue),
+				a: d2.a
+			}
+		}
+	};
+
+	var errorPixelTransformer = errorPixelTransform.flat;
+
+	var largeImageThreshold = 1200;
+	
+	var httpRegex = /^https?:\/\//;
+	var document = typeof window != "undefined" ? window.document : {};
+	var documentDomainRegex = new RegExp('^https?://' + document.domain);
+
+	_this['resemble'] = function( fileData ){
+
+		var data = {};
+		var images = [];
+		var updateCallbackArray = [];
+
+		var tolerance = { // between 0 and 255
+			red: 16,
+			green: 16,
+			blue: 16,
+			alpha: 16,
+			minBrightness: 16,
+			maxBrightness: 240
+		};
+
+		var ignoreAntialiasing = false;
+		var ignoreColors = false;
+
+		function triggerDataUpdate(){
+			var len = updateCallbackArray.length;
+			var i;
+			for(i=0;i<len;i++){
+				if (typeof updateCallbackArray[i] === 'function'){
+					updateCallbackArray[i](data);
+				}
+			}
+		}
+
+		function loop(x, y, callback){
+			var i,j;
+
+			for (i=0;i<x;i++){
+				for (j=0;j<y;j++){
+					callback(i, j);
+				}
+			}
+		}
+
+		function parseImage(sourceImageData, width, height){
+
+			var pixelCount = 0;
+			var redTotal = 0;
+			var greenTotal = 0;
+			var blueTotal = 0;
+			var brightnessTotal = 0;
+
+			loop(height, width, function(verticalPos, horizontalPos){
+				var offset = (verticalPos*width + horizontalPos) * 4;
+				var red = sourceImageData[offset];
+				var green = sourceImageData[offset + 1];
+				var blue = sourceImageData[offset + 2];
+				var brightness = getBrightness(red,green,blue);
+
+				pixelCount++;
+
+				redTotal += red / 255 * 100;
+				greenTotal += green / 255 * 100;
+				blueTotal += blue / 255 * 100;
+				brightnessTotal += brightness / 255 * 100;
+			});
+
+			data.red = Math.floor(redTotal / pixelCount);
+			data.green = Math.floor(greenTotal / pixelCount);
+			data.blue = Math.floor(blueTotal / pixelCount);
+			data.brightness = Math.floor(brightnessTotal / pixelCount);
+
+			triggerDataUpdate();
+		}
+
+		function loadImageData( fileData, callback ){
+			var fileReader;
+			var hiddenImage = new Image();
+			
+			if (httpRegex.test(fileData) && !documentDomainRegex.test(fileData)) {
+				hiddenImage.setAttribute('crossorigin', 'anonymous');
+			}
+
+			hiddenImage.onload = function() {
+
+				var hiddenCanvas =  document.createElement('canvas');
+				var imageData;
+				var width = hiddenImage.width;
+				var height = hiddenImage.height;
+
+				hiddenCanvas.width = width;
+				hiddenCanvas.height = height;
+				hiddenCanvas.getContext('2d').drawImage(hiddenImage, 0, 0, width, height);
+				imageData = hiddenCanvas.getContext('2d').getImageData(0, 0, width, height);
+
+				images.push(imageData);
+
+				callback(imageData, width, height);
+			};
+
+			if (typeof fileData === 'string') {
+				hiddenImage.src = fileData;
+				if (hiddenImage.complete) {
+					hiddenImage.onload();
+				}
+			} else if (typeof fileData.data !== 'undefined'
+					&& typeof fileData.width === 'number'
+					&& typeof fileData.height === 'number') {
+				images.push(fileData);
+				callback(fileData, fileData.width, fileData.height);
+			} else {
+				fileReader = new FileReader();
+				fileReader.onload = function (event) {
+					hiddenImage.src = event.target.result;
+				};
+				fileReader.readAsDataURL(fileData);
+			}
+		}
+
+		function isColorSimilar(a, b, color){
+
+			var absDiff = Math.abs(a - b);
+
+			if(typeof a === 'undefined'){
+				return false;
+			}
+			if(typeof b === 'undefined'){
+				return false;
+			}
+
+			if(a === b){
+				return true;
+			} else if ( absDiff < tolerance[color] ) {
+				return true;
+			} else {
+				return false;
+			}
+		}
+
+		function isNumber(n) {
+			return !isNaN(parseFloat(n));
+		}
+
+		function isPixelBrightnessSimilar(d1, d2){
+			var alpha = isColorSimilar(d1.a, d2.a, 'alpha');
+			var brightness = isColorSimilar(d1.brightness, d2.brightness, 'minBrightness');
+			return brightness && alpha;
+		}
+
+		function getBrightness(r,g,b){
+			return 0.3*r + 0.59*g + 0.11*b;
+		}
+
+		function isRGBSame(d1,d2){
+			var red = d1.r === d2.r;
+			var green = d1.g === d2.g;
+			var blue = d1.b === d2.b;
+			return red && green && blue;
+		}
+
+		function isRGBSimilar(d1, d2){
+			var red = isColorSimilar(d1.r,d2.r,'red');
+			var green = isColorSimilar(d1.g,d2.g,'green');
+			var blue = isColorSimilar(d1.b,d2.b,'blue');
+			var alpha = isColorSimilar(d1.a, d2.a, 'alpha');
+
+			return red && green && blue && alpha;
+		}
+
+		function isContrasting(d1, d2){
+			return Math.abs(d1.brightness - d2.brightness) > tolerance.maxBrightness;
+		}
+
+		function getHue(r,g,b){
+
+			r = r / 255;
+			g = g / 255;
+			b = b / 255;
+			var max = Math.max(r, g, b), min = Math.min(r, g, b);
+			var h;
+			var d;
+
+			if (max == min){
+				h = 0; // achromatic
+			} else{
+				d = max - min;
+				switch(max){
+					case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+					case g: h = (b - r) / d + 2; break;
+					case b: h = (r - g) / d + 4; break;
+				}
+				h /= 6;
+			}
+
+			return h;
+		}
+
+		function isAntialiased(sourcePix, data, cacheSet, verticalPos, horizontalPos, width){
+			var offset;
+			var targetPix;
+			var distance = 1;
+			var i;
+			var j;
+			var hasHighContrastSibling = 0;
+			var hasSiblingWithDifferentHue = 0;
+			var hasEquivilantSibling = 0;
+
+			addHueInfo(sourcePix);
+
+			for (i = distance*-1; i <= distance; i++){
+				for (j = distance*-1; j <= distance; j++){
+
+					if(i===0 && j===0){
+						// ignore source pixel
+					} else {
+
+						offset = ((verticalPos+j)*width + (horizontalPos+i)) * 4;
+						targetPix = getPixelInfo(data, offset, cacheSet);
+
+						if(targetPix === null){
+							continue;
+						}
+
+						addBrightnessInfo(targetPix);
+						addHueInfo(targetPix);
+
+						if( isContrasting(sourcePix, targetPix) ){
+							hasHighContrastSibling++;
+						}
+
+						if( isRGBSame(sourcePix,targetPix) ){
+							hasEquivilantSibling++;
+						}
+
+						if( Math.abs(targetPix.h - sourcePix.h) > 0.3 ){
+							hasSiblingWithDifferentHue++;
+						}
+
+						if( hasSiblingWithDifferentHue > 1 || hasHighContrastSibling > 1){
+							return true;
+						}
+					}
+				}
+			}
+
+			if(hasEquivilantSibling < 2){
+				return true;
+			}
+
+			return false;
+		}
+
+		function errorPixel(px, offset, data1, data2){
+			var data = errorPixelTransformer(data1, data2);
+			px[offset] = data.r;
+			px[offset + 1] = data.g;
+			px[offset + 2] = data.b;
+			px[offset + 3] = data.a;
+		}
+
+		function copyPixel(px, offset, data){
+			px[offset] = data.r; //r
+			px[offset + 1] = data.g; //g
+			px[offset + 2] = data.b; //b
+			px[offset + 3] = data.a * pixelTransparency; //a
+		}
+
+		function copyGrayScalePixel(px, offset, data){
+			px[offset] = data.brightness; //r
+			px[offset + 1] = data.brightness; //g
+			px[offset + 2] = data.brightness; //b
+			px[offset + 3] = data.a * pixelTransparency; //a
+		}
+
+		function getPixelInfo(data, offset, cacheSet){
+			var r;
+			var g;
+			var b;
+			var d;
+			var a;
+
+			r = data[offset];
+
+			if(typeof r !== 'undefined'){
+				g = data[offset+1];
+				b = data[offset+2];
+				a = data[offset+3];
+				d = {
+					r: r,
+					g: g,
+					b: b,
+					a: a
+				};
+
+				return d;
+			} else {
+				return null;
+			}
+		}
+
+		function addBrightnessInfo(data){
+			data.brightness = getBrightness(data.r,data.g,data.b); // 'corrected' lightness
+		}
+
+		function addHueInfo(data){
+			data.h = getHue(data.r,data.g,data.b);
+		}
+
+		function analyseImages(img1, img2, width, height){
+
+			var hiddenCanvas = document.createElement('canvas');
+
+			var data1 = img1.data;
+			var data2 = img2.data;
+
+			hiddenCanvas.width = width;
+			hiddenCanvas.height = height;
+
+			var context = hiddenCanvas.getContext('2d');
+			var imgd = context.createImageData(width,height);
+			var targetPix = imgd.data;
+
+			var mismatchCount = 0;
+			var diffBounds = {
+				top: height,
+				left: width,
+				bottom: 0,
+				right: 0
+			};
+			var updateBounds = function(x, y) {
+				diffBounds.left = Math.min(x, diffBounds.left);
+				diffBounds.right = Math.max(x, diffBounds.right);
+				diffBounds.top = Math.min(y, diffBounds.top);
+				diffBounds.bottom = Math.max(y, diffBounds.bottom);
+			}
+
+			var time = Date.now();
+
+			var skip;
+
+			if(!!largeImageThreshold && ignoreAntialiasing && (width > largeImageThreshold || height > largeImageThreshold)){
+				skip = 6;
+			}
+
+			loop(height, width, function(verticalPos, horizontalPos){
+
+				if(skip){ // only skip if the image isn't small
+					if(verticalPos % skip === 0 || horizontalPos % skip === 0){
+						return;
+					}
+				}
+
+				var offset = (verticalPos*width + horizontalPos) * 4;
+				var pixel1 = getPixelInfo(data1, offset, 1);
+				var pixel2 = getPixelInfo(data2, offset, 2);
+
+				if(pixel1 === null || pixel2 === null){
+					return;
+				}
+
+				if (ignoreColors){
+
+					addBrightnessInfo(pixel1);
+					addBrightnessInfo(pixel2);
+
+					if( isPixelBrightnessSimilar(pixel1, pixel2) ){
+						copyGrayScalePixel(targetPix, offset, pixel2);
+					} else {
+						errorPixel(targetPix, offset, pixel1, pixel2);
+						mismatchCount++;
+						updateBounds(horizontalPos, verticalPos);
+					}
+					return;
+				}
+
+				if( isRGBSimilar(pixel1, pixel2) ){
+					copyPixel(targetPix, offset, pixel1, pixel2);
+
+				} else if( ignoreAntialiasing && (
+						addBrightnessInfo(pixel1), // jit pixel info augmentation looks a little weird, sorry.
+						addBrightnessInfo(pixel2),
+						isAntialiased(pixel1, data1, 1, verticalPos, horizontalPos, width) ||
+						isAntialiased(pixel2, data2, 2, verticalPos, horizontalPos, width)
+					)){
+
+					if( isPixelBrightnessSimilar(pixel1, pixel2) ){
+						copyGrayScalePixel(targetPix, offset, pixel2);
+					} else {
+						errorPixel(targetPix, offset, pixel1, pixel2);
+						mismatchCount++;
+						updateBounds(horizontalPos, verticalPos);
+					}
+				} else {
+					errorPixel(targetPix, offset, pixel1, pixel2);
+					mismatchCount++;
+					updateBounds(horizontalPos, verticalPos);
+				}
+
+			});
+
+			data.misMatchPercentage = (mismatchCount / (height*width) * 100).toFixed(2);
+			data.diffBounds = diffBounds;
+			data.analysisTime = Date.now() - time;
+
+			data.getImageDataUrl = function(text){
+				var barHeight = 0;
+
+				if(text){
+					barHeight = addLabel(text,context,hiddenCanvas);
+				}
+
+				context.putImageData(imgd, 0, barHeight);
+
+				return hiddenCanvas.toDataURL("image/png");
+			};
+		}
+
+		function addLabel(text, context, hiddenCanvas){
+			var textPadding = 2;
+
+			context.font = '12px sans-serif';
+
+			var textWidth = context.measureText(text).width + textPadding*2;
+			var barHeight = 22;
+
+			if(textWidth > hiddenCanvas.width){
+				hiddenCanvas.width = textWidth;
+			}
+
+			hiddenCanvas.height += barHeight;
+
+			context.fillStyle = "#666";
+			context.fillRect(0,0,hiddenCanvas.width,barHeight -4);
+			context.fillStyle = "#fff";
+			context.fillRect(0,barHeight -4,hiddenCanvas.width, 4);
+
+			context.fillStyle = "#fff";
+			context.textBaseline = "top";
+			context.font = '12px sans-serif';
+			context.fillText(text, textPadding, 1);
+
+			return barHeight;
+		}
+
+		function normalise(img, w, h){
+			var c;
+			var context;
+
+			if(img.height < h || img.width < w){
+				c = document.createElement('canvas');
+				c.width = w;
+				c.height = h;
+				context = c.getContext('2d');
+				context.putImageData(img, 0, 0);
+				return context.getImageData(0, 0, w, h);
+			}
+
+			return img;
+		}
+
+		function compare(one, two){
+
+			function onceWeHaveBoth(){
+				var width;
+				var height;
+				if(images.length === 2){
+					width = images[0].width > images[1].width ? images[0].width : images[1].width;
+					height = images[0].height > images[1].height ? images[0].height : images[1].height;
+
+					if( (images[0].width === images[1].width) && (images[0].height === images[1].height) ){
+						data.isSameDimensions = true;
+					} else {
+						data.isSameDimensions = false;
+					}
+
+					data.dimensionDifference = { width: images[0].width - images[1].width, height: images[0].height - images[1].height };
+
+					analyseImages( normalise(images[0],width, height), normalise(images[1],width, height), width, height);
+
+					triggerDataUpdate();
+				}
+			}
+
+			images = [];
+			loadImageData(one, onceWeHaveBoth);
+			loadImageData(two, onceWeHaveBoth);
+		}
+
+		function getCompareApi(param){
+
+			var secondFileData,
+				hasMethod = typeof param === 'function';
+
+			if( !hasMethod ){
+				// assume it's file data
+				secondFileData = param;
+			}
 
-                    if( isPixelBrightnessSimilar(pixel1, pixel2) ){
-                        copyGrayScalePixel(targetPix, offset, pixel2);
-                    } else {
-                        errorPixel(targetPix, offset, pixel1, pixel2);
-                        mismatchCount++;
-                    }
-                } else {
-                    errorPixel(targetPix, offset, pixel1, pixel2);
-                    mismatchCount++;
-                }
+			var self = {
+				ignoreNothing: function(){
+
+					tolerance.red = 16;
+					tolerance.green = 16;
+					tolerance.blue = 16;
+					tolerance.alpha = 16;
+					tolerance.minBrightness = 16;
+					tolerance.maxBrightness = 240;
+
+					ignoreAntialiasing = false;
+					ignoreColors = false;
 
-            });
+					if(hasMethod) { param(); }
+					return self;
+				},
+				ignoreAntialiasing: function(){
+
+					tolerance.red = 32;
+					tolerance.green = 32;
+					tolerance.blue = 32;
+					tolerance.alpha = 32;
+					tolerance.minBrightness = 64;
+					tolerance.maxBrightness = 96;
+
+					ignoreAntialiasing = true;
+					ignoreColors = false;
+
+					if(hasMethod) { param(); }
+					return self;
+				},
+				ignoreColors: function(){
+
+					tolerance.alpha = 16;
+					tolerance.minBrightness = 16;
+					tolerance.maxBrightness = 240;
+
+					ignoreAntialiasing = false;
+					ignoreColors = true;
+
+					if(hasMethod) { param(); }
+					return self;
+				},
+				repaint: function(){
+					if(hasMethod) { param(); }
+					return self;
+				},
+				onComplete: function( callback ){
+
+					updateCallbackArray.push(callback);
+
+					var wrapper = function(){
+						compare(fileData, secondFileData);
+					};
+
+					wrapper();
+
+					return getCompareApi(wrapper);
+				}
+			};
+
+			return self;
+		}
+
+		return {
+			onComplete: function( callback ){
+				updateCallbackArray.push(callback);
+				loadImageData(fileData, function(imageData, width, height){
+					parseImage(imageData.data, width, height);
+				});
+			},
+			compareTo: function(secondFileData){
+				return getCompareApi(secondFileData);
+			}
+		};
 
-            data.misMatchPercentage = (mismatchCount / (height*width) * 100).toFixed(2);
-            data.analysisTime = Date.now() - time;
-
-            data.getImageDataUrl = function(text){
-                var barHeight = 0;
-
-                if(text){
-                    barHeight = addLabel(text,context,hiddenCanvas);
-                }
-
-                context.putImageData(imgd, 0, barHeight);
-
-                return hiddenCanvas.toDataURL("image/png");
-            };
-        }
-
-        function addLabel(text, context, hiddenCanvas){
-            var textPadding = 2;
-
-            context.font = '12px sans-serif';
-
-            var textWidth = context.measureText(text).width + textPadding*2;
-            var barHeight = 22;
-
-            if(textWidth > hiddenCanvas.width){
-                hiddenCanvas.width = textWidth;
-            }
-
-            hiddenCanvas.height += barHeight;
-
-            context.fillStyle = "#666";
-            context.fillRect(0,0,hiddenCanvas.width,barHeight -4);
-            context.fillStyle = "#fff";
-            context.fillRect(0,barHeight -4,hiddenCanvas.width, 4);
-
-            context.fillStyle = "#fff";
-            context.textBaseline = "top";
-            context.font = '12px sans-serif';
-            context.fillText(text, textPadding, 1);
-
-            return barHeight;
-        }
-
-        function normalise(img, w, h){
-            var c;
-            var context;
-
-            if(img.height < h || img.width < w){
-                c = document.createElement('canvas');
-                c.width = w;
-                c.height = h;
-                context = c.getContext('2d');
-                context.putImageData(img, 0, 0);
-                return context.getImageData(0, 0, w, h);
-            }
-
-            return img;
-        }
-
-        function compare(one, two){
-
-            function onceWeHaveBoth(){
-                var width;
-                var height;
-                if(images.length === 2){
-                    width = images[0].width > images[1].width ? images[0].width : images[1].width;
-                    height = images[0].height > images[1].height ? images[0].height : images[1].height;
-
-                    if( (images[0].width === images[1].width) && (images[0].height === images[1].height) ){
-                        data.isSameDimensions = true;
-                    } else {
-                        data.isSameDimensions = false;
-                    }
-
-                    data.dimensionDifference = { width: images[0].width - images[1].width, height: images[0].height - images[1].height };
-
-                    analyseImages( normalise(images[0],width, height), normalise(images[1],width, height), width, height);
-
-                    triggerDataUpdate();
-                }
-            }
-
-            images = [];
-            loadImageData(one, onceWeHaveBoth);
-            loadImageData(two, onceWeHaveBoth);
-        }
-
-        function getCompareApi(param){
-
-            var secondFileData,
-                hasMethod = typeof param === 'function';
-
-            if( !hasMethod ){
-                // assume it's file data
-                secondFileData = param;
-            }
-
-            var self = {
-                ignoreNothing: function(){
-
-                    tolerance.red = 16;
-                    tolerance.green = 16;
-                    tolerance.blue = 16;
-                    tolerance.alpha = 16;
-                    tolerance.minBrightness = 16;
-                    tolerance.maxBrightness = 240;
-
-                    ignoreAntialiasing = false;
-                    ignoreColors = false;
-
-                    if(hasMethod) { param(); }
-                    return self;
-                },
-                ignoreAntialiasing: function(){
-
-                    tolerance.red = 32;
-                    tolerance.green = 32;
-                    tolerance.blue = 32;
-                    tolerance.alpha = 32;
-                    tolerance.minBrightness = 64;
-                    tolerance.maxBrightness = 96;
-
-                    ignoreAntialiasing = true;
-                    ignoreColors = false;
-
-                    if(hasMethod) { param(); }
-                    return self;
-                },
-                ignoreColors: function(){
-
-                    tolerance.alpha = 16;
-                    tolerance.minBrightness = 16;
-                    tolerance.maxBrightness = 240;
-
-                    ignoreAntialiasing = false;
-                    ignoreColors = true;
-
-                    if(hasMethod) { param(); }
-                    return self;
-                },
-                repaint: function(){
-                    if(hasMethod) { param(); }
-                    return self;
-                },
-                onComplete: function( callback ){
-
-                    updateCallbackArray.push(callback);
-
-                    var wrapper = function(){
-                        compare(fileData, secondFileData);
-                    };
-
-                    wrapper();
-
-                    return getCompareApi(wrapper);
-                }
-            };
-
-            return self;
-        }
-
-        return {
-            onComplete: function( callback ){
-                updateCallbackArray.push(callback);
-                loadImageData(fileData, function(imageData, width, height){
-                    parseImage(imageData.data, width, height);
-                });
-            },
-            compareTo: function(secondFileData){
-                return getCompareApi(secondFileData);
-            }
-        };
-
-    };
-
-    _this['resemble'].outputSettings = function(options){
-        var key;
-        var undefined;
-
-        if(options.errorColor){
-            for (key in options.errorColor) {
-                errorPixelColor[key] = options.errorColor[key] === undefined ? errorPixelColor[key] : options.errorColor[key];
-            }
-        }
-
-        if(options.errorType && errorPixelTransform[options.errorType] ){
-            errorPixelTransformer = errorPixelTransform[options.errorType];
-        }
-
-        pixelTransparency = options.transparency || pixelTransparency;
-
-        return this;
-    };
-
-}(this));
\ No newline at end of file
+	};
+
+	_this['resemble'].outputSettings = function(options){
+		var key;
+		var undefined;
+
+		if(options.errorColor){
+			for (key in options.errorColor) {
+				errorPixelColor[key] = options.errorColor[key] === undefined ? errorPixelColor[key] : options.errorColor[key];
+			}
+		}
+
+		if(options.errorType && errorPixelTransform[options.errorType] ){
+			errorPixelTransformer = errorPixelTransform[options.errorType];
+		}
+
+		pixelTransparency = isNaN(Number(options.transparency)) ? pixelTransparency : options.transparency;
+
+		if (options.largeImageThreshold !== undefined) {
+			largeImageThreshold = options.largeImageThreshold;
+		}
+
+		return this;
+	};
+
+}(this));
diff --git a/tests/lib/screenshot-testing/support/test-environment.js b/tests/lib/screenshot-testing/support/test-environment.js
index 9379ab88daf78af3a5e8b9c1ad45c4b2e114a300..8a8dc183bf9171cc7bbce163502afede48138ad7 100644
--- a/tests/lib/screenshot-testing/support/test-environment.js
+++ b/tests/lib/screenshot-testing/support/test-environment.js
@@ -153,6 +153,9 @@ TestingEnvironment.prototype.setupFixture = function (fixtureClass, done) {
         self.reload();
         self.addPluginOnCmdLineToTestEnv();
 
+        self.fixtureClass = fixtureClass;
+        self.save();
+
         console.log();
 
         if (code) {
diff --git a/tests/travis/travis.sh b/tests/travis/travis.sh
index fcdcbfff1c15c6a31b72bb7381df47d703932382..2696abe897e2ceda6f0714c3fb0923a006aeb742 100755
--- a/tests/travis/travis.sh
+++ b/tests/travis/travis.sh
@@ -43,15 +43,21 @@ then
         echo ""
         echo "http://builds-artifacts.piwik.org/$artifacts_folder/$TRAVIS_JOB_NUMBER/screenshot-diffs/diffviewer.html"
         echo ""
-        echo "If the new screenshots are valid, then you can copy them over to tests/UI/expected-ui-screenshots/"
+        echo "If the new screenshots are valid, then you can copy them over to the right directory with the command:"
 
+        echo ""
+        echo "./console development:sync-ui-test-screenshots $TRAVIS_JOB_NUMBER"
         if [ -n "$PLUGIN_NAME" ]
         then
-            echo " with command:"
-            echo ""
-            echo "./console development:sync-ui-test-screenshots $TRAVIS_JOB_NUMBER"
-            echo ""
+            echo "  --plugin=$PLUGIN_NAME"
+
+            if [ "$UNPROTECTED_ARTIFACTS" = "" ];
+            then
+                echo " --http-user=... --http-password=..."
+            fi
         fi
+        
+        echo ""
 
         if [ -n "$PLUGIN_NAME" ]
         then