uploadDir CACHEFILE_NAME = '[civicrm.files]/persist/version-info-cache.json'; /** * The version of the current (local) installation * * @var string */ public $localVersion = NULL; /** * The major version (branch name) of the local version * * @var string */ public $localMajorVersion; /** * Info about available versions * * @var array */ public $versionInfo = array(); /** @var bool */ public $isInfoAvailable; /** * Pingback params * * @var array */ protected $stats = array(); /** * Path to cache file * * @var string */ protected $cacheFile; /** * Class constructor. */ public function __construct() { // Populate local version $localFile = Civi::paths()->getPath(self::LOCALFILE_NAME); if (file_exists($localFile)) { require_once $localFile; } if (function_exists('civicrmVersion')) { $info = civicrmVersion(); $this->localVersion = trim($info['version']); $this->localMajorVersion = $this->getMajorVersion($this->localVersion); } // Populate remote $versionInfo from cache file $this->cacheFile = Civi::paths()->getPath(self::CACHEFILE_NAME); $this->isInfoAvailable = $this->readCacheFile(); } /** * Finds the release info for a minor version. * @param string $version * @return array|null */ public function getReleaseInfo($version) { $majorVersion = $this->getMajorVersion($version); if (isset($this->versionInfo[$majorVersion])) { foreach ($this->versionInfo[$majorVersion]['releases'] as $info) { if ($info['version'] == $version) { return $info; } } } return NULL; } /** * @param $minorVersion * @return string */ public function getMajorVersion($minorVersion) { if (!$minorVersion) { return NULL; } list($a, $b) = explode('.', $minorVersion); return "$a.$b"; } /** * Get the latest version number if it's newer than the local one * * @return array * Returns version number of the latest release if it is greater than the local version, * along with the type of upgrade (regular/security) needed and the status of the major * version */ public function isNewerVersionAvailable() { $return = array( 'version' => NULL, 'upgrade' => NULL, 'status' => NULL, ); if ($this->versionInfo && $this->localVersion) { if (isset($this->versionInfo[$this->localMajorVersion])) { switch (CRM_Utils_Array::value('status', $this->versionInfo[$this->localMajorVersion])) { case 'stable': case 'lts': case 'testing': // look for latest version in this major version $releases = $this->checkBranchForNewVersion($this->versionInfo[$this->localMajorVersion]); if ($releases['newest']) { $return['version'] = $releases['newest']; // check for intervening security releases $return['upgrade'] = ($releases['security']) ? 'security' : 'regular'; } break; case 'eol': default: // look for latest version ever foreach ($this->versionInfo as $majorVersionNumber => $majorVersion) { if ($majorVersionNumber < $this->localMajorVersion || $majorVersion['status'] == 'testing') { continue; } $releases = $this->checkBranchForNewVersion($this->versionInfo[$majorVersionNumber]); if ($releases['newest']) { $return['version'] = $releases['newest']; // check for intervening security releases $return['upgrade'] = ($releases['security'] || $return['upgrade'] == 'security') ? 'security' : 'regular'; } } } $return['status'] = $this->versionInfo[$this->localMajorVersion]['status']; } else { // Figure if the version is really old or really new $wayOld = TRUE; foreach ($this->versionInfo as $majorVersionNumber => $majorVersion) { $wayOld = ($this->localMajorVersion < $majorVersionNumber); } if ($wayOld) { $releases = $this->checkBranchForNewVersion($majorVersion); $return = array( 'version' => $releases['newest'], 'upgrade' => 'security', 'status' => 'eol', ); } } } return $return; } /** * Called by version_check cron job */ public function fetch() { $this->getSiteStats(); $this->pingBack(); } /** * @param $majorVersion * @return null|string */ private function checkBranchForNewVersion($majorVersion) { $newerVersion = array( 'newest' => NULL, 'security' => NULL, ); if (!empty($majorVersion['releases'])) { foreach ($majorVersion['releases'] as $release) { if (version_compare($this->localVersion, $release['version']) < 0) { $newerVersion['newest'] = $release['version']; if (CRM_Utils_Array::value('security', $release)) { $newerVersion['security'] = $release['version']; } } } } return $newerVersion; } /** * Collect info about the site to be sent as pingback data. */ private function getSiteStats() { $config = CRM_Core_Config::singleton(); $siteKey = md5(defined('CIVICRM_SITE_KEY') ? CIVICRM_SITE_KEY : ''); // Calorie-free pingback for alphas $this->stats = array('version' => $this->localVersion); // Non-alpha versions get the full treatment if ($this->localVersion && !strpos($this->localVersion, 'alpha')) { $this->stats += array( 'hash' => md5($siteKey . $config->userFrameworkBaseURL), 'uf' => $config->userFramework, 'lang' => $config->lcMessages, 'co' => $config->defaultContactCountry, 'ufv' => $config->userSystem->getVersion(), 'PHP' => phpversion(), 'MySQL' => CRM_CORE_DAO::singleValueQuery('SELECT VERSION()'), 'communityMessagesUrl' => Civi::settings()->get('communityMessagesUrl'), ); $this->getPayProcStats(); $this->getEntityStats(); $this->getExtensionStats(); } } /** * Get active payment processor types. */ private function getPayProcStats() { $dao = new CRM_Financial_DAO_PaymentProcessor(); $dao->is_active = 1; $dao->find(); $ppTypes = array(); // Get title and id for all processor types $ppTypeNames = CRM_Core_PseudoConstant::paymentProcessorType(); while ($dao->fetch()) { $ppTypes[] = $ppTypeNames[$dao->payment_processor_type_id]; } // add the .-separated list of the processor types $this->stats['PPTypes'] = implode(',', array_unique($ppTypes)); } /** * Fetch counts from entity tables. * Add info to the 'entities' array */ private function getEntityStats() { $tables = array( 'CRM_Activity_DAO_Activity' => 'is_test = 0', 'CRM_Case_DAO_Case' => 'is_deleted = 0', 'CRM_Contact_DAO_Contact' => 'is_deleted = 0', 'CRM_Contact_DAO_Relationship' => NULL, 'CRM_Campaign_DAO_Campaign' => NULL, 'CRM_Contribute_DAO_Contribution' => 'is_test = 0', 'CRM_Contribute_DAO_ContributionPage' => 'is_active = 1', 'CRM_Contribute_DAO_ContributionProduct' => NULL, 'CRM_Contribute_DAO_Widget' => 'is_active = 1', 'CRM_Core_DAO_Discount' => NULL, 'CRM_Price_DAO_PriceSetEntity' => NULL, 'CRM_Core_DAO_UFGroup' => 'is_active = 1', 'CRM_Event_DAO_Event' => 'is_active = 1', 'CRM_Event_DAO_Participant' => 'is_test = 0', 'CRM_Friend_DAO_Friend' => 'is_active = 1', 'CRM_Grant_DAO_Grant' => NULL, 'CRM_Mailing_DAO_Mailing' => 'is_completed = 1', 'CRM_Member_DAO_Membership' => 'is_test = 0', 'CRM_Member_DAO_MembershipBlock' => 'is_active = 1', 'CRM_Pledge_DAO_Pledge' => 'is_test = 0', 'CRM_Pledge_DAO_PledgeBlock' => NULL, ); foreach ($tables as $daoName => $where) { $dao = new $daoName(); if ($where) { $dao->whereAdd($where); } $short_name = substr($daoName, strrpos($daoName, '_') + 1); $this->stats['entities'][] = array( 'name' => $short_name, 'size' => $dao->count(), ); } } /** * Fetch stats about enabled components/extensions * Add info to the 'extensions' array */ private function getExtensionStats() { // Core components $config = CRM_Core_Config::singleton(); foreach ($config->enableComponents as $comp) { $this->stats['extensions'][] = array( 'name' => 'org.civicrm.component.' . strtolower($comp), 'enabled' => 1, 'version' => $this->stats['version'], ); } // Contrib extensions $mapper = CRM_Extension_System::singleton()->getMapper(); $dao = new CRM_Core_DAO_Extension(); $dao->find(); while ($dao->fetch()) { $info = $mapper->keyToInfo($dao->full_name); $this->stats['extensions'][] = array( 'name' => $dao->full_name, 'enabled' => $dao->is_active, 'version' => isset($info->version) ? $info->version : NULL, ); } } /** * Send the request to civicrm.org * Store results in the cache file */ private function pingBack() { $params = array( 'http' => array( 'method' => 'POST', 'header' => 'Content-type: application/x-www-form-urlencoded', 'content' => http_build_query($this->stats), ), ); $ctx = stream_context_create($params); $rawJson = file_get_contents(self::PINGBACK_URL, FALSE, $ctx); $versionInfo = $rawJson ? json_decode($rawJson, TRUE) : NULL; // If we couldn't fetch or parse the data $versionInfo will be NULL // Otherwise it will be an array and we'll cache it. // Note the array may be empty e.g. in the case of a pre-alpha with no releases if ($versionInfo !== NULL) { $this->writeCacheFile($rawJson); $this->versionInfo = $versionInfo; } } /** * @return bool */ private function readCacheFile() { if (file_exists($this->cacheFile)) { $this->versionInfo = (array) json_decode(file_get_contents($this->cacheFile), TRUE); // Sort version info in ascending order for easier comparisons ksort($this->versionInfo, SORT_NUMERIC); return TRUE; } return FALSE; } /** * Save version info to file. * @param string $contents */ private function writeCacheFile($contents) { $fp = fopen($this->cacheFile, 'w'); fwrite($fp, $contents); fclose($fp); } }