<?php

namespace dokuwiki\Remote;

/**
 * Provide the Remote XMLRPC API as a JSON based API
 */
class JsonRpcServer
{
    protected $remote;

    /** @var float The XML-RPC Version. 0 is our own simplified variant */
    protected $version = 0;

    /**
     * JsonRpcServer constructor.
     */
    public function __construct()
    {
        $this->remote = new Api();
    }

    /**
     * Serve the request
     *
     * @param string $body Should only be set for testing, otherwise the request body is read from php://input
     * @return mixed
     * @throws RemoteException
     */
    public function serve($body = '')
    {
        global $conf;
        global $INPUT;

        if (!$conf['remote']) {
            http_status(404);
            throw new RemoteException("JSON-RPC server not enabled.", -32605);
        }
        if (!empty($conf['remotecors'])) {
            header('Access-Control-Allow-Origin: ' . $conf['remotecors']);
        }
        if ($INPUT->server->str('REQUEST_METHOD') !== 'POST') {
            http_status(405);
            header('Allow: POST');
            throw new RemoteException("JSON-RPC server only accepts POST requests.", -32606);
        }
        [$contentType] = explode(';', $INPUT->server->str('CONTENT_TYPE'), 2); // ignore charset
        $contentType = strtolower($contentType); // mime types are case-insensitive
        if ($contentType !== 'application/json') {
            http_status(415);
            throw new RemoteException("JSON-RPC server only accepts application/json requests.", -32606);
        }

        try {
            if ($body === '') {
                $body = file_get_contents('php://input');
            }
            if ($body !== '') {
                $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
            } else {
                $data = [];
            }
        } catch (\Exception $e) {
            http_status(400);
            throw new RemoteException("JSON-RPC server only accepts valid JSON.", -32700);
        }

        return $this->createResponse($data);
    }

    /**
     * This executes the method and returns the result
     *
     * This should handle all JSON-RPC versions and our simplified version
     *
     * @link https://en.wikipedia.org/wiki/JSON-RPC
     * @link https://www.jsonrpc.org/specification
     * @param array $data
     * @return array
     * @throws RemoteException
     */
    protected function createResponse($data)
    {
        global $INPUT;
        $return = [];

        if (isset($data['method'])) {
            // this is a standard conform request (at least version 1.0)
            $method = $data['method'];
            $params = $data['params'] ?? [];
            $this->version = 1;

            // always return the same ID
            if (isset($data['id'])) $return['id'] = $data['id'];

            // version 2.0 request
            if (isset($data['jsonrpc'])) {
                $return['jsonrpc'] = $data['jsonrpc'];
                $this->version = (float)$data['jsonrpc'];
            }

            // version 1.1 request
            if (isset($data['version'])) {
                $return['version'] = $data['version'];
                $this->version = (float)$data['version'];
            }
        } else {
            // this is a simplified request
            $method = $INPUT->server->str('PATH_INFO');
            $method = trim($method, '/');
            $params = $data;
            $this->version = 0;
        }

        // excute the method
        $return['result'] = $this->call($method, $params);
        $this->addErrorData($return); // handles non-error info
        return $return;
    }

    /**
     * Create an error response
     *
     * @param \Exception $exception
     * @return array
     */
    public function returnError($exception)
    {
        $return = [];
        $this->addErrorData($return, $exception);
        return $return;
    }

    /**
     * Depending on the requested version, add error data to the response
     *
     * @param array $response
     * @param \Exception|null $e
     * @return void
     */
    protected function addErrorData(&$response, $e = null)
    {
        if ($e !== null) {
            // error occured, add to response
            $response['error'] = [
                'code' => $e->getCode() ?: 1, // 0 is success, so we use 1 as default
                'message' => $e->getMessage()
            ];
        } else {
            // no error, act according to version
            if ($this->version > 0 && $this->version < 2) {
                // version 1.* wants null
                $response['error'] = null;
            } elseif ($this->version < 1) {
                // simplified version wants success
                $response['error'] = [
                    'code' => 0,
                    'message' => 'success'
                ];
            }
            // version 2 wants no error at all
        }
    }

    /**
     * Call an API method
     *
     * @param string $methodname
     * @param array $args
     * @return mixed
     * @throws RemoteException
     */
    public function call($methodname, $args)
    {
        try {
            return $this->remote->call($methodname, $args);
        } catch (AccessDeniedException $e) {
            if (!isset($_SERVER['REMOTE_USER'])) {
                http_status(401);
                throw new RemoteException("server error. not authorized to call method $methodname", -32603);
            } else {
                http_status(403);
                throw new RemoteException("server error. forbidden to call the method $methodname", -32604);
            }
        } catch (RemoteException $e) {
            http_status(400);
            throw $e;
        }
    }
}