From 8ec39ef999f75eec30af0e5c3c43faf8c5f561a7 Mon Sep 17 00:00:00 2001 From: Olle Haerstedt Date: Fri, 5 Oct 2018 13:58:44 +0200 Subject: [PATCH] Dev: Implement the security update warning for extension updates --- .../core/plugins/UpdateCheck/UpdateCheck.php | 28 +++++--- .../ExtensionInstaller/ExtensionUpdater.php | 72 +++++++++++++++++-- .../ExtensionInstaller/GitVersionFetcher.php | 10 ++- .../ExtensionInstaller/PluginUpdater.php | 21 ++++-- .../ExtensionInstaller/RESTVersionFetcher.php | 59 +++++++++++++-- .../ExtensionInstaller/VersionFetcher.php | 6 ++ .../VersionFetcherServiceLocator.php | 10 +-- application/views/admin/super/footer.php | 2 +- 8 files changed, 173 insertions(+), 35 deletions(-) diff --git a/application/core/plugins/UpdateCheck/UpdateCheck.php b/application/core/plugins/UpdateCheck/UpdateCheck.php index 01a39c803e9..1afe6e5bf86 100644 --- a/application/core/plugins/UpdateCheck/UpdateCheck.php +++ b/application/core/plugins/UpdateCheck/UpdateCheck.php @@ -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('
', $messages) . '
' . implode('
', $errors) + 'title' => $title, + 'display_class' => $displayClass, + 'message' => implode('
', $messages) . '
' . implode('
', $errors), + 'importance' => $importance ], $superadmins ); diff --git a/application/libraries/ExtensionInstaller/ExtensionUpdater.php b/application/libraries/ExtensionInstaller/ExtensionUpdater.php index dda7773532c..c03fcebec4e 100644 --- a/application/libraries/ExtensionInstaller/ExtensionUpdater.php +++ b/application/libraries/ExtensionInstaller/ExtensionUpdater.php @@ -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 */ @@ -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', @@ -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. diff --git a/application/libraries/ExtensionInstaller/GitVersionFetcher.php b/application/libraries/ExtensionInstaller/GitVersionFetcher.php index cdc59672f27..ee10cd14094 100644 --- a/application/libraries/ExtensionInstaller/GitVersionFetcher.php +++ b/application/libraries/ExtensionInstaller/GitVersionFetcher.php @@ -10,10 +10,18 @@ class GitVersionFetcher extends VersionFetcher { /** * @param string $extensionName - * @return ExtensionUpdateInfo + * @return string */ public function getLatestVersion() { // todo } + + /** + * + */ + public function getLatestSecurityVersion() + { + // todo + } } diff --git a/application/libraries/ExtensionInstaller/PluginUpdater.php b/application/libraries/ExtensionInstaller/PluginUpdater.php index b416f8864b5..39c7216a0b5 100644 --- a/application/libraries/ExtensionInstaller/PluginUpdater.php +++ b/application/libraries/ExtensionInstaller/PluginUpdater.php @@ -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'); @@ -62,6 +62,14 @@ 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)) { @@ -69,13 +77,16 @@ public function getAvailableUpdates() } if (version_compare($newVersion, $this->model->version, '>')) { - $versions[] = $newVersion; + $versions[] = [ + 'isSecurityVersion' => false, + 'version' => $newVersion + ]; } else { // Ignore. } } - return [$this->model->name, 'plugin', $versions]; + return $versions; } /** diff --git a/application/libraries/ExtensionInstaller/RESTVersionFetcher.php b/application/libraries/ExtensionInstaller/RESTVersionFetcher.php index 63449f6896e..93835ef7712 100644 --- a/application/libraries/ExtensionInstaller/RESTVersionFetcher.php +++ b/application/libraries/ExtensionInstaller/RESTVersionFetcher.php @@ -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; @@ -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); } } } diff --git a/application/libraries/ExtensionInstaller/VersionFetcher.php b/application/libraries/ExtensionInstaller/VersionFetcher.php index 1e681663fe8..dbfdf4e6c8d 100644 --- a/application/libraries/ExtensionInstaller/VersionFetcher.php +++ b/application/libraries/ExtensionInstaller/VersionFetcher.php @@ -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(); } diff --git a/application/libraries/ExtensionInstaller/VersionFetcherServiceLocator.php b/application/libraries/ExtensionInstaller/VersionFetcherServiceLocator.php index e26b4024619..a7cae4df2f6 100644 --- a/application/libraries/ExtensionInstaller/VersionFetcherServiceLocator.php +++ b/application/libraries/ExtensionInstaller/VersionFetcherServiceLocator.php @@ -37,10 +37,10 @@ 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; } ); @@ -48,7 +48,7 @@ function () { // TODO: Not implemented. $this->addVersionFetcher( 'git', - function () { + function (\SimpleXMLElement $updaterXml) { return new GitVersionFetcher(); } ); @@ -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)); diff --git a/application/views/admin/super/footer.php b/application/views/admin/super/footer.php index 87fe84528a6..0a021060b49 100644 --- a/application/views/admin/super/footer.php +++ b/application/views/admin/super/footer.php @@ -215,7 +215,7 @@ class='btn btn-warning btn-sm' - +