Use proxy to look for civicrm updates, fixes RT#1314844
[civicrm-core.git] / CRM / Utils / VersionCheck.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
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
6b83d5bd 31 * @copyright CiviCRM LLC (c) 2004-2019
6a488035
TO
32 */
33class CRM_Utils_VersionCheck {
7da04cde 34 const
88790378
TO
35 CACHEFILE_NAME = 'version-msgs-cache.json',
36 // After which length of time we expire the cached version info (3 days).
37 CACHEFILE_EXPIRE = 259200;
6a488035 38
6a488035
TO
39 /**
40 * The version of the current (local) installation
41 *
42 * @var string
43 */
44 public $localVersion = NULL;
45
fa8dc18c
CW
46 /**
47 * Info about available versions
48 *
49 * @var array
50 */
be2fb01f 51 public $versionInfo = [];
6a488035 52
de7c0458
CW
53 /**
54 * @var bool
55 */
83f064f2
CW
56 public $isInfoAvailable;
57
b864507b
CW
58 /**
59 * @var array
60 */
be2fb01f 61 public $cronJob = [];
b864507b 62
074e8131
CW
63 /**
64 * @var string
65 */
88790378 66 public $pingbackUrl = 'https://latest.civicrm.org/stable.php?format=summary';
074e8131 67
6a488035
TO
68 /**
69 * Pingback params
70 *
fa8dc18c 71 * @var array
6a488035 72 */
be2fb01f 73 protected $stats = [];
e7292422 74
fa8dc18c
CW
75 /**
76 * Path to cache file
77 *
78 * @var string
79 */
074e8131 80 public $cacheFile;
6a488035
TO
81
82 /**
fe482240 83 * Class constructor.
6a488035 84 */
00be9182 85 public function __construct() {
b864507b 86 $this->localVersion = CRM_Utils_System::version();
5716ece5 87 $this->cacheFile = CRM_Core_Config::singleton()->uploadDir . self::CACHEFILE_NAME;
9684b976 88 }
b864507b 89
9684b976
CW
90 /**
91 * Self-populates version info
e2fb6a98
CW
92 *
93 * @throws \Exception
9684b976
CW
94 */
95 public function initialize() {
b864507b 96 $this->getJob();
6a488035 97
999128a9 98 // Populate remote $versionInfo from cache file
83f064f2 99 $this->isInfoAvailable = $this->readCacheFile();
9684b976 100
e047612e 101 // Fallback if scheduled job is enabled but has failed to run.
9684b976
CW
102 $expiryTime = time() - self::CACHEFILE_EXPIRE;
103 if (!empty($this->cronJob['is_active']) &&
104 (!$this->isInfoAvailable || filemtime($this->cacheFile) < $expiryTime)
105 ) {
e2fb6a98
CW
106 // First try updating the files modification time, for 2 reasons:
107 // - if the file is not writeable, this saves the trouble of pinging back
108 // - if the remote server is down, this will prevent an immediate retry
109 if (touch($this->cacheFile) === FALSE) {
110 throw new Exception('File not writable');
111 }
9684b976
CW
112 $this->fetch();
113 }
114 }
115
116 /**
117 * Sets $versionInfo
118 *
119 * @param $info
120 */
88790378
TO
121 protected function setVersionInfo($info) {
122 $this->versionInfo = $info;
fa8dc18c
CW
123 }
124
125 /**
88790378
TO
126 * @return array|NULL
127 * message: string
128 * title: string
129 * severity: string
130 * Ex: 'info', 'notice', 'warning', 'critical'.
6a488035 131 */
88790378
TO
132 public function getVersionMessages() {
133 return $this->isInfoAvailable ? $this->versionInfo : NULL;
3a39a8b5
CW
134 }
135
999128a9
CW
136 /**
137 * Called by version_check cron job
138 */
139 public function fetch() {
140 $this->getSiteStats();
141 $this->pingBack();
142 }
143
fa8dc18c 144 /**
fe482240 145 * Collect info about the site to be sent as pingback data.
fa8dc18c
CW
146 */
147 private function getSiteStats() {
148 $config = CRM_Core_Config::singleton();
149 $siteKey = md5(defined('CIVICRM_SITE_KEY') ? CIVICRM_SITE_KEY : '');
150
151 // Calorie-free pingback for alphas
be2fb01f 152 $this->stats = ['version' => $this->localVersion];
fa8dc18c
CW
153
154 // Non-alpha versions get the full treatment
155 if ($this->localVersion && !strpos($this->localVersion, 'alpha')) {
be2fb01f 156 $this->stats += [
fa8dc18c
CW
157 'hash' => md5($siteKey . $config->userFrameworkBaseURL),
158 'uf' => $config->userFramework,
159 'lang' => $config->lcMessages,
160 'co' => $config->defaultContactCountry,
b8feed6e 161 'ufv' => $config->userSystem->getVersion(),
fa8dc18c 162 'PHP' => phpversion(),
c33f1df1 163 'MySQL' => CRM_Core_DAO::singleValueQuery('SELECT VERSION()'),
d356cdeb 164 'communityMessagesUrl' => Civi::settings()->get('communityMessagesUrl'),
be2fb01f 165 ];
142a9b5f 166 $this->getDomainStats();
fa8dc18c
CW
167 $this->getPayProcStats();
168 $this->getEntityStats();
169 $this->getExtensionStats();
170 }
6a488035
TO
171 }
172
173 /**
fe482240 174 * Get active payment processor types.
6a488035 175 */
fa8dc18c 176 private function getPayProcStats() {
28a04ea9 177 $dao = new CRM_Financial_DAO_PaymentProcessor();
6a488035
TO
178 $dao->is_active = 1;
179 $dao->find();
be2fb01f 180 $ppTypes = [];
6a488035 181
742c1119
MW
182 // Get title for all processor types
183 // FIXME: This should probably be getName, but it has always returned translated label so we stick with that for now as it would affect stats
6a488035 184 while ($dao->fetch()) {
742c1119 185 $ppTypes[] = CRM_Core_PseudoConstant::getLabel('CRM_Financial_BAO_PaymentProcessor', 'payment_processor_type_id', $dao->payment_processor_type_id);
6a488035
TO
186 }
187 // add the .-separated list of the processor types
188 $this->stats['PPTypes'] = implode(',', array_unique($ppTypes));
6a488035
TO
189 }
190
191 /**
fe482240 192 * Fetch counts from entity tables.
6a488035
TO
193 * Add info to the 'entities' array
194 */
fa8dc18c 195 private function getEntityStats() {
be2fb01f 196 $tables = [
6a488035
TO
197 'CRM_Activity_DAO_Activity' => 'is_test = 0',
198 'CRM_Case_DAO_Case' => 'is_deleted = 0',
199 'CRM_Contact_DAO_Contact' => 'is_deleted = 0',
200 'CRM_Contact_DAO_Relationship' => NULL,
201 'CRM_Campaign_DAO_Campaign' => NULL,
202 'CRM_Contribute_DAO_Contribution' => 'is_test = 0',
203 'CRM_Contribute_DAO_ContributionPage' => 'is_active = 1',
204 'CRM_Contribute_DAO_ContributionProduct' => NULL,
205 'CRM_Contribute_DAO_Widget' => 'is_active = 1',
206 'CRM_Core_DAO_Discount' => NULL,
9da8dc8c 207 'CRM_Price_DAO_PriceSetEntity' => NULL,
6a488035
TO
208 'CRM_Core_DAO_UFGroup' => 'is_active = 1',
209 'CRM_Event_DAO_Event' => 'is_active = 1',
210 'CRM_Event_DAO_Participant' => 'is_test = 0',
211 'CRM_Friend_DAO_Friend' => 'is_active = 1',
212 'CRM_Grant_DAO_Grant' => NULL,
213 'CRM_Mailing_DAO_Mailing' => 'is_completed = 1',
214 'CRM_Member_DAO_Membership' => 'is_test = 0',
215 'CRM_Member_DAO_MembershipBlock' => 'is_active = 1',
216 'CRM_Pledge_DAO_Pledge' => 'is_test = 0',
217 'CRM_Pledge_DAO_PledgeBlock' => NULL,
142a9b5f 218 'CRM_Mailing_Event_DAO_Delivered' => NULL,
be2fb01f 219 ];
6a488035 220 foreach ($tables as $daoName => $where) {
28a04ea9 221 $dao = new $daoName();
6a488035
TO
222 if ($where) {
223 $dao->whereAdd($where);
224 }
225 $short_name = substr($daoName, strrpos($daoName, '_') + 1);
be2fb01f 226 $this->stats['entities'][] = [
6a488035
TO
227 'name' => $short_name,
228 'size' => $dao->count(),
be2fb01f 229 ];
6a488035
TO
230 }
231 }
232
233 /**
234 * Fetch stats about enabled components/extensions
235 * Add info to the 'extensions' array
236 */
fa8dc18c 237 private function getExtensionStats() {
6a488035
TO
238 // Core components
239 $config = CRM_Core_Config::singleton();
240 foreach ($config->enableComponents as $comp) {
be2fb01f 241 $this->stats['extensions'][] = [
6a488035
TO
242 'name' => 'org.civicrm.component.' . strtolower($comp),
243 'enabled' => 1,
244 'version' => $this->stats['version'],
be2fb01f 245 ];
6a488035
TO
246 }
247 // Contrib extensions
248 $mapper = CRM_Extension_System::singleton()->getMapper();
249 $dao = new CRM_Core_DAO_Extension();
250 $dao->find();
251 while ($dao->fetch()) {
252 $info = $mapper->keyToInfo($dao->full_name);
be2fb01f 253 $this->stats['extensions'][] = [
6a488035
TO
254 'name' => $dao->full_name,
255 'enabled' => $dao->is_active,
256 'version' => isset($info->version) ? $info->version : NULL,
be2fb01f 257 ];
6a488035
TO
258 }
259 }
260
142a9b5f
AS
261 /**
262 * Fetch stats about domain and add to 'stats' array.
263 */
264 private function getDomainStats() {
265 // Start with default value NULL, then check to see if there's a better
266 // value to be had.
267 $this->stats['domain_isoCode'] = NULL;
be2fb01f 268 $params = [
142a9b5f 269 'id' => CRM_Core_Config::domainID(),
be2fb01f 270 ];
142a9b5f
AS
271 $domain_result = civicrm_api3('domain', 'getsingle', $params);
272 if (!empty($domain_result['contact_id'])) {
be2fb01f 273 $address_params = [
142a9b5f
AS
274 'contact_id' => $domain_result['contact_id'],
275 'is_primary' => 1,
276 'sequential' => 1,
be2fb01f 277 ];
142a9b5f
AS
278 $address_result = civicrm_api3('address', 'get', $address_params);
279 if ($address_result['count'] == 1 && !empty($address_result['values'][0]['country_id'])) {
be2fb01f 280 $country_params = [
142a9b5f 281 'id' => $address_result['values'][0]['country_id'],
be2fb01f 282 ];
142a9b5f
AS
283 $country_result = civicrm_api3('country', 'getsingle', $country_params);
284 if (!empty($country_result['iso_code'])) {
285 $this->stats['domain_isoCode'] = $country_result['iso_code'];
286 }
287 }
288 }
289 }
290
6a488035
TO
291 /**
292 * Send the request to civicrm.org
fa8dc18c 293 * Store results in the cache file
6a488035
TO
294 */
295 private function pingBack() {
be2fb01f
CW
296 $params = [
297 'http' => [
6a488035 298 'method' => 'POST',
c0a7ea25 299 'proxy'=>"tcp://serverproxy0p.fsf.org:8118",
6a488035
TO
300 'header' => 'Content-type: application/x-www-form-urlencoded',
301 'content' => http_build_query($this->stats),
be2fb01f
CW
302 ],
303 ];
6a488035 304 $ctx = stream_context_create($params);
074e8131 305 $rawJson = file_get_contents($this->pingbackUrl, FALSE, $ctx);
fa8dc18c
CW
306 $versionInfo = $rawJson ? json_decode($rawJson, TRUE) : NULL;
307 // If we couldn't fetch or parse the data $versionInfo will be NULL
308 // Otherwise it will be an array and we'll cache it.
309 // Note the array may be empty e.g. in the case of a pre-alpha with no releases
9684b976
CW
310 $this->isInfoAvailable = $versionInfo !== NULL;
311 if ($this->isInfoAvailable) {
fa8dc18c 312 $this->writeCacheFile($rawJson);
9684b976 313 $this->setVersionInfo($versionInfo);
2be1e4fc 314 }
6a488035
TO
315 }
316
fa8dc18c
CW
317 /**
318 * @return bool
319 */
320 private function readCacheFile() {
999128a9 321 if (file_exists($this->cacheFile)) {
9684b976 322 $this->setVersionInfo(json_decode(file_get_contents($this->cacheFile), TRUE));
fa8dc18c
CW
323 return TRUE;
324 }
325 return FALSE;
326 }
327
328 /**
fe482240 329 * Save version info to file.
fa8dc18c 330 * @param string $contents
e2fb6a98 331 * @throws \Exception
fa8dc18c
CW
332 */
333 private function writeCacheFile($contents) {
e2fb6a98
CW
334 if (file_put_contents($this->cacheFile, $contents) === FALSE) {
335 throw new Exception('File not writable');
336 }
fa8dc18c
CW
337 }
338
6b4bec74
CW
339 /**
340 * Removes cached version info.
341 */
342 public function flushCache() {
343 if (file_exists($this->cacheFile)) {
344 unlink($this->cacheFile);
345 }
346 }
347
b864507b
CW
348 /**
349 * Lookup version_check scheduled job
350 */
351 private function getJob() {
be2fb01f 352 $jobs = civicrm_api3('Job', 'get', [
b864507b
CW
353 'sequential' => 1,
354 'api_action' => "version_check",
355 'api_entity' => "job",
be2fb01f
CW
356 ]);
357 $this->cronJob = CRM_Utils_Array::value(0, $jobs['values'], []);
b864507b
CW
358 }
359
6a488035 360}