*/
class CRM_Utils_VersionCheck {
CONST
- LATEST_VERSION_AT = 'http://latest.civicrm.org/stable.php',
+ 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;
+
+ /**
+ * Info about available versions
+ *
+ * @var array
+ */
+ public $versionInfo = array();
/**
* Pingback params
*
- * @var string
+ * @var array
*/
protected $stats = array();
+
+ /**
+ * Path to cache file
+ *
+ * @var string
+ */
+ protected $cacheFile;
/**
* Class constructor
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);
+ }
+ 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();
+ }
+ }
+ // Make sure version info is in ascending order for easier comparisons
+ ksort($this->versionInfo, SORT_NUMERIC);
+ }
- // 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);
+ /**
+ * Magic property accessor
+ * @param $variable
+ * @return mixed
+ */
+ function __get($variable) {
+ switch ($variable) {
+ case "localVersionStatus":
+ if ($this->localVersion && $this->versionInfo) {
+ $versionInfo = CRM_Utils_Array::value($this->localMajorVersion, $this->versionInfo);
+ return CRM_Utils_Array::value('status', $versionInfo);
}
- }
+ return NULL;
}
+ return NULL;
}
/**
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";
+ }
+
+ 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) {
+ 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
*/
- 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) {
+ // If using an alpha or beta, find the absolute latest available
+ $latest = end($this->versionInfo);
+ // Otherwise find the latest stable version available
+ if ($this->localVersionStatus != 'testing') {
+ foreach ($this->versionInfo as $majorVersion) {
+ if ($majorVersion['status'] == 'stable') {
+ $latest = $majorVersion;
+ }
+ }
+ }
+ if ($latest && !empty($latest['releases'])) {
+ foreach ($latest['releases'] as $release) {
+ if (version_compare($this->localVersion, $release['version']) < 0) {
+ $newerVersion = $release['version'];
+ }
+ }
}
}
- return NULL;
+ 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);
+ 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>
+ </ul>';
+ $session->setStatus($msg, ts('Security Alert'));
}
}
+ 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))
+ . '<br /><a href="https://civicrm.org/download">' . ts('Download now') . '</a>';
+ $session->setStatus($msg, ts('Update Available'), 'info');
+ }
+ }
+ }
+ }
+
+ /**
+ * 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);
+ }
+
}
'group_name' => 'CiviCRM Preferences',
'group' => 'core',
'name' => 'versionAlert',
- 'type' => 'Boolean',
- 'quick_form_type' => 'YesNo',
+ 'type' => 'Integer',
+ 'quick_form_type' => 'Element',
+ 'html_type' => 'select',
+ 'option_values' => array(
+ ts('Disabled'),
+ ts('Display In Page Footer'),
+ ts('Display As Popup Alert'),
+ ts('Page Footer + Popup Alert'),
+ ),
'default' => 1,
'add' => '4.3',
'title' => 'New Version Alerts',
'is_domain' => 1,
'is_contact' => 0,
- 'description' => 'Displays an on-screen alert to users with "Administer CiviCRM" permissions when a new version of CiviCRM is available. This setting will only work if the "Version Check & Statistics Reporting" setting is enabled.',
+ 'description' => "",
+ 'help_text' => null,
+ ),
+ 'securityUpdateAlert' => array(
+ 'group_name' => 'CiviCRM Preferences',
+ 'group' => 'core',
+ 'name' => 'securityUpdateAlert',
+ 'type' => 'Integer',
+ 'quick_form_type' => 'Element',
+ 'html_type' => 'select',
+ 'option_values' => array(
+ ts('Disabled'),
+ ts('Display In Page Footer'),
+ ts('Display As Popup Alert'),
+ ts('Page Footer + Popup Alert'),
+ ),
+ 'default' => 3,
+ 'add' => '4.6',
+ 'title' => 'Security Update Alerts',
+ 'is_domain' => 1,
+ 'is_contact' => 0,
+ 'description' => "",
'help_text' => null,
),
'versionCheck' => array(
'group_name' => 'CiviCRM Preferences',
'group' => 'core',
'name' => 'versionCheck',
- 'prefetch' => 1, // prefetch causes it to be cached in config settings. Usually this is a transitional setting. Some things like urls are permanent. Remove this comment if you have assessed & it should be permanent
- 'config_only'=> 1, //@todo - see https://wiki.civicrm.org/confluence/display/CRMDOC/Settings+Reference#SettingsReference-Convertingaconfigobjecttoasetting on removing this deprecated value
'type' => 'Boolean',
'quick_form_type' => 'YesNo',
'default' => 1,
'add' => '4.3',
- 'title' => 'Version Check & Statistics Reporting',
+ 'title' => 'Automatically Check for Updates',
'is_domain' => 1,
'is_contact' => 0,
- 'description' => "If enabled, CiviCRM automatically checks availablity of a newer version of the software. New version alerts will be displayed on the main CiviCRM Administration page.
-When enabled, statistics about your CiviCRM installation are reported anonymously to the CiviCRM team to assist in prioritizing ongoing development efforts. The following information is gathered: CiviCRM version, versions of PHP, MySQL and framework (Drupal/Joomla/standalone), and default language. Counts (but no actual data) of the following record types are reported: contacts, activities, cases, relationships, contributions, contribution pages, contribution products, contribution widgets, discounts, price sets, profiles, events, participants, tell-a-friend pages, grants, mailings, memberships, membership blocks, pledges, pledge blocks and active payment processor types.",
+ 'description' => "",
'help_text' => null,
),
'securityAlert' => array(
'group_name' => 'CiviCRM Preferences',
'group' => 'core',
- 'name' => 'versionCheck',
- 'prefetch' => 0,
+ 'name' => 'securityAlert',
'type' => 'Boolean',
'quick_form_type' => 'YesNo',
'default' => 1,
'add' => '4.4',
- 'title' => 'Security Alerts',
+ 'title' => 'Security Audits',
'is_domain' => 1,
'is_contact' => 0,
'description' => "If enabled, CiviCRM will automatically run checks for significant mis-configurations such as ineffective file protections.",
<p class="description">{ts 1="http://wkhtmltopdf.org/"}<a href="%1">wkhtmltopdf is an alternative utility for generating PDF's</a> which may provide better performance especially if you are generating a large number of PDF letters or receipts. Your system administrator will need to download and install this utility, and enter the executable path here.{/ts}</p>
</td>
</tr>
- <tr class="crm-miscellaneous-form-block-versionAlert">
- <td class="label">{$form.versionAlert.label}</td>
- <td>{$form.versionAlert.html}<br />
- <p class="description">{ts}Displays an on-screen alert to users with "Administer CiviCRM" permissions when a new version of CiviCRM is available. This setting will only work if the "Version Check & Statistics Reporting" setting is enabled.{/ts}</p></td>
- </tr>
<tr class="crm-miscellaneous-form-block-versionCheck">
<td class="label">{$form.versionCheck.label}</td>
<td>{$form.versionCheck.html}<br />
<p class="description">{ts}When enabled, statistics about your CiviCRM installation are reported anonymously to the CiviCRM team to assist in prioritizing ongoing development efforts. The following information is gathered: CiviCRM version, versions of PHP, MySQL and framework (Drupal/Joomla/standalone), and default language. Counts (but no actual data) of the following record types are reported: contacts, activities, cases, relationships, contributions, contribution pages, contribution products, contribution widgets, discounts, price sets, profiles, events, participants, tell-a-friend pages, grants, mailings, memberships, membership blocks, pledges, pledge blocks and active payment processor types.{/ts}</p></td>
</tr>
+ <tr class="crm-miscellaneous-form-block-securityUpdateAlert">
+ <td class="label">{$form.securityUpdateAlert.label}</td>
+ <td>{$form.securityUpdateAlert.html}<br />
+ <p class="description">{ts}Alert site admins about security advisories. <strong>Strongly recommended.</strong>{/ts}</p></td>
+ </tr>
+ <tr class="crm-miscellaneous-form-block-versionAlert">
+ <td class="label">{$form.versionAlert.label}</td>
+ <td>{$form.versionAlert.html}<br />
+ <p class="description">{ts}Alert site admins about all new CiviCRM releases.{/ts}</p></td>
+ </tr>
<tr class="crm-miscellaneous-form-block-empoweredBy">
<td class="label">{$form.empoweredBy.label}</td>
<td>{$form.empoweredBy.html}<br />
</table>
<div class="crm-submit-buttons">{include file="CRM/common/formButtons.tpl" location="bottom"}</div>
</div>
+{literal}
+<script type="text/javascript">
+ CRM.$(function($) {
+ 'use strict';
+ $('input[name=versionCheck][value=0]').change(function() {
+ if ($(this).is(':checked')) {
+ CRM.confirm({message: {/literal}"{ts escape='js'}Disabling this option will prevent CiviCRM from checking for important security updates. Are you sure?{/ts}"{literal}})
+ .on('crmConfirm:no', function() {
+ $('input[name=versionCheck][value=0]').prop('checked', false);
+ $('input[name=versionCheck][value=1]').prop('checked', true);
+ })
+ }
+ });
+ $('select[name=securityUpdateAlert]').change(function() {
+ if ($(this).val() == '0') {
+ CRM.confirm({message: {/literal}"{ts escape='js'}Disabling this option will prevent CiviCRM from checking for important security updates. Are you sure?{/ts}"{literal}})
+ .on('crmConfirm:no', function() {
+ $('select[name=securityUpdateAlert]').val('3');
+ })
+ }
+ });
+ });
+</script>
+{/literal}
\ No newline at end of file