PageRenderTime 26ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/modules/system/classes/VersionManager.php

https://gitlab.com/fbi/october
PHP | 463 lines | 273 code | 69 blank | 121 comment | 40 complexity | 5e0e72640ab8acec508ac0fc2f470350 MD5 | raw file
  1. <?php namespace System\Classes;
  2. use Str;
  3. use File;
  4. use Yaml;
  5. use Db;
  6. use Carbon\Carbon;
  7. use October\Rain\Database\Updater;
  8. /**
  9. * Version manager
  10. *
  11. * Manages the versions and database updates for plugins.
  12. *
  13. * @package october\system
  14. * @author Alexey Bobkov, Samuel Georges
  15. */
  16. class VersionManager
  17. {
  18. use \October\Rain\Support\Traits\Singleton;
  19. /**
  20. * Value when no updates are found.
  21. */
  22. const NO_VERSION_VALUE = 0;
  23. /**
  24. * Morph types for history table.
  25. */
  26. const HISTORY_TYPE_COMMENT = 'comment';
  27. const HISTORY_TYPE_SCRIPT = 'script';
  28. /**
  29. * The notes for the current operation.
  30. * @var array
  31. */
  32. protected $notes = [];
  33. /**
  34. * Cache of plugin versions as files.
  35. */
  36. protected $fileVersions;
  37. /**
  38. * Cache of database versions
  39. */
  40. protected $databaseVersions;
  41. /**
  42. * Cache of database history
  43. */
  44. protected $databaseHistory;
  45. /**
  46. * @var October\Rain\Database\Updater
  47. */
  48. protected $updater;
  49. /**
  50. * @var System\Classes\PluginManager
  51. */
  52. protected $pluginManager;
  53. protected function init()
  54. {
  55. $this->updater = new Updater;
  56. $this->pluginManager = PluginManager::instance();
  57. }
  58. /**
  59. * Updates a single plugin by its code or object with it's latest changes.
  60. */
  61. public function updatePlugin($plugin)
  62. {
  63. $code = (is_string($plugin)) ? $plugin : $this->pluginManager->getIdentifier($plugin);
  64. if (!$this->hasVersionFile($code)) {
  65. return false;
  66. }
  67. $currentVersion = $this->getLatestFileVersion($code);
  68. $databaseVersion = $this->getDatabaseVersion($code);
  69. // No updates needed
  70. if ($currentVersion == $databaseVersion) {
  71. $this->note('<info>Nothing to update.</info>');
  72. return;
  73. }
  74. $newUpdates = $this->getNewFileVersions($code, $databaseVersion);
  75. foreach ($newUpdates as $version => $details) {
  76. $this->applyPluginUpdate($code, $version, $details);
  77. }
  78. return true;
  79. }
  80. /**
  81. * Applies a single version update to a plugin.
  82. */
  83. protected function applyPluginUpdate($code, $version, $details)
  84. {
  85. if (is_array($details)) {
  86. $comment = array_shift($details);
  87. $scripts = $details;
  88. }
  89. else {
  90. $comment = $details;
  91. $scripts = [];
  92. }
  93. /*
  94. * Apply scripts, if any
  95. */
  96. foreach ($scripts as $script) {
  97. if ($this->hasDatabaseHistory($code, $version, $script)) {
  98. continue;
  99. }
  100. $this->applyDatabaseScript($code, $version, $script);
  101. }
  102. /*
  103. * Register the comment and update the version
  104. */
  105. if (!$this->hasDatabaseHistory($code, $version)) {
  106. $this->applyDatabaseComment($code, $version, $comment);
  107. }
  108. $this->setDatabaseVersion($code, $version);
  109. $this->note(sprintf('<info>v%s: </info> %s', $version, $comment));
  110. }
  111. /**
  112. * Removes and packs down a plugin from the system. Files are left intact.
  113. */
  114. public function removePlugin($plugin)
  115. {
  116. $code = (is_string($plugin)) ? $plugin : $this->pluginManager->getIdentifier($plugin);
  117. if (!$this->hasVersionFile($code)) {
  118. return false;
  119. }
  120. $pluginHistory = $this->getDatabaseHistory($code);
  121. $pluginHistory = array_reverse($pluginHistory);
  122. foreach ($pluginHistory as $history) {
  123. if ($history->type == self::HISTORY_TYPE_COMMENT) {
  124. $this->removeDatabaseComment($code, $history->version);
  125. }
  126. elseif ($history->type == self::HISTORY_TYPE_SCRIPT) {
  127. $this->removeDatabaseScript($code, $history->version, $history->detail);
  128. }
  129. }
  130. $this->setDatabaseVersion($code);
  131. if (isset($this->fileVersions[$code])) {
  132. unset($this->fileVersions[$code]);
  133. }
  134. if (isset($this->databaseVersions[$code])) {
  135. unset($this->databaseVersions[$code]);
  136. }
  137. if (isset($this->databaseHistory[$code])) {
  138. unset($this->databaseHistory[$code]);
  139. }
  140. return true;
  141. }
  142. /**
  143. * Deletes all records from the version and history tables for a plugin.
  144. * @param string $pluginCode Plugin code
  145. * @return void
  146. */
  147. public function purgePlugin($pluginCode)
  148. {
  149. $versions = Db::table('system_plugin_versions')->where('code', $pluginCode);
  150. if ($countVersions = $versions->count()) {
  151. $versions->delete();
  152. }
  153. $history = Db::table('system_plugin_history')->where('code', $pluginCode);
  154. if ($countHistory = $history->count()) {
  155. $history->delete();
  156. }
  157. return (($countHistory + $countVersions) > 0) ? true : false;
  158. }
  159. //
  160. // File representation
  161. //
  162. /**
  163. * Returns the latest version of a plugin from its version file.
  164. */
  165. protected function getLatestFileVersion($code)
  166. {
  167. $versionInfo = $this->getFileVersions($code);
  168. if (!$versionInfo) {
  169. return self::NO_VERSION_VALUE;
  170. }
  171. $latest = trim(key(array_slice($versionInfo, -1, 1)));
  172. return $latest;
  173. }
  174. /**
  175. * Returns any new versions from a supplied version, ie. unapplied versions.
  176. */
  177. protected function getNewFileVersions($code, $version = null)
  178. {
  179. if ($version === null) {
  180. $version = self::NO_VERSION_VALUE;
  181. }
  182. $versions = $this->getFileVersions($code);
  183. $position = array_search($version, array_keys($versions));
  184. return array_slice($versions, ++$position);
  185. }
  186. /**
  187. * Returns all versions of a plugin from its version file.
  188. */
  189. protected function getFileVersions($code)
  190. {
  191. if ($this->fileVersions !== null && array_key_exists($code, $this->fileVersions)) {
  192. return $this->fileVersions[$code];
  193. }
  194. $versionFile = $this->getVersionFile($code);
  195. $versionInfo = Yaml::parseFile($versionFile);
  196. if (!is_array($versionInfo)) {
  197. $versionInfo = [];
  198. }
  199. if ($versionInfo) {
  200. uksort($versionInfo, function ($a, $b) {
  201. return version_compare($a, $b);
  202. });
  203. }
  204. return $this->fileVersions[$code] = $versionInfo;
  205. }
  206. /**
  207. * Returns the absolute path to a version file for a plugin.
  208. */
  209. protected function getVersionFile($code)
  210. {
  211. $versionFile = $this->pluginManager->getPluginPath($code) . '/updates/version.yaml';
  212. return $versionFile;
  213. }
  214. /**
  215. * Checks if a plugin has a version file.
  216. */
  217. protected function hasVersionFile($code)
  218. {
  219. $versionFile = $this->getVersionFile($code);
  220. return File::isFile($versionFile);
  221. }
  222. //
  223. // Database representation
  224. //
  225. /**
  226. * Returns the latest version of a plugin from the database.
  227. */
  228. protected function getDatabaseVersion($code)
  229. {
  230. if ($this->databaseVersions === null) {
  231. $this->databaseVersions = Db::table('system_plugin_versions')->lists('version', 'code');
  232. }
  233. if (!isset($this->databaseVersions[$code])) {
  234. $this->databaseVersions[$code] = Db::table('system_plugin_versions')
  235. ->where('code', $code)
  236. ->pluck('version')
  237. ;
  238. }
  239. return (isset($this->databaseVersions[$code]))
  240. ? $this->databaseVersions[$code]
  241. : self::NO_VERSION_VALUE;
  242. }
  243. /**
  244. * Updates a plugin version in the database.
  245. */
  246. protected function setDatabaseVersion($code, $version = null)
  247. {
  248. $currentVersion = $this->getDatabaseVersion($code);
  249. if ($version && !$currentVersion) {
  250. Db::table('system_plugin_versions')->insert([
  251. 'code' => $code,
  252. 'version' => $version,
  253. 'created_at' => new Carbon
  254. ]);
  255. }
  256. elseif ($version && $currentVersion) {
  257. Db::table('system_plugin_versions')->where('code', $code)->update([
  258. 'version' => $version,
  259. 'created_at' => new Carbon
  260. ]);
  261. }
  262. elseif ($currentVersion) {
  263. Db::table('system_plugin_versions')->where('code', $code)->delete();
  264. }
  265. $this->databaseVersions[$code] = $version;
  266. }
  267. /**
  268. * Registers a database update comment in the history table.
  269. */
  270. protected function applyDatabaseComment($code, $version, $comment)
  271. {
  272. Db::table('system_plugin_history')->insert([
  273. 'code' => $code,
  274. 'type' => self::HISTORY_TYPE_COMMENT,
  275. 'version' => $version,
  276. 'detail' => $comment,
  277. 'created_at' => new Carbon
  278. ]);
  279. }
  280. /**
  281. * Removes a database update comment in the history table.
  282. */
  283. protected function removeDatabaseComment($code, $version)
  284. {
  285. Db::table('system_plugin_history')
  286. ->where('code', $code)
  287. ->where('type', self::HISTORY_TYPE_COMMENT)
  288. ->where('version', $version)
  289. ->delete();
  290. }
  291. /**
  292. * Registers a database update script in the history table.
  293. */
  294. protected function applyDatabaseScript($code, $version, $script)
  295. {
  296. /*
  297. * Execute the database PHP script
  298. */
  299. $updateFile = $this->pluginManager->getPluginPath($code) . '/updates/' . $script;
  300. $this->updater->setUp($updateFile);
  301. Db::table('system_plugin_history')->insert([
  302. 'code' => $code,
  303. 'type' => self::HISTORY_TYPE_SCRIPT,
  304. 'version' => $version,
  305. 'detail' => $script,
  306. 'created_at' => new Carbon
  307. ]);
  308. }
  309. /**
  310. * Removes a database update script in the history table.
  311. */
  312. protected function removeDatabaseScript($code, $version, $script)
  313. {
  314. /*
  315. * Execute the database PHP script
  316. */
  317. $updateFile = $this->pluginManager->getPluginPath($code) . '/updates/' . $script;
  318. $this->updater->packDown($updateFile);
  319. Db::table('system_plugin_history')
  320. ->where('code', $code)
  321. ->where('type', self::HISTORY_TYPE_SCRIPT)
  322. ->where('version', $version)
  323. ->where('detail', $script)
  324. ->delete();
  325. }
  326. /**
  327. * Returns all the update history for a plugin.
  328. */
  329. protected function getDatabaseHistory($code)
  330. {
  331. if ($this->databaseHistory !== null && array_key_exists($code, $this->databaseHistory)) {
  332. return $this->databaseHistory[$code];
  333. }
  334. $historyInfo = Db::table('system_plugin_history')->where('code', $code)->get();
  335. if (is_array($historyInfo)) {
  336. usort($historyInfo, function ($a, $b) {
  337. return version_compare($a->version, $b->version);
  338. });
  339. }
  340. return $this->databaseHistory[$code] = $historyInfo;
  341. }
  342. /**
  343. * Checks if a plugin has an applied update version.
  344. */
  345. protected function hasDatabaseHistory($code, $version, $script = null)
  346. {
  347. $historyInfo = $this->getDatabaseHistory($code);
  348. if (!$historyInfo) {
  349. return false;
  350. }
  351. foreach ($historyInfo as $history) {
  352. if ($history->version != $version) {
  353. continue;
  354. }
  355. if ($history->type == self::HISTORY_TYPE_COMMENT && !$script) {
  356. return true;
  357. }
  358. if ($history->type == self::HISTORY_TYPE_SCRIPT && $history->detail == $script) {
  359. return true;
  360. }
  361. }
  362. return false;
  363. }
  364. //
  365. // Notes
  366. //
  367. /**
  368. * Raise a note event for the migrator.
  369. * @param string $message
  370. * @return void
  371. */
  372. protected function note($message)
  373. {
  374. $this->notes[] = $message;
  375. return $this;
  376. }
  377. /**
  378. * Get the notes for the last operation.
  379. * @return array
  380. */
  381. public function getNotes()
  382. {
  383. return $this->notes;
  384. }
  385. /**
  386. * Resets the notes store.
  387. * @return array
  388. */
  389. public function resetNotes()
  390. {
  391. $this->notes = [];
  392. return $this;
  393. }
  394. }