PageRenderTime 40ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/CorePluginsAdmin/PluginInstaller.php

https://github.com/CodeYellowBV/piwik
PHP | 294 lines | 202 code | 63 blank | 29 comment | 21 complexity | 625e159c0e4a012c694103d451fe9c25 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\Plugins\CorePluginsAdmin;
  10. use Piwik\Filechecks;
  11. use Piwik\Filesystem;
  12. use Piwik\Piwik;
  13. use Piwik\Plugin\Dependency as PluginDependency;
  14. use Piwik\SettingsPiwik;
  15. use Piwik\Unzip;
  16. /**
  17. *
  18. */
  19. class PluginInstaller
  20. {
  21. const PATH_TO_DOWNLOAD = '/tmp/latest/plugins/';
  22. const PATH_TO_EXTRACT = '/plugins/';
  23. private $pluginName;
  24. public function __construct($pluginName)
  25. {
  26. $this->pluginName = $pluginName;
  27. }
  28. public function installOrUpdatePluginFromMarketplace()
  29. {
  30. $tmpPluginZip = PIWIK_USER_PATH . self::PATH_TO_DOWNLOAD . $this->pluginName . '.zip';
  31. $tmpPluginFolder = PIWIK_USER_PATH . self::PATH_TO_DOWNLOAD . $this->pluginName;
  32. $tmpPluginZip = SettingsPiwik::rewriteTmpPathWithInstanceId($tmpPluginZip);
  33. $tmpPluginFolder = SettingsPiwik::rewriteTmpPathWithInstanceId($tmpPluginFolder);
  34. try {
  35. $this->makeSureFoldersAreWritable();
  36. $this->makeSurePluginNameIsValid();
  37. $this->downloadPluginFromMarketplace($tmpPluginZip);
  38. $this->extractPluginFiles($tmpPluginZip, $tmpPluginFolder);
  39. $this->makeSurePluginJsonExists($tmpPluginFolder);
  40. $metadata = $this->getPluginMetadataIfValid($tmpPluginFolder);
  41. $this->makeSureThereAreNoMissingRequirements($metadata);
  42. $this->copyPluginToDestination($tmpPluginFolder);
  43. } catch (\Exception $e) {
  44. $this->removeFileIfExists($tmpPluginZip);
  45. $this->removeFolderIfExists($tmpPluginFolder);
  46. throw $e;
  47. }
  48. $this->removeFileIfExists($tmpPluginZip);
  49. $this->removeFolderIfExists($tmpPluginFolder);
  50. }
  51. public function installOrUpdatePluginFromFile($pathToZip)
  52. {
  53. $tmpPluginFolder = PIWIK_USER_PATH . self::PATH_TO_DOWNLOAD . $this->pluginName;
  54. $tmpPluginFolder = SettingsPiwik::rewriteTmpPathWithInstanceId($tmpPluginFolder);
  55. try {
  56. $this->makeSureFoldersAreWritable();
  57. $this->extractPluginFiles($pathToZip, $tmpPluginFolder);
  58. $this->makeSurePluginJsonExists($tmpPluginFolder);
  59. $metadata = $this->getPluginMetadataIfValid($tmpPluginFolder);
  60. $this->makeSureThereAreNoMissingRequirements($metadata);
  61. $this->pluginName = $metadata->name;
  62. $this->fixPluginFolderIfNeeded($tmpPluginFolder);
  63. $this->copyPluginToDestination($tmpPluginFolder);
  64. } catch (\Exception $e) {
  65. $this->removeFileIfExists($pathToZip);
  66. $this->removeFolderIfExists($tmpPluginFolder);
  67. throw $e;
  68. }
  69. $this->removeFileIfExists($pathToZip);
  70. $this->removeFolderIfExists($tmpPluginFolder);
  71. return $metadata;
  72. }
  73. private function makeSureFoldersAreWritable()
  74. {
  75. Filechecks::dieIfDirectoriesNotWritable(array(self::PATH_TO_DOWNLOAD, self::PATH_TO_EXTRACT));
  76. }
  77. private function downloadPluginFromMarketplace($pluginZipTargetFile)
  78. {
  79. $this->removeFileIfExists($pluginZipTargetFile);
  80. $marketplace = new MarketplaceApiClient();
  81. try {
  82. $marketplace->download($this->pluginName, $pluginZipTargetFile);
  83. } catch (\Exception $e) {
  84. try {
  85. $downloadUrl = $marketplace->getDownloadUrl($this->pluginName);
  86. $errorMessage = sprintf('Failed to download plugin from %s: %s', $downloadUrl, $e->getMessage());
  87. } catch (\Exception $ex) {
  88. $errorMessage = sprintf('Failed to download plugin: %s', $e->getMessage());
  89. }
  90. throw new PluginInstallerException($errorMessage);
  91. }
  92. }
  93. /**
  94. * @param $pluginZipFile
  95. * @param $pathExtracted
  96. * @throws \Exception
  97. */
  98. private function extractPluginFiles($pluginZipFile, $pathExtracted)
  99. {
  100. $archive = Unzip::factory('PclZip', $pluginZipFile);
  101. $this->removeFolderIfExists($pathExtracted);
  102. if (0 == ($pluginFiles = $archive->extract($pathExtracted))) {
  103. throw new PluginInstallerException(Piwik::translate('CoreUpdater_ExceptionArchiveIncompatible', $archive->errorInfo()));
  104. }
  105. if (0 == count($pluginFiles)) {
  106. throw new PluginInstallerException(Piwik::translate('Plugin Zip File Is Empty'));
  107. }
  108. }
  109. private function makeSurePluginJsonExists($tmpPluginFolder)
  110. {
  111. $pluginJsonPath = $this->getPathToPluginJson($tmpPluginFolder);
  112. if (!file_exists($pluginJsonPath)) {
  113. throw new PluginInstallerException('Plugin is not valid, it is missing the plugin.json file.');
  114. }
  115. }
  116. private function makeSureThereAreNoMissingRequirements($metadata)
  117. {
  118. $requires = array();
  119. if(!empty($metadata->require)) {
  120. $requires = (array) $metadata->require;
  121. }
  122. $dependency = new PluginDependency();
  123. $missingDependencies = $dependency->getMissingDependencies($requires);
  124. if (!empty($missingDependencies)) {
  125. $message = '';
  126. foreach ($missingDependencies as $dep) {
  127. $params = array(ucfirst($dep['requirement']), $dep['actualVersion'], $dep['requiredVersion']);
  128. $message .= Piwik::translate('CorePluginsAdmin_MissingRequirementsNotice', $params);
  129. }
  130. throw new PluginInstallerException($message);
  131. }
  132. }
  133. private function getPluginMetadataIfValid($tmpPluginFolder)
  134. {
  135. $pluginJsonPath = $this->getPathToPluginJson($tmpPluginFolder);
  136. $metadata = file_get_contents($pluginJsonPath);
  137. $metadata = json_decode($metadata);
  138. if (empty($metadata)) {
  139. throw new PluginInstallerException('Plugin is not valid, plugin.json is empty or does not contain valid JSON.');
  140. }
  141. if (empty($metadata->name)) {
  142. throw new PluginInstallerException('Plugin is not valid, the plugin.json file does not specify the plugin name.');
  143. }
  144. if (!preg_match('/^[a-zA-Z0-9_-]+$/', $metadata->name)) {
  145. throw new PluginInstallerException('The plugin name specified in plugin.json contains illegal characters. ' .
  146. 'Plugin name can only contain following characters: [a-zA-Z0-9-_].');
  147. }
  148. if (empty($metadata->version)) {
  149. throw new PluginInstallerException('Plugin is not valid, the plugin.json file does not specify the plugin version.');
  150. }
  151. if (empty($metadata->description)) {
  152. throw new PluginInstallerException('Plugin is not valid, the plugin.json file does not specify a description.');
  153. }
  154. return $metadata;
  155. }
  156. private function getPathToPluginJson($tmpPluginFolder)
  157. {
  158. $firstSubFolder = $this->getNameOfFirstSubfolder($tmpPluginFolder);
  159. $path = $tmpPluginFolder . DIRECTORY_SEPARATOR . $firstSubFolder . DIRECTORY_SEPARATOR . 'plugin.json';
  160. return $path;
  161. }
  162. /**
  163. * @param $pluginDir
  164. * @throws PluginInstallerException
  165. * @return string
  166. */
  167. private function getNameOfFirstSubfolder($pluginDir)
  168. {
  169. if (!($dir = opendir($pluginDir))) {
  170. return false;
  171. }
  172. $firstSubFolder = '';
  173. while ($file = readdir($dir)) {
  174. if ($file[0] != '.' && is_dir($pluginDir . DIRECTORY_SEPARATOR . $file)) {
  175. $firstSubFolder = $file;
  176. break;
  177. }
  178. }
  179. if (empty($firstSubFolder)) {
  180. 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.');
  181. }
  182. return $firstSubFolder;
  183. }
  184. private function fixPluginFolderIfNeeded($tmpPluginFolder)
  185. {
  186. $firstSubFolder = $this->getNameOfFirstSubfolder($tmpPluginFolder);
  187. if ($firstSubFolder === $this->pluginName) {
  188. return;
  189. }
  190. $from = $tmpPluginFolder . DIRECTORY_SEPARATOR . $firstSubFolder;
  191. $to = $tmpPluginFolder . DIRECTORY_SEPARATOR . $this->pluginName;
  192. rename($from, $to);
  193. }
  194. private function copyPluginToDestination($tmpPluginFolder)
  195. {
  196. $pluginTargetPath = PIWIK_USER_PATH . self::PATH_TO_EXTRACT . $this->pluginName;
  197. $this->removeFolderIfExists($pluginTargetPath);
  198. Filesystem::copyRecursive($tmpPluginFolder, PIWIK_USER_PATH . self::PATH_TO_EXTRACT);
  199. }
  200. /**
  201. * @param $pathExtracted
  202. */
  203. private function removeFolderIfExists($pathExtracted)
  204. {
  205. Filesystem::unlinkRecursive($pathExtracted, true);
  206. }
  207. /**
  208. * @param $targetTmpFile
  209. */
  210. private function removeFileIfExists($targetTmpFile)
  211. {
  212. if (file_exists($targetTmpFile)) {
  213. unlink($targetTmpFile);
  214. }
  215. }
  216. /**
  217. * @throws PluginInstallerException
  218. */
  219. private function makeSurePluginNameIsValid()
  220. {
  221. try {
  222. $marketplace = new MarketplaceApiClient();
  223. $pluginDetails = $marketplace->getPluginInfo($this->pluginName);
  224. } catch (\Exception $e) {
  225. throw new PluginInstallerException($e->getMessage());
  226. }
  227. if (empty($pluginDetails)) {
  228. throw new PluginInstallerException('This plugin was not found in the Marketplace.');
  229. }
  230. }
  231. }