Subversion Repositories web.creative

Rev

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 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;
    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 session
    foreach($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;
  }


}