-
Jared Hancock authored
This patch rebases filters into a row-based layout and redesigns the filter apply method to be more extensible. It also redesigns the UI to be more dynamic and to allow for actions to be added without database modification and actions can also have complex configurations.
9150e18e
class.filter.php 29.81 KiB
<?php
/*********************************************************************
class.filter.php
Ticket Filter
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:
**********************************************************************/
require_once INCLUDE_DIR . 'class.filter_action.php';
class Filter {
var $id;
var $ht;
static $match_types = array(
/* @trans */ 'User Information' => array(
array('name' => /* @trans */ 'Name',
'email' => /* @trans */ 'Email',
),
900
),
/* @trans */ 'Email Meta-Data' => array(
array('reply-to' => /* @trans */ 'Reply-To Email',
'reply-to-name' => /* @trans */ 'Reply-To Name',
'addressee' => /* @trans */ 'Addressee (To and Cc)',
),
200
),
);
function Filter($id) {
$this->id=0;
$this->load($id);
}
function load($id=0) {
if(!$id && !($id=$this->getId()))
return false;
$sql='SELECT filter.*,count(rule.id) as rule_count '
.' FROM '.FILTER_TABLE.' filter '
.' LEFT JOIN '.FILTER_RULE_TABLE.' rule ON(rule.filter_id=filter.id) '
.' WHERE filter.id='.db_input($id)
.' GROUP BY filter.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($this->getId());
}
function getId() {
return $this->id;
}
function getTarget() {
return $this->ht['target'];
}
function getName() {
return $this->ht['name'];
}
function getNotes() {
return $this->ht['notes'];
}
function getInfo() {
return $this->ht;
}
function getNumRules() {
return $this->ht['rule_count'];
}
function getExecOrder() {
return $this->ht['execorder'];
}
function getEmailId() {
return $this->ht['email_id'];
}
function isActive() {
return ($this->ht['isactive']);
}
function isSystemBanlist() {
return !strcasecmp($this->getName(),'SYSTEM BAN LIST');
}
function getDeptId() {
return $this->ht['dept_id'];
}
function getStatusId() {
return $this->ht['status_id'];
}
function getPriorityId() {
return $this->ht['priority_id'];
}
function getSLAId() {
return $this->ht['sla_id'];
}
function getStaffId() {
return $this->ht['staff_id'];
}
function getTeamId() {
return $this->ht['team_id'];
}
function getCannedResponse() {
return $this->ht['canned_response_id'];
}
function getHelpTopic() {
return $this->ht['topic_id'];
}
function stopOnMatch() {
return ($this->ht['stop_onmatch']);
}
function matchAllRules() {
return ($this->ht['match_all_rules']);
}
function rejectOnMatch() {
return ($this->ht['reject_ticket']);
}
function useReplyToEmail() {
return ($this->ht['use_replyto_email']);
}
function disableAlerts() {
return ($this->ht['disable_autoresponder']);
}
function sendAlerts() {
return (!$this->disableAlerts());
}
function getRules() {
if (!$this->ht['rules']) {
$rules=array();
//We're getting the rules...live because it gets cleared on update.
$sql='SELECT * FROM '.FILTER_RULE_TABLE.' WHERE filter_id='.db_input($this->getId());
if(($res=db_query($sql)) && db_num_rows($res)) {
while($row=db_fetch_array($res))
$rules[]=array('w'=>$row['what'],'h'=>$row['how'],'v'=>$row['val']);
}
$this->ht['rules'] = $rules;
}
return $this->ht['rules'];
}
function getFlatRules() { //Format used on html... I'm ashamed
$info=array();
if(($rules=$this->getRules())) {
foreach($rules as $k=>$rule) {
$i=$k+1;
$info["rule_w$i"]=$rule['w'];
$info["rule_h$i"]=$rule['h'];
$info["rule_v$i"]=$rule['v'];
}
}
return $info;
}
function addRule($what, $how, $val,$extra=array()) {
$errors = array();
$rule= array_merge($extra,array('what'=>$what, 'how'=>$how, 'val'=>$val));
$rule['filter_id']=$this->getId();
return FilterRule::create($rule,$errors);
}
function removeRule($what, $how, $val) {
$sql='DELETE FROM '.FILTER_RULE_TABLE
.' WHERE filter_id='.db_input($this->getId())
.' AND what='.db_input($what)
.' AND how='.db_input($how)
.' AND val='.db_input($val);
return (db_query($sql) && db_affected_rows());
}
function getRule($id) {
return $this->getRuleById($id);
}
function getRuleById($id) {
return FilterRule::lookup($id,$this->getId());
}
function containsRule($what, $how, $val) {
$val = trim($val);
if (isset($this->ht['rules'])) {
$match = array("w"=>$what, "h"=>$how, "v"=>$val);
foreach ($this->ht['rules'] as $rule) {
if ($match == $rule)
return true;
}
return false;
} else {
# Fetch from database
return 0 != db_count(
"SELECT COUNT(*) FROM ".FILTER_RULE_TABLE
." WHERE filter_id=".db_input($this->id)
." AND what=".db_input($what)." AND how=".db_input($how)
." AND val=".db_input($val)
);
}
}
/**
* Simple true/false if the rules defined for this filter match the
* incoming email
*
* $info is an ARRAY, which has valid keys
* email - FROM email address of the ticket owner
* name - name of ticket owner
* subject - subject line of the ticket
* body - body content of the message (no attachments, please)
* reply-to - reply-to email address
* reply-to-name - name of sender to reply-to
* headers - array of email headers
* emailId - osTicket system email id
*/
function matches($what) {
if(!$what || !is_array($what)) return false;
$how = array(
# how => array(function, null or === this, null or !== this)
'equal' => array('strcasecmp', 0),
'not_equal' => array('strcasecmp', null, 0),
'contains' => array('stripos', null, false),
'dn_contain'=> array('stripos', false),
'starts' => array('stripos', 0),
'ends' => array('iendsWith', true),
'match' => array('pregMatchB', 1),
'not_match' => array('pregMatchB', null, 0),
);
$match = false;
# Respect configured filter email-id
if ($this->getEmailId()
&& !strcasecmp($this->getTarget(), 'Email')
&& $this->getEmailId() != $what['emailId'])
return false;
foreach ($this->getRules() as $rule) {
if (!isset($how[$rule['h']])) continue;
list($func, $pos, $neg) = $how[$rule['h']];
$result = call_user_func($func, $what[$rule['w']], $rule['v']);
if (($pos === null && $result !== $neg) or ($result === $pos)) {
# Match.
$match = true;
if (!$this->matchAllRules()) break;
} else {
# No match. Continue?
if ($this->matchAllRules()) {
$match = false;
break;
}
}
}
return $match;
}
function getActions() {
return FilterAction::objects()->filter(array(
'filter_id'=>$this->getId()
));
}
/**
* If the matches() method returns TRUE, send the initial ticket to this
* method to apply the filter actions defined
*/
function apply(&$ticket, $info=null) {
foreach ($this->getActions() as $a) {
$a->apply($ticket, $info);
}
}
static function getSupportedMatches() {
foreach (static::$match_types as $k=>&$v) {
if (is_callable($v[0]))
$v[0] = $v[0]();
}
unset($v);
uasort(static::$match_types, function($a, $b) { return $a[1] - $b[1]; });
return array_map(function($a) { return $a[0]; }, static::$match_types);
}
static function addSupportedMatches($group, $callable, $order=10) {
static::$match_types[$group] = array($callable, $order);
}
static function getSupportedMatchFields() {
$keys = array();
foreach (static::getSupportedMatches() as $group=>$matches) {
foreach ($matches as $key=>$label) {
$keys[] = $key;
}
}
return $keys;
}
/* static */ function getSupportedMatchTypes() {
return array(
'equal'=> __('Equal'),
'not_equal'=> __('Not Equal'),
'contains'=> __('Contains'),
'dn_contain'=> __('Does Not Contain'),
'starts'=> __('Starts With'),
'ends'=> __('Ends With'),
'match'=> __('Matches Regex'),
'not_match'=> __('Does Not Match Regex'),
);
}
function update($vars,&$errors) {
if(!Filter::save($this->getId(),$vars,$errors))
return false;
$this->reload();
return true;
}
function delete() {
$id=$this->getId();
$sql='DELETE FROM '.FILTER_TABLE.' WHERE id='.db_input($id).' LIMIT 1';
if(db_query($sql) && ($num=db_affected_rows())) {
db_query('DELETE FROM '.FILTER_RULE_TABLE.' WHERE filter_id='.db_input($id));
}
return $num;
}
/** static functions **/
function getTargets() {
return array(
'Any' => __('Any'),
'Web' => __('Web Forms'),
'API' => __('API Calls'),
'Email' => __('Emails'));
}
function create($vars,&$errors) {
return Filter::save(0,$vars,$errors);
}
function getIdByName($name) {
$sql='SELECT id FROM '.FILTER_TABLE.' WHERE name='.db_input($name);
if(($res=db_query($sql)) && db_num_rows($res))
list($id)=db_fetch_row($res);
return $id;
}
function lookup($id) {
return ($id && is_numeric($id) && ($f= new Filter($id)) && $f->getId()==$id)?$f:null;
}
function validate_rules($vars,&$errors) {
return self::save_rules(0,$vars,$errors);
}
function save_rules($id,$vars,&$errors) {
$matches = array_keys(self::getSupportedMatchFields());
$types = array_keys(self::getSupportedMatchTypes());
$rules=array();
for($i=1; $i<=25; $i++) { //Expecting no more than 25 rules...
if($vars["rule_w$i"] || $vars["rule_h$i"]) {
// Check for REGEX compile errors
if (in_array($vars["rule_h$i"], array('match','not_match'))) {
$wrapped = "/".$vars["rule_v$i"]."/iu";
if (false === @preg_match($vars["rule_v$i"], ' ')
&& (false !== @preg_match($wrapped, ' ')))
$vars["rule_v$i"] = $wrapped;
}
if(!$vars["rule_w$i"] || !in_array($vars["rule_w$i"],$matches))
$errors["rule_$i"]=__('Invalid match selection');
elseif(!$vars["rule_h$i"] || !in_array($vars["rule_h$i"],$types))
$errors["rule_$i"]=__('Invalid match type selection');
elseif(!$vars["rule_v$i"])
$errors["rule_$i"]=__('Value required');
elseif($vars["rule_w$i"]=='email'
&& $vars["rule_h$i"]=='equal'
&& !Validator::is_email($vars["rule_v$i"]))
$errors["rule_$i"]=__('Valid email required for the match type');
elseif (in_array($vars["rule_h$i"], array('match','not_match'))
&& (false === @preg_match($vars["rule_v$i"], ' ')))
$errors["rule_$i"] = sprintf(__('Regex compile error: (#%s)'),
preg_last_error());
else //for everything-else...we assume it's valid.
$rules[]=array('what'=>$vars["rule_w$i"],
'how'=>$vars["rule_h$i"],'val'=>$vars["rule_v$i"]);
}elseif($vars["rule_v$i"]) {
$errors["rule_$i"]=__('Incomplete selection');
}
}
if(!$rules && is_array($vars["rules"]))
# XXX: Validation bypass
$rules = $vars["rules"];
elseif(!$rules && !$errors)
$errors['rules']=__('You must set at least one rule.');
if($errors) return false;
if(!$id) return true; //When ID is 0 then assume it was just validation...
//Clear existing rules...we're doing mass replace on each save!!
db_query('DELETE FROM '.FILTER_RULE_TABLE.' WHERE filter_id='.db_input($id));
$num=0;
foreach($rules as $rule) {
$rule['filter_id']=$id;
if(FilterRule::create($rule, $errors))
$num++;
}
return $num;
}
function save($id,$vars,&$errors) {
if(!$vars['execorder'])
$errors['execorder'] = __('Order required');
elseif(!is_numeric($vars['execorder']))
$errors['execorder'] = __('Must be numeric value');
if(!$vars['name'])
$errors['name'] = __('Name required');
elseif(($sid=self::getIdByName($vars['name'])) && $sid!=$id)
$errors['name'] = __('Name already in use');
if(!$errors && !self::validate_rules($vars,$errors) && !$errors['rules'])
$errors['rules'] = __('Unable to validate rules as entered');
$targets = self::getTargets();
if(!$vars['target'])
$errors['target'] = __('Target required');
else if(!is_numeric($vars['target']) && !$targets[$vars['target']])
$errors['target'] = __('Unknown or invalid target');
if($errors) return false;
$emailId = 0;
if(is_numeric($vars['target'])) {
$emailId = $vars['target'];
$vars['target'] = 'Email';
}
$sql=' updated=NOW() '
.',isactive='.db_input($vars['isactive'])
.',target='.db_input($vars['target'])
.',name='.db_input($vars['name'])
.',execorder='.db_input($vars['execorder'])
.',email_id='.db_input($emailId)
.',match_all_rules='.db_input($vars['match_all_rules'])
.',stop_onmatch='.db_input(isset($vars['stop_onmatch'])?1:0)
.',reject_ticket='.db_input(isset($vars['reject_ticket'])?1:0)
.',notes='.db_input(Format::sanitize($vars['notes']));
//Auto assign ID is overloaded...
if($vars['assign'] && $vars['assign'][0]=='s')
$sql.=',team_id=0,staff_id='.db_input(preg_replace("/[^0-9]/", "",$vars['assign']));
elseif($vars['assign'] && $vars['assign'][0]=='t')
$sql.=',staff_id=0,team_id='.db_input(preg_replace("/[^0-9]/", "",$vars['assign']));
else
$sql.=',staff_id=0,team_id=0 '; //no auto-assignment!
if($id) {
$sql='UPDATE '.FILTER_TABLE.' SET '.$sql.' WHERE id='.db_input($id);
if(!db_query($sql))
$errors['err']=sprintf(__('Unable to update %s.'), __('this ticket filter'))
.' '.__('Internal error occurred');
}else{
$sql='INSERT INTO '.FILTER_TABLE.' SET '.$sql.',created=NOW() ';
if(!db_query($sql) || !($id=db_insert_id()))
$errors['err']=sprintf(__('Unable to add %s.'), __('this ticket filter'))
.' '.__('Internal error occurred');
}
if($errors || !$id) return false;
//Success with update/create...save the rules. We can't recover from any errors at this point.
# Don't care about errors stashed in $xerrors
$xerrors = array();
self::save_rules($id,$vars,$xerrors);
self::save_actions($id, $vars, $errors);
return true;
}
function save_actions($id, $vars, &$errors) {
if (!is_array(@$vars['actions']))
return;
foreach ($vars['actions'] as $v) {
$info = substr($v, 1);
switch ($v[0]) {
case 'N': # new filter action
$I = FilterAction::create(array(
'type'=>$info,
'filter_id'=>$id,
));
$I->setConfiguration($errors);
$I->save();
break;
case 'I': # exiting filter action
if ($I = FilterAction::lookup($info))
$I->setConfiguration() && $I->save();
break;
case 'D': # deleted filter action
if ($I = FilterAction::lookup($info))
$I->delete();
break;
}
}
}
}
class FilterRule {
var $id;
var $ht;
var $filter;
function FilterRule($id,$filterId=0) {
$this->id=0;
$this->load($id,$filterId);
}
function load($id,$filterId=0) {
$sql='SELECT rule.* FROM '.FILTER_RULE_TABLE.' rule '
.' WHERE rule.id='.db_input($id);
if($filterId)
$sql.=' AND rule.filter_id='.db_input($filterId);
if(!($res=db_query($sql)) || !db_num_rows($res))
return false;
$this->ht=db_fetch_array($res);
$this->id=$this->ht['id'];
$this->filter=null;
return true;
}
function reload() {
return $this->load($this->getId());
}
function getId() {
return $this->id;
}
function isActive() {
return ($this->ht['isactive']);
}
function getHashtable() {
return $this->ht;
}
function getInfo() {
return $this->getHashtable();
}
function getFilterId() {
return $this->ht['filter_id'];
}
function getFilter() {
if(!$this->filter && $this->getFilterId())
$this->filter = Filter::lookup($this->getFilterId());
return $this->filter;
}
function update($vars,&$errors) {
if(!$this->save($this->getId(),$vars,$errors))
return false;
$this->reload();
return true;
}
function delete() {
$sql='DELETE FROM '.FILTER_RULE_TABLE.' WHERE id='.db_input($this->getId()).' AND filter_id='.db_input($this->getFilterId());
return (db_query($sql) && db_affected_rows());
}
/* static */ function create($vars,&$errors) {
return self::save(0,$vars,$errors);
}
/* static private */ function save($id,$vars,&$errors) {
if(!$vars['filter_id'])
$errors['err']=__('Parent filter ID required');
if($errors) return false;
$sql=' updated=NOW() '.
',what='.db_input($vars['what']).
',how='.db_input($vars['how']).
',val='.db_input($vars['val']).
',isactive='.db_input(isset($vars['isactive'])?$vars['isactive']:1);
if(isset($vars['notes']))
$sql.=',notes='.db_input($vars['notes']);
if($id) {
$sql='UPDATE '.FILTER_RULE_TABLE.' SET '.$sql.' WHERE id='.db_input($id).' AND filter_id='.db_input($vars['filter_id']);
if(db_query($sql))
return true;
} else {
$sql='INSERT INTO '.FILTER_RULE_TABLE.' SET created=NOW(), filter_id='.db_input($vars['filter_id']).', '.$sql;
if(db_query($sql) && ($id=db_insert_id()))
return $id;
}
return false;
}
/* static */ function lookup($id,$filterId=0) {
return ($id && is_numeric($id) && ($r= new FilterRule($id,$filterId)) && $r->getId()==$id)?$r:null;
}
}
/**
* Applies rules defined in the admin control panel > Settings tab > "Ticket Filters". Each
* filter can have up to 25 rules (*currently). This will attempt to match
* the incoming tickets against the defined rules, and, if the email matches,
* the ticket will be modified as described in the filter actions.
*/
class TicketFilter {
var $target;
var $vars;
/**
* Construct a list of filters to handle a new ticket
* taking into account the source/origin of the ticket.
*
* $vars is an ARRAY, which has valid keys
* *email - email address of user
* name - name of user
* subject - subject of the ticket
* emailId - id of osTicket's system email (for emailed tickets)
* ---------------
* @see Filter::matches() for a complete list of supported keys
*/
function TicketFilter($origin, $vars=array()) {
//Normalize the target based on ticket's origin.
$this->target = self::origin2target($origin);
//Extract the vars we care about (fields we filter by!).
$this->vars = array('body'=>$vars['message']);
$interest = Filter::getSupportedMatchFields();
// emailId is always significant to the filter process
$interest[] = 'emailId';
foreach ($vars as $k=>$v) {
if (in_array($k, $interest))
$this->vars[$k] = trim($v);
}
if (isset($vars['recipients']) && $vars['recipients']) {
foreach ($vars['recipients'] as $r) {
$this->vars['addressee'][] = $r['name'];
$this->vars['addressee'][] = $r['email'];
}
$this->vars['addressee'] = implode(' ', $this->vars['addressee']);
}
//Init filters.
$this->build();
}
function build() {
//Clear any memoized filters
$this->filters = array();
$this->short_list = null;
//Query DB for "possibly" matching filters.
$res = $this->getAllActive();
if($res) {
while (list($id) = db_fetch_row($res))
$this->filters[] = new Filter($id);
}
return $this->filters;
}
function getTarget() {
return $this->target;
}
/**
* Fetches the short list of filters that match the ticket vars received in the
* constructor. This function is memoized so subsequent calls will
* return immediately.
*/
function getMatchingFilterList() {
if (!isset($this->short_list)) {
$this->short_list = array();
foreach ($this->filters as $filter)
if ($filter->matches($this->vars))
$this->short_list[] = $filter;
}
return $this->short_list;
}
/**
* Determine if any filters match the received email, and if so, apply
* actions defined in those filters to the ticket-to-be-created.
*
* Throws:
* RejectedException if the email should not be acceptable. If the email
* should be rejected, the first filter that matches and has reject
* ticket set is returned.
*/
function apply(&$ticket) {
foreach ($this->getMatchingFilterList() as $filter) {
if ($filter->rejectOnMatch())
throw new RejectedException($filter, $ticket);
$filter->apply($ticket, $this->vars);
if ($filter->stopOnMatch()) break;
}
}
function getAllActive() {
$sql='SELECT id FROM '.FILTER_TABLE
.' WHERE isactive=1 '
.' AND target IN ("Any", '.db_input($this->getTarget()).') ';
#Take into account email ID.
if($this->vars['emailId'])
$sql.=' AND (email_id=0 OR email_id='.db_input($this->vars['emailId']).')';
$sql.=' ORDER BY execorder';
return db_query($sql);
}
/**
* Quick function to determine if the received email-address is
* indicated by an active email filter to be banned. Returns the id of
* the filter that has the address blacklisted and FALSE if the email is
* not blacklisted.
*
* XXX: If more detailed matching is to be supported, perhaps this
* should receive an array like the constructor and
* Filter::matches() method.
* Peter - Let's keep it as a quick scan for obviously banned emails.
*/
/* static */
function isBanned($addr) {
$sql='SELECT filter.id, what, how, UPPER(val) '
.' FROM '.FILTER_TABLE.' filter'
.' INNER JOIN '.FILTER_RULE_TABLE.' rule'
.' ON (filter.id=rule.filter_id)'
.' WHERE filter.reject_ticket'
.' AND filter.match_all_rules=0'
.' AND filter.email_id=0'
.' AND filter.isactive'
.' AND rule.isactive '
.' AND rule.what="email"'
.' AND LOCATE(rule.val,'.db_input($addr).')';
if(!($res=db_query($sql)) || !db_num_rows($res))
return false;
# XXX: Use MB_xxx function for proper unicode support
$addr = strtoupper($addr);
$how=array('equal' => array('strcmp', 0),
'contains' => array('strpos', null, false));
while ($row=db_fetch_array($res)) {
list($func, $pos, $neg) = $how[$row['how']];
if (!$func) continue;
$result = call_user_func($func, $addr, $row['val']);
if (($neg === null && $result === $pos) || $result !== $neg)
return $row['id'];
}
return false;
}
/**
* Simple true/false if the headers of the email indicate that the email
* is an automatic response.
*
* Thanks to http://wiki.exim.org/EximAutoReply
* X-Auto-Response-Supress is outlined here,
* http://msdn.microsoft.com/en-us/library/ee219609(v=exchg.80).aspx
*/
/* static */
function isAutoReply($headers) {
if($headers && !is_array($headers))
$headers = Mail_Parse::splitHeaders($headers);
$auto_headers = array(
'Auto-Submitted' => array('AUTO-REPLIED', 'AUTO-GENERATED'),
'Precedence' => array('AUTO_REPLY', 'BULK', 'JUNK', 'LIST'),
'X-Precedence' => array('AUTO_REPLY', 'BULK', 'JUNK', 'LIST'),
'X-Autoreply' => 'YES',
'X-Auto-Response-Suppress' => array('ALL', 'DR', 'RN', 'NRN', 'OOF', 'AutoReply'),
'X-Autoresponse' => '*',
'X-AutoReply-From' => '*',
'X-Autorespond' => '*',
'X-Mail-Autoreply' => '*',
'X-Autogenerated' => 'REPLY',
'X-AMAZON-MAIL-RELAY-TYPE' => 'NOTIFICATION',
);
foreach ($auto_headers as $header=>$find) {
if(!isset($headers[$header])) continue;
$value = strtoupper($headers[$header]);
# Search text must be found at the beginning of the header
# value. This is especially import for something like the
# subject line, where something like an autoreponse may
# appear somewhere else in the value.
if (is_array($find)) {
foreach ($find as $f)
if (strpos($value, $f) === 0)
return true;
} elseif ($find === '*') {
return true;
} elseif (strpos($value, $find) === 0) {
return true;
}
}
return false;
}
static function isBounce($headers) {
if($headers && !is_array($headers))
$headers = Mail_Parse::splitHeaders($headers);
$bounce_headers = array(
'From' => array('stripos',
array('MAILER-DAEMON', '<>'), null, false),
'Subject' => array('stripos',
array('DELIVERY FAILURE', 'DELIVERY STATUS',
'UNDELIVERABLE:', 'Undelivered Mail Returned'), 0),
'Return-Path' => array('strcmp', array('<>'), 0),
'Content-Type' => array('stripos', array('report-type=delivery-status'), null, false),
'X-Failed-Recipients' => array('strpos', array('@'), null, false)
);
foreach ($bounce_headers as $header => $find) {
if(!isset($headers[$header])) continue;
@list($func, $searches, $pos, $neg) = $find;
if(!($value = $headers[$header]) || !is_array($searches))
continue;
foreach ($searches as $f) {
$result = call_user_func($func, $value, $f);
if (($pos === null && $result !== $neg) or ($result === $pos))
return true;
}
}
return false;
}
/**
* Normalize ticket source to supported filter target
*
*/
function origin2target($origin) {
$sources=array('web' => 'Web', 'email' => 'Email', 'phone' => 'Web', 'staff' => 'Web', 'api' => 'API');
return $sources[strtolower($origin)];
}
}
class RejectedException extends Exception {
var $filter;
var $vars;
function __construct(Filter $filter, $vars) {
parent::__construct('Ticket rejected by a filter');
$this->filter = $filter;
$this->vars = $vars;
}
function getRejectingFilter() {
return $this->filter;
}
function get($what) {
return $this->vars[$what];
}
}
class FilterDataChanged extends Exception {}
/**
* Function: endsWith
*
* Returns TRUE if the haystack ends with needle and FALSE otherwise.
* Thanks, http://stackoverflow.com/a/834355
*/
function iendsWith($haystack, $needle)
{
$length = mb_strlen($needle);
if ($length == 0) {
return true;
}
return (strcasecmp(mb_substr($haystack, -$length), $needle) === 0);
}
function pregMatchB($subject, $pattern) {
return preg_match($pattern, $subject);
}
?>