translator = $translator; $this->releaseChannels = $releaseChannels; $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'); if (Marketplace::isMarketplaceEnabled()) { // we need to load the marketplace already here, otherwise it will use the new, updated file in Piwik 3 // we also need to make sure to create a new instance here as otherwise we would change the "global" // environment, but we only want to change piwik version temporarily for this task here $environment = StaticContainer::getContainer()->make('Piwik\Plugins\Marketplace\Environment'); $environment->setPiwikVersion($newVersion); /** @var \Piwik\Plugins\Marketplace\Api\Client $marketplaceClient */ $marketplaceClient = StaticContainer::getContainer()->make('Piwik\Plugins\Marketplace\Api\Client', array( 'environment' => $environment )); require_once PIWIK_DOCUMENT_ROOT . '/plugins/CorePluginsAdmin/PluginInstaller.php'; require_once PIWIK_DOCUMENT_ROOT . '/plugins/Marketplace/Api/Exception.php'; } $this->installNewFiles($extractedArchiveDirectory); $messages[] = $this->translator->translate('CoreUpdater_InstallingTheLatestVersion'); } catch (ArchiveDownloadException $e) { throw $e; } catch (Exception $e) { throw new UpdaterException($e, $messages); } try { if (Marketplace::isMarketplaceEnabled() && !empty($marketplaceClient)) { $messages[] = $this->translator->translate('CoreUpdater_CheckingForPluginUpdates'); $pluginManager = PluginManager::getInstance(); $pluginManager->loadAllPluginsAndGetTheirInfo(); $loadedPlugins = $pluginManager->getLoadedPlugins(); $marketplaceClient->clearAllCacheEntries(); $pluginsWithUpdate = $marketplaceClient->checkUpdates($loadedPlugins); foreach ($pluginsWithUpdate as $pluginWithUpdate) { $pluginName = $pluginWithUpdate['name']; $messages[] = $this->translator->translate('CoreUpdater_UpdatingPluginXToVersionY', array($pluginName, $pluginWithUpdate['version'])); $pluginInstaller = new PluginInstaller($marketplaceClient); $pluginInstaller->installOrUpdatePluginFromMarketplace($pluginName); } } } catch (MarketplaceApi\Exception $e) { // there is a problem with the connection to the server, ignore for now } catch (Exception $e) { throw new UpdaterException($e, $messages); } try { $disabledPluginNames = $this->disableIncompatiblePlugins($newVersion); if (!empty($disabledPluginNames)) { $messages[] = $this->translator->translate('CoreUpdater_DisablingIncompatiblePlugins', implode(', ', $disabledPluginNames)); } } 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; foreach (['piwik', 'matomo'] as $flavor) { $extractedArchiveDirectory = $extractionPath . $flavor; // 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); foreach (['piwik', 'matomo'] as $flavor) { $extractedArchiveDirectory = $extractionPath . $flavor; if (file_exists($extractedArchiveDirectory)) { return $extractedArchiveDirectory; } } throw new \Exception('Could not find matomo or piwik directory in downloaded archive!'); } private function verifyDecompressedArchive($extractedArchiveDirectory) { $someExpectedFiles = array( '/config/global.ini.php', '/index.php', '/core/Piwik.php', '/piwik.php', '/matomo.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); } 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) { $channel = $this->releaseChannels->getActiveReleaseChannel(); $url = $channel->getDownloadUrlWithoutScheme($version); if ($this->isUpdatingOverHttps() && $https) { $url = 'https' . $url; } else { $url = 'http' . $url; } return $url; } private function getIncompatiblePlugins($piwikVersion) { return PluginManager::getInstance()->getIncompatiblePlugins($piwikVersion); } }