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