d8376d6cdf
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
Universal: PR Check / Secret Scan (pull_request) Successful in 16s
Generic: Project CI / Lint & Validate (pull_request) Successful in 48s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 52s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 5s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
#95 — ACL + Options: - Add access.xml (core actions + mokoog.batch / mokoog.import custom actions) - Add config.xml (Permissions tab + note pointing settings to the system plugin) - Declare both in the manifest; Batch/ImportExport controllers now check the custom actions with a fallback to the prior core checks (no lockout) #103 — CSV import is now reachable: - Add an Import toolbar button that toggles a multipart file-upload form (jform[csv_file]) posting to importexport.import with a CSRF token #104 — Dead code + disk leak: - Delete unused ImageGenerator class and JsonLdBuilder::buildOrganization() - Add ImageHelper::pruneOldFiles() (deletes generated images older than 30d) and call it on content save so the generated-image cache is bounded #107 — Packaging: - Declare language/en-US in the component manifest (was never installed) - Remove undeclared empty stub dirs src/Field, src/Service
190 lines
6.2 KiB
PHP
190 lines
6.2 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') || throw new \RuntimeException(Text::_('JINVALID_TOKEN'), 403);
|
|
|
|
$identity = Factory::getApplication()->getIdentity();
|
|
|
|
if (!$identity->authorise('mokoog.batch', 'com_mokoog')
|
|
&& !$identity->authorise('core.create', 'com_mokoog')) {
|
|
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
|
}
|
|
|
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
|
$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') || throw new \RuntimeException(Text::_('JINVALID_TOKEN'), 403);
|
|
|
|
$identity = Factory::getApplication()->getIdentity();
|
|
|
|
if (!$identity->authorise('mokoog.batch', 'com_mokoog')
|
|
&& !$identity->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::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
|
$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++;
|
|
\Joomla\CMS\Log\Log::add('Batch insert failed for article ' . $article->id . ': ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokoog');
|
|
}
|
|
}
|
|
|
|
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 '';
|
|
}
|
|
}
|