Commit | Line | Data |
---|---|---|
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 | */ |
17 | class 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() { |
be2fb01f | 181 | $tables = [ |
6a488035 TO |
182 | 'CRM_Activity_DAO_Activity' => 'is_test = 0', |
183 | 'CRM_Case_DAO_Case' => 'is_deleted = 0', | |
184 | 'CRM_Contact_DAO_Contact' => 'is_deleted = 0', | |
185 | 'CRM_Contact_DAO_Relationship' => NULL, | |
186 | 'CRM_Campaign_DAO_Campaign' => NULL, | |
187 | 'CRM_Contribute_DAO_Contribution' => 'is_test = 0', | |
188 | 'CRM_Contribute_DAO_ContributionPage' => 'is_active = 1', | |
189 | 'CRM_Contribute_DAO_ContributionProduct' => NULL, | |
190 | 'CRM_Contribute_DAO_Widget' => 'is_active = 1', | |
191 | 'CRM_Core_DAO_Discount' => NULL, | |
9da8dc8c | 192 | 'CRM_Price_DAO_PriceSetEntity' => NULL, |
6a488035 TO |
193 | 'CRM_Core_DAO_UFGroup' => 'is_active = 1', |
194 | 'CRM_Event_DAO_Event' => 'is_active = 1', | |
195 | 'CRM_Event_DAO_Participant' => 'is_test = 0', | |
196 | 'CRM_Friend_DAO_Friend' => 'is_active = 1', | |
197 | 'CRM_Grant_DAO_Grant' => NULL, | |
198 | 'CRM_Mailing_DAO_Mailing' => 'is_completed = 1', | |
199 | 'CRM_Member_DAO_Membership' => 'is_test = 0', | |
200 | 'CRM_Member_DAO_MembershipBlock' => 'is_active = 1', | |
201 | 'CRM_Pledge_DAO_Pledge' => 'is_test = 0', | |
202 | 'CRM_Pledge_DAO_PledgeBlock' => NULL, | |
142a9b5f | 203 | 'CRM_Mailing_Event_DAO_Delivered' => NULL, |
be2fb01f | 204 | ]; |
6a488035 | 205 | foreach ($tables as $daoName => $where) { |
28a04ea9 | 206 | $dao = new $daoName(); |
6a488035 TO |
207 | if ($where) { |
208 | $dao->whereAdd($where); | |
209 | } | |
210 | $short_name = substr($daoName, strrpos($daoName, '_') + 1); | |
be2fb01f | 211 | $this->stats['entities'][] = [ |
6a488035 TO |
212 | 'name' => $short_name, |
213 | 'size' => $dao->count(), | |
be2fb01f | 214 | ]; |
6a488035 TO |
215 | } |
216 | } | |
217 | ||
218 | /** | |
219 | * Fetch stats about enabled components/extensions | |
220 | * Add info to the 'extensions' array | |
221 | */ | |
fa8dc18c | 222 | private function getExtensionStats() { |
6a488035 TO |
223 | // Core components |
224 | $config = CRM_Core_Config::singleton(); | |
225 | foreach ($config->enableComponents as $comp) { | |
be2fb01f | 226 | $this->stats['extensions'][] = [ |
6a488035 TO |
227 | 'name' => 'org.civicrm.component.' . strtolower($comp), |
228 | 'enabled' => 1, | |
229 | 'version' => $this->stats['version'], | |
be2fb01f | 230 | ]; |
6a488035 TO |
231 | } |
232 | // Contrib extensions | |
233 | $mapper = CRM_Extension_System::singleton()->getMapper(); | |
234 | $dao = new CRM_Core_DAO_Extension(); | |
235 | $dao->find(); | |
236 | while ($dao->fetch()) { | |
237 | $info = $mapper->keyToInfo($dao->full_name); | |
be2fb01f | 238 | $this->stats['extensions'][] = [ |
6a488035 TO |
239 | 'name' => $dao->full_name, |
240 | 'enabled' => $dao->is_active, | |
2e1f50d6 | 241 | 'version' => $info->version ?? NULL, |
be2fb01f | 242 | ]; |
6a488035 TO |
243 | } |
244 | } | |
245 | ||
142a9b5f AS |
246 | /** |
247 | * Fetch stats about domain and add to 'stats' array. | |
248 | */ | |
249 | private function getDomainStats() { | |
250 | // Start with default value NULL, then check to see if there's a better | |
251 | // value to be had. | |
252 | $this->stats['domain_isoCode'] = NULL; | |
be2fb01f | 253 | $params = [ |
142a9b5f | 254 | 'id' => CRM_Core_Config::domainID(), |
be2fb01f | 255 | ]; |
142a9b5f AS |
256 | $domain_result = civicrm_api3('domain', 'getsingle', $params); |
257 | if (!empty($domain_result['contact_id'])) { | |
be2fb01f | 258 | $address_params = [ |
142a9b5f AS |
259 | 'contact_id' => $domain_result['contact_id'], |
260 | 'is_primary' => 1, | |
261 | 'sequential' => 1, | |
be2fb01f | 262 | ]; |
142a9b5f AS |
263 | $address_result = civicrm_api3('address', 'get', $address_params); |
264 | if ($address_result['count'] == 1 && !empty($address_result['values'][0]['country_id'])) { | |
be2fb01f | 265 | $country_params = [ |
142a9b5f | 266 | 'id' => $address_result['values'][0]['country_id'], |
be2fb01f | 267 | ]; |
142a9b5f AS |
268 | $country_result = civicrm_api3('country', 'getsingle', $country_params); |
269 | if (!empty($country_result['iso_code'])) { | |
270 | $this->stats['domain_isoCode'] = $country_result['iso_code']; | |
271 | } | |
272 | } | |
273 | } | |
274 | } | |
275 | ||
6a488035 TO |
276 | /** |
277 | * Send the request to civicrm.org | |
fa8dc18c | 278 | * Store results in the cache file |
6a488035 TO |
279 | */ |
280 | private function pingBack() { | |
be2fb01f CW |
281 | $params = [ |
282 | 'http' => [ | |
6a488035 TO |
283 | 'method' => 'POST', |
284 | 'header' => 'Content-type: application/x-www-form-urlencoded', | |
285 | 'content' => http_build_query($this->stats), | |
be2fb01f CW |
286 | ], |
287 | ]; | |
6a488035 | 288 | $ctx = stream_context_create($params); |
074e8131 | 289 | $rawJson = file_get_contents($this->pingbackUrl, FALSE, $ctx); |
fa8dc18c CW |
290 | $versionInfo = $rawJson ? json_decode($rawJson, TRUE) : NULL; |
291 | // If we couldn't fetch or parse the data $versionInfo will be NULL | |
292 | // Otherwise it will be an array and we'll cache it. | |
293 | // Note the array may be empty e.g. in the case of a pre-alpha with no releases | |
9684b976 CW |
294 | $this->isInfoAvailable = $versionInfo !== NULL; |
295 | if ($this->isInfoAvailable) { | |
fa8dc18c | 296 | $this->writeCacheFile($rawJson); |
9684b976 | 297 | $this->setVersionInfo($versionInfo); |
2be1e4fc | 298 | } |
6a488035 TO |
299 | } |
300 | ||
fa8dc18c CW |
301 | /** |
302 | * @return bool | |
303 | */ | |
304 | private function readCacheFile() { | |
999128a9 | 305 | if (file_exists($this->cacheFile)) { |
9684b976 | 306 | $this->setVersionInfo(json_decode(file_get_contents($this->cacheFile), TRUE)); |
fa8dc18c CW |
307 | return TRUE; |
308 | } | |
309 | return FALSE; | |
310 | } | |
311 | ||
312 | /** | |
fe482240 | 313 | * Save version info to file. |
fa8dc18c | 314 | * @param string $contents |
e2fb6a98 | 315 | * @throws \Exception |
fa8dc18c CW |
316 | */ |
317 | private function writeCacheFile($contents) { | |
e2fb6a98 CW |
318 | if (file_put_contents($this->cacheFile, $contents) === FALSE) { |
319 | throw new Exception('File not writable'); | |
320 | } | |
fa8dc18c CW |
321 | } |
322 | ||
6b4bec74 CW |
323 | /** |
324 | * Removes cached version info. | |
325 | */ | |
326 | public function flushCache() { | |
327 | if (file_exists($this->cacheFile)) { | |
328 | unlink($this->cacheFile); | |
329 | } | |
330 | } | |
331 | ||
b864507b CW |
332 | /** |
333 | * Lookup version_check scheduled job | |
334 | */ | |
335 | private function getJob() { | |
be2fb01f | 336 | $jobs = civicrm_api3('Job', 'get', [ |
b864507b CW |
337 | 'sequential' => 1, |
338 | 'api_action' => "version_check", | |
339 | 'api_entity' => "job", | |
be2fb01f CW |
340 | ]); |
341 | $this->cronJob = CRM_Utils_Array::value(0, $jobs['values'], []); | |
b864507b CW |
342 | } |
343 | ||
6a488035 | 344 | } |