'Upgrades Checker', 'summary' => 'Automatically checks for core and installed module upgrades at routine intervals.', 'version' => 9, 'autoload' => "template=admin", 'singular' => true, 'author' => 'Ryan Cramer', 'icon' => 'coffee', 'requires' => 'ProcessWireUpgrade', ); } const tagsURL = 'https://api.github.com/repos/processwire/processwire/tags'; const branchesURL = 'https://api.github.com/repos/processwire/processwire/branches'; const versionURL = 'https://raw.githubusercontent.com/processwire/processwire/{branch}/wire/core/ProcessWire.php'; const zipURL = 'https://github.com/processwire/processwire/archive/{branch}.zip'; const timeout = 4.5; /** * Repository information indexed by branch * * @var array * */ protected $repos = array(); /** * Construct * */ public function __construct() { $this->set('useLoginHook', 1); $this->repos = array( 'main' => array( 'branchesURL' => self::branchesURL, 'zipURL' => self::zipURL, 'versionURL' => self::versionURL, 'tagsURL' => self::tagsURL, ), ); } /** * Initialize and perform access checks * */ public function init() { if($this->useLoginHook) { $this->session->addHookAfter('login', $this, 'loginHook'); } } /** * Check for upgrades at login (superuser only) * * @param null|HookEvent $e * */ public function loginHook($e = null) { if($e) {} // ignored HookEvent if(!$this->user->isSuperuser()) return; // only show messages to superuser $moduleVersions = array(); $cache = $this->cache; $cacheName = $this->className() . "_loginHook"; $cacheData = $cache ? $cache->get($cacheName) : null; if(!empty($cacheData) && is_string($cacheData)) $cache = json_decode($cacheData, true); $branches = empty($cacheData) ? $this->getCoreBranches(false) : $cacheData['branches']; if(empty($branches)) return; $master = $branches['master']; $branch = null; $new = version_compare($master['version'], $this->config->version); if($new > 0) { // master is newer than current $branch = $master; } else if($new < 0 && isset($branches['dev'])) { // we will assume dev branch $dev = $branches['dev']; $new = version_compare($dev['version'], $this->config->version); if($new > 0) $branch = $dev; } if($branch) { $versionStr = "$branch[name] $branch[version]"; $msg = $this->_('A ProcessWire core upgrade is available') . " ($versionStr)"; $this->message($msg); } else { $this->message($this->_('Your ProcessWire core is up-to-date')); } if($this->config->moduleServiceKey) { $n = 0; if(empty($cacheData) || empty($cacheData['moduleVersions'])) { $moduleVersions = $this->getModuleVersions(true); } else { $moduleVersions = $cacheData['moduleVersions']; } foreach($moduleVersions as $name => $info) { $msg = sprintf($this->_('An upgrade for %s is available'), $name) . " ($info[remote])"; $this->message($msg); $n++; } if(!$n) $this->message($this->_('Your modules are up-to-date')); } if($cache) { $cacheData = array( 'branches' => $branches, 'moduleVersions' => $moduleVersions ); $cache->save($cacheName, $cacheData, 43200); // 43200=12hr } } /** * Get versions of core or modules * * @param bool $refresh * @return array of array( * 'ModuleName' => array( * 'title' => 'Module Title', * 'local' => '1.2.3', // current installed version * 'remote' => '1.2.4', // directory version available, or boolean false if not found in directory * 'new' => true|false, // true if newer version available, false if not * 'requiresVersions' => array('ModuleName' => array('>', '1.2.3')), // module requirements (for modules only) * 'branch' => 'master', // branch name (for core only) * ) * ) * */ public function getVersions($refresh = false) { $versions = array(); $branches = $this->getCoreBranches(false, $refresh); foreach($branches as $branchName => $branch) { $name = "ProcessWire $branchName"; $new = version_compare($branch['version'], $this->config->version); $versions[$name] = array( 'title' => "ProcessWire Core ($branch[title])", 'icon' => 'microchip', 'local' => $this->config->version, 'remote' => $branch['version'], 'new' => $new, 'branch' => $branch['name'], 'author' => 'ProcessWire', 'href' => '', 'summary' => '', 'urls' => isset($branch['urls']) ? $branch['urls'] : array() ); } if($this->config->moduleServiceKey) { foreach($this->getModuleVersions(false, $refresh) as $name => $info) { $versions[$name] = $info; } } return $versions; } protected function getModuleInfoVerbose($name) { $info = $this->modules->getModuleInfoVerbose($name); return $info; } /** * Check all site modules for newer versions from the directory * * @param bool $onlyNew Only return array of modules with new versions available * @param bool $refresh * @return array of array( * 'ModuleName' => array( * 'title' => 'Module Title', * 'local' => '1.2.3', // current installed version * 'remote' => '1.2.4', // directory version available, or boolean false if not found in directory * 'new' => true|false, // true if newer version available, false if not * 'requiresVersions' => array('ModuleName' => array('>', '1.2.3')), // module requirements * ) * ) * @throws WireException * */ public function getModuleVersions($onlyNew = false, $refresh = false) { $modules = $this->modules; $config = $this->config; if(!$this->config->moduleServiceKey) { throw new WireException('Missing /site/config.php: value for $config->moduleServiceKey'); } $url = $config->moduleServiceURL . "?apikey=" . $config->moduleServiceKey . "&version=2" . "&limit=100" . "&field=module_version,version,requires,urls" . "&class_name="; $names = array(); $versions = array(); foreach($modules as $module) { $name = $module->className(); $info = $this->getModuleInfoVerbose($name); if($info['core']) continue; $names[] = $name; $versions[$name] = array( 'title' => $info['title'], 'summary' => empty($info['summary']) ? '' : $info['summary'], 'icon' => empty($info['icon']) ? '' : $info['icon'], 'author' => empty($info['author']) ? '' : $info['author'], 'href' => empty($info['href']) ? '' : $info['href'], 'local' => $modules->formatVersion($info['version']), 'remote' => false, 'new' => 0, 'requiresVersions' => $info['requiresVersions'], 'installs' => $info['installs'], ); } if(!count($names)) return array(); ksort($versions); $url .= implode(',', $names); $data = $refresh ? null : $this->session->getFor($this, 'moduleVersionsData'); if(empty($data)) { // if not cached $http = new WireHttp(); $this->wire($http); $http->setTimeout(self::timeout); $data = $http->getJSON($url); $this->session->setFor($this, 'moduleVersionsData', $data); if(!is_array($data)) { $error = $http->getError(); if(!$error) $error = $this->_('Error retrieving modules directory data'); $this->error($error . " (" . $this->className() . ")"); return array(); } } $newVersions = array(); $installedBy = array(); // moduleName => installedByModuleName foreach($data['items'] as $item) { $name = $item['class_name']; if(empty($versions[$name]) || empty($versions[$name]['local'])) continue; $versions[$name]['remote'] = $item['version']; $new = version_compare($versions[$name]['remote'], $versions[$name]['local']); $versions[$name]['new'] = $new; $versions[$name]['urls'] = $item['urls']; $versions[$name]['installer'] = ''; $versions[$name]['pro'] = !empty($item['pro']); if(!empty($versions[$name]['installs'])) { foreach($versions[$name]['installs'] as $installsName) { $installedBy[$installsName] = $name; } } if($new <= 0) { // local is up-to-date or newer than remote if($onlyNew) unset($versions[$name]); continue; } // remote is newer than local $versions[$name]['requiresVersions'] = $item['requires']; if($new > 0 && !$onlyNew) { $newVersions[$name] = $versions[$name]; unset($versions[$name]); } } if($onlyNew) { foreach($versions as $name => $item) { if($item['remote'] === false) unset($versions[$name]); } } else if(count($newVersions)) { $versions = $newVersions + $versions; } foreach($versions as $name => $item) { if(!empty($versions[$name]['remote'])) continue; if(!isset($installedBy[$name])) continue; $versions[$name]['installer'] = $installedBy[$name]; $installer = $versions[$installedBy[$name]]; if(!empty($installer['pro'])) $versions[$name]['pro'] = true; } return $versions; } /** * Get all available branches with info for each * * @param bool $throw Whether or not to throw exceptions on error (default=true) * @param bool $refresh Specify true to refresh data from web service * @return array of branches each with: * - name (string) i.e. dev * - title (string) i.e. Development * - zipURL (string) URL to zip download file * - version (string) i.e. 2.5.0 * - versionURL (string) URL to we pull version from * @throws WireException * */ public function getCoreBranches($throw = true, $refresh = false) { if(!$refresh) { $branches = $this->session->getFor($this, 'branches'); if($branches && count($branches)) return $branches; } $branches = array(); foreach($this->repos as $repoName => $repo) { $http = new WireHttp(); $this->wire($http); $http->setTimeout(self::timeout); $http->setHeader('User-Agent', 'ProcessWireUpgrade'); $json = $http->get($repo['branchesURL']); $loadError = $this->_('Error loading GitHub branches') . ' - ' . $repo['branchesURL']; if(!$json) { $error = $loadError; $error .= ' - ' . $this->_('HTTP error(s):') . ' ' . $http->getError(); $error .= ' - ' . $this->_('Check that HTTP requests are not blocked by your server.'); if($throw) throw new WireException($error); $this->error($error); return array(); } $data = json_decode($json, true); if(!$data) { $error = $loadError; if($throw) throw new WireException($error); $this->error($error); return array(); } foreach($data as $key => $info) { if(empty($info['name'])) continue; $name = $info['name']; $branch = array( 'name' => $name, 'title' => ucfirst($name), 'zipURL' => str_replace('{branch}', $name, $repo['zipURL']), 'version' => '', 'versionURL' => str_replace('{branch}', $name, $repo['versionURL']), ); $content = $http->get($branch['versionURL']); if(!preg_match_all('/const\s+version(Major|Minor|Revision)\s*=\s*(\d+)/', $content, $matches)) { $branch['version'] = '?'; continue; } $version = array(); foreach($matches[1] as $k => $var) { $version[$var] = (int) $matches[2][$k]; } $branch['version'] = "$version[Major].$version[Minor].$version[Revision]"; $url = 'https://github.com/processwire/processwire'; $branch['urls'] = array( 'repo' => ($name === 'dev' ? "$url/tree/dev" : $url), 'download' => "$url/archive/refs/heads/$name.zip", 'support' => 'https://processwire.com/talk/', ); if($repoName != 'main') { $name = "$repoName-$name"; $branch['name'] = $name; $branch['title'] = ucfirst($repoName) . "/$branch[title]"; } $branches[$name] = $branch; } } $this->session->setFor($this, 'branches', $branches); return $branches; } }