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