27505f7501
Completes the MokoJoomCross → MokoSuiteCross rebrand across all language string keys, Joomla event names, documentation, and wiki pages. - 1,151 language key references renamed (COM_, PLG_, PKG_ prefixes) - Event names renamed (onMokoJoomCross* → onMokoSuiteCross*) - CLAUDE.md, CHANGELOG.md, wiki docs updated - Zero mokojoomcross references remaining in codebase Closes #128, closes #138
263 lines
8.7 KiB
PHP
263 lines
8.7 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoSuiteCross
|
|
* @subpackage com_mokosuitecross
|
|
* @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
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoSuiteCross\Administrator\Controller;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\MVC\Controller\BaseController;
|
|
use Joomla\CMS\Plugin\PluginHelper;
|
|
use Joomla\CMS\Uri\Uri;
|
|
use Joomla\Component\MokoSuiteCross\Administrator\Helper\CrossPostDispatcher;
|
|
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
|
|
|
|
/**
|
|
* REST API controller for dispatching cross-posts.
|
|
*
|
|
* Endpoint: POST /api/index.php/v1/mokosuitecross/dispatch
|
|
*
|
|
* JSON body:
|
|
* {
|
|
* "article_id": 123,
|
|
* "service_ids": [1, 2, 3] // optional — omit to post to all enabled services
|
|
* }
|
|
*
|
|
* Returns JSON with the created post IDs and status.
|
|
*
|
|
* Authentication is handled by Joomla's API application (token or session).
|
|
* The webservices plugin routes POST requests here via the API router.
|
|
*/
|
|
class DispatchController extends BaseController
|
|
{
|
|
/**
|
|
* Dispatch cross-posts for an article to one or more services.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function dispatch(): void
|
|
{
|
|
$app = $this->app;
|
|
|
|
// Enforce POST method — this is a state-changing action endpoint
|
|
if (strtoupper($this->input->getMethod()) !== 'POST') {
|
|
$this->sendJsonResponse(['error' => 'Method not allowed. Use POST.'], 405);
|
|
|
|
return;
|
|
}
|
|
|
|
// ACL check — require core.manage on the component
|
|
if (!Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
|
|
$this->sendJsonResponse(['error' => 'Forbidden'], 403);
|
|
|
|
return;
|
|
}
|
|
|
|
// Read JSON body
|
|
$input = json_decode(file_get_contents('php://input'), true) ?: [];
|
|
$articleId = (int) ($input['article_id'] ?? 0);
|
|
$serviceIds = $input['service_ids'] ?? null;
|
|
|
|
if ($articleId < 1) {
|
|
$this->sendJsonResponse(['error' => Text::_('COM_MOKOSUITECROSS_DISPATCH_MISSING_ARTICLE')], 400);
|
|
|
|
return;
|
|
}
|
|
|
|
// Validate service_ids if provided
|
|
if ($serviceIds !== null) {
|
|
if (!is_array($serviceIds) || empty($serviceIds)) {
|
|
$this->sendJsonResponse(['error' => Text::_('COM_MOKOSUITECROSS_DISPATCH_INVALID_SERVICES')], 400);
|
|
|
|
return;
|
|
}
|
|
|
|
$serviceIds = array_map('intval', $serviceIds);
|
|
}
|
|
|
|
$db = Factory::getDbo();
|
|
|
|
// Load the article
|
|
$query = $db->getQuery(true)
|
|
->select('*')
|
|
->from($db->quoteName('#__content'))
|
|
->where($db->quoteName('id') . ' = ' . $articleId);
|
|
|
|
$db->setQuery($query);
|
|
$article = $db->loadObject();
|
|
|
|
if (!$article) {
|
|
$this->sendJsonResponse(['error' => Text::_('COM_MOKOSUITECROSS_DISPATCH_ARTICLE_NOT_FOUND')], 404);
|
|
|
|
return;
|
|
}
|
|
|
|
// Load enabled services, optionally filtered by service_ids
|
|
$query = $db->getQuery(true)
|
|
->select('*')
|
|
->from($db->quoteName('#__mokosuitecross_services'))
|
|
->where($db->quoteName('published') . ' = 1')
|
|
->order($db->quoteName('ordering') . ' ASC');
|
|
|
|
if ($serviceIds !== null) {
|
|
$query->where($db->quoteName('id') . ' IN (' . implode(',', $serviceIds) . ')');
|
|
}
|
|
|
|
$db->setQuery($query);
|
|
$services = $db->loadObjectList() ?: [];
|
|
|
|
if (empty($services)) {
|
|
$this->sendJsonResponse(['error' => Text::_('COM_MOKOSUITECROSS_DISPATCH_NO_SERVICES')], 404);
|
|
|
|
return;
|
|
}
|
|
|
|
// Import service plugins and build type-to-plugin map.
|
|
// In Joomla 5+ with SubscriberInterface, plugins receive the Event object
|
|
// as their first argument. When they do $services[] = $this, they append to
|
|
// the Event via ArrayAccess at numeric indices starting at 1.
|
|
PluginHelper::importPlugin('mokosuitecross');
|
|
|
|
$servicePlugins = [];
|
|
$event = new \Joomla\Event\Event('onMokoSuiteCrossGetServices', [$servicePlugins]);
|
|
|
|
try {
|
|
$app->getDispatcher()->dispatch('onMokoSuiteCrossGetServices', $event);
|
|
} catch (\Throwable $e) {
|
|
// Dispatcher may not be available
|
|
}
|
|
|
|
// Read plugins back from the Event's ArrayAccess indices
|
|
$idx = 1;
|
|
|
|
while (isset($event[$idx])) {
|
|
$servicePlugins[] = $event[$idx];
|
|
$idx++;
|
|
}
|
|
|
|
$pluginMap = [];
|
|
|
|
foreach ($servicePlugins as $plugin) {
|
|
if ($plugin instanceof MokoSuiteCrossServiceInterface) {
|
|
$pluginMap[$plugin->getServiceType()] = $plugin;
|
|
}
|
|
}
|
|
|
|
// Create queue entries
|
|
$now = Factory::getDate()->toSql();
|
|
$createdIds = [];
|
|
$skipped = [];
|
|
|
|
// Build article URL
|
|
$articleUrl = Uri::root() . 'index.php?option=com_content&view=article&id=' . $article->id;
|
|
|
|
if (!empty($article->catid)) {
|
|
$articleUrl .= '&catid=' . $article->catid;
|
|
}
|
|
|
|
// Extract intro image for media
|
|
$media = [];
|
|
$images = json_decode($article->images ?? '{}');
|
|
|
|
if (!empty($images->image_intro)) {
|
|
$media[] = Uri::root() . ltrim($images->image_intro, '/');
|
|
}
|
|
|
|
foreach ($services as $service) {
|
|
// Duplicate guard — skip if article already posted/queued for this service
|
|
$query = $db->getQuery(true)
|
|
->select('COUNT(*)')
|
|
->from($db->quoteName('#__mokosuitecross_posts'))
|
|
->where($db->quoteName('article_id') . ' = ' . (int) $article->id)
|
|
->where($db->quoteName('service_id') . ' = ' . (int) $service->id)
|
|
->where($db->quoteName('status') . ' IN (' . $db->quote('queued') . ',' . $db->quote('posted') . ',' . $db->quote('posting') . ')');
|
|
|
|
$db->setQuery($query);
|
|
|
|
if ((int) $db->loadResult() > 0) {
|
|
$skipped[] = [
|
|
'service_id' => (int) $service->id,
|
|
'service_type' => $service->service_type,
|
|
'reason' => 'duplicate',
|
|
];
|
|
|
|
continue;
|
|
}
|
|
|
|
// Render template via shared dispatcher logic
|
|
$message = CrossPostDispatcher::renderTemplate($article, $service);
|
|
|
|
// Create queue entry
|
|
$post = (object) [
|
|
'article_id' => (int) $article->id,
|
|
'service_id' => (int) $service->id,
|
|
'status' => 'queued',
|
|
'message' => $message,
|
|
'platform_post_id' => '',
|
|
'platform_response' => '',
|
|
'error_message' => '',
|
|
'retry_count' => 0,
|
|
'created' => $now,
|
|
'modified' => $now,
|
|
];
|
|
|
|
$db->insertObject('#__mokosuitecross_posts', $post);
|
|
$postId = (int) $db->insertid();
|
|
|
|
$createdIds[] = [
|
|
'post_id' => $postId,
|
|
'service_id' => (int) $service->id,
|
|
'service_type' => $service->service_type,
|
|
'status' => 'queued',
|
|
];
|
|
|
|
// Write log entry
|
|
$log = (object) [
|
|
'post_id' => $postId,
|
|
'service_id' => (int) $service->id,
|
|
'level' => 'info',
|
|
'message' => sprintf('API dispatch: queued article %d to %s', $article->id, $service->service_type),
|
|
'context' => '{}',
|
|
'created' => $now,
|
|
];
|
|
|
|
$db->insertObject('#__mokosuitecross_logs', $log);
|
|
}
|
|
|
|
$this->sendJsonResponse([
|
|
'article_id' => (int) $article->id,
|
|
'dispatched' => $createdIds,
|
|
'skipped' => $skipped,
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Send a JSON response and close the application.
|
|
*
|
|
* @param array $data Response data
|
|
* @param int $httpCode HTTP status code
|
|
*
|
|
* @return void
|
|
*/
|
|
private function sendJsonResponse(array $data, int $httpCode): void
|
|
{
|
|
$app = $this->app;
|
|
|
|
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
$app->setHeader('Status', (string) $httpCode);
|
|
|
|
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
$app->close();
|
|
}
|
|
}
|