<?php

/*
 * Copyright 2005 - 2025 Centreon (https://www.centreon.com/)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * For more information : contact@centreon.com
 *
 */

if (! (class_exists('centreonDB') || class_exists('\\centreonDB')) && defined('_CENTREON_PATH_')) {
    require_once _CENTREON_PATH_ . '/www/class/centreonDB.class.php';
}

use Centreon\Infrastructure\Webservice\WebserviceAutorizePublicInterface;
use Centreon\Infrastructure\Webservice\WebserviceAutorizeRestApiInterface;
use Pimple\Container;

/**
 * Class
 *
 * @class CentreonWebService
 */
class CentreonWebService
{
    public const RESULT_HTML = 'html';
    public const RESULT_JSON = 'json';

    /** @var CentreonDB|null */
    protected $pearDB = null;

    /** @var array */
    protected $arguments = [];

    /** @var null */
    protected $token = null;

    /** @var */
    protected static $webServicePaths;

    /**
     * CentreonWebService constructor
     */
    public function __construct()
    {
        $this->loadDb();
        $this->loadArguments();
        $this->loadToken();
    }

    /**
     * Authorize to access to the action
     *
     * @param string $action The action name
     * @param CentreonUser $user The current user
     * @param bool $isInternal If the api is call in internal
     * @return bool If the user has access to the action
     */
    public function authorize($action, $user, $isInternal = false)
    {
        return (bool) ($isInternal || ($user && $user->admin));
    }

    /**
     * Send json return
     *
     * @param mixed $data
     * @param int $code
     * @param string|null $format
     *
     * @return void
     */
    public static function sendResult($data, $code = 200, $format = null): void
    {
        switch ($code) {
            case 500:
                header('HTTP/1.1 500 Internal Server Error');
                break;
            case 502:
                header('HTTP/1.1 502 Bad Gateway');
                break;
            case 503:
                header('HTTP/1.1 503 Service Unavailable');
                break;
            case 504:
                header('HTTP/1.1 504 Gateway Time-out');
                break;
            case 400:
                header('HTTP/1.1 400 Bad Request');
                break;
            case 401:
                header('HTTP/1.1 401 Unauthorized');
                break;
            case 403:
                header('HTTP/1.1 403 Forbidden');
                break;
            case 404:
                header('HTTP/1.1 404 Object not found');
                break;
            case 405:
                header('HTTP/1.1 405 Method not allowed');
                break;
            case 409:
                header('HTTP/1.1 409 Conflict');
                break;
            case 206:
                header('HTTP/1.1 206 Partial content');
                $data = json_decode($data, true);
                break;
        }

        switch ($format) {
            case static::RESULT_HTML:
                header('Content-type: text/html');
                echo $data;
                break;
            case static::RESULT_JSON:
            case null:
                header('Content-type: application/json;charset=utf-8');
                echo json_encode($data, JSON_UNESCAPED_UNICODE);
                break;
        }

        exit();
    }

    /**
     * Route the webservice to the good method
     *
     * @param Container $dependencyInjector
     * @param CentreonUser $user The current user
     * @param bool $isInternal If the Rest API call is internal
     *
     * @throws PDOException
     */
    public static function router(Container $dependencyInjector, $user, $isInternal = false): void
    {
        global $pearDB;

        // Test if route is defined
        if (isset($_GET['object']) === false || isset($_GET['action']) === false) {
            static::sendResult('Bad parameters', 400);
        }

        $resultFormat = 'json';
        if (isset($_GET['resultFormat'])) {
            $resultFormat = $_GET['resultFormat'];
        }

        $methodPrefix = strtolower($_SERVER['REQUEST_METHOD']);
        $object = $_GET['object'];
        $action = $methodPrefix . ucfirst($_GET['action']);

        // Generate path for WebService
        self::$webServicePaths = glob(_CENTREON_PATH_ . '/www/api/class/*.class.php');
        $res = $pearDB->query('SELECT name FROM modules_informations');
        while ($row = $res->fetch()) {
            self::$webServicePaths = array_merge(
                self::$webServicePaths,
                glob(_CENTREON_PATH_ . '/www/modules/' . $row['name'] . '/webServices/rest/*.class.php')
            );
        }
        self::$webServicePaths = array_merge(
            self::$webServicePaths,
            glob(_CENTREON_PATH_ . '/www/widgets/*/webServices/rest/*.class.php')
        );

        $isService = $dependencyInjector['centreon.webservice']->has($object);

        if ($isService === true) {
            $webService = [
                'class' => $dependencyInjector['centreon.webservice']->get($object),
            ];

            // Initialize the language translator
            $dependencyInjector['translator'];

            // Use the web service if has been initialized or initialize it
            if (isset($dependencyInjector[$webService['class']])) {
                $wsObj = $dependencyInjector[$webService['class']];
            } else {
                $wsObj = new $webService['class']();
                $wsObj->setDi($dependencyInjector);
            }
        } else {
            $webService = self::webservicePath($object);

            /**
             * Either we retrieve an instance of this web service that has been
             * created in the dependency injector, or we create a new one.
             */
            if (isset($dependencyInjector[$webService['class']])) {
                $wsObj = $dependencyInjector[$webService['class']];
            } else {
                // Initialize the webservice
                require_once $webService['path'];
                $wsObj = new $webService['class']();
            }
        }

        if ($wsObj instanceof CentreonWebServiceDiInterface) {
            $wsObj->finalConstruct($dependencyInjector);
        }

        if (method_exists($wsObj, $action) === false) {
            static::sendResult('Method not found', 404);
        }

        // Execute the action
        try {
            if (! static::isWebserviceAllowed($wsObj, $action, $user, $isInternal)) {
                static::sendResult('Forbidden', 403, static::RESULT_JSON);
            }

            static::updateTokenTtl();
            $data = $wsObj->{$action}();
            $wsObj::sendResult($data, 200, $resultFormat);
        } catch (RestException $e) {
            $wsObj::sendResult($e->getMessage(), $e->getCode());
        } catch (Exception $e) {
            $wsObj::sendResult($e->getMessage(), 500);
        }
    }

