Blame | Last modification | View Log | Download
<?php namespace ProcessWire;/*** Class SystemUpdater** ProcessWire System Helper Module** ProcessWire 3.x, Copyright 2020 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' => 18,);}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 updateif(!$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 modulesif($this->wire('page')->template != 'admin') return;if($this->wire('config')->ajax) return;$coreVersion = isset($this->configData['coreVersion']) ? $this->configData['coreVersion'] : '';$configVersion = $this->wire('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) {$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$config = $this->wire('config');if($config->templateCompile || $config->moduleCompile) {/** @var FileCompiler $compiler */$compiler = $this->wire(new FileCompiler($this->wire('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$this->modules->resetCache();}$this->configData['coreVersion'] = $toVersion;$this->wire('modules')->saveModuleConfigData($this, $this->configData);// remove admin theme cached info in sessionforeach($this->wire('session') as $key => $value) {if(strpos($key, 'AdminTheme') === 0) {$this->wire('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;$version = (int) $version;$this->wire('config')->systemVersion = $version;$this->configData['systemVersion'] = $version;$this->configData['coreVersion'] = $this->wire('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 $text**/public function log($text) {$options = array('showUser' => false, 'showPage' => false);$this->wire('log')->save('system-updater', $text, $options);}/*** Required for ConfigurableModule interface** @param array $data* @return InputfieldWrapper**/public function getModuleConfigInputfields(array $data) {$inputfields = $this->wire(new InputfieldWrapper());$logfile = $this->wire('config')->paths->logs . 'system-updater.txt';if(is_file($logfile)) {$f = $this->wire('modules')->get('InputfieldMarkup');$f->attr('name', '_log');$f->label = $this->_('System Update Log');$logContent = $this->wire('sanitizer')->unentities(file_get_contents($logfile));$logContent = preg_replace('!<a href=.+?>(.+?)</a>!', '$1', $logContent);$f->value = '<pre>' . $this->wire('sanitizer')->entities($logContent) . '</pre>';$inputfields->add($f);}$f = $this->wire('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);$f = $this->wire('modules')->get('InputfieldHidden');$f->attr('name', 'coreVersion');$f->attr('value', $this->wire('config')->version);$inputfields->add($f);return $inputfields;}}