__('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('!(.+?)!', '$1', $logContent); $f->value = '
' . $sanitizer->entities($logContent) . '
'; $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; } }