<?php
/*
 * This file is part of MODX Revolution.
 *
 * Copyright (c) MODX, LLC. All Rights Reserved.
 *
 * For complete copyright and license information, see the COPYRIGHT and LICENSE
 * files found in the top-level directory of this distribution.
 */

require_once MODX_CORE_PATH . 'model/modx/modresponse.class.php';
/**
 * Encapsulates an HTTP response from the MODX manager.
 *
 * {@inheritdoc}
 *
 * @package modx
 * @extends modResponse
 */
class modConnectorResponse extends modResponse {
    /**
     * The base location of the processors called by the connectors.
     *
     * @var string
     * @access private
     */
    protected $_directory;

    public $responseCode = 200;

    protected $_responseCodes = array(
        100 => 'Continue',
        101 => 'Switching Protocols',
        200 => 'OK',
        201 => 'Created',
        202 => 'Accepted',
        203 => 'Non-Authoritative Information',
        204 => 'No Content',
        205 => 'Reset Content',
        206 => 'Partial Content',
        300 => 'Multiple Choices',
        301 => 'Moved Permanently',
        302 => 'Found',
        303 => 'See Other',
        304 => 'Not Modified',
        305 => 'Use Proxy',
        306 => '(Unused)',
        307 => 'Temporary Redirect',
        400 => 'Bad Request',
        401 => 'Unauthorized',
        402 => 'Payment Required',
        403 => 'Forbidden',
        404 => 'Not Found',
        405 => 'Method Not Allowed',
        406 => 'Not Acceptable',
        407 => 'Proxy Authentication Required',
        408 => 'Request Timeout',
        409 => 'Conflict',
        410 => 'Gone',
        411 => 'Length Required',
        412 => 'Precondition Failed',
        413 => 'Request Entity Too Large',
        414 => 'Request-URI Too Long',
        415 => 'Unsupported Media Type',
        416 => 'Requested Range Not Satisfiable',
        417 => 'Expectation Failed',
        500 => 'Internal Server Error',
        501 => 'Not Implemented',
        502 => 'Bad Gateway',
        503 => 'Service Unavailable',
        504 => 'Gateway Timeout',
        505 => 'HTTP Version Not Supported'
    );
    /**
     * Creates a modConnectorResponse object.
     *
     * {@inheritdoc}
     */
    function __construct(modX & $modx) {
        parent :: __construct($modx);
        $this->setDirectory();
    }

