diff --git a/source/packages/com_mokoog/src/Controller/BatchController.php b/source/packages/com_mokoog/src/Controller/BatchController.php index 1f86892..468984b 100644 --- a/source/packages/com_mokoog/src/Controller/BatchController.php +++ b/source/packages/com_mokoog/src/Controller/BatchController.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. @@ -67,7 +67,7 @@ class BatchController extends BaseController } $app = Factory::getApplication(); - $limit = $app->getInput()->getInt('limit', 50); + $limit = min($app->getInput()->getInt('limit', 50), 200); $db = Factory::getDbo(); $query = $db->getQuery(true) diff --git a/source/packages/com_mokoog/src/Controller/ImportExportController.php b/source/packages/com_mokoog/src/Controller/ImportExportController.php index b135575..5fcb49f 100644 --- a/source/packages/com_mokoog/src/Controller/ImportExportController.php +++ b/source/packages/com_mokoog/src/Controller/ImportExportController.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. @@ -59,6 +59,7 @@ class ImportExportController extends BaseController $db->quoteName('t.meta_description'), $db->quoteName('t.robots'), $db->quoteName('t.canonical_url'), + $db->quoteName('t.language'), ]) ->from($db->quoteName('#__mokoog_tags', 't')) ->leftJoin( @@ -83,6 +84,7 @@ class ImportExportController extends BaseController 'content_type', 'content_id', 'article_title', 'og_title', 'og_description', 'og_image', 'og_type', 'seo_title', 'meta_description', 'robots', 'canonical_url', + 'language', ]); foreach ($rows as $row) { @@ -184,6 +186,12 @@ class ImportExportController extends BaseController $metaDesc = trim($row[8] ?? ''); $robots = trim($row[9] ?? ''); $canonicalUrl = trim($row[10] ?? ''); + $language = trim($row[11] ?? '*'); + + // Validate language tag format (e.g., 'en-GB', '*') + if ($language !== '*' && !preg_match('/^[a-z]{2,3}-[A-Z]{2}$/', $language)) { + $language = '*'; + } if (empty($contentType) || $contentId <= 0) { $skipped++; @@ -198,12 +206,13 @@ class ImportExportController extends BaseController continue; } - // Check for existing record + // Check for existing record (unique key includes language) $query = $db->getQuery(true) ->select($db->quoteName('id')) ->from($db->quoteName('#__mokoog_tags')) ->where($db->quoteName('content_type') . ' = ' . $db->quote($contentType)) - ->where($db->quoteName('content_id') . ' = ' . $contentId); + ->where($db->quoteName('content_id') . ' = ' . $contentId) + ->where($db->quoteName('language') . ' = ' . $db->quote($language)); $db->setQuery($query); $existingId = $db->loadResult(); @@ -219,6 +228,7 @@ class ImportExportController extends BaseController 'meta_description' => $metaDesc, 'robots' => $robots, 'canonical_url' => $canonicalUrl, + 'language' => $language, 'published' => 1, 'modified' => $now, ]; diff --git a/source/packages/com_mokoog/src/Table/TagTable.php b/source/packages/com_mokoog/src/Table/TagTable.php index 29d68e4..af85bac 100644 --- a/source/packages/com_mokoog/src/Table/TagTable.php +++ b/source/packages/com_mokoog/src/Table/TagTable.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. @@ -32,6 +32,16 @@ class TagTable extends Table * * @return bool */ + private const VALID_OG_TYPES = [ + 'article', 'website', 'product', 'profile', 'book', 'music.song', + 'music.album', 'video.movie', 'video.episode', 'video.other', + ]; + + private const VALID_ROBOTS = [ + 'index', 'noindex', 'follow', 'nofollow', 'none', 'noarchive', + 'nosnippet', 'noimageindex', 'max-snippet', 'max-image-preview', + ]; + public function check(): bool { if (empty($this->content_type)) { @@ -40,12 +50,58 @@ class TagTable extends Table return false; } + if (!preg_match('/^[a-z][a-z0-9_.]*$/', $this->content_type)) { + $this->setError('Content type contains invalid characters.'); + + return false; + } + if (empty($this->content_id)) { $this->setError('Content ID is required.'); return false; } + // Validate og_type against known values + if (!empty($this->og_type) && !\in_array($this->og_type, self::VALID_OG_TYPES, true)) { + $this->og_type = 'article'; + } + + // Truncate fields to schema max lengths + if (mb_strlen($this->og_title ?? '') > 255) { + $this->og_title = mb_substr($this->og_title, 0, 255); + } + + if (mb_strlen($this->seo_title ?? '') > 70) { + $this->seo_title = mb_substr($this->seo_title, 0, 70); + } + + if (mb_strlen($this->meta_description ?? '') > 200) { + $this->meta_description = mb_substr($this->meta_description, 0, 200); + } + + // Validate canonical_url format if non-empty + if (!empty($this->canonical_url) && !filter_var($this->canonical_url, FILTER_VALIDATE_URL)) { + $this->canonical_url = ''; + } + + // Validate robots directives + if (!empty($this->robots)) { + $parts = array_map('trim', explode(',', strtolower($this->robots))); + $valid = array_filter($parts, function ($part) { + // Allow directives with values like "max-snippet:-1" + $directive = explode(':', $part)[0]; + + return \in_array($directive, self::VALID_ROBOTS, true); + }); + $this->robots = $valid ? implode(', ', $valid) : ''; + } + + // Default language to '*' if not set + if (empty($this->language)) { + $this->language = '*'; + } + return true; } }