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); ?>
-
escape($this->source->filename); ?>
+ type === 'file') : ?>
+
escape($this->source->filename); ?>
+
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') . '_FILTER_LABEL'); ?>
-
- 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']);
+ }
+}