diff options
23 files changed, 584 insertions, 205 deletions
diff --git a/.gitignore b/.gitignore index 344d651bf5..7a8311d0be 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 cbbc36a00a..af2a09b695 100644 --- a/core/Container/ContainerFactory.php +++ b/core/Container/ContainerFactory.php @@ -28,11 +28,17 @@ 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 874851acde..8468dce2ae 100644 --- a/core/Container/StaticContainer.php +++ b/core/Container/StaticContainer.php @@ -32,6 +32,13 @@ class StaticContainer private static $environment; /** + * Definitions to register in the container. + * + * @var array + */ + private static $definitions = array(); + + /** * @return Container */ public static function getContainer() @@ -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 e07c2c3bdd..1d8c8b809f 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 0000000000..0b68f30c18 --- /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 c8f3e927b5..49cb1080a2 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 index e2b3a0024e..89a29126f2 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 0000000000..5060c99320 --- /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 index 91b6e2c2a1..6ec5fdaf83 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 0000000000..7a73129c5b --- /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 index 3098595b77..521aa8fc35 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 0000000000..1adcbc6cc0 --- /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 0000000000..bd4599b111 --- /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 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/lang/en.json b/plugins/CoreUpdater/lang/en.json index 67bc20f27b..2c3ffb9be2 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 cf87a949e6..8691d97595 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 d08378d6c3..8ded5fd604 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>✓ {{ message }}</p> + <p>✓ {{ 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 d5f9aa397c..302d1192e2 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 97d98dbff8..3acc202a91 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 -Subproject 560c44710748f4f27ceaee0955f59aa1336ee4a +Subproject 8fe69f4a0708df35348bf701be916ad87d3761e diff --git a/tests/UI/specs/CoreUpdaterCode_spec.js b/tests/UI/specs/CoreUpdaterCode_spec.js new file mode 100644 index 0000000000..807810869b --- /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 index 0ddc8b67cb..f77776a4ca 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 9379ab88da..8a8dc183bf 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) { |