artabro/site/modules/ProcessWireUpgrade/ProcessWireUpgradeCheck.module
2024-08-27 11:35:37 +02:00

430 lines
12 KiB
Text

<?php namespace ProcessWire;
/**
* ProcessWire Upgrade Check
*
* Automatically checks for core and installed module upgrades at routine intervals.
* Also provides the API functions for repository info used by ProcessWireUpgrade module.
*
* ProcessWire
* Copyright (C) 2021 by Ryan Cramer
* Licensed under MPL 2.0
*
* https://processwire.com
*
* @property bool $useLoginHook
*
*/
class ProcessWireUpgradeCheck extends WireData implements Module {
/**
* Return information about this module (required)
*
*/
public static function getModuleInfo() {
return array(
'title' => '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;
}
}