Commit | Line | Data |
---|---|---|
6a488035 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
7e9e8871 | 4 | | CiviCRM version 4.7 | |
6a488035 | 5 | +--------------------------------------------------------------------+ |
0f03f337 | 6 | | Copyright CiviCRM LLC (c) 2004-2017 | |
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 | |
0f03f337 | 31 | * @copyright CiviCRM LLC (c) 2004-2017 |
6a488035 TO |
32 | */ |
33 | class CRM_Utils_VersionCheck { | |
7da04cde | 34 | const |
5716ece5 | 35 | CACHEFILE_NAME = 'version-info-cache.json', |
e047612e | 36 | // After which length of time we expire the cached version info (7+ days). |
9684b976 | 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 | */ | |
a80769d4 | 73 | public $pingbackUrl = 'https://latest.civicrm.org/stable.php?format=json'; |
074e8131 | 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); | |
5716ece5 | 95 | $this->cacheFile = CRM_Core_Config::singleton()->uploadDir . self::CACHEFILE_NAME; |
9684b976 | 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 | 108 | |
e047612e | 109 | // Fallback if scheduled job is enabled but has failed to run. |
9684b976 CW |
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 | 289 | ); |
142a9b5f | 290 | $this->getDomainStats(); |
fa8dc18c CW |
291 | $this->getPayProcStats(); |
292 | $this->getEntityStats(); | |
293 | $this->getExtensionStats(); | |
294 | } | |
6a488035 TO |
295 | } |
296 | ||
297 | /** | |
fe482240 | 298 | * Get active payment processor types. |
6a488035 | 299 | */ |
fa8dc18c | 300 | private function getPayProcStats() { |
28a04ea9 | 301 | $dao = new CRM_Financial_DAO_PaymentProcessor(); |
6a488035 TO |
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)); | |
6a488035 TO |
314 | } |
315 | ||
316 | /** | |
fe482240 | 317 | * Fetch counts from entity tables. |
6a488035 TO |
318 | * Add info to the 'entities' array |
319 | */ | |
fa8dc18c | 320 | private function getEntityStats() { |
6a488035 TO |
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, | |
9da8dc8c | 332 | 'CRM_Price_DAO_PriceSetEntity' => NULL, |
6a488035 TO |
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, | |
142a9b5f | 343 | 'CRM_Mailing_Event_DAO_Delivered' => NULL, |
6a488035 TO |
344 | ); |
345 | foreach ($tables as $daoName => $where) { | |
28a04ea9 | 346 | $dao = new $daoName(); |
6a488035 TO |
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 | */ | |
fa8dc18c | 362 | private function getExtensionStats() { |
6a488035 TO |
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 | ||
142a9b5f AS |
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 | ||
6a488035 TO |
416 | /** |
417 | * Send the request to civicrm.org | |
fa8dc18c | 418 | * Store results in the cache file |
6a488035 TO |
419 | */ |
420 | private function pingBack() { | |
6a488035 TO |
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); | |
074e8131 | 429 | $rawJson = file_get_contents($this->pingbackUrl, FALSE, $ctx); |
fa8dc18c CW |
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 | |
9684b976 CW |
434 | $this->isInfoAvailable = $versionInfo !== NULL; |
435 | if ($this->isInfoAvailable) { | |
fa8dc18c | 436 | $this->writeCacheFile($rawJson); |
9684b976 | 437 | $this->setVersionInfo($versionInfo); |
2be1e4fc | 438 | } |
6a488035 TO |
439 | } |
440 | ||
fa8dc18c CW |
441 | /** |
442 | * @return bool | |
443 | */ | |
444 | private function readCacheFile() { | |
999128a9 | 445 | if (file_exists($this->cacheFile)) { |
9684b976 | 446 | $this->setVersionInfo(json_decode(file_get_contents($this->cacheFile), TRUE)); |
fa8dc18c CW |
447 | return TRUE; |
448 | } | |
449 | return FALSE; | |
450 | } | |
451 | ||
452 | /** | |
fe482240 | 453 | * Save version info to file. |
fa8dc18c | 454 | * @param string $contents |
e2fb6a98 | 455 | * @throws \Exception |
fa8dc18c CW |
456 | */ |
457 | private function writeCacheFile($contents) { | |
e2fb6a98 CW |
458 | if (file_put_contents($this->cacheFile, $contents) === FALSE) { |
459 | throw new Exception('File not writable'); | |
460 | } | |
fa8dc18c CW |
461 | } |
462 | ||
b864507b CW |
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 | ||
6a488035 | 475 | } |