<?php
/**
 *    Copyright (C) 2015 Deciso B.V.
 *
 *    All rights reserved.
 *
 *    Redistribution and use in source and binary forms, with or without
 *    modification, are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *
 *    THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 *    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 *    AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 *    AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 *    OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *    POSSIBILITY OF SUCH DAMAGE.
 *
 */
namespace OPNsense\Base;

use OPNsense\Core\ACL;
use OPNsense\Auth\AuthenticationFactory;

/**
 * Class ApiControllerBase, inherit this class to implement API calls
 * @package OPNsense\Base
 */
class ApiControllerBase extends ControllerRoot
{
    /**
     * @var bool cleanse output before sending to client, be very careful to disable this (XSS).
     */
    private $cleanseOutput = true;

    /**
     * disable output cleansing.
     * Prevents the framework from executing automatic XSS protection on all delivered json data.
     * Be very careful to disable this, if content can't be guaranteed you might introduce XSS vulnerabilities.
     */
    protected function disableOutputCleansing()
    {
        $this->cleanseOutput = false;
    }

    /**
     * parse raw json type content to POST data depending on content type
     * (only for api calls)
     * @return string
     */
    private function parseJsonBodyData()
    {
        switch ($this->request->getHeader('CONTENT_TYPE')) {
            case 'application/json':
            case 'application/json;charset=UTF-8':
                $jsonRawBody = $this->request->getJsonRawBody(true);
                if (empty($this->request->getRawBody()) && empty($jsonRawBody)) {
                    return "Invalid JSON syntax";
                }
                $_POST = $jsonRawBody;
                break;
        }
        return null;
    }

    /**
     * Initialize API controller
     */
    public function initialize()
    {
        // disable view processing
        $this->view->disable();
    }

    /**
     * before routing event.
     * Handles authentication and authentication of user requests
     * In case of API calls, also prevalidates if request can be executed to return a more readable response
     * to the user.
     * @param Dispatcher $dispatcher
     * @return null|bool
     */
    public function beforeExecuteRoute($dispatcher)
    {
        // handle authentication / authorization
        if (!empty($this->request->getHeader('Authorization'))) {
            // Authorization header send, handle API request
            $authHeader = explode(' ', $this->request->getHeader('Authorization'));
            if (count($authHeader) > 1) {
                $key_secret_hash = $authHeader[1];
                $key_secret = explode(':', base64_decode($key_secret_hash));
                if (count($key_secret) > 1) {
                    $apiKey = $key_secret[0];
                    $apiSecret = $key_secret[1];

                    $authFactory = new AuthenticationFactory();
                    $authenticator = $authFactory->get("Local API");
                    if ($authenticator->authenticate($apiKey, $apiSecret)) {
                        $authResult = $authenticator->getLastAuthProperties();
                        if (array_key_exists('username', $authResult)) {
                            // check ACL if user is returned by the Authenticator object
                            $acl = new ACL();
                            if (!$acl->isPageAccessible($authResult['username'], $_SERVER['REQUEST_URI'])) {
                                $this->getLogger()->error("uri ".$_SERVER['REQUEST_URI'].
                                    " not accessible for user ".$authResult['username'] . " using api key ".
                                    $apiKey);
                            } else {
                                // authentication + authorization successful.
                                // pre validate request and communicate back to the user on errors
                                $callMethodName = $dispatcher->getActionName().'Action';
                                $dispatchError = null;
                                // check number of parameters using reflection
                                $object_info = new \ReflectionObject($this);
                                $req_c = $object_info->getMethod($callMethodName)->getNumberOfRequiredParameters();
                                if ($req_c > count($dispatcher->getParams())) {
                                    $dispatchError = 'action ' . $dispatcher->getActionName() .
                                      ' expects at least '. $req_c . ' parameter(s)';
                                } else {
                                    // if body is send as json data, parse to $_POST first
                                    $dispatchError = $this->parseJsonBodyData();
                                }
                                if ($dispatchError != null) {
                                    // send error to client
                                    $this->response->setStatusCode(400, "Bad Request");
                                    $this->response->setContentType('application/json', 'UTF-8');
                                    $this->response->setJsonContent(
                                        array('message' => $dispatchError,
                                              'status'  => 400)
                                    );
                                    $this->response->send();
                                    return false;
                                }

                                return true;
                            }
                        }
                    }
                }
            }
            // not authenticated
            $this->response->setStatusCode(401, "Unauthorized");
            $this->response->setContentType('application/json', 'UTF-8');
            $this->response->setJsonContent(array(
                'status'  => 401,
                'message' => 'Authentication Failed',
            ));
            $this->response->send();
            return false;
        } else {
            // handle UI ajax requests
            // use session data and ACL to validate request.
            if (!$this->doAuth()) {
                return false;
            }

            // check for valid csrf on post requests
            $csrf_tokenkey = $this->request->getHeader('X_CSRFTOKENKEY');
            $csrf_token =   $this->request->getHeader('X_CSRFTOKEN');
            $csrf_valid = $this->security->checkToken($csrf_tokenkey, $csrf_token, false);

            if (($this->request->isPost() ||
                    $this->request->isPut() ||
                    $this->request->isDelete()
                ) && !$csrf_valid
            ) {
                // missing csrf, exit.
                $this->getLogger()->error("no matching csrf found for request");
                return false;
            }
        }
    }

    /**
     * process API results, serialize return data to json.
     * @param $dispatcher
     * @return string json data
     */
    protected function afterExecuteRoute($dispatcher)
    {
        // exit when reponse headers are already set
        if ($this->response->getHeaders()->get("Status") != null) {
            return false;
        } else {
            // process response, serialize to json object
            $data = $dispatcher->getReturnedValue();
            if (is_array($data)) {
                $this->response->setContentType('application/json', 'UTF-8');
                if ($this->cleanseOutput) {
                    echo htmlspecialchars(json_encode($data), ENT_NOQUOTES);
                } else {
                    echo json_encode($data);
                }

            } else {
                // output raw data
                echo $data;
            }
        }

        return true;
    }
}