You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
453 lines
13 KiB
453 lines
13 KiB
<?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:
|
|
**********************************************************************/
|
|
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 **/
|
|
function add($vars, &$errors) {
|
|
return API::save(0, $vars, $errors);
|
|
}
|
|
|
|
function validate($key, $ip) {
|
|
return ($key && $ip && self::getIdByKey($key, $ip));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
function lookupByKey($key, $ip='') {
|
|
return self::lookup(self::getIdByKey($key, $ip));
|
|
}
|
|
|
|
function lookup($id) {
|
|
return ($id && is_numeric($id) && ($k= new API($id)) && $k->getId()==$id)?$k:null;
|
|
}
|
|
|
|
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 {
|
|
|
|
var $apikey;
|
|
|
|
function requireApiKey() {
|
|
# Validate the API key -- required to be sent via the X-API-Key
|
|
# header
|
|
|
|
if(!($key=$this->getApiKey()))
|
|
return $this->exerr(401, __('Valid API key required'));
|
|
elseif (!$key->isActive() || $key->getIPAddr()!=$_SERVER['REMOTE_ADDR'])
|
|
return $this->exerr(401, __('API key not found/active or source IP not authorized'));
|
|
|
|
return $key;
|
|
}
|
|
|
|
function getApiKey() {
|
|
|
|
if (!$this->apikey && isset($_SERVER['HTTP_X_API_KEY']) && isset($_SERVER['REMOTE_ADDR']))
|
|
$this->apikey = API::lookupByKey($_SERVER['HTTP_X_API_KEY'], $_SERVER['REMOTE_ADDR']);
|
|
|
|
return $this->apikey;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
global $ost;
|
|
|
|
$input = osTicket::is_cli()?'php://stdin':'php://input';
|
|
|
|
if (!($stream = @fopen($input, 'r')))
|
|
$this->exerr(400, __("Unable to read request body"));
|
|
|
|
$parser = null;
|
|
switch(strtolower($format)) {
|
|
case 'xml':
|
|
if (!function_exists('xml_parser_create'))
|
|
$this->exerr(501, __('XML extension not supported'));
|
|
|
|
$parser = new ApiXmlDataParser();
|
|
break;
|
|
case 'json':
|
|
$parser = new ApiJsonDataParser();
|
|
break;
|
|
case 'email':
|
|
$parser = new ApiEmailDataParser();
|
|
break;
|
|
default:
|
|
$this->exerr(415, __('Unsupported data format'));
|
|
}
|
|
|
|
if (!($data = $parser->parse($stream)))
|
|
$this->exerr(400, $parser->lastError());
|
|
|
|
//Validate structure of the request.
|
|
$this->validate($data, $format, false);
|
|
|
|
return $data;
|
|
}
|
|
|
|
function getEmailRequest() {
|
|
return $this->getRequest('email');
|
|
}
|
|
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
if ($strict)
|
|
return $this->exerr(400, sprintf(__("%s: Unexpected data received in API request"), "$prefix$key"));
|
|
else
|
|
$ost->logWarning(__('API Unexpected Data'),
|
|
sprintf(__("%s: Unexpected data received in API request"), "$prefix$key"),
|
|
false);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validate request.
|
|
*
|
|
*/
|
|
function validate(&$data, $format, $strict=true) {
|
|
return $this->validateRequestStructure(
|
|
$data,
|
|
$this->getRequestStructure($format, $data),
|
|
"",
|
|
$strict);
|
|
}
|
|
|
|
/**
|
|
* API error & logging and response!
|
|
*
|
|
*/
|
|
|
|
/* If possible - DO NOT - overwrite the method downstream */
|
|
function exerr($code, $error='') {
|
|
global $ost;
|
|
|
|
if($error && is_array($error))
|
|
$error = Format::array_implode(": ", "\n", $error);
|
|
|
|
//Log the error as a warning - include api key if available.
|
|
$msg = $error;
|
|
if($_SERVER['HTTP_X_API_KEY'])
|
|
$msg.="\n*[".$_SERVER['HTTP_X_API_KEY']."]*\n";
|
|
$ost->logWarning(__('API Error')." ($code)", $msg, false);
|
|
|
|
if (PHP_SAPI == 'cli') {
|
|
fwrite(STDERR, "({$code}) $error\n");
|
|
}
|
|
else {
|
|
$this->response($code, $error); //Responder should exit...
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//Default response method - can be overwritten in subclasses.
|
|
function response($code, $resp) {
|
|
Http::response($code, $resp);
|
|
exit();
|
|
}
|
|
}
|
|
|
|
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($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 {
|
|
function parse($stream, $tidy=false) {
|
|
return $this->fixup(parent::parse($stream));
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
?>
|