2026-05-23 18:38:18 -05:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-30 19:03:10 -05:00
|
|
|
* @package MokoJoomOpenGraph
|
2026-05-23 18:38:18 -05:00
|
|
|
* @subpackage com_mokoog
|
|
|
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
|
|
|
* @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
|
|
|
|
|
{
|
2026-05-30 20:37:50 -05:00
|
|
|
/**
|
|
|
|
|
* 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_.]*$/';
|
|
|
|
|
|
2026-05-23 18:38:18 -05:00
|
|
|
/**
|
|
|
|
|
* Export all OG tags as CSV.
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public function export(): void
|
|
|
|
|
{
|
|
|
|
|
Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN'));
|
|
|
|
|
|
2026-05-30 20:37:50 -05:00
|
|
|
if (!Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokoog')) {
|
|
|
|
|
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 18:38:18 -05:00
|
|
|
$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'),
|
|
|
|
|
])
|
|
|
|
|
->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',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
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'));
|
|
|
|
|
|
2026-05-30 20:37:50 -05:00
|
|
|
$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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 18:38:18 -05:00
|
|
|
$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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 20:37:50 -05:00
|
|
|
$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'];
|
2026-05-23 18:38:18 -05:00
|
|
|
$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] ?? '');
|
|
|
|
|
|
|
|
|
|
if (empty($contentType) || $contentId <= 0) {
|
|
|
|
|
$skipped++;
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 20:37:50 -05:00
|
|
|
// Validate content_type against allowed pattern
|
|
|
|
|
if (!preg_match(self::CONTENT_TYPE_PATTERN, $contentType)) {
|
|
|
|
|
$skipped++;
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 18:38:18 -05:00
|
|
|
// Check for existing record
|
|
|
|
|
$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);
|
|
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
'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');
|
|
|
|
|
}
|
|
|
|
|
}
|