*
*/
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;
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<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);
}
}
*
* @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;
}
/**
* 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');
+ }
}
}
}
+ /**
+ * 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();
}
// 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',
* 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) {
/**
* 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);
),
);
$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);
+ }
+
}