X-Git-Url: https://vcs.fsf.org/?a=blobdiff_plain;f=CRM%2FUtils%2FVersionCheck.php;h=f7f19905e84299e472cb60b0cbc97dc376560aa9;hb=ed5a0ff7196e306e1319bd3f5913fdb068974daa;hp=15a9572f970bfeecd648dfd15dbaa257fb2cecf4;hpb=1761553f7e28866ef0c0a9f54abbb222487cea5c;p=civicrm-core.git diff --git a/CRM/Utils/VersionCheck.php b/CRM/Utils/VersionCheck.php index 15a9572f97..f7f19905e8 100644 --- a/CRM/Utils/VersionCheck.php +++ b/CRM/Utils/VersionCheck.php @@ -33,14 +33,14 @@ * */ 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; @@ -61,84 +61,72 @@ 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 + */ + protected $cacheFile; + + /** + * Class constructor */ - function __construct() { + 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
" . 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); } } @@ -150,27 +138,103 @@ class CRM_Utils_VersionCheck { * * @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; + } + + /** + * @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; } /** @@ -178,21 +242,71 @@ class CRM_Utils_VersionCheck { * 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)) - . '
' . ts('Download Now', array(1 => 'http://civicrm.org/download')); - $session->setStatus($msg, ts('Update Available')); + $msg = ts('This version of CiviCRM requires a security update.') . + ''; + $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)) . + ''; + $session->setStatus($msg, ts('Update Available'), 'info'); + CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'templates/CRM/Admin/Form/Setting/versionCheckOptions.js'); + } } } } + /** + * Collect info about the site to be sent as pingback data + */ + 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 payProcStats() { + private function getPayProcStats() { $dao = new CRM_Financial_DAO_PaymentProcessor; $dao->is_active = 1; $dao->find(); @@ -206,14 +320,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 * 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', @@ -254,7 +367,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 +394,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 +406,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
" . ts('Please check your system file permissions.'), + ts('File Error'), 'error'); + } + return; + } + fwrite($fp, $contents); + fclose($fp); + } + }