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