core/model/modx/rest/modrestserver.class.php
<?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.
*/
/**
* An extendable class for handling REST requests.
*
* @deprecated To be removed in 2.3. See modRestService instead.
*
* @package modx
* @subpackage rest
*/
class modRestServer {
const OPT_AUTH = 'authenticate';
const OPT_AUTH_GET = 'authenticateGet';
const OPT_AUTH_USER_VAR = 'authUserVar';
const OPT_AUTH_PASS_VAR = 'authPassVar';
const OPT_ENCODING = 'encoding';
const OPT_ERROR_DATA_NODE = 'error_data_node';
const OPT_ERROR_NODE = 'error_node';
const OPT_ERROR_MESSAGE_NODE = 'error_message_node';
const OPT_FORMAT = 'format';
const OPT_PROCESSORS_PATH = 'processors_path';
const OPT_REQUEST_PATH = 'request_path';
const OPT_REQUEST_VAR = 'requestVar';
const OPT_REALM = 'realm';
const OPT_RENDERERS = 'renderers';
/**
* @var $error The current error message
* @access protected
*/
protected $error = false;
/**
* @param modX $modx A reference to the modX object
* @param array $config An array of configuration options
*/
function __construct(modX &$modx,array $config = array()) {
$this->modx =& $modx;
$this->config = array_merge(array(
modRestServer::OPT_AUTH => true,
modRestServer::OPT_AUTH_GET => false,
modRestServer::OPT_AUTH_USER_VAR => 'user',
modRestServer::OPT_AUTH_PASS_VAR => 'password',
modRestServer::OPT_ENCODING => 'UTF-8',
modRestServer::OPT_FORMAT => 'xml',
modRestServer::OPT_PROCESSORS_PATH => '',
modRestServer::OPT_REQUEST_VAR => 'p',
modRestServer::OPT_REALM => 'MODX',
modRestServer::OPT_RENDERERS => 'renderers',
modRestServer::OPT_ERROR_DATA_NODE => 'data',
modRestServer::OPT_ERROR_NODE => 'error',
modRestServer::OPT_ERROR_MESSAGE_NODE => 'message',
),$config);
$this->modx->deprecated('2.3.0', 'Use the modRestService classes instead.');
}
/**
* Handles the REST request and loads the correct processor. Checks for
* authentication should it be a type not equal to GET if authenticate is
* set to true, or always if authenticateGet is set to true.
*
* @access public
* @return string
*/
public function handle() {
$scriptProperties = array();
$scriptProperties[modRestServer::OPT_REQUEST_PATH] = $this->computePath();
$output = '';
if (file_exists($scriptProperties[modRestServer::OPT_REQUEST_PATH])) {
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
if ($this->config[modRestServer::OPT_AUTH_GET]) {
$result = $this->authenticate();
if ($result !== true) {
return $result;
}
}
$scriptProperties = array_merge($scriptProperties,$_GET);
} else {
if ($this->config[modRestServer::OPT_AUTH]) {
$result = $this->authenticate();
if ($result !== true) {
return $result;
}
}
}
$modx =& $this->modx;
$output = include $scriptProperties[modRestServer::OPT_REQUEST_PATH];
} else {
return $this->error('404 Not Found',$scriptProperties);
}
return $output;
}
/**
* Computes the path for the REST request
*
* @access public
* @return string The absolute path to the processor to load
*/
public function computePath() {
$path = $this->config[modRestServer::OPT_PROCESSORS_PATH];
$path .= trim($_REQUEST[$this->config[modRestServer::OPT_REQUEST_VAR]],'/').'/';
$path .= strtolower($_SERVER['REQUEST_METHOD']).'.php';
return $path;
}
/**
* Handles basic authentication for the server
*
* @todo Add an optional usergroup check
*
* @return boolean True if successful.
*/
public function authenticate() {
$this->modx->getService('lexicon','modLexicon');
$this->modx->lexicon->load('user');
if (empty($_REQUEST[$this->config[modRestServer::OPT_AUTH_USER_VAR]])) {
return $this->deny($this->modx->lexicon('user_err_ns'));
}
if (empty($_REQUEST[$this->config[modRestServer::OPT_AUTH_PASS_VAR]])) {
return $this->deny($this->modx->lexicon('user_err_not_specified_password'));
}
$user = $this->modx->getObject('modUser',array(
'username' => $_REQUEST[$this->config[modRestServer::OPT_AUTH_USER_VAR]],
));
if (empty($user)) return $this->deny($this->modx->lexicon('user_err_nf'));
if (!$user->passwordMatches($_REQUEST[$this->config[modRestServer::OPT_AUTH_PASS_VAR]])) {
return $this->deny($this->modx->lexicon('user_err_password'));
}
return true;
}
/**
* Deny access and send a 401.
*
* @param string $message
* @param array $data
* @return string
*/
public function deny($message,array $data = array()) {
return $this->error($message,$data,'401');
}
/**
* Handles success messages
*
* @param array|xPDOObject $data The data to pass and encode
* @param string $root
* @return string The encoded message
*/
public function success($data,$root = '') {
header($_SERVER['SERVER_PROTOCOL'].' 200 OK');
return $this->encode($data,$root);
}
/**
* Handles error messages
*
* @access public
* @param string $message An error message
* @param array|xPDOObject $data Any additional data
* @param string $type The type of the error message
* @return string
*/
public function error($message = '',$data = array(),$type = '404') {
$this->error = true;
if (method_exists($this,'_err'.$type)) {
$errType = '_err'.$type;
$this->$errType();
} else {
$this->_err404();
}
return $this->encode(array(
$this->config[modRestServer::OPT_ERROR_MESSAGE_NODE] => $message,
$this->config[modRestServer::OPT_ERROR_DATA_NODE] => $data,
),'<'.$this->config[modRestServer::OPT_ERROR_NODE].'>');
}
/**
* Encodes the data to the specified format. Defaults to XML.
*
* @access public
* @param array|xPDOObject $data
* @param string $root
* @return string The encoded message
*/
public function encode($data,$root = '') {
$output = '';
$format = $this->modx->getOption(modRestServer::OPT_FORMAT,$_REQUEST,$this->config[modRestServer::OPT_FORMAT]);
switch ($format) {
case 'json':
header('Content-Type: application/javascript');
if (is_array($data)) {
$list = array();
foreach ($data as $k => $v) {
$list[$k.'s'] = $v;
}
} else {
$list = array($root => $data->toArray());
}
$output = $this->modx->toJSON($list);
break;
case 'xml':
default:
header('Content-Type: text/xml');
if (is_array($data)) {
$list = $data;
} else if ($data instanceof xPDOObject) {
$list = $data->toArray();
}
$output = '<?xml version="1.0" encoding="'.$this->config['encoding'].'"?>'.
"\n{$root}\n";
$output .= $this->array2xml($list);
$endRootTag = '</'.substr($root,1,strpos($root,' ')-1).'>';
$output .= "{$endRootTag}\n";
break;
}
return $output;
}
/**
* Sets HTTP 204 response headers
* @param string $output The outputted response to send
* @return string
*/
private function _err204($output = '') {
header($_SERVER['SERVER_PROTOCOL'].' 204 No Content');
return $output;
}
/**
* Sets HTTP 400 response headers
* @param string $output The outputted response to send
* @return string
*/
private function _err400($output = '') {
header($_SERVER['SERVER_PROTOCOL'].' 400 Bad Request');
return '';
}
/**
* Sets HTTP 401 response headers
* @return string
*/
private function _err401() {
header('WWW-Authenticate: Basic realm="'.$this->config['realm'].'"');
header($_SERVER['SERVER_PROTOCOL'].' 401 Unauthorized');
return '';
}
/**
* Sets HTTP 404 response headers
* @param array $scriptProperties An array of properties
* @return string
*/
private function _err404($scriptProperties = array()) {
header("{$_SERVER['SERVER_PROTOCOL']} 404 Not Found");
$output = '<h2>404 Not Found</h2>';
if (!empty($scriptProperties)) {
$output .= '<p>'.$scriptProperties['called'].' is not a valid request.</p>';
}
return $output;
}
/**
* Sets HTTP 405 response headers
* @param string $allowed A comma-separated list of allowed protocols
* @return string
*/
private function _err405($allowed = 'GET, HEAD') {
header($_SERVER['SERVER_PROTOCOL'].' 405 Method Not Allowed');
header('Allow: '.$allowed);
return '';
}
/**
* Sets HTTP 406 response headers
* @param string $output The outputted response to send
* @return string
*/
private function _err406($output = '') {
header($_SERVER['SERVER_PROTOCOL'].' 406 Not Acceptable');
$output = join(', ', array_keys($this->config[modRestServer::OPT_RENDERERS]));
return $output;
}
/**
* Sets HTTP 411 response headers
* @param string $output The outputted response to send
* @return string
*/
private function _err411($output = '') {
header($_SERVER['SERVER_PROTOCOL'].' 411 Length Required');
return '';
}
/**
* Sets HTTP 500 response headers
* @param string $output The outputted response to send
* @return string
*/
private function _err500($output = '') {
header($_SERVER['SERVER_PROTOCOL'].' 500 Internal Server Error');
return '';
}
/**
* Converts an array to xml
*
* @access protected
* @param array $array
* @param integer $level
* @return string
*/
protected function array2xml($array,$level=1) {
$xml = '';
foreach ($array as $key=>$value) {
$key = strtolower($key);
if (is_array($value)) {
$multi_tags = false;
foreach($value as $key2=>$value2) {
if (is_array($value2)) {
$xml .= str_repeat("\t",$level)."<$key>\n";
$xml .= $this->array2xml($value2,$level+1);
$xml .= str_repeat("\t",$level)."</$key>\n";
$multi_tags = true;
} else {
if (trim($value2)!='') {
if (htmlspecialchars($value2)!=$value2) {
$xml .= str_repeat("\t",$level).
"<$key><![CDATA[$value2]]>".
"</$key>\n";
} else {
$xml .= str_repeat("\t",$level).
"<$key>$value2</$key>\n";
}
}
$multi_tags = true;
}
}
if (!$multi_tags and count($value)>0) {
$xml .= str_repeat("\t",$level)."<$key>\n";
$xml .= $this->array2xml($value,$level+1);
$xml .= str_repeat("\t",$level)."</$key>\n";
}
} else {
if (trim($value)!='') {
if (htmlspecialchars($value)!=$value) {
$xml .= str_repeat("\t",$level)."<$key>".
"<![CDATA[$value]]></$key>\n";
} else {
$xml .= str_repeat("\t",$level).
"<$key>$value</$key>\n";
}
}
}
}
return $xml;
}
}