From f2f8944dd7fb81b86e846b296160062782774756 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 19 Sep 2024 16:38:15 -0400 Subject: [PATCH] Fix Dashboard Updates widget's display of the most recent modx version (#16608) ### What does it do? Added new `SoftwareUpdate` Processors for retrieving available MODX and Extras updates information, utilizing the new upgrades API. This PR is a Phase 1 fix that solves the issue at hand, but also paves the way for a more flexible and comprehensive Updates widget display to be done in a Phase 2 PR. In that next phase, users will be able to control what level of update MODX candidates should be shown (via a few new system settings). The new MODX download functionality provides a direct download instead of a link to the modx.com downloads area. ### Why is it needed? The widget currently incorrectly shows the current -dev version. ### How to test 1. Manually delete session data from the database. 2. Clear browser and MODX caches and verify that the widget displays the correct update version. 3. Test with an up-to-date `-pl` release (currently 3.0.5), a `-dev` release (github 3.0.x or 3.x), and an older `-pl` or `-dev` release (3.0.0 - 3.0.4). This is a little tricky, because you're going to pull down the 3.1.0-dev from github to review. You may want to manually override the installed version in `core/src/Revolution/Processors/SoftwareUpdate/GetList.php` (line 33), setting it to various versions to see what happens in the Dashboard widget (e.g., 3.0.1-pl or 3.0.6-dev, etc.). ### Example (with 3.0.4-dev installed) https://github.com/user-attachments/assets/a74f8872-4047-4307-b1c0-0d522581066d ### Related issue(s)/PR(s) Resolves #16466 --- .../Processors/SoftwareUpdate/Base.php | 75 +++++++++ .../Processors/SoftwareUpdate/GetFile.php | 59 +++++++ .../Processors/SoftwareUpdate/GetList.php | 145 ++++++++++++++++++ .../default/dashboard/widget.updates.php | 73 +++------ manager/controllers/default/welcome.class.php | 3 +- .../templates/default/dashboard/updates.tpl | 70 ++++++++- 6 files changed, 364 insertions(+), 61 deletions(-) create mode 100644 core/src/Revolution/Processors/SoftwareUpdate/Base.php create mode 100644 core/src/Revolution/Processors/SoftwareUpdate/GetFile.php create mode 100644 core/src/Revolution/Processors/SoftwareUpdate/GetList.php diff --git a/core/src/Revolution/Processors/SoftwareUpdate/Base.php b/core/src/Revolution/Processors/SoftwareUpdate/Base.php new file mode 100644 index 00000000000..ca6373c09d7 --- /dev/null +++ b/core/src/Revolution/Processors/SoftwareUpdate/Base.php @@ -0,0 +1,75 @@ +apiClient) { + $this->apiClient = $this->modx->services->get(ClientInterface::class); + $this->apiFactory = $this->modx->services->get(RequestFactoryInterface::class); + } + } + + /** + * Builds the API link used to fetch file data + * + * @param array $requestParams Query parameters + * @param string $targetId An intermediate id used to fetch the actual download link + * @return string The full URI to pass into the upgrades API + */ + public function buildRequestUri(array $requestParams = [], string $targetId = ''): string + { + $uri = $this->apiHost; + /* + When a $targetId is passed in, we are making the final request whose response + reveals the real update file path. Otherwise the request gets a full list of + potential upgrades based on criteria passed in the $requestParams + */ + $uri .= !empty($targetId) + ? str_replace('[downloadId]', $targetId, $this->apiGetFilePath) + : $this->apiGetReleasesPath + ; + if (count($requestParams) > 0) { + $uri .= '?' . http_build_query($requestParams); + } + return $uri; + } +} diff --git a/core/src/Revolution/Processors/SoftwareUpdate/GetFile.php b/core/src/Revolution/Processors/SoftwareUpdate/GetFile.php new file mode 100644 index 00000000000..9fe489d09bf --- /dev/null +++ b/core/src/Revolution/Processors/SoftwareUpdate/GetFile.php @@ -0,0 +1,59 @@ +getProperty('downloadId', null); + $responseData = []; + + if ($downloadId) { + $this->initApiClient(); + + $uri = $this->buildRequestUri(['uuid' => $this->modx->uuid], $downloadId); + $request = $this->apiFactory->createRequest('GET', $uri) + ->withHeader('Accept', 'application/json') + ->withHeader('Content-Type', 'application/json'); + try { + $response = $this->apiClient->sendRequest($request); + } catch (ClientExceptionInterface $e) { + $this->modx->log(modX::LOG_LEVEL_ERROR, $e->getMessage()); + return $this->failure($e->getMessage()); + } + + $fileData = $response->getBody()->getContents(); + + if ($fileData) { + $fileData = json_decode($fileData, true); + if (!empty($fileData['zip_url']) && strpos($fileData['zip_url'], 'http') === 0) { + $name = basename($fileData['zip_url']); + $responseData['filename'] = $name; + $responseData['zip'] = $fileData['zip_url']; + $responseData['status'] = $response->getStatusCode(); + } + } + return $this->success('', $responseData); + } + } +} diff --git a/core/src/Revolution/Processors/SoftwareUpdate/GetList.php b/core/src/Revolution/Processors/SoftwareUpdate/GetList.php new file mode 100644 index 00000000000..54e8c422e92 --- /dev/null +++ b/core/src/Revolution/Processors/SoftwareUpdate/GetList.php @@ -0,0 +1,145 @@ +installedVersionData = $this->modx->getVersionData(); + return parent::initialize(); + } + + public function process() + { + $softwareType = $this->getProperty('softwareType', 'modx'); + $categoryData = [ + 'updateable' => 0 + ]; + if ($softwareType === 'modx') { + $modxData = $this->getModxUpdates(); + if (is_array($modxData)) { + $categoryData = array_merge($categoryData, $modxData); + } + } else { + $extrasData = $this->getExtrasUpdates(); + if (is_array($extrasData)) { + $categoryData = array_merge($categoryData, $extrasData); + } + } + return $this->success('', $categoryData); + } + + /** + * Fetches a list of MODX update candidates + * + * @return array Data indicating whether the current installation is + * updatable and the available releases if so + */ + public function getModxUpdates(): array + { + $this->initApiClient(); + + $uri = $this->buildRequestUri([ + 'current' => $this->installedVersionData['full_version'], + 'level' => 'major', + 'variant' => 'Traditional', + 'prereleases' => 0 + ]); + + $request = $this->apiFactory->createRequest('GET', $uri) + ->withHeader('Accept', 'application/json') + ->withHeader('Content-Type', 'application/json'); + try { + $response = $this->apiClient->sendRequest($request); + } catch (ClientExceptionInterface $e) { + $this->modx->log(modX::LOG_LEVEL_ERROR, 'ClientExceptionInterface Err: ' . $e->getMessage()); + return $this->failure($e->getMessage()); + } + + $listData = $response->getBody()->getContents(); + $categoryData = []; + if ($listData) { + $listData = json_decode($listData, true); + $upgrades = $listData['upgrades']; + $selectedUpgrade = null; + if (!empty($upgrades)) { + $i = 0; + $upgradesCount = count($upgrades); + if ($upgradesCount === 1) { + $categoryData['updateable'] = 1; + $selectedUpgrade = $upgrades; + } else { + foreach ($upgrades as $upgrade) { + $selectedUpgrade = $upgrade; + break; + } + $categoryData['updateable'] = (int)version_compare($this->installedVersionData['full_version'], $upgrade['version'], '<'); + } + if ($categoryData['updateable']) { + /* + NOTE: This is superfluous now, but is done in preparation + for iterating through multiple displayable versions + */ + $categoryData['versions'][$i]['version'] = $selectedUpgrade['version']; + $urlSegments = explode('/', trim($selectedUpgrade['url'], '/')); + $categoryData['versions'][$i]['downloadId'] = $urlSegments[count($urlSegments) - 2]; + + $categoryData['latest']['version'] = $categoryData['versions'][0]['version']; + $categoryData['latest']['downloadId'] = $categoryData['versions'][0]['downloadId']; + } + } + } + return $categoryData; + } + + /** + * Fetches a list of Extras update candidates + * + * @return array Data indicating whether any installed Extras are updatable + * and, if so, providing the names of those that are update candidates + */ + public function getExtrasUpdates(): array + { + $categoryData = []; + $packages = $this->modx->call(modTransportPackage::class, 'listPackages', [$this->modx, 1]); + if ($packages && array_key_exists('collection', $packages)) { + $packagesProcessor = new PackagesGetList($this->modx); + + /** @var modTransportPackage $package */ + foreach ($packages['collection'] as $package) { + $tmp = []; + $tmp = $packagesProcessor->checkForUpdates($package, $tmp); + if (!empty($tmp['updateable'])) { + $categoryData['names'][] = $package->get('package_name'); + $categoryData['updateable']++; + } + } + } + return $categoryData; + } +} diff --git a/manager/controllers/default/dashboard/widget.updates.php b/manager/controllers/default/dashboard/widget.updates.php index decd14df99f..60c41c4030f 100644 --- a/manager/controllers/default/dashboard/widget.updates.php +++ b/manager/controllers/default/dashboard/widget.updates.php @@ -2,9 +2,8 @@ use MODX\Revolution\modX; use MODX\Revolution\modDashboardWidgetInterface; -use MODX\Revolution\Processors\Workspace\Packages\GetList; +use MODX\Revolution\Processors\SoftwareUpdate\GetList as SoftwareUpdateGetList; use MODX\Revolution\Smarty\modSmarty; -use MODX\Revolution\Transport\modTransportPackage; use xPDO\xPDO; /** @@ -13,73 +12,44 @@ */ class modDashboardWidgetUpdates extends modDashboardWidgetInterface { - /** @var modX $modx */ - public $modx; - public $latest_url = 'https://raw.githubusercontent.com/modxcms/revolution/3.x/_build/build.xml'; - public $download_url = 'https://modx.com/download/latest'; public $updatesCacheExpire = 3600; - /** * @return string * @throws Exception */ public function render() { - $processor = new GetList($this->modx); - $updateCacheKey = 'mgr/providers/updates/modx-core'; $updateCacheOptions = [ - xPDO::OPT_CACHE_KEY => $this->modx->cacheManager->getOption('cache_packages_key', null, 'packages'), - xPDO::OPT_CACHE_HANDLER => $this->modx->cacheManager->getOption('cache_packages_handler', null, $this->modx->cacheManager->getOption(xPDO::OPT_CACHE_HANDLER)), + xPDO::OPT_CACHE_KEY => $this->modx->cacheManager->getOption( + 'cache_packages_key', + null, + 'packages' + ), + xPDO::OPT_CACHE_HANDLER => $this->modx->cacheManager->getOption( + 'cache_packages_handler', + null, + $this->modx->cacheManager->getOption(xPDO::OPT_CACHE_HANDLER) + ), ]; if (!$data = $this->modx->cacheManager->get($updateCacheKey, $updateCacheOptions)) { $data = [ - 'modx' => [ - 'updateable' => 0, - ], - 'packages' => [ - 'names' => [], - 'updateable' => 0, - ], + 'modx' => [], + 'extras' => [] ]; - if (function_exists('curl_init')) { - $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, $this->latest_url); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curl, CURLOPT_TIMEOUT, 1); - $content = curl_exec($curl); - curl_close($curl); - if ($content) { - $xml = new SimpleXMLElement($content); - foreach ($xml->property as $key => $value) { - $name = (string)$value->attributes()->name; - if ($name == 'modx.core.version') { - $data['modx']['version'] = (string)$value->attributes()->value; - } elseif ($name == 'modx.core.release') { - $data['modx']['release'] = (string)$value->attributes()->value; - } - } - } - if (!empty($data['modx']['version']) && !empty($data['modx']['release'])) { - if ($version = $this->modx->getVersionData()) { - $data['modx']['full_version'] = $data['modx']['version'] . '-' . $data['modx']['release']; - $data['modx']['updateable'] = (int)version_compare($version['full_version'], $data['modx']['full_version'], '<'); - } - } + $modxUpdatesProcessor = new SoftwareUpdateGetList($this->modx); + $modxData = $modxUpdatesProcessor->run()->getObject(); + if (is_array($modxData) && array_key_exists('updateable', $modxData)) { + $data['modx'] = $modxData; } - $packages = $this->modx->call(modTransportPackage::class, 'listPackages', [$this->modx, 1, 11, 0]); - /** @var modTransportPackage $package */ - foreach ($packages['collection'] as $package) { - $tmp = []; - $tmp = $processor->checkForUpdates($package, $tmp); - if (!empty($tmp['updateable'])) { - $data['packages']['names'][] = $package->get('package_name'); - $data['packages']['updateable']++; - } + $extrasUpdatesProcessor = new SoftwareUpdateGetList($this->modx, ['softwareType' => 'extras']); + $extrasData = $extrasUpdatesProcessor->run()->getObject(); + if (is_array($extrasData) && array_key_exists('updateable', $extrasData)) { + $data['extras'] = $extrasData; } $this->modx->cacheManager->set($updateCacheKey, $data, $this->updatesCacheExpire, $updateCacheOptions); @@ -92,7 +62,6 @@ public function render() return $this->modx->smarty->fetch('dashboard/updates.tpl'); } - } return 'modDashboardWidgetUpdates'; diff --git a/manager/controllers/default/welcome.class.php b/manager/controllers/default/welcome.class.php index 54ebb89decc..defc69ca174 100644 --- a/manager/controllers/default/welcome.class.php +++ b/manager/controllers/default/welcome.class.php @@ -1,4 +1,5 @@ MODX {if $modx.updateable} - {$modx.full_version} - {$_lang.updates_update} + {$modx.latest.version} + + + {$_lang.download} + + {else} {$_lang.updates_ok} - + {/if} - {if $packages.updateable} + {if $extras.updateable} {$_lang.updates_extras} - {if $packages.updateable > 10}10+{else}{$packages.updateable}{/if} + {if $extras.updateable > 10}10+{else}{$extras.updateable}{/if} {$_lang.updates_available} {$_lang.updates_update} + class="dashboard-button package">{$_lang.updates_update} {else} {$_lang.updates_extras} {$_lang.updates_ok} - + {/if} + +{literal} + + {/literal} \ No newline at end of file