3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 |
9 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
17 class CRM_Utils_VersionCheck
{
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;
24 * The version of the current (local) installation
28 public $localVersion = NULL;
31 * Info about available versions
35 public $versionInfo = [];
40 public $isInfoAvailable;
50 public $pingbackUrl = 'https://latest.civicrm.org/stable.php?format=summary';
57 protected $stats = [];
69 public function __construct() {
70 $this->localVersion
= CRM_Utils_System
::version();
71 $this->cacheFile
= CRM_Core_Config
::singleton()->uploadDir
. self
::CACHEFILE_NAME
;
75 * Self-populates version info
80 public function initialize($force = FALSE) {
83 // Populate remote $versionInfo from cache file
84 $this->isInfoAvailable
= $this->readCacheFile();
86 // Fallback if scheduled job is enabled but has failed to run.
87 $expiryTime = time() - self
::CACHEFILE_EXPIRE
;
88 if ($force ||
(!empty($this->cronJob
['is_active']) &&
89 (!$this->isInfoAvailable ||
filemtime($this->cacheFile
) < $expiryTime)
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');
106 protected function setVersionInfo($info) {
107 $this->versionInfo
= $info;
115 * Ex: 'info', 'notice', 'warning', 'critical'.
117 public function getVersionMessages() {
118 return $this->isInfoAvailable ?
$this->versionInfo
: NULL;
122 * Called by version_check cron job
124 public function fetch() {
125 $this->getSiteStats();
130 * Collect info about the site to be sent as pingback data.
132 private function getSiteStats() {
133 $config = CRM_Core_Config
::singleton();
134 $siteKey = md5(defined('CIVICRM_SITE_KEY') ? CIVICRM_SITE_KEY
: '');
136 // Calorie-free pingback for alphas
137 $this->stats
= ['version' => $this->localVersion
];
139 // Non-alpha versions get the full treatment
140 if ($this->localVersion
&& !strpos($this->localVersion
, 'alpha')) {
142 'hash' => md5($siteKey . $config->userFrameworkBaseURL
),
143 'uf' => $config->userFramework
,
144 'lang' => $config->lcMessages
,
145 'co' => $config->defaultContactCountry
,
146 'ufv' => $config->userSystem
->getVersion(),
147 'PHP' => phpversion(),
148 'MySQL' => CRM_Core_DAO
::singleValueQuery('SELECT VERSION()'),
149 'communityMessagesUrl' => Civi
::settings()->get('communityMessagesUrl'),
151 $this->getDomainStats();
152 $this->getPayProcStats();
153 $this->getEntityStats();
154 $this->getExtensionStats();
159 * Get active payment processor types.
161 private function getPayProcStats() {
162 $dao = new CRM_Financial_DAO_PaymentProcessor();
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
169 while ($dao->fetch()) {
170 $ppTypes[] = CRM_Core_PseudoConstant
::getLabel('CRM_Financial_BAO_PaymentProcessor', 'payment_processor_type_id', $dao->payment_processor_type_id
);
172 // add the .-separated list of the processor types
173 $this->stats
['PPTypes'] = implode(',', array_unique($ppTypes));
177 * Fetch counts from entity tables.
178 * Add info to the 'entities' array
180 private function getEntityStats() {
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,
192 'CRM_Price_DAO_PriceSetEntity' => NULL,
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,
203 'CRM_Mailing_Event_DAO_Delivered' => NULL,
205 foreach ($tables as $daoName => $where) {
206 $dao = new $daoName();
208 $dao->whereAdd($where);
210 $short_name = substr($daoName, strrpos($daoName, '_') +
1);
211 $this->stats
['entities'][] = [
212 'name' => $short_name,
213 'size' => $dao->count(),
219 * Fetch stats about enabled components/extensions
220 * Add info to the 'extensions' array
222 private function getExtensionStats() {
224 $config = CRM_Core_Config
::singleton();
225 foreach ($config->enableComponents
as $comp) {
226 $this->stats
['extensions'][] = [
227 'name' => 'org.civicrm.component.' . strtolower($comp),
229 'version' => $this->stats
['version'],
232 // Contrib extensions
233 $mapper = CRM_Extension_System
::singleton()->getMapper();
234 $dao = new CRM_Core_DAO_Extension();
236 while ($dao->fetch()) {
237 $info = $mapper->keyToInfo($dao->full_name
);
238 $this->stats
['extensions'][] = [
239 'name' => $dao->full_name
,
240 'enabled' => $dao->is_active
,
241 'version' => $info->version ??
NULL,
247 * Fetch stats about domain and add to 'stats' array.
249 private function getDomainStats() {
250 // Start with default value NULL, then check to see if there's a better
252 $this->stats
['domain_isoCode'] = NULL;
254 'id' => CRM_Core_Config
::domainID(),
256 $domain_result = civicrm_api3('domain', 'getsingle', $params);
257 if (!empty($domain_result['contact_id'])) {
259 'contact_id' => $domain_result['contact_id'],
263 $address_result = civicrm_api3('address', 'get', $address_params);
264 if ($address_result['count'] == 1 && !empty($address_result['values'][0]['country_id'])) {
266 'id' => $address_result['values'][0]['country_id'],
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'];
277 * Send the request to civicrm.org
278 * Store results in the cache file
280 private function pingBack() {
284 'header' => 'Content-type: application/x-www-form-urlencoded',
285 'content' => http_build_query($this->stats
),
288 $ctx = stream_context_create($params);
289 $rawJson = file_get_contents($this->pingbackUrl
, FALSE, $ctx);
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
294 $this->isInfoAvailable
= $versionInfo !== NULL;
295 if ($this->isInfoAvailable
) {
296 $this->writeCacheFile($rawJson);
297 $this->setVersionInfo($versionInfo);
304 private function readCacheFile() {
305 if (file_exists($this->cacheFile
)) {
306 $this->setVersionInfo(json_decode(file_get_contents($this->cacheFile
), TRUE));
313 * Save version info to file.
314 * @param string $contents
317 private function writeCacheFile($contents) {
318 if (file_put_contents($this->cacheFile
, $contents) === FALSE) {
319 throw new Exception('File not writable');
324 * Removes cached version info.
326 public function flushCache() {
327 if (file_exists($this->cacheFile
)) {
328 unlink($this->cacheFile
);
333 * Lookup version_check scheduled job
335 private function getJob() {
336 $jobs = civicrm_api3('Job', 'get', [
338 'api_action' => "version_check",
339 'api_entity' => "job",
341 $this->cronJob
= CRM_Utils_Array
::value(0, $jobs['values'], []);