INFRA-132 - Remove extra newlines from the bottom of docblocks
[civicrm-core.git] / CRM / Utils / VersionCheck.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.6 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2014
32 * $Id: $
33 *
34 */
35 class CRM_Utils_VersionCheck {
36 const
37 PINGBACK_URL = 'http://latest.civicrm.org/stable.php?format=json',
38 // timeout for when the connection or the server is slow
39 CHECK_TIMEOUT = 5,
40 // relative to $civicrm_root
41 LOCALFILE_NAME = 'civicrm-version.php',
42 // relative to $config->uploadDir
43 CACHEFILE_NAME = 'version-info-cache.json',
44 // cachefile expiry time (in seconds) - one day
45 CACHEFILE_EXPIRE = 86400;
46
47 /**
48 * We only need one instance of this object, so we use the
49 * singleton pattern and cache the instance in this variable
50 *
51 * @var object
52 * @static
53 */
54 static private $_singleton = NULL;
55
56 /**
57 * The version of the current (local) installation
58 *
59 * @var string
60 */
61 public $localVersion = NULL;
62
63 /**
64 * The major version (branch name) of the local version
65 *
66 * @var string
67 */
68 public $localMajorVersion;
69
70 /**
71 * User setting to skip updates prior to a certain date
72 *
73 * @var string
74 */
75 public $ignoreDate;
76
77 /**
78 * Info about available versions
79 *
80 * @var array
81 */
82 public $versionInfo = array();
83
84 /**
85 * Pingback params
86 *
87 * @var array
88 */
89 protected $stats = array();
90
91 /**
92 * Path to cache file
93 *
94 * @var string
95 */
96 protected $cacheFile;
97
98 /**
99 * Class constructor
100 */
101 public function __construct() {
102 global $civicrm_root;
103 $config = CRM_Core_Config::singleton();
104
105 $localFile = $civicrm_root . DIRECTORY_SEPARATOR . self::LOCALFILE_NAME;
106 $this->cacheFile = $config->uploadDir . self::CACHEFILE_NAME;
107
108 if (file_exists($localFile)) {
109 require_once $localFile;
110 }
111 if (function_exists('civicrmVersion')) {
112 $info = civicrmVersion();
113 $this->localVersion = trim($info['version']);
114 $this->localMajorVersion = $this->getMajorVersion($this->localVersion);
115 }
116 // Populate $versionInfo
117 if (CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'versionCheck', NULL, 1)) {
118 // Use cached data if available and not stale
119 if (!$this->readCacheFile()) {
120 // Collect stats for pingback
121 $this->getSiteStats();
122
123 // Get the latest version and send site info
124 $this->pingBack();
125 }
126 $this->ignoreDate = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'versionCheckIgnoreDate');
127
128 // Sort version info in ascending order for easier comparisons
129 ksort($this->versionInfo, SORT_NUMERIC);
130 }
131 }
132
133 /**
134 * Static instance provider
135 *
136 * Method providing static instance of CRM_Utils_VersionCheck,
137 * as in Singleton pattern
138 *
139 * @return CRM_Utils_VersionCheck
140 */
141 public static function &singleton() {
142 if (!isset(self::$_singleton)) {
143 self::$_singleton = new CRM_Utils_VersionCheck();
144 }
145 return self::$_singleton;
146 }
147
148 /**
149 * Finds the release info for a minor version
150 * @param string $version
151 * @return array|null
152 */
153 public function getReleaseInfo($version) {
154 $majorVersion = $this->getMajorVersion($version);
155 if (isset($this->versionInfo[$majorVersion])) {
156 foreach ($this->versionInfo[$majorVersion]['releases'] as $info) {
157 if ($info['version'] == $version) {
158 return $info;
159 }
160 }
161 }
162 return NULL;
163 }
164
165 /**
166 * @param $minorVersion
167 * @return string
168 */
169 public function getMajorVersion($minorVersion) {
170 if (!$minorVersion) {
171 return NULL;
172 }
173 list($a, $b) = explode('.', $minorVersion);
174 return "$a.$b";
175 }
176
177 /**
178 * @return bool
179 */
180 public function isSecurityUpdateAvailable() {
181 $thisVersion = $this->getReleaseInfo($this->localVersion);
182 $localVersionDate = CRM_Utils_Array::value('date', $thisVersion, 0);
183 foreach ($this->versionInfo as $majorVersion) {
184 foreach ($majorVersion['releases'] as $release) {
185 if (!empty($release['security']) && $release['date'] > $localVersionDate
186 && version_compare($this->localVersion, $release['version']) < 0) {
187 if (!$this->ignoreDate || $this->ignoreDate < $release['date']) {
188 return TRUE;
189 }
190 }
191 }
192 }
193 }
194
195 /**
196 * Get the latest version number if it's newer than the local one
197 *
198 * @return string|null
199 * Returns version number of the latest release if it is greater than the local version
200 */
201 public function isNewerVersionAvailable() {
202 $newerVersion = NULL;
203 if ($this->versionInfo && $this->localVersion) {
204 foreach ($this->versionInfo as $majorVersionNumber => $majorVersion) {
205 $release = $this->checkBranchForNewVersion($majorVersion);
206 if ($release) {
207 // If we have a release with the same majorVersion as local, return it
208 if ($majorVersionNumber == $this->localMajorVersion) {
209 return $release;
210 }
211 // Search outside the local majorVersion (excluding non-stable)
212 elseif ($majorVersion['status'] != 'testing') {
213 // We found a new release but don't return yet, keep searching newer majorVersions
214 $newerVersion = $release;
215 }
216 }
217 }
218 }
219 return $newerVersion;
220 }
221
222 /**
223 * @param $majorVersion
224 * @return null|string
225 */
226 private function checkBranchForNewVersion($majorVersion) {
227 $newerVersion = NULL;
228 if (!empty($majorVersion['releases'])) {
229 foreach ($majorVersion['releases'] as $release) {
230 if (version_compare($this->localVersion, $release['version']) < 0) {
231 if (!$this->ignoreDate || $this->ignoreDate < $release['date']) {
232 $newerVersion = $release['version'];
233 }
234 }
235 }
236 }
237 return $newerVersion;
238 }
239
240 /**
241 * Alert the site admin of new versions of CiviCRM
242 * Show the message once a day
243 */
244 public function versionAlert() {
245 $versionAlertSetting = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'versionAlert', NULL, 1);
246 $securityAlertSetting = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'securityUpdateAlert', NULL, 3);
247 $settingsUrl = CRM_Utils_System::url('civicrm/admin/setting/misc', 'reset=1', FALSE, NULL, FALSE, FALSE, TRUE);
248 if (CRM_Core_Permission::check('administer CiviCRM') && $securityAlertSetting > 1 && $this->isSecurityUpdateAvailable()) {
249 $session = CRM_Core_Session::singleton();
250 if ($session->timer('version_alert', 24 * 60 * 60)) {
251 $msg = ts('This version of CiviCRM requires a security update.') .
252 '<ul>
253 <li><a href="https://civicrm.org/advisory">' . ts('Read advisory') . '</a></li>
254 <li><a href="https://civicrm.org/download">' . ts('Download now') . '</a></li>
255 <li><a class="crm-setVersionCheckIgnoreDate" href="' . $settingsUrl . '">' . ts('Suppress this message') . '</a></li>
256 </ul>';
257 $session->setStatus($msg, ts('Security Alert'), 'alert');
258 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'templates/CRM/Admin/Form/Setting/versionCheckOptions.js');
259 }
260 }
261 elseif (CRM_Core_Permission::check('administer CiviCRM') && $versionAlertSetting > 1) {
262 $newerVersion = $this->isNewerVersionAvailable();
263 if ($newerVersion) {
264 $session = CRM_Core_Session::singleton();
265 if ($session->timer('version_alert', 24 * 60 * 60)) {
266 $msg = ts('A newer version of CiviCRM is available: %1', array(1 => $newerVersion)) .
267 '<ul>
268 <li><a href="https://civicrm.org/download">' . ts('Download now') . '</a></li>
269 <li><a class="crm-setVersionCheckIgnoreDate" href="' . $settingsUrl . '">' . ts('Suppress this message') . '</a></li>
270 </ul>';
271 $session->setStatus($msg, ts('Update Available'), 'info');
272 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'templates/CRM/Admin/Form/Setting/versionCheckOptions.js');
273 }
274 }
275 }
276 }
277
278 /**
279 * Collect info about the site to be sent as pingback data
280 */
281 private function getSiteStats() {
282 $config = CRM_Core_Config::singleton();
283 $siteKey = md5(defined('CIVICRM_SITE_KEY') ? CIVICRM_SITE_KEY : '');
284
285 // Calorie-free pingback for alphas
286 $this->stats = array('version' => $this->localVersion);
287
288 // Non-alpha versions get the full treatment
289 if ($this->localVersion && !strpos($this->localVersion, 'alpha')) {
290 $this->stats += array(
291 'hash' => md5($siteKey . $config->userFrameworkBaseURL),
292 'uf' => $config->userFramework,
293 'lang' => $config->lcMessages,
294 'co' => $config->defaultContactCountry,
295 'ufv' => $config->userFrameworkVersion,
296 'PHP' => phpversion(),
297 'MySQL' => CRM_CORE_DAO::singleValueQuery('SELECT VERSION()'),
298 'communityMessagesUrl' => CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'communityMessagesUrl', NULL, '*default*'),
299 );
300 $this->getPayProcStats();
301 $this->getEntityStats();
302 $this->getExtensionStats();
303 }
304 }
305
306 /**
307 * Get active payment processor types
308 */
309 private function getPayProcStats() {
310 $dao = new CRM_Financial_DAO_PaymentProcessor;
311 $dao->is_active = 1;
312 $dao->find();
313 $ppTypes = array();
314
315 // Get title and id for all processor types
316 $ppTypeNames = CRM_Core_PseudoConstant::paymentProcessorType();
317
318 while ($dao->fetch()) {
319 $ppTypes[] = $ppTypeNames[$dao->payment_processor_type_id];
320 }
321 // add the .-separated list of the processor types
322 $this->stats['PPTypes'] = implode(',', array_unique($ppTypes));
323 }
324
325 /**
326 * Fetch counts from entity tables
327 * Add info to the 'entities' array
328 */
329 private function getEntityStats() {
330 $tables = array(
331 'CRM_Activity_DAO_Activity' => 'is_test = 0',
332 'CRM_Case_DAO_Case' => 'is_deleted = 0',
333 'CRM_Contact_DAO_Contact' => 'is_deleted = 0',
334 'CRM_Contact_DAO_Relationship' => NULL,
335 'CRM_Campaign_DAO_Campaign' => NULL,
336 'CRM_Contribute_DAO_Contribution' => 'is_test = 0',
337 'CRM_Contribute_DAO_ContributionPage' => 'is_active = 1',
338 'CRM_Contribute_DAO_ContributionProduct' => NULL,
339 'CRM_Contribute_DAO_Widget' => 'is_active = 1',
340 'CRM_Core_DAO_Discount' => NULL,
341 'CRM_Price_DAO_PriceSetEntity' => NULL,
342 'CRM_Core_DAO_UFGroup' => 'is_active = 1',
343 'CRM_Event_DAO_Event' => 'is_active = 1',
344 'CRM_Event_DAO_Participant' => 'is_test = 0',
345 'CRM_Friend_DAO_Friend' => 'is_active = 1',
346 'CRM_Grant_DAO_Grant' => NULL,
347 'CRM_Mailing_DAO_Mailing' => 'is_completed = 1',
348 'CRM_Member_DAO_Membership' => 'is_test = 0',
349 'CRM_Member_DAO_MembershipBlock' => 'is_active = 1',
350 'CRM_Pledge_DAO_Pledge' => 'is_test = 0',
351 'CRM_Pledge_DAO_PledgeBlock' => NULL,
352 );
353 foreach ($tables as $daoName => $where) {
354 $dao = new $daoName;
355 if ($where) {
356 $dao->whereAdd($where);
357 }
358 $short_name = substr($daoName, strrpos($daoName, '_') + 1);
359 $this->stats['entities'][] = array(
360 'name' => $short_name,
361 'size' => $dao->count(),
362 );
363 }
364 }
365
366 /**
367 * Fetch stats about enabled components/extensions
368 * Add info to the 'extensions' array
369 */
370 private function getExtensionStats() {
371 // Core components
372 $config = CRM_Core_Config::singleton();
373 foreach ($config->enableComponents as $comp) {
374 $this->stats['extensions'][] = array(
375 'name' => 'org.civicrm.component.' . strtolower($comp),
376 'enabled' => 1,
377 'version' => $this->stats['version'],
378 );
379 }
380 // Contrib extensions
381 $mapper = CRM_Extension_System::singleton()->getMapper();
382 $dao = new CRM_Core_DAO_Extension();
383 $dao->find();
384 while ($dao->fetch()) {
385 $info = $mapper->keyToInfo($dao->full_name);
386 $this->stats['extensions'][] = array(
387 'name' => $dao->full_name,
388 'enabled' => $dao->is_active,
389 'version' => isset($info->version) ? $info->version : NULL,
390 );
391 }
392 }
393
394 /**
395 * Send the request to civicrm.org
396 * Set timeout and suppress errors
397 * Store results in the cache file
398 */
399 private function pingBack() {
400 ini_set('default_socket_timeout', self::CHECK_TIMEOUT);
401 $params = array(
402 'http' => array(
403 'method' => 'POST',
404 'header' => 'Content-type: application/x-www-form-urlencoded',
405 'content' => http_build_query($this->stats),
406 ),
407 );
408 $ctx = stream_context_create($params);
409 $rawJson = @file_get_contents(self::PINGBACK_URL, FALSE, $ctx);
410 $versionInfo = $rawJson ? json_decode($rawJson, TRUE) : NULL;
411 // If we couldn't fetch or parse the data $versionInfo will be NULL
412 // Otherwise it will be an array and we'll cache it.
413 // Note the array may be empty e.g. in the case of a pre-alpha with no releases
414 if ($versionInfo !== NULL) {
415 $this->writeCacheFile($rawJson);
416 $this->versionInfo = $versionInfo;
417 }
418 ini_restore('default_socket_timeout');
419 }
420
421 /**
422 * @return bool
423 */
424 private function readCacheFile() {
425 $expiryTime = time() - self::CACHEFILE_EXPIRE;
426
427 // if there's a cachefile and it's not stale, use it
428 if (file_exists($this->cacheFile) && (filemtime($this->cacheFile) > $expiryTime)) {
429 $this->versionInfo = (array) json_decode(file_get_contents($this->cacheFile), TRUE);
430 return TRUE;
431 }
432 return FALSE;
433 }
434
435 /**
436 * Save version info to file
437 * @param string $contents
438 */
439 private function writeCacheFile($contents) {
440 $fp = @fopen($this->cacheFile, 'w');
441 if (!$fp) {
442 if (CRM_Core_Permission::check('administer CiviCRM')) {
443 CRM_Core_Session::setStatus(
444 ts('Unable to write file') . ": $this->cacheFile<br />" . ts('Please check your system file permissions.'),
445 ts('File Error'), 'error');
446 }
447 return;
448 }
449 fwrite($fp, $contents);
450 fclose($fp);
451 }
452
453 }