* @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\AdminController; use Joomla\CMS\Router\Route; class PostsController extends AdminController { public function getModel($name = 'Post', $prefix = 'Administrator', $config = ['ignore_request' => true]) { return parent::getModel($name, $prefix, $config); } /** * Schedule selected posts for a future date/time. * * @return void */ public function schedule(): void { $this->checkToken(); $ids = $this->input->get('cid', [], 'array'); $scheduledAt = $this->input->getString('scheduled_at', ''); if (empty($ids)) { $this->setRedirect( Route::_('index.php?option=com_mokosuitecross&view=posts', false), Text::_('COM_MOKOSUITECROSS_POSTS_NO_ITEM_SELECTED'), 'warning' ); return; } if (empty($scheduledAt)) { $this->setRedirect( Route::_('index.php?option=com_mokosuitecross&view=posts', false), Text::_('COM_MOKOSUITECROSS_SCHEDULE_NO_DATE'), 'warning' ); return; } try { $scheduledDate = Factory::getDate($scheduledAt); $scheduledAt = $scheduledDate->toSql(); } catch (\Throwable $e) { $this->setRedirect( Route::_('index.php?option=com_mokosuitecross&view=posts', false), Text::_('COM_MOKOSUITECROSS_SCHEDULE_INVALID_DATE'), 'error' ); return; } $db = Factory::getDbo(); $now = Factory::getDate()->toSql(); foreach ($ids as $id) { $query = $db->getQuery(true) ->update($db->quoteName('#__mokosuitecross_posts')) ->set($db->quoteName('scheduled_at') . ' = ' . $db->quote($scheduledAt)) ->set($db->quoteName('status') . ' = ' . $db->quote('queued')) ->set($db->quoteName('modified') . ' = ' . $db->quote($now)) ->where($db->quoteName('id') . ' = ' . (int) $id) ->where($db->quoteName('status') . ' IN (' . $db->quote('queued') . ',' . $db->quote('failed') . ',' . $db->quote('permanently_failed') . ',' . $db->quote('cancelled') . ')'); $db->setQuery($query); $db->execute(); } $this->setRedirect( Route::_('index.php?option=com_mokosuitecross&view=posts', false), Text::sprintf('COM_MOKOSUITECROSS_POSTS_N_SCHEDULED', count($ids)), 'success' ); } /** * Retry selected failed/permanently_failed posts. * * @return void */ public function retrySelected(): void { $this->checkToken(); $ids = $this->input->get('cid', [], 'array'); if (empty($ids)) { $this->setRedirect( Route::_('index.php?option=com_mokosuitecross&view=posts', false), Text::_('COM_MOKOSUITECROSS_POSTS_NO_ITEM_SELECTED'), 'warning' ); return; } $count = \Joomla\Component\MokoSuiteCross\Administrator\Helper\QueueProcessor::retryPosts($ids); $this->setRedirect( Route::_('index.php?option=com_mokosuitecross&view=posts', false), Text::sprintf('COM_MOKOSUITECROSS_POSTS_N_RETRIED', $count), 'success' ); } /** * Re-queue all failed posts by resetting their status to queued and retry count to 0. * * @return void */ public function retryFailed(): void { $this->checkToken(); $db = Factory::getDbo(); $query = $db->getQuery(true) ->update($db->quoteName('#__mokosuitecross_posts')) ->set($db->quoteName('status') . ' = ' . $db->quote('queued')) ->set($db->quoteName('retry_count') . ' = 0') ->set($db->quoteName('error_message') . ' = ' . $db->quote('')) ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) ->where($db->quoteName('status') . ' IN (' . $db->quote('failed') . ',' . $db->quote('permanently_failed') . ',' . $db->quote('cancelled') . ')'); $db->setQuery($query); $db->execute(); $count = $db->getAffectedRows(); $this->setRedirect( Route::_('index.php?option=com_mokosuitecross&view=posts', false), Text::plural('COM_MOKOSUITECROSS_POSTS_N_RETRIED', $count), 'success' ); } /** * Export posts as CSV download. * * @return void */ public function exportCsv(): void { $this->checkToken('get'); if (!$this->app->getIdentity()->authorise('mokosuitecross.queue.export', 'com_mokosuitecross')) { throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); } $app = $this->app; $db = Factory::getDbo(); $query = $db->getQuery(true) ->select([ $db->quoteName('c.title', 'article_title'), 'CONCAT(' . $db->quoteName('s.title') . ', ' . $db->quote(' (') . ', ' . $db->quoteName('s.service_type') . ', ' . $db->quote(')') . ') AS service', $db->quoteName('a.status'), $db->quoteName('a.message'), $db->quoteName('a.posted_at'), $db->quoteName('a.error_message'), $db->quoteName('a.platform_post_id'), $db->quoteName('a.created'), ]) ->from($db->quoteName('#__mokosuitecross_posts', 'a')) ->join('LEFT', $db->quoteName('#__content', 'c') . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('a.article_id')) ->join('LEFT', $db->quoteName('#__mokosuitecross_services', 's') . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('a.service_id')) ->order($db->quoteName('a.created') . ' DESC'); // Apply current filters $status = $app->input->get('filter_status', '', 'string'); if (!empty($status)) { $query->where($db->quoteName('a.status') . ' = ' . $db->quote($status)); } $serviceId = $app->input->getInt('filter_service_id', 0); if (!empty($serviceId)) { $query->where($db->quoteName('a.service_id') . ' = ' . (int) $serviceId); } $search = $app->input->get('filter_search', '', 'string'); if (!empty($search)) { $search = '%' . $db->escape(trim($search), true) . '%'; $query->where('(' . $db->quoteName('c.title') . ' LIKE ' . $db->quote($search) . ' OR ' . $db->quoteName('a.message') . ' LIKE ' . $db->quote($search) . ')'); } $db->setQuery($query); $rows = $db->loadAssocList() ?: []; $filename = 'mokosuitecross-posts-' . Factory::getDate()->format('Y-m-d') . '.csv'; $app->setHeader('Content-Type', 'text/csv; charset=utf-8'); $app->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); $app->sendHeaders(); $fp = fopen('php://output', 'w'); fputcsv($fp, ['Article', 'Service', 'Status', 'Message', 'Posted At', 'Error', 'Platform Post ID', 'Created']); foreach ($rows as $row) { fputcsv($fp, $row); } fclose($fp); $app->close(); } /** * Purge (delete) all posts with status 'posted'. * * @return void */ public function purgePosted(): void { $this->checkToken(); $db = Factory::getDbo(); $query = $db->getQuery(true) ->delete($db->quoteName('#__mokosuitecross_posts')) ->where($db->quoteName('status') . ' = ' . $db->quote('posted')); $db->setQuery($query); $db->execute(); $count = $db->getAffectedRows(); $this->setRedirect( Route::_('index.php?option=com_mokosuitecross&view=posts', false), Text::plural('COM_MOKOSUITECROSS_POSTS_N_PURGED', $count), 'success' ); } }