queueManager = $queueManager ?? new ContaiQueueManager(); } public function handleRequest(): void { if (!$this->isPostRequest()) { return; } if (!$this->verifyNonce()) { $this->redirectWithError(__('Security verification failed. Please try again.', '1platform-content-ai')); return; } if (!$this->verifyCapability()) { $this->redirectWithError(__('You do not have permission to perform this action.', '1platform-content-ai')); return; } $action = $this->getRequestedAction(); if (!$action) { return; } $this->executeCommand($action); } private function isPostRequest(): bool { return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST'; } private function verifyNonce(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash return isset($_POST[self::NONCE_FIELD]) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST[self::NONCE_FIELD])), self::NONCE_ACTION); } private function verifyCapability(): bool { return current_user_can('manage_options'); } private function getRequestedAction(): ?string { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in handleRequest() via verifyNonce(). if (isset($_POST['contai_enqueue_posts'])) { return self::ACTION_ENQUEUE; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in handleRequest() via verifyNonce(). if (isset($_POST['contai_clear_queue'])) { return self::ACTION_CLEAR; } return null; } private function executeCommand(string $action): void { try { switch ($action) { case self::ACTION_ENQUEUE: $result = $this->enqueuePosts(); break; case self::ACTION_CLEAR: $result = $this->clearQueue(); break; default: $this->redirectWithError(__('Invalid action.', '1platform-content-ai')); return; } if ($result['success']) { $this->redirectWithSuccess($result['message']); } else { $this->redirectWithError($result['message']); } } catch (Exception $e) { $this->redirectWithError($e->getMessage()); } } private function enqueuePosts(): array { // Validate credits before enqueueing posts require_once __DIR__ . '/../../../services/billing/CreditGuard.php'; $creditGuard = new ContaiCreditGuard(); $creditCheck = $creditGuard->validateCredits(); if (!$creditCheck['has_credits']) { return [ 'success' => false, 'message' => $creditCheck['message'] ]; } $validation = $this->validateEnqueueRequest(); if (!$validation['valid']) { return [ 'success' => false, 'message' => $validation['message'] ]; } $count = $validation['count']; $config = [ 'lang' => $validation['lang'], 'country' => $validation['country'], 'image_provider' => $validation['image_provider'], ]; $enqueuedCount = $this->queueManager->enqueuePostGeneration($count, $config); return [ 'success' => true, 'message' => sprintf( /* translators: %d: number of posts enqueued for generation */ _n( 'Successfully enqueued %d post for generation.', 'Successfully enqueued %d posts for generation.', $enqueuedCount, '1platform-content-ai' ), $enqueuedCount ) ]; } private function validateEnqueueRequest(): array { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handleRequest() via verifyNonce(). $count = isset($_POST['post_count']) ? absint(wp_unslash($_POST['post_count'])) : 0; $lang = sanitize_text_field(wp_unslash($_POST['content_lang'] ?? '')); $country = sanitize_text_field(wp_unslash($_POST['content_country'] ?? '')); $imageProvider = sanitize_text_field(wp_unslash($_POST['image_provider'] ?? '')); // phpcs:enable WordPress.Security.NonceVerification.Missing if ($count < 1 || $count > 100) { return [ 'valid' => false, 'message' => __('Number of posts must be between 1 and 100.', '1platform-content-ai') ]; } if (empty($lang) || empty($country)) { return [ 'valid' => false, 'message' => __('Language and country are required.', '1platform-content-ai') ]; } $validProviders = ['pexels', 'pixabay']; if (!in_array($imageProvider, $validProviders, true)) { return [ 'valid' => false, 'message' => __('Invalid image provider selected.', '1platform-content-ai') ]; } return [ 'valid' => true, 'count' => $count, 'lang' => $lang, 'country' => $country, 'image_provider' => $imageProvider ]; } private function clearQueue(): array { $deletedCount = $this->queueManager->clearAllJobs(); return [ 'success' => true, 'message' => sprintf( /* translators: %d: number of jobs cleared from the queue */ _n( 'Successfully cleared %d job from the queue.', 'Successfully cleared %d jobs from the queue.', $deletedCount, '1platform-content-ai' ), $deletedCount ) ]; } private function redirectWithSuccess(string $message): void { $this->redirect([ 'success' => 1, 'message' => $message ]); } private function redirectWithError(string $message): void { $this->redirect([ 'error' => 1, 'message' => $message ]); } private function redirect(array $params): void { $url = add_query_arg( array_merge( [ 'page' => 'contai-content-generator', 'section' => 'post-generator' ], $params ), admin_url('admin.php') ); wp_safe_redirect($url); exit; } /** * Resolve the credit status badge for a queue-table row. * * Returns an array with `label` (translated text), `tone` ('info'/'success'/ * 'warning'/'neutral'), or `null` when no badge should be displayed. * * The mapping mirrors the Authorize+Capture lifecycle: * - hold_id present + status pending/processing → Reserved (info) * - credits_released = 1 → Refunded (warning) * - hold_id present + status done → Charged (success) * - no hold_id + status done → Charged (success, legacy path) * * @param array $row Raw row from JobRepository::getProcessingJobsWithKeywords() * or any other listing that exposes status, hold_id, credits_released. * @return array{label: string, tone: string}|null */ public static function resolveCreditStatusBadge(array $row): ?array { $status = (string) ($row['status'] ?? ''); $hold_id = isset($row['hold_id']) ? (string) $row['hold_id'] : ''; $credits_released = !empty($row['credits_released']); if ($credits_released) { return [ 'label' => __('Refunded', '1platform-content-ai'), 'tone' => 'warning', ]; } if ($hold_id !== '' && in_array($status, ['pending', 'processing'], true)) { return [ 'label' => __('Reserved', '1platform-content-ai'), 'tone' => 'info', ]; } if ($status === 'done') { return [ 'label' => __('Charged', '1platform-content-ai'), 'tone' => 'success', ]; } return null; } }