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