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