420 lines
11 KiB
Text
420 lines
11 KiB
Text
|
<?php namespace ProcessWire;
|
||
|
|
||
|
/**
|
||
|
* Class SystemUpdater
|
||
|
*
|
||
|
* ProcessWire System Helper Module
|
||
|
*
|
||
|
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||
|
* https://processwire.com
|
||
|
*
|
||
|
* @method coreVersionChange($fromVersion, $toVersion)
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
class SystemUpdater extends WireData implements Module, ConfigurableModule {
|
||
|
|
||
|
public static function getModuleInfo() {
|
||
|
return array(
|
||
|
'title' => __('System Updater', __FILE__), // Module Title
|
||
|
'summary' => __('Manages system versions and upgrades.', __FILE__), // Module Summary
|
||
|
'permanent' => true,
|
||
|
'singular' => true,
|
||
|
'autoload' => false,
|
||
|
|
||
|
/**
|
||
|
* This version number is important, as this updater keeps the systemVersion up with this version
|
||
|
*
|
||
|
*/
|
||
|
'version' => 20,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
protected $configData = array(
|
||
|
// systemVersion generally represents the DB schema version, but
|
||
|
// can represent anything about the system that's related to the individual installation.
|
||
|
// 0 = the first version when this module was created, should remain there.
|
||
|
'systemVersion' => 0,
|
||
|
);
|
||
|
|
||
|
/**
|
||
|
* Number of updates that were applied during this request
|
||
|
*
|
||
|
*/
|
||
|
protected $numUpdatesApplied = 0;
|
||
|
|
||
|
/**
|
||
|
* Get array of updates that were applied
|
||
|
*
|
||
|
* @var array Array of update version numbers
|
||
|
*
|
||
|
*/
|
||
|
protected $updatesApplied = array();
|
||
|
|
||
|
/**
|
||
|
* Is an update being applied manually?
|
||
|
*
|
||
|
* @var bool|int Contains update number when one is being manually applied
|
||
|
*
|
||
|
*/
|
||
|
protected $manualVersion = false;
|
||
|
|
||
|
/**
|
||
|
* Part of the ConfigurableModule interface, sets config data to the module
|
||
|
*
|
||
|
* @param array $data
|
||
|
*
|
||
|
*/
|
||
|
public function setConfigData(array $data) {
|
||
|
$this->configData = array_merge($this->configData, $data);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Perform version checks and update as needed
|
||
|
*
|
||
|
*/
|
||
|
public function init() {
|
||
|
|
||
|
$config = $this->wire()->config;
|
||
|
$info = self::getModuleInfo();
|
||
|
$moduleVersion = $info['version'];
|
||
|
|
||
|
foreach($this->configData as $key => $value) {
|
||
|
if($key == 'coreVersion') continue;
|
||
|
$config->$key = $value;
|
||
|
}
|
||
|
|
||
|
$systemVersion = (int) $config->systemVersion;
|
||
|
|
||
|
if(empty($systemVersion)) {
|
||
|
// double check, just in case (should not be possible for this to occur)
|
||
|
$this->configData = $this->wire()->modules->getModuleConfigData($this);
|
||
|
$systemVersion = (int) isset($this->configData['systemVersion']) ? $this->configData['systemVersion'] : 0;
|
||
|
}
|
||
|
|
||
|
while($systemVersion < $moduleVersion) {
|
||
|
|
||
|
// apply the incremental version update
|
||
|
if(!$this->update($systemVersion+1)) break;
|
||
|
|
||
|
// we increment the config systemVersion so that the version is also available to the updater
|
||
|
$systemVersion++;
|
||
|
|
||
|
// we save the configData for every version in case an update throws an exception
|
||
|
// then already applied updates won't be applied again
|
||
|
$this->saveSystemVersion($systemVersion);
|
||
|
$this->numUpdatesApplied++;
|
||
|
$this->updatesApplied[] = ($systemVersion-1);
|
||
|
}
|
||
|
|
||
|
if($this->numUpdatesApplied > 0) {
|
||
|
// if updates were applied, reset the modules cache
|
||
|
$this->modules->resetCache();
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called after ProcessWire API ready
|
||
|
*
|
||
|
*/
|
||
|
public function ready() {
|
||
|
static $called = false;
|
||
|
if($called) return; // just in case we add auto-ready support to non-autoload modules
|
||
|
|
||
|
if($this->wire()->page->template != 'admin') return;
|
||
|
|
||
|
$config = $this->wire()->config;
|
||
|
if($config->ajax) return;
|
||
|
|
||
|
$coreVersion = isset($this->configData['coreVersion']) ? $this->configData['coreVersion'] : '';
|
||
|
$configVersion = $config->version;
|
||
|
if($coreVersion != $configVersion) $this->coreVersionChange($coreVersion, $configVersion);
|
||
|
$called = true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Hook called when the core version changes, in case any listeners want it
|
||
|
*
|
||
|
* Note that version change is only detected when a page from the admin is viewed.
|
||
|
* To hook this, hook to "SystemUpdater::coreVersionChange"
|
||
|
*
|
||
|
* @param string $fromVersion
|
||
|
* @param string $toVersion
|
||
|
*
|
||
|
*/
|
||
|
protected function ___coreVersionChange($fromVersion, $toVersion) {
|
||
|
|
||
|
$modules = $this->wire()->modules;
|
||
|
$session = $this->wire()->session;
|
||
|
$config = $this->wire()->config;
|
||
|
|
||
|
$this->message(sprintf($this->_('Detected core version change %1$s => %2$s'), $fromVersion, $toVersion));
|
||
|
|
||
|
if( (strpos($fromVersion, '2') === 0 && strpos($toVersion, '3') === 0) ||
|
||
|
(strpos($fromVersion, '3') === 0 && strpos($toVersion, '2') === 0)) {
|
||
|
// clear FileCompiler cache
|
||
|
if($config->templateCompile || $config->moduleCompile) {
|
||
|
/** @var FileCompiler $compiler */
|
||
|
$compiler = $this->wire(new FileCompiler($config->paths->templates));
|
||
|
$compiler->clearCache(true);
|
||
|
$this->message($this->_('Cleared file compiler cache'));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(!$this->numUpdatesApplied) {
|
||
|
// reset modules cache, only if it hasn't been reset already by a system update
|
||
|
$modules->resetCache();
|
||
|
}
|
||
|
|
||
|
$this->configData['coreVersion'] = $toVersion;
|
||
|
$modules->saveModuleConfigData($this, $this->configData);
|
||
|
|
||
|
// remove admin theme cached info in session
|
||
|
foreach($session as $key => $value) {
|
||
|
if(strpos($key, 'AdminTheme') === 0) {
|
||
|
$session->remove($key);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Save the system version as the given version number
|
||
|
*
|
||
|
* @param int $version
|
||
|
* @return bool
|
||
|
*
|
||
|
*/
|
||
|
public function saveSystemVersion($version) {
|
||
|
if($this->manualVersion == $version) return false;
|
||
|
$config = $this->wire()->config;
|
||
|
$version = (int) $version;
|
||
|
$config->systemVersion = $version;
|
||
|
$this->configData['systemVersion'] = $version;
|
||
|
$this->configData['coreVersion'] = $config->version;
|
||
|
$this->wire()->modules->saveModuleConfigData($this, $this->configData);
|
||
|
$this->message("Update #$version: Completed!");
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check for an update file in the format: SystemUpdater123 where '123' is the version it upgrades to
|
||
|
*
|
||
|
* If found, instantiate the class and its constructor should perform the update or add any hooks necessary to perform the update
|
||
|
*
|
||
|
* @param int $version
|
||
|
* @return bool
|
||
|
*
|
||
|
*/
|
||
|
protected function update($version) {
|
||
|
|
||
|
$errorMessage = sprintf('Failed to apply update %d', $version);
|
||
|
$update = null;
|
||
|
|
||
|
try {
|
||
|
$update = $this->getUpdate($version);
|
||
|
if(!$update) return true;
|
||
|
$update->message('Initializing update');
|
||
|
$success = $update->execute();
|
||
|
if($success === false) $update->error($errorMessage);
|
||
|
} catch(\Exception $e) {
|
||
|
$msg = $errorMessage . " - " . $e->getMessage();
|
||
|
$messenger = $update ? $update : $this;
|
||
|
$messenger->error($msg);
|
||
|
$success = false;
|
||
|
}
|
||
|
|
||
|
return $success;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get a specific SystemUpdate class instance by version number and return it (without executing it)
|
||
|
*
|
||
|
* @param int $version Update version number
|
||
|
* @return null|SystemUpdate Returns SystemUpdate instance of available or null if not
|
||
|
* @since 3.0.135
|
||
|
*
|
||
|
*/
|
||
|
public function getUpdate($version) {
|
||
|
|
||
|
$path = dirname(__FILE__) . '/';
|
||
|
|
||
|
require_once($path . 'SystemUpdate.php');
|
||
|
|
||
|
$className = 'SystemUpdate' . $version;
|
||
|
$filename = $path . $className . '.php';
|
||
|
|
||
|
if(!is_file($filename)) return null;
|
||
|
|
||
|
require_once($filename);
|
||
|
|
||
|
$className = wireClassName($className, true);
|
||
|
|
||
|
/** @var SystemUpdate $update */
|
||
|
$update = $this->wire(new $className($this));
|
||
|
|
||
|
return $update;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Manually apply a update
|
||
|
*
|
||
|
* The system version is not changed when applying an update manually.
|
||
|
*
|
||
|
* @param int|SystemUpdate $version Update version number or instance of SystemUpdate you want to apply
|
||
|
* @return bool True on success, false on fail
|
||
|
* @since 3.0.135
|
||
|
*
|
||
|
*/
|
||
|
public function apply($version) {
|
||
|
|
||
|
if(is_object($version)) {
|
||
|
$update = $version;
|
||
|
$version = $update->getVersion();
|
||
|
} else {
|
||
|
$update = null;
|
||
|
$version = (int) $version;
|
||
|
}
|
||
|
|
||
|
$this->manualVersion = $version;
|
||
|
|
||
|
try {
|
||
|
if(!$update) $update = $this->getUpdate($version);
|
||
|
$success = $update ? $update->execute() : true;
|
||
|
} catch(\Exception $e) {
|
||
|
$this->error($e->getMessage());
|
||
|
$success = false;
|
||
|
}
|
||
|
|
||
|
$this->manualVersion = false;
|
||
|
|
||
|
return $success;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get instance of SystemUpdaterChecks for performing system checks
|
||
|
*
|
||
|
* #pw-internal
|
||
|
*
|
||
|
* @return SystemUpdaterChecks
|
||
|
* @since 3.0.135
|
||
|
*
|
||
|
*/
|
||
|
public function getChecks() {
|
||
|
require_once(dirname(__FILE__) . '/SystemUpdaterChecks.php');
|
||
|
$checks = new SystemUpdaterChecks();
|
||
|
$this->wire($checks);
|
||
|
return $checks;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get array of updates (update version numbers) that were automatically applied during this request
|
||
|
*
|
||
|
* @return array
|
||
|
* @since 3.0.135
|
||
|
*
|
||
|
*/
|
||
|
public function getUpdatesApplied() {
|
||
|
return $this->updatesApplied;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Message notice
|
||
|
*
|
||
|
* @param string $text
|
||
|
* @param int $flags
|
||
|
* @return SystemUpdater|WireData
|
||
|
*
|
||
|
*/
|
||
|
public function message($text, $flags = 0) {
|
||
|
$this->log($text);
|
||
|
return parent::message($text, $flags);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Warning notice
|
||
|
*
|
||
|
* @param string $text
|
||
|
* @param int $flags
|
||
|
* @return SystemUpdater|WireData
|
||
|
*
|
||
|
*/
|
||
|
public function warning($text, $flags = 0) {
|
||
|
$text = "WARNING: $text";
|
||
|
$this->log($text);
|
||
|
return parent::warning($text, $flags);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Error notice
|
||
|
*
|
||
|
* @param string $text
|
||
|
* @param int $flags
|
||
|
* @return SystemUpdater|WireData
|
||
|
*
|
||
|
*/
|
||
|
public function error($text, $flags = 0) {
|
||
|
$text = "ERROR: $text";
|
||
|
$this->log($text);
|
||
|
return parent::error($text, $flags);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Log a message to system-updater.txt log file
|
||
|
*
|
||
|
* @param string $str
|
||
|
*
|
||
|
*/
|
||
|
public function log($str) {
|
||
|
$options = array('showUser' => false, 'showPage' => false);
|
||
|
$this->wire()->log->save('system-updater', $str, $options);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Required for ConfigurableModule interface
|
||
|
*
|
||
|
* @param array $data
|
||
|
* @return InputfieldWrapper
|
||
|
*
|
||
|
*/
|
||
|
public function getModuleConfigInputfields(array $data) {
|
||
|
|
||
|
$modules = $this->wire()->modules;
|
||
|
$config = $this->wire()->config;
|
||
|
$sanitizer = $this->wire()->sanitizer;
|
||
|
|
||
|
$inputfields = $this->wire(new InputfieldWrapper());
|
||
|
|
||
|
$logfile = $config->paths->logs . 'system-updater.txt';
|
||
|
if(is_file($logfile)) {
|
||
|
/** @var InputfieldMarkup $f */
|
||
|
$f = $modules->get('InputfieldMarkup');
|
||
|
$f->attr('name', '_log');
|
||
|
$f->label = $this->_('System Update Log');
|
||
|
$logContent = $sanitizer->unentities(file_get_contents($logfile));
|
||
|
$logContent = preg_replace('!<a href=.+?>(.+?)</a>!', '$1', $logContent);
|
||
|
$f->value = '<pre>' . $sanitizer->entities($logContent) . '</pre>';
|
||
|
$inputfields->add($f);
|
||
|
}
|
||
|
|
||
|
/** @var InputfieldInteger $f */
|
||
|
$f = $modules->get('InputfieldInteger');
|
||
|
$f->attr('name', 'systemVersion');
|
||
|
$f->label = $this->_('System Version');
|
||
|
$f->description = $this->_('This lets you re-apply a system version update by reducing the version number.');
|
||
|
$f->attr('value', $data['systemVersion']);
|
||
|
$inputfields->add($f);
|
||
|
|
||
|
/** @var InputfieldHidden $f */
|
||
|
$f = $modules->get('InputfieldHidden');
|
||
|
$f->attr('name', 'coreVersion');
|
||
|
$f->attr('value', $config->version);
|
||
|
$inputfields->add($f);
|
||
|
|
||
|
return $inputfields;
|
||
|
}
|
||
|
|
||
|
|
||
|
}
|