Commit | Line | Data |
---|---|---|
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 | */ |
33 | class 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 CW |
162 | 'PHP' => phpversion(), |
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 TO |
298 | 'method' => 'POST', |
299 | 'header' => 'Content-type: application/x-www-form-urlencoded', | |
300 | 'content' => http_build_query($this->stats), | |
be2fb01f CW |
301 | ], |
302 | ]; | |
6a488035 | 303 | $ctx = stream_context_create($params); |
074e8131 | 304 | $rawJson = file_get_contents($this->pingbackUrl, FALSE, $ctx); |
fa8dc18c CW |
305 | $versionInfo = $rawJson ? json_decode($rawJson, TRUE) : NULL; |
306 | // If we couldn't fetch or parse the data $versionInfo will be NULL | |
307 | // Otherwise it will be an array and we'll cache it. | |
308 | // Note the array may be empty e.g. in the case of a pre-alpha with no releases | |
9684b976 CW |
309 | $this->isInfoAvailable = $versionInfo !== NULL; |
310 | if ($this->isInfoAvailable) { | |
fa8dc18c | 311 | $this->writeCacheFile($rawJson); |
9684b976 | 312 | $this->setVersionInfo($versionInfo); |
2be1e4fc | 313 | } |
6a488035 TO |
314 | } |
315 | ||
fa8dc18c CW |
316 | /** |
317 | * @return bool | |
318 | */ | |
319 | private function readCacheFile() { | |
999128a9 | 320 | if (file_exists($this->cacheFile)) { |
9684b976 | 321 | $this->setVersionInfo(json_decode(file_get_contents($this->cacheFile), TRUE)); |
fa8dc18c CW |
322 | return TRUE; |
323 | } | |
324 | return FALSE; | |
325 | } | |
326 | ||
327 | /** | |
fe482240 | 328 | * Save version info to file. |
fa8dc18c | 329 | * @param string $contents |
e2fb6a98 | 330 | * @throws \Exception |
fa8dc18c CW |
331 | */ |
332 | private function writeCacheFile($contents) { | |
e2fb6a98 CW |
333 | if (file_put_contents($this->cacheFile, $contents) === FALSE) { |
334 | throw new Exception('File not writable'); | |
335 | } | |
fa8dc18c CW |
336 | } |
337 | ||
6b4bec74 CW |
338 | /** |
339 | * Removes cached version info. | |
340 | */ | |
341 | public function flushCache() { | |
342 | if (file_exists($this->cacheFile)) { | |
343 | unlink($this->cacheFile); | |
344 | } | |
345 | } | |
346 | ||
b864507b CW |
347 | /** |
348 | * Lookup version_check scheduled job | |
349 | */ | |
350 | private function getJob() { | |
be2fb01f | 351 | $jobs = civicrm_api3('Job', 'get', [ |
b864507b CW |
352 | 'sequential' => 1, |
353 | 'api_action' => "version_check", | |
354 | 'api_entity' => "job", | |
be2fb01f CW |
355 | ]); |
356 | $this->cronJob = CRM_Utils_Array::value(0, $jobs['values'], []); | |
b864507b CW |
357 | } |
358 | ||
6a488035 | 359 | } |