Skip to content

Commit

Permalink
Dev: Implement the security update warning for extension updates
Browse files Browse the repository at this point in the history
  • Loading branch information
olleharstedt committed Oct 5, 2018
1 parent 268603c commit 8ec39ef
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 35 deletions.
28 changes: 17 additions & 11 deletions application/core/plugins/UpdateCheck/UpdateCheck.php
Expand Up @@ -93,28 +93,34 @@ public function checkAll()
/** @var string[] */
$messages = [];

/** @var boolean */
$foundSecurityVersion = false;

foreach ($updaters as $updater) {
try {
list($extensionName, $extensionType, $availableVersions) = $updater->getAvailableUpdates();
if ($availableVersions) {
$messages[] = sprintf(
gT('There are updates available for %s %s, new version number(s): %s.'),
$extensionType,
$extensionName,
implode(', ', $availableVersions)
);
$versions = $updater->fetchVersions();
if ($updater->foundSecurityVersion($versions)) {
$foundSecurityVersion = true;
}
if ($versions) {
$messages[] = $updater->getVersionMessage($versions);
}
} catch (\Throwable $ex) {
$errors[] = $ex->getMessage();
}
}

if ($messages) {
if ($messages || $errors) {
$superadmins = User::model()->getSuperAdmins();
$title = $foundSecurityVersion ? gT('Security updates available') : gT('Updates available');
$displayClass = $foundSecurityVersion ? 'danger' : '';
$importance = $foundSecurityVersion ? Notification::HIGH_IMPORTANCE : Notification::NORMAL_IMPORTANCE;
UniqueNotification::broadcast(
[
'title' => gT('Updates available'),
'message' => implode('<br/>', $messages) . '<br/>' . implode('<br/>', $errors)
'title' => $title,
'display_class' => $displayClass,
'message' => implode('<br/>', $messages) . '<br/>' . implode('<br/>', $errors),
'importance' => $importance
],
$superadmins
);
Expand Down
72 changes: 65 additions & 7 deletions application/libraries/ExtensionInstaller/ExtensionUpdater.php
Expand Up @@ -54,13 +54,6 @@ public function __construct($model)
$this->model = $model;
}

/**
* Uses the version fetcher to get info about available updates for
* this extension.
* @return ?
*/
abstract public function getAvailableUpdates();

/**
* @return void
*/
Expand Down Expand Up @@ -94,12 +87,18 @@ public function versionHigherThan($currentVersion)
}

/**
* Returns true if $version is stable.
* The version is stable IF it does not contain
* alpha, beta or rc suffixes.
* @param string $version
* @return boolean
*/
public function versionIsStable($version)
{
if (empty($version)) {
return false;
}

$suffixes = [
'alpha',
'beta',
Expand All @@ -114,6 +113,65 @@ public function versionIsStable($version)
return true;
}

/**
* Returns true if $versions contain a security version.
* @param array $versions Each version has keys 'version' and 'isSecurityVersion'.
* @return boolean
*/
public function foundSecurityVersion(array $versions)
{
foreach ($versions as $version) {
if ($version['isSecurityVersion']) {
return true;
}
}
return false;
}

/**
* Implode $versions into string, stripping security version field.
* @param array $versions Each version has keys 'version' and 'isSecurityVersion'.
* @return string
*/
public function implodeVersions(array $versions)
{
$versions = array_map(
function ($version) {
return $version['version'];
},
$versions
);
return implode(', ', $versions);
}

/**
* Compose version message to display of $versions.
* @param array $versions Each version has keys 'version' and 'isSecurityVersion'.
* @return string
*/
public function getVersionMessage(array $versions)
{
$extensionName = $this->getExtensionName();
$extensionType = $this->getExtensionType();
if ($this->foundSecurityVersion($versions)) {
$message = gT('There are security updates available for %s %s, new version number(s): %s.');
} else {
$message = gT('There are updates available for %s %s, new version number(s): %s.');
}
return sprintf(
$message,
$extensionType,
$extensionName,
$this->implodeVersions($versions)
);
}

/**
* Uses the version fetcher to fetch info about available updates for
* this extension.
* @return array
*/
abstract public function fetchVersions();

/**
* Create an updater object for every extension of corresponding type.
Expand Down
10 changes: 9 additions & 1 deletion application/libraries/ExtensionInstaller/GitVersionFetcher.php
Expand Up @@ -10,10 +10,18 @@ class GitVersionFetcher extends VersionFetcher
{
/**
* @param string $extensionName
* @return ExtensionUpdateInfo
* @return string
*/
public function getLatestVersion()
{
// todo
}

/**
*
*/
public function getLatestSecurityVersion()
{
// todo
}
}
21 changes: 16 additions & 5 deletions application/libraries/ExtensionInstaller/PluginUpdater.php
Expand Up @@ -44,15 +44,15 @@ public static function createUpdaters() : array

/**
* Fetch all new available version from each version fetcher.
* @return array [string $extensionName, string $extensionType, string[] $versions]
* @return array $versions
*/
public function getAvailableUpdates()
public function fetchVersions()
{
$this->setupVersionFetchers();

if (empty($this->versionFetchers)) {
// No fetchers, can't fetch remote version.
return [$this->model->name, 'plugin', []];
return [];
}

$allowUnstable = getGlobalSetting('allow_unstable_extension_update');
Expand All @@ -62,20 +62,31 @@ public function getAvailableUpdates()
$fetcher->setExtensionName($this->getExtensionName());
$fetcher->setExtensionType($this->getExtensionType());
$newVersion = $fetcher->getLatestVersion();
$lastSecurityVersion = $fetcher->getLatestSecurityVersion();

if (version_compare($lastSecurityVersion, $this->model->version, '>')) {
$versions[] = [
'isSecurityVersion' => true,
'version' => $lastSecurityVersion
];
}

// If this version is unstable and we're not allowed to use it, continue.
if (!$allowUnstable && !$this->versionIsStable($newVersion)) {
continue;
}

if (version_compare($newVersion, $this->model->version, '>')) {
$versions[] = $newVersion;
$versions[] = [
'isSecurityVersion' => false,
'version' => $newVersion
];
} else {
// Ignore.
}
}

return [$this->model->name, 'plugin', $versions];
return $versions;
}

