* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ namespace Joomla\Component\MokoOG\Administrator\Controller; defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\BaseController; 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. * * @return void */ public function export(): void { 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(); // Join with #__content to get article titles for reference $query = $db->getQuery(true) ->select([ $db->quoteName('t.content_type'), $db->quoteName('t.content_id'), 'COALESCE(' . $db->quoteName('c.title') . ', ' . $db->quote('') . ') AS ' . $db->quoteName('article_title'), $db->quoteName('t.og_title'), $db->quoteName('t.og_description'), $db->quoteName('t.og_image'), $db->quoteName('t.og_type'), $db->quoteName('t.seo_title'), $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( $db->quoteName('#__content', 'c') . ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content') . ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id') ) ->order($db->quoteName('t.content_type') . ', ' . $db->quoteName('t.content_id')); $db->setQuery($query); $rows = $db->loadAssocList(); // Send CSV headers $app->setHeader('Content-Type', 'text/csv; charset=utf-8'); $app->setHeader('Content-Disposition', 'attachment; filename="mokoog_tags_export.csv"'); $app->sendHeaders(); $output = fopen('php://output', 'w'); // Header row fputcsv($output, [ '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) { fputcsv($output, $row); } fclose($output); $app->close(); } /** * Import OG tags from uploaded CSV. * * @return void */ public function import(): void { 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'); if (empty($files['csv_file']['tmp_name'])) { $app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_NO_FILE'), 'error'); $app->redirect('index.php?option=com_mokoog&view=tags'); return; } $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) { $app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_READ_ERROR'), 'error'); $app->redirect('index.php?option=com_mokoog&view=tags'); return; } $db = Factory::getDbo(); $header = fgetcsv($handle); $created = 0; $updated = 0; $skipped = 0; $now = Factory::getDate()->toSql(); while (($row = fgetcsv($handle)) !== false) { if (\count($row) < 7) { $skipped++; continue; } $contentType = trim($row[0]); $contentId = (int) $row[1]; // $row[2] = article_title (informational, skip) $ogTitle = trim($row[3] ?? ''); $ogDescription = trim($row[4] ?? ''); $ogImage = trim($row[5] ?? ''); $ogType = trim($row[6] ?? 'article'); $seoTitle = trim($row[7] ?? ''); $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++; continue; } // Validate content_type against allowed pattern if (!preg_match(self::CONTENT_TYPE_PATTERN, $contentType)) { $skipped++; continue; } // 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('language') . ' = ' . $db->quote($language)); $db->setQuery($query); $existingId = $db->loadResult(); $record = (object) [ 'content_type' => $contentType, 'content_id' => $contentId, 'og_title' => $ogTitle, 'og_description' => $ogDescription, 'og_image' => $ogImage, 'og_type' => $ogType, 'seo_title' => $seoTitle, 'meta_description' => $metaDesc, 'robots' => $robots, 'canonical_url' => $canonicalUrl, 'language' => $language, 'published' => 1, 'modified' => $now, ]; if ($existingId) { $record->id = $existingId; $db->updateObject('#__mokoog_tags', $record, 'id'); $updated++; } else { $record->created = $now; $db->insertObject('#__mokoog_tags', $record); $created++; } } fclose($handle); $app->enqueueMessage( Text::sprintf('COM_MOKOOG_IMPORT_RESULT', $created, $updated, $skipped), 'success' ); $app->redirect('index.php?option=com_mokoog&view=tags'); } }