46e30c950b
- Add missing language field to batch-generated records - Wrap batch insert in try-catch to handle duplicate key races - Add logging to all empty catch blocks (script.php, MokoOG license check) - Guard loadShopProduct() with try-catch for missing MokoSuiteShop tables - Guard reviews query in JsonLdBuilder for missing #__mokoshop_reviews
183 lines
5.7 KiB
PHP
183 lines
5.7 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoSuiteOpenGraph
|
|
* @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\Response\JsonResponse;
|
|
use Joomla\CMS\Session\Session;
|
|
|
|
class BatchController extends BaseController
|
|
{
|
|
/**
|
|
* Count the total articles eligible for batch generation.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function count(): void
|
|
{
|
|
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(*)')
|
|
->from($db->quoteName('#__content', 'c'))
|
|
->leftJoin(
|
|
$db->quoteName('#__mokoog_tags', 't')
|
|
. ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content')
|
|
. ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id')
|
|
)
|
|
->where($db->quoteName('c.state') . ' = 1')
|
|
->where($db->quoteName('t.id') . ' IS NULL');
|
|
|
|
$db->setQuery($query);
|
|
$total = (int) $db->loadResult();
|
|
|
|
echo new JsonResponse(['total' => $total]);
|
|
|
|
Factory::getApplication()->close();
|
|
}
|
|
|
|
/**
|
|
* Process a chunk of articles for batch OG generation.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function process(): void
|
|
{
|
|
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();
|
|
$limit = min($app->getInput()->getInt('limit', 50), 200);
|
|
|
|
$db = Factory::getDbo();
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName([
|
|
'c.id', 'c.title', 'c.metadesc', 'c.introtext', 'c.fulltext', 'c.images',
|
|
]))
|
|
->from($db->quoteName('#__content', 'c'))
|
|
->leftJoin(
|
|
$db->quoteName('#__mokoog_tags', 't')
|
|
. ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content')
|
|
. ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id')
|
|
)
|
|
->where($db->quoteName('c.state') . ' = 1')
|
|
->where($db->quoteName('t.id') . ' IS NULL')
|
|
->order($db->quoteName('c.id') . ' ASC');
|
|
|
|
// Always offset=0: processed articles now have #__mokoog_tags rows
|
|
// and are excluded by the LEFT JOIN ... IS NULL filter automatically.
|
|
$db->setQuery($query, 0, $limit);
|
|
$articles = $db->loadObjectList();
|
|
|
|
$created = 0;
|
|
$skipped = 0;
|
|
$now = Factory::getDate()->toSql();
|
|
|
|
foreach ($articles as $article) {
|
|
$ogTitle = $article->title;
|
|
$ogDescription = $this->extractDescription($article);
|
|
$ogImage = $this->extractImage($article);
|
|
|
|
$record = (object) [
|
|
'content_type' => 'com_content',
|
|
'content_id' => (int) $article->id,
|
|
'og_title' => $ogTitle,
|
|
'og_description' => $ogDescription,
|
|
'og_image' => $ogImage,
|
|
'og_type' => 'article',
|
|
'seo_title' => '',
|
|
'meta_description' => $article->metadesc ?: '',
|
|
'robots' => '',
|
|
'canonical_url' => '',
|
|
'language' => '*',
|
|
'published' => 1,
|
|
'created' => $now,
|
|
'modified' => $now,
|
|
];
|
|
|
|
try {
|
|
$db->insertObject('#__mokoog_tags', $record);
|
|
$created++;
|
|
} catch (\RuntimeException $e) {
|
|
$skipped++;
|
|
}
|
|
}
|
|
|
|
echo new JsonResponse([
|
|
'created' => $created,
|
|
]);
|
|
|
|
$app->close();
|
|
}
|
|
|
|
/**
|
|
* Extract a description from article content.
|
|
*
|
|
* @param object $article Article record
|
|
*
|
|
* @return string
|
|
*/
|
|
private function extractDescription(object $article): string
|
|
{
|
|
// Prefer meta description if set
|
|
if (!empty($article->metadesc)) {
|
|
return $article->metadesc;
|
|
}
|
|
|
|
// Fall back to intro text
|
|
$text = $article->introtext ?: $article->fulltext;
|
|
$text = strip_tags($text);
|
|
$text = trim(preg_replace('/\s+/', ' ', $text));
|
|
|
|
if (mb_strlen($text) > 160) {
|
|
$text = mb_substr($text, 0, 157) . '...';
|
|
}
|
|
|
|
return $text;
|
|
}
|
|
|
|
/**
|
|
* Extract the best image from article data.
|
|
*
|
|
* @param object $article Article record
|
|
*
|
|
* @return string
|
|
*/
|
|
private function extractImage(object $article): string
|
|
{
|
|
if (!empty($article->images)) {
|
|
$images = json_decode($article->images, true);
|
|
|
|
if (!empty($images['image_fulltext'])) {
|
|
return $images['image_fulltext'];
|
|
}
|
|
|
|
if (!empty($images['image_intro'])) {
|
|
return $images['image_intro'];
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
}
|