    /**
     * Overrides modResponse::outputContent to provide connector-specific
     * processing.
     *
     * {@inheritdoc}
     */
    public function outputContent(array $options = array()) {
        /* variable pointer for easier access */
        $modx =& $this->modx;

        /* backwards compat */
        $error =& $this->modx->error;
        /* prevent browsing of subdirectories for security */
        $target = preg_replace('/[\.]{2,}/', '', htmlspecialchars($options['action']));

        $siteId = $this->modx->user->getUserToken($this->modx->context->get('key'));
        $isLogin = $target == 'login' || $target == 'security/login';

        /* Block the user if there's no user token for the current context, and permissions are in fact required */
        if (empty($siteId) && (!defined('MODX_REQP') || MODX_REQP === TRUE)) {
            $this->responseCode = 401;
            $this->body = $modx->error->failure($modx->lexicon('access_denied'),array('code' => 401));
        }
        /* Make sure we've got a token */
        elseif (!$isLogin && !isset($_SERVER['HTTP_MODAUTH']) && (!isset($_REQUEST['HTTP_MODAUTH']) || empty($_REQUEST['HTTP_MODAUTH']))) {
            $this->responseCode = 401;
            $this->body = $modx->error->failure($modx->lexicon('access_denied'),array('code' => 401));
        }
        /* If the token was passed as a request header (like in the manager), check if it's right */
        else if (!$isLogin && isset($_SERVER['HTTP_MODAUTH']) && $_SERVER['HTTP_MODAUTH'] != $siteId) {
            $this->responseCode = 401;
            $this->body = $modx->error->failure($modx->lexicon('access_denied'),array('code' => 401));
        }
        /* If the token was passed a request variable, check if it's right */
        else if (!$isLogin && isset($_REQUEST['HTTP_MODAUTH']) && $_REQUEST['HTTP_MODAUTH'] != $siteId) {
            $this->responseCode = 401;
            $this->body = $modx->error->failure($modx->lexicon('access_denied'), array('code' => 401));
        }
        /* verify the location and action */
        /*else if (!isset($options['location']) || !isset($options['action'])) {
            $this->responseCode = 404;
            $this->body = $this->modx->error->failure($modx->lexicon('action_err_ns'),array('code' => 404));

        }*/
        /* If we don't have an action, 404 out */
        else if (empty($options['action'])) {
            $this->responseCode = 404;
            $this->body = $this->modx->error->failure($modx->lexicon('action_err_ns'), array('code' => 404));
        }
        /* execute a processor and format the response */
        else {
            /* create scriptProperties array from HTTP GPC vars */
            if (!isset($_POST)) $_POST = array();
            if (!isset($_GET) || $isLogin) $_GET = array();
            $scriptProperties = array_merge($_GET,$_POST);
            if (isset($_FILES) && !empty($_FILES)) {
                $scriptProperties = array_merge($scriptProperties,$_FILES);
            }

            /** @var modProcessorResponse $response */
            $response = $this->modx->runProcessor($target,$scriptProperties,$options);
            if (!$response) {
                $this->responseCode = 404;
                $this->body = $this->modx->error->failure($this->modx->lexicon('processor_err_nf',array(
                    'target' => $target,
                )));
            } else {
                $this->body = $response->getResponse();
            }
        }
        /* if files sent, this means that the browser needs it in text/plain,
         * so ignore text/json header type
         */
        if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
            header("Content-Type: application/json; charset=UTF-8");
            $message = 'OK';
            if (array_key_exists($this->responseCode,$this->_responseCodes)) {
                $message = $this->_responseCodes[$this->responseCode];
            }
            header('Status: '.$this->responseCode.' '.$message);
            header('Version: ' . $_SERVER['SERVER_PROTOCOL']);
        }
        if (is_array($this->header)) {
            foreach ($this->header as $header) header($header);
        }
        if (is_array($this->body)) {
            @session_write_close();
            $json = $this->modx->toJSON(array(
                'success' => isset($this->body['success']) ? $this->body['success'] : 0,
                'message' => isset($this->body['message']) ? $this->body['message'] : $this->modx->lexicon('error'),
                'total' => (isset($this->body['total']) && $this->body['total'] > 0)
                    ? intval($this->body['total'])
                    : (isset($this->body['errors'])
                        ? count($this->body['errors'])
                        : 1),
                'data' => isset($this->body['errors']) ? $this->body['errors'] : array(),
                'object' => isset($this->body['object']) ? $this->body['object'] : array(),
            ));

            if (!empty($_GET['callback'])) {
                $json = $modx->stripTags($_GET['callback']) . '(' . $json . ')';
            }
            die($json);
        } else {
            @session_write_close();
            die($this->body);
        }
    }

    /**
     * Return arrays of objects (with count) converted to JSON.
     *
     * The JSON result includes two main elements, total and results. This format is used for list
     * results.
     *
     * @access public
     * @param array $array An array of data objects.
     * @param mixed $count The total number of objects. Used for pagination.
     * @return string The JSON output.
     */
    public function outputArray(array $array,$count = false) {
        if (!is_array($array)) return false;
        if ($count === false) { $count = count($array); }
        return '({"total":"'.$count.'","results":'.$this->modx->toJSON($array).'})';
    }

    /**
     * Set the physical location of the processor directory for the response handler.
     *
     * This allows for dynamic processor locations.
     *
     * @access public
     * @param string $dir The directory to set as the processors directory.
     */
    public function setDirectory($dir = '') {
        if ($dir == '') {
            $this->_directory = $this->modx->getOption('processors_path');
        } else {
            $this->_directory = $dir;
        }
    }

    /**
     * Converts PHP to JSON with JavaScript literals left in-tact.
     *
     * JSON does not allow JavaScript literals, but this function encodes certain identifiable
     * literals and decodes them back into literals after modX::toJSON() formats the data.
     *
     * @access public
     * @param mixed $data The PHP data to be converted.
     * @return string The extended JSON-encoded string.
     */
    public function toJSON($data) {
        if (is_array($data)) {
            array_walk_recursive($data, array(&$this, '_encodeLiterals'));
        }
        return $this->_decodeLiterals($this->modx->toJSON($data));
    }

    /**
     * Encodes certain JavaScript literal strings for later decoding.
     *
     * @access protected
     * @param mixed &$value A reference to the value to be encoded if it is identified as a literal.
     * @param integer|string $key The array key corresponding to the value.
     */
    protected function _encodeLiterals(&$value, $key) {
        if (is_string($value)) {
            /* properly handle common literal structures */
            if (strpos($value, 'function(') === 0
             || strpos($value, 'this.') === 0
             || strpos($value, 'new Function(') === 0
             || strpos($value, 'Ext.') === 0) {
                $value = '@@' . base64_encode($value) . '@@';
             }
        }
    }

    /**
     * Decodes strings encoded by _encodeLiterals to restore JavaScript literals.
     *
     * @access protected
     * @param string $string The JSON-encoded string with encoded literals.
     * @return string The JSON-encoded string with literals restored.
     */
    protected function _decodeLiterals($string) {
        $pattern = '/"@@(.*?)@@"/';
        $string = preg_replace_callback(
            $pattern,
            function ($matches) { return base64_decode($matches[1]); },
            $string
        );
        return $string;
    }
}
