diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index be67d08896bee..efb59b68659c7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -32,11 +32,11 @@ before it will be accepted, we ask that you follow these simple guidelines: Please be patient as not all items will be viewed or tested immediately (remember, all bug testing for the Joomla! CMS is done by volunteers) and be receptive to feedback about your code. #### Branches -Bug fixing PRs should be made to the `5.4-dev` branch. Merged bugfixes will be upmerged into the current branches. New features that do not break backwards compatibility should be made to the `6.2-dev`. +Bug fixing PRs should be made to the `5.4-dev` branch. Merged bugfixes will be upmerged into the current branches. New features that do not break backwards compatibility should be made to the `6.3-dev`. | Branch | Purpose | |---------|--------------------------------------------------------------------------------------------------------------| | 5.4-dev | Branch for the current 5.x Joomla version. | -| 6.1-dev | Branch for the current 6.x Joomla version. Bugfixes only for 6.1 go into this branch. | -| 6.2-dev | Branch for the next minor 6.x Joomla version. New features have to go into this branch. | +| 6.2-dev | Branch for the current 6.x Joomla version. Bugfixes only for 6.2 go into this branch. | +| 6.3-dev | Branch for the next minor 6.x Joomla version. New features have to go into this branch. | | 7.0-dev | Branch for the next major Joomla version. New features that include a b/c break have to go into this branch. | diff --git a/.github/workflows/create-translation-pull-request-v6.yml b/.github/workflows/create-translation-pull-request-v6.yml index fa61039c8d267..03017595a8feb 100644 --- a/.github/workflows/create-translation-pull-request-v6.yml +++ b/.github/workflows/create-translation-pull-request-v6.yml @@ -12,7 +12,7 @@ on: env: JOOMLA_MAJOR_VERSION: 6 - JOOMLA_MINOR_VERSION: '6.1' + JOOMLA_MINOR_VERSION: '6.2' permissions: contents: read @@ -61,7 +61,7 @@ jobs: - name: Update static error pages run: | - npm ci --ignore-scripts && node build/build.mjs --build-pages + npm ci --ignore-scripts && node --env-file=./build/production.env build/build.mjs build -n error-pages - name: Create commit continue-on-error: true diff --git a/README.md b/README.md index 3f324ff11b375..d6838855f2442 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,18 @@ Joomla! CMS™ | Actions | PHP | Node | npm | |-------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|----------------------------------------------------------------------------------| -| [![Build Status](https://github.com/joomla/joomla-cms/actions/workflows/ci.yml/badge.svg?branch=6.1-dev)](https://github.com/joomla/joomla-cms) | [![PHP](https://img.shields.io/badge/PHP-V8.3.0-green)](https://www.php.net/) | [![node-lts](https://img.shields.io/badge/Node-V20.0-green)](https://nodejs.org/en/) | [![npm](https://img.shields.io/badge/npm-v10.1.0-green)](https://nodejs.org/en/) | + +| [![Build Status](https://github.com/joomla/joomla-cms/actions/workflows/ci.yml/badge.svg?branch=7.0-dev)](https://github.com/joomla/joomla-cms) | [![PHP](https://img.shields.io/badge/PHP-V8.6.0-green)](https://www.php.net/) | [![node-lts](https://img.shields.io/badge/Node-V20.0-green)](https://nodejs.org/en/) | [![npm](https://img.shields.io/badge/npm-v10.1.0-green)](https://nodejs.org/en/) | ## Overview * This is the source of Joomla! 7.x. * Joomla's [Official website](https://www.joomla.org). * Joomla! 7.0 [version history](https://docs.joomla.org/Special:MyLanguage/Joomla_7.0_version_history). -* Detailed changes are in the [changelog](https://github.com/joomla/joomla-cms/commits/6.1-dev). +* Detailed changes are in the [changelog](https://github.com/joomla/joomla-cms/commits/7.0-dev). * Release Managers responsible for Joomla! 7.0 - * tbd + * Harald Leithner @HLeithner + * Stefan Wendhausen @tecpromotion ## What is Joomla? @@ -49,9 +51,9 @@ git clone https://github.com/joomla/joomla-cms.git ```bash cd joomla-cms ``` -- Go to the 6.1-dev branch: +- Go to the 7.0-dev branch: ```bash -git checkout 6.1-dev +git checkout 7.0-dev ``` - Install all the needed composer packages: ```bash @@ -80,13 +82,14 @@ and it will be regenerated on the next call to Joomla. ## Which branch should my Pull Request target? Using a simple classification keeps the project **stable**, **transparent**, and **easy** for everyone to contribute. - -| Type of change | What it means | Target branch | -|---|---|-------------------------------------------------------------------------------------------------------------------------------------| -| **Bug / Patch release** | The change fixes an actual error. The software crashes, produces the wrong result, or behaves contrary to its specification. It can be resolved without large‑scale refactoring or new functionality. | **[5.4-dev](https://github.com/joomla/joomla-cms/tree/5.4-dev)** (6.0-dev) **\*** | -| **Feature / Minor release** | Anything that isn’t a strict bug – new behavior, refactoring, performance improvements, enhancements, UI tweaks, etc. These changes are bundled together for the next minor version. | **[6.1-dev](https://github.com/joomla/joomla-cms/tree/6.1-dev)** | - -**\*** All bugs that already exist in version 5.4.x or 6.0.x should be fixed in `5.4-dev` or `6.0-dev`. Only bugs that are introduced for the first time in version 6.1.x should target the [`6.1-dev`](https://github.com/joomla/joomla-cms/tree/6.1-dev) branch. + | +======= +| Type of change | What it means | Target branch | +|---|---|-----------------------------------------------------------------------------------| +| **Bug / Patch release** | The change fixes an actual error. The software crashes, produces the wrong result, or behaves contrary to its specification. It can be resolved without large‑scale refactoring or new functionality. | **[6.2-dev](https://github.com/joomla/joomla-cms/tree/6.2-dev)** | +| **Feature / Minor release** | Anything that isn’t a strict bug – new behavior, refactoring, performance improvements, enhancements, UI tweaks, etc. These changes are bundled together for the next minor version. | **[6.3-dev](https://github.com/joomla/joomla-cms/tree/6.3-dev)** | + +**\*** All bugs that already exist in version 6.2.x should be fixed in `6.2-dev`. Only bugs that are introduced for the first time in version 6.3.x should target the `6.3-dev` branch. A member of the maintainer or bug squad team confirms the classification and sets the appropriate labels when a PR is opened. If a PR is opened in the wrong branch, a maintainer will simply ask you to retarget it to the proper branch. diff --git a/README.txt b/README.txt index 2a9ca1507ef07..64589355c8339 100644 --- a/README.txt +++ b/README.txt @@ -4,10 +4,10 @@ The Joomla! 7.0 branch is under heavy development and not all links in this docu ------------------------------------------------------------------------------------------------------ 1- Overview - * This is a Joomla! 6.x installation/upgrade package. + * This is a Joomla! 7.x installation/upgrade package. * Joomla! Official site: https://www.joomla.org * Joomla! 7.0 version history - https://docs.joomla.org/Special:MyLanguage/Joomla_7.0_version_history - * Detailed changes in the Changelog: https://github.com/joomla/joomla-cms/commits/6.1-dev + * Detailed changes in the Changelog: https://github.com/joomla/joomla-cms/commits/7.0-dev * Release Managers responsible for Joomla! 7.0 * tbd @@ -43,7 +43,7 @@ The Joomla! 7.0 branch is under heavy development and not all links in this docu 8- Ready to install Joomla? * Check the minimum requirements here: https://downloads.joomla.org/technical-requirements - * How do you install Joomla - https://docs.joomla.org/Special:MyLanguage/J5.x:Installing_Joomla + * How do you install Joomla - https://guide.joomla.org/user-manual/getting-started/getting-started-installing-joomla * You could start your Joomla! experience building your site on a local test server. When ready it can be moved to an online hosting account of your choice. See the tutorial: https://docs.joomla.org/Special:MyLanguage/Installing_Joomla_locally diff --git a/administrator/components/com_actionlogs/src/Model/ActionlogModel.php b/administrator/components/com_actionlogs/src/Model/ActionlogModel.php index c4fd19f0a77b8..cad7552234ff9 100644 --- a/administrator/components/com_actionlogs/src/Model/ActionlogModel.php +++ b/administrator/components/com_actionlogs/src/Model/ActionlogModel.php @@ -192,7 +192,8 @@ protected function sendNotificationEmails($messages, $username, $context) 'com_actionlogs.notification', $app->getLanguage()->getTag(), $this->getMailerFactory()->createMailer(), - $this->getLanguageFactory() + $this->getLanguageFactory(), + $this->getDatabase() ); $mailer->addTemplateData($templateData); $mailer->addTemplateData($templateDataPlain, true); diff --git a/administrator/components/com_admin/sql/updates/mysql/6.1.4-2026-08-27.sql b/administrator/components/com_admin/sql/updates/mysql/6.1.4-2026-08-27.sql new file mode 100644 index 0000000000000..0f3ee94db4d88 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/6.1.4-2026-08-27.sql @@ -0,0 +1,5 @@ +-- +-- Add security column to updates table +-- + +ALTER TABLE `#__updates` ADD COLUMN `security` TINYINT NULL DEFAULT NULL AFTER `extra_query` /** CAN FAIL **/; diff --git a/administrator/components/com_admin/sql/updates/mysql/6.2.0-2026-08-09.sql b/administrator/components/com_admin/sql/updates/mysql/6.2.0-2026-08-09.sql new file mode 100644 index 0000000000000..b56cdf76a49dd --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/6.2.0-2026-08-09.sql @@ -0,0 +1,9 @@ +-- +-- Add show_subcat_image param to com_newsfeeds +-- + +UPDATE `#__extensions` +SET `params` = JSON_SET(`params`, '$.show_subcat_image', '0') +WHERE `element` = 'com_newsfeeds' + AND `type` = 'component' + AND JSON_EXTRACT(`params`, '$.show_subcat_image') IS NULL; diff --git a/administrator/components/com_admin/sql/updates/mysql/6.2.0-2026-08-10.sql b/administrator/components/com_admin/sql/updates/mysql/6.2.0-2026-08-10.sql new file mode 100644 index 0000000000000..c589d3f6efefa --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/6.2.0-2026-08-10.sql @@ -0,0 +1,9 @@ +-- +-- Add show_subcat_image param to com_contact +-- + +UPDATE `#__extensions` +SET `params` = JSON_SET(`params`, '$.show_subcat_image', '0') +WHERE `element` = 'com_contact' + AND `type` = 'component' + AND JSON_EXTRACT(`params`, '$.show_subcat_image') IS NULL; diff --git a/administrator/components/com_admin/sql/updates/mysql/6.2.0-2026-09-15.sql b/administrator/components/com_admin/sql/updates/mysql/6.2.0-2026-09-15.sql new file mode 100644 index 0000000000000..be502b3e05618 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/6.2.0-2026-09-15.sql @@ -0,0 +1,11 @@ +-- disable autostart for the previous tour +UPDATE `#__guidedtours` SET `autostart` = 0 WHERE `uid` = 'joomla-whatsnew-6-1'; + +INSERT INTO `#__guidedtours` (`title`, `description`, `extensions`, `url`, `published`, `language`, `note`, `access`, `uid`, `autostart`, `created`, `created_by`, `modified`, `modified_by`) +SELECT 'COM_GUIDEDTOURS_TOUR_WHATSNEW_6_2_TITLE', 'COM_GUIDEDTOURS_TOUR_WHATSNEW_6_2_DESCRIPTION', '["com_cpanel"]', 'administrator/index.php', 1, '*', '', 1, 'joomla-whatsnew-6-2', 1, CURRENT_TIMESTAMP(), 0, CURRENT_TIMESTAMP(), 0 + WHERE NOT EXISTS (SELECT * FROM `#__guidedtours` g WHERE g.`uid` = 'joomla-whatsnew-6-2'); + +INSERT INTO `#__guidedtour_steps` (`title`, `description`, `position`, `target`, `type`, `interactive_type`, `url`, `published`, `language`, `note`, `params`, `created`, `created_by`, `modified`, `modified_by`, `tour_id`) +SELECT 'COM_GUIDEDTOURS_TOUR_WHATSNEW_6_2_STEP_0_TITLE', 'COM_GUIDEDTOURS_TOUR_WHATSNEW_6_2_STEP_0_DESCRIPTION', 'right', '#sidebarmenu nav > ul:first-of-type > li:last-child', 0, 1, '', 1, '*', '', '{"required":1,"requiredvalue":""}', CURRENT_TIMESTAMP(), 0, CURRENT_TIMESTAMP(), 0, MAX(`id`) + FROM `#__guidedtours` + WHERE `uid` = 'joomla-whatsnew-6-2'; diff --git a/administrator/components/com_admin/sql/updates/postgresql/6.1.4-2026-08-27.sql b/administrator/components/com_admin/sql/updates/postgresql/6.1.4-2026-08-27.sql new file mode 100644 index 0000000000000..52bef3ceb85dd --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/6.1.4-2026-08-27.sql @@ -0,0 +1,5 @@ +-- +-- Add security column to updates table +-- + +ALTER TABLE "#__updates" ADD COLUMN "security" smallint NULL DEFAULT NULL /** CAN FAIL **/; diff --git a/administrator/components/com_admin/sql/updates/postgresql/6.2.0-2026-08-09.sql b/administrator/components/com_admin/sql/updates/postgresql/6.2.0-2026-08-09.sql new file mode 100644 index 0000000000000..9ae963aa9060b --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/6.2.0-2026-08-09.sql @@ -0,0 +1,9 @@ +-- +-- Add show_subcat_image param to com_newsfeeds +-- + +UPDATE "#__extensions" +SET "params" = jsonb_set("params"::jsonb, '{show_subcat_image}', '0', true) +WHERE "element" = 'com_newsfeeds' + AND "type" = 'component' + AND "params"::jsonb->>'show_subcat_image' IS NULL; diff --git a/administrator/components/com_admin/sql/updates/postgresql/6.2.0-2026-08-10.sql b/administrator/components/com_admin/sql/updates/postgresql/6.2.0-2026-08-10.sql new file mode 100644 index 0000000000000..55f667c95cae3 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/6.2.0-2026-08-10.sql @@ -0,0 +1,9 @@ +-- +-- Add show_subcat_image param to com_contact +-- + +UPDATE "#__extensions" +SET "params" = jsonb_set("params"::jsonb, '{show_subcat_image}', '0', true) +WHERE "element" = 'com_contact' + AND "type" = 'component' + AND "params"::jsonb->>'show_subcat_image' IS NULL; diff --git a/administrator/components/com_admin/sql/updates/postgresql/6.2.0-2026-09-15.sql b/administrator/components/com_admin/sql/updates/postgresql/6.2.0-2026-09-15.sql new file mode 100644 index 0000000000000..30e65829334e9 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/6.2.0-2026-09-15.sql @@ -0,0 +1,11 @@ +-- disable autostart for the previous tour +UPDATE "#__guidedtours" SET "autostart" = 0 WHERE "uid" = 'joomla-whatsnew-6-1'; + +INSERT INTO "#__guidedtours" ("title", "description", "extensions", "url", "published", "language", "note", "access", "uid", "autostart", "created", "created_by", "modified", "modified_by") +SELECT 'COM_GUIDEDTOURS_TOUR_WHATSNEW_6_2_TITLE', 'COM_GUIDEDTOURS_TOUR_WHATSNEW_6_2_DESCRIPTION', '["com_cpanel"]', 'administrator/index.php', 1, '*', '', 1, 'joomla-whatsnew-6-2', 1, CURRENT_TIMESTAMP, 0, CURRENT_TIMESTAMP, 0 + WHERE NOT EXISTS (SELECT * FROM "#__guidedtours" g WHERE g."uid" = 'joomla-whatsnew-6-2'); + +INSERT INTO "#__guidedtour_steps" ("title", "description", "position", "target", "type", "interactive_type", "url", "published", "language", "note", "params", "created", "created_by", "modified", "modified_by", "tour_id") +SELECT 'COM_GUIDEDTOURS_TOUR_WHATSNEW_6_2_STEP_0_TITLE', 'COM_GUIDEDTOURS_TOUR_WHATSNEW_6_2_STEP_0_DESCRIPTION', 'right', '#sidebarmenu nav > ul:first-of-type > li:last-child', 0, 1, '', 1, '*', '', '{"required":1,"requiredvalue":""}', CURRENT_TIMESTAMP, 0, CURRENT_TIMESTAMP, 0, MAX("id") + FROM "#__guidedtours" + WHERE "uid" = 'joomla-whatsnew-6-2'; diff --git a/administrator/components/com_admin/src/Model/SysinfoModel.php b/administrator/components/com_admin/src/Model/SysinfoModel.php index c2172b578dc53..21d29ea008dee 100644 --- a/administrator/components/com_admin/src/Model/SysinfoModel.php +++ b/administrator/components/com_admin/src/Model/SysinfoModel.php @@ -317,7 +317,7 @@ public function &getInfo(): array 'compatpluginenabled' => PluginHelper::isEnabled('behaviour', 'compat' . Version::MAJOR_VERSION), 'compatpluginparameters' => $this->getCompatPluginParameters(), 'phpversion' => PHP_VERSION, - 'php' => php_uname(), + 'php' => \function_exists('php_uname') ? php_uname() : '', 'dbserver' => $db->getServerType(), 'dbversion' => $db->getVersion(), 'dbcollation' => $db->getCollation(), diff --git a/administrator/components/com_admin/tmpl/help/toc-build.php b/administrator/components/com_admin/tmpl/help/toc-build.php index 49da7d3bfad3b..e5f01ccbe3864 100644 --- a/administrator/components/com_admin/tmpl/help/toc-build.php +++ b/administrator/components/com_admin/tmpl/help/toc-build.php @@ -46,6 +46,8 @@ echo "
  • toclevel}\">{$link}
  • \n"; } } -echo "\n"; +if ($this->toclevel > 1) { + echo "\n"; +} // On return decrease the toclevel $this->toclevel -= 1; diff --git a/administrator/components/com_banners/src/Model/TracksModel.php b/administrator/components/com_banners/src/Model/TracksModel.php index c2f60ce7e468c..62621d9e48e65 100644 --- a/administrator/components/com_banners/src/Model/TracksModel.php +++ b/administrator/components/com_banners/src/Model/TracksModel.php @@ -441,9 +441,9 @@ public function getContent() . str_replace('"', '""', Text::_('JDATE')) . '"' . "\n"; foreach ($this->getItems() as $item) { - $this->content .= '"' . str_replace('"', '""', $item->banner_name) . '","' - . str_replace('"', '""', $item->client_name) . '","' - . str_replace('"', '""', $item->category_title) . '","' + $this->content .= '"' . str_replace('"', '""', $this->escapeCsvFormula($item->banner_name)) . '","' + . str_replace('"', '""', $this->escapeCsvFormula($item->client_name ?? '')) . '","' + . str_replace('"', '""', $this->escapeCsvFormula($item->category_title)) . '","' . str_replace('"', '""', ($item->track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'))) . '","' . str_replace('"', '""', $item->count) . '","' . str_replace('"', '""', $item->track_date) . '"' . "\n"; @@ -496,4 +496,26 @@ public function getContent() return $this->content; } + + /** + * Escapes potential characters that start a formula in a CSV value to prevent injection attacks + * + * @param string $value csv field value + * + * @return string + * + * @since 6.2.0 + */ + private function escapeCsvFormula(string $value): string + { + if ($value === '') { + return $value; + } + + if (\in_array($value[0], ['=', '+', '-', '@'], true)) { + return ' ' . $value; + } + + return $value; + } } diff --git a/administrator/components/com_categories/src/Model/CategoryModel.php b/administrator/components/com_categories/src/Model/CategoryModel.php index b60f7c73dc15b..b380ae1614305 100644 --- a/administrator/components/com_categories/src/Model/CategoryModel.php +++ b/administrator/components/com_categories/src/Model/CategoryModel.php @@ -28,7 +28,6 @@ use Joomla\CMS\UCM\UCMType; use Joomla\CMS\Versioning\VersionableModelInterface; use Joomla\CMS\Versioning\VersionableModelTrait; -use Joomla\Component\Categories\Administrator\Helper\CategoriesHelper; use Joomla\Component\Categories\Administrator\Table\CategoryTable; use Joomla\Database\ParameterType; use Joomla\Filesystem\Path; @@ -228,10 +227,14 @@ public function getItem($pk = null) $assoc = $this->getAssoc(); if ($assoc) { + $result->associations = []; + if ($result->id != null) { - $result->associations = ArrayHelper::toInteger(CategoriesHelper::getAssociations($result->id, $result->extension)); - } else { - $result->associations = []; + $associations = Associations::getAssociations($result->extension, '#__categories', 'com_categories.item', $result->id, 'id', 'alias', ''); + + foreach ($associations as $tag => $association) { + $result->associations[$tag] = (int) $association->id; + } } } diff --git a/administrator/components/com_categories/src/Service/HTML/AdministratorService.php b/administrator/components/com_categories/src/Service/HTML/AdministratorService.php index cddb935cf5517..3c297bc7b797a 100644 --- a/administrator/components/com_categories/src/Service/HTML/AdministratorService.php +++ b/administrator/components/com_categories/src/Service/HTML/AdministratorService.php @@ -11,13 +11,12 @@ namespace Joomla\Component\Categories\Administrator\Service\HTML; use Joomla\CMS\Factory; +use Joomla\CMS\Language\Associations; use Joomla\CMS\Language\LanguageHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Router\Route; -use Joomla\Component\Categories\Administrator\Helper\CategoriesHelper; use Joomla\Database\ParameterType; -use Joomla\Utilities\ArrayHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; @@ -47,8 +46,10 @@ public function association($catid, $extension = 'com_content') $html = ''; // Get the associations - if ($associations = CategoriesHelper::getAssociations($catid, $extension)) { - $associations = ArrayHelper::toInteger($associations); + if ($associations = Associations::getAssociations($extension, '#__categories', 'com_categories.item', $catid, 'id', 'alias', '')) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } // Get the associated categories $db = Factory::getDbo(); diff --git a/administrator/components/com_config/forms/application.xml b/administrator/components/com_config/forms/application.xml index 3e9bb13234a0e..dc24060f14b05 100644 --- a/administrator/components/com_config/forms/application.xml +++ b/administrator/components/com_config/forms/application.xml @@ -586,22 +586,32 @@ - - + + + + + @@ -616,6 +626,67 @@ lock="true" /> + + + + + + + + + + + + + + +
    + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Config\Administrator\Controller; + +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Response\JsonResponse; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Session\Session; +use Joomla\CMS\Uri\Uri; +use Joomla\CMS\User\UserHelper; +use Joomla\OAuth2\Client; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Mail Controller + * + * @since 6.2.0 + */ +class MailController extends BaseController +{ + /** + * Redirects an authorized administrator to the provider authorization endpoint. + * + * @return void + * + * @since 6.2.0 + */ + public function oauth2auth(): void + { + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + Text::_('JERROR_ALERTNOAUTHOR'), + 'error', + ); + + return; + } + + $providerConfig = $this->resolveProviderConfig(); + + if (empty($providerConfig['authorize_url']) || empty($providerConfig['client_id'])) { + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + Text::_('COM_CONFIG_MAIL_OAUTH2_AUTHORIZE_CONFIG_MISSING'), + 'warning', + ); + + return; + } + + $state = UserHelper::genRandomPassword(32); + + $this->app->getSession()->set('com_config.oauth2_state', $state); + + try { + $oauth2 = $this->createOAuth2Client($providerConfig, $state); + + $this->setRedirect($oauth2->createUrl()); + } catch (\Throwable $e) { + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + $e->getMessage(), + 'error', + ); + } + } + + + /** + * Processes the OAuth2 callback and stores the received refresh token. + * + * @return void + * + * @since 6.2.0 + */ + public function oauth2callback(): void + { + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + Text::_('JERROR_ALERTNOAUTHOR'), + 'error', + ); + + return; + } + + $session = $this->app->getSession(); + $stateFromProvider = $this->input->getString('state'); + $stateFromSession = $session->get('com_config.oauth2_state'); + + if ( + !$stateFromProvider + || !$stateFromSession + || !hash_equals($stateFromSession, $stateFromProvider) + ) { + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + Text::_('JINVALID_TOKEN'), + 'error', + ); + + return; + } + + $session->set('com_config.oauth2_state', null); + + $error = $this->input->getString('error', ''); + + if ($error !== '') { + $description = $this->input->getString('error_description'); + + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + $description ?: $error, + 'error' + ); + + return; + } + + if (!$this->input->getString('code')) { + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + Text::_('JERROR_AN_ERROR_HAS_OCCURRED'), + 'error', + ); + + return; + } + + $providerConfig = $this->resolveProviderConfig(); + + if ( + empty($providerConfig['token_url']) + || empty($providerConfig['client_id']) + || empty($providerConfig['client_secret']) + ) { + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + Text::_('COM_CONFIG_MAIL_OAUTH2_TOKEN_CONFIG_MISSING'), + 'warning', + ); + + return; + } + + try { + $oauth2 = $this->createOAuth2Client($providerConfig); + + $token = $oauth2->authenticate(); + + if ( + !\is_array($token) + || empty($token['refresh_token']) + ) { + throw new \RuntimeException( + Text::_('COM_CONFIG_MAIL_OAUTH2_TOKEN_CREATE_FAILED'), + ); + } + + $refreshToken = (string)$token['refresh_token']; + } catch (\Throwable $e) { + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + $e->getMessage(), + 'error', + ); + + return; + } + + $issuedAt = gmdate('Y-m-d H:i:s') . ' UTC'; + + $formData = (array)$this->app->getUserState( + 'com_config.config.global.data', + [], + ); + + $formData['smtp_oauth2_refresh_token'] = $refreshToken; + $formData['smtp_oauth2_token_issued_at'] = $issuedAt; + + $this->app->setUserState( + 'com_config.config.global.data', + $formData, + ); + + try { + $model = $this->getModel('Application', 'Administrator'); + $saveData = $model->getData(); + + $saveData['smtp_oauth2_refresh_token'] = $refreshToken; + $saveData['smtp_oauth2_token_issued_at'] = $issuedAt; + + if (!$model->save($saveData)) { + throw new \RuntimeException( + Text::_('COM_CONFIG_ERROR_WRITE_FAILED'), + ); + } + } catch (\Throwable $e) { + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + Text::sprintf( + 'COM_CONFIG_MAIL_OAUTH2_TOKEN_SAVE_FAILED', + $e->getMessage(), + ), + 'warning', + ); + + return; + } + + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + Text::_('COM_CONFIG_MAIL_OAUTH2_TOKEN_SAVED'), + 'message', + ); + } + + /** + * Checks whether the configured OAuth2 refresh token can be exchanged successfully. + * + * @return void + * + * @since 6.2.0 + */ + public function oauth2checktoken(): void + { + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + Text::_('JERROR_ALERTNOAUTHOR'), + 'error', + ); + + return; + } + + $providerConfig = $this->resolveProviderConfig(); + + if ( + empty($providerConfig['token_url']) + || empty($providerConfig['client_id']) + || empty($providerConfig['client_secret']) + || empty($providerConfig['refresh_token']) + ) { + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + Text::_('COM_CONFIG_MAIL_OAUTH2_TOKEN_CHECK_MISSING'), + 'warning', + ); + + return; + } + + try { + $oauth2 = $this->createOAuth2Client($providerConfig); + + $token = $oauth2->refreshToken( + $providerConfig['refresh_token'], + ); + + if ( + !\is_array($token) + || empty($token['access_token']) + ) { + throw new \RuntimeException( + Text::_('COM_CONFIG_MAIL_OAUTH2_TOKEN_CHECK_INVALID'), + ); + } + + $expiresIn = isset($token['expires_in']) + ? (int)$token['expires_in'] + : 0; + + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + Text::sprintf( + 'COM_CONFIG_MAIL_OAUTH2_TOKEN_CHECK_VALID', + $expiresIn, + ), + 'message', + ); + } catch (\Throwable $e) { + $this->setRedirect( + Route::_('index.php?option=com_config&view=application', false), + Text::sprintf( + 'COM_CONFIG_MAIL_OAUTH2_TOKEN_CHECK_FAILED', + $e->getMessage(), + ), + 'error', + ); + } + } + + /** + * Creates the OAuth2 client. + * + * @param array $providerConfig OAuth2 provider configuration. + * @param string $state OAuth2 state value. + * + * @return Client + * + * @since 6.2.0 + */ + private function createOAuth2Client(array $providerConfig, string $state = ''): Client + { + $requestParams = [ + 'response_mode' => 'query', + 'prompt' => 'consent', + 'access_type' => 'offline', + ]; + + $options = [ + 'authurl' => $providerConfig['authorize_url'], + 'tokenurl' => $providerConfig['token_url'], + 'clientid' => $providerConfig['client_id'], + 'clientsecret' => $providerConfig['client_secret'], + 'redirecturi' => Uri::base() . 'index.php?option=com_config&task=mail.oauth2callback&format=raw', + 'scope' => $providerConfig['scope'], + 'state' => $state, + 'requestparams' => $requestParams, + 'userefresh' => true, + 'sendheaders' => false, + ]; + + return new Client( + $options, + null, + $this->input, + ); + } + + /** + * Build OAuth2 configuration from global configuration. + * + * @return array + * + * @since 6.2.0 + */ + private function resolveProviderConfig(): array + { + $params = $this->app->getConfig(); + + return [ + 'client_id' => $params->get('smtp_oauth2_client_id', ''), + 'client_secret' => $params->get('smtp_oauth2_client_secret', ''), + 'refresh_token' => $params->get('smtp_oauth2_refresh_token', ''), + 'scope' => $params->get('smtp_oauth2_scope', ''), + 'authorize_url' => $params->get('smtp_oauth2_authorize_url', ''), + 'token_url' => $params->get('smtp_oauth2_token_url', ''), + ]; + } + + /** + * Method to send the test mail. + * + * @return void + * + * @since 6.2.0 + */ + public function sendtestmail() + { + // Send json mime type. + $this->app->mimeType = 'application/json'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + // Check if user token is valid. + if (!Session::checkToken()) { + $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); + echo new JsonResponse(); + $this->app->close(); + } + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + echo new JsonResponse(); + $this->app->close(); + } + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + + echo new JsonResponse($model->sendTestMail()); + + $this->app->close(); + } + +} diff --git a/administrator/components/com_config/src/Field/Oauth2TokenField.php b/administrator/components/com_config/src/Field/Oauth2TokenField.php new file mode 100644 index 0000000000000..7c9e319b6bc1a --- /dev/null +++ b/administrator/components/com_config/src/Field/Oauth2TokenField.php @@ -0,0 +1,69 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Config\Administrator\Field; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +use Joomla\CMS\Form\FormField; +use Joomla\CMS\Uri\Uri; + +/** + * Renders OAuth2 token actions for Global Configuration mail settings. + * + * @since 6.2.0 + */ +class Oauth2TokenField extends FormField +{ + /** + * The form field type. + * + * @var string + * @since 6.2.0 + */ + protected $type = 'Oauth2Token'; + + /** + * The layout to render. + * + * @var string + * @since 6.2.0 + */ + protected $layout = 'joomla.form.field.oauth2token'; + + /** + * Build the OAuth2 token action UI. + * + * @return string + * + * @since 6.2.0 + */ + protected function getInput() + { + + $formData = $this->form->getData(); + $clientId = (string) $formData->get('smtp_oauth2_client_id'); + $refreshToken = (string) $formData->get('smtp_oauth2_refresh_token'); + $tokenIssuedAt = (string) $formData->get('smtp_oauth2_token_issued_at'); + + $data = $this->getLayoutData(); + + $data['clientId'] = $clientId; + $data['refreshToken'] = !empty($refreshToken); + $data['tokenIssuedAt'] = $tokenIssuedAt; + $data['callbackUrl'] = Uri::base() . 'index.php?option=com_config&task=mail.oauth2callback&format=raw'; + $data['issueUrl'] = 'index.php?option=com_config&task=mail.oauth2auth'; + $data['checkUrl'] = 'index.php?option=com_config&task=mail.oauth2checktoken'; + + return $this->getRenderer($this->layout)->render($data); + } +} diff --git a/administrator/components/com_config/src/Model/ApplicationModel.php b/administrator/components/com_config/src/Model/ApplicationModel.php index e954140844f90..2a66c4c80212a 100644 --- a/administrator/components/com_config/src/Model/ApplicationModel.php +++ b/administrator/components/com_config/src/Model/ApplicationModel.php @@ -18,7 +18,8 @@ use Joomla\CMS\Event\Application\AfterSaveConfigurationEvent; use Joomla\CMS\Event\Application\BeforeSaveConfigurationEvent; use Joomla\CMS\Factory; -use Joomla\CMS\Language\LanguageFactoryInterface; +use Joomla\CMS\Language\LanguageFactoryAwareInterface; +use Joomla\CMS\Language\LanguageFactoryAwareTrait; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Mail\Exception\MailDisabledException; @@ -50,9 +51,10 @@ * * @since 3.2 */ -class ApplicationModel extends FormModel implements MailerFactoryAwareInterface +class ApplicationModel extends FormModel implements MailerFactoryAwareInterface, LanguageFactoryAwareInterface { use MailerFactoryAwareTrait; + use LanguageFactoryAwareTrait; /** * Array of protected password fields from the configuration.php @@ -60,7 +62,7 @@ class ApplicationModel extends FormModel implements MailerFactoryAwareInterface * @var array * @since 3.9.23 */ - private $protectedConfigurationFields = ['password', 'secret', 'smtppass', 'redis_server_auth', 'session_redis_server_auth']; + private $protectedConfigurationFields = ['password', 'secret', 'smtppass', 'smtp_oauth2_client_secret', 'smtp_oauth2_refresh_token', 'redis_server_auth', 'session_redis_server_auth']; /** * Method to get a form object. @@ -1205,6 +1207,14 @@ public function sendTestMail() $config->set('mailer', $input->get('mailer')); $config->set('mailonline', $input->get('mailonline')); + // We do not load the current oauth2 information since this information needs to be already saved after authorization + $config->set('smtp_oauth2_client_id', $app->get('smtp_oauth2_client_id', '')); + $config->set('smtp_oauth2_client_secret', $app->get('smtp_oauth2_client_secret', '')); + $config->set('smtp_oauth2_scope', $app->get('smtp_oauth2_scope', '')); + $config->set('smtp_oauth2_authorize_url', $app->get('smtp_oauth2_authorize_url', '')); + $config->set('smtp_oauth2_token_url', $app->get('smtp_oauth2_token_url', '')); + $config->set('smtp_oauth2_refresh_token', $app->get('smtp_oauth2_refresh_token', '')); + // Use smtppass only if it was submitted if ($smtppass !== null) { $config->set('smtppass', $smtppass); @@ -1217,7 +1227,7 @@ public function sendTestMail() 'com_config.test_mail', $user->getParam('language', $app->get('language')), $mail, - Factory::getContainer()->get(LanguageFactoryInterface::class), + $this->getLanguageFactory(), $this->getDatabase() ); $mailer->addTemplateData( diff --git a/administrator/components/com_config/tmpl/application/default_mail.php b/administrator/components/com_config/tmpl/application/default_mail.php index 955b7750b6ea2..6a82956cd055a 100644 --- a/administrator/components/com_config/tmpl/application/default_mail.php +++ b/administrator/components/com_config/tmpl/application/default_mail.php @@ -36,7 +36,7 @@ Text::script('JLIB_JS_AJAX_ERROR_TIMEOUT'); // Ajax request data. -$ajaxUri = Route::_('index.php?option=com_config&task=application.sendtestmail&format=json'); +$ajaxUri = Route::_('index.php?option=com_config&task=mail.sendtestmail&format=json'); $this->name = Text::_('COM_CONFIG_MAIL_SETTINGS'); $this->description = ''; diff --git a/administrator/components/com_contact/config.xml b/administrator/components/com_contact/config.xml index 85608932e1eec..e44e540825613 100644 --- a/administrator/components/com_contact/config.xml +++ b/administrator/components/com_contact/config.xml @@ -592,6 +592,18 @@ + + + + + COM_CONTENT_FIELD_OPTION_BELOW - -
    - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\Component\Content\Administrator\Service; - -// phpcs:disable PSR1.Files.SideEffects -\defined('_JEXEC') or die; -// phpcs:enable PSR1.Files.SideEffects - -/** - * Service class for generating and validating article preview tokens. - * - * Tokens are URL-safe, HMAC-SHA256 signed, and time-limited. - * They allow unauthenticated public access to article previews without - * requiring a frontend login or published state. - * - * Usage: - * $helper = new PreviewTokenService($app->get('secret')); - * $token = $helper->createToken($articleId, 24); - * $valid = $helper->validateToken($token, $articleId); - * - * @since 6.2.0 - */ -class PreviewTokenService -{ - /** - * The secret key used to sign and verify preview tokens. - * - * @var string - */ - private string $secret; - - /** - * @param string $secret The secret key - * - * @since 6.2.0 - */ - public function __construct(string $secret) - { - $this->secret = $secret; - } - - /** - * Generate a signed preview token for a given article. - * - * @param int $id The article ID. - * @param int $expiresInMinutes Number of minutes before the token expires. - * - * @return string - * - * @since 6.2.0 - */ - public function createToken(int $id, int $expiresInMinutes): string - { - $payload = $this->encode(json_encode([ - 'id' => $id, - 'exp' => time() + ($expiresInMinutes * 60), - ])); - - $signature = $this->encode(hash_hmac('sha256', $payload, $this->secret, true)); - - return $payload . '.' . $signature; - } - - /** - * Validate a preview token against a given article ID. - * - * Checks that the token: - * - Has a valid structure - * - Has not been tampered with (HMAC signature check) - * - Has not expired - * - Belongs to the expected article - * - * @param string $token The preview token from the URL. - * @param int $id The expected article ID. - * - * @return bool - * - * @since 6.2.0 - */ - public function validateToken(string $token, int $id): bool - { - $parts = explode('.', $token); - - if (\count($parts) !== 2) { - return false; - } - - [$payload, $signature] = $parts; - - if (!hash_equals($this->encode(hash_hmac('sha256', $payload, $this->secret, true)), $signature)) { - return false; - } - - $decoded = $this->decode($payload); - - if ($decoded === false) { - return false; - } - - $data = json_decode($decoded, true); - - if (!\is_array($data) || !isset($data['id'], $data['exp'])) { - return false; - } - - if (!\is_int($data['id']) || !\is_int($data['exp'])) { - return false; - } - - if ($data['id'] !== $id) { - return false; - } - - return time() < $data['exp']; - } - - /** - * Encode binary data to URL-safe Base64 string. - * - * @param string $data - * - * @return string - * - * @since 6.2.0 - */ - private function encode(string $data): string - { - return str_replace('=', '', strtr(base64_encode($data), '+/', '-_')); - } - - /** - * Decode a URL-safe Base64 string to binary data. - * - * @param string $data - * - * @return string|false - * - * @since 6.2.0 - */ - private function decode(string $data): string|false - { - if ($remainder = \strlen($data) % 4) { - $data .= str_repeat('=', 4 - $remainder); - } - - return base64_decode(strtr($data, '-_', '+/')); - } -} diff --git a/administrator/components/com_content/src/View/Article/HtmlView.php b/administrator/components/com_content/src/View/Article/HtmlView.php index 6ded1f8cbe799..cdebf91aacaf9 100644 --- a/administrator/components/com_content/src/View/Article/HtmlView.php +++ b/administrator/components/com_content/src/View/Article/HtmlView.php @@ -10,13 +10,11 @@ namespace Joomla\Component\Content\Administrator\View\Article; -use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Helper\ContentHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\View\FormView; use Joomla\CMS\Toolbar\ToolbarHelper; -use Joomla\Component\Content\Administrator\Service\PreviewTokenService; use Joomla\Component\Content\Site\Helper\RouteHelper; // phpcs:disable PSR1.Files.SideEffects @@ -89,18 +87,6 @@ protected function initializeView() $url = RouteHelper::getArticleRoute($this->item->id . ':' . $this->item->alias, $this->item->catid, $this->item->language); - // Generate preview token if editing an existing article - if ($this->item->id > 0) { - $params = ComponentHelper::getParams('com_content'); - $expiration = (int) $params->get('preview_token_expiration', 15); - - $tokenHelper = new PreviewTokenService(Factory::getApplication()->get('secret')); - $token = $tokenHelper->createToken((int) $this->item->id, $expiration); - - // Append token using proper URL query parameter handling - $url .= (!str_contains($url, '?') ? '?' : '&') . 'preview_token=' . $token; - } - $this->previewLink = $url; $this->jooa11yLink = $url . '&jooa11y=1'; diff --git a/administrator/components/com_contenthistory/src/Model/CompareModel.php b/administrator/components/com_contenthistory/src/Model/CompareModel.php index 25b3268de970a..50d445b654caa 100644 --- a/administrator/components/com_contenthistory/src/Model/CompareModel.php +++ b/administrator/components/com_contenthistory/src/Model/CompareModel.php @@ -85,8 +85,10 @@ public function getItems() $user = $this->getCurrentUser(); // Access check - if (!$user->authorise('core.edit', $table1->item_id) && !$this->canEdit($table1)) { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + foreach ([$table1, $table2] as $table) { + if (!$user->authorise('core.edit', $table->item_id) && !$this->canEdit($table)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } } $nullDate = $this->getDatabase()->getNullDate(); diff --git a/administrator/components/com_fields/src/Model/FieldModel.php b/administrator/components/com_fields/src/Model/FieldModel.php index a09edbd323d57..18e2d8b5aefb3 100644 --- a/administrator/components/com_fields/src/Model/FieldModel.php +++ b/administrator/components/com_fields/src/Model/FieldModel.php @@ -996,6 +996,21 @@ protected function loadFormData() */ public function validate($form, $data, $group = null) { + // Check if the values are unique. This should only apply for list, checkboxes and radio custom field types + if (\in_array($data['type'], ['list', 'checkboxes', 'radio']) + && !empty($data['fieldparams']['options']) + && \is_array($data['fieldparams']['options'])) { + $tmpValues = []; + foreach ($data['fieldparams']['options'] as $option) { + $tmpValues[] = $option['value']; + } + + if (\count($tmpValues) !== \count(array_unique($tmpValues))) { + Factory::getApplication()->enqueueMessage(Text::_('COM_FIELDS_FIELD_INVALID_DUPLICATE_VALUES'), 'error'); + + return false; + } + } if (!$this->getCurrentUser()->authorise('core.admin', 'com_fields')) { if (isset($data['rules'])) { unset($data['rules']); diff --git a/administrator/components/com_finder/src/Indexer/Indexer.php b/administrator/components/com_finder/src/Indexer/Indexer.php index 5a8784255ea0d..b347132f962c8 100644 --- a/administrator/components/com_finder/src/Indexer/Indexer.php +++ b/administrator/components/com_finder/src/Indexer/Indexer.php @@ -734,8 +734,13 @@ public function optimize() $query2->select('COUNT(lt.link_id)') ->from($db->quoteName('#__finder_links_terms', 'lt')) ->where($db->quoteName('lt.term_id') . ' = ' . $db->quoteName('t.term_id')); - $query->update($db->quoteName('#__finder_terms', 't')) - ->set($db->quoteName('t.links') . ' = (' . $query2 . ')'); + if ($serverType === 'mysql') { + $query->update($db->quoteName('#__finder_terms', 't')) + ->set($db->quoteName('t.links') . ' = (' . $query2 . ')'); + } else { + $query->update($db->quoteName('#__finder_terms', 't')) + ->set($db->quoteName('links') . ' = (' . $query2 . ')'); + } $db->setQuery($query); $db->execute(); diff --git a/administrator/components/com_joomlaupdate/config.xml b/administrator/components/com_joomlaupdate/config.xml index c2156111baae9..920f6401b009e 100644 --- a/administrator/components/com_joomlaupdate/config.xml +++ b/administrator/components/com_joomlaupdate/config.xml @@ -45,6 +45,7 @@ length="50" showon="updatesource:custom" validate="url" + schemes="http,https,ftp,ftps" /> getMailerFactory()->createMailer(), - $this->getLanguageFactory() + $this->getLanguageFactory(), + $this->getDatabase() ); $mailer->addRecipient($receiver->email, $receiver->name); $mailer->addTemplateData($substitutions); diff --git a/administrator/components/com_menus/tmpl/item/edit_container.php b/administrator/components/com_menus/tmpl/item/edit_container.php index 05ed03830327c..3cb65ba4678af 100644 --- a/administrator/components/com_menus/tmpl/item/edit_container.php +++ b/administrator/components/com_menus/tmpl/item/edit_container.php @@ -18,6 +18,7 @@ // Initialise related data. $menuLinks = MenusHelper::getMenuLinks('main'); +$lang = $this->getLanguage(); /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->getDocument()->getWebAssetManager(); diff --git a/administrator/components/com_messages/src/Model/MessageModel.php b/administrator/components/com_messages/src/Model/MessageModel.php index a3d910fbfd785..5855a23b66492 100644 --- a/administrator/components/com_messages/src/Model/MessageModel.php +++ b/administrator/components/com_messages/src/Model/MessageModel.php @@ -375,7 +375,7 @@ public function save($data) $linkMode, true ); - $subject = html_entity_decode($table->subject, ENT_COMPAT, 'UTF-8'); + $subject = strip_tags(html_entity_decode($table->subject, ENT_COMPAT, 'UTF-8')); $message = strip_tags(html_entity_decode($table->message, ENT_COMPAT, 'UTF-8')); // Send the email @@ -383,7 +383,8 @@ public function save($data) 'com_messages.new_message', $lang->getTag(), $this->getMailerFactory()->createMailer(), - $this->getLanguageFactory() + $this->getLanguageFactory(), + $this->getDatabase() ); $data = [ 'subject' => $subject, @@ -398,6 +399,7 @@ public function save($data) $mailer->addTemplateData($data); $mailer->setReplyTo($fromUser->email, $fromUser->name); $mailer->addRecipient($toUser->email, $toUser->name); + $mailer->addUnsafeTags(['fromname']); try { $mailer->send(); diff --git a/administrator/components/com_modules/tmpl/modules/default.php b/administrator/components/com_modules/tmpl/modules/default.php index 679f51614f2f1..993888bcbb000 100644 --- a/administrator/components/com_modules/tmpl/modules/default.php +++ b/administrator/components/com_modules/tmpl/modules/default.php @@ -107,7 +107,7 @@ $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->id || is_null($item->checked_out); $canChange = $user->authorise('core.edit.state', 'com_modules.module.' . $item->id) && $canCheckin; ?> - + id, false, 'cid', 'cb', $item->title); ?> diff --git a/administrator/components/com_newsfeeds/config.xml b/administrator/components/com_newsfeeds/config.xml index 52240dd2c3930..447a1b017cf9a 100644 --- a/administrator/components/com_newsfeeds/config.xml +++ b/administrator/components/com_newsfeeds/config.xml @@ -213,6 +213,18 @@ + + + + + getLanguage()->getTag(), $this->getMailerFactory()->createMailer(), - $this->getLanguageFactory() + $this->getLanguageFactory(), + $this->getDatabase() ); $templateData = [ diff --git a/administrator/components/com_privacy/src/Model/RequestModel.php b/administrator/components/com_privacy/src/Model/RequestModel.php index 3ad7988dba5a3..abd67609e5005 100644 --- a/administrator/components/com_privacy/src/Model/RequestModel.php +++ b/administrator/components/com_privacy/src/Model/RequestModel.php @@ -328,7 +328,8 @@ public function notifyUserAdminCreatedRequest($id) 'com_privacy.notification.admin.export', $app->getLanguage()->getTag(), $this->getMailerFactory()->createMailer(), - $this->getLanguageFactory() + $this->getLanguageFactory(), + $this->getDatabase() ); break; @@ -338,7 +339,8 @@ public function notifyUserAdminCreatedRequest($id) 'com_privacy.notification.admin.remove', $app->getLanguage()->getTag(), $this->getMailerFactory()->createMailer(), - $this->getLanguageFactory() + $this->getLanguageFactory(), + $this->getDatabase() ); break; diff --git a/administrator/components/com_tags/src/Model/TagModel.php b/administrator/components/com_tags/src/Model/TagModel.php index 06ed62fa7ae39..8e29733b935f8 100644 --- a/administrator/components/com_tags/src/Model/TagModel.php +++ b/administrator/components/com_tags/src/Model/TagModel.php @@ -15,6 +15,7 @@ use Joomla\CMS\Event\Model\AfterSaveEvent; use Joomla\CMS\Event\Model\BeforeSaveEvent; use Joomla\CMS\Factory; +use Joomla\CMS\Filter\OutputFilter; use Joomla\CMS\Form\Form; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Model\AdminModel; @@ -278,6 +279,30 @@ public function save($data) $data['published'] = 0; } + // Automatic handling of alias for empty fields + if (\in_array($input->get('task'), ['apply', 'save', 'save2new']) && (!isset($data['id']) || (int) $data['id'] == 0)) { + if (empty($data['alias'])) { + if (Factory::getApplication()->get('unicodeslugs') == 1) { + $data['alias'] = OutputFilter::stringUrlUnicodeSlug($data['title']); + } else { + $data['alias'] = OutputFilter::stringURLSafe($data['title']); + } + + $tagTable = $this->getTable(); + + if ($tagTable->load(['alias' => $data['alias'], 'parent_id' => $data['parent_id']])) { + $msg = Text::_('COM_TAGS_SAVE_WARNING'); + } + + [$title, $alias] = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); + $data['alias'] = $alias; + + if (isset($msg)) { + Factory::getApplication()->enqueueMessage($msg, 'warning'); + } + } + } + // Bind the data. if (!$table->bind($data)) { $this->setError($table->getError()); diff --git a/administrator/components/com_tags/src/Table/TagTable.php b/administrator/components/com_tags/src/Table/TagTable.php index 307e0a647c4ac..de02bd2c8b6e6 100644 --- a/administrator/components/com_tags/src/Table/TagTable.php +++ b/administrator/components/com_tags/src/Table/TagTable.php @@ -198,7 +198,7 @@ public function store($updateNulls = true) // Verify that the alias is unique $table = new static($this->getDatabase()); - if ($table->load(['alias' => $this->alias]) && ($table->id != $this->id || $this->id == 0)) { + if ($table->load(['alias' => $this->alias, 'parent_id' => (int) $this->parent_id]) && ($table->id != $this->id || $this->id == 0)) { $this->setError(Text::_('COM_TAGS_ERROR_UNIQUE_ALIAS')); // Is the existing tag trashed? diff --git a/administrator/components/com_templates/src/Controller/TemplateController.php b/administrator/components/com_templates/src/Controller/TemplateController.php index 619aaa6b65f2f..2171fe090d8a4 100644 --- a/administrator/components/com_templates/src/Controller/TemplateController.php +++ b/administrator/components/com_templates/src/Controller/TemplateController.php @@ -848,11 +848,13 @@ public function extractArchive() if ($model->extractArchive($file)) { $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file + . '&isMedia=' . $this->input->getInt('isMedia', 0); $this->setRedirect(Route::_($url, false)); } else { $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_FAIL'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file + . '&isMedia=' . $this->input->getInt('isMedia', 0); $this->setRedirect(Route::_($url, false)); } } diff --git a/administrator/components/com_templates/src/Helper/TemplateHelper.php b/administrator/components/com_templates/src/Helper/TemplateHelper.php index a50a62f4d11f7..ecddf205802c4 100644 --- a/administrator/components/com_templates/src/Helper/TemplateHelper.php +++ b/administrator/components/com_templates/src/Helper/TemplateHelper.php @@ -64,10 +64,10 @@ public static function canUpload($file, $err = '') // Media file names should never have executable extensions buried in them. $executable = [ - 'exe', 'phtml','java', 'perl', 'py', 'asp','dll', 'go', 'jar', - 'ade', 'adp', 'bat', 'chm', 'cmd', 'com', 'cpl', 'hta', 'ins', 'isp', - 'jse', 'lib', 'mde', 'msc', 'msp', 'mst', 'pif', 'scr', 'sct', 'shb', - 'sys', 'vb', 'vbe', 'vbs', 'vxd', 'wsc', 'wsf', 'wsh', 'shtml', 'shtm', 'sht', 'stm', + 'ade', 'adp', 'asp', 'bat', 'chm', 'cmd', 'com', 'cpl', 'dll', 'exe', 'go', + 'hta', 'ins', 'isp', 'jar', 'java', 'jse', 'lib', 'mde', 'msc', 'msp', 'mst', + 'perl', 'phtml', 'pif', 'py', 'scr', 'sct', 'shb', 'sht', 'shtm', 'shtml', + 'stm', 'sys', 'vb', 'vbe', 'vbs', 'vxd', 'wsc', 'wsf', 'wsh', ]; $explodedFileName = explode('.', $file['name']); diff --git a/administrator/components/com_templates/src/Model/TemplateModel.php b/administrator/components/com_templates/src/Model/TemplateModel.php index 112876f14ca83..31aecfe89819d 100644 --- a/administrator/components/com_templates/src/Model/TemplateModel.php +++ b/administrator/components/com_templates/src/Model/TemplateModel.php @@ -181,16 +181,23 @@ public function getUpdatedList($state = false, $all = false, $cleanup = false) $template = $this->getTemplate(); - $query->from($db->quoteName('#__template_overrides', 'a')); + $query->from($db->quoteName('#__template_overrides', 'a')) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'e') + . ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.extension_id') + ) + ->where($db->quoteName('e.enabled') . ' = 1') + ->where($db->quoteName('e.type') . ' = ' . $db->quote('template')); if (!$all) { $teid = (int) $template->extension_id; - $query->where($db->quoteName('extension_id') . ' = :teid') + $query->where($db->quoteName('a.extension_id') . ' = :teid') ->bind(':teid', $teid, ParameterType::INTEGER); } if ($state) { - $query->where($db->quoteName('state') . ' = 0'); + $query->where($db->quoteName('a.state') . ' = 0'); } $query->order($db->quoteName('a.modified_date') . ' DESC'); @@ -1174,11 +1181,11 @@ public function createOverride($override) $client = ApplicationHelper::getClientInfo($template->client_id); if (stristr($name, 'mod_') !== false) { - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $name); + $htmlPath = Path::check($client->path . '/templates/' . $template->element . '/html/' . $name); } elseif (stristr($override, 'com_') !== false) { $size = \count($explodeArray); - $url = Path::check($explodeArray[$size - 3] . '/' . $explodeArray[$size - 1]); + $url = Path::clean($explodeArray[$size - 3] . '/' . $explodeArray[$size - 1]); if ($explodeArray[$size - 2] == 'layouts') { $htmlPath = Path::check($client->path . '/templates/' . $template->element . '/html/layouts/' . $url); @@ -1187,7 +1194,7 @@ public function createOverride($override) } } elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) { $size = \count($explodeArray); - $layoutPath = Path::check('plg_' . $explodeArray[$size - 2] . '_' . $explodeArray[$size - 1]); + $layoutPath = Path::clean('plg_' . $explodeArray[$size - 2] . '_' . $explodeArray[$size - 1]); $htmlPath = Path::check($client->path . '/templates/' . $template->element . '/html/' . $layoutPath); } else { $layoutPath = implode('/', \array_slice($explodeArray, -2)); @@ -1195,7 +1202,7 @@ public function createOverride($override) } // Check Html folder, create if not exist - if (!is_dir(Path::check($htmlPath)) && !Folder::create($htmlPath)) { + if (!is_dir($htmlPath) && !Folder::create($htmlPath)) { $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_ERROR'), 'error'); return false; @@ -1212,10 +1219,10 @@ public function createOverride($override) } $return = $this->createTemplateOverride(Path::check($path), $htmlPath); - } elseif (stripos($override, Path::check(JPATH_ROOT . '/plugins/')) === 0) { + } elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) { $return = $this->createTemplateOverride(Path::check($override . '/tmpl'), $htmlPath); } else { - $return = $this->createTemplateOverride($override, $htmlPath); + $return = $this->createTemplateOverride(Path::check($override), $htmlPath); } if ($return) { @@ -1299,7 +1306,7 @@ public function deleteFile($file) { if ($this->getTemplate()) { $app = Factory::getApplication(); - $filePath = $this->getBasePath() . urldecode(base64_decode($file)); + $filePath = Path::check($this->getBasePath() . urldecode(base64_decode($file))); try { $return = File::delete($filePath); @@ -1424,7 +1431,7 @@ public function createFolder($name, $location) { if ($this->getTemplate()) { $app = Factory::getApplication(); - $path = Path::check($location . '/'); + $path = Path::clean($location . '/'); $base = $this->getBasePath(); if (file_exists(Path::check($base . $path . $name))) { @@ -1457,17 +1464,18 @@ public function createFolder($name, $location) public function deleteFolder($location) { if ($this->getTemplate()) { - $app = Factory::getApplication(); - $base = $this->getBasePath(); - $path = Path::check($location . '/'); + $app = Factory::getApplication(); + $base = $this->getBasePath(); + $path = Path::clean($location . '/'); + $folderPath = Path::check($base . $path); - if (!file_exists($base . $path)) { + if (!file_exists($folderPath)) { $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_NOT_EXISTS'), 'error'); return false; } - $return = Folder::delete($base . $path); + $return = Folder::delete($folderPath); if (!$return) { $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); @@ -1830,27 +1838,26 @@ public function getArchive() public function extractArchive($file) { if ($this->getTemplate()) { - $app = Factory::getApplication(); - $relPath = base64_decode($file); - $explodeArray = explode('/', $relPath); - $fileName = end($explodeArray); - $path = $this->getBasePath() . base64_decode($file); + $app = Factory::getApplication(); + $basePath = $this->getBasePath(); + $archiveFilePath = Path::check($basePath . base64_decode($file), $basePath); + $extractPath = \dirname($archiveFilePath); - if (file_exists(Path::check($path . '/' . $fileName))) { + if (file_exists($archiveFilePath)) { $zip = new \ZipArchive(); - if ($zip->open(Path::clean($path . '/' . $fileName)) === true) { + if ($zip->open($archiveFilePath) === true) { for ($i = 0; $i < $zip->numFiles; $i++) { $entry = $zip->getNameIndex($i); - if (file_exists(Path::clean($path . '/' . $entry))) { + if (file_exists(Path::check($extractPath . '/' . $entry, $extractPath))) { $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXISTS'), 'error'); return false; } } - $zip->extractTo($path); + $zip->extractTo($extractPath); return true; } diff --git a/administrator/components/com_templates/tmpl/template/default.php b/administrator/components/com_templates/tmpl/template/default.php index 4474b252c0f10..881f5cc9c2d93 100644 --- a/administrator/components/com_templates/tmpl/template/default.php +++ b/administrator/components/com_templates/tmpl/template/default.php @@ -68,7 +68,9 @@
    type != 'home') : ?>

    get('isMedia', 0) ? '/media/templates/' . ((int) $this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . str_replace('//', '/', $this->escape(base64_decode($this->file))) : '/' . ((int) $this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . str_replace('//', '/', $this->escape(base64_decode($this->file)))), $this->template->element); ?>

    - + type === 'file') : ?> + +
    type == 'file' && !empty($this->source->coreFile)) : ?> diff --git a/administrator/components/com_users/src/Controller/CaptiveController.php b/administrator/components/com_users/src/Controller/CaptiveController.php index 35ec3478803ff..064f03a616452 100644 --- a/administrator/components/com_users/src/Controller/CaptiveController.php +++ b/administrator/components/com_users/src/Controller/CaptiveController.php @@ -11,19 +11,24 @@ namespace Joomla\Component\Users\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Authentication\RememberMe; use Joomla\CMS\Date\Date; use Joomla\CMS\Event\MultiFactor\NotifyActionLog; use Joomla\CMS\Event\MultiFactor\Validate; +use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Router\Route; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\UserFactoryAwareInterface; use Joomla\CMS\User\UserFactoryAwareTrait; use Joomla\Component\Users\Administrator\Model\BackupcodesModel; use Joomla\Component\Users\Administrator\Model\CaptiveModel; +use Joomla\Database\DatabaseInterface; use Joomla\Input\Input; +use Joomla\Registry\Registry; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; @@ -229,6 +234,25 @@ function (bool $carry, $result) { $session->set('com_users.mfa_checked', 1); $session->set('com_users.mandatory_mfa_setup', 0); + // If the Remember Me cookie was deferred pending MFA, create it now that MFA has been passed + if ($session->get('com_users.remember_deferred', 0)) { + $session->set('com_users.remember_deferred', 0); + + $plugin = PluginHelper::getPlugin('authentication', 'cookie'); + + // Only mint the cookie when the cookie authentication plugin is enabled + if ($plugin && $this->app->isClient('site')) { + $user = $this->app->getIdentity() ?: $this->getUserFactory()->loadUserById(0); + + RememberMe::createOrUpdateCookie( + $user, + $this->app, + Factory::getContainer()->get(DatabaseInterface::class), + new Registry($plugin->params ?? '') + ); + } + } + // Get the return URL stored by the plugin in the session $returnUrl = $session->get('com_users.return_url', ''); diff --git a/administrator/components/com_users/src/Helper/Mfa.php b/administrator/components/com_users/src/Helper/Mfa.php index ea4e4ca4e1195..cbba09b1776eb 100644 --- a/administrator/components/com_users/src/Helper/Mfa.php +++ b/administrator/components/com_users/src/Helper/Mfa.php @@ -319,6 +319,40 @@ function ($record) use (&$hasBackupCodes) { return $records; } + /** + * Could the user still owe a Multi-factor Authentication step after their password login? + * + * This is a conservative check used to decide whether issuing a persistent credential + * (e.g. a Remember Me cookie) must be deferred until the login is fully complete. It + * returns true when the user either has MFA records or belongs to a user group which + * enforces MFA setup. The final decision on whether the captive MFA page or the + * mandatory setup page is actually shown lies with the MultiFactorAuthenticationHandler. + * + * @param User $user The user to check + * + * @return boolean + * @throws \Exception + * + * @since 6.1.4 + */ + public static function userMayNeedMfaGate(User $user): bool + { + if ($user->guest) { + return false; + } + + // The user has MFA records, so the captive MFA page may apply. + if (\count(self::getUserMfaRecords((int) $user->id))) { + return true; + } + + // The user is in a group which enforces MFA, so the mandatory setup page may apply. + $forceMFAUserGroups = ComponentHelper::getParams('com_users')->get('forceMFAUserGroups', []); + $forceMFAUserGroups = \is_array($forceMFAUserGroups) ? $forceMFAUserGroups : []; + + return \count(array_intersect($forceMFAUserGroups, $user->getAuthorisedGroups())) >= 1; + } + /** * Are the conditions for showing the MFA configuration interface met? * diff --git a/administrator/components/com_users/src/Model/MailModel.php b/administrator/components/com_users/src/Model/MailModel.php index 1b1047ea8fb5d..9c6ee1cc0dd7a 100644 --- a/administrator/components/com_users/src/Model/MailModel.php +++ b/administrator/components/com_users/src/Model/MailModel.php @@ -178,7 +178,8 @@ public function send() 'com_users.massmail.mail', $language->getTag(), $this->getMailerFactory()->createMailer(), - $this->getLanguageFactory() + $this->getLanguageFactory(), + $this->getDatabase() ); $params = ComponentHelper::getParams('com_users'); diff --git a/administrator/components/com_users/src/Model/UserModel.php b/administrator/components/com_users/src/Model/UserModel.php index 5482a9e4e00db..c1b2039c222e0 100644 --- a/administrator/components/com_users/src/Model/UserModel.php +++ b/administrator/components/com_users/src/Model/UserModel.php @@ -548,10 +548,12 @@ public function activate(&$pks) 'com_users.registration.user.admin_activated', $langTag, $this->getMailerFactory()->createMailer(), - $this->getLanguageFactory() + $this->getLanguageFactory(), + $this->getDatabase() ); $mailer->addTemplateData($mailData); $mailer->addRecipient($userData['email']); + $mailer->addUnsafeTags(['username', 'name']); try { $return = $mailer->send(); diff --git a/administrator/components/com_workflow/src/Controller/DisplayController.php b/administrator/components/com_workflow/src/Controller/DisplayController.php index 030e0aad3ccaf..308fe25b19fe1 100644 --- a/administrator/components/com_workflow/src/Controller/DisplayController.php +++ b/administrator/components/com_workflow/src/Controller/DisplayController.php @@ -70,7 +70,7 @@ public function __construct($config = [], ?MVCFactoryInterface $factory = null, // If extension is not set try to get it from input or throw an exception if (empty($this->extension)) { - $extension = $this->input->getCmd('extension'); + $extension = $this->input->getCmd('extension', ''); $parts = explode('.', $extension); diff --git a/administrator/components/com_workflow/src/Controller/StageController.php b/administrator/components/com_workflow/src/Controller/StageController.php index 4c191984b229e..ca821cfaa83ac 100644 --- a/administrator/components/com_workflow/src/Controller/StageController.php +++ b/administrator/components/com_workflow/src/Controller/StageController.php @@ -78,7 +78,7 @@ public function __construct($config = [], ?MVCFactoryInterface $factory = null, // If extension is not set try to get it from input or throw an exception if (empty($this->extension)) { - $extension = $this->input->getCmd('extension'); + $extension = $this->input->getCmd('extension', ''); $parts = explode('.', $extension); @@ -155,7 +155,6 @@ protected function allowEdit($data = [], $key = 'id') protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') { $append = parent::getRedirectToItemAppend($recordId); - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); return $append; diff --git a/administrator/components/com_workflow/src/Controller/StagesController.php b/administrator/components/com_workflow/src/Controller/StagesController.php index ae86156575217..ccadc8715f423 100644 --- a/administrator/components/com_workflow/src/Controller/StagesController.php +++ b/administrator/components/com_workflow/src/Controller/StagesController.php @@ -88,7 +88,7 @@ public function __construct(array $config = [], ?MVCFactoryInterface $factory = // If extension is not set try to get it from input or throw an exception if (empty($this->extension)) { - $extension = $this->input->getCmd('extension'); + $extension = $this->input->getCmd('extension', ''); $parts = explode('.', $extension); diff --git a/administrator/components/com_workflow/src/Controller/TransitionController.php b/administrator/components/com_workflow/src/Controller/TransitionController.php index c63b65f741c18..176c714f0409e 100644 --- a/administrator/components/com_workflow/src/Controller/TransitionController.php +++ b/administrator/components/com_workflow/src/Controller/TransitionController.php @@ -78,7 +78,7 @@ public function __construct($config = [], ?MVCFactoryInterface $factory = null, // If extension is not set try to get it from input or throw an exception if (empty($this->extension)) { - $extension = $this->input->getCmd('extension'); + $extension = $this->input->getCmd('extension', ''); $parts = explode('.', $extension); @@ -157,7 +157,7 @@ protected function allowEdit($data = [], $key = 'id') protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') { $append = parent::getRedirectToItemAppend($recordId); - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); return $append; } @@ -172,7 +172,7 @@ protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') protected function getRedirectToListAppend() { $append = parent::getRedirectToListAppend(); - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); return $append; } diff --git a/administrator/components/com_workflow/src/Controller/TransitionsController.php b/administrator/components/com_workflow/src/Controller/TransitionsController.php index 823be6773f2aa..ac0eee84e2e52 100644 --- a/administrator/components/com_workflow/src/Controller/TransitionsController.php +++ b/administrator/components/com_workflow/src/Controller/TransitionsController.php @@ -85,7 +85,7 @@ public function __construct($config = [], ?MVCFactoryInterface $factory = null, // If extension is not set try to get it from input or throw an exception if (empty($this->extension)) { - $extension = $this->input->getCmd('extension'); + $extension = $this->input->getCmd('extension', ''); $parts = explode('.', $extension); diff --git a/administrator/components/com_workflow/src/Controller/WorkflowController.php b/administrator/components/com_workflow/src/Controller/WorkflowController.php index af0c52bfdfb12..be92f9f72d83e 100644 --- a/administrator/components/com_workflow/src/Controller/WorkflowController.php +++ b/administrator/components/com_workflow/src/Controller/WorkflowController.php @@ -62,7 +62,7 @@ public function __construct($config = [], ?MVCFactoryInterface $factory = null, // If extension is not set try to get it from input or throw an exception if (empty($this->extension)) { - $extension = $this->input->getCmd('extension'); + $extension = $this->input->getCmd('extension', ''); $parts = explode('.', $extension); diff --git a/administrator/components/com_workflow/src/Controller/WorkflowsController.php b/administrator/components/com_workflow/src/Controller/WorkflowsController.php index 3d4f59877b21f..11670c5c241ff 100644 --- a/administrator/components/com_workflow/src/Controller/WorkflowsController.php +++ b/administrator/components/com_workflow/src/Controller/WorkflowsController.php @@ -62,7 +62,7 @@ public function __construct($config = [], ?MVCFactoryInterface $factory = null, // If extension is not set try to get it from input or throw an exception if (empty($this->extension)) { - $extension = $this->input->getCmd('extension'); + $extension = $this->input->getCmd('extension', ''); $parts = explode('.', $extension); diff --git a/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php b/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php index 77acbe6da1a16..2425b61efecfc 100644 --- a/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php @@ -35,7 +35,7 @@ protected function checkAccess() $view = $input->getCmd('view'); $layout = $input->getCmd('layout'); $task = $input->getCmd('task'); - $extension = $input->getCmd('extension'); + $extension = $input->getCmd('extension', ''); $parts = explode('.', $extension); $allowedTasks = [ diff --git a/administrator/components/com_workflow/src/Model/StageModel.php b/administrator/components/com_workflow/src/Model/StageModel.php index bda6664101ab6..429b8c95eb4d6 100644 --- a/administrator/components/com_workflow/src/Model/StageModel.php +++ b/administrator/components/com_workflow/src/Model/StageModel.php @@ -356,7 +356,7 @@ public function publish(&$pks, $value = 1) */ protected function preprocessForm(Form $form, $data, $group = 'content') { - $extension = Factory::getApplication()->getInput()->get('extension'); + $extension = Factory::getApplication()->getInput()->get('extension', ''); $parts = explode('.', $extension); diff --git a/administrator/components/com_workflow/src/Model/TransitionModel.php b/administrator/components/com_workflow/src/Model/TransitionModel.php index 9242a22b3920f..ffc0b3f2c635a 100644 --- a/administrator/components/com_workflow/src/Model/TransitionModel.php +++ b/administrator/components/com_workflow/src/Model/TransitionModel.php @@ -317,7 +317,7 @@ public function getWorkflow() */ protected function preprocessForm(Form $form, $data, $group = 'content') { - $extension = Factory::getApplication()->getInput()->get('extension'); + $extension = Factory::getApplication()->getInput()->get('extension', ''); $parts = explode('.', $extension); diff --git a/administrator/components/com_workflow/src/Model/WorkflowModel.php b/administrator/components/com_workflow/src/Model/WorkflowModel.php index 9ca07c065f60f..1fbbaa8b27ac6 100644 --- a/administrator/components/com_workflow/src/Model/WorkflowModel.php +++ b/administrator/components/com_workflow/src/Model/WorkflowModel.php @@ -223,7 +223,7 @@ protected function loadFormData() */ protected function preprocessForm(Form $form, $data, $group = 'content') { - $extension = Factory::getApplication()->getInput()->get('extension'); + $extension = Factory::getApplication()->getInput()->get('extension', ''); $parts = explode('.', $extension); diff --git a/administrator/components/com_workflow/src/Table/TransitionTable.php b/administrator/components/com_workflow/src/Table/TransitionTable.php index 14045f382f0e4..6986da64729da 100644 --- a/administrator/components/com_workflow/src/Table/TransitionTable.php +++ b/administrator/components/com_workflow/src/Table/TransitionTable.php @@ -58,6 +58,27 @@ public function __construct(DatabaseInterface $db, ?DispatcherInterface $dispatc parent::__construct('#__workflow_transitions', 'id', $db, $dispatcher); } + /** + * Overloaded check function + * + * @return boolean True on success + * + * @see Table::check() + * @since 6.1.4 + */ + public function check(): bool + { + if (!parent::check()) { + return false; + } + + if (empty($this->options)) { + $this->options = '{}'; + } + + return true; + } + /** * Method to bind an associative array or object to the Table instance. * This method only binds properties that are publicly accessible and optionally diff --git a/administrator/components/com_workflow/tmpl/stage/edit.php b/administrator/components/com_workflow/tmpl/stage/edit.php index 83eae9b3757a6..1dcc5638e079d 100644 --- a/administrator/components/com_workflow/tmpl/stage/edit.php +++ b/administrator/components/com_workflow/tmpl/stage/edit.php @@ -40,7 +40,7 @@ - + item->id != 0) : ?>
    diff --git a/administrator/components/com_workflow/tmpl/stages/default.php b/administrator/components/com_workflow/tmpl/stages/default.php index 22ba1d112bf35..6d28cbcec98d2 100644 --- a/administrator/components/com_workflow/tmpl/stages/default.php +++ b/administrator/components/com_workflow/tmpl/stages/default.php @@ -61,7 +61,7 @@
    - +
    , , diff --git a/administrator/components/com_workflow/tmpl/transitions/default.php b/administrator/components/com_workflow/tmpl/transitions/default.php index c416ff7e2acfc..f691c101f52f6 100644 --- a/administrator/components/com_workflow/tmpl/transitions/default.php +++ b/administrator/components/com_workflow/tmpl/transitions/default.php @@ -60,7 +60,7 @@ - +
    , , diff --git a/administrator/components/com_workflow/tmpl/workflows/default.php b/administrator/components/com_workflow/tmpl/workflows/default.php index 4aef59dca7797..d8ecbb7714cc0 100644 --- a/administrator/components/com_workflow/tmpl/workflows/default.php +++ b/administrator/components/com_workflow/tmpl/workflows/default.php @@ -61,7 +61,7 @@
    $this, 'options' => ['selectorFieldName' => 'extension']]); + echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this, 'options' => ['selectorFieldName' => 'extension', 'fieldsToPreserveOnClear' => ['extension']]]); ?> workflows)) : ?>
    @@ -69,7 +69,7 @@
    - +
    diff --git a/api/components/com_categories/src/Controller/CategoriesController.php b/api/components/com_categories/src/Controller/CategoriesController.php index 6b11683d191c7..ba3ac6b3cbc86 100644 --- a/api/components/com_categories/src/Controller/CategoriesController.php +++ b/api/components/com_categories/src/Controller/CategoriesController.php @@ -161,6 +161,7 @@ private function getExtensionFromInput() * * @return boolean * + * @since 5.4.8 * @since 6.1.3 */ protected function allowAdd($data = []) @@ -184,6 +185,7 @@ protected function allowAdd($data = []) * * @return boolean * + * @since 5.4.8 * @since 6.1.3 */ protected function allowEdit($data = [], $key = 'id') @@ -227,6 +229,7 @@ protected function allowEdit($data = [], $key = 'id') * * @return boolean * + * @since 5.4.8 * @since 6.1.3 */ protected function allowDelete(): bool diff --git a/api/components/com_contact/src/Controller/ContactController.php b/api/components/com_contact/src/Controller/ContactController.php index e9f77ec0a790c..eb2fc32d31650 100644 --- a/api/components/com_contact/src/Controller/ContactController.php +++ b/api/components/com_contact/src/Controller/ContactController.php @@ -247,6 +247,7 @@ private function _sendEmail($data, $contact, $emailCopyToSender) $mailer->addRecipient($contact->email_to); $mailer->setReplyTo($templateData['email'], $templateData['name']); $mailer->addTemplateData($templateData); + $mailer->addUnsafeTags(['name', 'email', 'body']); $sent = $mailer->send(); // If we are supposed to copy the sender, do so. @@ -260,6 +261,7 @@ private function _sendEmail($data, $contact, $emailCopyToSender) $mailer->addRecipient($templateData['email']); $mailer->setReplyTo($templateData['email'], $templateData['name']); $mailer->addTemplateData($templateData); + $mailer->addUnsafeTags(['name', 'email', 'body']); $sent = $mailer->send(); } } catch (MailDisabledException | phpMailerException $exception) { diff --git a/api/components/com_fields/src/Controller/FieldsController.php b/api/components/com_fields/src/Controller/FieldsController.php index da1fe66357dca..e5347ad148203 100644 --- a/api/components/com_fields/src/Controller/FieldsController.php +++ b/api/components/com_fields/src/Controller/FieldsController.php @@ -92,6 +92,7 @@ private function getContextFromInput() * * @return boolean * + * @since 5.4.7 * @since 6.1.2 */ protected function allowAdd($data = []) @@ -117,6 +118,7 @@ protected function allowAdd($data = []) * * @return boolean * + * @since 5.4.7 * @since 6.1.2 */ protected function allowEdit($data = [], $key = 'id') @@ -169,6 +171,7 @@ protected function allowEdit($data = [], $key = 'id') * * @return boolean * + * @since 5.4.8 * @since 6.1.3 */ protected function allowDelete(): bool diff --git a/api/components/com_fields/src/Controller/GroupsController.php b/api/components/com_fields/src/Controller/GroupsController.php index d70b58872db27..8c8d374f63bc1 100644 --- a/api/components/com_fields/src/Controller/GroupsController.php +++ b/api/components/com_fields/src/Controller/GroupsController.php @@ -92,6 +92,7 @@ private function getContextFromInput() * * @return boolean * + * @since 5.4.7 * @since 6.1.2 */ protected function allowAdd($data = []) @@ -116,6 +117,7 @@ protected function allowAdd($data = []) * * @return boolean * + * @since 5.4.7 * @since 6.1.2 */ protected function allowEdit($data = [], $key = 'parent_id') @@ -175,6 +177,7 @@ protected function allowEdit($data = [], $key = 'parent_id') * * @return boolean * + * @since 5.4.8 * @since 6.1.3 */ protected function allowDelete(): bool diff --git a/api/components/com_joomlaupdate/src/View/Healthcheck/JsonapiView.php b/api/components/com_joomlaupdate/src/View/Healthcheck/JsonapiView.php index e6aaa886f370a..66af708e3588e 100644 --- a/api/components/com_joomlaupdate/src/View/Healthcheck/JsonapiView.php +++ b/api/components/com_joomlaupdate/src/View/Healthcheck/JsonapiView.php @@ -70,7 +70,7 @@ protected function getStatsData() 'db_type' => $db->name, 'db_version' => $db->getVersion(), 'cms_version' => JVERSION, - 'server_os' => php_uname('s') . ' ' . php_uname('r'), + 'server_os' => \function_exists('php_uname') ? php_uname('s') . ' ' . php_uname('r') : (getenv('OSTYPE') ?: \PHP_OS), 'update_requirement_state' => $updateModel->getAutoUpdateRequirementsState(), ]; diff --git a/api/components/com_users/src/Controller/GroupsController.php b/api/components/com_users/src/Controller/GroupsController.php index 4d55cd5b3827c..a777373cc53be 100644 --- a/api/components/com_users/src/Controller/GroupsController.php +++ b/api/components/com_users/src/Controller/GroupsController.php @@ -68,7 +68,7 @@ public function delete($id = null) * * @since 6.1.1 */ - public function allowAdd($data = []) + protected function allowAdd($data = []) { // Overrides the default behavior to check the core.admin permission. return $this->app->getIdentity()->authorise('core.admin', $this->option); @@ -84,7 +84,7 @@ public function allowAdd($data = []) * * @since 6.1.1 */ - public function allowEdit($data = [], $key = 'id') + protected function allowEdit($data = [], $key = 'id') { // Overrides the default behavior to check the core.admin permission. return $this->app->getIdentity()->authorise('core.admin', $this->option); diff --git a/api/components/com_users/src/Controller/LevelsController.php b/api/components/com_users/src/Controller/LevelsController.php index 65ffeac9c231c..e87418e78ff99 100644 --- a/api/components/com_users/src/Controller/LevelsController.php +++ b/api/components/com_users/src/Controller/LevelsController.php @@ -56,4 +56,48 @@ protected function preprocessSaveData(array $data): array return $data; } + + /** + * Method to check if it's allowed to delete a record + * + * @return boolean + * + * @since 6.1.4 + */ + protected function allowDelete(): bool + { + // Overrides the default behavior to check the core.admin permission. + return $this->app->getIdentity()->authorise('core.admin', $this->option); + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 6.1.4 + */ + protected function allowAdd($data = []) + { + // Overrides the default behavior to check the core.admin permission. + return $this->app->getIdentity()->authorise('core.admin', $this->option); + } + + /** + * Method to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key; default is id. + * + * @return boolean + * + * @since 6.1.4 + */ + protected function allowEdit($data = [], $key = 'id') + { + // Overrides the default behavior to check the core.admin permission. + return $this->app->getIdentity()->authorise('core.admin', $this->option); + } } diff --git a/api/language/en-GB/joomla.ini b/api/language/en-GB/joomla.ini index 18fde5672d4c2..4769c0f544d52 100644 --- a/api/language/en-GB/joomla.ini +++ b/api/language/en-GB/joomla.ini @@ -710,10 +710,10 @@ JGRID_HEADING_ORDERING_DESC="Ordering descending" ; If there is an error connecting database before initialisation, en-GB.lib_joomla.ini can't be loaded ; we therefore have to load the strings from en-GB.ini -JLIB_DATABASE_ERROR_ADAPTER_MYSQL="The MySQL adapter 'mysql' is not available." -JLIB_DATABASE_ERROR_ADAPTER_MYSQLI="The MySQL adapter 'mysqli' is not available." +JLIB_DATABASE_ERROR_ADAPTER_MYSQL="The MariaDB/MySQL adapter 'mysql' is not available." +JLIB_DATABASE_ERROR_ADAPTER_MYSQLI="The MariaDB/MySQL adapter 'mysqli' is not available." JLIB_DATABASE_ERROR_CONNECT_DATABASE="Unable to connect to the Database: %s." -JLIB_DATABASE_ERROR_CONNECT_MYSQL="Could not connect to MySQL." +JLIB_DATABASE_ERROR_CONNECT_MYSQL="Could not connect to MariaDB/MySQL." JLIB_DATABASE_ERROR_DATABASE_CONNECT="Could not connect to database." JLIB_DATABASE_ERROR_LOAD_DATABASE_DRIVER="Unable to load Database Driver: %s." @@ -922,8 +922,8 @@ PHPMAILER_TLS="Could not start TLS" PHPMAILER_VARIABLE_SET="Can't set or reset variable: " ; Database types (allows for a more descriptive label than the internal name) -MYSQL="MySQL (PDO)" -MYSQLI="MySQLi" +MYSQL="MariaDB or MySQL (PDO)" +MYSQLI="MariaDB or MySQL (MySQLi)" ORACLE="Oracle" PGSQL="PostgreSQL (PDO)" POSTGRESQL="PostgreSQL" diff --git a/components/com_banners/src/Model/BannerModel.php b/components/com_banners/src/Model/BannerModel.php index 3e226465159ca..1f091add9f1e6 100644 --- a/components/com_banners/src/Model/BannerModel.php +++ b/components/com_banners/src/Model/BannerModel.php @@ -169,9 +169,11 @@ public function &getItem() $id = (int) $this->getState('banner.id'); // For PHP 5.3 compat we can't use $this in the lambda function below, so grab the database driver now to use it - $db = $this->getDatabase(); + $db = $this->getDatabase(); + $user = $this->getCurrentUser(); + $cacheId = md5(__METHOD__ . $id . ':' . implode(',', $user->getAuthorisedViewLevels())); - $loader = function ($id) use ($db) { + $loader = function ($id) use ($db, $user) { $nowDate = Factory::getDate()->toSql(); $query = $db->createQuery(); @@ -179,14 +181,17 @@ public function &getItem() [ $db->quoteName('a.clickurl'), $db->quoteName('a.cid'), + $db->quoteName('a.catid'), $db->quoteName('a.track_clicks'), $db->quoteName('cl.track_clicks', 'client_track_clicks'), ] ) ->from($db->quoteName('#__banners', 'a')) ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) + ->join('INNER', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) ->where($db->quoteName('a.id') . ' = :id') ->where($db->quoteName('a.state') . ' = 1') + ->where($db->quoteName('c.published') . ' = 1') ->extendWhere( 'AND', [ @@ -204,7 +209,8 @@ public function &getItem() 'OR' ) ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':nowDate', $nowDate); + ->bind(':nowDate', $nowDate) + ->whereIn($db->quoteName('c.access'), $user->getAuthorisedViewLevels(), ParameterType::INTEGER); $db->setQuery($query); @@ -212,7 +218,7 @@ public function &getItem() }; try { - $this->_item = $cache->get($loader, [$id], md5(__METHOD__ . $id)); + $this->_item = $cache->get($loader, [$id], $cacheId); } catch (CacheExceptionInterface) { $this->_item = $loader($id); } diff --git a/components/com_banners/src/Model/BannersModel.php b/components/com_banners/src/Model/BannersModel.php index bca819a72bb2c..dcfdc275986fb 100644 --- a/components/com_banners/src/Model/BannersModel.php +++ b/components/com_banners/src/Model/BannersModel.php @@ -73,6 +73,7 @@ protected function getListQuery() $keywords = $this->getState('filter.keywords'); $randomise = ($ordering === 'random'); $nowDate = Factory::getDate()->toSql(); + $groups = $this->getCurrentUser()->getAuthorisedViewLevels(); $query->select( [ @@ -91,7 +92,10 @@ protected function getListQuery() ) ->from($db->quoteName('#__banners', 'a')) ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) + ->join('INNER', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) ->where($db->quoteName('a.state') . ' = 1') + ->where($db->quoteName('c.published') . ' = 1') + ->whereIn($db->quoteName('c.access'), $groups, ParameterType::INTEGER) ->extendWhere( 'AND', [ diff --git a/components/com_banners/src/Service/Category.php b/components/com_banners/src/Service/Category.php index 4e09e9981c1cf..9cc79fd0dc4c2 100644 --- a/components/com_banners/src/Service/Category.php +++ b/components/com_banners/src/Service/Category.php @@ -32,8 +32,9 @@ class Category extends Categories */ public function __construct($options = []) { - $options['table'] = '#__banners'; - $options['extension'] = 'com_banners'; + $options['table'] = '#__banners'; + $options['extension'] = 'com_banners'; + $options['check_dates'] = true; parent::__construct($options); } diff --git a/components/com_contact/src/Model/CategoryModel.php b/components/com_contact/src/Model/CategoryModel.php index 4ddfa2bdc023f..8e7fda5f1de79 100644 --- a/components/com_contact/src/Model/CategoryModel.php +++ b/components/com_contact/src/Model/CategoryModel.php @@ -371,7 +371,7 @@ public function getCategory() } $options = []; - $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); + $options['countItems'] = $params->get('show_cat_items', 1) || !$params->get('show_empty_categories', 0); $categories = Categories::getInstance('Contact', $options); $this->_item = $categories->get($this->getState('category.id', 'root')); diff --git a/components/com_contact/src/Service/Category.php b/components/com_contact/src/Service/Category.php index d5c737181918d..fe9e7f866aa1e 100644 --- a/components/com_contact/src/Service/Category.php +++ b/components/com_contact/src/Service/Category.php @@ -36,6 +36,7 @@ public function __construct($options = []) $options['extension'] = 'com_contact'; $options['accessfield'] = 'access'; $options['statefield'] = 'published'; + $options['check_dates'] = true; parent::__construct($options); } diff --git a/components/com_contact/tmpl/category/default.xml b/components/com_contact/tmpl/category/default.xml index c80d904e96a95..5812596da3194 100644 --- a/components/com_contact/tmpl/category/default.xml +++ b/components/com_contact/tmpl/category/default.xml @@ -137,6 +137,17 @@ + + + + +
      children[$this->category->id] as $id => $child) : ?> + getParams(); ?> params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?>
    • @@ -31,7 +33,15 @@ numitems; ?>

      - + params->get('show_subcat_image') && $childParams->get('image')) : ?> + $childParams->get('image'), + 'alt' => $childParams->get('image_alt', false), + ] + ); ?> + params->get('show_subcat_desc') == 1) : ?> description) : ?>
      diff --git a/components/com_content/src/Model/ArticleModel.php b/components/com_content/src/Model/ArticleModel.php index 1c91f19f43229..6782dc4cbefdb 100644 --- a/components/com_content/src/Model/ArticleModel.php +++ b/components/com_content/src/Model/ArticleModel.php @@ -17,7 +17,6 @@ use Joomla\CMS\MVC\Model\ItemModel; use Joomla\CMS\Table\Content; use Joomla\Component\Content\Administrator\Extension\ContentComponent; -use Joomla\Component\Content\Administrator\Service\PreviewTokenService; use Joomla\Database\ParameterType; use Joomla\Registry\Registry; use Joomla\Utilities\IpHelper; @@ -69,17 +68,6 @@ protected function populateState() // If $pk is set then authorise on complete asset, else on component only $asset = empty($pk) ? 'com_content' : 'com_content.article.' . $pk; - // Validate preview token if present, before any permission checks. - $token = $app->getInput()->getString('preview_token', ''); - - if ($token !== '' && $pk > 0) { - $previewTokenHelper = new PreviewTokenService($app->get('secret')); - - if ($previewTokenHelper->validateToken($token, $pk)) { - $this->setState('article.preview', true); - } - } - if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) { $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); $this->setState('filter.archived', ContentComponent::CONDITION_ARCHIVED); @@ -189,11 +177,8 @@ public function getItem($pk = null) $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); } - $isPreview = $this->getState('article.preview', false); - if ( - !$isPreview - && !$user->authorise('core.edit.state', 'com_content.article.' . $pk) + !$user->authorise('core.edit.state', 'com_content.article.' . $pk) && !$user->authorise('core.edit', 'com_content.article.' . $pk) ) { // Filter by start and end dates. @@ -222,7 +207,7 @@ public function getItem($pk = null) $published = $this->getState('filter.published'); $archived = $this->getState('filter.archived'); - if (!$isPreview && is_numeric($published)) { + if (is_numeric($published)) { $query->whereIn($db->quoteName('a.state'), [(int) $published, (int) $archived]); } @@ -235,7 +220,7 @@ public function getItem($pk = null) } // Check for published state if filter set. - if (!$isPreview && (is_numeric($published) || is_numeric($archived)) && ($data->state != $published && $data->state != $archived)) { + if ((is_numeric($published) || is_numeric($archived)) && ($data->state != $published && $data->state != $archived)) { throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); } @@ -277,8 +262,8 @@ public function getItem($pk = null) } // Compute view access permissions. - if ($isPreview || $this->getState('filter.access')) { - // Valid preview token or access filter set — user can view. + if ($this->getState('filter.access')) { + // If the access filter has been set, we already know this user can view. $data->params->set('access-view', true); } else { // If no access filter is set, the layout takes some responsibility for display of limited information. diff --git a/components/com_content/src/Model/CategoryModel.php b/components/com_content/src/Model/CategoryModel.php index d64a842b75b7c..966de3227e111 100644 --- a/components/com_content/src/Model/CategoryModel.php +++ b/components/com_content/src/Model/CategoryModel.php @@ -362,7 +362,7 @@ public function getCategory() if (isset($this->state) && !empty($this->state->get('params'))) { $params = $this->state->get('params'); $options = []; - $options['countItems'] = $params->get('show_cat_num_articles', 1) || !$params->get('show_empty_categories_cat', 0); + $options['countItems'] = $params->get('show_cat_num_articles', 1) || !$params->get('show_empty_categories', 0); $options['access'] = $params->get('check_access_rights', 1); $options['accessOnItems'] = !$params->get('show_noauth', 0); } else { diff --git a/components/com_content/src/Service/Category.php b/components/com_content/src/Service/Category.php index ad5d8b055293b..7fff87207c335 100644 --- a/components/com_content/src/Service/Category.php +++ b/components/com_content/src/Service/Category.php @@ -35,6 +35,7 @@ public function __construct($options = []) $options['table'] = '#__content'; $options['extension'] = 'com_content'; $options['accessfield'] = 'access'; + $options['check_dates'] = true; parent::__construct($options); } diff --git a/components/com_content/tmpl/category/blog.php b/components/com_content/tmpl/category/blog.php index aeb5dbaedbae5..c1bef7831d01a 100644 --- a/components/com_content/tmpl/category/blog.php +++ b/components/com_content/tmpl/category/blog.php @@ -145,7 +145,7 @@
      - maxLevel != 0 && !empty($this->children[$this->category->id])) : ?> + maxLevel != 0 && $this->category->hasVisibleChildren($this->getCurrentUser()->getAuthorisedViewLevels(), (bool) $this->params->get('show_empty_categories', 0))) : ?>
    , , diff --git a/administrator/language/en-GB/com_config.ini b/administrator/language/en-GB/com_config.ini index a512f603f88d9..7b82ff32550b6 100644 --- a/administrator/language/en-GB/com_config.ini +++ b/administrator/language/en-GB/com_config.ini @@ -62,7 +62,7 @@ COM_CONFIG_FIELD_DATABASE_NAME_LABEL="Database Name" COM_CONFIG_FIELD_DATABASE_PASSWORD_DESC="Do not edit this field unless absolutely necessary (eg after the transfer of the database to a new hosting provider)." COM_CONFIG_FIELD_DATABASE_PASSWORD_LABEL="Database Password" COM_CONFIG_FIELD_DATABASE_PREFIX_LABEL="Database Tables Prefix" -COM_CONFIG_FIELD_DATABASE_TYPE_DESC="Do not change this to a different database technology e.g. from MySQLi (MySQL database technology) to PostgreSQL (Postgres database technology); it will break your site." +COM_CONFIG_FIELD_DATABASE_TYPE_DESC="Do not change this to a different database technology e.g. from MySQLi (MariaDB/MySQL database technology) to PostgreSQL (Postgres database technology); it will break your site." COM_CONFIG_FIELD_DATABASE_TYPE_LABEL="Database Type" COM_CONFIG_FIELD_DATABASE_USERNAME_LABEL="Database Username" COM_CONFIG_FIELD_DEBUG_CONST="Constant" @@ -130,7 +130,17 @@ COM_CONFIG_FIELD_MAIL_REPLY_TO_EMAIL_LABEL="Reply To Email" COM_CONFIG_FIELD_MAIL_REPLY_TO_NAME_LABEL="Reply To Name" COM_CONFIG_FIELD_MAIL_SENDMAIL_PATH_LABEL="Sendmail Path" COM_CONFIG_FIELD_MAIL_SMTP_AUTH_LABEL="SMTP Authentication" +COM_CONFIG_FIELD_SMTP_AUTH_VALUE_OAUTH2="OAuth 2.0" +COM_CONFIG_FIELD_SMTP_AUTH_VALUE_USERNAME_PASSWORD="Username and Password" COM_CONFIG_FIELD_MAIL_SMTP_HOST_LABEL="SMTP Host" +COM_CONFIG_FIELD_MAIL_SMTP_OAUTH2_AUTHORIZE_URL_LABEL="OAuth2 Authorize URL" +COM_CONFIG_FIELD_MAIL_SMTP_OAUTH2_CLIENT_ID_LABEL="OAuth2 Client ID" +COM_CONFIG_FIELD_MAIL_SMTP_OAUTH2_CLIENT_SECRET_LABEL="OAuth2 Client Secret" +COM_CONFIG_FIELD_MAIL_SMTP_OAUTH2_GET_TOKEN_LABEL="OAuth2 Token" +COM_CONFIG_FIELD_MAIL_SMTP_OAUTH2_INFO_DESCRIPTION="For configuration help, please refer to the Joomla Documentation.
    The call back URL for your OAuth2 application is:
    https://[domain]/administrator/index.php?option=com_config&task=mail.oauth2callback&format=raw
    In most cases the domain should satisfy the requirements." +COM_CONFIG_FIELD_MAIL_SMTP_OAUTH2_INFO_LABEL="OAuth2 Information" +COM_CONFIG_FIELD_MAIL_SMTP_OAUTH2_SCOPE_LABEL="OAuth2 Scope" +COM_CONFIG_FIELD_MAIL_SMTP_OAUTH2_TOKEN_URL_LABEL="OAuth2 Token URL" COM_CONFIG_FIELD_MAIL_SMTP_PASSWORD_LABEL="SMTP Password" COM_CONFIG_FIELD_MAIL_SMTP_PORT_LABEL="SMTP Port" COM_CONFIG_FIELD_MAIL_SMTP_SECURE_LABEL="SMTP Security" @@ -234,6 +244,24 @@ COM_CONFIG_LOCATION_SETTINGS="Location" COM_CONFIG_LOGGING="Logging" COM_CONFIG_LOGGING_CUSTOM_SETTINGS="Custom logging" COM_CONFIG_LOGGING_SETTINGS="Logging" +COM_CONFIG_MAIL_OAUTH2_AUTHORIZE_CONFIG_MISSING="OAuth2 authorize URL or client ID is missing." +COM_CONFIG_MAIL_OAUTH2_BUTTON_CHECK="Validate Token" +COM_CONFIG_MAIL_OAUTH2_BUTTON_ISSUE="Get OAuth2 Token" +COM_CONFIG_MAIL_OAUTH2_BUTTON_REISSUE="Reissue OAuth2 Token" +COM_CONFIG_MAIL_OAUTH2_BUTTON_TITLE="Get OAuth2 Token" +COM_CONFIG_MAIL_OAUTH2_BUTTON_TITLE_REISSUE="Reissue OAuth2 Token" +COM_CONFIG_MAIL_OAUTH2_BUTTON_TITLE_VALIDATE="Validate Token" +COM_CONFIG_MAIL_OAUTH2_CALLBACK_HINT="Make sure callback URL %s is set in provider setup" +COM_CONFIG_MAIL_OAUTH2_CLIENT_ID_REQUIRED="OAuth2 Client ID required to request token" +COM_CONFIG_MAIL_OAUTH2_TOKEN_CHECK_FAILED="Token check failed: %s" +COM_CONFIG_MAIL_OAUTH2_TOKEN_CHECK_INVALID="Token is invalid or refresh failed." +COM_CONFIG_MAIL_OAUTH2_TOKEN_CHECK_MISSING="Token validation requires client ID, client secret and refresh token." +COM_CONFIG_MAIL_OAUTH2_TOKEN_CHECK_VALID="Token is valid. Access token expires in %d seconds." +COM_CONFIG_MAIL_OAUTH2_TOKEN_CONFIG_MISSING="OAuth2 token URL, client ID or client secret is missing." +COM_CONFIG_MAIL_OAUTH2_TOKEN_CREATE_FAILED="Failed to acquire OAuth2 refresh token." +COM_CONFIG_MAIL_OAUTH2_TOKEN_ISSUED_AT="Token issued at: %s" +COM_CONFIG_MAIL_OAUTH2_TOKEN_SAVED="OAuth2 token received and saved." +COM_CONFIG_MAIL_OAUTH2_TOKEN_SAVE_FAILED="OAuth2 token received but could not be saved automatically: %s" COM_CONFIG_MAIL_SETTINGS="Mail" COM_CONFIG_MAIL_TEST_MAIL_DESC="Sent when you click the "Send Test Mail" button in the Global Configuration. It is sent to the sending mail address that is set in the mail settings." COM_CONFIG_MAIL_TEST_MAIL_TITLE="Global Configuration: Test Mail" diff --git a/administrator/language/en-GB/com_content.ini b/administrator/language/en-GB/com_content.ini index 972328dfdf72e..72b75be7c2f00 100644 --- a/administrator/language/en-GB/com_content.ini +++ b/administrator/language/en-GB/com_content.ini @@ -88,8 +88,6 @@ COM_CONTENT_FIELD_NOTE_LABEL="Note" COM_CONTENT_FIELD_OPTION_ABOVE="Above" COM_CONTENT_FIELD_OPTION_BELOW="Below" COM_CONTENT_FIELD_OPTION_SPLIT="Split" -COM_CONTENT_FIELD_PREVIEW_TOKEN_EXPIRATION_DESC="Number of minutes before a preview token expires. Preview tokens allow unauthenticated users to view unpublished articles. Default is 15 minutes." -COM_CONTENT_FIELD_PREVIEW_TOKEN_EXPIRATION_LABEL="Preview Token Expiration (minutes)" COM_CONTENT_FIELD_PUBLISH_DOWN_LABEL="Finish Publishing" COM_CONTENT_FIELD_PUBLISH_UP_LABEL="Start Publishing" COM_CONTENT_FIELD_SELECT_ARTICLE_LABEL="Select Article" diff --git a/administrator/language/en-GB/com_fields.ini b/administrator/language/en-GB/com_fields.ini index d262ead0d9af4..4d03188263815 100644 --- a/administrator/language/en-GB/com_fields.ini +++ b/administrator/language/en-GB/com_fields.ini @@ -29,6 +29,7 @@ COM_FIELDS_FIELD_IMAGE_ALT_LABEL="Alt Text" COM_FIELDS_FIELD_IMAGE_DESC="Image label." COM_FIELDS_FIELD_IMAGE_LABEL="Image" COM_FIELDS_FIELD_INVALID_DEFAULT_VALUE="The default value is invalid." +COM_FIELDS_FIELD_INVALID_DUPLICATE_VALUES="The option values must be unique." COM_FIELDS_FIELD_LABEL_FORM_CLASS_LABEL="Label Class (Form)" COM_FIELDS_FIELD_LABEL_LABEL="Label" COM_FIELDS_FIELD_LABEL_RENDER_CLASS_LABEL="Label Class (Output)" @@ -98,7 +99,7 @@ COM_FIELDS_GROUPS_TABLE_CAPTION="Field Groups" COM_FIELDS_MUSTCONTAIN_A_TITLE_FIELD="Field must have a title." COM_FIELDS_MUSTCONTAIN_A_TITLE_GROUP="Field Group must have a title." COM_FIELDS_NO_FIELDS_TO_CREATE_SUBFORM_FIELD_WARNING="You need to create standard fields before creating a Subform field." -COM_FIELDS_ONLY_USE_IN_SUBFORM="- Use in Subform -" +COM_FIELDS_ONLY_USE_IN_SUBFORM="- Select Field Usage -" COM_FIELDS_ONLY_USE_IN_SUBFORM_ANY="Any Form" COM_FIELDS_ONLY_USE_IN_SUBFORM_SUBFORM="Only in Subform" COM_FIELDS_SEARCHINDEX_MIGHT_REQUIRE_REINDEXING="Please note, that your changes in the Custom Fields Manager might require you to rebuild the index of Smart Search." diff --git a/administrator/language/en-GB/com_tags.ini b/administrator/language/en-GB/com_tags.ini index 4a4cf96e1d3fa..746148f3edf66 100644 --- a/administrator/language/en-GB/com_tags.ini +++ b/administrator/language/en-GB/com_tags.ini @@ -40,7 +40,7 @@ COM_TAGS_COUNT_UNPUBLISHED_ITEMS="Unpublished items" COM_TAGS_EMPTYSTATE_BUTTON_ADD="Add your first tag" COM_TAGS_EMPTYSTATE_CONTENT="Tags in Joomla! provide a flexible way of organizing content. The same tag can be applied to many different content items across content types." COM_TAGS_EMPTYSTATE_TITLE="No Tags have been created yet." -COM_TAGS_ERROR_UNIQUE_ALIAS="Another Tag has the same alias." +COM_TAGS_ERROR_UNIQUE_ALIAS="Another Tag with the same parent tag has the same alias." COM_TAGS_ERROR_UNIQUE_ALIAS_TRASHED="A trashed Tag with the same parent tag has the same alias." COM_TAGS_EXCLUDE="Exclude" COM_TAGS_FIELD_CONTENT_TYPE_LABEL="Content types" @@ -115,6 +115,7 @@ COM_TAGS_REBUILD_FAILURE="Failed rebuilding Tags tree data." COM_TAGS_REBUILD_SUCCESS="Tags tree data rebuilt." COM_TAGS_RIGHT="Right" COM_TAGS_SAVE_SUCCESS="Tag saved." +COM_TAGS_SAVE_WARNING="Alias already existed so a number was added at the end. You can re-edit the tag to customise the alias." COM_TAGS_SEARCH_TYPE_LABEL="Match Type" COM_TAGS_SELECT_TAGTYPE="- Select Tag Type -" COM_TAGS_SHOW_ALL_TAGS_IMAGE_LABEL="Show Heading Image" diff --git a/administrator/language/en-GB/guidedtours.joomla_whatsnew_6_2.ini b/administrator/language/en-GB/guidedtours.joomla_whatsnew_6_2.ini new file mode 100644 index 0000000000000..43922ba1377b2 --- /dev/null +++ b/administrator/language/en-GB/guidedtours.joomla_whatsnew_6_2.ini @@ -0,0 +1,2 @@ +COM_GUIDEDTOURS_TOUR_WHATSNEW_6_2_DESCRIPTION="

    Welcome to Joomla 6.2!

    Core Updates

    Content Editing Upgrades

    • Media Manager/Read More integrations: Adding images and media has become easier with the updated picker that now opens the Joomla Media Manager, and the new Read More button provides a quicker way to insert article breaks.
    • Separate image and figure classes: Article intro/full-text images now have independent CSS class fields for the image and its surrounding figure element.
    \"Sublevel

    Front-End Template (Cassiopeia)

    • Multi-level dropdown menus: Cassiopeia menus now support more than one sublevel in dropdown/collapsible layouts.

    Modules & Site Configuration

    • mod_login link toggles: New options let admins hide the “Forgot your password?” and “Forgot your username?” links without a template override. Defaults to Show, so no impact on existing sites.
    • Security flag for extension updates: Developers can flag a release as a security update (severity None to Critical); flagged updates are highlighted in System -> Update -> Extensions and on the dashboard quick icon.
    \"Security
    " +COM_GUIDEDTOURS_TOUR_WHATSNEW_6_2_TITLE="What’s New in Joomla 6.2!" diff --git a/administrator/language/en-GB/guidedtours.joomla_whatsnew_6_2_steps.ini b/administrator/language/en-GB/guidedtours.joomla_whatsnew_6_2_steps.ini new file mode 100644 index 0000000000000..12daf14d27a49 --- /dev/null +++ b/administrator/language/en-GB/guidedtours.joomla_whatsnew_6_2_steps.ini @@ -0,0 +1,2 @@ +COM_GUIDEDTOURS_TOUR_WHATSNEW_6_2_STEP_0_DESCRIPTION="

    Joomla 6.2 Full Release Notes

    View the full release notes on joomla.org


    Help and Information

    Many resources to help you can be found here together with additional links to more resources, documentation, support and how to become involved in Joomla.

    \"New

    " +COM_GUIDEDTOURS_TOUR_WHATSNEW_6_2_STEP_0_TITLE="More Information and Help" diff --git a/administrator/language/en-GB/joomla.ini b/administrator/language/en-GB/joomla.ini index 7ccabcf0c069a..d81402c728a42 100644 --- a/administrator/language/en-GB/joomla.ini +++ b/administrator/language/en-GB/joomla.ini @@ -719,10 +719,10 @@ JGRID_HEADING_ORDERING_DESC="Ordering descending" ; If there is an error connecting database before initialisation, en-GB.lib_joomla.ini can't be loaded ; we therefore have to load the strings from en-GB.ini -JLIB_DATABASE_ERROR_ADAPTER_MYSQL="The MySQL adapter 'mysql' is not available." -JLIB_DATABASE_ERROR_ADAPTER_MYSQLI="The MySQL adapter 'mysqli' is not available." +JLIB_DATABASE_ERROR_ADAPTER_MYSQL="The MariaDB/MySQL adapter 'mysql' is not available." +JLIB_DATABASE_ERROR_ADAPTER_MYSQLI="The MariaDB/MySQL adapter 'mysqli' is not available." JLIB_DATABASE_ERROR_CONNECT_DATABASE="Unable to connect to the Database: %s." -JLIB_DATABASE_ERROR_CONNECT_MYSQL="Could not connect to MySQL." +JLIB_DATABASE_ERROR_CONNECT_MYSQL="Could not connect to MariaDB/MySQL." JLIB_DATABASE_ERROR_DATABASE_CONNECT="Could not connect to database." JLIB_DATABASE_ERROR_LOAD_DATABASE_DRIVER="Unable to load Database Driver: %s." @@ -933,8 +933,8 @@ PHPMAILER_TLS="Could not start TLS" PHPMAILER_VARIABLE_SET="Can't set or reset variable: " ; Database types (allows for a more descriptive label than the internal name) -MYSQL="MySQL (PDO)" -MYSQLI="MySQLi" +MYSQL="MariaDB or MySQL (PDO)" +MYSQLI="MariaDB or MySQL (MySQLi)" ORACLE="Oracle" PGSQL="PostgreSQL (PDO)" POSTGRESQL="PostgreSQL" diff --git a/administrator/language/en-GB/lib_joomla.ini b/administrator/language/en-GB/lib_joomla.ini index cbb1f370ea2de..0f2b6b5e0ce98 100644 --- a/administrator/language/en-GB/lib_joomla.ini +++ b/administrator/language/en-GB/lib_joomla.ini @@ -106,8 +106,8 @@ JLIB_CLIENT_ERROR_LDAP_ADDRESS_NOT_AVAILABLE="Address not available." JLIB_CMS_WARNING_PROVIDE_VALID_NAME="Please provide a valid, non-blank title." -JLIB_DATABASE_ERROR_ADAPTER_MYSQL="The MySQL adapter 'mysql' is not available." -JLIB_DATABASE_ERROR_ADAPTER_MYSQLI="The MySQL adapter 'mysqli' is not available." +JLIB_DATABASE_ERROR_ADAPTER_MYSQL="The MariaDB/MySQL adapter 'mysql' is not available." +JLIB_DATABASE_ERROR_ADAPTER_MYSQLI="The MariaDB/MySQL adapter 'mysqli' is not available." JLIB_DATABASE_ERROR_ARTICLE_UNIQUE_ALIAS="Another article from this category has the same alias (remember it may be a trashed item)." JLIB_DATABASE_ERROR_BIND_FAILED_INVALID_SOURCE_ARGUMENT="%s: :bind failed. Invalid source argument." JLIB_DATABASE_ERROR_CATEGORY_REQUIRED="Category is required." @@ -121,9 +121,9 @@ JLIB_DATABASE_ERROR_CLASS_DOES_NOT_SUPPORT_ORDERING="%s does not support orderin JLIB_DATABASE_ERROR_CLASS_IS_MISSING_FIELD="Missing field in the database: %s   %s." JLIB_DATABASE_ERROR_CLASS_NOT_FOUND_IN_FILE="Table class %s not found in file." JLIB_DATABASE_ERROR_CONNECT_DATABASE="Unable to connect to the Database: %s" -JLIB_DATABASE_ERROR_CONNECT_MYSQL="Could not connect to MySQL." +JLIB_DATABASE_ERROR_CONNECT_MYSQL="Could not connect to MariaDB/MySQL." JLIB_DATABASE_ERROR_DATABASE_CONNECT="Could not connect to database." -JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED="MySQL Database Upgrade failed. Please check the Database Fixer." +JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED="MariaDB/MySQL Database Upgrade failed. Please check the Database Fixer." JLIB_DATABASE_ERROR_DELETE_CATEGORY="Left-Right data inconsistency. Can't delete category." JLIB_DATABASE_ERROR_DELETE_FAILED="%s: :delete failed - %s" JLIB_DATABASE_ERROR_DELETE_ROOT_CATEGORIES="Root categories can't be deleted." @@ -189,7 +189,7 @@ JLIB_DATABASE_ERROR_USERGROUP_TITLE_EXISTS="User group title already exists. Tit JLIB_DATABASE_ERROR_USERLEVEL_NAME_EXISTS="Level with the name "%s" already exists." JLIB_DATABASE_ERROR_USERNAME_CANNOT_CHANGE="Can't use this username." JLIB_DATABASE_ERROR_USERNAME_INUSE="Username in use." -JLIB_DATABASE_ERROR_VALID_AZ09="Please enter a valid username. No space at beginning or end, at least %d characters, must not have the following characters: < > \ " ' % ; ( ) & and be less than 150 characters long." +JLIB_DATABASE_ERROR_VALID_AZ09="Please enter a valid username. No space at beginning or end, at least %d characters, must not have the following characters: < > \ " ' % ; ( ) & and be less than 150 characters long." JLIB_DATABASE_ERROR_VALID_MAIL="The email address you entered is invalid. Please enter another email address." JLIB_DATABASE_ERROR_VIEWLEVEL="Viewlevel must have a title." JLIB_DATABASE_FUNCTION_NOERROR="DB function reports no errors." diff --git a/administrator/language/en-GB/plg_quickicon_extensionupdate.ini b/administrator/language/en-GB/plg_quickicon_extensionupdate.ini index 4b123d0dba781..bfb1562633c25 100644 --- a/administrator/language/en-GB/plg_quickicon_extensionupdate.ini +++ b/administrator/language/en-GB/plg_quickicon_extensionupdate.ini @@ -8,6 +8,7 @@ PLG_QUICKICON_EXTENSIONUPDATE_CHECKING="Checking extensions …" PLG_QUICKICON_EXTENSIONUPDATE_ERROR="Unknown extensions …" PLG_QUICKICON_EXTENSIONUPDATE_GROUP_DESC="The group of this plugin (this value is compared with the group value used in Quick Icons modules to inject icons)." PLG_QUICKICON_EXTENSIONUPDATE_GROUP_LABEL="Group" +PLG_QUICKICON_EXTENSIONUPDATE_SECURITY_UPDATEFOUND="* Security update is available for %s!" PLG_QUICKICON_EXTENSIONUPDATE_UPDATEFOUND="Updates are available! %s" PLG_QUICKICON_EXTENSIONUPDATE_SECURITY_UPDATEFOUND="* Security update is available for %s!" PLG_QUICKICON_EXTENSIONUPDATE_UPTODATE="Extensions are up to date." diff --git a/administrator/modules/mod_feed/mod_feed.xml b/administrator/modules/mod_feed/mod_feed.xml index fafc901502bd2..b07bd03c12a19 100644 --- a/administrator/modules/mod_feed/mod_feed.xml +++ b/administrator/modules/mod_feed/mod_feed.xml @@ -30,6 +30,7 @@ filter="url" required="true" validate="url" + schemes="http,https" /> hits; ?>
    - publish_up, Text::_('DATE_FORMAT_LC4')); ?> + publish_up) ? $item->created : $item->publish_up, Text::_('DATE_FORMAT_LC4')); ?>