diff --git a/administrator/components/com_joomlaupdate/config.xml b/administrator/components/com_joomlaupdate/config.xml index 18fea2ce567ee..938aed8bde579 100644 --- a/administrator/components/com_joomlaupdate/config.xml +++ b/administrator/components/com_joomlaupdate/config.xml @@ -49,5 +49,29 @@ showon="updatesource:custom" /> + + + + + + + + + + diff --git a/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php b/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php index 94bba2ca11317..87a9f5912aa99 100644 --- a/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php +++ b/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php @@ -18,6 +18,7 @@ use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\Response\JsonResponse; use Joomla\CMS\Session\Session; +use Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel; /** * The Joomla! update controller for the Update view @@ -472,7 +473,8 @@ public function finaliseconfirm() * Prints a JSON string. * Called from JS. * - * @since 3.10.0 + * @since 3.10.0 + * @deprecated 5.0 Use batchextensioncompatibility instead. * * @return void */ @@ -580,6 +582,143 @@ public function fetchExtensionCompatibility() $this->app->close(); } + /** + * Determines the compatibility information for a number of extensions. + * + * Called by the Joomla Update JavaScript (PreUpdateChecker.checkNextChunk). + * + * @return void + * @since __DEPLOY_VERSION__ + * + */ + public function batchextensioncompatibility() + { + $joomlaTargetVersion = $this->input->post->get('joomla-target-version', '', 'DEFAULT'); + $joomlaCurrentVersion = $this->input->post->get('joomla-current-version', JVERSION); + $extensionInformation = $this->input->post->get('extensions', []); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $extensionResults = []; + $leftover = []; + $startTime = microtime(true); + + foreach ($extensionInformation as $information) + { + // Only process an extension if we have spent less than 5 seconds already + $currentTime = microtime(true); + + if ($currentTime - $startTime > 5.0) + { + $leftover[] = $information; + + continue; + } + + // Get the extension information and fetch its compatibility information + $extensionID = $information['eid'] ?: ''; + $extensionVersion = $information['version'] ?: ''; + $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); + $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); + $upgradeUpdateVersion = false; + $currentUpdateVersion = false; + $upgradeWarning = 0; + + if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) + { + $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); + } + + if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) + { + $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); + } + + if ($upgradeUpdateVersion !== false) + { + $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; + + if ($currentUpdateVersion !== false) + { + // If there are updates compatible with both CMS versions use these + $bothCompatibleVersions = array_values( + array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) + ); + + if (!empty($bothCompatibleVersions)) + { + $upgradeOldestVersion = $bothCompatibleVersions[0]; + $upgradeUpdateVersion = end($bothCompatibleVersions); + } + } + + if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) + { + // Installed version is empty or older than the oldest compatible update: Update required + $resultGroup = 2; + } + else + { + // Current version is compatible + $resultGroup = 3; + } + + if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) + { + // Special case warning when version compatible with target is lower than current + $upgradeWarning = 2; + } + } + elseif ($currentUpdateVersion !== false) + { + // No compatible version for target version but there is a compatible version for current version + $resultGroup = 1; + } + else + { + // No update server available + $resultGroup = 1; + } + + // Do we need to capture + $extensionResults[] = [ + 'id' => $extensionID, + 'upgradeCompatibilityStatus' => (object) [ + 'state' => $upgradeCompatibilityStatus->state, + 'compatibleVersion' => $upgradeUpdateVersion + ], + 'currentCompatibilityStatus' => (object) [ + 'state' => $currentCompatibilityStatus->state, + 'compatibleVersion' => $currentUpdateVersion + ], + 'resultGroup' => $resultGroup, + 'upgradeWarning' => $upgradeWarning, + ]; + } + + $this->app->mimeType = 'application/json'; + $this->app->charSet = 'utf-8'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + try + { + $return = [ + 'compatibility' => $extensionResults, + 'extensions' => $leftover, + ]; + + echo new JsonResponse($return); + } + catch (\Exception $e) + { + echo $e; + } + + $this->app->close(); + } + /** * Fetch and report updates in \JSON format, for AJAX requests * diff --git a/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php b/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php index a4318560674e9..e24e607386d2f 100644 --- a/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php +++ b/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php @@ -115,6 +115,22 @@ class HtmlView extends BaseHtmlView */ protected $nonCoreCriticalPlugins = []; + /** + * Should I disable the confirmation checkbox for pre-update extension version checks? + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $noVersionCheck = false; + + /** + * Should I disable the confirmation checkbox for taking a backup before updating? + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $noBackupCheck = false; + /** * Renders the view * @@ -243,6 +259,9 @@ public function display($tpl = null) $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_DEFAULT'); } + $this->noVersionCheck = $params->get('versioncheck', 1) == 0; + $this->noBackupCheck = $params->get('backupcheck', 1) == 0; + // Remove temporary files $this->getModel()->removePackageFiles(); diff --git a/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php b/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php index 9ffbbfdca3f14..3ca0a5fb4ba25 100644 --- a/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php +++ b/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php @@ -11,6 +11,7 @@ \defined('_JEXEC') or die; +use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; @@ -50,6 +51,14 @@ class HtmlView extends BaseHtmlView */ protected $warnings = []; + /** + * Should I disable the confirmation checkbox for taking a backup before updating? + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $noBackupCheck = false; + /** * Renders the view. * @@ -74,6 +83,9 @@ public function display($tpl = null) $this->warnings = $this->get('Items', 'warnings'); } + $params = ComponentHelper::getParams('com_joomlaupdate'); + $this->noBackupCheck = $params->get('backupcheck', 1) == 0; + $this->addToolbar(); // Render the view. diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php index 2961b2d945065..63f5e5d5473f2 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php @@ -29,6 +29,7 @@ // Text::script doesn't have a sprintf equivalent so work around this $this->document->addScriptOptions('nonCoreCriticalPlugins', $this->nonCoreCriticalPlugins); +// Push Joomla! Update client-side error messages Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_CONFIRM_MESSAGE'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_NO_COMPATIBILITY_INFORMATION'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_WARNING_UNKNOWN'); @@ -41,6 +42,13 @@ Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_CONFIRM_MESSAGE'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_HELP'); +// Push Joomla! core Joomla.Request error messages +Text::script('JLIB_JS_AJAX_ERROR_CONNECTION_ABORT'); +Text::script('JLIB_JS_AJAX_ERROR_NO_CONTENT'); +Text::script('JLIB_JS_AJAX_ERROR_OTHER'); +Text::script('JLIB_JS_AJAX_ERROR_PARSE'); +Text::script('JLIB_JS_AJAX_ERROR_TIMEOUT'); + $compatibilityTypes = array( 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS' => array( 'class' => 'info', @@ -301,7 +309,7 @@ version; ?> - + noVersionCheck): ?>
@@ -339,8 +348,10 @@ class="extension-check upcomp hidden"
+ - diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php index 4daba3221259a..258fdc97cc655 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php @@ -47,8 +47,10 @@ endif; // Confirm backup and check -$displayData['content'] .= '
- +$classVisibility = $this->noBackupCheck ? 'd-none' : ''; +$checked = $this->noBackupCheck ? 'checked' : ''; +$displayData['content'] .= '
+ diff --git a/administrator/components/com_joomlaupdate/tmpl/upload/default.php b/administrator/components/com_joomlaupdate/tmpl/upload/default.php index 560fff207b5f3..c0f7837f3ffb2 100644 --- a/administrator/components/com_joomlaupdate/tmpl/upload/default.php +++ b/administrator/components/com_joomlaupdate/tmpl/upload/default.php @@ -82,8 +82,10 @@
-
- +
+ noBackupCheck ? 'checked' : '' ?>> diff --git a/administrator/language/en-GB/com_joomlaupdate.ini b/administrator/language/en-GB/com_joomlaupdate.ini index adfa68698301f..edd8a2b24c5df 100644 --- a/administrator/language/en-GB/com_joomlaupdate.ini +++ b/administrator/language/en-GB/com_joomlaupdate.ini @@ -5,6 +5,8 @@ COM_JOOMLAUPDATE_CAPTIVE_HEADLINE="Confirm your credentials" COM_JOOMLAUPDATE_CHECKED_UPDATES="Checked for updates." +COM_JOOMLAUPDATE_CONFIG_BACKUPCHECK_DESC="Shows the checkbox to confirm you have taken a backup and you're ready to update in the final step before the update is actually applied." +COM_JOOMLAUPDATE_CONFIG_BACKUPCHECK_LABEL="Confirm Backup Checkbox" COM_JOOMLAUPDATE_CONFIG_CUSTOMURL_LABEL="Custom URL" COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where Joomla gets its update information from." COM_JOOMLAUPDATE_CONFIG_SOURCES_LABEL="Update Source" @@ -14,6 +16,8 @@ COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_DEFAULT="Default" COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_LABEL="Update Channel" COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="Joomla Next" COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_TESTING="Testing" +COM_JOOMLAUPDATE_CONFIG_VERSIONCHECK_DESC="Shows the checkbox in the pre–update check if any of the extensions installed on your site is potentially incompatible with the version of Joomla you are upgrading to. Note: the checkbox is displayed when upgrading to a new Joomla version family (minor or major version)." +COM_JOOMLAUPDATE_CONFIG_VERSIONCHECK_LABEL="Potentially incompatible extensions checkbox" COM_JOOMLAUPDATE_CONFIGURATION="Joomla Update: Options" COM_JOOMLAUPDATE_CONFIRM="Confirm" COM_JOOMLAUPDATE_EMPTYSTATE_APPEND="Upload and Update" @@ -114,7 +118,7 @@ COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS_NOTES="

Ple COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_SHOW_LESS_COMPATIBILITY_INFORMATION="Less Details" COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_SHOW_MORE_COMPATIBILITY_INFORMATION="More Details" COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION="Update Information Unavailable" -COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION_NOTES="Extension does not offer a compatible version for the selected target version of Joomla. This could mean the extension does not use the Joomla update system or the developer has not provided compatibility information for this Joomla version yet." +COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION_NOTES="Joomla cannot detect the extension's compatibility with the target version of Joomla." COM_JOOMLAUPDATE_VIEW_DEFAULT_NON_CORE_BACKEND_TEMPLATE_USED_NOTICE="We detected that you are not using a core admin template. You might find the upgrade process smoother if you switch to use the %s template." COM_JOOMLAUPDATE_VIEW_DEFAULT_HELP="More Information" COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL="Additional Information" diff --git a/build/media_source/com_joomlaupdate/js/default.es6.js b/build/media_source/com_joomlaupdate/js/default.es6.js index 09072232968e2..f8b54dfa634dd 100644 --- a/build/media_source/com_joomlaupdate/js/default.es6.js +++ b/build/media_source/com_joomlaupdate/js/default.es6.js @@ -53,6 +53,7 @@ Joomla = window.Joomla || {}; const form = installButton ? installButton.closest('form') : null; const task = form ? form.querySelector('[name=task]', form) : null; if (uploadButton) { + uploadButton.disabled = !updateCheck.checked; uploadButton.addEventListener('click', Joomla.submitbuttonUpload); updateCheck.addEventListener('change', () => { uploadButton.disabled = !updateCheck.checked; @@ -80,11 +81,11 @@ Joomla = window.Joomla || {}; } else if (fileSize <= allowedSize && !updateCheck.disabled && !updateCheck.checked) { updateCheck.disabled = false; } else if (fileSize <= allowedSize && updateCheck.checked) { - updateCheck.checked = false; + updateCheck.checked = updateCheck.classList.contains('d-none'); uploadButton.disabled = true; } else if (fileSize > allowedSize && !updateCheck.disabled) { updateCheck.disabled = !updateCheck.disabled; - updateCheck.checked = false; + updateCheck.checked = updateCheck.classList.contains('d-none'); uploadButton.disabled = true; } }); @@ -118,6 +119,7 @@ Joomla = window.Joomla || {}; */ PreUpdateChecker.config = { serverUrl: 'index.php?option=com_joomlaupdate&task=update.fetchextensioncompatibility', + batchUrl: 'index.php?option=com_joomlaupdate&task=update.batchextensioncompatibility', selector: '.extension-check', }; @@ -249,12 +251,130 @@ Joomla = window.Joomla || {}; }); // Grab all extensions based on the selector set in the config object + const extensionsInformation = []; + [].slice.call(extensions) .forEach((extension) => { - // Check compatibility for each extension, pass an object and a callback - // function after completing the request - PreUpdateChecker.checkCompatibility(extension, PreUpdateChecker.setResultView); + const thisInfo = { + eid: extension.getAttribute('data-extension-id'), + version: extension.getAttribute('data-extension-current-version'), + }; + extensionsInformation.push(thisInfo); }); + + PreUpdateChecker.checkNextChunk(extensionsInformation); + }; + + /** + * Converts a simple object containing query string parameters to a single, escaped query string + * + * @param data {object|string} A plain object containing the query parameters to pass + * @param prefix {string} Prefix for array-type parameters + * + * @returns {string} + */ + PreUpdateChecker.interpolateParameters = (data, prefix) => { + let encodedString = ''; + + if (typeof data !== 'object' || data === null || !data) { + return ''; + } + + Object.keys(data).forEach((prop) => { + const item = data[prop]; + + if (encodedString.length > 0) { + encodedString += '&'; + } + + // Scalar values + if (typeof item === 'object') { + const newPrefix = prefix.length ? `${prefix}[${prop}]` : prop; + + encodedString += PreUpdateChecker.interpolateParameters(item, newPrefix); + + return; + } + + if (prefix === '') { + encodedString += `${encodeURIComponent(prop)}=${encodeURIComponent(item)}`; + + return; + } + + encodedString += `${encodeURIComponent(prefix)}[${encodeURIComponent(prop)}]=${encodeURIComponent(item)}`; + }); + + return encodedString; + }; + + /** + * Check the compatibility of several extensions. + * + * Asks the server to check the compatibility of as many extensions as possible. The server + * returns these results and the remainder of the extensions not already checked. + * + * @param {Array} extensionsArray + */ + PreUpdateChecker.checkNextChunk = (extensionsArray) => { + if (extensionsArray.length === 0) { + return; + } + + Joomla.request({ + url: PreUpdateChecker.config.batchUrl, + method: 'POST', + data: PreUpdateChecker.interpolateParameters({ + 'joomla-target-version': PreUpdateChecker.joomlaTargetVersion, + 'joomla-current-version': PreUpdateChecker.joomlaCurrentVersion, + extensions: extensionsArray, + }, ''), + onSuccess(data) { + const response = JSON.parse(data); + + if (response.messages) { + Joomla.renderMessages(response.messages); + } + + const extensions = response.data.extensions || []; + + response.data.compatibility.forEach((record) => { + const node = document.getElementById(`preUpdateCheck_${record.id}`); + + if (!node) { + return; + } + + PreUpdateChecker.setResultView({ + element: node, + compatibleVersion: 0, + serverError: 0, + compatibilityData: record, + }); + }); + + PreUpdateChecker.checkNextChunk(extensions); + }, + onError(xhr) { + // Report the XHR error + Joomla.renderMessages(Joomla.ajaxErrorsMessages(xhr)); + + // Mark all pending extensions as errored out on the server side + extensionsArray.forEach((info) => { + const node = document.getElementById(`preUpdateCheck_${info.eid}`); + + if (!node) { + return; + } + + PreUpdateChecker.setResultView({ + element: node, + compatibleVersion: 0, + serverError: 1, + }); + }); + }, + }); }; /** @@ -263,9 +383,8 @@ Joomla = window.Joomla || {}; * on the data set in the element's data attributes. * * @param {Object} extension - * @param {callable} callback */ - PreUpdateChecker.checkCompatibility = (node, callback) => { + PreUpdateChecker.checkCompatibility = (node) => { // Result object passed to the callback // Set to server error by default const extension = { @@ -287,12 +406,12 @@ Joomla = window.Joomla || {}; extension.serverError = 0; extension.compatibilityData = response.data; // Pass the retrieved data to the callback - callback(extension); + PreUpdateChecker.setResultView(extension); }, onError() { extension.serverError = 1; // Pass the retrieved data to the callback - callback(extension); + PreUpdateChecker.setResultView(extension); }, }); };