Download plugins/CoreUpdater/Updater.php from Leon4gr45/matomo: direct link, hf CLI and curl.
- Browser
- Download file 18.7 kB
-
https://huggingface.co/spaces/Leon4gr45/matomo/resolve/main/plugins/CoreUpdater/Updater.php
- Command line
-
hf download hf://spaces/Leon4gr45/matomo/plugins/CoreUpdater/Updater.php
-
curl -L -o Updater.php https://huggingface.co/spaces/Leon4gr45/matomo/resolve/main/plugins/CoreUpdater/Updater.php
18.7 kB
| /** | |
| * Matomo - free/libre analytics platform | |
| * | |
| * @link https://matomo.org | |
| * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later | |
| */ | |
| 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; | |
| /** | |
| * @var Translator | |
| */ | |
| private $translator; | |
| /** | |
| * @var ReleaseChannels | |
| */ | |
| private $releaseChannels; | |
| /** | |
| * @var string | |
| */ | |
| private $tmpPath; | |
| public function __construct(Translator $translator, ReleaseChannels $releaseChannels, $tmpPath) | |
| { | |
| $this->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; | |
| } | |
| /** | |
| * 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 = []; | |
| $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)) { | |
| // we expect a json encoded array response from oneClickUpdatePartTwo. Otherwise something went wrong. | |
| $messages = array_merge($messages, $responseCliMulti); | |
| } else { | |
| // there was likely an error eg such as an invalid ssl certificate... let's try executing it directly | |
| // in case this works. For example $response is in this case not an array but a string because the "communication" | |
| // with the controller went wrong: "Got invalid response from API request: https://ABC/?module=CoreUpdater&action=oneClickUpdatePartTwo&nonce=ABC. Response was \'curl_exec: SSL certificate problem: unable to get local issuer certificate. Hostname requested was: ABC" | |
| try { | |
| $response = $this->oneClickUpdatePartTwo($newVersion); | |
| if (!empty($response) && is_array($response)) { | |
| $messages = array_merge($messages, $response); | |
| } | |
| } catch (Exception $e) { | |
| // ignore any error should this fail too. this might be the case eg if | |
| // the user upgrades from one major version to another major version | |
| if (is_string($responseCliMulti)) { | |
| $messages[] = $responseCliMulti; // show why the original request failed eg invalid ssl certificate | |
| } | |
| } | |
| } | |
| } | |
| return $messages; | |
| } | |
| public function oneClickUpdatePartTwo($newVersion = null) | |
| { | |
| $messages = []; | |
| if (!Marketplace::isMarketplaceEnabled()) { | |
| $messages[] = 'Marketplace is disabled. Not updating any plugins.'; | |
| // prevent error Entry "Piwik\Plugins\Marketplace\Api\Client" cannot be resolved: Entry "Piwik\Plugins\Marketplace\Api\Service" cannot be resolved | |
| } else { | |
| if (!isset($newVersion)) { | |
| $newVersion = Version::VERSION; | |
| } | |
| // 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', [ | |
| '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) { | |
| // there is a problem with the connection to the server, ignore for now | |
| } catch (Exception $e) { | |
| throw new UpdaterException($e, $messages); | |
| } | |
| } | |
| // get a list of previously activated plugins and try to reactivate them if there are no missing requirements | |
| $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) { | |
| // noop - we will try to reactivate other plugins in the list. | |
| } | |
| } | |
| } | |
| } | |
| if (!empty($reactivatedPlugins)) { | |
| $messages[] = $this->translator->translate('CoreUpdater_ReactivatedPlugins', implode(', ', $reactivatedPlugins)); | |
| } | |
| try { | |
| // incompatible plugins may have already been disabled in oneClickUpdatePartTwo | |
| // if this might have failed we try it here again. | |
| $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) { | |
| // 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) | |
| { | |
| $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; | |
| } | |
| /** | |
| * Some dependency classes move to a different file path between Matomo major versions (for example | |
| * psr/log moved from Psr/Log to src). Loading them now - while the current files are still in place - | |
| * keeps them available to this already running process after installNewFiles() has replaced the files. | |
| * Otherwise the initialised autoloader can no longer resolve them from their old paths and the rest of | |
| * the update request fails (e.g. "Class Psr\Log\NullLogger not found"). | |
| * | |
| * This is only needed for the one-click update from Matomo 5 to Matomo 6 (the upgrade in which psr/log | |
| * relocates). It can be removed again in Matomo 6: once an install runs Matomo 6 the classes are already | |
| * at their new location, so no preloading is required for subsequent updates. | |
| */ | |
| 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) | |
| { | |
| // Load classes that move to a different file path in the new version before any files are replaced. | |
| $this->preloadRelocatedClasses(); | |
| // Make sure the execute bit is set for this shell script | |
| if (!Rules::isBrowserTriggerEnabled()) { | |
| @chmod($extractedArchiveDirectory . '/misc/cron/archive.sh', 0755); | |
| } | |
| $model = new Model(); | |
| // Check if the target directories are writable | |
| $this->checkFolderPermissions($extractedArchiveDirectory, PIWIK_INCLUDE_PATH); | |
| /* | |
| * 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) { // @phpstan-ignore notIdentical.alwaysFalse | |
| // 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 (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); | |
| } | |
| /** | |
| * check if the target file directory is writeable | |
| * @param string $source | |
| * @param string $target | |
| * @throws Exception | |
| */ | |
| 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)) { | |
| //add the wrong permission to the array | |
| $wrongPermissionDir[] = $target . '/' . $entry; | |
| } | |
| } | |
| } | |
| if (!empty($wrongPermissionDir)) { | |
| throw new Exception($this->translator->translate( | |
| 'CoreUpdater_ExceptionDirWrongPermission', | |
| implode(', ', $wrongPermissionDir) | |
| )); | |
| } | |
| } | |
| } | |