updater = new Updater;
$this->pluginManager = PluginManager::instance();
}
/**
* Updates a single plugin by its code or object with it's latest changes.
* If the $stopOnVersion parameter is specified, the process stops after
* the specified version is applied.
*/
public function updatePlugin($plugin, $stopOnVersion = null)
{
$code = is_string($plugin) ? $plugin : $this->pluginManager->getIdentifier($plugin);
if (!$this->hasVersionFile($code)) {
return false;
}
$currentVersion = $this->getLatestFileVersion($code);
$databaseVersion = $this->getDatabaseVersion($code);
// No updates needed
if ($currentVersion == $databaseVersion) {
$this->note(' - Nothing to update.');
return;
}
$newUpdates = $this->getNewFileVersions($code, $databaseVersion);
foreach ($newUpdates as $version => $details) {
$this->applyPluginUpdate($code, $version, $details);
if ($stopOnVersion === $version) {
return true;
}
}
return true;
}
/**
* Update the current replaced plugin's version to reference the replacing plugin.
*/
public function replacePlugin(PluginBase $plugin, string $replace)
{
$currentVersion = $this->getDatabaseVersion($replace);
if ($currentVersion === self::NO_VERSION_VALUE) {
return;
}
// We only care about the database version of the replaced plugin at this point
if (!$plugin->canReplacePlugin($replace, $currentVersion)) {
return;
}
$code = $plugin->getPluginIdentifier();
// add history up to $currentVersion
if ($versions = $this->getOldFileVersions($code, $currentVersion)) {
foreach ($versions as $version => $details) {
list($comments, $scripts) = $this->extractScriptsAndComments($details);
$now = now()->toDateTimeString();
foreach ($scripts as $script) {
Db::table('system_plugin_history')->insert([
'code' => $code,
'type' => self::HISTORY_TYPE_SCRIPT,
'version' => $version,
'detail' => $script,
'created_at' => $now,
]);
}
foreach ($comments as $comment) {
$this->applyDatabaseComment($code, $version, $comment);
}
}
}
// delete replaced plugin history
Db::table('system_plugin_history')->where('code', $replace)->delete();
// replace installed version
Db::table('system_plugin_versions')
->where('code', '=', $replace)
->update([
'code' => $code
]);
}
/**
* Returns a list of unapplied plugin versions.
*/
public function listNewVersions($plugin)
{
$code = is_string($plugin) ? $plugin : $this->pluginManager->getIdentifier($plugin);
if (!$this->hasVersionFile($code)) {
return [];
}
$databaseVersion = $this->getDatabaseVersion($code);
return $this->getNewFileVersions($code, $databaseVersion);
}
/**
* Applies a single version update to a plugin.
*/
protected function applyPluginUpdate($code, $version, $details)
{
list($comments, $scripts) = $this->extractScriptsAndComments($details);
/*
* Apply scripts, if any
*/
foreach ($scripts as $script) {
if ($this->hasDatabaseHistory($code, $version, $script)) {
continue;
}
$this->applyDatabaseScript($code, $version, $script);
}
/*
* Register the comment and update the version
*/
if (!$this->hasDatabaseHistory($code, $version)) {
foreach ($comments as $comment) {
$this->applyDatabaseComment($code, $version, $comment);
$this->note(sprintf(' - v%s: %s', $version, $comment));
}
}
$this->setDatabaseVersion($code, $version);
}
/**
* 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, $stopCurrentVersion = false)
{
$code = is_string($plugin) ? $plugin : $this->pluginManager->getIdentifier($plugin);
if (!$this->hasVersionFile($code)) {
return false;
}
$pluginHistory = $this->getDatabaseHistory($code);
$pluginHistory = array_reverse($pluginHistory);
$stopOnNextVersion = false;
$newPluginVersion = null;
try {
foreach ($pluginHistory as $history) {
if ($stopCurrentVersion && $stopOnVersion === $history->version) {
$newPluginVersion = $history->version;
break;
}
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 ($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);
if (isset($this->fileVersions[$code])) {
unset($this->fileVersions[$code]);
}
if (isset($this->databaseVersions[$code])) {
unset($this->databaseVersions[$code]);
}
if (isset($this->databaseHistory[$code])) {
unset($this->databaseHistory[$code]);
}
return true;
}
/**
* Deletes all records from the version and history tables for a plugin.
* @param string $pluginCode Plugin code
* @return void
*/
public function purgePlugin($pluginCode)
{
$versions = Db::table('system_plugin_versions')->where('code', $pluginCode);
if ($countVersions = $versions->count()) {
$versions->delete();
}
$history = Db::table('system_plugin_history')->where('code', $pluginCode);
if ($countHistory = $history->count()) {
$history->delete();
}
return ($countHistory + $countVersions) > 0;
}
//
// File representation
//
/**
* Returns the latest version of a plugin from its version file.
*/
protected function getLatestFileVersion($code)
{
$versionInfo = $this->getFileVersions($code);
if (!$versionInfo) {
return self::NO_VERSION_VALUE;
}
return trim(key(array_slice($versionInfo, -1, 1)));
}
/**
* Returns older versions up to a supplied version, ie. applied versions.
*/
protected function getOldFileVersions($code, $version = null)
{
if ($version === null) {
$version = self::NO_VERSION_VALUE;
}
$versions = $this->getFileVersions($code);
$position = array_search($version, array_keys($versions));
return array_slice($versions, 0, ++$position);
}
/**
* Returns any new versions from a supplied version, ie. unapplied versions.
*/
protected function getNewFileVersions($code, $version = null)
{
if ($version === null) {
$version = self::NO_VERSION_VALUE;
}
$versions = $this->getFileVersions($code);
$position = array_search($version, array_keys($versions));
return array_slice($versions, ++$position);
}
/**
* Returns all versions of a plugin from its version file.
*/
protected function getFileVersions($code)
{
if ($this->fileVersions !== null && array_key_exists($code, $this->fileVersions)) {
return $this->fileVersions[$code];
}
$versionFile = $this->getVersionFile($code);
$versionInfo = Yaml::parseFile($versionFile);
if (!is_array($versionInfo)) {
$versionInfo = [];
}
if ($versionInfo) {
uksort($versionInfo, function ($a, $b) {
return version_compare($a, $b);
});
}
return $this->fileVersions[$code] = $versionInfo;
}
/**
* Returns the absolute path to a version file for a plugin.
*/
protected function getVersionFile($code)
{
$versionFile = $this->pluginManager->getPluginPath($code) . '/updates/version.yaml';
return $versionFile;
}
/**
* Checks if a plugin has a version file.
*/
protected function hasVersionFile($code)
{
$versionFile = $this->getVersionFile($code);
return File::isFile($versionFile);
}
//
// Database representation
//
/**
* Returns the latest version of a plugin from the database.
*/
protected function getDatabaseVersion($code)
{
if ($this->databaseVersions === null) {
$this->databaseVersions = Db::table('system_plugin_versions')->lists('version', 'code');
}
if (!isset($this->databaseVersions[$code])) {
$this->databaseVersions[$code] = Db::table('system_plugin_versions')
->where('code', $code)
->value('version');
}
return $this->databaseVersions[$code] ?? self::NO_VERSION_VALUE;
}
/**
* Updates a plugin version in the database.
*/
protected function setDatabaseVersion($code, $version = null)
{
$currentVersion = $this->getDatabaseVersion($code);
if ($version && !$currentVersion) {
Db::table('system_plugin_versions')->insert([
'code' => $code,
'version' => $version,
'created_at' => new Carbon
]);
} elseif ($version && $currentVersion) {
Db::table('system_plugin_versions')->where('code', $code)->update([
'version' => $version,
'created_at' => new Carbon
]);
} elseif ($currentVersion) {
Db::table('system_plugin_versions')->where('code', $code)->delete();
}
$this->databaseVersions[$code] = $version;
}
/**
* Registers a database update comment in the history table.
*/
protected function applyDatabaseComment($code, $version, $comment)
{
Db::table('system_plugin_history')->insert([
'code' => $code,
'type' => self::HISTORY_TYPE_COMMENT,
'version' => $version,
'detail' => $comment,
'created_at' => new Carbon
]);
}
/**
* Removes a database update comment in the history table.
*/
protected function removeDatabaseComment($code, $version)
{
Db::table('system_plugin_history')
->where('code', $code)
->where('type', self::HISTORY_TYPE_COMMENT)
->where('version', $version)
->delete();
}
/**
* Registers a database update script in the history table.
*/
protected function applyDatabaseScript($code, $version, $script)
{
/*
* Execute the database PHP script
*/
$updateFile = $this->pluginManager->getPluginPath($code) . '/updates/' . $script;
if (!File::isFile($updateFile)) {
$this->note('- v' . $version . ': Migration file "' . $script . '" not found');
return;
}
$this->updater->setUp($updateFile);
Db::table('system_plugin_history')->insert([
'code' => $code,
'type' => self::HISTORY_TYPE_SCRIPT,
'version' => $version,
'detail' => $script,
'created_at' => new Carbon
]);
}
/**
* Removes a database update script in the history table.
*/
protected function removeDatabaseScript($code, $version, $script)
{
/*
* Execute the database PHP script
*/
$updateFile = $this->pluginManager->getPluginPath($code) . '/updates/' . $script;
$this->updater->packDown($updateFile);
Db::table('system_plugin_history')
->where('code', $code)
->where('type', self::HISTORY_TYPE_SCRIPT)
->where('version', $version)
->where('detail', $script)
->delete();
}
/**
* Returns all the update history for a plugin.
*/
protected function getDatabaseHistory($code)
{
if ($this->databaseHistory !== null && array_key_exists($code, $this->databaseHistory)) {
return $this->databaseHistory[$code];
}
$historyInfo = Db::table('system_plugin_history')
->where('code', $code)
->orderBy('id')
->get()
->all();
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.
*/
protected function hasDatabaseHistory($code, $version, $script = null)
{
$historyInfo = $this->getDatabaseHistory($code);
if (!$historyInfo) {
return false;
}
foreach ($historyInfo as $history) {
if ($history->version != $version) {
continue;
}
if ($history->type == self::HISTORY_TYPE_COMMENT && !$script) {
return true;
}
if ($history->type == self::HISTORY_TYPE_SCRIPT && $history->detail == $script) {
return true;
}
}
return false;
}
//
// Notes
//
/**
* Raise a note event for the migrator.
* @param string $message
* @return void
*/
protected function note($message)
{
if ($this->notesOutput !== null) {
$this->notesOutput->writeln($message);
}
return $this;
}
/**
* Sets an output stream for writing notes.
* @param Illuminate\Console\Command $output
* @return self
*/
public function setNotesOutput($output)
{
$this->notesOutput = $output;
return $this;
}
/**
* Extract script and comments from version details
* @return array
*/
protected function extractScriptsAndComments($details): array
{
if (is_array($details)) {
$fileNamePattern = "/^[a-z0-9\_\-\.\/\\\]+\.php$/i";
$comments = array_values(array_filter($details, function ($detail) use ($fileNamePattern) {
return !preg_match($fileNamePattern, $detail);
}));
$scripts = array_values(array_filter($details, function ($detail) use ($fileNamePattern) {
return preg_match($fileNamePattern, $detail);
}));
}
else {
$comments = (array)$details;
$scripts = [];
}
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 : '';
}
}