| Current Path : /var/www/v3.cesa.co.za/src/ContentBundle/Controller/ |
| 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
);
}
}