<?php

/**
 * Copyright 2024-2025 Simply.IN Sp. z o.o.
 *
 * NOTICE OF LICENSE
 *
 * Licensed under the EUPL-1.2 or later.
 * You may not use this work except in compliance with the Licence.
 *
 * Copy of the Licence is available at:
 * https://joinup.ec.europa.eu/software/page/eupl
 * It is bundled with this package in the file LICENSE.txt
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the Licence is distributed on an as is basis,
 * without warranties or conditions of any kind, either express or implied.
 * Check the Licence for the specific language governing permissions
 * and limitations under the License.
 *
 * @author   Simply.IN Sp. z o.o.
 * @copyright 2024-2025 Simply.IN Sp. z o.o.
 * @license   https://joinup.ec.europa.eu/software/page/eupl
 */

declare(strict_types=1);

use SimplyIn\Ajax\AjaxRegistryFactory;
use SimplyIn\Ajax\AjaxRequest;
use SimplyIn\Ajax\AjaxTokenValidator;

if (!defined('_PS_VERSION_')) {
    exit;
}

class SimplyinAjaxModuleFrontController extends ModuleFrontController
{
    /** @var Simplyin|null */
    public $module;

    public function __construct()
    {
        parent::__construct();

        if (!Tools::isSubmit('ajax')) {
            exit;
        }

        $this->module = Module::getInstanceByName('simplyin');
    }

    public function initContent(): void
    {
        parent::initContent();
    }

    public function displayAjax(): void
    {
        // Require POST for every action — blocks trivial GET/<img>-based CSRF against these
        // state-changing endpoints. The ajax token is public (window.simplyin.ajax_token) and
        // install-global, so it is not a CSRF defence on its own. Every module caller already
        // uses POST. (A rotating per-session nonce is the follow-up hardening on top of this.)
        if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') {
            http_response_code(405);
            exit(json_encode(['error' => 'Method not allowed']));
        }

        $request = new AjaxRequest();
        $validator = new AjaxTokenValidator($this->module);
        if (!$validator->isValid($request)) {
            exit(json_encode(['error' => 'Invalid token']));
        }

        if (!Tools::isSubmit('action')) {
            exit(json_encode(['error' => 'No action specified']));
        }

        $registry = AjaxRegistryFactory::create($this->module, $this->context);
        $action = (string) Tools::getValue('action');

        try {
            $response = $registry->dispatch($action, $request);
        } catch (\Throwable $e) {
            // The action name comes straight from the request, so normalize it before it reaches
            // the log — a raw value could inject arbitrary content into the log line.
            $safeAction = substr((string) preg_replace('/[^a-zA-Z0-9_-]/', '', $action), 0, 64);
            // Log file:line + trace (not just the message) so preprod can pinpoint which call threw.
            PrestaShopLogger::addLog(
                'SimplyIn ajax action "' . $safeAction . '" failed: ' . $e->getMessage()
                . ' @ ' . $e->getFile() . ':' . $e->getLine() . "\n" . $e->getTraceAsString(),
                3,
            );
            exit(json_encode(['error' => 'Internal error']));
        }

        // Never emit an empty body on an un-encodable payload (e.g. malformed UTF-8 from upstream) —
        // that is exactly the "silence" the widget cannot react to. Substitute bad bytes, and on a
        // hard encode failure return an explicit JSON error the front can surface.
        $encoded = json_encode($response, JSON_INVALID_UTF8_SUBSTITUTE);
        if ($encoded === false) {
            PrestaShopLogger::addLog(
                'SimplyIn ajax action "' . Tools::getValue('action') . '" response encode failed: ' . json_last_error_msg(),
                3,
            );
            $encoded = json_encode(['error' => 'Internal error']);
        }

        exit($encoded);
    }
}
