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