Added plugin:rollback command (#4389)

This commit is contained in:
Kallef Alexandre 2020-04-11 12:46:22 -03:00 committed by GitHub
parent d69ade86d1
commit b4c65e9feb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 259 additions and 89 deletions

View File

@ -253,6 +253,7 @@ class ServiceProvider extends ModuleServiceProvider
$this->registerConsoleCommand('plugin.disable', 'System\Console\PluginDisable');
$this->registerConsoleCommand('plugin.enable', 'System\Console\PluginEnable');
$this->registerConsoleCommand('plugin.refresh', 'System\Console\PluginRefresh');
$this->registerConsoleCommand('plugin.rollback', 'System\Console\PluginRollback');
$this->registerConsoleCommand('plugin.list', 'System\Console\PluginList');
$this->registerConsoleCommand('theme.install', 'System\Console\ThemeInstall');

View File

@ -50,17 +50,17 @@ class UpdateManager
protected $tempDirectory;
/**
* @var System\Classes\PluginManager
* @var \System\Classes\PluginManager
*/
protected $pluginManager;
/**
* @var Cms\Classes\ThemeManager
* @var \Cms\Classes\ThemeManager
*/
protected $themeManager;
/**
* @var System\Classes\VersionManager
* @var \System\Classes\VersionManager
*/
protected $versionManager;
@ -85,12 +85,12 @@ class UpdateManager
protected $productCache;
/**
* @var Illuminate\Database\Migrations\Migrator
* @var \Illuminate\Database\Migrations\Migrator
*/
protected $migrator;
/**
* @var Illuminate\Database\Migrations\DatabaseMigrationRepository
* @var \Illuminate\Database\Migrations\DatabaseMigrationRepository
*/
protected $repository;
@ -173,7 +173,7 @@ class UpdateManager
/**
* Checks for new updates and returns the amount of unapplied updates.
* Only requests from the server at a set interval (retry timer).
* @param boolean $force Ignore the retry timer.
* @param boolean $force Ignore the retry timer.
* @return int Number of unapplied updates.
*/
public function check($force = false)
@ -199,8 +199,7 @@ class UpdateManager
try {
$result = $this->requestUpdateList();
$newCount = array_get($result, 'update', 0);
}
catch (Exception $ex) {
} catch (Exception $ex) {
$newCount = 0;
}
@ -215,7 +214,7 @@ class UpdateManager
/**
* Requests an update list used for checking for new updates.
* @param boolean $force Request application and plugins hash list regardless of version.
* @param boolean $force Request application and plugins hash list regardless of version.
* @return array
*/
public function requestUpdateList($force = false)
@ -234,11 +233,11 @@ class UpdateManager
}
$params = [
'core' => $this->getHash(),
'core' => $this->getHash(),
'plugins' => serialize($versions),
'themes' => serialize($themes),
'build' => $build,
'force' => $force
'themes' => serialize($themes),
'build' => $build,
'force' => $force
];
$result = $this->requestServerData('core/update', $params);
@ -270,8 +269,7 @@ class UpdateManager
(isset($updatable[$code]) && !$updatable[$code])
) {
$updateCount = max(0, --$updateCount);
}
else {
} else {
$plugins[$code] = $info;
}
}
@ -312,7 +310,7 @@ class UpdateManager
/**
* Requests details about a project based on its identifier.
* @param string $projectId
* @param string $projectId
* @return array
*/
public function requestProjectDetails($projectId)
@ -341,7 +339,7 @@ class UpdateManager
$modules = Config::get('cms.loadModules', []);
foreach ($modules as $module) {
$paths[] = $path = base_path() . '/modules/'.strtolower($module).'/database/migrations';
$paths[] = $path = base_path() . '/modules/' . strtolower($module) . '/database/migrations';
}
/*
@ -405,12 +403,12 @@ class UpdateManager
*/
public function migrateModule($module)
{
$this->migrator->run(base_path() . '/modules/'.strtolower($module).'/database/migrations');
$this->migrator->run(base_path() . '/modules/' . strtolower($module) . '/database/migrations');
$this->note($module);
foreach ($this->migrator->getNotes() as $note) {
$this->note(' - '.$note);
$this->note(' - ' . $note);
}
return $this;
@ -423,7 +421,7 @@ class UpdateManager
*/
public function seedModule($module)
{
$className = '\\'.$module.'\Database\Seeds\DatabaseSeeder';
$className = '\\' . $module . '\Database\Seeds\DatabaseSeeder';
if (!class_exists($className)) {
return;
}
@ -532,11 +530,13 @@ class UpdateManager
}
/**
* Removes an existing plugin
* Rollback an existing plugin
*
* @param string $name Plugin name.
* @param string $stopOnVersion If this parameter is specified, the process stops once the provided version number is reached
* @return self
*/
public function rollbackPlugin($name)
public function rollbackPlugin(string $name, string $stopOnVersion = null)
{
/*
* Remove the plugin database and version
@ -548,8 +548,17 @@ class UpdateManager
return $this;
}
if ($this->versionManager->removePlugin($plugin)) {
if ($stopOnVersion && !$this->versionManager->hasDatabaseVersion($plugin, $stopOnVersion)) {
throw new ApplicationException(Lang::get('system::lang.updates.plugin_version_not_found'));
}
if ($this->versionManager->removePlugin($plugin, $stopOnVersion, true)) {
$this->note('<info>Rolled back:</info> ' . $name);
if ($currentVersion = $this->versionManager->getCurrentVersion($plugin)) {
$this->note('<info>Current Version:</info> ' . $currentVersion . ' (' . $this->versionManager->getCurrentVersionNote($plugin) . ')');
}
return $this;
}
@ -569,7 +578,7 @@ class UpdateManager
{
$fileCode = $name . $hash;
$this->requestServerFile('plugin/get', $fileCode, $hash, [
'name' => $name,
'name' => $name,
'installation' => $installation ? 1 : 0
]);
}
@ -654,7 +663,7 @@ class UpdateManager
$newCodes = array_diff($codes, array_keys($this->productCache[$type]));
if (count($newCodes)) {
$dataCodes = [];
$data = $this->requestServerData($type.'/details', ['names' => $newCodes]);
$data = $this->requestServerData($type . '/details', ['names' => $newCodes]);
foreach ($data as $product) {
$code = array_get($product, 'code', -1);
$this->cacheProductDetail($type, $code, $product);
@ -697,13 +706,13 @@ class UpdateManager
$type = 'plugin';
}
$cacheKey = 'system-updates-popular-'.$type;
$cacheKey = 'system-updates-popular-' . $type;
if (Cache::has($cacheKey)) {
return @unserialize(@base64_decode(Cache::get($cacheKey))) ?: [];
}
$data = $this->requestServerData($type.'/popular');
$data = $this->requestServerData($type . '/popular');
Cache::put($cacheKey, base64_encode(serialize($data)), 60);
foreach ($data as $product) {
@ -723,8 +732,7 @@ class UpdateManager
if (Cache::has($cacheKey)) {
$this->productCache = @unserialize(@base64_decode(Cache::get($cacheKey))) ?: $defaultCache;
}
else {
} else {
$this->productCache = $defaultCache;
}
}
@ -774,8 +782,7 @@ class UpdateManager
try {
$resultData = json_decode($result->body, true);
}
catch (Exception $ex) {
} catch (Exception $ex) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}
@ -788,15 +795,14 @@ class UpdateManager
/**
* Raise a note event for the migrator.
* @param string $message
* @param string $message
* @return self
*/
protected function note($message)
{
if ($this->notesOutput !== null) {
$this->notesOutput->writeln($message);
}
else {
} else {
$this->notes[] = $message;
}
@ -827,7 +833,7 @@ class UpdateManager
/**
* Sets an output stream for writing notes.
* @param Illuminate\Console\Command $output
* @param Illuminate\Console\Command $output
* @return self
*/
public function setNotesOutput($output)
@ -843,8 +849,8 @@ class UpdateManager
/**
* Contacts the update server for a response.
* @param string $uri Gateway API URI
* @param array $postData Extra post data
* @param string $uri Gateway API URI
* @param array $postData Extra post data
* @return array
*/
public function requestServerData($uri, $postData = [])
@ -869,8 +875,7 @@ class UpdateManager
try {
$resultData = @json_decode($result->body, true);
}
catch (Exception $ex) {
} catch (Exception $ex) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}
@ -883,10 +888,10 @@ class UpdateManager
/**
* Downloads a file from the update server.
* @param string $uri Gateway API URI
* @param string $fileCode A unique code for saving the file.
* @param string $expectedHash The expected file hash of the file.
* @param array $postData Extra post data
* @param string $uri Gateway API URI
* @param string $fileCode A unique code for saving the file.
* @param string $expectedHash The expected file hash of the file.
* @param array $postData Extra post data
* @return void
*/
public function requestServerFile($uri, $fileCode, $expectedHash, $postData = [])
@ -910,7 +915,7 @@ class UpdateManager
/**
* Calculates a file path for a file code
* @param string $fileCode A unique file code
* @param string $fileCode A unique file code
* @return string Full path on the disk
*/
protected function getFilePath($fileCode)
@ -921,7 +926,7 @@ class UpdateManager
/**
* Set the API security for all transmissions.
* @param string $key API Key
* @param string $key API Key
* @param string $secret API Secret
*/
public function setSecurity($key, $secret)
@ -932,7 +937,7 @@ class UpdateManager
/**
* Create a complete gateway server URL from supplied URI
* @param string $uri URI
* @param string $uri URI
* @return string URL
*/
protected function createServerUrl($uri)
@ -947,8 +952,8 @@ class UpdateManager
/**
* Modifies the Network HTTP object with common attributes.
* @param Http $http Network object
* @param array $postData Post data
* @param Http $http Network object
* @param array $postData Post data
* @return void
*/
protected function applyHttpAttributes($http, $postData)

View File

@ -157,8 +157,13 @@ class VersionManager
* Removes and packs down a plugin from the system. Files are left intact.
* If the $stopOnVersion parameter is specified, the process stops after
* the specified version is rolled back.
*
* @param mixed $plugin Either the identifier of a plugin as a string, or a Plugin class.
* @param string $stopOnVersion
* @param bool $stopCurrentVersion
* @return bool
*/
public function removePlugin($plugin, $stopOnVersion = null)
public function removePlugin($plugin, $stopOnVersion = null, $stopCurrentVersion = false)
{
$code = is_string($plugin) ? $plugin : $this->pluginManager->getIdentifier($plugin);
@ -172,25 +177,37 @@ class VersionManager
$stopOnNextVersion = false;
$newPluginVersion = null;
foreach ($pluginHistory as $history) {
if ($stopOnNextVersion && $history->version !== $stopOnVersion) {
// Stop if the $stopOnVersion value was found and
// this is a new version. The history could contain
// multiple items for a single version (comments and scripts).
$newPluginVersion = $history->version;
break;
}
try {
foreach ($pluginHistory as $history) {
if ($stopCurrentVersion && $stopOnVersion === $history->version) {
$newPluginVersion = $history->version;
break;
}
if ($history->type == self::HISTORY_TYPE_COMMENT) {
$this->removeDatabaseComment($code, $history->version);
}
elseif ($history->type == self::HISTORY_TYPE_SCRIPT) {
$this->removeDatabaseScript($code, $history->version, $history->detail);
}
if ($stopOnNextVersion && $history->version !== $stopOnVersion) {
// Stop if the $stopOnVersion value was found and
// this is a new version. The history could contain
// multiple items for a single version (comments and scripts).
$newPluginVersion = $history->version;
break;
}
if ($stopOnVersion === $history->version) {
$stopOnNextVersion = true;
if ($history->type == self::HISTORY_TYPE_COMMENT) {
$this->removeDatabaseComment($code, $history->version);
} elseif ($history->type == self::HISTORY_TYPE_SCRIPT) {
$this->removeDatabaseScript($code, $history->version, $history->detail);
}
if ($stopOnVersion === $history->version) {
$stopOnNextVersion = true;
}
}
} catch (\Exception $exception) {
$lastHistory = $this->getLastHistory($code);
if ($lastHistory) {
$this->setDatabaseVersion($code, $lastHistory->version);
}
throw $exception;
}
$this->setDatabaseVersion($code, $newPluginVersion);
@ -209,7 +226,7 @@ class VersionManager
/**
* Deletes all records from the version and history tables for a plugin.
* @param string $pluginCode Plugin code
* @param string $pluginCode Plugin code
* @return void
*/
public function purgePlugin($pluginCode)
@ -317,8 +334,7 @@ class VersionManager
if (!isset($this->databaseVersions[$code])) {
$this->databaseVersions[$code] = Db::table('system_plugin_versions')
->where('code', $code)
->value('version')
;
->value('version');
}
return $this->databaseVersions[$code] ?? self::NO_VERSION_VALUE;
@ -333,18 +349,16 @@ class VersionManager
if ($version && !$currentVersion) {
Db::table('system_plugin_versions')->insert([
'code' => $code,
'version' => $version,
'code' => $code,
'version' => $version,
'created_at' => new Carbon
]);
}
elseif ($version && $currentVersion) {
} elseif ($version && $currentVersion) {
Db::table('system_plugin_versions')->where('code', $code)->update([
'version' => $version,
'version' => $version,
'created_at' => new Carbon
]);
}
elseif ($currentVersion) {
} elseif ($currentVersion) {
Db::table('system_plugin_versions')->where('code', $code)->delete();
}
@ -357,10 +371,10 @@ class VersionManager
protected function applyDatabaseComment($code, $version, $comment)
{
Db::table('system_plugin_history')->insert([
'code' => $code,
'type' => self::HISTORY_TYPE_COMMENT,
'version' => $version,
'detail' => $comment,
'code' => $code,
'type' => self::HISTORY_TYPE_COMMENT,
'version' => $version,
'detail' => $comment,
'created_at' => new Carbon
]);
}
@ -395,10 +409,10 @@ class VersionManager
$this->updater->setUp($updateFile);
Db::table('system_plugin_history')->insert([
'code' => $code,
'type' => self::HISTORY_TYPE_SCRIPT,
'version' => $version,
'detail' => $script,
'code' => $code,
'type' => self::HISTORY_TYPE_SCRIPT,
'version' => $version,
'detail' => $script,
'created_at' => new Carbon
]);
}
@ -440,6 +454,20 @@ class VersionManager
return $this->databaseHistory[$code] = $historyInfo;
}
/**
* Returns the last update history for a plugin.
*
* @param string $code The plugin identifier
* @return stdClass|null
*/
protected function getLastHistory($code)
{
return Db::table('system_plugin_history')
->where('code', $code)
->orderBy('id', 'DESC')
->first();
}
/**
* Checks if a plugin has an applied update version.
*/
@ -473,15 +501,14 @@ class VersionManager
/**
* Raise a note event for the migrator.
* @param string $message
* @param string $message
* @return void
*/
protected function note($message)
{
if ($this->notesOutput !== null) {
$this->notesOutput->writeln($message);
}
else {
} else {
$this->notes[] = $message;
}
@ -512,7 +539,7 @@ class VersionManager
/**
* Sets an output stream for writing notes.
* @param Illuminate\Console\Command $output
* @param Illuminate\Console\Command $output
* @return self
*/
public function setNotesOutput($output)
@ -527,7 +554,7 @@ class VersionManager
*
* @return array
*/
protected function extractScriptsAndComments($details)
protected function extractScriptsAndComments($details): array
{
if (is_array($details)) {
$fileNamePattern = "/^[a-z0-9\_\-\.\/\\\]+\.php$/i";
@ -546,4 +573,52 @@ class VersionManager
return [$comments, $scripts];
}
/**
* Get the currently installed version of the plugin.
*
* @param string|PluginBase $plugin Either the identifier of a plugin as a string, or a Plugin class.
* @return string
*/
public function getCurrentVersion($plugin): string
{
$code = $this->pluginManager->getIdentifier($plugin);
return $this->getDatabaseVersion($code);
}
/**
* Check if a certain version of the plugin exists in the plugin history database.
*
* @param string|PluginBase $plugin Either the identifier of a plugin as a string, or a Plugin class.
* @param string $version
* @return bool
*/
public function hasDatabaseVersion($plugin, string $version): bool
{
$code = $this->pluginManager->getIdentifier($plugin);
$histories = $this->getDatabaseHistory($code);
foreach ($histories as $history) {
if ($history->version === $version) {
return true;
}
}
return false;
}
/**
* Get last version note
*
* @param string|PluginBase $plugin
* @return string
*/
public function getCurrentVersionNote($plugin): string
{
$code = $this->pluginManager->getIdentifier($plugin);
$histories = $this->getDatabaseHistory($code);
$lastHistory = array_last(array_where($histories, function ($history) {
return $history->type === self::HISTORY_TYPE_COMMENT;
}));
return $lastHistory ? $lastHistory->detail : '';
}
}

View File

@ -0,0 +1,88 @@
<?php namespace System\Console;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use System\Classes\UpdateManager;
use System\Classes\PluginManager;
use Symfony\Component\Console\Input\InputArgument;
use System\Classes\VersionManager;
/**
* Console command to rollback a plugin.
*
* @package october\system
* @author Alexey Bobkov, Samuel Georges
*/
class PluginRollback extends Command
{
/**
* The console command name.
* @var string
*/
protected $name = 'plugin:rollback';
/**
* The console command description.
* @var string
*/
protected $description = 'Rollback an existing plugin.';
/**
* Execute the console command.
* @return void
*/
public function handle()
{
/*
* Lookup plugin
*/
$pluginName = $this->argument('name');
$pluginName = PluginManager::instance()->normalizeIdentifier($pluginName);
if (!PluginManager::instance()->exists($pluginName)) {
throw new \InvalidArgumentException('Plugin not found');
}
$stopOnVersion = ltrim(($this->argument('version') ?: null), 'v');
if ($stopOnVersion) {
if (!VersionManager::instance()->hasDatabaseVersion($pluginName, $stopOnVersion)) {
throw new \InvalidArgumentException('Plugin version not found');
}
$confirmQuestion = 'Please confirm that you wish to revert the plugin to version ' . $stopOnVersion . '. This may result in changes to your database and potential data loss.';
} else {
$confirmQuestion = 'Please confirm that you wish to completely rollback this plugin. This may result in potential data loss.';
}
if ($this->option('force') || $this->confirm($confirmQuestion)) {
$manager = UpdateManager::instance()->setNotesOutput($this->output);
$stopOnVersion = ltrim(($this->argument('version') ?: null), 'v');
try {
$manager->rollbackPlugin($pluginName, $stopOnVersion);
} catch (\Exception $exception) {
$lastVersion = VersionManager::instance()->getCurrentVersion($pluginName);
$this->output->writeln(sprintf("<comment>An exception occurred during the rollback and the process has been stopped. The plugin was rolled back to version v%s.</comment>", $lastVersion));
throw $exception;
}
}
}
/**
* Get the console command arguments.
* @return array
*/
protected function getArguments()
{
return [
['name', InputArgument::REQUIRED, 'The name of the plugin to be rolled back. Eg: AuthorName.PluginName'],
['version', InputArgument::OPTIONAL, 'If this parameter is specified, the process will stop on the specified version, if not, it will completely rollback the plugin. Example: 1.3.9'],
];
}
protected function getOptions()
{
return [
['force', 'f', InputOption::VALUE_NONE, 'Force rollback', null],
];
}
}

View File

@ -322,6 +322,7 @@ return [
'plugin_version' => 'Version',
'plugin_author' => 'Author',
'plugin_not_found' => 'Plugin not found',
'plugin_version_not_found' => 'Plugin version not found',
'core_current_build' => 'Current build',
'core_view_changelog' => 'View Changelog',
'core_build' => 'Build :build',

View File

@ -276,7 +276,7 @@ return [
'plugin_description' => 'Descrição',
'plugin_version' => 'Versão',
'plugin_author' => 'Autor',
'plugin_not_found' => 'Plugin encotrado',
'plugin_not_found' => 'Plugin não encontrado',
'plugin_version_not_found' => 'Versão do plugin não encontrada',
'core_current_build' => 'Compilação atual',
'core_view_changelog' => 'Visualizar Changelog',