Skip to content

Commit

Permalink
Fix Dashboard Updates widget's display of the most recent modx version (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
smg6511 committed Sep 19, 2024
1 parent 80c160b commit 1755b98
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 61 deletions.
75 changes: 75 additions & 0 deletions core/src/Revolution/Processors/SoftwareUpdate/Base.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

/*
* This file is part of MODX Revolution.
*
* Copyright (c) MODX, LLC. All Rights Reserved.
*
* For complete copyright and license information, see the COPYRIGHT and LICENSE
* files found in the top-level directory of this distribution.
*/

namespace MODX\Revolution\Processors\SoftwareUpdate;

use MODX\Revolution\Processors\Processor;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use MODX\Revolution\modX;

/**
* Provides base methods and shared properties for building status data used
* in the front end display of software updates (MODX and Extras)
*
* @package MODX\Revolution\Processors\SoftwareUpdate
*/
class Base extends Processor
{
public $apiClient = null;
public $apiFactory = null;
public $apiHost = 'https://sentinel.modx.com';
public $apiGetReleasesPath = '/releases/products/997329d1-6f68-48d5-8e5e-5251adbb1f38/upgrades/';
public $apiGetFilePath = '/releases/variants/[downloadId]/download/';

public function process()
{
return parent::process();
}

/**
* Initialize the client responsible for fetching upgrades-related data.
*
* @return
*/
public function initApiClient()
{
if (!$this->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;
}
}
59 changes: 59 additions & 0 deletions core/src/Revolution/Processors/SoftwareUpdate/GetFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/*
* This file is part of MODX Revolution.
*
* Copyright (c) MODX, LLC. All Rights Reserved.
*
* For complete copyright and license information, see the COPYRIGHT and LICENSE
* files found in the top-level directory of this distribution.
*/

namespace MODX\Revolution\Processors\SoftwareUpdate;

use MODX\Revolution\Processors\SoftwareUpdate\Base;
use Psr\Http\Client\ClientExceptionInterface;
use MODX\Revolution\modX;

/**
* Retrieves the downloadable file URL and other metadata for the specified MODX upgrade package
*
* @property string $downloadId An identifier used to retrieve the package's download URL
* @package MODX\Revolution\Processors\SoftwareUpdate
*/
class GetFile extends Base
{
public function process()
{
$downloadId = $this->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);
}
}
}
145 changes: 145 additions & 0 deletions core/src/Revolution/Processors/SoftwareUpdate/GetList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

/*
* This file is part of MODX Revolution.
*
* Copyright (c) MODX, LLC. All Rights Reserved.
*
* For complete copyright and license information, see the COPYRIGHT and LICENSE
* files found in the top-level directory of this distribution.
*/

namespace MODX\Revolution\Processors\SoftwareUpdate;

use MODX\Revolution\Processors\SoftwareUpdate\Base;
use MODX\Revolution\Processors\Workspace\Packages\GetList as PackagesGetList;
use MODX\Revolution\Transport\modTransportPackage;
use Psr\Http\Client\ClientExceptionInterface;
use MODX\Revolution\modX;

/**
* Retrieves status data for use in the front end display of software updates (MODX and Extras)
*
* @property string $softwareType Identifies which type of software status data should be
* retrieved (currently only two options: 'modx' or 'extras')
* @package MODX\Revolution\Processors\SoftwareUpdate
*/
class GetList extends Base
{
public $installedVersionData;

public function initialize()
{
$this->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;
}
}
73 changes: 21 additions & 52 deletions manager/controllers/default/dashboard/widget.updates.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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);
Expand All @@ -92,7 +62,6 @@ public function render()

return $this->modx->smarty->fetch('dashboard/updates.tpl');
}

}

return 'modDashboardWidgetUpdates';
Loading

0 comments on commit 1755b98

Please sign in to comment.