Your IP : 216.73.217.79


Current Path : /var/www/v3.cesa.co.za/src/ContentBundle/Controller/
Upload File :
Current File : /var/www/v3.cesa.co.za/src/ContentBundle/Controller/ExternalFormApiController.php

<?php

namespace App\ContentBundle\Controller;

use App\CoreBundle\Services\ServiceGetterManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ExternalFormApiController extends AbstractController
{
    private function logInfo(ServiceGetterManager $sg, string $message): void
    {
        try {
            $sg->get('error_log.manager')->info('[ExternalFormApiController] ' . $message);
        } catch (\Throwable $ex) {
            error_log('[ExternalFormApiController] ' . $message . ' (log fallback: ' . $ex->getMessage() . ')');
        }
    }

    private function logError(ServiceGetterManager $sg, string $message): void
    {
        try {
            $sg->get('error_log.manager')->error('[ExternalFormApiController] ' . $message, false, false, false);
        } catch (\Throwable $ex) {
            error_log('[ExternalFormApiController][ERROR] ' . $message . ' (log fallback: ' . $ex->getMessage() . ')');
        }
    }

    /**
     * Build a detailed, operator-facing error string from an exception (IP-whitelisted API only).
     */
    private function formatThrowableForApi(string $context, \Throwable $ex): string
    {
        $lines = array();
        $lines[] = $context . ': ' . trim($ex->getMessage());
        $lines[] = 'Exception: ' . get_class($ex);
        $lines[] = 'Location: ' . basename($ex->getFile()) . ':' . $ex->getLine();

        $previous = $ex->getPrevious();
        $depth = 0;
        while ($previous instanceof \Throwable && $depth < 5) {
            $lines[] = 'Caused by [' . ($depth + 1) . ']: ' . trim($previous->getMessage())
                . ' (' . get_class($previous) . ' at ' . basename($previous->getFile()) . ':' . $previous->getLine() . ')';
            $previous = $previous->getPrevious();
            $depth++;
        }

        return implode("\n", $lines);
    }

    private function getAllowedIpsCsv(Request $request): string
    {
        return (string) (
            $_ENV['CESA_MAIN_ALLOWED_IPS']
            ?? $_SERVER['CESA_MAIN_ALLOWED_IPS']
            ?? getenv('CESA_MAIN_ALLOWED_IPS')
            ?? ''
        );
    }

    private function isAuthorisedCaller(Request $request, string $allowedIpsCsv): bool
    {
        if (strlen(trim($allowedIpsCsv)) <= 0) {
            return false;
        }

        $allowedIps = array();
        foreach (explode(',', $allowedIpsCsv) as $ip) {
            $ip = trim($ip);
            if ($ip !== '') {
                $allowedIps[] = $ip;
            }
        }

        $remoteAddr = (string) $request->server->get('REMOTE_ADDR', '');
        $forwardedFor = (string) $request->server->get('HTTP_X_FORWARDED_FOR', '');
        $candidateIps = array();

        if ($forwardedFor !== '') {
            foreach (explode(',', $forwardedFor) as $forwardedIp) {
                $forwardedIp = trim($forwardedIp);
                if ($forwardedIp !== '') {
                    $candidateIps[] = $forwardedIp;
                }
            }
        }

        if ($remoteAddr !== '') {
            $candidateIps[] = $remoteAddr;
        }

        foreach ($candidateIps as $candidateIp) {
            if (in_array($candidateIp, $allowedIps, true)) {
                return true;
            }
        }

        return false;
    }

    #[Route('/api/external/forms/reports', name: 'content_external_form_reports_list', methods: ['GET'])]
    public function listFormReports(Request $request, ServiceGetterManager $sg): JsonResponse
    {
        $allowedIpsCsv = $this->getAllowedIpsCsv($request);
        if (strlen(trim($allowedIpsCsv)) <= 0 || !$this->isAuthorisedCaller($request, $allowedIpsCsv)) {
            return new JsonResponse(array('success' => false, 'error' => 'Forbidden caller IP.'), 403);
        }

        $data = $sg->get('form_manager.manager')->getExternalReportsFormsList();

        return new JsonResponse(
            array(
                'success' => true,
                'forms' => $data,
            ),
            200
        );
    }

    #[Route('/api/external/forms/{formId}/report', name: 'content_external_form_report', methods: ['GET'])]
    public function getFormReport(Request $request, ServiceGetterManager $sg, $formId): JsonResponse
    {
        $allowedIpsCsv = $this->getAllowedIpsCsv($request);
        if (strlen(trim($allowedIpsCsv)) <= 0 || !$this->isAuthorisedCaller($request, $allowedIpsCsv)) {
            return new JsonResponse(array('success' => false, 'error' => 'Forbidden caller IP.'), 403);
        }

        $form = $sg->get('form_manager.manager')->find($formId);
        if (!is_object($form)) {
            return new JsonResponse(array('success' => false, 'error' => 'Form not found'), 404);
        }

        try {
            $formCompletedReport = $sg->get('form_completed.manager')->getFormReportData($formId);
            $reportHtml = $this->renderView(
                '@ContentBundle/Form/report.content.html.twig',
                array('formCompletedReport' => $formCompletedReport)
            );

            // External callers (cesa-main) only need HTML; omit raw report data (contains entities/non-JSON-safe values).
            $jsonResponse = new JsonResponse(
                array(
                    'success' => true,
                    'form_id' => $form->getId(),
                    'title' => $form->getTitle(),
                    'report_html' => $reportHtml,
                ),
                200
            );
            if (method_exists($jsonResponse, 'setEncodingOptions')) {
                $jsonResponse->setEncodingOptions(JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
            }

            return $jsonResponse;
        } catch (\Throwable $ex) {
            $detail = $this->formatThrowableForApi('Report generation failed', $ex);
            $this->logError($sg, 'getFormReport failed for form ' . $formId . ': ' . str_replace("\n", ' | ', $detail));

            return new JsonResponse(
                array(
                    'success' => false,
                    'error' => $detail,
                ),
                500
            );
        }
    }

    #[Route('/api/external/forms/{formId}/report/export.xlsx', name: 'content_external_form_report_export', methods: ['GET'])]
    public function exportFormReportExcel(Request $request, ServiceGetterManager $sg, $formId): Response
    {
        $allowedIpsCsv = $this->getAllowedIpsCsv($request);
        if (strlen(trim($allowedIpsCsv)) <= 0 || !$this->isAuthorisedCaller($request, $allowedIpsCsv)) {
            return new JsonResponse(array('success' => false, 'error' => 'Forbidden caller IP.'), 403);
        }

        $form = $sg->get('form_manager.manager')->find($formId);
        if (!is_object($form)) {
            return new JsonResponse(array('success' => false, 'error' => 'Form not found'), 404);
        }

        try {
            $binary = $sg->get('form_completed.manager')->generateFormReportExcel($formId);
        } catch (\InvalidArgumentException $ex) {
            return new JsonResponse(array('success' => false, 'error' => $ex->getMessage()), 404);
        } catch (\Throwable $ex) {
            $detail = $this->formatThrowableForApi('Export failed', $ex);
            $this->logError($sg, 'exportFormReportExcel failed for form ' . $formId . ': ' . str_replace("\n", ' | ', $detail));

            return new JsonResponse(array('success' => false, 'error' => $detail), 500);
        }

        $safeTitle = preg_replace('/[^a-zA-Z0-9_-]+/', '-', (string) $form->getTitle());
        $safeTitle = trim($safeTitle, '-');
        if ($safeTitle === '') {
            $safeTitle = 'form-report';
        }
        $filename = 'form-report-' . $safeTitle . '.xlsx';

        return new Response(
            $binary,
            Response::HTTP_OK,
            array(
                'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                'Content-Disposition' => 'attachment; filename="' . $filename . '"',
            )
        );
    }

    #[Route('/api/external/forms/{formId}/copy', name: 'content_external_form_copy', methods: ['POST'])]
    public function copyForm(Request $request, ServiceGetterManager $sg, $formId): JsonResponse
    {
        $this->logInfo(
            $sg,
            'copyForm start; formId=' . strval($formId)
            . '; remote=' . strval($request->server->get('REMOTE_ADDR', ''))
            . '; xff=' . strval($request->server->get('HTTP_X_FORWARDED_FOR', ''))
        );

        $allowedIpsCsv = $this->getAllowedIpsCsv($request);
        if (strlen(trim($allowedIpsCsv)) <= 0) {
            $this->logError($sg, 'copyForm denied; whitelist env missing');
            return new JsonResponse(
                array('success' => false, 'error' => 'API IP whitelist is not configured.'),
                403
            );
        }
        if (!$this->isAuthorisedCaller($request, $allowedIpsCsv)) {
            $this->logError($sg, 'copyForm denied; forbidden caller IP=' . strval($request->server->get('REMOTE_ADDR', '')));
            return new JsonResponse(
                array(
                    'success' => false,
                    'error' => 'Forbidden caller IP.',
                    'remote_ip' => (string) $request->server->get('REMOTE_ADDR', '')
                ),
                403
            );
        }

        $payload = json_decode($request->getContent(), true);
        if (!is_array($payload)) {
            $payload = array();
        }

        $newTitle = '';
        if (isset($payload['title'])) {
            $newTitle = trim((string) $payload['title']);
        }

        if ($newTitle === '') {
            return new JsonResponse(
                array('success' => false, 'error' => 'Missing required field: title'),
                400
            );
        }

        $assignmentId = null;
        if (isset($payload['assignment_id'])) {
            $assignmentId = intval($payload['assignment_id']);
            if ($assignmentId <= 0) {
                $assignmentId = null;
            }
        }

        $form = $sg->get('form_manager.manager')->find($formId);
        if (!is_object($form)) {
            $this->logError($sg, 'copyForm failed; form not found for formId=' . strval($formId));
            return new JsonResponse(
                array('success' => false, 'error' => 'Form not found'),
                404
            );
        }

        $copyResult = $sg->get('form_manager.manager')->copyOrGetForAssignment($form, $newTitle, $assignmentId);
        $newForm = $copyResult['form'] ?? null;
        $alreadyCopied = !empty($copyResult['already_copied']);

        if (!is_object($newForm)) {
            $this->logError($sg, 'copyForm failed; copyOrGetForAssignment returned no form for formId=' . strval($formId));
            return new JsonResponse(
                array('success' => false, 'error' => 'Unable to copy form'),
                500
            );
        }

        $this->logInfo(
            $sg,
            'copyForm success; source=' . $form->getId()
            . '; copied=' . $newForm->getId()
            . '; already_copied=' . ($alreadyCopied ? '1' : '0')
        );

        return new JsonResponse(
            array(
                'success' => true,
                'source_form_id' => $form->getId(),
                'copied_form_id' => $newForm->getId(),
                'title' => $newForm->getTitle(),
                'already_copied' => $alreadyCopied,
            ),
            ($alreadyCopied ? 200 : 201)
        );
    }

    #[Route('/api/external/forms/{formId}/render', name: 'content_external_form_render', methods: ['GET'])]
    public function renderExternalForm(Request $request, ServiceGetterManager $sg, $formId): JsonResponse
    {
        $this->logInfo(
            $sg,
            'renderExternalForm start; formId=' . strval($formId)
            . '; remote=' . strval($request->server->get('REMOTE_ADDR', ''))
            . '; xff=' . strval($request->server->get('HTTP_X_FORWARDED_FOR', ''))
        );

        $allowedIpsCsv = $this->getAllowedIpsCsv($request);
        if (strlen(trim($allowedIpsCsv)) <= 0) {
            $this->logError($sg, 'renderExternalForm denied; whitelist env missing');
            return new JsonResponse(
                array('success' => false, 'error' => 'API IP whitelist is not configured.'),
                403
            );
        }
        if (!$this->isAuthorisedCaller($request, $allowedIpsCsv)) {
            $this->logError($sg, 'renderExternalForm denied; forbidden caller IP=' . strval($request->server->get('REMOTE_ADDR', '')));
            return new JsonResponse(
                array(
                    'success' => false,
                    'error' => 'Forbidden caller IP.',
                    'remote_ip' => (string) $request->server->get('REMOTE_ADDR', '')
                ),
                403
            );
        }

        $form = $sg->get('form_manager.manager')->find($formId);
        if (!is_object($form)) {
            $this->logError($sg, 'renderExternalForm failed; form not found for formId=' . strval($formId));
            return new JsonResponse(array('success' => false, 'error' => 'Form not found'), 404);
        }

        $rendered = $sg->get('form_manager.manager')->renderForm($formId);
        $formCompleted = $sg->get('form_completed.manager')->getObject($form);

        $this->logInfo(
            $sg,
            'renderExternalForm success; formId=' . $form->getId()
            . '; formCompletedId=' . (is_object($formCompleted) ? strval($formCompleted->getId()) : 'null')
            . '; htmlLength=' . strval(strlen(is_array($rendered) ? strval($rendered['formBody'] ?? '') : ''))
        );

        return new JsonResponse(
            array(
                'success' => true,
                'form_id' => $form->getId(),
                'title' => $form->getTitle(),
                'form_completed_id' => is_object($formCompleted) ? $formCompleted->getId() : null,
                'html' => is_array($rendered) ? ($rendered['formBody'] ?? '') : '',
            )
        );
    }

    #[Route('/api/external/forms/{formId}/submit', name: 'content_external_form_submit', methods: ['POST'])]
    public function submitForm(Request $request, ServiceGetterManager $sg, $formId): JsonResponse
    {
        $this->logInfo(
            $sg,
            'submitForm start; formId=' . strval($formId)
            . '; remote=' . strval($request->server->get('REMOTE_ADDR', ''))
            . '; xff=' . strval($request->server->get('HTTP_X_FORWARDED_FOR', ''))
        );

        $allowedIpsCsv = $this->getAllowedIpsCsv($request);
        if (strlen(trim($allowedIpsCsv)) <= 0) {
            $this->logError($sg, 'submitForm denied; whitelist env missing');
            return new JsonResponse(
                array('success' => false, 'error' => 'API IP whitelist is not configured.'),
                403
            );
        }
        if (!$this->isAuthorisedCaller($request, $allowedIpsCsv)) {
            $this->logError($sg, 'submitForm denied; forbidden caller IP=' . strval($request->server->get('REMOTE_ADDR', '')));
            return new JsonResponse(
                array(
                    'success' => false,
                    'error' => 'Forbidden caller IP.',
                    'remote_ip' => (string) $request->server->get('REMOTE_ADDR', '')
                ),
                403
            );
        }

        $form = $sg->get('form_manager.manager')->find($formId);
        if (!is_object($form)) {
            $this->logError($sg, 'submitForm failed; form not found for formId=' . strval($formId));
            return new JsonResponse(array('success' => false, 'error' => 'Form not found'), 404);
        }

        $formCompletedValues = $request->get('form_completed');
        if (!is_array($formCompletedValues)) {
            $payload = json_decode($request->getContent(), true);
            $formCompletedValues = (is_array($payload) && isset($payload['form_completed']) && is_array($payload['form_completed']))
                ? $payload['form_completed']
                : array();
        }

        $formCompletedValues['form'] = $form;

        $formCompletedManager = $sg->get('form_completed.manager');
        $formCompleted = $formCompletedManager->getObject($form);
        $result = $formCompletedManager->saveFromRequest($formCompletedValues, $formCompleted);

        if (strlen((string) $result) > 0) {
            $this->logError($sg, 'submitForm failed validation/save; formId=' . $form->getId() . '; result=' . strval($result));
            return new JsonResponse(array('success' => false, 'error' => (string) $result), 400);
        }

        $this->logInfo(
            $sg,
            'submitForm success; formId=' . $form->getId()
            . '; formCompletedId=' . strval($formCompleted->getId())
            . '; postedKeys=' . strval(count($formCompletedValues))
        );

        return new JsonResponse(
            array(
                'success' => true,
                'form_id' => $form->getId(),
                'title' => $form->getTitle(),
                'form_completed_id' => $formCompleted->getId(),
            ),
            201
        );
    }
}