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