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