-
Jared Hancock authored
Here's an example email structure: ``` multipart/mixed - multipart/report; delivery-status - multipart/alternative - text/plain - text/html - message/delivery-status - message/rfc822 ``` The previous code would only find the body if the email main headers had: Content-Type: multipart/report; report-type="delivery-status". In such a case it would scan for a plain/text body. This patch will scan for the usual body if the scan for the body as usual if the report scan did not find anything. Also, output errors to stderr when running API from the command line
Jared Hancock authoredHere's an example email structure: ``` multipart/mixed - multipart/report; delivery-status - multipart/alternative - text/plain - text/html - message/delivery-status - message/rfc822 ``` The previous code would only find the body if the email main headers had: Content-Type: multipart/report; report-type="delivery-status". In such a case it would scan for a plain/text body. This patch will scan for the usual body if the scan for the body as usual if the report scan did not find anything. Also, output errors to stderr when running API from the command line
class.api.php 13.03 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 API($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(__('Unable to add %s. Correct error(s) below and try again.'),
__('this API key'));
}
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 = $ost->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 HtmlThreadBody($value['body']);
else
$value = new TextThreadBody($value['body']);
} else if ($key == "attachments") {
if(!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) {
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::parseRfc2397($value, 'utf-8');
if (isset($data['type']) && $data['type'] == 'text/html')
$value = new HtmlThreadBody($data['data']);
else
$value = new TextThreadBody($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;
}
}
?>