fix: resolve 5 bugs found during code assessment
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Universal: Security Audit / Dependency Audit (pull_request) Successful in 4s
Joomla: Extension CI / Lint & Validate (pull_request) Successful in 9s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled

- fix(batch): use offset=0 for self-consuming LEFT JOIN query that
  excludes already-processed articles, preventing chunk skips
- fix(license): move session flag after DB query succeeds so a failed
  check retries on next page load instead of silently giving up
- fix(og:image): detect actual image dimensions via getimagesize()
  instead of hardcoding 1200x630 which was wrong for unresized,
  small, or external images
- fix(i18n): use mb_strlen() consistently with mb_substr() for
  multibyte-safe description truncation across all 4 call sites
- fix(ImageGenerator): guard wrapText truncation when third line is
  shorter than 3 characters to prevent broken output

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-06-06 07:12:40 -05:00
parent 8fe8469287
commit 252d75c44f
6 changed files with 135 additions and 17 deletions
@@ -52,7 +52,7 @@ class HikaShopAdapter implements ContentTypeInterface
$text = strip_tags($text);
$text = trim(preg_replace('/\s+/', ' ', $text));
if (\strlen($text) > 160) {
if (mb_strlen($text) > 160) {
$text = mb_substr($text, 0, 157) . '...';
}
@@ -52,7 +52,7 @@ class K2Adapter implements ContentTypeInterface
$text = strip_tags($text);
$text = trim(preg_replace('/\s+/', ' ', $text));
if (\strlen($text) > 160) {
if (mb_strlen($text) > 160) {
$text = mb_substr($text, 0, 157) . '...';
}
@@ -67,7 +67,6 @@ class BatchController extends BaseController
}
$app = Factory::getApplication();
$offset = $app->getInput()->getInt('offset', 0);
$limit = $app->getInput()->getInt('limit', 50);
$db = Factory::getDbo();
@@ -85,7 +84,9 @@ class BatchController extends BaseController
->where($db->quoteName('t.id') . ' IS NULL')
->order($db->quoteName('c.id') . ' ASC');
$db->setQuery($query, $offset, $limit);
// 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;
@@ -118,8 +119,6 @@ class BatchController extends BaseController
echo new JsonResponse([
'created' => $created,
'offset' => $offset,
'processed' => $offset + $created,
]);
$app->close();
@@ -144,7 +143,7 @@ class BatchController extends BaseController
$text = strip_tags($text);
$text = trim(preg_replace('/\s+/', ' ', $text));
if (\strlen($text) > 160) {
if (mb_strlen($text) > 160) {
$text = mb_substr($text, 0, 157) . '...';
}
@@ -215,22 +215,23 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
function processChunk(offset, total, chunkSize, token, bar, status) {
fetch('index.php?option=com_mokoog&task=batch.process&format=json&offset=' + offset + '&limit=' + chunkSize + '&' + token + '=1')
function processChunk(processed, total, chunkSize, token, bar, status) {
// Always offset=0: processed items are excluded by the IS NULL filter
fetch('index.php?option=com_mokoog&task=batch.process&format=json&limit=' + chunkSize + '&' + token + '=1')
.then(function(r) { return r.json(); })
.then(function(resp) {
var processed = resp.data.processed;
processed += resp.data.created;
var pct = Math.min(100, Math.round((processed / total) * 100));
bar.style.width = pct + '%';
bar.textContent = pct + '%';
status.textContent = processed + ' / ' + total + ' <?php echo Text::_('COM_MOKOOG_BATCH_PROCESSED', true); ?>';
if (processed < total) {
if (resp.data.created > 0 && processed < total) {
processChunk(processed, total, chunkSize, token, bar, status);
} else {
bar.classList.remove('progress-bar-animated');
bar.classList.add('bg-success');
status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_COMPLETE', true); ?> ' + total + ' articles.';
status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_COMPLETE', true); ?> ' + processed + ' articles.';
setTimeout(function() { location.reload(); }, 2000);
}
})
@@ -35,10 +35,27 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
public static function getSubscribedEvents(): array
{
return [
'onAfterRoute' => 'onAfterRoute',
'onBeforeCompileHead' => 'onBeforeCompileHead',
];
}
/**
* Run admin-side license key check after routing.
*
* @param Event $event The event object
*
* @return void
*/
public function onAfterRoute(Event $event): void
{
$app = $this->getApplication();
if ($app->isClient('administrator')) {
$this->warnMissingLicenseKey();
}
}
/**
* Inject Open Graph and Twitter Card meta tags before the document head is compiled.
*
@@ -108,9 +125,13 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
$imageUrl = $this->resolveImageUrl($image);
$doc->setMetaData('og:image', $imageUrl, 'property');
// Image dimensions help Facebook, LinkedIn, and Discord render previews faster
$doc->setMetaData('og:image:width', '1200', 'property');
$doc->setMetaData('og:image:height', '630', 'property');
// Emit actual image dimensions when detectable
$imageDims = $this->getImageDimensions($image);
if ($imageDims) {
$doc->setMetaData('og:image:width', (string) $imageDims[0], 'property');
$doc->setMetaData('og:image:height', (string) $imageDims[1], 'property');
}
}
// og:locale from current language
@@ -355,7 +376,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
$description = trim(preg_replace('/\s+/', ' ', $description));
if (\strlen($description) > $maxLength) {
if (mb_strlen($description) > $maxLength) {
$description = mb_substr($description, 0, $maxLength - 3) . '...';
}
@@ -487,4 +508,96 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
return $db->loadResult() ?: '';
}
/**
* Warn administrators once per session when no license key is configured.
*
* @return void
*/
private function warnMissingLicenseKey(): void
{
$session = Factory::getSession();
if ($session->get('mokoog.license_warned', false)) {
return;
}
$user = Factory::getUser();
if ($user->guest || !$user->authorise('core.manage')) {
return;
}
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('extra_query'))
->from($db->quoteName('#__update_sites'))
->where($db->quoteName('name') . ' = ' . $db->quote('MokoJoomOpenGraph Updates'))
->setLimit(1);
$db->setQuery($query);
$extraQuery = (string) $db->loadResult();
// Mark as checked only after the DB query succeeds
$session->set('mokoog.license_warned', true);
if (!empty($extraQuery)) {
parse_str($extraQuery, $parsed);
if (!empty($parsed['dlid']) && preg_match('/^MOKO-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/', $parsed['dlid'])) {
return;
}
}
$this->getApplication()->enqueueMessage(
'<strong>Moko Consulting License Key Required</strong> — '
. 'No download key is configured. Updates will not be available until a valid license key is entered. '
. 'Go to <a href="index.php?option=com_installer&view=updatesites">System → Update Sites</a> '
. 'and enter your license key (<code>MOKO-XXXX-XXXX-XXXX-XXXX</code>) in the Download Key field '
. 'for the MokoJoomOpenGraph update site.',
'warning'
);
} catch (\Throwable $e) {
// Don't break admin over a license check
}
}
/**
* Get the actual pixel dimensions of a local image.
*
* Returns [width, height] or null for external URLs or unreadable images.
*
* @param string $image Image path (relative or absolute URL)
*
* @return array{0: int, 1: int}|null
*/
private function getImageDimensions(string $image): ?array
{
// Cannot determine dimensions for external URLs
if (str_starts_with($image, 'http://') || str_starts_with($image, 'https://')) {
return null;
}
// If auto-resize is on, the resized image lives in the generated dir
if ($this->params->get('auto_resize', 1)) {
$resolved = ImageHelper::resize($image);
} else {
$resolved = $image;
}
$absPath = JPATH_ROOT . '/' . ltrim($resolved, '/');
if (!is_file($absPath)) {
return null;
}
$info = @getimagesize($absPath);
if (!$info) {
return null;
}
return [$info[0], $info[1]];
}
}
@@ -152,7 +152,12 @@ class ImageGenerator
// Limit to 3 lines, truncate last line if needed
if (\count($lines) > 3) {
$lines = \array_slice($lines, 0, 3);
$lines[2] = mb_substr($lines[2], 0, -3) . '...';
if (mb_strlen($lines[2]) > 3) {
$lines[2] = mb_substr($lines[2], 0, -3) . '...';
} else {
$lines[2] .= '...';
}
}
return implode("\n", $lines);