Commit | Line | Data |
---|---|---|
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 | * $Id: $ |
33 | * | |
34 | */ | |
35 | class CRM_Utils_VersionCheck { | |
7da04cde | 36 | const |
fa8dc18c | 37 | PINGBACK_URL = 'http://latest.civicrm.org/stable.php?format=json', |
6a488035 TO |
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 | |
fa8dc18c | 43 | CACHEFILE_NAME = 'version-info-cache.json', |
42f366d7 CW |
44 | // cachefile expiry time (in seconds) - one day |
45 | CACHEFILE_EXPIRE = 86400; | |
6a488035 TO |
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 | |
6a488035 TO |
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 | /** | |
fa8dc18c | 63 | * The major version (branch name) of the local version |
6a488035 TO |
64 | * |
65 | * @var string | |
66 | */ | |
fa8dc18c CW |
67 | public $localMajorVersion; |
68 | ||
3a39a8b5 CW |
69 | /** |
70 | * User setting to skip updates prior to a certain date | |
71 | * | |
72 | * @var string | |
73 | */ | |
74 | public $ignoreDate; | |
75 | ||
fa8dc18c CW |
76 | /** |
77 | * Info about available versions | |
78 | * | |
79 | * @var array | |
80 | */ | |
81 | public $versionInfo = array(); | |
6a488035 TO |
82 | |
83 | /** | |
84 | * Pingback params | |
85 | * | |
fa8dc18c | 86 | * @var array |
6a488035 TO |
87 | */ |
88 | protected $stats = array(); | |
e7292422 | 89 | |
fa8dc18c CW |
90 | /** |
91 | * Path to cache file | |
92 | * | |
93 | * @var string | |
94 | */ | |
95 | protected $cacheFile; | |
6a488035 TO |
96 | |
97 | /** | |
fe482240 | 98 | * Class constructor. |
6a488035 | 99 | */ |
00be9182 | 100 | public function __construct() { |
6a488035 TO |
101 | global $civicrm_root; |
102 | $config = CRM_Core_Config::singleton(); | |
103 | ||
fa8dc18c CW |
104 | $localFile = $civicrm_root . DIRECTORY_SEPARATOR . self::LOCALFILE_NAME; |
105 | $this->cacheFile = $config->uploadDir . self::CACHEFILE_NAME; | |
6a488035 | 106 | |
fa8dc18c | 107 | if (file_exists($localFile)) { |
e7292422 | 108 | require_once $localFile; |
6a488035 | 109 | } |
fa8dc18c CW |
110 | if (function_exists('civicrmVersion')) { |
111 | $info = civicrmVersion(); | |
112 | $this->localVersion = trim($info['version']); | |
113 | $this->localMajorVersion = $this->getMajorVersion($this->localVersion); | |
114 | } | |
3a39a8b5 | 115 | // Populate $versionInfo |
fa8dc18c CW |
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(); | |
6a488035 TO |
121 | |
122 | // Get the latest version and send site info | |
123 | $this->pingBack(); | |
fa8dc18c | 124 | } |
3a39a8b5 | 125 | $this->ignoreDate = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'versionCheckIgnoreDate'); |
6a488035 | 126 | |
3a39a8b5 CW |
127 | // Sort version info in ascending order for easier comparisons |
128 | ksort($this->versionInfo, SORT_NUMERIC); | |
6a488035 TO |
129 | } |
130 | } | |
131 | ||
132 | /** | |
fe482240 | 133 | * Static instance provider. |
6a488035 TO |
134 | * |
135 | * Method providing static instance of CRM_Utils_VersionCheck, | |
136 | * as in Singleton pattern | |
137 | * | |
138 | * @return CRM_Utils_VersionCheck | |
139 | */ | |
00be9182 | 140 | public static function &singleton() { |
6a488035 TO |
141 | if (!isset(self::$_singleton)) { |
142 | self::$_singleton = new CRM_Utils_VersionCheck(); | |
143 | } | |
144 | return self::$_singleton; | |
145 | } | |
146 | ||
fa8dc18c | 147 | /** |
fe482240 | 148 | * Finds the release info for a minor version. |
fa8dc18c CW |
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 | ||
fa8dc18c | 176 | |
6a488035 TO |
177 | /** |
178 | * Get the latest version number if it's newer than the local one | |
179 | * | |
06576a03 AH |
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 | |
6a488035 | 184 | */ |
fa8dc18c | 185 | public function isNewerVersionAvailable() { |
06576a03 AH |
186 | $return = array( |
187 | 'version' => NULL, | |
188 | 'upgrade' => NULL, | |
189 | 'status' => NULL, | |
190 | ); | |
191 | ||
fa8dc18c | 192 | if ($this->versionInfo && $this->localVersion) { |
06576a03 | 193 | if (isset($this->versionInfo[$this->localMajorVersion])) { |
097c681e | 194 | switch (CRM_Utils_Array::value('status', $this->versionInfo[$this->localMajorVersion])) { |
06576a03 AH |
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 | ); | |
fa8dc18c CW |
243 | } |
244 | } | |
3a39a8b5 | 245 | } |
06576a03 AH |
246 | |
247 | return $return; | |
3a39a8b5 CW |
248 | } |
249 | ||
250 | /** | |
251 | * @param $majorVersion | |
252 | * @return null|string | |
253 | */ | |
254 | private function checkBranchForNewVersion($majorVersion) { | |
06576a03 AH |
255 | $newerVersion = array( |
256 | 'newest' => NULL, | |
257 | 'security' => NULL, | |
258 | ); | |
3a39a8b5 CW |
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']) { | |
06576a03 | 263 | $newerVersion['newest'] = $release['version']; |
097c681e | 264 | if (CRM_Utils_Array::value('security', $release)) { |
06576a03 AH |
265 | $newerVersion['security'] = $release['version']; |
266 | } | |
fa8dc18c CW |
267 | } |
268 | } | |
6a488035 TO |
269 | } |
270 | } | |
fa8dc18c | 271 | return $newerVersion; |
6a488035 TO |
272 | } |
273 | ||
fa8dc18c | 274 | /** |
fe482240 | 275 | * Collect info about the site to be sent as pingback data. |
fa8dc18c CW |
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, | |
b8feed6e | 291 | 'ufv' => $config->userSystem->getVersion(), |
fa8dc18c CW |
292 | 'PHP' => phpversion(), |
293 | 'MySQL' => CRM_CORE_DAO::singleValueQuery('SELECT VERSION()'), | |
d356cdeb | 294 | 'communityMessagesUrl' => Civi::settings()->get('communityMessagesUrl'), |
fa8dc18c CW |
295 | ); |
296 | $this->getPayProcStats(); | |
297 | $this->getEntityStats(); | |
298 | $this->getExtensionStats(); | |
299 | } | |
6a488035 TO |
300 | } |
301 | ||
302 | /** | |
fe482240 | 303 | * Get active payment processor types. |
6a488035 | 304 | */ |
fa8dc18c | 305 | private function getPayProcStats() { |
28a04ea9 | 306 | $dao = new CRM_Financial_DAO_PaymentProcessor(); |
6a488035 TO |
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)); | |
6a488035 TO |
319 | } |
320 | ||
321 | /** | |
fe482240 | 322 | * Fetch counts from entity tables. |
6a488035 TO |
323 | * Add info to the 'entities' array |
324 | */ | |
fa8dc18c | 325 | private function getEntityStats() { |
6a488035 TO |
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, | |
9da8dc8c | 337 | 'CRM_Price_DAO_PriceSetEntity' => NULL, |
6a488035 TO |
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) { | |
28a04ea9 | 350 | $dao = new $daoName(); |
6a488035 TO |
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 | */ | |
fa8dc18c | 366 | private function getExtensionStats() { |
6a488035 TO |
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 | |
fa8dc18c | 393 | * Store results in the cache file |
6a488035 TO |
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); | |
fa8dc18c CW |
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; | |
2be1e4fc | 413 | } |
6a488035 TO |
414 | ini_restore('default_socket_timeout'); |
415 | } | |
416 | ||
fa8dc18c CW |
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 | /** | |
fe482240 | 432 | * Save version info to file. |
fa8dc18c CW |
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 | ||
6a488035 | 449 | } |