diff options
author | Matthieu Napoli <matthieu@mnapoli.fr> | 2015-03-13 06:04:35 +0300 |
---|---|---|
committer | Matthieu Napoli <matthieu@mnapoli.fr> | 2015-03-16 00:37:32 +0300 |
commit | 3f2d8e0bdcd62887dfa800c5391954f26bb98871 (patch) | |
tree | dd9c46a5ee6d6b0cdbf2ba54956743c15a373652 /plugins | |
parent | 36214a73090b91cb322329fe39d2e1a718229c58 (diff) |
Refactor Piwik update with a Updater class + UI tests
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/CoreUpdater/Controller.php | 205 | ||||
-rw-r--r-- | plugins/CoreUpdater/Updater.php | 266 | ||||
-rw-r--r-- | plugins/CoreUpdater/UpdaterException.php | 31 | ||||
-rw-r--r-- | plugins/CoreUpdater/config/config.php | 6 | ||||
-rw-r--r-- | plugins/CoreUpdater/tests/Fixtures/FailUpdateHttpsFixture.php | 24 | ||||
-rw-r--r-- | plugins/CoreUpdater/tests/Mock/UpdaterMock.php | 52 | ||||
-rw-r--r-- | plugins/CoreUpdater/tests/UI/PiwikUpdater_spec.js | 22 | ||||
-rw-r--r-- | plugins/CoreUpdater/tests/UI/expected-ui-screenshots/PiwikUpdater_newVersion.png | bin | 0 -> 27898 bytes |
8 files changed, 420 insertions, 186 deletions
diff --git a/plugins/CoreUpdater/Controller.php b/plugins/CoreUpdater/Controller.php index c8f3e927b5..be860af8bd 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; - } - if (self::isUpdatingOverHttps()) { - $url = 'https' . $url; - } else { - $url = 'http' . $url; - } + /** + * @var Updater + */ + private $updater; - 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,40 +79,13 @@ 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))); - } - $steps[] = array('oneClick_Copy', Piwik::translate('CoreUpdater_InstallingTheLatestVersion')); - $steps[] = array('oneClick_Finished', Piwik::translate('CoreUpdater_PiwikUpdatedSuccessfully')); - - $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(); + $errorMessage = false; + } catch (UpdaterException $e) { + $errorMessage = $e->getMessage(); + $messages = $e->getUpdateLogMessages(); } $view = new OneClickDone(Piwik::getCurrentUserTokenAuth()); @@ -166,130 +120,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 +141,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/plugins/CoreUpdater/Updater.php b/plugins/CoreUpdater/Updater.php new file mode 100644 index 0000000000..18ed1e6e09 --- /dev/null +++ b/plugins/CoreUpdater/Updater.php @@ -0,0 +1,266 @@ +<?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'; + + /** + * @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 Exception + * @throws UpdaterException + */ + 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; + + Http::fetchRemoteFile($url, $archiveFile, 0, 120); + + 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 0000000000..d9a543f30f --- /dev/null +++ b/plugins/CoreUpdater/UpdaterException.php @@ -0,0 +1,31 @@ +<?php + +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 0000000000..419e43cc7b --- /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/tests/Fixtures/FailUpdateHttpsFixture.php b/plugins/CoreUpdater/tests/Fixtures/FailUpdateHttpsFixture.php new file mode 100644 index 0000000000..13b8868eaa --- /dev/null +++ b/plugins/CoreUpdater/tests/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\Tests\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\tests\Mock\UpdaterMock'), + ); + } +} diff --git a/plugins/CoreUpdater/tests/Mock/UpdaterMock.php b/plugins/CoreUpdater/tests/Mock/UpdaterMock.php new file mode 100644 index 0000000000..e1eaee61a1 --- /dev/null +++ b/plugins/CoreUpdater/tests/Mock/UpdaterMock.php @@ -0,0 +1,52 @@ +<?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\tests\Mock; + +use Piwik\Plugins\CoreUpdater\Updater; +use Piwik\Plugins\CoreUpdater\UpdaterException; +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) { + throw new UpdaterException(new \Exception('Error while downloading Piwik'), 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/UI/PiwikUpdater_spec.js b/plugins/CoreUpdater/tests/UI/PiwikUpdater_spec.js new file mode 100644 index 0000000000..4438c1b48b --- /dev/null +++ b/plugins/CoreUpdater/tests/UI/PiwikUpdater_spec.js @@ -0,0 +1,22 @@ +/*! + * 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("PiwikUpdater", function () { + this.timeout(0); + + this.fixture = "Piwik\\Plugins\\CoreUpdater\\Tests\\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); + }); +}); diff --git a/plugins/CoreUpdater/tests/UI/expected-ui-screenshots/PiwikUpdater_newVersion.png b/plugins/CoreUpdater/tests/UI/expected-ui-screenshots/PiwikUpdater_newVersion.png Binary files differnew file mode 100644 index 0000000000..483dde95f8 --- /dev/null +++ b/plugins/CoreUpdater/tests/UI/expected-ui-screenshots/PiwikUpdater_newVersion.png |