diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini
index 4a5fef3..f970f01 100644
--- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini
+++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini
@@ -53,5 +53,7 @@ COM_MOKOOG_BATCH_ERROR="Error:"
COM_MOKOOG_TOOLBAR_EXPORT="Export CSV"
COM_MOKOOG_TOOLBAR_IMPORT="Import CSV"
COM_MOKOOG_IMPORT_NO_FILE="No CSV file was uploaded."
+COM_MOKOOG_IMPORT_INVALID_TYPE="Invalid file type. Please upload a .csv file."
+COM_MOKOOG_IMPORT_FILE_TOO_LARGE="File is too large. Maximum allowed size is %s."
COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file."
COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped."
diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.ini
index 4a5fef3..f970f01 100644
--- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini
+++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini
@@ -53,5 +53,7 @@ COM_MOKOOG_BATCH_ERROR="Error:"
COM_MOKOOG_TOOLBAR_EXPORT="Export CSV"
COM_MOKOOG_TOOLBAR_IMPORT="Import CSV"
COM_MOKOOG_IMPORT_NO_FILE="No CSV file was uploaded."
+COM_MOKOOG_IMPORT_INVALID_TYPE="Invalid file type. Please upload a .csv file."
+COM_MOKOOG_IMPORT_FILE_TOO_LARGE="File is too large. Maximum allowed size is %s."
COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file."
COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped."
diff --git a/src/packages/com_mokoog/src/Controller/BatchController.php b/src/packages/com_mokoog/src/Controller/BatchController.php
index 42cf3b4..ca9aaeb 100644
--- a/src/packages/com_mokoog/src/Controller/BatchController.php
+++ b/src/packages/com_mokoog/src/Controller/BatchController.php
@@ -29,6 +29,10 @@ class BatchController extends BaseController
{
Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN'));
+ if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokoog')) {
+ throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
+ }
+
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('COUNT(*)')
@@ -58,6 +62,10 @@ class BatchController extends BaseController
{
Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN'));
+ if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokoog')) {
+ throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
+ }
+
$app = Factory::getApplication();
$offset = $app->getInput()->getInt('offset', 0);
$limit = $app->getInput()->getInt('limit', 50);
diff --git a/src/packages/com_mokoog/src/Controller/ImportExportController.php b/src/packages/com_mokoog/src/Controller/ImportExportController.php
index 95a8143..b135575 100644
--- a/src/packages/com_mokoog/src/Controller/ImportExportController.php
+++ b/src/packages/com_mokoog/src/Controller/ImportExportController.php
@@ -19,6 +19,16 @@ use Joomla\CMS\Session\Session;
class ImportExportController extends BaseController
{
+ /**
+ * Maximum upload file size in bytes (2 MB).
+ */
+ private const MAX_FILE_SIZE = 2 * 1024 * 1024;
+
+ /**
+ * Allowed content_type patterns for import.
+ */
+ private const CONTENT_TYPE_PATTERN = '/^[a-z][a-z0-9_.]*$/';
+
/**
* Export all OG tags as CSV.
*
@@ -28,6 +38,10 @@ class ImportExportController extends BaseController
{
Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN'));
+ if (!Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokoog')) {
+ throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
+ }
+
$app = Factory::getApplication();
$db = Factory::getDbo();
@@ -88,6 +102,12 @@ class ImportExportController extends BaseController
{
Session::checkToken() || jexit(Text::_('JINVALID_TOKEN'));
+ $identity = Factory::getApplication()->getIdentity();
+
+ if (!$identity->authorise('core.create', 'com_mokoog') || !$identity->authorise('core.edit', 'com_mokoog')) {
+ throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
+ }
+
$app = Factory::getApplication();
$input = $app->getInput();
$files = $input->files->get('jform', [], 'array');
@@ -99,7 +119,37 @@ class ImportExportController extends BaseController
return;
}
- $tmpFile = $files['csv_file']['tmp_name'];
+ $csvFile = $files['csv_file'];
+
+ // Validate file extension
+ $ext = strtolower(pathinfo($csvFile['name'] ?? '', PATHINFO_EXTENSION));
+
+ if ($ext !== 'csv') {
+ $app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_INVALID_TYPE'), 'error');
+ $app->redirect('index.php?option=com_mokoog&view=tags');
+
+ return;
+ }
+
+ // Validate MIME type
+ $allowedMimes = ['text/csv', 'text/plain', 'application/csv', 'application/vnd.ms-excel'];
+
+ if (!empty($csvFile['type']) && !\in_array($csvFile['type'], $allowedMimes, true)) {
+ $app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_INVALID_TYPE'), 'error');
+ $app->redirect('index.php?option=com_mokoog&view=tags');
+
+ return;
+ }
+
+ // Validate file size
+ if (($csvFile['size'] ?? 0) > self::MAX_FILE_SIZE) {
+ $app->enqueueMessage(Text::sprintf('COM_MOKOOG_IMPORT_FILE_TOO_LARGE', '2 MB'), 'error');
+ $app->redirect('index.php?option=com_mokoog&view=tags');
+
+ return;
+ }
+
+ $tmpFile = $csvFile['tmp_name'];
$handle = fopen($tmpFile, 'r');
if (!$handle) {
@@ -141,6 +191,13 @@ class ImportExportController extends BaseController
continue;
}
+ // Validate content_type against allowed pattern
+ if (!preg_match(self::CONTENT_TYPE_PATTERN, $contentType)) {
+ $skipped++;
+
+ continue;
+ }
+
// Check for existing record
$query = $db->getQuery(true)
->select($db->quoteName('id'))
diff --git a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php
index 7a485f3..7bd1b9b 100644
--- a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php
+++ b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php
@@ -94,7 +94,8 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
'com_categories.categorycom_content' => 'com_content.category',
];
$contentType = $formTypeMap[$formName] ?? 'com_content';
- $ogData = $this->loadOgData($contentType, $id);
+ $language = $this->getContentLanguage($data);
+ $ogData = $this->loadOgData($contentType, $id, $language);
if ($ogData) {
$form->bind(['mokoog' => (array) $ogData]);
@@ -132,6 +133,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
$contentType = $supportedContexts[$context];
$contentId = (int) $article->id;
+ $language = $this->getContentLanguage($article);
$input = $app->getInput();
$jform = $input->get('jform', [], 'array');
@@ -141,7 +143,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
return;
}
- $this->saveOgData($contentType, $contentId, $ogData);
+ $this->saveOgData($contentType, $contentId, $ogData, $language);
}
/**
@@ -179,14 +181,15 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
}
/**
- * Load existing OG data for a content item.
+ * Load existing OG data for a content item, filtered by language.
*
* @param string $contentType Content type identifier
* @param int $contentId Content ID
+ * @param string $language Language tag (e.g. 'en-GB') or '*' for all
*
* @return object|null
*/
- private function loadOgData(string $contentType, int $contentId): ?object
+ private function loadOgData(string $contentType, int $contentId, string $language = '*'): ?object
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
@@ -196,9 +199,12 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
]))
->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)
+ . ' OR ' . $db->quoteName('language') . ' = ' . $db->quote('*') . ')')
+ ->order('CASE WHEN ' . $db->quoteName('language') . ' = ' . $db->quote('*') . ' THEN 1 ELSE 0 END ASC');
- $db->setQuery($query);
+ $db->setQuery($query, 0, 1);
return $db->loadObject();
}
@@ -209,19 +215,21 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
* @param string $contentType Content type identifier
* @param int $contentId Content ID
* @param array $ogData OG field values
+ * @param string $language Language tag (e.g. 'en-GB') or '*' for all
*
* @return void
*/
- private function saveOgData(string $contentType, int $contentId, array $ogData): void
+ private function saveOgData(string $contentType, int $contentId, array $ogData, string $language = '*'): void
{
$db = Factory::getDbo();
- // Check if record exists
+ // Check if record exists for this content + language
$query = $db->getQuery(true)
->select('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();
@@ -236,6 +244,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
$record = (object) [
'content_type' => $contentType,
'content_id' => $contentId,
+ 'language' => $language,
'og_title' => trim($ogData['og_title'] ?? ''),
'og_description' => trim($ogData['og_description'] ?? ''),
'og_image' => trim($ogData['og_image'] ?? ''),
@@ -256,4 +265,24 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
$db->insertObject('#__mokoog_tags', $record);
}
}
+
+ /**
+ * Extract the language tag from content data.
+ *
+ * @param object|array $data Content data from form or article object
+ *
+ * @return string Language tag (e.g. 'en-GB') or '*' for all languages
+ */
+ private function getContentLanguage($data): string
+ {
+ $language = '*';
+
+ if (\is_object($data) && isset($data->language)) {
+ $language = $data->language;
+ } elseif (\is_array($data) && isset($data['language'])) {
+ $language = $data['language'];
+ }
+
+ return !empty($language) ? $language : '*';
+ }
}
diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini
index 34f17fd..3d1c0f6 100644
--- a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini
+++ b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini
@@ -7,6 +7,10 @@ PLG_SYSTEM_MOKOOG_FIELDSET_ADVANCED="Advanced Settings"
PLG_SYSTEM_MOKOOG_FIELD_SITE_NAME="Site Name"
PLG_SYSTEM_MOKOOG_FIELD_SITE_NAME_DESC="The og:site_name value. Leave blank to use the Joomla site name."
+PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_TITLE="Default OG Title"
+PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_TITLE_DESC="Site-wide fallback title for social sharing. Used when a page has no custom OG title. Leave blank to use the page title."
+PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_DESCRIPTION="Default OG Description"
+PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_DESCRIPTION_DESC="Site-wide fallback description for social sharing. Used when a page has no custom OG description and no meta description. Leave blank to auto-generate from page content."
PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_IMAGE="Default Image"
PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_IMAGE_DESC="Fallback image used when no article or page image is found."
PLG_SYSTEM_MOKOOG_FIELD_TWITTER_CARD="Twitter Card Type"
@@ -17,6 +21,8 @@ PLG_SYSTEM_MOKOOG_FIELD_TWITTER_SITE="Twitter @username"
PLG_SYSTEM_MOKOOG_FIELD_TWITTER_SITE_DESC="Your site's Twitter handle (e.g. @mokoconsulting)."
PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID="Facebook App ID"
PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID_DESC="Your Facebook App ID for fb:app_id meta tag."
+PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR="Discord Embed Color"
+PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC="The color of the embed sidebar when shared on Discord. Sets the theme-color meta tag. Leave blank to use Discord defaults."
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE="Auto-generate Tags"
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE_DESC="Automatically generate OG tags from article content when no custom tags are set."
PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description"
diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini
index 34f17fd..3d1c0f6 100644
--- a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini
+++ b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini
@@ -7,6 +7,10 @@ PLG_SYSTEM_MOKOOG_FIELDSET_ADVANCED="Advanced Settings"
PLG_SYSTEM_MOKOOG_FIELD_SITE_NAME="Site Name"
PLG_SYSTEM_MOKOOG_FIELD_SITE_NAME_DESC="The og:site_name value. Leave blank to use the Joomla site name."
+PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_TITLE="Default OG Title"
+PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_TITLE_DESC="Site-wide fallback title for social sharing. Used when a page has no custom OG title. Leave blank to use the page title."
+PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_DESCRIPTION="Default OG Description"
+PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_DESCRIPTION_DESC="Site-wide fallback description for social sharing. Used when a page has no custom OG description and no meta description. Leave blank to auto-generate from page content."
PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_IMAGE="Default Image"
PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_IMAGE_DESC="Fallback image used when no article or page image is found."
PLG_SYSTEM_MOKOOG_FIELD_TWITTER_CARD="Twitter Card Type"
@@ -17,6 +21,8 @@ PLG_SYSTEM_MOKOOG_FIELD_TWITTER_SITE="Twitter @username"
PLG_SYSTEM_MOKOOG_FIELD_TWITTER_SITE_DESC="Your site's Twitter handle (e.g. @mokoconsulting)."
PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID="Facebook App ID"
PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID_DESC="Your Facebook App ID for fb:app_id meta tag."
+PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR="Discord Embed Color"
+PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC="The color of the embed sidebar when shared on Discord. Sets the theme-color meta tag. Leave blank to use Discord defaults."
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE="Auto-generate Tags"
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE_DESC="Automatically generate OG tags from article content when no custom tags are set."
PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description"
diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml
index c5a0e17..231c803 100644
--- a/src/packages/plg_system_mokoog/mokoog.xml
+++ b/src/packages/plg_system_mokoog/mokoog.xml
@@ -41,6 +41,23 @@
description="PLG_SYSTEM_MOKOOG_FIELD_SITE_NAME_DESC"
default=""
/>
+