    /**
     * Load database
     *
     * @return void
     */
    protected function loadDb()
    {
        $this->pearDB ??= new CentreonDB();
    }

    /**
     * Load arguments compared http method
     *
     * @return void
     */
    protected function loadArguments()
    {
        switch ($_SERVER['REQUEST_METHOD']) {
            case 'GET':
                $httpParams = $_GET;
                unset($httpParams['action'], $httpParams['object']);

                $this->arguments = $httpParams;
                break;
            case 'POST':
            case 'PUT':
            case 'PATCH':
                $this->arguments = $this->parseBody();
                break;
            case 'DELETE':
                break;
            default:
                static::sendResult('Bad request', 400);
                break;
        }
    }

    /**
     * Parse the body for get arguments
     * The body must be JSON format
     * @return array
     */
    protected function parseBody()
    {
        try {
            $httpParams = json_decode(file_get_contents('php://input'), true);
        } catch (Exception $e) {
            static::sendResult('Bad parameters', 400);
        }

        return $httpParams;
    }

    /**
     * Load the token for class if exists
     *
     * @return void
     */
    protected function loadToken()
    {
        if (isset($_SERVER['HTTP_CENTREON_AUTH_TOKEN'])) {
            $this->token = $_SERVER['HTTP_CENTREON_AUTH_TOKEN'];
        }
    }

    /**
     * Get webservice
     *
     * @param string $object
     *
     * @return array|mixed
     */
    protected static function webservicePath($object = '')
    {
        $webServiceClass = [];
        foreach (self::$webServicePaths as $webServicePath) {
            if (str_contains($webServicePath, $object . '.class.php')) {
                require_once $webServicePath;
                $explodedClassName = explode('_', $object);
                $className = '';
                foreach ($explodedClassName as $partClassName) {
                    $className .= ucfirst(strtolower($partClassName));
                }
                if (class_exists($className)) {
                    $webServiceClass = ['path' => $webServicePath, 'class' => $className];
                }
            }
        }

        if ($webServiceClass === []) {
            static::sendResult('Method not found', 404);
        }

        return $webServiceClass;
    }

    /**
     * Update the ttl for a token if the authentication is by token
     * Does not update the manual tokens
     *
     * @return void
     */
    protected static function updateTokenTtl()
    {
        global $pearDB;

        if (isset($_SERVER['HTTP_CENTREON_AUTH_TOKEN'])) {
            try {
                $stmt = $pearDB->prepare(
                    'UPDATE security_token st
                    INNER JOIN security_authentication_tokens sat
                        ON st.id = sat.provider_token_id AND sat.token_type = \'auto\'
                    SET st.expiration_date = (
                        SELECT UNIX_TIMESTAMP(NOW() + INTERVAL (`value` * 60) SECOND)
                        FROM `options`
                        wHERE `key` = \'session_expire\'
                    )
                    WHERE st.token = :token'
                );
                $stmt->bindValue(':token', $_SERVER['HTTP_CENTREON_AUTH_TOKEN'], PDO::PARAM_STR);
                $stmt->execute();
            } catch (Exception $e) {
                static::sendResult('Internal error', 500);
            }
        }
    }

    /**
     * Check webservice authorization
     *
     * @param WebserviceAutorizePublicInterface|WebserviceAutorizeRestApiInterface $webservice
     * @param string $action The action name
     * @param CentreonUser|null $user The current user
     * @param bool $isInternal If the api is call from internal
     *
     * @return bool if the webservice is allowed for the current user
     */
    private static function isWebserviceAllowed($webservice, $action, $user, $isInternal): bool
    {
        $allowed = false;

        // skip checks if public interface is implemented
        if ($webservice instanceof WebserviceAutorizePublicInterface) {
            $allowed = true;
        } else {
            $allowed = $webservice->authorize($action, $user, $isInternal);
        }

        return $allowed;
    }
}
