| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\Plugins\CorePluginsAdmin; |
|
|
| use Piwik\Common; |
| use Piwik\Container\StaticContainer; |
| use Piwik\Filechecks; |
| use Piwik\Filesystem; |
| use Piwik\Piwik; |
| use Piwik\Plugin\Manager as PluginManager; |
| use Piwik\Plugin\Dependency as PluginDependency; |
| use Piwik\Plugin\Manager; |
| use Piwik\Plugins\Marketplace\Environment; |
| use Piwik\Plugins\Marketplace\Marketplace; |
| use Piwik\Unzip; |
| use Piwik\Plugins\Marketplace\Api\Client; |
|
|
| class PluginInstaller |
| { |
| public const PATH_TO_DOWNLOAD = '/latest/plugins/'; |
|
|
| private $pluginName; |
|
|
| |
| |
| |
| |
| private $marketplaceClient; |
|
|
| |
| |
| |
| |
| public function __construct($client = null) |
| { |
| if (!empty($client)) { |
| $this->marketplaceClient = $client; |
| } elseif (Marketplace::isMarketplaceEnabled()) { |
| |
| $this->marketplaceClient = StaticContainer::get('Piwik\Plugins\Marketplace\Api\Client'); |
| } |
| } |
|
|
| public function installOrUpdatePluginFromMarketplace($pluginName) |
| { |
| $this->checkMarketplaceIsEnabled(); |
|
|
| $this->pluginName = $pluginName; |
|
|
| try { |
| $this->makeSureFoldersAreWritable(); |
| $this->makeSurePluginNameIsValid(); |
|
|
| $tmpPluginZip = $this->downloadPluginFromMarketplace(); |
| $tmpPluginFolder = dirname($tmpPluginZip) . '/' . basename($tmpPluginZip, '.zip') . '/'; |
| $this->extractPluginFiles($tmpPluginZip, $tmpPluginFolder); |
| $this->makeSurePluginJsonExists($tmpPluginFolder); |
| $metadata = $this->getPluginMetadataIfValid($tmpPluginFolder); |
| $this->makeSureThereAreNoMissingRequirements($metadata); |
| $this->copyPluginToDestination($tmpPluginFolder); |
|
|
| Filesystem::deleteAllCacheOnUpdate($this->pluginName); |
|
|
| $pluginManager = PluginManager::getInstance(); |
| if ($pluginManager->isPluginLoaded($this->pluginName)) { |
| $plugin = PluginManager::getInstance()->getLoadedPlugin($this->pluginName); |
| if (!empty($plugin)) { |
| $plugin->reloadPluginInformation(); |
| } |
| } |
| } catch (\Exception $e) { |
| if (!empty($tmpPluginZip)) { |
| Filesystem::deleteFileIfExists($tmpPluginZip); |
| } |
| if (!empty($tmpPluginFolder)) { |
| $this->removeFolderIfExists($tmpPluginFolder); |
| } |
|
|
| throw $e; |
| } |
|
|
| $this->removeFileIfExists($tmpPluginZip); |
| $this->removeFolderIfExists($tmpPluginFolder); |
| } |
|
|
| public function installOrUpdatePluginFromFile($pathToZip) |
| { |
| $tmpPluginName = 'uploaded' . Common::generateUniqId(); |
| $tmpPluginFolder = StaticContainer::get('path.tmp') . self::PATH_TO_DOWNLOAD . $tmpPluginName; |
|
|
| try { |
| $this->makeSureFoldersAreWritable(); |
| $this->extractPluginFiles($pathToZip, $tmpPluginFolder); |
|
|
| $this->makeSurePluginJsonExists($tmpPluginFolder); |
| $metadata = $this->getPluginMetadataIfValid($tmpPluginFolder); |
| $this->makeSureThereAreNoMissingRequirements($metadata); |
|
|
| $this->pluginName = $metadata->name; |
|
|
| $this->fixPluginFolderIfNeeded($tmpPluginFolder); |
| $this->copyPluginToDestination($tmpPluginFolder); |
|
|
| Filesystem::deleteAllCacheOnUpdate($this->pluginName); |
| } catch (\Exception $e) { |
| $this->removeFileIfExists($pathToZip); |
| $this->removeFolderIfExists($tmpPluginFolder); |
|
|
| throw $e; |
| } |
|
|
| $this->removeFileIfExists($pathToZip); |
| $this->removeFolderIfExists($tmpPluginFolder); |
|
|
| return $metadata; |
| } |
|
|
| private function makeSureFoldersAreWritable() |
| { |
| $dirs = array( |
| StaticContainer::get('path.tmp') . self::PATH_TO_DOWNLOAD, |
| Manager::getPluginsDirectory(), |
| ); |
| |
| |
| Filechecks::dieIfDirectoriesNotWritable($dirs); |
| } |
|
|
| |
| |
| |
| |
| private function downloadPluginFromMarketplace() |
| { |
| try { |
| return $this->marketplaceClient->download($this->pluginName); |
| } catch (\Exception $e) { |
| try { |
| $downloadUrl = $this->marketplaceClient->getDownloadUrl($this->pluginName); |
| $errorMessage = sprintf('Failed to download plugin from %s: %s', $downloadUrl, $e->getMessage()); |
| } catch (\Exception $ex) { |
| $errorMessage = sprintf('Failed to download plugin: %s', $e->getMessage()); |
| } |
|
|
| throw new PluginInstallerException($errorMessage); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| private function extractPluginFiles($pluginZipFile, $pathExtracted) |
| { |
| $archive = Unzip::factory('PclZip', $pluginZipFile); |
|
|
| $this->removeFolderIfExists($pathExtracted); |
|
|
| if (0 == ($pluginFiles = $archive->extract($pathExtracted))) { |
| throw new PluginInstallerException(Piwik::translate('CoreUpdater_ExceptionArchiveIncompatible', $archive->errorInfo())); |
| } |
|
|
| if (0 == count($pluginFiles)) { |
| throw new PluginInstallerException(Piwik::translate('Plugin Zip File Is Empty')); |
| } |
| } |
|
|
| private function makeSurePluginJsonExists($tmpPluginFolder) |
| { |
| $pluginJsonPath = $this->getPathToPluginJson($tmpPluginFolder); |
|
|
| if (!file_exists($pluginJsonPath)) { |
| throw new PluginInstallerException('Plugin is not valid, it is missing the plugin.json file.'); |
| } |
| } |
|
|
| private function makeSureThereAreNoMissingRequirements($metadata) |
| { |
| $requires = array(); |
| if (!empty($metadata->require)) { |
| $requires = (array) $metadata->require; |
| } |
|
|
| $dependency = new PluginDependency(); |
| $dependency->setEnvironment($this->getEnvironment()); |
| $missingDependencies = $dependency->getMissingDependencies($requires); |
|
|
| if (!empty($missingDependencies)) { |
| $message = ''; |
| foreach ($missingDependencies as $dep) { |
| if (empty($dep['actualVersion'])) { |
| $params = array(ucfirst($dep['requirement']), $dep['requiredVersion'], $metadata->name); |
| $message .= Piwik::translate('CorePluginsAdmin_MissingRequirementsPleaseInstallNotice', $params); |
| } else { |
| $params = array(ucfirst($dep['requirement']), $dep['actualVersion'], $dep['requiredVersion']); |
| $message .= Piwik::translate('CorePluginsAdmin_MissingRequirementsNotice', $params); |
| } |
| } |
|
|
| throw new PluginInstallerException($message); |
| } |
| } |
|
|
| private function getPluginMetadataIfValid($tmpPluginFolder) |
| { |
| $pluginJsonPath = $this->getPathToPluginJson($tmpPluginFolder); |
|
|
| $metadata = file_get_contents($pluginJsonPath); |
| $metadata = json_decode($metadata); |
|
|
| if (empty($metadata)) { |
| throw new PluginInstallerException('Plugin is not valid, plugin.json is empty or does not contain valid JSON.'); |
| } |
|
|
| if (empty($metadata->name)) { |
| throw new PluginInstallerException('Plugin is not valid, the plugin.json file does not specify the plugin name.'); |
| } |
|
|
| if (!preg_match('/^[a-zA-Z0-9_-]+$/', $metadata->name)) { |
| throw new PluginInstallerException('The plugin name specified in plugin.json contains illegal characters. ' . |
| 'Plugin name can only contain following characters: [a-zA-Z0-9-_].'); |
| } |
|
|
| if (empty($metadata->version)) { |
| throw new PluginInstallerException('Plugin is not valid, the plugin.json file does not specify the plugin version.'); |
| } |
|
|
| if (empty($metadata->description)) { |
| throw new PluginInstallerException('Plugin is not valid, the plugin.json file does not specify a description.'); |
| } |
|
|
| return $metadata; |
| } |
|
|
| private function getPathToPluginJson($tmpPluginFolder) |
| { |
| $firstSubFolder = $this->getNameOfFirstSubfolder($tmpPluginFolder); |
| $path = $tmpPluginFolder . DIRECTORY_SEPARATOR . $firstSubFolder . DIRECTORY_SEPARATOR . 'plugin.json'; |
|
|
| return $path; |
| } |
|
|
| |
| |
| |
| |
| private function getNameOfFirstSubfolder($pluginDir) |
| { |
| if (!($dir = opendir($pluginDir))) { |
| return false; |
| } |
| $firstSubFolder = ''; |
|
|
| while ($file = readdir($dir)) { |
| if ($file[0] != '.' && is_dir($pluginDir . DIRECTORY_SEPARATOR . $file)) { |
| $firstSubFolder = $file; |
| break; |
| } |
| } |
|
|
| if (empty($firstSubFolder)) { |
| throw new PluginInstallerException('The plugin ZIP file does not contain a subfolder, but Piwik expects plugin files to be within a subfolder in the Zip archive.'); |
| } |
|
|
| return $firstSubFolder; |
| } |
|
|
| private function fixPluginFolderIfNeeded($tmpPluginFolder) |
| { |
| $firstSubFolder = $this->getNameOfFirstSubfolder($tmpPluginFolder); |
|
|
| if ($firstSubFolder === $this->pluginName) { |
| return; |
| } |
|
|
| $from = $tmpPluginFolder . DIRECTORY_SEPARATOR . $firstSubFolder; |
| $to = $tmpPluginFolder . DIRECTORY_SEPARATOR . $this->pluginName; |
| rename($from, $to); |
| } |
|
|
| private function copyPluginToDestination($tmpPluginFolder) |
| { |
| $pluginsDir = Manager::getPluginsDirectory(); |
|
|
| if (!empty($GLOBALS['MATOMO_PLUGIN_COPY_DIR'])) { |
| $pluginsDir = $GLOBALS['MATOMO_PLUGIN_COPY_DIR']; |
| } |
| $pluginTargetPath = $pluginsDir . $this->pluginName; |
|
|
| $this->removeFolderIfExists($pluginTargetPath); |
|
|
| Filesystem::copyRecursive($tmpPluginFolder, $pluginsDir); |
| } |
|
|
| |
| |
| |
| private function removeFolderIfExists($pathExtracted) |
| { |
| Filesystem::unlinkRecursive($pathExtracted, true); |
| } |
|
|
| |
| |
| |
| private function removeFileIfExists($targetTmpFile) |
| { |
| Filesystem::deleteFileIfExists($targetTmpFile); |
| } |
|
|
| |
| |
| |
| private function makeSurePluginNameIsValid() |
| { |
| try { |
| $pluginDetails = $this->marketplaceClient->getPluginInfo($this->pluginName); |
| } catch (\Exception $e) { |
| throw new PluginInstallerException($e->getMessage()); |
| } |
|
|
| if (empty($pluginDetails)) { |
| throw new PluginInstallerException('This plugin was not found in the Marketplace.'); |
| } |
| } |
|
|
| private function checkMarketplaceIsEnabled() |
| { |
| if (!isset($this->marketplaceClient)) { |
| throw new PluginInstallerException('Marketplace plugin needs to be enabled to perform this action.'); |
| } |
| } |
|
|
| private function getEnvironment() |
| { |
| if ($this->marketplaceClient) { |
| return $this->marketplaceClient->getEnvironment(); |
| } else { |
| return StaticContainer::get(Environment::class); |
| } |
| } |
| } |
|
|