composer.json - Move ezc components from packages to composer.json
[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
9684b976
CW
35 CACHEFILE_NAME = '[civicrm.files]/persist/version-info-cache.json',
36 // after this length of time we fall back on poor-man's cron (7+ days)
37 CACHEFILE_EXPIRE = 605000;
6a488035 38
6a488035
TO
39 /**
40 * The version of the current (local) installation
41 *
42 * @var string
43 */
44 public $localVersion = NULL;
45
46 /**
fa8dc18c 47 * The major version (branch name) of the local version
6a488035
TO
48 *
49 * @var string
50 */
fa8dc18c
CW
51 public $localMajorVersion;
52
53 /**
54 * Info about available versions
55 *
56 * @var array
57 */
58 public $versionInfo = array();
6a488035 59
de7c0458
CW
60 /**
61 * @var bool
62 */
83f064f2
CW
63 public $isInfoAvailable;
64
b864507b
CW
65 /**
66 * @var array
67 */
68 public $cronJob = array();
69
074e8131
CW
70 /**
71 * @var string
72 */
73 public $pingbackUrl = 'http://latest.civicrm.org/stable.php?format=json';
74
6a488035
TO
75 /**
76 * Pingback params
77 *
fa8dc18c 78 * @var array
6a488035
TO
79 */
80 protected $stats = array();
e7292422 81
fa8dc18c
CW
82 /**
83 * Path to cache file
84 *
85 * @var string
86 */
074e8131 87 public $cacheFile;
6a488035
TO
88
89 /**
fe482240 90 * Class constructor.
6a488035 91 */
00be9182 92 public function __construct() {
b864507b
CW
93 $this->localVersion = CRM_Utils_System::version();
94 $this->localMajorVersion = $this->getMajorVersion($this->localVersion);
9684b976
CW
95 $this->cacheFile = Civi::paths()->getPath(self::CACHEFILE_NAME);
96 }
b864507b 97
9684b976
CW
98 /**
99 * Self-populates version info
e2fb6a98
CW
100 *
101 * @throws \Exception
9684b976
CW
102 */
103 public function initialize() {
b864507b 104 $this->getJob();
6a488035 105
999128a9 106 // Populate remote $versionInfo from cache file
83f064f2 107 $this->isInfoAvailable = $this->readCacheFile();
9684b976
CW
108
109 // Poor-man's cron 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 ) {
e2fb6a98
CW
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 }
9684b976
CW
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);
6a488035
TO
133 }
134
fa8dc18c 135 /**
fe482240 136 * Finds the release info for a minor version.
fa8dc18c
CW
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
fa8dc18c 164
6a488035
TO
165 /**
166 * Get the latest version number if it's newer than the local one
167 *
06576a03
AH
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
6a488035 172 */
fa8dc18c 173 public function isNewerVersionAvailable() {
06576a03
AH
174 $return = array(
175 'version' => NULL,
176 'upgrade' => NULL,
177 'status' => NULL,
178 );
179
fa8dc18c 180 if ($this->versionInfo && $this->localVersion) {
06576a03 181 if (isset($this->versionInfo[$this->localMajorVersion])) {
097c681e 182 switch (CRM_Utils_Array::value('status', $this->versionInfo[$this->localMajorVersion])) {
06576a03
AH
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 );
fa8dc18c
CW
231 }
232 }
3a39a8b5 233 }
06576a03
AH
234
235 return $return;
3a39a8b5
CW
236 }
237
999128a9
CW
238 /**
239 * Called by version_check cron job
240 */
241 public function fetch() {
242 $this->getSiteStats();
243 $this->pingBack();
244 }
245
3a39a8b5
CW
246 /**
247 * @param $majorVersion
248 * @return null|string
249 */
250 private function checkBranchForNewVersion($majorVersion) {
06576a03
AH
251 $newerVersion = array(
252 'newest' => NULL,
253 'security' => NULL,
254 );
3a39a8b5
CW
255 if (!empty($majorVersion['releases'])) {
256 foreach ($majorVersion['releases'] as $release) {
257 if (version_compare($this->localVersion, $release['version']) < 0) {
bf4b8752
CW
258 $newerVersion['newest'] = $release['version'];
259 if (CRM_Utils_Array::value('security', $release)) {
260 $newerVersion['security'] = $release['version'];
fa8dc18c
CW
261 }
262 }
6a488035
TO
263 }
264 }
fa8dc18c 265 return $newerVersion;
6a488035
TO
266 }
267
fa8dc18c 268 /**
fe482240 269 * Collect info about the site to be sent as pingback data.
fa8dc18c
CW
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,
b8feed6e 285 'ufv' => $config->userSystem->getVersion(),
fa8dc18c
CW
286 'PHP' => phpversion(),
287 'MySQL' => CRM_CORE_DAO::singleValueQuery('SELECT VERSION()'),
d356cdeb 288 'communityMessagesUrl' => Civi::settings()->get('communityMessagesUrl'),
fa8dc18c
CW
289 );
290 $this->getPayProcStats();
291 $this->getEntityStats();
292 $this->getExtensionStats();
293 }
6a488035
TO
294 }
295
296 /**
fe482240 297 * Get active payment processor types.
6a488035 298 */
fa8dc18c 299 private function getPayProcStats() {
28a04ea9 300 $dao = new CRM_Financial_DAO_PaymentProcessor();
6a488035
TO
301 $dao->is_active = 1;
302 $dao->find();
303 $ppTypes = array();
304
305 // Get title and id for all processor types
306 $ppTypeNames = CRM_Core_PseudoConstant::paymentProcessorType();
307
308 while ($dao->fetch()) {
309 $ppTypes[] = $ppTypeNames[$dao->payment_processor_type_id];
310 }
311 // add the .-separated list of the processor types
312 $this->stats['PPTypes'] = implode(',', array_unique($ppTypes));
6a488035
TO
313 }
314
315 /**
fe482240 316 * Fetch counts from entity tables.
6a488035
TO
317 * Add info to the 'entities' array
318 */
fa8dc18c 319 private function getEntityStats() {
6a488035
TO
320 $tables = array(
321 'CRM_Activity_DAO_Activity' => 'is_test = 0',
322 'CRM_Case_DAO_Case' => 'is_deleted = 0',
323 'CRM_Contact_DAO_Contact' => 'is_deleted = 0',
324 'CRM_Contact_DAO_Relationship' => NULL,
325 'CRM_Campaign_DAO_Campaign' => NULL,
326 'CRM_Contribute_DAO_Contribution' => 'is_test = 0',
327 'CRM_Contribute_DAO_ContributionPage' => 'is_active = 1',
328 'CRM_Contribute_DAO_ContributionProduct' => NULL,
329 'CRM_Contribute_DAO_Widget' => 'is_active = 1',
330 'CRM_Core_DAO_Discount' => NULL,
9da8dc8c 331 'CRM_Price_DAO_PriceSetEntity' => NULL,
6a488035
TO
332 'CRM_Core_DAO_UFGroup' => 'is_active = 1',
333 'CRM_Event_DAO_Event' => 'is_active = 1',
334 'CRM_Event_DAO_Participant' => 'is_test = 0',
335 'CRM_Friend_DAO_Friend' => 'is_active = 1',
336 'CRM_Grant_DAO_Grant' => NULL,
337 'CRM_Mailing_DAO_Mailing' => 'is_completed = 1',
338 'CRM_Member_DAO_Membership' => 'is_test = 0',
339 'CRM_Member_DAO_MembershipBlock' => 'is_active = 1',
340 'CRM_Pledge_DAO_Pledge' => 'is_test = 0',
341 'CRM_Pledge_DAO_PledgeBlock' => NULL,
342 );
343 foreach ($tables as $daoName => $where) {
28a04ea9 344 $dao = new $daoName();
6a488035
TO
345 if ($where) {
346 $dao->whereAdd($where);
347 }
348 $short_name = substr($daoName, strrpos($daoName, '_') + 1);
349 $this->stats['entities'][] = array(
350 'name' => $short_name,
351 'size' => $dao->count(),
352 );
353 }
354 }
355
356 /**
357 * Fetch stats about enabled components/extensions
358 * Add info to the 'extensions' array
359 */
fa8dc18c 360 private function getExtensionStats() {
6a488035
TO
361 // Core components
362 $config = CRM_Core_Config::singleton();
363 foreach ($config->enableComponents as $comp) {
364 $this->stats['extensions'][] = array(
365 'name' => 'org.civicrm.component.' . strtolower($comp),
366 'enabled' => 1,
367 'version' => $this->stats['version'],
368 );
369 }
370 // Contrib extensions
371 $mapper = CRM_Extension_System::singleton()->getMapper();
372 $dao = new CRM_Core_DAO_Extension();
373 $dao->find();
374 while ($dao->fetch()) {
375 $info = $mapper->keyToInfo($dao->full_name);
376 $this->stats['extensions'][] = array(
377 'name' => $dao->full_name,
378 'enabled' => $dao->is_active,
379 'version' => isset($info->version) ? $info->version : NULL,
380 );
381 }
382 }
383
384 /**
385 * Send the request to civicrm.org
fa8dc18c 386 * Store results in the cache file
6a488035
TO
387 */
388 private function pingBack() {
6a488035
TO
389 $params = array(
390 'http' => array(
391 'method' => 'POST',
392 'header' => 'Content-type: application/x-www-form-urlencoded',
393 'content' => http_build_query($this->stats),
394 ),
395 );
396 $ctx = stream_context_create($params);
074e8131 397 $rawJson = file_get_contents($this->pingbackUrl, FALSE, $ctx);
fa8dc18c
CW
398 $versionInfo = $rawJson ? json_decode($rawJson, TRUE) : NULL;
399 // If we couldn't fetch or parse the data $versionInfo will be NULL
400 // Otherwise it will be an array and we'll cache it.
401 // Note the array may be empty e.g. in the case of a pre-alpha with no releases
9684b976
CW
402 $this->isInfoAvailable = $versionInfo !== NULL;
403 if ($this->isInfoAvailable) {
fa8dc18c 404 $this->writeCacheFile($rawJson);
9684b976 405 $this->setVersionInfo($versionInfo);
2be1e4fc 406 }
6a488035
TO
407 }
408
fa8dc18c
CW
409 /**
410 * @return bool
411 */
412 private function readCacheFile() {
999128a9 413 if (file_exists($this->cacheFile)) {
9684b976 414 $this->setVersionInfo(json_decode(file_get_contents($this->cacheFile), TRUE));
fa8dc18c
CW
415 return TRUE;
416 }
417 return FALSE;
418 }
419
420 /**
fe482240 421 * Save version info to file.
fa8dc18c 422 * @param string $contents
e2fb6a98 423 * @throws \Exception
fa8dc18c
CW
424 */
425 private function writeCacheFile($contents) {
e2fb6a98
CW
426 if (file_put_contents($this->cacheFile, $contents) === FALSE) {
427 throw new Exception('File not writable');
428 }
fa8dc18c
CW
429 }
430
b864507b
CW
431 /**
432 * Lookup version_check scheduled job
433 */
434 private function getJob() {
435 $jobs = civicrm_api3('Job', 'get', array(
436 'sequential' => 1,
437 'api_action' => "version_check",
438 'api_entity' => "job",
439 ));
440 $this->cronJob = CRM_Utils_Array::value(0, $jobs['values'], array());
441 }
442
6a488035 443}