Merge pull request #11723 from mukeshcompucorp/CRM-21805-fix-fields-and-labels-in...
[civicrm-core.git] / CRM / Utils / VersionCheck.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2018 |
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-2018
32 */
33 class CRM_Utils_VersionCheck {
34 const
35 CACHEFILE_NAME = 'version-info-cache.json',
36 // After which length of time we expire the cached version info (7+ days).
37 CACHEFILE_EXPIRE = 605000;
38
39 /**
40 * The version of the current (local) installation
41 *
42 * @var string
43 */
44 public $localVersion = NULL;
45
46 /**
47 * The major version (branch name) of the local version
48 *
49 * @var string
50 */
51 public $localMajorVersion;
52
53 /**
54 * Info about available versions
55 *
56 * @var array
57 */
58 public $versionInfo = array();
59
60 /**
61 * @var bool
62 */
63 public $isInfoAvailable;
64
65 /**
66 * @var array
67 */
68 public $cronJob = array();
69
70 /**
71 * @var string
72 */
73 public $pingbackUrl = 'https://latest.civicrm.org/stable.php?format=json';
74
75 /**
76 * Pingback params
77 *
78 * @var array
79 */
80 protected $stats = array();
81
82 /**
83 * Path to cache file
84 *
85 * @var string
86 */
87 public $cacheFile;
88
89 /**
90 * Class constructor.
91 */
92 public function __construct() {
93 $this->localVersion = CRM_Utils_System::version();
94 $this->localMajorVersion = $this->getMajorVersion($this->localVersion);
95 $this->cacheFile = CRM_Core_Config::singleton()->uploadDir . self::CACHEFILE_NAME;
96 }
97
98 /**
99 * Self-populates version info
100 *
101 * @throws \Exception
102 */
103 public function initialize() {
104 $this->getJob();
105
106 // Populate remote $versionInfo from cache file
107 $this->isInfoAvailable = $this->readCacheFile();
108
109 // Fallback if scheduled job is enabled but has failed to run.
110 $expiryTime = time() - self::CACHEFILE_EXPIRE;
111 if (!empty($this->cronJob['is_active']) &&
112 (!$this->isInfoAvailable || filemtime($this->cacheFile) < $expiryTime)
113 ) {
114 // First try updating the files modification time, for 2 reasons:
115 // - if the file is not writeable, this saves the trouble of pinging back
116 // - if the remote server is down, this will prevent an immediate retry
117 if (touch($this->cacheFile) === FALSE) {
118 throw new Exception('File not writable');
119 }
120 $this->fetch();
121 }
122 }
123
124 /**
125 * Sets $versionInfo
126 *
127 * @param $info
128 */
129 public function setVersionInfo($info) {
130 $this->versionInfo = (array) $info;
131 // Sort version info in ascending order for easier comparisons
132 ksort($this->versionInfo, SORT_NUMERIC);
133 }
134
135 /**
136 * Finds the release info for a minor version.
137 * @param string $version
138 * @return array|null
139 */
140 public function getReleaseInfo($version) {
141 $majorVersion = $this->getMajorVersion($version);
142 if (isset($this->versionInfo[$majorVersion])) {
143 foreach ($this->versionInfo[$majorVersion]['releases'] as $info) {
144 if ($info['version'] == $version) {
145 return $info;
146 }
147 }
148 }
149 return NULL;
150 }
151
152 /**
153 * @param $minorVersion
154 * @return string
155 */
156 public function getMajorVersion($minorVersion) {
157 if (!$minorVersion) {
158 return NULL;
159 }
160 list($a, $b) = explode('.', $minorVersion);
161 return "$a.$b";
162 }
163
164
165 /**
166 * Get the latest version number if it's newer than the local one
167 *
168 * @return array
169 * Returns version number of the latest release if it is greater than the local version,
170 * along with the type of upgrade (regular/security) needed and the status of the major
171 * version
172 */
173 public function isNewerVersionAvailable() {
174 $return = array(
175 'version' => NULL,
176 'upgrade' => NULL,
177 'status' => NULL,
178 );
179
180 if ($this->versionInfo && $this->localVersion) {
181 if (isset($this->versionInfo[$this->localMajorVersion])) {
182 switch (CRM_Utils_Array::value('status', $this->versionInfo[$this->localMajorVersion])) {
183 case 'stable':
184 case 'lts':
185 case 'testing':
186 // look for latest version in this major version
187 $releases = $this->checkBranchForNewVersion($this->versionInfo[$this->localMajorVersion]);
188 if ($releases['newest']) {
189 $return['version'] = $releases['newest'];
190
191 // check for intervening security releases
192 $return['upgrade'] = ($releases['security']) ? 'security' : 'regular';
193 }
194 break;
195
196 case 'eol':
197 default:
198 // look for latest version ever
199 foreach ($this->versionInfo as $majorVersionNumber => $majorVersion) {
200 if ($majorVersionNumber < $this->localMajorVersion || $majorVersion['status'] == 'testing') {
201 continue;
202 }
203 $releases = $this->checkBranchForNewVersion($this->versionInfo[$majorVersionNumber]);
204
205 if ($releases['newest']) {
206 $return['version'] = $releases['newest'];
207
208 // check for intervening security releases
209 $return['upgrade'] = ($releases['security'] || $return['upgrade'] == 'security') ? 'security' : 'regular';
210 }
211 }
212 }
213 $return['status'] = $this->versionInfo[$this->localMajorVersion]['status'];
214 }
215 else {
216 // Figure if the version is really old or really new
217 $wayOld = TRUE;
218
219 foreach ($this->versionInfo as $majorVersionNumber => $majorVersion) {
220 $wayOld = ($this->localMajorVersion < $majorVersionNumber);
221 }
222
223 if ($wayOld) {
224 $releases = $this->checkBranchForNewVersion($majorVersion);
225
226 $return = array(
227 'version' => $releases['newest'],
228 'upgrade' => 'security',
229 'status' => 'eol',
230 );
231 }
232 }
233 }
234
235 return $return;
236 }
237
238 /**
239 * Called by version_check cron job
240 */
241 public function fetch() {
242 $this->getSiteStats();
243 $this->pingBack();
244 }
245
246 /**
247 * @param $majorVersion
248 * @return null|string
249 */
250 private function checkBranchForNewVersion($majorVersion) {
251 $newerVersion = array(
252 'newest' => NULL,
253 'security' => NULL,
254 );
255 if (!empty($majorVersion['releases'])) {
256 foreach ($majorVersion['releases'] as $release) {
257 if (version_compare($this->localVersion, $release['version']) < 0) {
258 $newerVersion['newest'] = $release['version'];
259 if (CRM_Utils_Array::value('security', $release)) {
260 $newerVersion['security'] = $release['version'];
261 }
262 }
263 }
264 }
265 return $newerVersion;
266 }
267
268 /**
269 * Collect info about the site to be sent as pingback data.
270 */
271 private function getSiteStats() {
272 $config = CRM_Core_Config::singleton();
273 $siteKey = md5(defined('CIVICRM_SITE_KEY') ? CIVICRM_SITE_KEY : '');
274
275 // Calorie-free pingback for alphas
276 $this->stats = array('version' => $this->localVersion);
277
278 // Non-alpha versions get the full treatment
279 if ($this->localVersion && !strpos($this->localVersion, 'alpha')) {
280 $this->stats += array(
281 'hash' => md5($siteKey . $config->userFrameworkBaseURL),
282 'uf' => $config->userFramework,
283 'lang' => $config->lcMessages,
284 'co' => $config->defaultContactCountry,
285 'ufv' => $config->userSystem->getVersion(),
286 'PHP' => phpversion(),
287 'MySQL' => CRM_CORE_DAO::singleValueQuery('SELECT VERSION()'),
288 'communityMessagesUrl' => Civi::settings()->get('communityMessagesUrl'),
289 );
290 $this->getDomainStats();
291 $this->getPayProcStats();
292 $this->getEntityStats();
293 $this->getExtensionStats();
294 }
295 }
296
297 /**
298 * Get active payment processor types.
299 */
300 private function getPayProcStats() {
301 $dao = new CRM_Financial_DAO_PaymentProcessor();
302 $dao->is_active = 1;
303 $dao->find();
304 $ppTypes = array();
305
306 // Get title and id for all processor types
307 $ppTypeNames = CRM_Core_PseudoConstant::paymentProcessorType();
308
309 while ($dao->fetch()) {
310 $ppTypes[] = $ppTypeNames[$dao->payment_processor_type_id];
311 }
312 // add the .-separated list of the processor types
313 $this->stats['PPTypes'] = implode(',', array_unique($ppTypes));
314 }
315
316 /**
317 * Fetch counts from entity tables.
318 * Add info to the 'entities' array
319 */
320 private function getEntityStats() {
321 $tables = array(
322 'CRM_Activity_DAO_Activity' => 'is_test = 0',
323 'CRM_Case_DAO_Case' => 'is_deleted = 0',
324 'CRM_Contact_DAO_Contact' => 'is_deleted = 0',
325 'CRM_Contact_DAO_Relationship' => NULL,
326 'CRM_Campaign_DAO_Campaign' => NULL,
327 'CRM_Contribute_DAO_Contribution' => 'is_test = 0',
328 'CRM_Contribute_DAO_ContributionPage' => 'is_active = 1',
329 'CRM_Contribute_DAO_ContributionProduct' => NULL,
330 'CRM_Contribute_DAO_Widget' => 'is_active = 1',
331 'CRM_Core_DAO_Discount' => NULL,
332 'CRM_Price_DAO_PriceSetEntity' => NULL,
333 'CRM_Core_DAO_UFGroup' => 'is_active = 1',
334 'CRM_Event_DAO_Event' => 'is_active = 1',
335 'CRM_Event_DAO_Participant' => 'is_test = 0',
336 'CRM_Friend_DAO_Friend' => 'is_active = 1',
337 'CRM_Grant_DAO_Grant' => NULL,
338 'CRM_Mailing_DAO_Mailing' => 'is_completed = 1',
339 'CRM_Member_DAO_Membership' => 'is_test = 0',
340 'CRM_Member_DAO_MembershipBlock' => 'is_active = 1',
341 'CRM_Pledge_DAO_Pledge' => 'is_test = 0',
342 'CRM_Pledge_DAO_PledgeBlock' => NULL,
343 'CRM_Mailing_Event_DAO_Delivered' => NULL,
344 );
345 foreach ($tables as $daoName => $where) {
346 $dao = new $daoName();
347 if ($where) {
348 $dao->whereAdd($where);
349 }
350 $short_name = substr($daoName, strrpos($daoName, '_') + 1);
351 $this->stats['entities'][] = array(
352 'name' => $short_name,
353 'size' => $dao->count(),
354 );
355 }
356 }
357
358 /**
359 * Fetch stats about enabled components/extensions
360 * Add info to the 'extensions' array
361 */
362 private function getExtensionStats() {
363 // Core components
364 $config = CRM_Core_Config::singleton();
365 foreach ($config->enableComponents as $comp) {
366 $this->stats['extensions'][] = array(
367 'name' => 'org.civicrm.component.' . strtolower($comp),
368 'enabled' => 1,
369 'version' => $this->stats['version'],
370 );
371 }
372 // Contrib extensions
373 $mapper = CRM_Extension_System::singleton()->getMapper();
374 $dao = new CRM_Core_DAO_Extension();
375 $dao->find();
376 while ($dao->fetch()) {
377 $info = $mapper->keyToInfo($dao->full_name);
378 $this->stats['extensions'][] = array(
379 'name' => $dao->full_name,
380 'enabled' => $dao->is_active,
381 'version' => isset($info->version) ? $info->version : NULL,
382 );
383 }
384 }
385
386 /**
387 * Fetch stats about domain and add to 'stats' array.
388 */
389 private function getDomainStats() {
390 // Start with default value NULL, then check to see if there's a better
391 // value to be had.
392 $this->stats['domain_isoCode'] = NULL;
393 $params = array(
394 'id' => CRM_Core_Config::domainID(),
395 );
396 $domain_result = civicrm_api3('domain', 'getsingle', $params);
397 if (!empty($domain_result['contact_id'])) {
398 $address_params = array(
399 'contact_id' => $domain_result['contact_id'],
400 'is_primary' => 1,
401 'sequential' => 1,
402 );
403 $address_result = civicrm_api3('address', 'get', $address_params);
404 if ($address_result['count'] == 1 && !empty($address_result['values'][0]['country_id'])) {
405 $country_params = array(
406 'id' => $address_result['values'][0]['country_id'],
407 );
408 $country_result = civicrm_api3('country', 'getsingle', $country_params);
409 if (!empty($country_result['iso_code'])) {
410 $this->stats['domain_isoCode'] = $country_result['iso_code'];
411 }
412 }
413 }
414 }
415
416 /**
417 * Send the request to civicrm.org
418 * Store results in the cache file
419 */
420 private function pingBack() {
421 $params = array(
422 'http' => array(
423 'method' => 'POST',
424 'header' => 'Content-type: application/x-www-form-urlencoded',
425 'content' => http_build_query($this->stats),
426 ),
427 );
428 $ctx = stream_context_create($params);
429 $rawJson = file_get_contents($this->pingbackUrl, FALSE, $ctx);
430 $versionInfo = $rawJson ? json_decode($rawJson, TRUE) : NULL;
431 // If we couldn't fetch or parse the data $versionInfo will be NULL
432 // Otherwise it will be an array and we'll cache it.
433 // Note the array may be empty e.g. in the case of a pre-alpha with no releases
434 $this->isInfoAvailable = $versionInfo !== NULL;
435 if ($this->isInfoAvailable) {
436 $this->writeCacheFile($rawJson);
437 $this->setVersionInfo($versionInfo);
438 }
439 }
440
441 /**
442 * @return bool
443 */
444 private function readCacheFile() {
445 if (file_exists($this->cacheFile)) {
446 $this->setVersionInfo(json_decode(file_get_contents($this->cacheFile), TRUE));
447 return TRUE;
448 }
449 return FALSE;
450 }
451
452 /**
453 * Save version info to file.
454 * @param string $contents
455 * @throws \Exception
456 */
457 private function writeCacheFile($contents) {
458 if (file_put_contents($this->cacheFile, $contents) === FALSE) {
459 throw new Exception('File not writable');
460 }
461 }
462
463 /**
464 * Lookup version_check scheduled job
465 */
466 private function getJob() {
467 $jobs = civicrm_api3('Job', 'get', array(
468 'sequential' => 1,
469 'api_action' => "version_check",
470 'api_entity' => "job",
471 ));
472 $this->cronJob = CRM_Utils_Array::value(0, $jobs['values'], array());
473 }
474
475 }