diff --git a/modules/system/ServiceProvider.php b/modules/system/ServiceProvider.php
index 43aa516e1..9fc858f39 100644
--- a/modules/system/ServiceProvider.php
+++ b/modules/system/ServiceProvider.php
@@ -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');
diff --git a/modules/system/classes/UpdateManager.php b/modules/system/classes/UpdateManager.php
index 0aed46961..7e4779de1 100644
--- a/modules/system/classes/UpdateManager.php
+++ b/modules/system/classes/UpdateManager.php
@@ -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('Rolled back: ' . $name);
+
+ if ($currentVersion = $this->versionManager->getCurrentVersion($plugin)) {
+ $this->note('Current Version: ' . $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)
diff --git a/modules/system/classes/VersionManager.php b/modules/system/classes/VersionManager.php
index 55e8636fe..77127cf12 100644
--- a/modules/system/classes/VersionManager.php
+++ b/modules/system/classes/VersionManager.php
@@ -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 : '';
+ }
}
diff --git a/modules/system/console/PluginRollback.php b/modules/system/console/PluginRollback.php
new file mode 100644
index 000000000..af4d2065c
--- /dev/null
+++ b/modules/system/console/PluginRollback.php
@@ -0,0 +1,88 @@
+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("An exception occurred during the rollback and the process has been stopped. The plugin was rolled back to version v%s.", $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],
+ ];
+ }
+}
diff --git a/modules/system/lang/en/lang.php b/modules/system/lang/en/lang.php
index 760bfde5d..3adf7f6c9 100644
--- a/modules/system/lang/en/lang.php
+++ b/modules/system/lang/en/lang.php
@@ -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',
diff --git a/modules/system/lang/pt-br/lang.php b/modules/system/lang/pt-br/lang.php
index 6f080515c..adbaa043c 100644
--- a/modules/system/lang/pt-br/lang.php
+++ b/modules/system/lang/pt-br/lang.php
@@ -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',