<?php /* * @package bfNetwork * @copyright Copyright (C) 2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023 Blue Flame Digital Solutions Ltd. All rights reserved. * @license GNU General Public License version 3 or later * * @see https://mySites.guru/ * @see https://www.phil-taylor.com/ * * @author Phil Taylor / Blue Flame Digital Solutions Limited. * * bfNetwork is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * bfNetwork is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this package. If not, see http://www.gnu.org/licenses/ * * If you have any questions regarding this code, please contact [email protected] */ /** * @copyright Copyright (c)2010-2014 Nicholas K. Dionysopoulos * @license GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /** * Provides update information for the Joomla! CMS. */ class AcuUpdateProviderJoomla { /** * The source for LTS updates. * * @var string */ protected static $lts_url = 'http://update.joomla.org/core/list.xml'; /** * The source for STS updates. * * @var string */ protected static $sts_url = 'http://update.joomla.org/core/sts/list_sts.xml'; /** * The source for test release updates. * * @var string */ protected static $test_url = 'http://update.joomla.org/core/test/list_test.xml'; /** * Reads a "collection" XML update source and picks the correct source URL for the extension update source. * * @param string $url The collection XML update source URL to read from * @param string $jVersion Joomla! version to fetch updates for, or null to use JVERSION * * @return string The URL of the extension update source, or empty if no updates are provided / fetching failed */ public function getUpdateSourceFromCollection($url, $jVersion = null) { $provider = new AcuUpdateProviderCollection(); return $provider->getExtensionUpdateSource($url, 'file', 'joomla', $jVersion); } /** * Reads an "extension" XML update source and returns all listed update entries. * * @param string $url The extension XML update source URL to read from * * @return array An array of update entries */ public function getUpdatesFromExtension($url) { // Initialise $ret = []; // Get and parse the XML source $donwloader = new AcuDownload(); $xmlSource = $donwloader->getFromURL($url); try { $xml = new SimpleXMLElement($xmlSource, \LIBXML_NONET); } catch (Exception) { return $ret; } // Sanity check if (('updates' != $xml->getName())) { unset($xml); return $ret; } // Let's populate the list of updates foreach ($xml->children() as $update) { // Sanity check if ('update' != $update->getName()) { continue; } $entry = [ 'infourl' => [ 'title' => '', 'url' => '', ], 'downloads' => [], 'tags' => [], 'targetplatform' => [], ]; $properties = get_object_vars($update); foreach ($properties as $nodeName => $nodeContent) { switch ($nodeName) { default: $entry[$nodeName] = $nodeContent; break; case 'infourl': case 'downloads': case 'tags': case 'targetplatform': break; } } $infourlNode = $update->xpath('infourl'); $entry['infourl']['title'] = (string) $infourlNode[0]['title']; $entry['infourl']['url'] = (string) $infourlNode[0]; $downloadNodes = $update->xpath('downloads/downloadurl'); foreach ($downloadNodes as $downloadNode) { $entry['downloads'][] = [ 'type' => (string) $downloadNode['type'], 'format' => (string) $downloadNode['format'], 'url' => (string) $downloadNode, ]; } $tagNodes = $update->xpath('tags/tag'); foreach ($tagNodes as $tagNode) { $entry['tags'][] = (string) $tagNode; } $targetPlatformNode = $update->xpath('targetplatform'); $entry['targetplatform']['name'] = (string) $targetPlatformNode[0]['name']; $entry['targetplatform']['version'] = (string) $targetPlatformNode[0]['version']; $ret[] = $entry; } unset($xml); return $ret; } /** * Determines the properties of a version: STS/LTS, normal or testing. * * @param string $jVersion The version number to check * @param string $currentVersion The current Joomla! version number * * @return array The properties analysis */ public function getVersionProperties($jVersion, $currentVersion = null) { // Initialise $ret = [ 'lts' => true, // Is this an LTS release? False means STS. 'current' => false, // Is this a release in the $currentVersion branch? 'upgrade' => 'none', // Upgrade relation of $jVersion to $currentVersion: 'none' (can't upgrade), 'lts' (next or current LTS), 'sts' (next or current STS) or 'current' (same release, no upgrade available) 'testing' => false, // Is this a testing (alpha, beta, RC) release? ]; // Get the current version if none is defined if (null === $currentVersion) { $currentVersion = JVERSION; } // Sanitise version numbers $jVersion = $this->sanitiseVersion($jVersion); $currentVersion = $this->sanitiseVersion($currentVersion); // Get the base version $baseVersion = substr($jVersion, 0, 3); // Get the minimum and maximum current version numbers $current_minimum = substr($currentVersion, 0, 3); $current_maximum = $current_minimum . '.9999'; // Initialise STS/LTS version numbers $sts_minimum = false; $sts_maximum = false; $lts_minimum = false; // Is it an LTS or STS release? switch ($baseVersion) { case '1.5': $ret['lts'] = true; break; case '1.6': $ret['lts'] = false; $sts_minimum = '1.7'; $sts_maximum = '1.7.999'; $lts_minimum = '2.5'; break; case '1.7': $ret['lts'] = false; $sts_minimum = false; $lts_minimum = '2.5'; break; default: $majorVersion = substr($jVersion, 0, 1); $minorVersion = substr($jVersion, 2, 1); if ('5' == $minorVersion) { $ret['lts'] = true; // This is an LTS release, it can be superseded by .0 through .4 STS releases on the next branch... $sts_minimum = ($majorVersion + 1) . '.0'; $sts_maximum = ($majorVersion + 1) . '.4.9999'; // ...or a .5 LTS on the next branch $lts_minimum = ($majorVersion + 1) . '.5'; } else { $ret['lts'] = false; // This is an STS release, it can be superseded by a .1/.2/.3/.4 STS release on the same branch... $sts_minimum = $majorVersion . '.1'; $sts_maximum = $majorVersion . '.4.9999'; // ...or a .5 LTS on the same branch $lts_minimum = $majorVersion . '.5'; } break; } // Is it a current release? if (version_compare($jVersion, $current_minimum, 'ge') && version_compare($jVersion, $current_maximum, 'le')) { $ret['current'] = true; } // Is this a testing release? $versionParts = explode('.', $jVersion); $lastVersionPart = array_pop($versionParts); if (in_array(substr($lastVersionPart, 0, 1), ['a', 'b'])) { $ret['testing'] = true; } elseif (str_starts_with($lastVersionPart, 'rc')) { $ret['testing'] = true; } elseif (str_starts_with($lastVersionPart, 'dev')) { $ret['testing'] = true; } // Find the upgrade relation of $jVersion to $currentVersion if (version_compare($jVersion, $currentVersion, 'eq')) { $ret['upgrade'] = 'current'; } elseif ((false !== $sts_minimum) && version_compare($jVersion, $sts_minimum, 'ge') && version_compare( $jVersion, $sts_maximum, 'le' )) { $ret['upgrade'] = 'sts'; } elseif ((false !== $lts_minimum) && version_compare($jVersion, $lts_minimum, 'ge')) { $ret['upgrade'] = 'lts'; } elseif ($baseVersion == $current_minimum) { $ret['upgrade'] = $ret['lts'] ? 'lts' : 'sts'; } else { $ret['upgrade'] = 'none'; } return $ret; } /** * Filters a list of updates, making sure they apply to the specifed CMS release. * * @param array $updates A list of update records returned by the getUpdatesFromExtension method * @param string $jVersion The current Joomla! version number * * @return array A filtered list of updates. Each update record also includes version relevance information. */ public function filterApplicableUpdates($updates, $jVersion = null) { if (empty($jVersion)) { $jVersion = JVERSION; } $versionParts = explode('.', (string) $jVersion, 4); $platformVersionMajor = $versionParts[0]; $platformVersionMinor = $platformVersionMajor . '.' . $versionParts[1]; $platformVersionNormal = $platformVersionMinor . '.' . $versionParts[2]; $platformVersionFull = (count($versionParts) > 3) ? $platformVersionNormal . '.' . $versionParts[3] : $platformVersionNormal; $ret = []; foreach ($updates as $update) { // Check each update for platform match if ('joomla' != strtolower((string) $update['targetplatform']['name'])) { continue; } $targetPlatformVersion = $update['targetplatform']['version']; if (($targetPlatformVersion !== $platformVersionMajor) && ($targetPlatformVersion !== $platformVersionMinor) && ($targetPlatformVersion !== $platformVersionNormal) && ($targetPlatformVersion !== $platformVersionFull)) { continue; } // Get some information from the version number $updateVersion = $update['version']; $versionProperties = $this->getVersionProperties($updateVersion, $jVersion); if ('none' == $versionProperties['upgrade']) { continue; } // The XML files are ill-maintained. Maybe we already have this update? if (! array_key_exists($updateVersion, $ret)) { $ret[$updateVersion] = array_merge($update, $versionProperties); } } return $ret; } /** * Joomla! has a lousy track record in naming its alpha, beta and release candidate releases. The convention used * seems to be "what the hell the current package maintainer thinks looks better". This method tries to figure out * what was in the mind of the maintainer and translate the funky version number to an actual PHP-format version * string. * * @param string $version The whatever-format version number * * @return string A standard formatted version number */ public function sanitiseVersion($version) { $test = strtolower($version); $alphaQualifierPosition = strpos($test, 'alpha-'); $betaQualifierPosition = strpos($test, 'beta-'); $rcQualifierPosition = strpos($test, 'rc-'); $rcQualifierPosition2 = strpos($test, 'rc'); $devQualifiedPosition = strpos($test, 'dev'); if (false !== $alphaQualifierPosition) { $betaRevision = substr($test, $alphaQualifierPosition + 6); if (! $betaRevision) { $betaRevision = 1; } $test = substr($test, 0, $alphaQualifierPosition) . '.a' . $betaRevision; } elseif (false !== $betaQualifierPosition) { $betaRevision = substr($test, $betaQualifierPosition + 5); if (! $betaRevision) { $betaRevision = 1; } $test = substr($test, 0, $betaQualifierPosition) . '.b' . $betaRevision; } elseif (false !== $rcQualifierPosition) { $betaRevision = substr($test, $rcQualifierPosition + 5); if (! $betaRevision) { $betaRevision = 1; } $test = substr($test, 0, $rcQualifierPosition) . '.rc' . $betaRevision; } elseif (false !== $rcQualifierPosition2) { $betaRevision = substr($test, $rcQualifierPosition2 + 5); if (! $betaRevision) { $betaRevision = 1; } $test = substr($test, 0, $rcQualifierPosition2) . '.rc' . $betaRevision; } elseif (false !== $devQualifiedPosition) { $betaRevision = substr($test, $devQualifiedPosition + 6); if (! $betaRevision) { $betaRevision = ''; } $test = substr($test, 0, $devQualifiedPosition) . '.dev' . $betaRevision; } return $test; } /** * Reloads the list of all updates available for the specified Joomla! version from the network. * * @param array $sources The enabled sources to look into * @param string $jVersion The Joomla! version we are checking updates for * * @return array A list of updates for the installed, current, lts and sts versions */ public function getUpdates($sources = [], $jVersion = null) { // Make sure we have a valid list of sources if (empty($sources) || ! is_array($sources)) { $sources = []; } $defaultSources = [ 'lts' => true, 'sts' => true, 'test' => true, 'custom' => '', ]; $sources = array_merge($defaultSources, $sources); // Use the current JVERSION if none is specified if (empty($jVersion)) { $jVersion = JVERSION; } // Get the current branch' min/max versions $versionParts = explode('.', (string) $jVersion, 4); $currentMinVersion = $versionParts[0] . '.' . $versionParts[1]; $currentMaxVersion = $versionParts[0] . '.' . $versionParts[1] . '.9999'; // Retrieve all updates $allUpdates = []; foreach ($sources as $source => $value) { if ((false === $value) || empty($value)) { continue; } switch ($source) { case 'lts': $url = self::$lts_url; break; case 'sts': $url = self::$sts_url; break; case 'test': $url = self::$test_url; break; case 'custom': $url = $value; break; } $url = $this->getUpdateSourceFromCollection($url, $jVersion); if (! empty($url)) { $updates = $this->getUpdatesFromExtension($url); if (! empty($updates)) { $applicableUpdates = $this->filterApplicableUpdates($updates, $jVersion); if (! empty($applicableUpdates)) { $allUpdates = array_merge($allUpdates, $applicableUpdates); } } } } $ret = [ // Currently installed version (used to reinstall, if available) 'installed' => [ 'version' => '', 'package' => '', 'infourl' => '', ], // Current branch 'current' => [ 'version' => '', 'package' => '', 'infourl' => '', ], // Upgrade to STS release 'sts' => [ 'version' => '', 'package' => '', 'infourl' => '', ], // Upgrade to LTS release 'lts' => [ 'version' => '', 'package' => '', 'infourl' => '', ], // Upgrade to LTS release 'test' => [ 'version' => '', 'package' => '', 'infourl' => '', ], ]; foreach ($allUpdates as $update) { $sections = []; if ('current' == $update['upgrade']) { $sections[0] = 'installed'; } elseif (version_compare($update['version'], $currentMinVersion, 'ge') && version_compare( $update['version'], $currentMaxVersion, 'le' )) { $sections[0] = 'current'; } else { $sections[0] = ''; } $sections[1] = $update['lts'] ? 'lts' : 'sts'; if ($update['testing']) { $sections = ['test']; } foreach ($sections as $section) { if (empty($section)) { continue; } $existingVersionForSection = $ret[$section]['version']; if (empty($existingVersionForSection)) { $existingVersionForSection = '0.0.0'; } if (version_compare($update['version'], $existingVersionForSection, 'ge')) { $ret[$section]['version'] = $update['version']; $ret[$section]['package'] = $update['downloads'][0]['url']; $ret[$section]['infourl'] = $update['infourl']['url']; } } } // Catch the case when the latest current branch version is the installed version (up to date site) if (empty($ret['current']['version']) && ! empty($ret['installed']['version'])) { $ret['current'] = $ret['installed']; } return $ret; } }