514 lines
15 KiB
PHP
514 lines
15 KiB
PHP
<?php
|
|
/*********************************************************************
|
|
class.api.php
|
|
|
|
API
|
|
|
|
Peter Rotich <peter@osticket.com>
|
|
Copyright (c) 2006-2013 osTicket
|
|
http://www.osticket.com
|
|
|
|
Released under the GNU General Public License WITHOUT ANY WARRANTY.
|
|
See LICENSE.TXT for details.
|
|
|
|
vim: expandtab sw=4 ts=4 sts=4:
|
|
**********************************************************************/
|
|
include_once INCLUDE_DIR.'class.controller.php';
|
|
|
|
class API {
|
|
|
|
var $id;
|
|
|
|
var $ht;
|
|
|
|
function __construct($id) {
|
|
$this->id = 0;
|
|
$this->load($id);
|
|
}
|
|
|
|
function load($id=0) {
|
|
|
|
if(!$id && !($id=$this->getId()))
|
|
return false;
|
|
|
|
$sql='SELECT * FROM '.API_KEY_TABLE.' WHERE id='.db_input($id);
|
|
if(!($res=db_query($sql)) || !db_num_rows($res))
|
|
return false;
|
|
|
|
$this->ht = db_fetch_array($res);
|
|
$this->id = $this->ht['id'];
|
|
|
|
return true;
|
|
}
|
|
|
|
function reload() {
|
|
return $this->load();
|
|
}
|
|
|
|
function getId() {
|
|
return $this->id;
|
|
}
|
|
|
|
function getKey() {
|
|
return $this->ht['apikey'];
|
|
}
|
|
|
|
function getIPAddr() {
|
|
return $this->ht['ipaddr'];
|
|
}
|
|
|
|
function getNotes() {
|
|
return $this->ht['notes'];
|
|
}
|
|
|
|
function getHashtable() {
|
|
return $this->ht;
|
|
}
|
|
|
|
function isActive() {
|
|
return ($this->ht['isactive']);
|
|
}
|
|
|
|
function canCreateTickets() {
|
|
return ($this->ht['can_create_tickets']);
|
|
}
|
|
|
|
function canExecuteCron() {
|
|
return ($this->ht['can_exec_cron']);
|
|
}
|
|
|
|
function update($vars, &$errors) {
|
|
|
|
if(!API::save($this->getId(), $vars, $errors))
|
|
return false;
|
|
|
|
$this->reload();
|
|
|
|
return true;
|
|
}
|
|
|
|
function delete() {
|
|
$sql='DELETE FROM '.API_KEY_TABLE.' WHERE id='.db_input($this->getId()).' LIMIT 1';
|
|
return (db_query($sql) && ($num=db_affected_rows()));
|
|
}
|
|
|
|
/** Static functions **/
|
|
static function add($vars, &$errors) {
|
|
return API::save(0, $vars, $errors);
|
|
}
|
|
|
|
static function validate($key, $ip) {
|
|
return ($key && $ip && self::getIdByKey($key, $ip));
|
|
}
|
|
|
|
static function getIdByKey($key, $ip='') {
|
|
|
|
$sql='SELECT id FROM '.API_KEY_TABLE.' WHERE apikey='.db_input($key);
|
|
if($ip)
|
|
$sql.=' AND ipaddr='.db_input($ip);
|
|
|
|
if(($res=db_query($sql)) && db_num_rows($res))
|
|
list($id) = db_fetch_row($res);
|
|
|
|
return $id;
|
|
}
|
|
|
|
static function lookupByKey($key, $ip='') {
|
|
return self::lookup(self::getIdByKey($key, $ip));
|
|
}
|
|
|
|
static function lookup($id) {
|
|
return ($id && is_numeric($id) && ($k= new API($id)) && $k->getId()==$id)?$k:null;
|
|
}
|
|
|
|
static function save($id, $vars, &$errors) {
|
|
|
|
if(!$id && (!$vars['ipaddr'] || !Validator::is_ip($vars['ipaddr'])))
|
|
$errors['ipaddr'] = __('Valid IP is required');
|
|
|
|
if($errors) return false;
|
|
|
|
$sql=' updated=NOW() '
|
|
.',isactive='.db_input($vars['isactive'])
|
|
.',can_create_tickets='.db_input($vars['can_create_tickets'])
|
|
.',can_exec_cron='.db_input($vars['can_exec_cron'])
|
|
.',notes='.db_input(Format::sanitize($vars['notes']));
|
|
|
|
if($id) {
|
|
$sql='UPDATE '.API_KEY_TABLE.' SET '.$sql.' WHERE id='.db_input($id);
|
|
if(db_query($sql))
|
|
return true;
|
|
|
|
$errors['err']=sprintf(__('Unable to update %s.'), __('this API key'))
|
|
.' '.__('Internal error occurred');
|
|
|
|
} else {
|
|
$sql='INSERT INTO '.API_KEY_TABLE.' SET '.$sql
|
|
.',created=NOW() '
|
|
.',ipaddr='.db_input($vars['ipaddr'])
|
|
.',apikey='.db_input(strtoupper(md5(time().$vars['ipaddr'].md5(Misc::randCode(16)))));
|
|
|
|
if(db_query($sql) && ($id=db_insert_id()))
|
|
return $id;
|
|
|
|
$errors['err']=sprintf('%s %s',
|
|
sprintf(__('Unable to add %s.'), __('this API key')),
|
|
__('Correct any errors below and try again.'));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Controller for API methods. Provides methods to check to make sure the
|
|
* API key was sent and that the Client-IP and API-Key have been registered
|
|
* in the database, and methods for parsing and validating data sent in the
|
|
* API request.
|
|
*/
|
|
|
|
class ApiController extends Controller {
|
|
|
|
private $sapi;
|
|
private $key;
|
|
|
|
public function __construct($sapi = null) {
|
|
// Set API Interface the request is coming from
|
|
$this->sapi = $sapi ?: (osTicket::is_cli() ? 'cli' : 'http');
|
|
}
|
|
|
|
public function isCli() {
|
|
return (strcasecmp($this->sapi, 'cli') === 0);
|
|
}
|
|
|
|
private function getInputStream() {
|
|
return $this->isCli() ? 'php://stdin' : 'php://input';
|
|
}
|
|
|
|
protected function getRemoteAddr() {
|
|
return $_SERVER['REMOTE_ADDR'];
|
|
}
|
|
|
|
protected function getApiKey() {
|
|
return $_SERVER['HTTP_X_API_KEY'];
|
|
}
|
|
|
|
function requireApiKey() {
|
|
// Require a valid API key sent as X-API-Key HTTP header
|
|
// see getApiKey method.
|
|
if (!($key=$this->getKey()))
|
|
return $this->exerr(401, __('Valid API key required'));
|
|
elseif (!$key->isActive() || $key->getIPAddr() != $this->getRemoteAddr())
|
|
return $this->exerr(401, __('API key not found/active or source IP not authorized'));
|
|
|
|
return $key;
|
|
}
|
|
|
|
function getKey() {
|
|
// Lookup record using sent API Key && IP Addr
|
|
if (!$this->key
|
|
&& ($key=$this->getApiKey())
|
|
&& ($ip=$this->getRemoteAddr()))
|
|
$this->key = API::lookupByKey($key, $ip);
|
|
|
|
return $this->key;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the body of the API request and converts it to a common
|
|
* hashtable. For JSON formats, this is mostly a noop, the conversion
|
|
* work will be done for XML requests
|
|
*/
|
|
function getRequest($format, $validate=true) {
|
|
$input = $this->getInputStream();
|
|
if (!($stream = @fopen($input, 'r')))
|
|
$this->exerr(400, sprintf('%s (%s)',
|
|
__("Unable to read request body"), $input));
|
|
|
|
return $this->parseRequest($stream, $format, $validate);
|
|
}
|
|
|
|
function getEmailRequest() {
|
|
if (!($data=$this->getRequest('email', false)))
|
|
$this->exerr(400, __("Unable to read email request"));
|
|
|
|
return $data;
|
|
}
|
|
|
|
function parseRequest($stream, $format, $validate=true) {
|
|
$parser = null;
|
|
switch(strtolower($format)) {
|
|
case 'xml':
|
|
if (!function_exists('xml_parser_create'))
|
|
return $this->exerr(501, __('XML extension not supported'));
|
|
$parser = new ApiXmlDataParser();
|
|
break;
|
|
case 'json':
|
|
$parser = new ApiJsonDataParser();
|
|
break;
|
|
case 'email':
|
|
$parser = new ApiEmailDataParser();
|
|
break;
|
|
default:
|
|
return $this->exerr(415, __('Unsupported data format'));
|
|
}
|
|
|
|
if (!($data = $parser->parse($stream)) || !is_array($data)) {
|
|
$this->exerr(400, $parser->lastError());
|
|
}
|
|
|
|
//Validate structure of the request.
|
|
if ($validate && $data)
|
|
$this->validate($data, $format, false);
|
|
|
|
return $data;
|
|
}
|
|
|
|
function parseEmail($content) {
|
|
return $this->parseRequest($content, 'email', false);
|
|
}
|
|
|
|
/**
|
|
* Structure to validate the request against -- must be overridden to be
|
|
* useful
|
|
*/
|
|
function getRequestStructure($format, $data=null) { return array(); }
|
|
/**
|
|
* Simple validation that makes sure the keys of a parsed request are
|
|
* expected. It is assumed that the functions actually implementing the
|
|
* API will further validate the contents of the request
|
|
*/
|
|
function validateRequestStructure($data, $structure, $prefix="", $strict=true) {
|
|
global $ost;
|
|
|
|
foreach ($data as $key=>$info) {
|
|
if (is_array($structure) && (is_array($info) || $info instanceof ArrayAccess)) {
|
|
$search = (isset($structure[$key]) && !is_numeric($key)) ? $key : "*";
|
|
if (isset($structure[$search])) {
|
|
$this->validateRequestStructure($info, $structure[$search], "$prefix$key/", $strict);
|
|
continue;
|
|
}
|
|
} elseif (in_array($key, $structure)) {
|
|
continue;
|
|
}
|
|
|
|
$error = sprintf(__("%s: Unexpected data received in API request"),
|
|
"$prefix$key");
|
|
$this->onError(400, $error, __('Unexpected Data'), !$strict);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validate request.
|
|
*
|
|
*/
|
|
function validate(&$data, $format, $strict=true) {
|
|
return $this->validateRequestStructure(
|
|
$data,
|
|
$this->getRequestStructure($format, $data),
|
|
"",
|
|
$strict);
|
|
}
|
|
|
|
protected function debug($subj, $msg) {
|
|
return $this->log($subj, $msg, LOG_DEBUG);
|
|
}
|
|
|
|
protected function logError($subj, $msg, $fatal=false) {
|
|
// If error is not fatal then log it as a warning
|
|
return $this->log($subj, $msg, $fatal ? LOG_ERR : LOG_WARN);
|
|
}
|
|
|
|
protected function log($title, $msg, $level = LOG_WARN) {
|
|
global $ost;
|
|
switch ($level) {
|
|
case LOG_WARN:
|
|
case LOG_ERR:
|
|
// We are disabling email alerts on API errors / warnings
|
|
// due to potential abuse or loops that might cause email DOS
|
|
$ost->log($level, $title, $msg, false);
|
|
break;
|
|
case LOG_DEBUG:
|
|
default:
|
|
$ost->logDebug($title, $msg);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* API error & logging and response!
|
|
*
|
|
* final - don not override downstream.
|
|
*/
|
|
final public function onError($code, $error, $title = null,
|
|
$logOnly = false) {
|
|
|
|
// Unpack the errors to string if error is an array
|
|
if ($error && is_array($error))
|
|
$error = Format::array_implode(": ", "\n", $error);
|
|
|
|
// Log the error
|
|
$msg = $error;
|
|
// TODO: Only include API Key when in debug mode to avoid
|
|
// potentialialy leaking a valid key in system logs
|
|
if (($key=$this->getApiKey()))
|
|
$msg .= "\n[$key]\n";
|
|
|
|
$title = sprintf('%s (%s)', $title ?: __('API Error'), $code);
|
|
$this->logError($title, $msg, $logOnly);
|
|
|
|
// If the error is not deemed fatal then simply return
|
|
if ($logOnly)
|
|
return;
|
|
|
|
// If the API Interface is CLI then throw a TicketApiError exception
|
|
// so the caller can handle the error gracefully.
|
|
// Note that we set the error code as well
|
|
if ($this->isCli())
|
|
throw new TicketApiError($error, $code);
|
|
|
|
// Respond and exit since HTTP endpoint requests
|
|
// are considered stateless
|
|
$this->response($code, $error);
|
|
}
|
|
}
|
|
|
|
include_once "class.xml.php";
|
|
class ApiXmlDataParser extends XmlDataParser {
|
|
|
|
function parse($stream) {
|
|
return $this->fixup(parent::parse($stream));
|
|
}
|
|
/**
|
|
* Perform simple operations to make data consistent between JSON and
|
|
* XML data types
|
|
*/
|
|
function fixup($current) {
|
|
global $cfg;
|
|
|
|
if (isset($current['ticket']))
|
|
$current = $current['ticket'];
|
|
|
|
if (!is_array($current))
|
|
return $current;
|
|
foreach ($current as $key=>&$value) {
|
|
if ($key == "phone" && is_array($value)) {
|
|
$value = $value[":text"];
|
|
} else if ($key == "alert") {
|
|
$value = (bool) (strtolower($value) === 'false' ? false : $value);
|
|
} else if ($key == "autorespond") {
|
|
$value = (bool) (strtolower($value) === 'false' ? false : $value);
|
|
} else if ($key == "message") {
|
|
if (!is_array($value)) {
|
|
$value = array(
|
|
"body" => $value,
|
|
"type" => "text/plain",
|
|
# Use encoding from root <xml> node
|
|
);
|
|
} else {
|
|
$value["body"] = $value[":text"];
|
|
unset($value[":text"]);
|
|
}
|
|
if (isset($value['encoding']))
|
|
$value['body'] = Charset::utf8($value['body'], $value['encoding']);
|
|
|
|
if (!strcasecmp($value['type'], 'text/html'))
|
|
$value = new HtmlThreadEntryBody($value['body']);
|
|
else
|
|
$value = new TextThreadEntryBody($value['body']);
|
|
|
|
} else if ($key == "attachments") {
|
|
if(isset($value['file']) && !isset($value['file'][':text']))
|
|
$value = $value['file'];
|
|
|
|
if($value && is_array($value)) {
|
|
foreach ($value as &$info) {
|
|
$info["data"] = $info[":text"];
|
|
unset($info[":text"]);
|
|
}
|
|
unset($info);
|
|
}
|
|
} else if(is_array($value)) {
|
|
$value = $this->fixup($value);
|
|
}
|
|
}
|
|
unset($value);
|
|
|
|
return $current;
|
|
}
|
|
}
|
|
|
|
include_once "class.json.php";
|
|
class ApiJsonDataParser extends JsonDataParser {
|
|
static function parse($stream, $tidy=false) {
|
|
return self::fixup(parent::parse($stream));
|
|
}
|
|
static function fixup($current) {
|
|
if (!is_array($current))
|
|
return $current;
|
|
foreach ($current as $key=>&$value) {
|
|
if ($key == "phone") {
|
|
$value = strtoupper($value);
|
|
} else if ($key == "alert") {
|
|
$value = (bool)$value;
|
|
} else if ($key == "autorespond") {
|
|
$value = (bool)$value;
|
|
} elseif ($key == "message") {
|
|
// Allow message specified in RFC 2397 format
|
|
$data = Format::strip_emoticons(Format::parseRfc2397($value, 'utf-8'));
|
|
|
|
if (isset($data['type']) && $data['type'] == 'text/html')
|
|
$value = new HtmlThreadEntryBody($data['data']);
|
|
else
|
|
$value = new TextThreadEntryBody($data['data']);
|
|
|
|
} else if ($key == "attachments") {
|
|
foreach ($value as &$info) {
|
|
$data = reset($info);
|
|
# PHP5: fopen("data://$data[5:]");
|
|
$contents = Format::parseRfc2397($data, 'utf-8', false);
|
|
$info = array(
|
|
"data" => $contents['data'],
|
|
"type" => $contents['type'],
|
|
"name" => key($info),
|
|
);
|
|
}
|
|
unset($info);
|
|
}
|
|
}
|
|
unset($value);
|
|
|
|
return $current;
|
|
}
|
|
}
|
|
|
|
/* Email parsing */
|
|
include_once "class.mailparse.php";
|
|
class ApiEmailDataParser extends EmailDataParser {
|
|
|
|
function parse($stream) {
|
|
return $this->fixup(parent::parse($stream));
|
|
}
|
|
|
|
function fixup($data) {
|
|
global $cfg;
|
|
|
|
if (!$data) return $data;
|
|
|
|
$data['source'] = 'Email';
|
|
|
|
if (!$data['subject'])
|
|
$data['subject'] = '[No Subject]';
|
|
|
|
if (!$data['emailId'])
|
|
$data['emailId'] = $cfg->getDefaultEmailId();
|
|
|
|
if( !$cfg->useEmailPriority())
|
|
unset($data['priorityId']);
|
|
|
|
return $data;
|
|
}
|
|
}
|
|
?>
|