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/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/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/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 c8f3e927b582a82cef24e4067d0364d0d57d62f5..49cb1080a26c784503e4f691a07a871a1036d018 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,56 +79,33 @@ 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();
     }
@@ -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 = CoreUpdater::getComponentUpdates($updater);
         if (empty($componentsWithUpdateFile)) {
             throw new NoUpdatesFoundException("Everything is already up to date.");
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/tests/PHPUnit/Framework/Fixture.php b/tests/PHPUnit/Framework/Fixture.php
index d5f9aa397c993c73371993ea3b76e33901c19411..302d1192e2cc84a9b8955370e0370f3753e74775 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..8fe69f4a0708df35348bf701be916ad87d3761e8 160000
--- a/tests/UI/expected-ui-screenshots
+++ b/tests/UI/expected-ui-screenshots
@@ -1 +1 @@
-Subproject commit 560c44710748f4f27ceaee0955f59aa1336ee4a9
+Subproject commit 8fe69f4a0708df35348bf701be916ad87d3761e8
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/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) {