* @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 ''; } }