diff --git a/README.txt b/README.txt index 56fd3c8c79f77..29d8e4e3154a3 100644 --- a/README.txt +++ b/README.txt @@ -44,7 +44,7 @@ The Joomla! 6.2 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_admin/script.php b/administrator/components/com_admin/script.php index a15167112a580..abf7351fc2611 100644 --- a/administrator/components/com_admin/script.php +++ b/administrator/components/com_admin/script.php @@ -454,6 +454,7 @@ public function deleteUnexistingFiles($dryRun = false, $suppressOutput = false) '/administrator/components/com_admin/sql/updates/mysql/5.4.0-2025-04-23.sql', '/administrator/components/com_admin/sql/updates/mysql/5.4.0-2025-05-10.sql', '/administrator/components/com_admin/sql/updates/mysql/5.4.0-2025-08-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/5.4.9-2026-08-11.sql', '/administrator/components/com_admin/sql/updates/postgresql/5.0.0-2023-03-11.sql', '/administrator/components/com_admin/sql/updates/postgresql/5.0.0-2023-03-17.sql', '/administrator/components/com_admin/sql/updates/postgresql/5.0.0-2023-07-12.sql', @@ -493,6 +494,7 @@ public function deleteUnexistingFiles($dryRun = false, $suppressOutput = false) '/administrator/components/com_admin/sql/updates/postgresql/5.4.0-2025-05-10.sql', '/administrator/components/com_admin/sql/updates/postgresql/5.4.0-2025-08-02.sql', '/administrator/components/com_admin/sql/updates/postgresql/5.4.0-2025-10-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/5.4.9-2026-08-11.sql', '/administrator/components/com_content/forms/filter_featured.xml', '/administrator/components/com_content/tmpl/featured/default.php', '/administrator/components/com_content/tmpl/featured/default.xml', 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/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/src/Model/SysinfoModel.php b/administrator/components/com_admin/src/Model/SysinfoModel.php index 9ebd648c6176a..4d389f461da6d 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', 'compat6'), '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_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_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_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/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..19716165e25a2 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'); 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/language/en-GB/com_fields.ini b/administrator/language/en-GB/com_fields.ini index 210d3ede06963..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)" 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/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_popular/tmpl/default.php b/administrator/modules/mod_popular/tmpl/default.php index bbec31bf7f940..13fbfbb9e1a34 100644 --- a/administrator/modules/mod_popular/tmpl/default.php +++ b/administrator/modules/mod_popular/tmpl/default.php @@ -55,7 +55,7 @@ hits; ?> - publish_up, Text::_('DATE_FORMAT_LC4')); ?> + publish_up) ? $item->created : $item->publish_up, Text::_('DATE_FORMAT_LC4')); ?> 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_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/components/com_content/tmpl/category/default_articles.php b/components/com_content/tmpl/category/default_articles.php index 2088fc2e2c84c..cd5d6abaf778d 100644 --- a/components/com_content/tmpl/category/default_articles.php +++ b/components/com_content/tmpl/category/default_articles.php @@ -93,18 +93,16 @@ - - params->get('filter_field') !== 'tag' && $this->params->get('filter_field') !== 'month') : ?> - - + diff --git a/components/com_tags/src/Service/Router.php b/components/com_tags/src/Service/Router.php index 45af977b8af2c..e9af234340ae4 100644 --- a/components/com_tags/src/Service/Router.php +++ b/components/com_tags/src/Service/Router.php @@ -250,6 +250,18 @@ public function build(&$query) unset($query['id']); } + // Strip 'types' from the URL if they match the menu item's own configured types + if (isset($query['types'], $menuItem->query['types'])) { + $queryTypes = ArrayHelper::toInteger((array) $query['types']); + $menuTypes = ArrayHelper::toInteger((array) $menuItem->query['types']); + sort($queryTypes); + sort($menuTypes); + + if ($queryTypes === $menuTypes) { + unset($query['types']); + } + } + unset($query['view']); } else { if (isset($query['view'])) { diff --git a/installation/language/af-ZA/langmetadata.xml b/installation/language/af-ZA/langmetadata.xml index 36f98a5efa56d..bd48b7702ce88 100644 --- a/installation/language/af-ZA/langmetadata.xml +++ b/installation/language/af-ZA/langmetadata.xml @@ -2,7 +2,7 @@ Afrikaans (Suid-Afrika) 6.1.3 - 2026-07 + 2026-08 Afrikaans Translation Team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/ar-AA/langmetadata.xml b/installation/language/ar-AA/langmetadata.xml index 30383c15681b5..45d5169dc1ab6 100644 --- a/installation/language/ar-AA/langmetadata.xml +++ b/installation/language/ar-AA/langmetadata.xml @@ -2,7 +2,7 @@ Arabic (اللغة العربية) 6.1.3 - 2026-07 + 2026-08 Joomla! Project (Arabic Translation Team) (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/be-BY/langmetadata.xml b/installation/language/be-BY/langmetadata.xml index c77631c152f74..3d9b80d98f2e4 100644 --- a/installation/language/be-BY/langmetadata.xml +++ b/installation/language/be-BY/langmetadata.xml @@ -2,7 +2,7 @@ Belarusian (be-BY) 6.1.3 - 2026-07 + 2026-08 Joomla Belarus Community (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/bg-BG/langmetadata.xml b/installation/language/bg-BG/langmetadata.xml index cb5791a64ace6..13f5922794afc 100644 --- a/installation/language/bg-BG/langmetadata.xml +++ b/installation/language/bg-BG/langmetadata.xml @@ -2,7 +2,7 @@ Bulgarian (bg-BG) 6.1.3 - 2026-07 + 2026-08 Joomla! Bulgaria (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/ca-ES/langmetadata.xml b/installation/language/ca-ES/langmetadata.xml index 87131f805f4c4..7af8c1c18637d 100644 --- a/installation/language/ca-ES/langmetadata.xml +++ b/installation/language/ca-ES/langmetadata.xml @@ -2,7 +2,7 @@ Catalan (ca-ES) 6.1.3 - 2026-07 + 2026-08 Catalan [ca-ES] Translation Team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/cs-CZ/langmetadata.xml b/installation/language/cs-CZ/langmetadata.xml index 7f3ac9f8cd6a7..fc83aa1ba8e39 100644 --- a/installation/language/cs-CZ/langmetadata.xml +++ b/installation/language/cs-CZ/langmetadata.xml @@ -2,7 +2,7 @@ Czech (Čeština) 6.1.3 - 2026-07 + 2026-08 Czech Translation Team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/cy-GB/langmetadata.xml b/installation/language/cy-GB/langmetadata.xml index eb10056410377..eb9c80689d32c 100644 --- a/installation/language/cy-GB/langmetadata.xml +++ b/installation/language/cy-GB/langmetadata.xml @@ -2,7 +2,7 @@ Welsh (United Kingdom) 6.1.3 - 2026-07 + 2026-08 Joomla! Project - Welsh Translation Team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/da-DK/langmetadata.xml b/installation/language/da-DK/langmetadata.xml index 988c634001991..8ea5254839950 100644 --- a/installation/language/da-DK/langmetadata.xml +++ b/installation/language/da-DK/langmetadata.xml @@ -2,7 +2,7 @@ Danish (Danmark) 6.1.3 - 2026-07 + 2026-08 Danish Translation Team (Transl.: Ronny Buelund) (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/el-GR/langmetadata.xml b/installation/language/el-GR/langmetadata.xml index c59cd69b3e020..d3e942b999778 100644 --- a/installation/language/el-GR/langmetadata.xml +++ b/installation/language/el-GR/langmetadata.xml @@ -2,7 +2,7 @@ Greek (el-GR) 6.1.3 - 2026-07 + 2026-08 Ομάδα Μετάφρασης: joomla. gr (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/en-AU/langmetadata.xml b/installation/language/en-AU/langmetadata.xml index 65d398297cc83..23d9c375ab853 100644 --- a/installation/language/en-AU/langmetadata.xml +++ b/installation/language/en-AU/langmetadata.xml @@ -2,7 +2,7 @@ English (Australia) 6.1.3 - 2026-07 + 2026-08 Joomla! Project (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/en-CA/langmetadata.xml b/installation/language/en-CA/langmetadata.xml index 24345eb98f59e..780a6702b1154 100644 --- a/installation/language/en-CA/langmetadata.xml +++ b/installation/language/en-CA/langmetadata.xml @@ -2,7 +2,7 @@ English (Canada) 6.1.3 - 2026-07 + 2026-08 Joomla! Project (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/en-NZ/langmetadata.xml b/installation/language/en-NZ/langmetadata.xml index 0b3205462e423..af48d12b539db 100644 --- a/installation/language/en-NZ/langmetadata.xml +++ b/installation/language/en-NZ/langmetadata.xml @@ -2,7 +2,7 @@ English (New Zealand) 6.1.3 - 2026-07 + 2026-08 Joomla! Project (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/en-US/langmetadata.xml b/installation/language/en-US/langmetadata.xml index 6d93257a79f93..7ad0fbae047a9 100644 --- a/installation/language/en-US/langmetadata.xml +++ b/installation/language/en-US/langmetadata.xml @@ -2,7 +2,7 @@ English (United States) 6.1.3 - 2026-07 + 2026-08 Joomla! Project (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/es-ES/langmetadata.xml b/installation/language/es-ES/langmetadata.xml index db570206e0696..71c143dea02bb 100644 --- a/installation/language/es-ES/langmetadata.xml +++ b/installation/language/es-ES/langmetadata.xml @@ -2,7 +2,7 @@ Spanish (es-ES) 6.1.3 - 2026-07 + 2026-08 Spanish [es-ES] Translation Team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/et-EE/langmetadata.xml b/installation/language/et-EE/langmetadata.xml index dbb787217aeee..89e92c114650f 100644 --- a/installation/language/et-EE/langmetadata.xml +++ b/installation/language/et-EE/langmetadata.xml @@ -2,7 +2,7 @@ Estonian 6.1.3 - 2026-07 + 2026-08 Joomla! Project (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/eu-ES/langmetadata.xml b/installation/language/eu-ES/langmetadata.xml index 2ac11f84df959..c0cc8e163d62e 100644 --- a/installation/language/eu-ES/langmetadata.xml +++ b/installation/language/eu-ES/langmetadata.xml @@ -2,7 +2,7 @@ Basque 6.1.3 - 2026-07 + 2026-08 Joomla! Basque Translation Team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/fa-AF/langmetadata.xml b/installation/language/fa-AF/langmetadata.xml index 5212e0e0338c9..c348327aa6529 100644 --- a/installation/language/fa-AF/langmetadata.xml +++ b/installation/language/fa-AF/langmetadata.xml @@ -2,7 +2,7 @@ فارسی (دری) 6.1.3 - 2026-07 + 2026-08 JoomlaPersian Translation Team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/fa-IR/langmetadata.xml b/installation/language/fa-IR/langmetadata.xml index 8b0fcef2f2295..268c9630a4fbb 100644 --- a/installation/language/fa-IR/langmetadata.xml +++ b/installation/language/fa-IR/langmetadata.xml @@ -2,7 +2,7 @@ Persian (fa-IR) 6.1.3 - 2026-07 + 2026-08 Persian Translation Team: joomlafarsi.com (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/fi-FI/langmetadata.xml b/installation/language/fi-FI/langmetadata.xml index 0f200a70b513d..20250ec7024c2 100644 --- a/installation/language/fi-FI/langmetadata.xml +++ b/installation/language/fi-FI/langmetadata.xml @@ -2,7 +2,7 @@ Finnish (Finland) 6.1.3 - 2026-07 + 2026-08 Finnish translation team: Joomla.fi (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/fr-CA/langmetadata.xml b/installation/language/fr-CA/langmetadata.xml index 9d9a0fb45340f..c8858ea7d543f 100644 --- a/installation/language/fr-CA/langmetadata.xml +++ b/installation/language/fr-CA/langmetadata.xml @@ -2,7 +2,7 @@ French (Canada) 6.1.3 - 2026-07 + 2026-08 Joomla! Project - Canadian translation team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/fr-FR/langmetadata.xml b/installation/language/fr-FR/langmetadata.xml index 7bf8bdbeed298..c87f8398f3a37 100644 --- a/installation/language/fr-FR/langmetadata.xml +++ b/installation/language/fr-FR/langmetadata.xml @@ -2,7 +2,7 @@ French (fr-FR) 6.1.3 - 2026-07 + 2026-08 Joomla! Project - French translation team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/he-IL/langmetadata.xml b/installation/language/he-IL/langmetadata.xml index a9bc1ce45039b..6dd6ee5a9514c 100644 --- a/installation/language/he-IL/langmetadata.xml +++ b/installation/language/he-IL/langmetadata.xml @@ -2,7 +2,7 @@ Hebrew (Israel) 6.1.3 - 2026-07 + 2026-08 פרוייקט ג'ומלה (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/hr-HR/langmetadata.xml b/installation/language/hr-HR/langmetadata.xml index 61183bda0c70e..a5ae2b5a2b768 100644 --- a/installation/language/hr-HR/langmetadata.xml +++ b/installation/language/hr-HR/langmetadata.xml @@ -2,7 +2,7 @@ Hrvatski (Hrvatska) 6.1.3 - 2026-07 + 2026-08 Joomla! Hrvatska team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/hu-HU/langmetadata.xml b/installation/language/hu-HU/langmetadata.xml index 41ebb231f2b64..af77edb512304 100644 --- a/installation/language/hu-HU/langmetadata.xml +++ b/installation/language/hu-HU/langmetadata.xml @@ -2,7 +2,7 @@ Hungarian (Magyar) 6.1.3 - 2026-07 + 2026-08 Joomla! Magyarország (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/id-ID/langmetadata.xml b/installation/language/id-ID/langmetadata.xml index 53c4853863e1e..390204275a0c4 100644 --- a/installation/language/id-ID/langmetadata.xml +++ b/installation/language/id-ID/langmetadata.xml @@ -2,7 +2,7 @@ Bahasa Indonesia (id-ID) 6.1.3 - 2026-07 + 2026-08 Joomla! Indonesia (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/it-IT/langmetadata.xml b/installation/language/it-IT/langmetadata.xml index d856ca3dc551c..6aa6d6df893df 100644 --- a/installation/language/it-IT/langmetadata.xml +++ b/installation/language/it-IT/langmetadata.xml @@ -2,7 +2,7 @@ Italiano (it-IT) 6.1.3 - 2026-07 + 2026-08 Joomla! Project (Italian Translation Team) (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/ja-JP/langmetadata.xml b/installation/language/ja-JP/langmetadata.xml index 076e33bca8370..c1120155931c8 100644 --- a/installation/language/ja-JP/langmetadata.xml +++ b/installation/language/ja-JP/langmetadata.xml @@ -2,7 +2,7 @@ Japanese (Japan) 6.1.3 - 2026-07 + 2026-08 Joomla!じゃぱん (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/ka-GE/langmetadata.xml b/installation/language/ka-GE/langmetadata.xml index 2699e37e0ec48..62d75a0545ac5 100644 --- a/installation/language/ka-GE/langmetadata.xml +++ b/installation/language/ka-GE/langmetadata.xml @@ -2,7 +2,7 @@ Georgian (Georgia) 6.1.3 - 2026-07 + 2026-08 Georgian Translation Team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/kk-KZ/langmetadata.xml b/installation/language/kk-KZ/langmetadata.xml index a1e454c59cb9c..666f03aa7a80c 100644 --- a/installation/language/kk-KZ/langmetadata.xml +++ b/installation/language/kk-KZ/langmetadata.xml @@ -2,7 +2,7 @@ Kazakh (Kazakhstan) 6.1.3 - 2026-07 + 2026-08 Sarvarov Akylkerey (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/ko-KR/langmetadata.xml b/installation/language/ko-KR/langmetadata.xml index 54141d821a8b9..420bcb7b6fd7e 100644 --- a/installation/language/ko-KR/langmetadata.xml +++ b/installation/language/ko-KR/langmetadata.xml @@ -2,7 +2,7 @@ Korean (ko-KR) 6.1.3 - 2026-07 + 2026-08 Joomla! 프로젝트 (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/lt-LT/langmetadata.xml b/installation/language/lt-LT/langmetadata.xml index 1676a65c27dad..e79f8b2410f80 100644 --- a/installation/language/lt-LT/langmetadata.xml +++ b/installation/language/lt-LT/langmetadata.xml @@ -2,7 +2,7 @@ Lietuvių (lt-LT) 6.1.3 - 2026-07 + 2026-08 Oskaras Jankauskas (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/lv-LV/langmetadata.xml b/installation/language/lv-LV/langmetadata.xml index abe63d34ae097..65e63879ce433 100644 --- a/installation/language/lv-LV/langmetadata.xml +++ b/installation/language/lv-LV/langmetadata.xml @@ -2,7 +2,7 @@ Latvian (Latvia) 6.1.3 - 2026-07 + 2026-08 Joomla! Projekts (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/mk-MK/langmetadata.xml b/installation/language/mk-MK/langmetadata.xml index 2e2960d0f1310..65a9c278e77d0 100644 --- a/installation/language/mk-MK/langmetadata.xml +++ b/installation/language/mk-MK/langmetadata.xml @@ -2,7 +2,7 @@ Macedonian (mk-MK) 6.1.3 - 2026-07 + 2026-08 Joomla! Project (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/nl-BE/langmetadata.xml b/installation/language/nl-BE/langmetadata.xml index 42d6481159e9c..0a347be60e18a 100644 --- a/installation/language/nl-BE/langmetadata.xml +++ b/installation/language/nl-BE/langmetadata.xml @@ -2,7 +2,7 @@ Dutch (Belgium) 6.1.3 - 2026-07 + 2026-08 Dutch (BE) translation team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/nl-NL/langmetadata.xml b/installation/language/nl-NL/langmetadata.xml index 0767980352266..2c050bf306642 100644 --- a/installation/language/nl-NL/langmetadata.xml +++ b/installation/language/nl-NL/langmetadata.xml @@ -2,7 +2,7 @@ Dutch (nl-NL) 6.1.3 - 2026-07 + 2026-08 Dutch Translation Team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/pt-BR/langmetadata.xml b/installation/language/pt-BR/langmetadata.xml index f3e09f2bdfe0c..ff4596d1dd642 100644 --- a/installation/language/pt-BR/langmetadata.xml +++ b/installation/language/pt-BR/langmetadata.xml @@ -2,7 +2,7 @@ Português do Brasil (pt-BR) 6.1.3 - 2026-07 + 2026-08 Projeto Joomla! (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/pt-PT/langmetadata.xml b/installation/language/pt-PT/langmetadata.xml index bbef891907435..9790d72a7812f 100644 --- a/installation/language/pt-PT/langmetadata.xml +++ b/installation/language/pt-PT/langmetadata.xml @@ -2,7 +2,7 @@ Português (Portugal) 6.1.3 - 2026-07 + 2026-08 Comunidade JoomlaPortugal (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/ro-RO/langmetadata.xml b/installation/language/ro-RO/langmetadata.xml index 1a8618e317e2d..698831587369d 100644 --- a/installation/language/ro-RO/langmetadata.xml +++ b/installation/language/ro-RO/langmetadata.xml @@ -2,7 +2,7 @@ Română (România) 6.1.3 - 2026-07 + 2026-08 Horia Negura - Quanta (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/sk-SK/langmetadata.xml b/installation/language/sk-SK/langmetadata.xml index 568b5d540aa95..2c9f5d54b2820 100644 --- a/installation/language/sk-SK/langmetadata.xml +++ b/installation/language/sk-SK/langmetadata.xml @@ -2,7 +2,7 @@ Slovak (Slovakia) 6.1.3 - 2026-07 + 2026-08 Slovak translation team : Peter Michnica (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/sl-SI/langmetadata.xml b/installation/language/sl-SI/langmetadata.xml index 655a6bfbb3781..5a7f8dc93b281 100644 --- a/installation/language/sl-SI/langmetadata.xml +++ b/installation/language/sl-SI/langmetadata.xml @@ -2,7 +2,7 @@ Slovenščina (Slovenija) 6.1.3 - 2026-07 + 2026-08 Slovenska prevajalska ekipa (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/sr-YU/langmetadata.xml b/installation/language/sr-YU/langmetadata.xml index b211217b23c06..ca9a68abaf4d3 100644 --- a/installation/language/sr-YU/langmetadata.xml +++ b/installation/language/sr-YU/langmetadata.xml @@ -2,7 +2,7 @@ Srpski (Republika Srbija) 6.1.3 - 2026-07 + 2026-08 Goran Nešić & Saša Matić (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/sv-SE/langmetadata.xml b/installation/language/sv-SE/langmetadata.xml index 61c6cc239203b..6d11b210a90a0 100644 --- a/installation/language/sv-SE/langmetadata.xml +++ b/installation/language/sv-SE/langmetadata.xml @@ -2,7 +2,7 @@ Swedish (Sweden) 6.1.3 - 2026-07 + 2026-08 Swedish Translation Team - SvenskJoomla (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/ta-IN/langmetadata.xml b/installation/language/ta-IN/langmetadata.xml index 00b644c75072e..6b922dc81320f 100644 --- a/installation/language/ta-IN/langmetadata.xml +++ b/installation/language/ta-IN/langmetadata.xml @@ -2,7 +2,7 @@ Tamil (India) 6.1.3 - 2026-07 + 2026-08 Ilagnayeru 'MIG' Manickam, Elango Samy Manim (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/th-TH/langmetadata.xml b/installation/language/th-TH/langmetadata.xml index 4d71a0e8bb1d8..7576429d907be 100644 --- a/installation/language/th-TH/langmetadata.xml +++ b/installation/language/th-TH/langmetadata.xml @@ -2,7 +2,7 @@ Thai (ภาษาไทย) 6.1.3 - 2026-07 + 2026-08 Thai Translation Team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/tr-TR/langmetadata.xml b/installation/language/tr-TR/langmetadata.xml index f35028c57f82a..3565b70422740 100644 --- a/installation/language/tr-TR/langmetadata.xml +++ b/installation/language/tr-TR/langmetadata.xml @@ -2,7 +2,7 @@ Turkish (Turkey) 6.1.3 - 2026-07 + 2026-08 Joomla! Türkiye (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/uk-UA/langmetadata.xml b/installation/language/uk-UA/langmetadata.xml index b194b75cce4c3..2c145f4977774 100644 --- a/installation/language/uk-UA/langmetadata.xml +++ b/installation/language/uk-UA/langmetadata.xml @@ -2,7 +2,7 @@ Ukrainian (uk-UA) 6.1.3 - 2026-07 + 2026-08 Denys Nosov (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/ur-PK/langmetadata.xml b/installation/language/ur-PK/langmetadata.xml index efd049d158560..02ec768222114 100644 --- a/installation/language/ur-PK/langmetadata.xml +++ b/installation/language/ur-PK/langmetadata.xml @@ -2,7 +2,7 @@ Urdu (ur-PK) 6.1.3 - 2026-07 + 2026-08 Urdu Translation Team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/vi-VN/langmetadata.xml b/installation/language/vi-VN/langmetadata.xml index c58c4272bbac1..70880b15de25c 100644 --- a/installation/language/vi-VN/langmetadata.xml +++ b/installation/language/vi-VN/langmetadata.xml @@ -2,7 +2,7 @@ Vietnamese (Vietnam) 6.1.3 - 2026-07 + 2026-08 Joomla! Project (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/zh-CN/langmetadata.xml b/installation/language/zh-CN/langmetadata.xml index 29d7ca0b1994f..008c0bf2b0b60 100644 --- a/installation/language/zh-CN/langmetadata.xml +++ b/installation/language/zh-CN/langmetadata.xml @@ -2,7 +2,7 @@ Chinese Simplified (China) 6.1.3 - 2026-07 + 2026-08 Joomla中文网 (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/installation/language/zh-TW/langmetadata.xml b/installation/language/zh-TW/langmetadata.xml index a7a97fb788f57..baed3b6f27ecb 100644 --- a/installation/language/zh-TW/langmetadata.xml +++ b/installation/language/zh-TW/langmetadata.xml @@ -2,7 +2,7 @@ 正體中文 6.1.3 - 2026-07 + 2026-08 正體中文 Translation Team (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/libraries/src/Application/ApiApplication.php b/libraries/src/Application/ApiApplication.php index c42e22de95420..cfc1b72b4d373 100644 --- a/libraries/src/Application/ApiApplication.php +++ b/libraries/src/Application/ApiApplication.php @@ -217,6 +217,7 @@ protected function respond($options = []) * * @return boolean True if the request origin is present in the whitelist. * + * @since 5.4.8 * @since 6.1.3 */ private function isOriginAllowed(string $requestOrigin, string $allowedOrigins): bool diff --git a/libraries/src/Encrypt/Totp.php b/libraries/src/Encrypt/Totp.php index 395e378ac8eef..7706ecd0b74bc 100644 --- a/libraries/src/Encrypt/Totp.php +++ b/libraries/src/Encrypt/Totp.php @@ -118,7 +118,7 @@ public function checkCode($secret, $code) $time = $this->getPeriod(); for ($i = -1; $i <= 1; $i++) { - if ($this->getCode($secret, ($time + $i) * $this->_timeStep) == $code) { + if (hash_equals($this->getCode($secret, ($time + $i) * $this->_timeStep), (string) $code)) { return true; } } diff --git a/libraries/src/Filter/InputFilter.php b/libraries/src/Filter/InputFilter.php index fcdf648c472c5..69e5ca86d1d4b 100644 --- a/libraries/src/Filter/InputFilter.php +++ b/libraries/src/Filter/InputFilter.php @@ -34,8 +34,8 @@ class InputFilter extends BaseInputFilter * @since 4.0.0 */ public const FORBIDDEN_FILE_EXTENSIONS = [ - 'asp', 'php', 'phps', 'pht', 'phtml', 'php3', 'php4', 'php5', 'php6', 'php7', 'php8', 'phar', - 'inc', 'pl', 'cgi', 'fcgi', 'java', 'jar', 'py', 'aspx', 'shtml', 'shtm', 'sht', 'stm', + 'asp', 'aspx', 'cgi', 'fcgi', 'inc', 'jar', 'java', 'phar', 'php', 'php3', 'php4', 'php5', 'php6', 'php7', + 'php8', 'php9', 'phps', 'pht', 'phtml', 'pl', 'py', 'sht', 'shtm', 'shtml', 'stm', ]; /** diff --git a/libraries/src/Form/Field/ComponentlayoutField.php b/libraries/src/Form/Field/ComponentlayoutField.php index fb5ec948c485e..df08bdb1cb587 100644 --- a/libraries/src/Form/Field/ComponentlayoutField.php +++ b/libraries/src/Form/Field/ComponentlayoutField.php @@ -212,21 +212,24 @@ protected function getInput() foreach ($files as $file) { // Add an option to the template group $value = basename($file, '.php'); - $text = $lang - ->hasKey( - $key = strtoupper( - 'TPL_' - . $template->name - . '_' - . $extension - . '_' - . $view - . '_LAYOUT_' - . $value - ) - ) - ? Text::_($key) : $value; - $groups[$template->name]['items'][] = HTMLHelper::_('select.option', $template->element . ':' . $value, $text); + $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']); + } + } + } + + $groups[$template->name]['items'][] = HTMLHelper::_('select.option', $template->element . ':' . $view . ':' . $value, $text); } } } diff --git a/libraries/src/Form/Form.php b/libraries/src/Form/Form.php index 233748de35ffa..a640a4607fc8a 100644 --- a/libraries/src/Form/Form.php +++ b/libraries/src/Form/Form.php @@ -656,7 +656,7 @@ public function load($data, $replace = true, $xpath = null) $olddom->parentNode->replaceChild($addeddom, $olddom); $loadeddom->parentNode->removeChild($loadeddom); } else { - unset($field); + unset($field[0]); } } } diff --git a/libraries/src/MVC/Controller/ApiController.php b/libraries/src/MVC/Controller/ApiController.php index 2c5cefcc1ec73..469e01cb7228f 100644 --- a/libraries/src/MVC/Controller/ApiController.php +++ b/libraries/src/MVC/Controller/ApiController.php @@ -587,6 +587,7 @@ protected function allowAdd($data = []) * * @return boolean * + * @since 5.4.8 * @since 6.1.3 */ protected function allowDelete(): bool diff --git a/media_source/system/js/fields/calendar.es5.js b/media_source/system/js/fields/calendar.es5.js index 4935230ef3061..1f9c8c8062ee4 100644 --- a/media_source/system/js/fields/calendar.es5.js +++ b/media_source/system/js/fields/calendar.es5.js @@ -157,6 +157,9 @@ this._documentClick = function(event) { return self._handleDocumentClick(event); }; + this._focusOut = function (event) { + return self._handleFocusOut(event); + }; // Set it up this.checkInputs(); @@ -233,7 +236,10 @@ JoomlaCalendar.prototype.moveCursorBy = function (step) { var date = new Date(this.date); date.setDate(date.getDate() - step); - this.setDate(date); + if (!date.equalsTo(this.date)) { + this.date = date; + this.callHandler(); + } }; /** Reset select element */ @@ -288,8 +294,8 @@ this.hidden = false; document.addEventListener("keydown", this._calKeyEvent, true); - document.addEventListener("keypress", this._calKeyEvent, true); document.addEventListener("mousedown", this._documentClick, true); + document.addEventListener("focusout", this._focusOut, true); /** Move the calendar to top position if it doesn't fit below. */ var containerTmp = this.element.querySelector('.js-calendar'); @@ -306,8 +312,8 @@ /** Method to hide the calendar. */ JoomlaCalendar.prototype.hide = function () { document.removeEventListener("keydown", this._calKeyEvent, true); - document.removeEventListener("keypress", this._calKeyEvent, true); document.removeEventListener("mousedown", this._documentClick, true); + document.removeEventListener("focusout", this._focusOut, true); this.dropdownElement.classList.remove('open'); this.dropdownElement.setAttribute('hidden', ''); @@ -329,6 +335,17 @@ } }; + JoomlaCalendar.prototype._handleFocusOut = function (ev) { + var relatedTarget = ev.relatedTarget; + + // Focus is still somewhere inside the calendar field. + if (relatedTarget && this.element.contains(relatedTarget)) { + return; + } + + this.hide(); + }; + /** Method to handle mouse click events (menus, buttons) **/ JoomlaCalendar.prototype._handleDayMouseDown = function (ev) { var self = this, @@ -511,11 +528,45 @@ var self = this, code = ev.code; - // Get value from input + // Tab from the calendar input moves into the time controls. if (ev.target === this.inputField && (code === 'Enter' || code === 'Tab')) { + if (code === 'Tab' && !ev.shiftKey && self.params.showsTime) { + var hoursEl = this.table.querySelector('.time-hours'); + + if (hoursEl) { + ev.preventDefault(); + hoursEl.focus(); + return; + } + } + this.close(); } + // Handle keyboard navigation and focus within the time controls. + if ( + ev.target.matches && + ev.target.matches('.time-hours, .time-minutes') + ) { + // Shift+Tab from Hours returns to the calendar input. + if (code === 'Tab' && ev.shiftKey && ev.target.matches('.time-hours')) { + ev.preventDefault(); + this.inputField.focus(); + return; + } + + // Let the time selects handle arrow keys and Tab natively. + if ( + code === 'ArrowUp' || + code === 'ArrowDown' || + code === 'ArrowLeft' || + code === 'ArrowRight' || + code === 'Tab' + ) { + return; + } + } + if (self.params.direction === 'rtl') { if (code === 'ArrowLeft') { code = 'ArrowRight'; @@ -524,23 +575,34 @@ } } + // Select the currently highlighted date. + if (code === 'Enter' && self.currentDateEl) { + self.cellClick(self.currentDateEl, ev); + return; + } + if (ev.shiftKey && code === 'Space') { ev.preventDefault(); this.cellClick(self._nav_now, ev); self.close(); } + if (code === 'Escape') { this.close(); } + if (code === 'ArrowUp') { this.moveCursorBy(7); } + if (code === 'ArrowDown') { this.moveCursorBy(-7); } + if (code === 'ArrowLeft') { this.moveCursorBy(1); } + if (code === 'ArrowRight') { this.moveCursorBy(-1); } @@ -936,7 +998,7 @@ // while setMonth() (the Date constructor) expects 0-indexed. // Forgetting either conversion causes an off-by-one day (or month) error. var ndate = Date.localCalToGregorian(date.rawYear, date.rawMonth + 1, date.rawDay); - + // Rebuild the Date atomically (year/month/day in one constructor call) instead of calling // setFullYear()/setMonth()/setDate() in sequence: // mutating month before day meant the OLD day (e.g. 31) could briefly clash with diff --git a/media_source/system/js/joomla-toolbar-button.w-c.es6.js b/media_source/system/js/joomla-toolbar-button.w-c.es6.js index 7cccf47597994..b682129cb7b19 100644 --- a/media_source/system/js/joomla-toolbar-button.w-c.es6.js +++ b/media_source/system/js/joomla-toolbar-button.w-c.es6.js @@ -58,6 +58,11 @@ window.customElements.define('joomla-toolbar-button', class extends HTMLElement // Watch on list selection this.formElement.boxchecked.addEventListener('change', this.onChange); } + + this.onHideDropdown = this.onHideDropdown.bind(this); + if (this.buttonElement.classList.contains('dropdown-toggle')) { + this.buttonElement.addEventListener('hide.bs.dropdown', this.onHideDropdown); + } } /** @@ -69,6 +74,21 @@ window.customElements.define('joomla-toolbar-button', class extends HTMLElement } this.buttonElement.removeEventListener('click', this.executeTask); + + if (this.buttonElement.classList.contains('dropdown-toggle')) { + this.buttonElement.removeEventListener('hide.bs.dropdown', this.onHideDropdown); + } + } + + onHideDropdown(e) { + if (e.clickEvent && e.clickEvent.target instanceof Element) { + const target = e.clickEvent.target; + if (this.formElement && this.formElement.contains(target) && target.closest('input[type="checkbox"], label')) { + if (this.formElement.boxchecked && this.formElement.boxchecked.value > 0) { + e.preventDefault(); + } + } + } } onChange({ target }) { @@ -84,6 +104,16 @@ window.customElements.define('joomla-toolbar-button', class extends HTMLElement // An anchor does not support "disabled" attribute, so use class if (this.buttonElement) { if (this.disabled) { + // If it's a dropdown toggle and it's currently showing, hide it before disabling + if (this.buttonElement.classList.contains('dropdown-toggle') && this.buttonElement.classList.contains('show')) { + if (window.bootstrap && window.bootstrap.Dropdown) { + const dropdown = window.bootstrap.Dropdown.getInstance(this.buttonElement); + if (dropdown) { + dropdown.hide(); + } + } + } + if (this.buttonElement.nodeName === 'BUTTON') { this.buttonElement.setAttribute('aria-disabled', 'true'); } diff --git a/modules/mod_articles/src/Helper/ArticlesHelper.php b/modules/mod_articles/src/Helper/ArticlesHelper.php index 8f2c0bbfef797..994c8542a8589 100644 --- a/modules/mod_articles/src/Helper/ArticlesHelper.php +++ b/modules/mod_articles/src/Helper/ArticlesHelper.php @@ -389,6 +389,8 @@ public function getArticles(Registry $params, SiteApplication $app) $item->displayDate = $show_date ? $item->displayDate : ''; $item->displayHits = $show_hits ? $item->hits : ''; + $item->introTextTruncated = false; + if ($show_introtext) { $item->displayIntrotext = HTMLHelper::_('content.prepare', $item->introtext, '', 'mod_articles.content'); @@ -399,7 +401,9 @@ public function getArticles(Registry $params, SiteApplication $app) } if ($introtext_limit != 0) { - $item->displayIntrotext = HTMLHelper::_('string.truncateComplex', $item->displayIntrotext, $introtext_limit); + $truncated = HTMLHelper::_('string.truncateComplex', $item->displayIntrotext, $introtext_limit); + $item->introTextTruncated = $truncated !== $item->displayIntrotext; + $item->displayIntrotext = $truncated; } } diff --git a/modules/mod_articles/tmpl/default_items.php b/modules/mod_articles/tmpl/default_items.php index 57d84087c43af..58729019cb8fe 100644 --- a/modules/mod_articles/tmpl/default_items.php +++ b/modules/mod_articles/tmpl/default_items.php @@ -124,7 +124,7 @@ event->afterDisplayContent; ?> - get('show_readmore') && !empty($item->fulltext)) : ?> + get('show_readmore') && (!empty($item->fulltext) || $item->introTextTruncated)) : ?> get('show_readmore_title', '') !== '') : ?> params->set('show_readmore_title', $params->get('show_readmore_title')); ?> params->set('readmore_limit', $params->get('readmore_limit')); ?> diff --git a/modules/mod_stats/src/Helper/StatsHelper.php b/modules/mod_stats/src/Helper/StatsHelper.php index 93a9d0782860b..5e8dc944a5f02 100644 --- a/modules/mod_stats/src/Helper/StatsHelper.php +++ b/modules/mod_stats/src/Helper/StatsHelper.php @@ -58,7 +58,7 @@ public function getStats(Registry &$params, CMSApplicationInterface $app) if ($serverinfo) { $rows[$i] = new \stdClass(); $rows[$i]->title = Text::_('MOD_STATS_OS'); - $rows[$i]->data = substr(php_uname(), 0, 7); + $rows[$i]->data = \function_exists('php_uname') ? substr(php_uname(), 0, 7) : ''; $i++; $rows[$i] = new \stdClass(); diff --git a/plugins/fields/imagelist/src/Extension/Imagelist.php b/plugins/fields/imagelist/src/Extension/Imagelist.php index 706dcf6f8d7fb..303081430da78 100644 --- a/plugins/fields/imagelist/src/Extension/Imagelist.php +++ b/plugins/fields/imagelist/src/Extension/Imagelist.php @@ -45,7 +45,8 @@ public function onCustomFieldsPrepareDom($field, \DOMElement $parent, Form $form } $fieldNode->setAttribute('hide_default', 'true'); - $fieldNode->setAttribute('directory', '/images/' . $fieldNode->getAttribute('directory')); + $directory = trim((string) $fieldNode->getAttribute('directory'), '/'); + $fieldNode->setAttribute('directory', $directory === '' ? 'images' : 'images/' . $directory); return $fieldNode; } diff --git a/plugins/filesystem/local/src/Adapter/LocalAdapter.php b/plugins/filesystem/local/src/Adapter/LocalAdapter.php index aa2bcb86e9220..95479b451d918 100644 --- a/plugins/filesystem/local/src/Adapter/LocalAdapter.php +++ b/plugins/filesystem/local/src/Adapter/LocalAdapter.php @@ -869,7 +869,14 @@ private function checkContent(string $localPath, string $mediaContent) throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT'), 500); } - $can = $helper->canUpload(['name' => $name, 'size' => \strlen($mediaContent), 'tmp_name' => $tmpFile], 'com_media'); + /* + * Pass the normalised name so the size, MIME and executable-extension + * checks still run without re-validating the existing filename, which may + * contain non-ASCII characters preserved from earlier uploads. + */ + $safeName = File::makeSafe($name) ?: $name; + + $can = $helper->canUpload(['name' => $safeName, 'size' => \strlen($mediaContent), 'tmp_name' => $tmpFile], 'com_media'); try { File::delete($tmpFile); diff --git a/plugins/multifactorauth/totp/src/Extension/Totp.php b/plugins/multifactorauth/totp/src/Extension/Totp.php index d1284601c8675..d666917d7dfb7 100644 --- a/plugins/multifactorauth/totp/src/Extension/Totp.php +++ b/plugins/multifactorauth/totp/src/Extension/Totp.php @@ -273,7 +273,7 @@ public function onUserMultifactorSaveSetup(SaveSetup $event): void * If the code is empty but the key already existed in $options someone is simply changing the title / default * Method status. We can allow this and stop checking anything else now. */ - $code = $input->getInt('code'); + $code = $input->getCmd('code'); if (empty($code) && !empty($optionsKey)) { $event->addResult($options); diff --git a/plugins/schemaorg/jobposting/src/Extension/JobPosting.php b/plugins/schemaorg/jobposting/src/Extension/JobPosting.php index 55eb9c9137c79..32543b44bf964 100644 --- a/plugins/schemaorg/jobposting/src/Extension/JobPosting.php +++ b/plugins/schemaorg/jobposting/src/Extension/JobPosting.php @@ -89,6 +89,23 @@ public function onSchemaBeforeCompileHead(BeforeCompileHeadEvent $event) if (!empty($entry['validThrough'])) { $entry['validThrough'] = $this->prepareDate($entry['validThrough']); } + + /** + * Search engines only accept TELECOMMUTE as jobLocationType. A remote job is + * marked TELECOMMUTE, a hybrid job is marked TELECOMMUTE combined with the + * physical jobLocation and an on-site job must not emit a jobLocationType at + * all and relies on jobLocation alone. + */ + switch ($entry['jobLocationType'] ?? '') { + case 'Telecommute': + case 'Hybrid': + $entry['jobLocationType'] = 'TELECOMMUTE'; + break; + + case 'Onsite': + unset($entry['jobLocationType']); + break; + } } $schema->set('@graph', $graph); diff --git a/plugins/system/schemaorg/src/Extension/Schemaorg.php b/plugins/system/schemaorg/src/Extension/Schemaorg.php index 366fc16502c46..8af09bb76109c 100644 --- a/plugins/system/schemaorg/src/Extension/Schemaorg.php +++ b/plugins/system/schemaorg/src/Extension/Schemaorg.php @@ -321,14 +321,21 @@ public function onBeforeCompileHead(BeforeCompileHeadApplicationEvent $event): v $siteSchema['url'] = $domain; // Image - $image = $this->params->get('image') ? HTMLHelper::_('cleanimageUrl', $this->params->get('image')) : false; + $image = $this->params->get('image') ? HTMLHelper::_('cleanImageUrl', $this->params->get('image')) : false; if ($image !== false) { + $logoUrl = $image->url; + + // Ensure absolute URL for schema logo + if (!preg_match('#^(https?:)?//#i', $logoUrl)) { + $logoUrl = Uri::root() . ltrim($logoUrl, '/'); + } + $siteSchema['logo'] = [ '@type' => 'ImageObject', '@id' => $domain . '#/schema/ImageObject/logo', - 'url' => $image->url, - 'contentUrl' => $image->url, + 'url' => $logoUrl, + 'contentUrl' => $logoUrl, 'width' => $image->attributes['width'] ?? 0, 'height' => $image->attributes['height'] ?? 0, ]; diff --git a/plugins/system/stats/src/Extension/Stats.php b/plugins/system/stats/src/Extension/Stats.php index 11ed5e0762f3c..013bc8ad6ad15 100644 --- a/plugins/system/stats/src/Extension/Stats.php +++ b/plugins/system/stats/src/Extension/Stats.php @@ -357,7 +357,7 @@ private function getStatsData() 'db_type' => $this->getDatabase()->name, 'db_version' => $this->getDatabase()->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), ]; // Check if we have a MariaDB version string and extract the proper version from it diff --git a/tests/System/integration/administrator/components/com_finder/Index.cy.js b/tests/System/integration/administrator/components/com_finder/Index.cy.js index f6decc45d1b20..72b79bd65be9d 100644 --- a/tests/System/integration/administrator/components/com_finder/Index.cy.js +++ b/tests/System/integration/administrator/components/com_finder/Index.cy.js @@ -16,6 +16,19 @@ describe('Test in backend that the Smart Search', () => { cy.contains('Test article').should('exist'); }); + it('can index content', () => { + // Create a new article + cy.visit('/administrator/index.php?option=com_content&task=article.add'); + cy.get('#jform_title').clear().type('Test article'); + cy.clickToolbarButton('Save & Close'); + // Visit the smart search page + cy.visit('/administrator/index.php?option=com_finder&view=index'); + // Click the "Index" button. Note: with JDEBUG enabled the button is moved + // into the #toolbar-indexing-group dropdown, see com_finder Index HtmlView. + cy.get('#toolbar-index > button').click(); + cy.contains('Test article').should('exist'); + }); + it('can purge the index', () => { // Visit the smart search page cy.visit('/administrator/index.php?option=com_finder&view=index'); diff --git a/tests/Unit/Libraries/Cms/Encrypt/TotpTest.php b/tests/Unit/Libraries/Cms/Encrypt/TotpTest.php new file mode 100644 index 0000000000000..2b3a1ce23448c --- /dev/null +++ b/tests/Unit/Libraries/Cms/Encrypt/TotpTest.php @@ -0,0 +1,52 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Libraries\Cms\Encrypt; + +use Joomla\CMS\Encrypt\Totp; +use Joomla\Tests\Unit\UnitTestCase; + +/** + * Test class for \Joomla\CMS\Encrypt\Totp + * + * @since __DEPLOY_VERSION__ + */ +class TotpTest extends UnitTestCase +{ + /** + * The currently valid passcode is accepted. + * + * @return void + * @since __DEPLOY_VERSION__ + */ + public function testCheckCodeAcceptsValidPasscode() + { + $totp = new Totp(); + $secret = $totp->generateSecret(); + + $this->assertTrue($totp->checkCode($secret, $totp->getCode($secret))); + } + + /** + * A numeric string that only equals the passcode under type juggling must be rejected. + * + * @return void + * @since __DEPLOY_VERSION__ + */ + public function testCheckCodeRejectsTypeJuggledPasscode() + { + $totp = new Totp(); + $secret = $totp->generateSecret(); + $code = $totp->getCode($secret); + + $this->assertFalse($totp->checkCode($secret, '+' . $code)); + $this->assertFalse($totp->checkCode($secret, ' ' . $code)); + } +} diff --git a/tests/Unit/Plugin/Filesystem/Local/Adapter/LocalAdapterTest.php b/tests/Unit/Plugin/Filesystem/Local/Adapter/LocalAdapterTest.php new file mode 100644 index 0000000000000..86f8263d9206d --- /dev/null +++ b/tests/Unit/Plugin/Filesystem/Local/Adapter/LocalAdapterTest.php @@ -0,0 +1,194 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Plugin\Filesystem\Local\Adapter; + +use Joomla\CMS\Application\CMSApplicationInterface; +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Component\ComponentRecord; +use Joomla\CMS\Factory; +use Joomla\Filesystem\File; +use Joomla\Plugin\Filesystem\Local\Adapter\LocalAdapter; +use Joomla\Registry\Registry; +use Joomla\Tests\Unit\UnitTestCase; + +/** + * Test class for the Local filesystem adapter. + * + * @package Joomla.UnitTest + * @subpackage Local + * + * @testdox The Local adapter + * + * @since __DEPLOY_VERSION__ + */ +class LocalAdapterTest extends UnitTestCase +{ + /** + * Absolute path of the throw-away media root used by a single test. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private $workDir; + + /** + * Creates an isolated media root and the minimal global state that + * MediaHelper::canUpload() reaches for (the application and the com_media + * component parameters), so the adapter can run as a pure unit test. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function setUp(): void + { + parent::setUp(); + + $this->workDir = sys_get_temp_dir() . '/joomla-localadapter-' . uniqid(); + mkdir($this->workDir); + + // canUpload() calls Factory::getApplication() unconditionally. + Factory::$application = $this->createStub(CMSApplicationInterface::class); + + // Seed com_media with empty params so ComponentHelper never queries the database. + $record = new ComponentRecord(['option' => 'com_media', 'enabled' => 1]); + $record->setParams(new Registry()); + + $components = new \ReflectionProperty(ComponentHelper::class, 'components'); + $components->setValue(null, ['com_media' => $record]); + } + + /** + * Removes the temporary media root and resets the global state touched in setUp(). + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function tearDown(): void + { + foreach (glob($this->workDir . '/*') ?: [] as $file) { + unlink($file); + } + + if (is_dir($this->workDir)) { + rmdir($this->workDir); + } + + Factory::$application = null; + + $components = new \ReflectionProperty(ComponentHelper::class, 'components'); + $components->setValue(null, []); + + parent::tearDown(); + } + + /** + * Returns the bytes of a tiny, valid single-colour JPEG so that the + * MIME/type checks in canUpload() see a real image. + * + * @param array $rgb The fill colour as [red, green, blue]. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + private function jpegBytes(array $rgb): string + { + $image = imagecreatetruecolor(4, 4); + $color = imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]); + imagefilledrectangle($image, 0, 0, 3, 3, $color); + + ob_start(); + imagejpeg($image); + $bytes = ob_get_clean(); + + + return $bytes; + } + + /** + * @testdox saves an edited file back to its original non-ASCII name + * + * This is the behaviour PR #47935 restores: a file whose on-disk name + * contains non-ASCII characters (uploaded via FTP, sample data, a + * migration, ...) can be re-saved by the image editor instead of being + * rejected by canUpload(). + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testUpdateFileSavesBackToNonAsciiName() + { + $name = 'erik_schön.jpg'; + file_put_contents($this->workDir . '/' . $name, $this->jpegBytes([255, 0, 0])); + + $newContent = $this->jpegBytes([0, 0, 255]); + + $adapter = new LocalAdapter($this->workDir, 'test-media'); + $adapter->updateFile($name, '/', $newContent); + + $this->assertSame( + $newContent, + file_get_contents($this->workDir . '/' . $name), + 'The non-ASCII named file should be updated in place.' + ); + } + + /** + * @testdox never touches a pre-existing file that already has the safe name + * + * Answers @brianteeman's review question: the "safe" name derived in + * checkContent() is only handed to canUpload() as a validation string. It + * is never used to name a file on disk (the probe file uses uniqid(), the + * real write targets the original path), so a file that already exists at + * File::makeSafe($name) is left completely untouched. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testUpdateFileDoesNotTouchExistingSafeNamedNeighbour() + { + $name = 'erik_schön.jpg'; + $safeName = File::makeSafe($name); + + // Guard the premise of the test: the two names really are different. + $this->assertNotSame($name, $safeName); + + file_put_contents($this->workDir . '/' . $name, $this->jpegBytes([255, 0, 0])); + + // A pre-existing, unrelated file that happens to sit at the safe name. + $neighbourContent = 'PRE-EXISTING-SAFE-NAME-CONTENT'; + file_put_contents($this->workDir . '/' . $safeName, $neighbourContent); + + $adapter = new LocalAdapter($this->workDir, 'test-media'); + $adapter->updateFile($name, '/', $this->jpegBytes([0, 0, 255])); + + // The neighbour is byte-for-byte unchanged. + $this->assertSame( + $neighbourContent, + file_get_contents($this->workDir . '/' . $safeName), + 'The pre-existing safe-named file must not be overwritten.' + ); + + // Only the two original files remain: the uniqid() probe file was + // cleaned up and no new safe-named file was created. + $remaining = array_values(array_diff(scandir($this->workDir), ['.', '..'])); + sort($remaining); + + $expected = [$name, $safeName]; + sort($expected); + + $this->assertSame($expected, $remaining, 'No stray files should be left behind.'); + } +} diff --git a/tests/Unit/Plugin/Schemaorg/JobPosting/Extension/JobPostingPluginTest.php b/tests/Unit/Plugin/Schemaorg/JobPosting/Extension/JobPostingPluginTest.php new file mode 100644 index 0000000000000..f60ef6a955a64 --- /dev/null +++ b/tests/Unit/Plugin/Schemaorg/JobPosting/Extension/JobPostingPluginTest.php @@ -0,0 +1,158 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Plugin\Schemaorg\JobPosting\Extension; + +use Joomla\CMS\Event\Plugin\System\Schemaorg\BeforeCompileHeadEvent; +use Joomla\Plugin\Schemaorg\JobPosting\Extension\JobPosting; +use Joomla\Registry\Registry; +use Joomla\Tests\Unit\UnitTestCase; + +/** + * Test class for JobPosting plugin + * + * @package Joomla.UnitTest + * @subpackage JobPosting + * + * @testdox The JobPosting plugin + * + * @since __DEPLOY_VERSION__ + */ +class JobPostingPluginTest extends UnitTestCase +{ + /** + * Dispatches the schema through the plugin and returns the resulting graph. + * + * @param array $entry The JobPosting graph entry to compile + * + * @return array The compiled graph entry + * + * @since __DEPLOY_VERSION__ + */ + private function compileGraphEntry(array $entry): array + { + $plugin = new JobPosting(['name' => 'jobposting', 'type' => 'schemaorg']); + + $schema = new Registry(); + $schema->loadArray(['@graph' => [$entry]]); + + $event = new BeforeCompileHeadEvent('onSchemaBeforeCompileHead', [ + 'subject' => $schema, + 'context' => 'com_content.article.1', + ]); + + $plugin->onSchemaBeforeCompileHead($event); + + return $schema->get('@graph')[0]; + } + + /** + * @testdox maps a telecommute job location type to the TELECOMMUTE value expected by search engines + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testTelecommuteIsMappedToUppercaseTelecommute() + { + $entry = $this->compileGraphEntry([ + '@type' => 'JobPosting', + 'title' => 'Remote Developer', + 'jobLocationType' => 'Telecommute', + ]); + + $this->assertSame('TELECOMMUTE', $entry['jobLocationType']); + } + + /** + * @testdox maps a hybrid job location type to TELECOMMUTE while keeping the physical job location + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testHybridIsMappedToTelecommuteAndKeepsJobLocation() + { + $jobLocation = [ + '@type' => 'Place', + 'address' => ['@type' => 'PostalAddress', 'addressLocality' => 'Berlin'], + ]; + + $entry = $this->compileGraphEntry([ + '@type' => 'JobPosting', + 'title' => 'Hybrid Developer', + 'jobLocationType' => 'Hybrid', + 'jobLocation' => $jobLocation, + ]); + + $this->assertSame('TELECOMMUTE', $entry['jobLocationType']); + $this->assertSame($jobLocation, $entry['jobLocation']); + } + + /** + * @testdox omits the job location type for an on-site job so only the job location is emitted + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testOnsiteJobLocationTypeIsOmitted() + { + $jobLocation = [ + '@type' => 'Place', + 'address' => ['@type' => 'PostalAddress', 'addressLocality' => 'Berlin'], + ]; + + $entry = $this->compileGraphEntry([ + '@type' => 'JobPosting', + 'title' => 'Onsite Developer', + 'jobLocationType' => 'Onsite', + 'jobLocation' => $jobLocation, + ]); + + $this->assertArrayNotHasKey('jobLocationType', $entry); + $this->assertSame($jobLocation, $entry['jobLocation']); + } + + /** + * @testdox leaves an entry without a job location type untouched + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testEntryWithoutJobLocationTypeIsUntouched() + { + $entry = $this->compileGraphEntry([ + '@type' => 'JobPosting', + 'title' => 'Developer', + ]); + + $this->assertArrayNotHasKey('jobLocationType', $entry); + $this->assertSame('Developer', $entry['title']); + } + + /** + * @testdox does not touch graph entries of other schema types + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testOtherSchemaTypesAreUntouched() + { + $entry = $this->compileGraphEntry([ + '@type' => 'Event', + 'jobLocationType' => 'Onsite', + ]); + + $this->assertSame('Onsite', $entry['jobLocationType']); + } +}