Merge pull request #5235 from atif-shaikh/CRM-5039MinorFixes
[civicrm-core.git] / CRM / Utils / VersionCheck.php
index cf9c9399b51a06b9f01fe07bfbf5391864b713e6..4f69360970738bd46a53ed449bdf76e9ff23dedb 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 /*
  +--------------------------------------------------------------------+
- | CiviCRM version 4.5                                                |
+ | CiviCRM version 4.6                                                |
  +--------------------------------------------------------------------+
  | Copyright CiviCRM LLC (c) 2004-2014                                |
  +--------------------------------------------------------------------+
@@ -23,7 +23,7 @@
  | GNU Affero General Public License or the licensing of CiviCRM,     |
  | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
  +--------------------------------------------------------------------+
-*/
+ */
 
 /**
  *
  *
  */
 class CRM_Utils_VersionCheck {
-  CONST
-    LATEST_VERSION_AT = 'http://latest.civicrm.org/stable.php',
+  const
+    PINGBACK_URL = 'http://latest.civicrm.org/stable.php?format=json',
     // timeout for when the connection or the server is slow
     CHECK_TIMEOUT = 5,
     // relative to $civicrm_root
     LOCALFILE_NAME = 'civicrm-version.php',
     // relative to $config->uploadDir
-    CACHEFILE_NAME = 'latest-version-cache.txt',
+    CACHEFILE_NAME = 'version-info-cache.json',
     // cachefile expiry time (in seconds) - one day
     CACHEFILE_EXPIRE = 86400;
 
@@ -49,7 +49,6 @@ class CRM_Utils_VersionCheck {
    * singleton pattern and cache the instance in this variable
    *
    * @var object
-   * @static
    */
   static private $_singleton = NULL;
 
@@ -61,139 +60,256 @@ class CRM_Utils_VersionCheck {
   public $localVersion = NULL;
 
   /**
-   * The latest version of CiviCRM
+   * The major version (branch name) of the local version
    *
    * @var string
    */
-  public $latestVersion = NULL;
+  public $localMajorVersion;
 
   /**
-   * Pingback params
+   * User setting to skip updates prior to a certain date
    *
    * @var string
    */
+  public $ignoreDate;
+
+  /**
+   * Info about available versions
+   *
+   * @var array
+   */
+  public $versionInfo = array();
+
+  /**
+   * Pingback params
+   *
+   * @var array
+   */
   protected $stats = array();
 
   /**
-   * Class constructor
+   * Path to cache file
    *
-   * @access private
+   * @var string
    */
-  function __construct() {
+  protected $cacheFile;
+
+  /**
+   * Class constructor.
+   */
+  public function __construct() {
     global $civicrm_root;
     $config = CRM_Core_Config::singleton();
 
-    $localfile = $civicrm_root . DIRECTORY_SEPARATOR . self::LOCALFILE_NAME;
-    $cachefile = $config->uploadDir . self::CACHEFILE_NAME;
+    $localFile = $civicrm_root . DIRECTORY_SEPARATOR . self::LOCALFILE_NAME;
+    $this->cacheFile = $config->uploadDir . self::CACHEFILE_NAME;
 
-    if (file_exists($localfile)) {
-      require_once ($localfile);
-      if (function_exists('civicrmVersion')) {
-        $info = civicrmVersion();
-        $this->localVersion = trim($info['version']);
-      }
+    if (file_exists($localFile)) {
+      require_once $localFile;
     }
-    if ($config->versionCheck) {
-      $expiryTime = time() - self::CACHEFILE_EXPIRE;
-
-      // if there's a cachefile and it's not stale use it to
-      // read the latestVersion, else read it from the Internet
-      if (file_exists($cachefile) && (filemtime($cachefile) > $expiryTime)) {
-        $this->latestVersion = trim(file_get_contents($cachefile));
-      }
-      else {
-        $siteKey = md5(defined('CIVICRM_SITE_KEY') ? CIVICRM_SITE_KEY : '');
-
-        $this->stats = array(
-          'hash' => md5($siteKey . $config->userFrameworkBaseURL),
-          'version' => $this->localVersion,
-          'uf' => $config->userFramework,
-          'lang' => $config->lcMessages,
-          'co' => $config->defaultContactCountry,
-          'ufv' => $config->userFrameworkVersion,
-          'PHP' => phpversion(),
-          'MySQL' => CRM_CORE_DAO::singleValueQuery('SELECT VERSION()'),
-          'communityMessagesUrl' => CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'communityMessagesUrl', NULL, '*default*'),
-        );
-
-        // Add usage stats
-        $this->payProcStats();
-        $this->entityStats();
-        $this->extensionStats();
+    if (function_exists('civicrmVersion')) {
+      $info = civicrmVersion();
+      $this->localVersion = trim($info['version']);
+      $this->localMajorVersion = $this->getMajorVersion($this->localVersion);
+    }
+    // Populate $versionInfo
+    if (CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'versionCheck', NULL, 1)) {
+      // Use cached data if available and not stale
+      if (!$this->readCacheFile()) {
+        // Collect stats for pingback
+        $this->getSiteStats();
 
         // Get the latest version and send site info
         $this->pingBack();
-
-        // Update cache file
-        if ($this->latestVersion) {
-          $fp = @fopen($cachefile, 'w');
-          if (!$fp) {
-            if (CRM_Core_Permission::check('administer CiviCRM')) {
-              CRM_Core_Session::setStatus(
-                ts('Unable to write file') . ":$cachefile<br />" . ts('Please check your system file permissions.'),
-                ts('File Error'), 'error');
-            }
-            return;
-          }
-          fwrite($fp, $this->latestVersion);
-          fclose($fp);
-        }
       }
+      $this->ignoreDate = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'versionCheckIgnoreDate');
+
+      // Sort version info in ascending order for easier comparisons
+      ksort($this->versionInfo, SORT_NUMERIC);
     }
   }
 
   /**
-   * Static instance provider
+   * Static instance provider.
    *
    * Method providing static instance of CRM_Utils_VersionCheck,
    * as in Singleton pattern
    *
    * @return CRM_Utils_VersionCheck
    */
-  static function &singleton() {
+  public static function &singleton() {
     if (!isset(self::$_singleton)) {
       self::$_singleton = new CRM_Utils_VersionCheck();
     }
     return self::$_singleton;
   }
 
+  /**
+   * Finds the release info for a minor version.
+   * @param string $version
+   * @return array|null
+   */
+  public function getReleaseInfo($version) {
+    $majorVersion = $this->getMajorVersion($version);
+    if (isset($this->versionInfo[$majorVersion])) {
+      foreach ($this->versionInfo[$majorVersion]['releases'] as $info) {
+        if ($info['version'] == $version) {
+          return $info;
+        }
+      }
+    }
+    return NULL;
+  }
+
+  /**
+   * @param $minorVersion
+   * @return string
+   */
+  public function getMajorVersion($minorVersion) {
+    if (!$minorVersion) {
+      return NULL;
+    }
+    list($a, $b) = explode('.', $minorVersion);
+    return "$a.$b";
+  }
+
+  /**
+   * @return bool
+   */
+  public function isSecurityUpdateAvailable() {
+    $thisVersion = $this->getReleaseInfo($this->localVersion);
+    $localVersionDate = CRM_Utils_Array::value('date', $thisVersion, 0);
+    foreach ($this->versionInfo as $majorVersion) {
+      foreach ($majorVersion['releases'] as $release) {
+        if (!empty($release['security']) && $release['date'] > $localVersionDate
+          && version_compare($this->localVersion, $release['version']) < 0
+        ) {
+          if (!$this->ignoreDate || $this->ignoreDate < $release['date']) {
+            return TRUE;
+          }
+        }
+      }
+    }
+  }
+
   /**
    * Get the latest version number if it's newer than the local one
    *
    * @return string|null
-   * Returns the newer version's number, or null if the versions are equal
+   *   Returns version number of the latest release if it is greater than the local version
    */
-  public function newerVersion() {
-    if ($this->latestVersion) {
-      if ((version_compare($this->localVersion, $this->latestVersion) < 0)
-        && CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'versionAlert', NULL, TRUE)) {
-        return $this->latestVersion;
+  public function isNewerVersionAvailable() {
+    $newerVersion = NULL;
+    if ($this->versionInfo && $this->localVersion) {
+      foreach ($this->versionInfo as $majorVersionNumber => $majorVersion) {
+        $release = $this->checkBranchForNewVersion($majorVersion);
+        if ($release) {
+          // If we have a release with the same majorVersion as local, return it
+          if ($majorVersionNumber == $this->localMajorVersion) {
+            return $release;
+          }
+          // Search outside the local majorVersion (excluding non-stable)
+          elseif ($majorVersion['status'] != 'testing') {
+            // We found a new release but don't return yet, keep searching newer majorVersions
+            $newerVersion = $release;
+          }
+        }
       }
     }
-    return NULL;
+    return $newerVersion;
   }
 
   /**
-   * Alert the site admin of new versions of CiviCRM
+   * @param $majorVersion
+   * @return null|string
+   */
+  private function checkBranchForNewVersion($majorVersion) {
+    $newerVersion = NULL;
+    if (!empty($majorVersion['releases'])) {
+      foreach ($majorVersion['releases'] as $release) {
+        if (version_compare($this->localVersion, $release['version']) < 0) {
+          if (!$this->ignoreDate || $this->ignoreDate < $release['date']) {
+            $newerVersion = $release['version'];
+          }
+        }
+      }
+    }
+    return $newerVersion;
+  }
+
+  /**
+   * Alert the site admin of new versions of CiviCRM.
    * Show the message once a day
    */
   public function versionAlert() {
-    if (CRM_Core_Permission::check('administer CiviCRM') && $this->newerVersion()
-    && CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'versionAlert', NULL, TRUE)) {
+    $versionAlertSetting = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'versionAlert', NULL, 1);
+    $securityAlertSetting = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'securityUpdateAlert', NULL, 3);
+    $settingsUrl = CRM_Utils_System::url('civicrm/admin/setting/misc', 'reset=1', FALSE, NULL, FALSE, FALSE, TRUE);
+    if (CRM_Core_Permission::check('administer CiviCRM') && $securityAlertSetting > 1 && $this->isSecurityUpdateAvailable()) {
       $session = CRM_Core_Session::singleton();
       if ($session->timer('version_alert', 24 * 60 * 60)) {
-        $msg = ts('A newer version of CiviCRM is available: %1', array(1 => $this->latestVersion))
-        . '<br />' . ts('<a href="%1">Download Now</a>', array(1 => 'http://civicrm.org/download'));
-        $session->setStatus($msg, ts('Update Available'));
+        $msg = ts('This version of CiviCRM requires a security update.') .
+          '<ul>
+            <li><a href="https://civicrm.org/advisory">' . ts('Read advisory') . '</a></li>
+            <li><a href="https://civicrm.org/download">' . ts('Download now') . '</a></li>
+            <li><a class="crm-setVersionCheckIgnoreDate" href="' . $settingsUrl . '">' . ts('Suppress this message') . '</a></li>
+          </ul>';
+        $session->setStatus($msg, ts('Security Alert'), 'alert');
+        CRM_Core_Resources::singleton()
+          ->addScriptFile('civicrm', 'templates/CRM/Admin/Form/Setting/versionCheckOptions.js');
+      }
+    }
+    elseif (CRM_Core_Permission::check('administer CiviCRM') && $versionAlertSetting > 1) {
+      $newerVersion = $this->isNewerVersionAvailable();
+      if ($newerVersion) {
+        $session = CRM_Core_Session::singleton();
+        if ($session->timer('version_alert', 24 * 60 * 60)) {
+          $msg = ts('A newer version of CiviCRM is available: %1', array(1 => $newerVersion)) .
+            '<ul>
+              <li><a href="https://civicrm.org/download">' . ts('Download now') . '</a></li>
+              <li><a class="crm-setVersionCheckIgnoreDate" href="' . $settingsUrl . '">' . ts('Suppress this message') . '</a></li>
+            </ul>';
+          $session->setStatus($msg, ts('Update Available'), 'info');
+          CRM_Core_Resources::singleton()
+            ->addScriptFile('civicrm', 'templates/CRM/Admin/Form/Setting/versionCheckOptions.js');
+        }
       }
     }
   }
 
   /**
-   * Get active payment processor types
+   * Collect info about the site to be sent as pingback data.
    */
-  private function payProcStats() {
-    $dao = new CRM_Financial_DAO_PaymentProcessor;
+  private function getSiteStats() {
+    $config = CRM_Core_Config::singleton();
+    $siteKey = md5(defined('CIVICRM_SITE_KEY') ? CIVICRM_SITE_KEY : '');
+
+    // Calorie-free pingback for alphas
+    $this->stats = array('version' => $this->localVersion);
+
+    // Non-alpha versions get the full treatment
+    if ($this->localVersion && !strpos($this->localVersion, 'alpha')) {
+      $this->stats += array(
+        'hash' => md5($siteKey . $config->userFrameworkBaseURL),
+        'uf' => $config->userFramework,
+        'lang' => $config->lcMessages,
+        'co' => $config->defaultContactCountry,
+        'ufv' => $config->userFrameworkVersion,
+        'PHP' => phpversion(),
+        'MySQL' => CRM_CORE_DAO::singleValueQuery('SELECT VERSION()'),
+        'communityMessagesUrl' => CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'communityMessagesUrl', NULL, '*default*'),
+      );
+      $this->getPayProcStats();
+      $this->getEntityStats();
+      $this->getExtensionStats();
+    }
+  }
+
+  /**
+   * Get active payment processor types.
+   */
+  private function getPayProcStats() {
+    $dao = new CRM_Financial_DAO_PaymentProcessor();
     $dao->is_active = 1;
     $dao->find();
     $ppTypes = array();
@@ -206,14 +322,13 @@ class CRM_Utils_VersionCheck {
     }
     // add the .-separated list of the processor types
     $this->stats['PPTypes'] = implode(',', array_unique($ppTypes));
-
   }
 
   /**
-   * Fetch counts from entity tables
+   * Fetch counts from entity tables.
    * Add info to the 'entities' array
    */
-  private function entityStats() {
+  private function getEntityStats() {
     $tables = array(
       'CRM_Activity_DAO_Activity' => 'is_test = 0',
       'CRM_Case_DAO_Case' => 'is_deleted = 0',
@@ -238,7 +353,7 @@ class CRM_Utils_VersionCheck {
       'CRM_Pledge_DAO_PledgeBlock' => NULL,
     );
     foreach ($tables as $daoName => $where) {
-      $dao = new $daoName;
+      $dao = new $daoName();
       if ($where) {
         $dao->whereAdd($where);
       }
@@ -254,7 +369,7 @@ class CRM_Utils_VersionCheck {
    * Fetch stats about enabled components/extensions
    * Add info to the 'extensions' array
    */
-  private function extensionStats() {
+  private function getExtensionStats() {
     // Core components
     $config = CRM_Core_Config::singleton();
     foreach ($config->enableComponents as $comp) {
@@ -281,6 +396,7 @@ class CRM_Utils_VersionCheck {
   /**
    * Send the request to civicrm.org
    * Set timeout and suppress errors
+   * Store results in the cache file
    */
   private function pingBack() {
     ini_set('default_socket_timeout', self::CHECK_TIMEOUT);
@@ -292,14 +408,48 @@ class CRM_Utils_VersionCheck {
       ),
     );
     $ctx = stream_context_create($params);
-    $this->latestVersion = @file_get_contents(self::LATEST_VERSION_AT, FALSE, $ctx);
-    if (!preg_match('/^\d+\.\d+\.\d+$/', $this->latestVersion)) {
-      $this->latestVersion = NULL;
-    }
-    else {
-      $this->latestVersion = trim($this->latestVersion);
+    $rawJson = @file_get_contents(self::PINGBACK_URL, FALSE, $ctx);
+    $versionInfo = $rawJson ? json_decode($rawJson, TRUE) : NULL;
+    // If we couldn't fetch or parse the data $versionInfo will be NULL
+    // Otherwise it will be an array and we'll cache it.
+    // Note the array may be empty e.g. in the case of a pre-alpha with no releases
+    if ($versionInfo !== NULL) {
+      $this->writeCacheFile($rawJson);
+      $this->versionInfo = $versionInfo;
     }
     ini_restore('default_socket_timeout');
   }
 
+  /**
+   * @return bool
+   */
+  private function readCacheFile() {
+    $expiryTime = time() - self::CACHEFILE_EXPIRE;
+
+    // if there's a cachefile and it's not stale, use it
+    if (file_exists($this->cacheFile) && (filemtime($this->cacheFile) > $expiryTime)) {
+      $this->versionInfo = (array) json_decode(file_get_contents($this->cacheFile), TRUE);
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Save version info to file.
+   * @param string $contents
+   */
+  private function writeCacheFile($contents) {
+    $fp = @fopen($this->cacheFile, 'w');
+    if (!$fp) {
+      if (CRM_Core_Permission::check('administer CiviCRM')) {
+        CRM_Core_Session::setStatus(
+          ts('Unable to write file') . ": $this->cacheFile<br />" . ts('Please check your system file permissions.'),
+          ts('File Error'), 'error');
+      }
+      return;
+    }
+    fwrite($fp, $contents);
+    fclose($fp);
+  }
+
 }