/**
Expand Down
59 changes: 54 additions & 5 deletions application/libraries/ExtensionInstaller/RESTVersionFetcher.php
Expand Up @@ -9,11 +9,61 @@
class RESTVersionFetcher extends VersionFetcher
{
/**
* @param string $extensionName
* @return ExtensionUpdateInfo
* Result from the curl fetch.
* @var object
*/
protected $curlResult = null;

/**
* @return string
*/
public function getLatestVersion()
{
if (empty($this->curlResult)) {
$this->fetchCurl();
}

if (empty($this->curlResult->version)) {
throw new \Exception('Found no version field in curl result');
}

return $this->curlResult->version;
}

/**
* @return string
*/
public function getLatestSecurityVersion()
{
if (empty($this->curlResult)) {
$this->fetchCurl();
}

if (empty($this->curlResult->last_security_version)) {
throw new \Exception('Found no last_security_version field in curl result');
}

return $this->curlResult->last_security_version;
}

/**
* Contact remote server and fetch extension information.
* @return void
*/
protected function fetchCurl()
{
if (empty($this->source)) {
throw new \Exception('Missing source');
}

if (empty($this->extensionName)) {
throw new \Exception('Missing extension name');
}

if (empty($this->extensionType)) {
throw new \Exception('Missing extension type');
}

// Simple CURL to source to fetch extension information.
$url = $this->source;
$url .= '&extension_name=' . $this->extensionName;
Expand All @@ -22,11 +72,10 @@ public function getLatestVersion()
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$content = json_decode(curl_exec($ch));
curl_close($ch);

if ($content && count($content) === 1) {
return $content[0]->version;
$this->curlResult = $content[0];
} else {
return null;
throw new \Exception('Could not fetch REST API information for extension ' . $this->extensionName);
}
}
}
6 changes: 6 additions & 0 deletions application/libraries/ExtensionInstaller/VersionFetcher.php
Expand Up @@ -81,4 +81,10 @@ public function setExtensionType(string $type)
* @return string Semantic versioning string.
*/
abstract public function getLatestVersion();

/**
* Get latest security version for configured source.
* @return string Semantic versioning string.
*/
abstract public function getLatestSecurityVersion();
}
Expand Up @@ -37,18 +37,18 @@ public function init()
{
// Add RESTVersionFetcher, available by default.
$this->addVersionFetcher(
'limesurvey.org',
function () {
'rest',
function (\SimpleXMLElement $updaterXml) {
$vf = new RESTVersionFetcher();
$vf->setSource('https://comfortupdate.limesurvey.org/index.php?r=limeshoprest');
$vf->setSource((string) $updaterXml->source);
return $vf;
}
);

// TODO: Not implemented.
$this->addVersionFetcher(
'git',
function () {
function (\SimpleXMLElement $updaterXml) {
return new GitVersionFetcher();
}
);
Expand All @@ -66,7 +66,7 @@ public function getVersionFetcher(\SimpleXMLElement $updaterXml)
$type = (string) $updaterXml->type;

if (isset($this->versionFetcherCreators[$type])) {
$fileFetcher = $this->versionFetcherCreators[$type]();
$fileFetcher = $this->versionFetcherCreators[$type]($updaterXml);
return $fileFetcher;
} else {
throw new \Exception('Did not find version fetcher of type ' . json_encode($type));
Expand Down
2 changes: 1 addition & 1 deletion application/views/admin/super/footer.php
Expand Up @@ -215,7 +215,7 @@ class='btn btn-warning btn-sm'
<div class="h3 modal-title">
<span class="sr-only"><?php eT("Notifications"); ?></span>
</div>
<span class='notification-date text-muted'></span>
<span class='notification-date'></span>
</div>
<div class="modal-body">
<p class='modal-body-text'></p>
Expand Down

0 comments on commit 8ec39ef

Please sign in to comment.