diff --git a/administrator/components/com_admin/script.php b/administrator/components/com_admin/script.php index abf7351fc2611..37cef488b28d3 100644 --- a/administrator/components/com_admin/script.php +++ b/administrator/components/com_admin/script.php @@ -1063,9 +1063,6 @@ public function deleteUnexistingFiles($dryRun = false, $suppressOutput = false) '/libraries/vendor/symfony/validator/Test/ConstraintValidatorTestCase.php', '/libraries/vendor/symfony/var-dumper/Test/VarDumperTestTrait.php', // From 6.1.0-alpha3 to 6.1.0-beta1 - '/media/vendor/tinymce/langs/cy.js', - '/media/vendor/tinymce/langs/cy.min.js', - '/media/vendor/tinymce/langs/cy.min.js.gz', '/media/vendor/tinymce/langs/dv.js', '/media/vendor/tinymce/langs/dv.min.js', '/media/vendor/tinymce/langs/dv.min.js.gz', diff --git a/administrator/components/com_banners/src/Model/TracksModel.php b/administrator/components/com_banners/src/Model/TracksModel.php index c2f60ce7e468c..ac3cc6b079d5b 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 __DEPLOY_VERSION__ + */ + 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_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_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/Model/TemplateModel.php b/administrator/components/com_templates/src/Model/TemplateModel.php index 19716165e25a2..31aecfe89819d 100644 --- a/administrator/components/com_templates/src/Model/TemplateModel.php +++ b/administrator/components/com_templates/src/Model/TemplateModel.php @@ -1838,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/composer.json b/composer.json index b948a65c065af..65c4becfe538b 100644 --- a/composer.json +++ b/composer.json @@ -68,7 +68,7 @@ "joomla/language": "^4.0", "joomla/oauth1": "^4.0", "joomla/oauth2": "^4.0", - "joomla/registry": "^4.0", + "joomla/registry": "^4.0.1", "joomla/router": "^4.0", "joomla/session": "^4.0", "joomla/string": "^4.0", @@ -80,7 +80,7 @@ "fig/link-util": "^1.2.0", "google/recaptcha": "^1.3.1", "laminas/laminas-diactoros": "^3.8.0", - "paragonie/sodium_compat": "^1.24.0", + "paragonie/sodium_compat": "^1.24.2", "phpmailer/phpmailer": "^6.12.0", "psr/link": "~1.1.1", "symfony/console": "^7", @@ -113,7 +113,7 @@ "require-dev": { "phpunit/phpunit": "^9.6.34", "friendsofphp/php-cs-fixer": "^3.94.2", - "squizlabs/php_codesniffer": "^3.13.5", + "squizlabs/php_codesniffer": "^3.13.6", "joomla/mediawiki": "^4.0", "joomla/test": "~4.0", "phpstan/phpstan": "^2.1.40", diff --git a/composer.lock b/composer.lock index e36ba9b39d320..0f618fb491129 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2409c13ac45e0db9103c9377e0a7fd00", + "content-hash": "15d3129bffcb3acb775fce38c2882e37", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -705,16 +705,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.12.1", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7" + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7", - "reference": "172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", "shasum": "" }, "require": { @@ -723,7 +723,7 @@ "psr/http-message": "^1.1 || ^2.0", "ralouphie/getallheaders": "^3.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -804,7 +804,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.12.1" + "source": "https://github.com/guzzle/psr7/tree/2.12.3" }, "funding": [ { @@ -820,7 +820,7 @@ "type": "tidelift" } ], - "time": "2026-06-18T09:49:37+00:00" + "time": "2026-06-23T15:21:08+00:00" }, { "name": "jakeasmith/http_build_url", @@ -1992,16 +1992,16 @@ }, { "name": "joomla/registry", - "version": "4.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/joomla-framework/registry.git", - "reference": "5692d8423c9b568627df701e9fd505eae5ea8f72" + "reference": "788009a10cb39caad29c39a7c718eb576692ebd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/joomla-framework/registry/zipball/5692d8423c9b568627df701e9fd505eae5ea8f72", - "reference": "5692d8423c9b568627df701e9fd505eae5ea8f72", + "url": "https://api.github.com/repos/joomla-framework/registry/zipball/788009a10cb39caad29c39a7c718eb576692ebd8", + "reference": "788009a10cb39caad29c39a7c718eb576692ebd8", "shasum": "" }, "require": { @@ -2011,8 +2011,8 @@ "require-dev": { "phpstan/phpstan": "^2.1.17", "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpunit/phpunit": "^12.2.6", - "squizlabs/php_codesniffer": "^3.7.2", + "phpunit/phpunit": "^12.5 || ^13.0", + "squizlabs/php_codesniffer": "^4.0", "symfony/yaml": "^7.3" }, "suggest": { @@ -2039,9 +2039,9 @@ ], "support": { "issues": "https://github.com/joomla-framework/registry/issues", - "source": "https://github.com/joomla-framework/registry/tree/4.0.0" + "source": "https://github.com/joomla-framework/registry/tree/4.0.1" }, - "time": "2025-07-23T19:26:22+00:00" + "time": "2026-08-24T19:58:10+00:00" }, { "name": "joomla/router", @@ -2604,16 +2604,16 @@ }, { "name": "paragonie/sodium_compat", - "version": "v1.24.0", + "version": "v1.24.2", "source": { "type": "git", "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "2cb48f26130919f92f30650bdcc30e6f4ebe45ac" + "reference": "4524c98a826776c9a32d6ce06932563cb10f3995" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/2cb48f26130919f92f30650bdcc30e6f4ebe45ac", - "reference": "2cb48f26130919f92f30650bdcc30e6f4ebe45ac", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/4524c98a826776c9a32d6ce06932563cb10f3995", + "reference": "4524c98a826776c9a32d6ce06932563cb10f3995", "shasum": "" }, "require": { @@ -2684,9 +2684,9 @@ ], "support": { "issues": "https://github.com/paragonie/sodium_compat/issues", - "source": "https://github.com/paragonie/sodium_compat/tree/v1.24.0" + "source": "https://github.com/paragonie/sodium_compat/tree/v1.24.2" }, - "time": "2025-12-30T16:16:35+00:00" + "time": "2026-08-18T23:44:07+00:00" }, { "name": "php-debugbar/php-debugbar", @@ -9095,16 +9095,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.5", + "version": "3.13.6", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", - "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/4c378e1a528ea066890fc2397cbdd2f94eb2fc91", + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91", "shasum": "" }, "require": { @@ -9170,7 +9170,7 @@ "type": "thanks_dev" } ], - "time": "2025-11-04T16:30:35+00:00" + "time": "2026-08-06T00:17:32+00:00" }, { "name": "symfony/event-dispatcher", @@ -9751,9 +9751,9 @@ "ext-gd": "*", "ext-dom": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.3.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/libraries/src/Form/Field/ComponentlayoutField.php b/libraries/src/Form/Field/ComponentlayoutField.php index df08bdb1cb587..e85d3556f3874 100644 --- a/libraries/src/Form/Field/ComponentlayoutField.php +++ b/libraries/src/Form/Field/ComponentlayoutField.php @@ -212,24 +212,52 @@ protected function getInput() foreach ($files as $file) { // Add an option to the template group $value = basename($file, '.php'); - $text = $value; - $path = str_replace('.php', '.xml', $file); - if (is_file($path) && $xml = simplexml_load_file($path)) { - $layout = $xml->xpath('//layout[1]'); - - if (isset($layout[0])) { - $attributes = $layout[0]; - - if (isset($attributes['option']) && (string) $attributes['option'] !== '') { - $text = Text::_((string)$attributes['option']); - } elseif (isset($attributes['title']) && (string) $attributes['title'] !== '') { - $text = Text::_((string)$attributes['title']); + // If template has a language item for the layout, use it. + $text = $lang + ->hasKey( + $key = strtoupper( + 'TPL_' + . $template->name + . '_' + . $extension + . '_' + . $view + . '_LAYOUT_' + . $value + ) + ) + ? Text::_($key) : ''; + + // Try to find the layout title in the XML file if no language item was found. + if ($text === '') { + $path = str_replace('.php', '.xml', $file); + + if (is_file($path) && $xml = simplexml_load_file($path)) { + $layout = $xml->xpath('//layout[1]'); + + if (isset($layout[0])) { + $attributes = $layout[0]; + + if (isset($attributes['option']) && (string) $attributes['option'] !== '') { + $text = Text::_((string) $attributes['option']); + } elseif (isset($attributes['title']) && (string) $attributes['title'] !== '') { + $text = Text::_((string) $attributes['title']); + } } } } - $groups[$template->name]['items'][] = HTMLHelper::_('select.option', $template->element . ':' . $view . ':' . $value, $text); + // Finally, if no title was found, use the layout name. + if ($text === '') { + $text = $value; + } + + $groups[$template->name]['items'][] = HTMLHelper::_( + 'select.option', + $template->element . ':' . $value, + $text + ); } } } diff --git a/libraries/src/Helper/MediaHelper.php b/libraries/src/Helper/MediaHelper.php index 00f8893c29e68..5e1cb6b5d0b58 100644 --- a/libraries/src/Helper/MediaHelper.php +++ b/libraries/src/Helper/MediaHelper.php @@ -56,7 +56,7 @@ public static function isImage($fileName) { static $imageTypes = 'xcf|odg|gif|jpg|jpeg|png|bmp|webp|avif'; - return preg_match("/\.(?:$imageTypes)$/i", $fileName); + return (bool) preg_match("/\.(?:$imageTypes)$/i", $fileName); } /** diff --git a/libraries/src/Helper/TagsHelper.php b/libraries/src/Helper/TagsHelper.php index d461b60ddf266..651f3af4b558d 100644 --- a/libraries/src/Helper/TagsHelper.php +++ b/libraries/src/Helper/TagsHelper.php @@ -910,16 +910,17 @@ public function postStore(TableInterface $table, array $newTags = [], bool $repl } $query = $db->createQuery() - ->select($db->quoteName('ucm_id')) - ->from($db->quoteName('#__ucm_base')) + ->select($db->quoteName('core_content_id')) + ->from($db->quoteName('#__ucm_content')) + ->where( [ - $db->quoteName('ucm_item_id') . ' = :itemId', - $db->quoteName('ucm_type_id') . ' = :typeId', + $db->quoteName('core_content_item_id') . ' = :itemId', + $db->quoteName('core_type_alias') . ' = :typeAlias', ] ) ->bind(':itemId', $ucmData['common']['core_content_item_id'], ParameterType::INTEGER) - ->bind(':typeId', $ucmData['common']['core_type_id'], ParameterType::INTEGER); + ->bind(':typeAlias', $ucmData['common']['core_type_alias']); $db->setQuery($query); $primaryId = $db->loadResult(); diff --git a/media_source/com_media/src/store/mutations.es6.js b/media_source/com_media/src/store/mutations.es6.js index e81deedf1d753..7b97a1eecfb24 100644 --- a/media_source/com_media/src/store/mutations.es6.js +++ b/media_source/com_media/src/store/mutations.es6.js @@ -279,7 +279,10 @@ export default { * @param payload the item */ [types.SELECT_BROWSER_ITEM]: (state, payload) => { - state.selectedItems.push(payload); + const isSelected = state.selectedItems.some((item) => item.path === payload.path); + if (!isSelected) { + state.selectedItems.push(payload); + } }, /** diff --git a/modules/mod_articles_news/src/Helper/ArticlesNewsHelper.php b/modules/mod_articles_news/src/Helper/ArticlesNewsHelper.php index 143a09f6b860c..7b1f68110ad73 100644 --- a/modules/mod_articles_news/src/Helper/ArticlesNewsHelper.php +++ b/modules/mod_articles_news/src/Helper/ArticlesNewsHelper.php @@ -169,7 +169,7 @@ public function getArticles(Registry $params, SiteApplication $app) $item->imageSrc = htmlspecialchars($images->image_fulltext, ENT_COMPAT, 'UTF-8'); $item->imageAlt = htmlspecialchars($images->image_fulltext_alt, ENT_COMPAT, 'UTF-8'); - if ($images->image_intro_caption) { + if ($images->image_fulltext_caption) { $item->imageCaption = htmlspecialchars($images->image_fulltext_caption, ENT_COMPAT, 'UTF-8'); } } diff --git a/plugins/captcha/powcaptcha/src/Extension/POWCaptcha.php b/plugins/captcha/powcaptcha/src/Extension/POWCaptcha.php index 689eac9d6207c..af1a856d92a6b 100644 --- a/plugins/captcha/powcaptcha/src/Extension/POWCaptcha.php +++ b/plugins/captcha/powcaptcha/src/Extension/POWCaptcha.php @@ -13,7 +13,6 @@ use Joomla\CMS\Event\Captcha\CaptchaSetupEvent; use Joomla\CMS\Event\Plugin\AjaxEvent; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\CMS\Session\Session; use Joomla\Event\SubscriberInterface; use Joomla\Plugin\Captcha\POWCaptcha\Provider\POWCaptchaProvider; @@ -64,13 +63,6 @@ public function setupCaptcha(CaptchaSetupEvent $event) */ public function handleAjaxRequest(AjaxEvent $event) { - // CRSF Token check - if (!Session::checkToken('get')) { - $event->updateEventResult(json_encode([])); - - return; - } - $event->updateEventResult( json_encode( $this->getProvider()->getChallenge() diff --git a/plugins/captcha/powcaptcha/src/Provider/POWCaptchaProvider.php b/plugins/captcha/powcaptcha/src/Provider/POWCaptchaProvider.php index caf7d774e7a70..37b6209f078ca 100644 --- a/plugins/captcha/powcaptcha/src/Provider/POWCaptchaProvider.php +++ b/plugins/captcha/powcaptcha/src/Provider/POWCaptchaProvider.php @@ -20,7 +20,6 @@ use Joomla\CMS\Form\FormField; use Joomla\CMS\Language\Text; use Joomla\CMS\Router\Route; -use Joomla\CMS\Session\Session; use Joomla\Registry\Registry; use Joomla\Utilities\ArrayHelper; @@ -95,10 +94,7 @@ public function display(string $name = '', array $attributes = []): string 'UTF-8' ), 'challengeurl' => Route::_( - \sprintf( - "index.php?option=com_ajax&plugin=powcaptcha&group=captcha&format=raw&%s=1", - Session::getFormToken() - ), + 'index.php?option=com_ajax&plugin=powcaptcha&group=captcha&format=raw', false, false, true @@ -114,7 +110,7 @@ public function display(string $name = '', array $attributes = []): string /** * Verify the users answer * - * @param null|string $code Answer provided by user. Not needed for the Recaptcha implementation + * @param null|string $code Answer provided by user. * * @return bool True if the answer is correct, false otherwise * @@ -126,6 +122,10 @@ public function checkAnswer(?string $code = null): bool return false; } + if (empty($code)) { + return false; + } + // Before we verify the actual solution, let's first verify our challenge key $decoded = base64_decode($code, true); diff --git a/tests/System/integration/site/components/com_content/Category.cy.js b/tests/System/integration/site/components/com_content/Category.cy.js index 5ae782476b634..021f90bbad1d8 100644 --- a/tests/System/integration/site/components/com_content/Category.cy.js +++ b/tests/System/integration/site/components/com_content/Category.cy.js @@ -1,4 +1,10 @@ describe('Test in frontend that the content category view', () => { + afterEach(() => { + cy.db_updateExtensionParameter('filter_field', 'hide', 'com_content'); + cy.db_updateExtensionParameter('list_show_author', '1', 'com_content'); + cy.db_updateExtensionParameter('list_show_hits', '1', 'com_content'); + }); + ['default', 'blog'].forEach((layout) => { it(`can display a list of articles in the ${layout} layout in a menu item`, () => { cy.db_createArticle({ title: 'article 1' }) @@ -38,6 +44,321 @@ describe('Test in frontend that the content category view', () => { }); }); + it('can use the title filter to narrow the article list', () => { + cy.db_createArticle({ title: 'article abc 1' }) + .then((article) => cy.db_createArticle({ title: 'article def 2', catid: article.catid })) + .then((article) => cy.db_createArticle({ title: 'article abc 3', catid: article.catid })) + .then((article) => cy.db_createArticle({ title: 'article xyz 4', catid: article.catid })) + .then((article) => cy.db_createArticle({ title: 'article def 5', catid: article.catid })) + .then((article) => cy.db_createMenuItem({ + title: 'automated test', + alias: 'automated-test', + path: 'automated-test', + link: `index.php?option=com_content&view=category&id=${article.catid}`, + params: { + filter_field: 'title', + list_show_author: '0', + }, + })) + .then(() => { + cy.visit('/'); + cy.get('a:contains(automated test)').click(); + cy.get('main div.com-content-category.category-list').within(() => { + cy.get('table.com-content-category__table thead th').should('have.length', 2); + cy.get('table.com-content-category__table thead th').eq(0).should('contain.text', 'Title'); + cy.get('table.com-content-category__table thead th').eq(1).should('contain.text', 'Hits'); + cy.get('table.com-content-category__table tbody th a') + .should('contain.text', 'article abc 1') + .should('contain.text', 'article def 2') + .should('contain.text', 'article abc 3') + .should('contain.text', 'article xyz 4') + .should('contain.text', 'article def 5'); + + cy.get('#filter-search').should('have.attr', 'placeholder', 'Title Filter'); + cy.get('#filter-search').clear().type('abc'); + cy.get('.com-content__filter button[type="submit"]').click(); + + cy.get('table.com-content-category__table tbody th a') + .should('contain.text', 'article abc 1') + .should('not.contain.text', 'article def 2') + .should('contain.text', 'article abc 3') + .should('not.contain.text', 'article xyz 4') + .should('not.contain.text', 'article def 5'); + + cy.get('#filter-search').clear().type('xyz'); + cy.get('.com-content__filter button[type="submit"]').click(); + + cy.get('table.com-content-category__table tbody th a') + .should('not.contain.text', 'article abc 1') + .should('not.contain.text', 'article def 2') + .should('not.contain.text', 'article abc 3') + .should('contain.text', 'article xyz 4') + .should('not.contain.text', 'article def 5'); + + cy.get('#filter-search').clear().type('def'); + cy.get('.com-content__filter button[type="submit"]').click(); + + cy.get('table.com-content-category__table tbody th a') + .should('not.contain.text', 'article abc 1') + .should('contain.text', 'article def 2') + .should('not.contain.text', 'article abc 3') + .should('not.contain.text', 'article xyz 4') + .should('contain.text', 'article def 5'); + + cy.get('.com-content__filter button[type="reset"]').click(); + cy.get('#filter-search').should('have.value', ''); + + cy.get('table.com-content-category__table tbody th a') + .should('contain.text', 'article abc 1') + .should('contain.text', 'article def 2') + .should('contain.text', 'article abc 3') + .should('contain.text', 'article xyz 4') + .should('contain.text', 'article def 5'); + }); + }); + }); + + it('can use the author filter to narrow the article list', () => { + cy.db_createArticle({ title: 'article 1', created_by_alias: 'James' }) + .then((article) => cy.db_createArticle({ title: 'article 2', catid: article.catid, created_by_alias: 'William' })) + .then((article) => cy.db_createArticle({ title: 'article 3', catid: article.catid, created_by_alias: 'Emma' })) + .then((article) => { + cy.db_createUser({ name: 'Emma' }).then((id) => { + cy.db_createArticle({ title: 'article 4', catid: article.catid, created_by: id }); + cy.db_createArticle({ title: 'article 5', catid: article.catid, created_by: id, created_by_alias: 'Charlotte' }); + }); + }) + .then((article) => cy.db_createMenuItem({ + title: 'automated test', + alias: 'automated-test', + path: 'automated-test', + link: `index.php?option=com_content&view=category&id=${article.catid}`, + params: { + filter_field: '', + list_show_date: 'published', + }, + })) + .then(() => { + cy.db_updateExtensionParameter('filter_field', 'author', 'com_content'); + cy.db_updateExtensionParameter('list_show_hits', '0', 'com_content'); + cy.visit('/'); + cy.get('a:contains(automated test)').click(); + cy.get('main div.com-content-category.category-list').within(() => { + cy.get('table.com-content-category__table thead th').should('have.length', 3); + cy.get('table.com-content-category__table thead th').eq(0).should('contain.text', 'Title'); + cy.get('table.com-content-category__table thead th').eq(1).should('contain.text', 'Published Date'); + cy.get('table.com-content-category__table thead th').eq(2).should('contain.text', 'Author'); + cy.get('table.com-content-category__table tbody th a') + .should('contain.text', 'article 1') + .should('contain.text', 'article 2') + .should('contain.text', 'article 3') + .should('contain.text', 'article 4') + .should('contain.text', 'article 5'); + + cy.get('#filter-search').should('have.attr', 'placeholder', 'Author Filter'); + cy.get('#filter-search').clear().type('william'); + cy.get('.com-content__filter button[type="submit"]').click(); + + cy.get('table.com-content-category__table tbody th a') + .should('not.contain.text', 'article 1') + .should('contain.text', 'article 2') + .should('not.contain.text', 'article 3') + .should('not.contain.text', 'article 4') + .should('not.contain.text', 'article 5'); + + cy.get('#filter-search').clear().type('James'); + cy.get('.com-content__filter button[type="submit"]').click(); + + cy.get('table.com-content-category__table tbody th a') + .should('contain.text', 'article 1') + .should('not.contain.text', 'article 2') + .should('not.contain.text', 'article 3') + .should('not.contain.text', 'article 4') + .should('not.contain.text', 'article 5'); + + cy.get('#filter-search').clear().type('Charlotte'); + cy.get('.com-content__filter button[type="submit"]').click(); + + cy.get('table.com-content-category__table tbody th a') + .should('not.contain.text', 'article 1') + .should('not.contain.text', 'article 2') + .should('not.contain.text', 'article 3') + .should('not.contain.text', 'article 4') + .should('contain.text', 'article 5'); + + cy.get('#filter-search').clear().type('emma'); + cy.get('.com-content__filter button[type="submit"]').click(); + + cy.get('table.com-content-category__table tbody th a') + .should('not.contain.text', 'article 1') + .should('not.contain.text', 'article 2') + .should('contain.text', 'article 3') + .should('contain.text', 'article 4') + .should('not.contain.text', 'article 5'); + + cy.get('.com-content__filter button[type="reset"]').click(); + cy.get('#filter-search').should('have.value', ''); + + cy.get('table.com-content-category__table tbody th a') + .should('contain.text', 'article 1') + .should('contain.text', 'article 2') + .should('contain.text', 'article 3') + .should('contain.text', 'article 4') + .should('contain.text', 'article 5'); + }); + }); + }); + + it('can use the tag filter to narrow the article list', () => { + cy.db_createArticle({ title: 'article 1' }) + .then((article) => { + cy.db_createTag({ title: 'tag1' }).then((id) => { + cy.db_createArticle({ title: 'article 2', catid: article.catid, tags: [id] }); + cy.db_createArticle({ title: 'article 3', catid: article.catid, tags: [id] }); + }); + }) + .then((article) => { + cy.db_createTag({ title: 'tag2' }).then((id) => { + cy.db_createArticle({ title: 'article 4', catid: article.catid, tags: [id] }); + cy.db_createArticle({ title: 'article 5', catid: article.catid, tags: [id] }); + }); + }) + .then((article) => cy.db_createMenuItem({ + title: 'automated test', + alias: 'automated-test', + path: 'automated-test', + link: `index.php?option=com_content&view=category&id=${article.catid}`, + params: { + filter_field: 'tag', + }, + })) + .then(() => { + cy.visit('/'); + cy.get('a:contains(automated test)').click(); + cy.get('main div.com-content-category.category-list').within(() => { + cy.get('table.com-content-category__table thead th').should('have.length', 3); + cy.get('table.com-content-category__table thead th').eq(0).should('contain.text', 'Title'); + cy.get('table.com-content-category__table thead th').eq(1).should('contain.text', 'Author'); + cy.get('table.com-content-category__table thead th').eq(2).should('contain.text', 'Hits'); + cy.get('table.com-content-category__table tbody th a') + .should('contain.text', 'article 1') + .should('contain.text', 'article 2') + .should('contain.text', 'article 3') + .should('contain.text', 'article 4') + .should('contain.text', 'article 5'); + + cy.get('#filter-search').select('tag1'); + cy.get('.com-content__filter button[type="submit"]').click(); + + cy.get('table.com-content-category__table tbody th a') + .should('not.contain.text', 'article 1') + .should('contain.text', 'article 2') + .should('contain.text', 'article 3') + .should('not.contain.text', 'article 4') + .should('not.contain.text', 'article 5'); + + cy.get('#filter-search').select('tag2'); + cy.get('.com-content__filter button[type="submit"]').click(); + + cy.get('table.com-content-category__table tbody th a') + .should('not.contain.text', 'article 1') + .should('not.contain.text', 'article 2') + .should('not.contain.text', 'article 3') + .should('contain.text', 'article 4') + .should('contain.text', 'article 5'); + + cy.get('.com-content__filter button[type="reset"]').click(); + cy.get('#filter-search').should('have.value', ''); + + cy.get('table.com-content-category__table tbody th a') + .should('contain.text', 'article 1') + .should('contain.text', 'article 2') + .should('contain.text', 'article 3') + .should('contain.text', 'article 4') + .should('contain.text', 'article 5'); + }); + }); + }); + + it('can use the month filter to narrow the article list', () => { + cy.db_createArticle({ title: 'article 1', publish_up: '2026-08-17 20:00:00' }) + .then((article) => cy.db_createArticle({ title: 'article 2', catid: article.catid, publish_up: '2026-08-28 20:00:00' })) + .then((article) => cy.db_createArticle({ title: 'article 3', catid: article.catid, publish_up: '2026-05-20 20:00:00' })) + .then((article) => cy.db_createArticle({ title: 'article 4', catid: article.catid, publish_up: '2026-05-25 20:00:00' })) + .then((article) => cy.db_createArticle({ title: 'article 5', catid: article.catid, publish_up: '2025-08-17 20:00:00' })) + .then((article) => cy.db_createMenuItem({ + title: 'automated test', + alias: 'automated-test', + path: 'automated-test', + link: `index.php?option=com_content&view=category&id=${article.catid}`, + params: { + filter_field: 'month', + list_show_date: 'modified', + list_show_hits: '0', + }, + })) + .then(() => { + cy.db_updateExtensionParameter('filter_field', 'hits', 'com_content'); + cy.db_updateExtensionParameter('list_show_author', '0', 'com_content'); + cy.visit('/'); + cy.get('a:contains(automated test)').click(); + cy.get('main div.com-content-category.category-list').within(() => { + cy.get('table.com-content-category__table thead th').should('have.length', 2); + cy.get('table.com-content-category__table thead th').eq(0).should('contain.text', 'Title'); + cy.get('table.com-content-category__table thead th').eq(1).should('contain.text', 'Modified Date'); + cy.get('table.com-content-category__table tbody th a') + .should('contain.text', 'article 1') + .should('contain.text', 'article 2') + .should('contain.text', 'article 3') + .should('contain.text', 'article 4') + .should('contain.text', 'article 5'); + + cy.get('#filter-search').select('2026-08-01'); + cy.get('#filter-search option:selected').should('have.text', 'August 2026 [2]'); + cy.get('.com-content__filter button[type="submit"]').click(); + + cy.get('table.com-content-category__table tbody th a') + .should('contain.text', 'article 1') + .should('contain.text', 'article 2') + .should('not.contain.text', 'article 3') + .should('not.contain.text', 'article 4') + .should('not.contain.text', 'article 5'); + + cy.get('#filter-search').select('2025-08-01'); + cy.get('#filter-search option:selected').should('have.text', 'August 2025 [1]'); + cy.get('.com-content__filter button[type="submit"]').click(); + + cy.get('table.com-content-category__table tbody th a') + .should('not.contain.text', 'article 1') + .should('not.contain.text', 'article 2') + .should('not.contain.text', 'article 3') + .should('not.contain.text', 'article 4') + .should('contain.text', 'article 5'); + + cy.get('#filter-search').select('2026-05-01'); + cy.get('#filter-search option:selected').should('have.text', 'May 2026 [2]'); + cy.get('.com-content__filter button[type="submit"]').click(); + + cy.get('table.com-content-category__table tbody th a') + .should('not.contain.text', 'article 1') + .should('not.contain.text', 'article 2') + .should('contain.text', 'article 3') + .should('contain.text', 'article 4') + .should('not.contain.text', 'article 5'); + + cy.get('.com-content__filter button[type="reset"]').click(); + cy.get('#filter-search').should('have.value', ''); + + cy.get('table.com-content-category__table tbody th a') + .should('contain.text', 'article 1') + .should('contain.text', 'article 2') + .should('contain.text', 'article 3') + .should('contain.text', 'article 4') + .should('contain.text', 'article 5'); + }); + }); + }); + it('can open the article form in the default layout', () => { cy.db_createArticle({ title: 'article 1' }) .then((article) => cy.db_createMenuItem({ diff --git a/tests/System/plugins/db.mjs b/tests/System/plugins/db.mjs index e57200820ebf1..d77a16553a20a 100644 --- a/tests/System/plugins/db.mjs +++ b/tests/System/plugins/db.mjs @@ -170,6 +170,7 @@ function deleteInsertedItems(config) { if (item.table === `${config.expose.db_prefix}content`) { promises.push(queryTestDB(`DELETE FROM #__content_frontpage WHERE content_id IN (${item.rows.join(',')})`, config)); + promises.push(queryTestDB(`DELETE FROM #__contentitem_tag_map WHERE content_item_id IN (${item.rows.join(',')}) AND type_alias = 'com_content.article'`, config)); promises.push(queryTestDB(`DELETE FROM #__workflow_associations WHERE item_id IN (${item.rows.join(',')}) AND extension = 'com_content.article'`, config)); } diff --git a/tests/System/support/commands/db.mjs b/tests/System/support/commands/db.mjs index 0cac699a380f3..f05be25bc2539 100644 --- a/tests/System/support/commands/db.mjs +++ b/tests/System/support/commands/db.mjs @@ -113,6 +113,9 @@ Cypress.Commands.add('db_createArticle', (articleData) => { metadata: '', }; + const tags = articleData.tags ?? []; + delete articleData.tags; + const article = { ...defaultArticleOptions, ...articleData }; return getDefaultCategoryId('com_content') @@ -128,6 +131,9 @@ Cypress.Commands.add('db_createArticle', (articleData) => { if (article.featured === 1) { await cy.task('queryDB', `INSERT INTO #__content_frontpage (content_id, ordering) VALUES ('${article.id}', '1')`); } + tags.forEach(async (tag) => { + await cy.task('queryDB', `INSERT INTO #__contentitem_tag_map (type_alias, core_content_id, content_item_id, tag_id, tag_date, type_id) VALUES ('com_content.article', '0', '${article.id}', '${tag}', '2026-08-29 11:00:00', '1')`); + }); await cy.task('queryDB', `INSERT INTO #__workflow_associations (item_id, stage_id, extension) VALUES (${article.id}, 1, 'com_content.article')`); return article; @@ -504,6 +510,11 @@ Cypress.Commands.add('db_createMenuItem', (menuItemData) => { defaultMenuItemOptions.rgt = myrgt[0].rgt + 1; const menuItem = { ...defaultMenuItemOptions, ...menuItemData }; + ['params'].forEach((key) => { + if (typeof menuItem[key] === 'object') { + menuItem[key] = JSON.stringify(menuItem[key]); + } + }); // Extract the component from the link const component = (new URLSearchParams(menuItem.link.replace('index.php', ''))).get('option'); return cy.task('queryDB', `SELECT extension_id FROM #__extensions WHERE name = '${component}'`).then((id) => {