forked from vergnet/site-accueil-insa
400 lines
14 KiB
PHP
400 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* Matomo - free/libre analytics platform
|
|
*
|
|
* @link https://matomo.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\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
|
|
{
|
|
const OPTION_LATEST_VERSION = 'UpdateCheck_LatestVersion';
|
|
const PATH_TO_EXTRACT_LATEST_VERSION = '/latest/';
|
|
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 = 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');
|
|
|
|
$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 explample $response is in this case not an array but a string because the "communcation"
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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
|
|
return $messages;
|
|
}
|
|
|
|
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', array(
|
|
'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',
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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();
|
|
|
|
// 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) {
|
|
// 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)));
|
|
}
|
|
}
|
|
}
|