| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\Plugins\CoreUpdater; |
|
|
| use Exception; |
| use Piwik\ArchiveProcessor\Rules; |
| use Piwik\CliMulti; |
| use Piwik\Common; |
| use Piwik\Config\GeneralConfig; |
| use Piwik\Container\StaticContainer; |
| use Piwik\Filechecks; |
| use Piwik\Filesystem; |
| use Piwik\Http; |
| use Piwik\Option; |
| use Piwik\Plugin\Manager as PluginManager; |
| use Piwik\Plugin\ReleaseChannels; |
| use Piwik\Plugins\CorePluginsAdmin\PluginInstaller; |
| use Piwik\Plugins\Marketplace\API as MarketplaceApi; |
| use Piwik\Plugins\Marketplace\Marketplace; |
| use Piwik\SettingsServer; |
| use Piwik\Translation\Translator; |
| use Piwik\Unzip; |
| use Piwik\Version; |
|
|
| class Updater |
| { |
| public const OPTION_LATEST_VERSION = 'UpdateCheck_LatestVersion'; |
| public const PATH_TO_EXTRACT_LATEST_VERSION = '/latest/'; |
| public const DOWNLOAD_TIMEOUT = 720; |
|
|
| |
| |
| |
| private $translator; |
|
|
| |
| |
| |
| private $releaseChannels; |
|
|
| |
| |
| |
| private $tmpPath; |
|
|
| public function __construct(Translator $translator, ReleaseChannels $releaseChannels, $tmpPath) |
| { |
| $this->translator = $translator; |
| $this->releaseChannels = $releaseChannels; |
| $this->tmpPath = $tmpPath; |
| } |
|
|
| |
| |
| |
| |
| |
| public function getLatestVersion() |
| { |
| return Option::get(self::OPTION_LATEST_VERSION); |
| } |
|
|
| |
| |
| |
| public function isNewVersionAvailable() |
| { |
| $latestVersion = self::getLatestVersion(); |
| return $latestVersion && version_compare(Version::VERSION, $latestVersion) === -1; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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 = []; |
|
|
| $pluginManager = PluginManager::getInstance(); |
| $activatedPlugins = $pluginManager->getActivatedPlugins(); |
| Option::set('OneClickUpdate_ActivatedPlugins', json_encode($activatedPlugins)); |
|
|
| 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'); |
|
|
| $this->installNewFiles($extractedArchiveDirectory); |
| $messages[] = $this->translator->translate('CoreUpdater_InstallingTheLatestVersion'); |
| } catch (ArchiveDownloadException $e) { |
| throw $e; |
| } catch (Exception $e) { |
| throw new UpdaterException($e, $messages); |
| } |
|
|
| $validFor10Minutes = time() + (60 * 10); |
| $nonce = Common::generateUniqId(); |
| Option::set('NonceOneClickUpdatePartTwo', json_encode(['nonce' => $nonce, 'ttl' => $validFor10Minutes])); |
|
|
| $cliMulti = new CliMulti(); |
| $responses = $cliMulti->request(['?module=CoreUpdater&action=oneClickUpdatePartTwo&nonce=' . $nonce]); |
|
|
| if (!empty($responses)) { |
| $responseCliMulti = array_shift($responses); |
| $responseCliMulti = @json_decode($responseCliMulti, $assoc = true); |
| if (is_array($responseCliMulti)) { |
| |
| $messages = array_merge($messages, $responseCliMulti); |
| } else { |
| |
| |
| |
| try { |
| $response = $this->oneClickUpdatePartTwo($newVersion); |
| if (!empty($response) && is_array($response)) { |
| $messages = array_merge($messages, $response); |
| } |
| } catch (Exception $e) { |
| |
| |
| if (is_string($responseCliMulti)) { |
| $messages[] = $responseCliMulti; |
| } |
| } |
| } |
| } |
|
|
| return $messages; |
| } |
|
|
| public function oneClickUpdatePartTwo($newVersion = null) |
| { |
| $messages = []; |
|
|
| if (!Marketplace::isMarketplaceEnabled()) { |
| $messages[] = 'Marketplace is disabled. Not updating any plugins.'; |
| |
| } else { |
| if (!isset($newVersion)) { |
| $newVersion = Version::VERSION; |
| } |
|
|
| |
| |
| $environment = StaticContainer::getContainer()->make('Piwik\Plugins\Marketplace\Environment'); |
| $environment->setPiwikVersion($newVersion); |
| |
| $marketplaceClient = StaticContainer::getContainer()->make('Piwik\Plugins\Marketplace\Api\Client', [ |
| 'environment' => $environment, |
| ]); |
|
|
| try { |
| $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', |
| [$pluginName, $pluginWithUpdate['version']] |
| ); |
| $pluginInstaller = new PluginInstaller($marketplaceClient); |
| $pluginInstaller->installOrUpdatePluginFromMarketplace($pluginName); |
| } |
| } catch (MarketplaceApi\Exception $e) { |
| |
| } catch (Exception $e) { |
| throw new UpdaterException($e, $messages); |
| } |
| } |
|
|
| |
| $previouslyActivePlugins = Option::get('OneClickUpdate_ActivatedPlugins'); |
| if (false !== $previouslyActivePlugins) { |
| $previouslyActivePlugins = json_decode($previouslyActivePlugins, true); |
| } else { |
| $previouslyActivePlugins = []; |
| } |
| Option::delete('OneClickUpdate_ActivatedPlugins'); |
|
|
| $reactivatedPlugins = []; |
| if (!empty($previouslyActivePlugins)) { |
| $pluginManager = PluginManager::getInstance(); |
| foreach ($previouslyActivePlugins as $previouslyActivePluginName) { |
| if (!$pluginManager->isPluginActivated($previouslyActivePluginName)) { |
| try { |
| $plugin = $pluginManager->loadPlugin($previouslyActivePluginName); |
| if (empty($plugin->getMissingDependencies($newVersion))) { |
| $pluginManager->activatePlugin($previouslyActivePluginName); |
| $reactivatedPlugins[] = $previouslyActivePluginName; |
| } |
| } catch (\Throwable $e) { |
| |
| } |
| } |
| } |
| } |
|
|
| if (!empty($reactivatedPlugins)) { |
| $messages[] = $this->translator->translate('CoreUpdater_ReactivatedPlugins', implode(', ', $reactivatedPlugins)); |
| } |
|
|
| try { |
| |
| |
| $disabledPluginNames = $this->disableIncompatiblePlugins($newVersion); |
| if (!empty($disabledPluginNames)) { |
| $messages[] = $this->translator->translate('CoreUpdater_DisablingIncompatiblePlugins', implode(', ', $disabledPluginNames)); |
| } |
| } catch (\Throwable $e) { |
| throw new UpdaterException($e, $messages); |
| } |
|
|
| Filesystem::deleteAllCacheOnUpdate(); |
|
|
| 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) { |
| |
| 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; |
|
|
| |
| 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) |
| { |
| $pluginManager = PluginManager::getInstance(); |
| $plugins = $pluginManager->getLoadedPlugins(); |
| foreach ($plugins as $plugin) { |
| $plugin->reloadPluginInformation(); |
| } |
|
|
| $incompatiblePlugins = $this->getIncompatiblePlugins($version); |
| $disabledPluginNames = array(); |
|
|
| foreach ($incompatiblePlugins as $plugin) { |
| $name = $plugin->getPluginName(); |
| PluginManager::getInstance()->deactivatePlugin($name); |
| $disabledPluginNames[] = $name; |
| } |
|
|
| return $disabledPluginNames; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private function preloadRelocatedClasses(): void |
| { |
| $classesToPreload = [ |
| 'Psr\Log\LoggerInterface', |
| 'Psr\Log\AbstractLogger', |
| 'Psr\Log\NullLogger', |
| 'Psr\Log\LoggerTrait', |
| 'Psr\Log\LogLevel', |
| 'Psr\Log\InvalidArgumentException', |
| 'Psr\Log\LoggerAwareInterface', |
| 'Psr\Log\LoggerAwareTrait', |
| ]; |
|
|
| foreach ($classesToPreload as $classToPreload) { |
| class_exists($classToPreload) || interface_exists($classToPreload) || trait_exists($classToPreload); |
| } |
| } |
|
|
| private function installNewFiles($extractedArchiveDirectory) |
| { |
| |
| $this->preloadRelocatedClasses(); |
|
|
| |
| if (!Rules::isBrowserTriggerEnabled()) { |
| @chmod($extractedArchiveDirectory . '/misc/cron/archive.sh', 0755); |
| } |
|
|
| $model = new Model(); |
|
|
| |
| $this->checkFolderPermissions($extractedArchiveDirectory, PIWIK_INCLUDE_PATH); |
|
|
| |
| |
| |
| |
| Filesystem::copyRecursive($extractedArchiveDirectory, PIWIK_INCLUDE_PATH); |
| $model->removeGoneFiles($extractedArchiveDirectory, PIWIK_INCLUDE_PATH); |
|
|
| |
| |
| |
| |
| if (PIWIK_INCLUDE_PATH !== PIWIK_DOCUMENT_ROOT) { |
| |
| $specialCases = array( |
| '/index.php', |
| '/piwik.php', |
| '/js/index.php', |
| ); |
|
|
| foreach ($specialCases as $file) { |
| Filesystem::copy($extractedArchiveDirectory . $file, PIWIK_DOCUMENT_ROOT . $file); |
| } |
|
|
| |
| Filesystem::copyRecursive($extractedArchiveDirectory, PIWIK_DOCUMENT_ROOT, true); |
| $model->removeGoneFiles($extractedArchiveDirectory, PIWIK_DOCUMENT_ROOT); |
| } |
|
|
| Filesystem::unlinkRecursive($extractedArchiveDirectory, true); |
|
|
| Filesystem::clearPhpCaches(); |
| } |
|
|
| |
| |
| |
| |
| |
| public function getArchiveUrl($version, $https = true) |
| { |
| $channel = $this->releaseChannels->getActiveReleaseChannel(); |
| $url = $channel->getDownloadUrlWithoutScheme($version); |
|
|
| if (Http::isUpdatingOverHttps() && $https && GeneralConfig::getConfigValue('force_matomo_http_request') == 0) { |
| $url = 'https' . $url; |
| } else { |
| $url = 'http' . $url; |
| } |
|
|
| return $url; |
| } |
|
|
| private function getIncompatiblePlugins($piwikVersion) |
| { |
| return PluginManager::getInstance()->getIncompatiblePlugins($piwikVersion); |
| } |
|
|
|
|
| |
| |
| |
| |
| |
| |
| private function checkFolderPermissions($source, $target) |
| { |
| $wrongPermissionDir = []; |
| if (is_dir($source)) { |
| $d = dir($source); |
| while (false !== ($entry = $d->read())) { |
| if ($entry == '.' || $entry == '..') { |
| continue; |
| } |
| $sourcePath = $source . '/' . $entry; |
| if (is_dir($sourcePath) && !is_writable($target . '/' . $entry)) { |
| |
| $wrongPermissionDir[] = $target . '/' . $entry; |
| } |
| } |
| } |
|
|
| if (!empty($wrongPermissionDir)) { |
| throw new Exception($this->translator->translate( |
| 'CoreUpdater_ExceptionDirWrongPermission', |
| implode(', ', $wrongPermissionDir) |
| )); |
| } |
| } |
| } |
|
|