117 changes: 94 additions & 23 deletions include/ajax.tickets.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function lookup() {
'entries' => SqlAggregate::COUNT('thread__entries__id', true),
))
->order_by(SqlAggregate::SUM(new SqlCode('Z1.relevance')), QuerySet::DESC)
->distinct('user__default_email__address')
->limit($limit);

$q = $_REQUEST['q'];
Expand All @@ -55,26 +56,15 @@ function lookup() {
$hits = $ost->searcher->find($q, $hits, false);

if (preg_match('/\d{2,}[^*]/', $q, $T = array())) {
$hits = Ticket::objects()
->values('user__default_email__address', 'number', 'cdata__subject', 'user__name', 'ticket_id', 'thread__id', 'flags')
->annotate(array(
'tickets' => new SqlCode('1'),
'tasks' => SqlAggregate::COUNT('tasks__id', true),
'collaborators' => SqlAggregate::COUNT('thread__collaborators__id', true),
'entries' => SqlAggregate::COUNT('thread__entries__id', true),
))
->filter($visibility)
->filter(array('number__startswith' => $q))
->order_by('number')
->limit($limit)
->union($hits);
$hits = $this->lookupByNumber($limit, $visibility, $hits);
}
elseif (!count($hits) && preg_match('`\w$`u', $q)) {
// Do wild-card fulltext search
$_REQUEST['q'] = $q.'*';
return $this->lookup();
}

// TODO: Why not aggregate based on relationship?
foreach ($hits as $T) {
$email = $T['user__default_email__address'];
$count = $T['tickets'];
Expand Down Expand Up @@ -102,6 +92,58 @@ function lookup() {
return $this->json_encode($tickets);
}

function lookupByNumber($limit=false, $visibility=false, $matches=false) {
global $thisstaff;

if (!$limit)
$limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;

$tickets=array();
// Bail out of query is empty
if (!$_REQUEST['q'])
return $this->json_encode($tickets);

$q = trim($_REQUEST['q']);

if (!$visibility)
$visibility = $thisstaff->getTicketsVisibility();

$hits = Ticket::objects()
->values('user__default_email__address', 'number', 'cdata__subject', 'user__name', 'ticket_id', 'thread__id', 'flags')
->annotate(array(
'tickets' => new SqlCode('1'),
'tasks' => SqlAggregate::COUNT('tasks__id', true),
'collaborators' => SqlAggregate::COUNT('thread__collaborators__id', true),
'entries' => SqlAggregate::COUNT('thread__entries__id', true),
))
->filter($visibility)
->filter(array('number__startswith' => $q))
->order_by('number')
->limit($limit);

if ($matches && is_a($matches, 'QuerySet'))
return $hits->union($matches);

foreach ($hits as $T) {
$email = $T['user__default_email__address'];
$tickets[$T['number']] = array('id'=>$T['number'], 'value'=>$T['number'],
'ticket_id'=>$T['ticket_id'],
'info'=>"{$T['number']} — {$email}",
'subject'=>$T['cdata__subject'],
'user'=>$T['user__name'],
'tasks'=>$T['tasks'],
'thread_id'=>$T['thread__id'],
'collaborators'=>$T['collaborators'],
'entries'=>$T['entries'],
'mergeType'=>Ticket::getMergeTypeByFlag($T['flags']),
'children'=>count(Ticket::getChildTickets($T['ticket_id'])) > 0 ? true : false,
'matches'=>$_REQUEST['q']);
}
$tickets = array_values($tickets);

return $this->json_encode($tickets);
}

function acquireLock($tid) {
global $cfg, $thisstaff;

Expand Down Expand Up @@ -384,12 +426,12 @@ function updateMerge($ticket_id) {
if (is_numeric($key) && $ticket = Ticket::lookup($value))
$ticket->unlink();
}
return true;
Http::response(201, 'Successfully managed');
} elseif ($_POST['tids']) {
if ($parent = Ticket::merge($_POST))
Http::response(201, 'Successfully managed');
else
$info['error'] = $errors['err'] ?: __('Unable to merge ticket');
Http::response(404, 'Unable to manage ticket');
}

$parentModel = Ticket::objects()
Expand Down Expand Up @@ -565,7 +607,7 @@ function refer($tid, $target=null) {
if ($v[0] == '-')
$remove[] = substr($v, 1);
if (count($remove)) {
$num = $ticket->thread->referrals
$num = $ticket->getThread()->referrals
->filter(array('id__in' => $remove))
->delete();
if ($num) {
Expand All @@ -590,7 +632,7 @@ function refer($tid, $target=null) {
}

function editField($tid, $fid) {
global $thisstaff;
global $cfg, $thisstaff;

if (!($ticket=Ticket::lookup($tid)))
Http::response(404, __('No such ticket'));
Expand Down Expand Up @@ -635,18 +677,44 @@ function editField($tid, $fid) {
case $field instanceof DepartmentField:
$clean = (string) Dept::lookup($field->getClean());
break;
case $field instanceof TextareaField:
$clean = (string) $field->getClean();
$clean = Format::striptags($clean) ? $clean : '—' . __('Empty') . '—';
if (strlen($clean) > 200)
$clean = Format::truncate($clean, 200);
break;
case $field instanceof BooleanField:
$clean = $field->toString($field->getClean());
break;
default:
$clean = $field->getClean();
$clean = is_array($clean) ? implode($clean, ',') :
(string) $clean;
if (strlen($clean) > 200)
$clean = Format::truncate($clean, 200);
}

// Set basic response data
$response = array(
'value' => $clean ?: '—' . __('Empty') . '—',
'id' => $fid, 'msg' => $msg
);

// If we require HT to close, the ticket is open, the staff has permission
// to close, and we set a HT - ensure we provide all available statuses
if ($cfg->requireTopicToClose() && $ticket->isOpen()
&& $ticket->checkStaffPerm($thisstaff, Ticket::PERM_CLOSE)
&& ($field instanceof TopicField) && $clean) {
$statuses = array();
foreach (TicketStatusList::getStatuses(
array('states' => array('closed'))) as $s) {
if (!$s->isEnabled()) continue;
$statuses[$s->getId()] = $s->getName();
}
if (!is_null($statuses))
$response['statuses'] = $statuses;
}

$clean = is_array($clean) ? $clean[0] : $clean;
Http::response(201, $this->json_encode(['value' =>
$clean ?: '—' . __('Empty') . '—',
'id' => $fid, 'msg' => $msg]));
Http::response(201, $this->json_encode($response));
}

$form->addErrors($errors);
Expand Down Expand Up @@ -1646,7 +1714,7 @@ private function _changeTicketStatus($ticket, $state, $info=array(), $errors=arr
$info['comments'] = Format::htmlchars($_REQUEST['comments']);

// Has Children?
$info['children'] = ($ticket->getChildren()->count());
$info['children'] = count($ticket->getChildren());

return self::_changeStatus($state, $info, $errors);
}
Expand Down Expand Up @@ -1861,6 +1929,9 @@ function export($id) {
else
$queue = AdhocSearch::load($id);

if (!$queue)
Http::response(404, 'Unknown Queue');

return $this->queueExport($queue);
}

Expand Down
2 changes: 1 addition & 1 deletion include/class.api.php
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ function fixup($current) {
$value = (bool)$value;
} elseif ($key == "message") {
// Allow message specified in RFC 2397 format
$data = Format::parseRfc2397($value, 'utf-8');
$data = Format::strip_emoticons(Format::parseRfc2397($value, 'utf-8'));

if (isset($data['type']) && $data['type'] == 'text/html')
$value = new HtmlThreadEntryBody($data['data']);
Expand Down
24 changes: 18 additions & 6 deletions include/class.businesshours.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,25 +52,30 @@ public function getSchedule() {
}

// Intitialize occurrenses buckets
private function initOccurrences(Datetime $date) {
private function initOccurrences(Datetime $date, $grace_period_hrs=72) {

$dt = clone $date;
// Start from next day if current date is already processed
if ($this->workhours[$dt->format('Y-m-d')])
if (isset($this->workhours[$dt->format('Y-m-d')]))
$dt->modify('+1 day');

// Apply grace period
$period = clone $dt;
$period->modify("+$grace_period_hrs hour");

// Reset occurrense buckets
$this->workhours = $this->holidays = array();
// Init workhours
foreach ($this->getSchedule()->getEntries() as $entry)
$this->workhours += $entry->getOccurrences($dt, null, 4);
$this->workhours += $entry->getOccurrences($dt,
$period->format('Y-m-d'));
ksort($this->workhours);
// Init holidays taking into account end date of the workhours in
// the current scope.
$enddate = array_pop(array_keys($this->workhours));
foreach ($this->getSchedule()->getHolidaysSchedules() as $schedule) {
foreach ($schedule->getEntries() as $entry)
$this->holidays += $entry->getOccurrences($dt, $enddate, 5);
$this->holidays += $entry->getOccurrences($dt, $enddate);
}
ksort($this->holidays);

Expand Down Expand Up @@ -116,7 +121,13 @@ public function addWorkingHours(Datetime $date, $hours, &$auditlog=array()) {
// partial / remaining hours
if (!$e->isBeforeHours($date))
$partial = true;

} elseif (strtotime("$d ".$e->getEndsTime()) <
$date->getTimestamp()) {
// Entry is out of scope
continue;
}

// Handle holidays - if within scope of current work day
$leadtime =0;
if (($holiday=$this->holidays[$d])) {
Expand All @@ -125,10 +136,11 @@ public function addWorkingHours(Datetime $date, $hours, &$auditlog=array()) {
$holiday->getHours(),
$holiday->getSchedule()->getName(),
$holiday->getDesc()));

//Move the date to end of the day of the holiday
$date->modify("$d ".$holiday->getEndsTime());
// If the holiday is a full day then assume the day is a goner
if ($holiday->isFullDay()) continue;
//Move the date to end of the partial day of the holiday
$date->modify("$d ".$holiday->getEndsTime());
$partial = true;
// See if we need to recover any time prior to start of
// holiday e.g if the day starts at 8am but the holiday
Expand Down
3 changes: 3 additions & 0 deletions include/class.category.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ function delete() {
catch (OrmException $e) {
return false;
}
$type = array('type' => 'deleted');
Signal::send('object.deleted', $this, $type);

return true;
}

Expand Down
8 changes: 5 additions & 3 deletions include/class.collaborator.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class Collaborator
const FLAG_ACTIVE = 0x0001;
const FLAG_CC = 0x0002;

var $active;

function __toString() {
return Format::htmlchars($this->toString());
}
Expand All @@ -61,7 +63,7 @@ function getThreadId() {
}

function getTicketId() {
if ($this->thread->object_type == ObjectModel::OBJECT_TYPE_TICKET)
if ($this->thread && $this->thread->object_type == ObjectModel::OBJECT_TYPE_TICKET)
return $this->thread->object_id;
}

Expand Down Expand Up @@ -144,8 +146,8 @@ public function setFlag($flag, $val) {
$this->flags &= ~$flag;
}

public function setCc() {
$this->setFlag(Collaborator::FLAG_ACTIVE, true);
public function setCc($active=true) {
$this->setFlag(Collaborator::FLAG_ACTIVE, $active);
$this->setFlag(Collaborator::FLAG_CC, true);
$this->save();
}
Expand Down
24 changes: 19 additions & 5 deletions include/class.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ class OsticketConfig extends Config {
'auto_claim_tickets'=> true,
'auto_refer_closed' => true,
'collaborator_ticket_visibility' => true,
'disable_agent_collabs' => false,
'require_topic_to_close' => false,
'system_language' => 'en_US',
'default_storage_bk' => 'D',
Expand All @@ -228,6 +229,7 @@ class OsticketConfig extends Config {
'ticket_lock' => 2, // Lock on activity
'max_open_tickets' => 0,
'files_req_auth' => 1,
'force_https' => '',
);

function __construct($section=null) {
Expand Down Expand Up @@ -655,6 +657,10 @@ function getTopicSortMode() {
return $this->get('help_topic_sort_mode');
}

function forceHttps() {
return $this->get('force_https') == 'on';
}

function setTopicSortMode($mode) {
$modes = static::allTopicSortModes();
if (!isset($modes[$mode]))
Expand Down Expand Up @@ -785,6 +791,13 @@ function getClientRegistrationMode() {
return $this->get('client_registration');
}

function isClientRegistrationMode($modes) {
if (!is_array($modes))
$modes = array($modes);

return in_array($this->getClientRegistrationMode(), $modes);
}

function isClientEmailVerificationRequired() {
return $this->get('client_verify_email');
}
Expand Down Expand Up @@ -1014,6 +1027,10 @@ function collaboratorTicketsVisibility() {
return $this->get('collaborator_ticket_visibility');
}

function disableAgentCollaborators() {
return $this->get('disable_agent_collabs');
}

function requireTopicToClose() {
return $this->get('require_topic_to_close');
}
Expand Down Expand Up @@ -1258,6 +1275,7 @@ function updateSystemSettings($vars, &$errors) {
'helpdesk_title'=>$vars['helpdesk_title'],
'helpdesk_url'=>$vars['helpdesk_url'],
'default_dept_id'=>$vars['default_dept_id'],
'force_https'=>$vars['force_https'] ? 'on' : '',
'max_page_size'=>$vars['max_page_size'],
'log_level'=>$vars['log_level'],
'log_graceperiod'=>$vars['log_graceperiod'],
Expand Down Expand Up @@ -1308,6 +1326,7 @@ function updateAgentsSettings($vars, &$errors) {
'agent_name_format'=>$vars['agent_name_format'],
'hide_staff_name'=>isset($vars['hide_staff_name']) ? 1 : 0,
'agent_avatar'=>$vars['agent_avatar'],
'disable_agent_collabs'=>isset($vars['disable_agent_collabs'])?1:0,
));
}

Expand Down Expand Up @@ -1628,11 +1647,6 @@ function updateAutoresponderSettings($vars, &$errors) {


function updateKBSettings($vars, &$errors) {

if ($vars['restrict_kb'] && !$this->isClientRegistrationEnabled())
$errors['restrict_kb'] =
__('The knowledge base cannot be restricted unless client registration is enabled');

if ($errors) return false;

return $this->updateAll(array(
Expand Down
26 changes: 10 additions & 16 deletions include/class.dept.php
Original file line number Diff line number Diff line change
Expand Up @@ -546,18 +546,6 @@ function delete() {
->filter(array('dept_id' => $id))
->delete();

foreach(FilterAction::objects()
->filter(array('type' => FA_RouteDepartment::$type)) as $fa
) {
$config = $fa->getConfiguration();
if ($config && $config['dept_id'] == $id) {
$config['dept_id'] = 0;
// FIXME: Move this code into FilterAction class
$fa->set('configuration', JsonDataEncoder::encode($config));
$fa->save();
}
}

// Delete extended access entries
StaffDeptAccess::objects()
->filter(array('dept_id' => $id))
Expand Down Expand Up @@ -795,8 +783,12 @@ function update($vars, &$errors) {
$errors['pid'] = __('Department selection is required');

$dept = Dept::lookup($vars['pid']);
if($dept && !$dept->isActive())
$errors['dept_id'] = sprintf(__('%s selected must be active'), __('Parent Department'));
if ($dept) {
if (!$dept->isActive())
$errors['dept_id'] = sprintf(__('%s selected must be active'), __('Parent Department'));
elseif (strpos($dept->getFullPath(), '/'.$this->getId().'/') !== false)
$errors['pid'] = sprintf(__('%s cannot contain the current %s'), __('Parent Department'), __('Department'));
}

if ($vars['sla_id'] && !SLA::lookup($vars['sla_id']))
$errors['sla_id'] = __('Invalid SLA');
Expand Down Expand Up @@ -845,6 +837,8 @@ function update($vars, &$errors) {
}
}
}
if ($vars['disable_auto_claim'] !== 1)
unset($vars['disable_auto_claim']);

$this->pid = $vars['pid'] ?: null;
$this->ispublic = isset($vars['ispublic']) ? (int) $vars['ispublic'] : 0;
Expand All @@ -867,9 +861,9 @@ function update($vars, &$errors) {

$filter_actions = FilterAction::objects()->filter(array('type' => 'dept', 'configuration' => '{"dept_id":'. $this->getId().'}'));
if ($filter_actions && $vars['status'] == 'active')
FilterAction::setFilterFlag($filter_actions, 'dept', false);
FilterAction::setFilterFlags($filter_actions, 'Filter::FLAG_INACTIVE_DEPT', false);
else
FilterAction::setFilterFlag($filter_actions, 'dept', true);
FilterAction::setFilterFlags($filter_actions, 'Filter::FLAG_INACTIVE_DEPT', true);

switch ($vars['status']) {
case 'active':
Expand Down
2 changes: 1 addition & 1 deletion include/class.dynamic_forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function getForm($source=false) {
$fields = $this->getFields();
$form = new SimpleForm($fields, $source, array(
'title' => $this->getLocal('title'),
'instructions' => $this->getLocal('instructions'),
'instructions' => Format::htmldecode($this->getLocal('instructions')),
'id' => $this->getId(),
));
return $form;
Expand Down
111 changes: 98 additions & 13 deletions include/class.filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class Filter

const FLAG_INACTIVE_HT = 0x0001;
const FLAG_INACTIVE_DEPT = 0x0002;
const FLAG_DELETED_OBJECT = 0x0004;

static $match_types = array(
/* @trans */ 'User Information' => array(
Expand Down Expand Up @@ -164,6 +165,58 @@ function useReplyToEmail() {
return ($this->use_replyto_email);
}

function disableFilters($object) {
switch (get_class($object)) {
case 'Topic':
$object_id = 'topic_id';
$FAClass = 'FA_AssignTopic';
break;
case 'Dept':
$object_id = 'dept_id';
$FAClass = 'FA_RouteDepartment';
break;
case 'Staff':
$object_id = 'staff_id';
$FAClass = 'FA_AssignAgent';
break;
case 'Team':
$object_id = 'team_id';
$FAClass = 'FA_AssignTeam';
break;
case 'SLA':
$object_id = 'sla_id';
$FAClass = 'FA_AssignSLA';
break;
case 'TicketStatus':
$object_id = 'status_id';
$FAClass = 'FA_SetStatus';
break;
case 'Email':
$object_id = 'from';
$FAClass = 'FA_SendEmail';
break;
case 'Canned':
$object_id = 'canned_id';
$FAClass = 'FA_AutoCannedResponse';
default:
return false;
}
$id = $object->getId();
$actions = FilterAction::objects()
->filter(array('type' => $FAClass::$type,
'configuration__like' => sprintf('%%"%s":%s}', $object_id, $id)));
foreach($actions as $fa) {
// Put a flag on the filter
$fa->setFilterFlags(false, 'Filter::FLAG_DELETED_OBJECT', true);
$fa->save();

// Disable the filter
$filter = Filter::lookup($fa->filter_id);
$filter->isactive = 0;
$filter->save();
}
}

function disableAlerts() {
return ($this->disable_autoresponder);
}
Expand Down Expand Up @@ -517,7 +570,7 @@ function validate_actions($vars, &$errors) {

if (!is_array(@$vars['actions']))
return;
foreach ($vars['actions'] as $sort=>$v) {
foreach ($vars['actions'] as $sort=>$v) {
if (is_array($v)) {
$info = $v['type'];
$sort = $v['sort'] ?: $sort;
Expand All @@ -527,18 +580,38 @@ function validate_actions($vars, &$errors) {
'type'=>$info,
'sort' => (int) $sort,
));
$errors = array();
$action->setConfiguration($errors, $vars);
$err = array();
$action->setConfiguration($err, $vars);
$config = json_decode($action->ht['configuration'], true);
if (is_numeric($action->ht['type'])) {
foreach ($config as $key => $value) {
if ($key == 'topic_id') {
$action->ht['type'] = 'topic';
$config['topic_id'] = $value;
}
if ($key == 'dept_id') {
$action->ht['type'] = 'dept';
$config['dept_id'] = $value;
switch ($key) {
case 'topic_id':
$action->ht['type'] = 'topic';
$config['topic_id'] = $value;
break;
case 'dept_id':
$action->ht['type'] = 'dept';
$config['dept_id'] = $value;
break;
case 'sla_id':
$action->ht['type'] = __('SLA');
break;
case 'team_id':
$action->ht['type'] = __('Team');
break;
case 'staff_id':
$action->ht['type'] = __('Agent');
break;
case 'status_id':
$action->ht['type'] = __('Ticket Status');
break;
case 'canned_id':
$action->ht['type'] = __('Canned Response');
break;
default:
$action->ht['type'] = __('All Actions');
break;
}
}
}
Expand All @@ -560,14 +633,25 @@ function validate_actions($vars, &$errors) {
break;
default:
foreach ($config as $key => $value) {
if (!$value) {
$errors['err'] = sprintf(__('Unable to save: Please insert a value for %s'), ucfirst($action->ht['type']));
if (!$value || is_null($value)) {
$errors['err'] = sprintf(__('Unable to save: Please insert a value for %s'), $action->ht['type']);
return 1;
}
}
break;
}
}
}
if (count($errors) == 0) {
$fa = FilterAction::lookup($info);
if ($fa) {
$filter = Filter::lookup($fa->getFilterId());
//Clear flags that may have been set on a successful save
$filter->setFlag(constant('Filter::FLAG_DELETED_OBJECT'), false);
$filter->setFlag(constant('Filter::FLAG_INACTIVE_DEPT'), false);
$filter->setFlag(constant('Filter::FLAG_INACTIVE_HT'), false);
}
}
return count($errors) == 0;
}

Expand Down Expand Up @@ -609,6 +693,7 @@ function save_actions($id, $vars, &$errors) {
}
}
}
Signal::connect('object.deleted', array('Filter', 'disableFilters'));

class FilterRule
extends VerySimpleModel {
Expand Down Expand Up @@ -804,7 +889,7 @@ static function isAutoReply($headers) {
'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-Auto-Response-Suppress' => array('AutoReply'),
'X-Autoresponse' => '*',
'X-AutoReply-From' => '*',
'X-Autorespond' => '*',
Expand Down
31 changes: 20 additions & 11 deletions include/class.filter_action.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ function getId() {
function setFilter($filter) {
$this->_filter = $filter;
}

function getFilterId() {
return $this->filter_id;
}

function getFilter() {
return $this->_filter;
}
Expand Down Expand Up @@ -84,14 +89,19 @@ function getImpl() {
return $this->_impl;
}

function setFilterFlag($actions, $flag, $bool) {
foreach ($actions as $action) {
$filter = Filter::lookup($action->filter_id);
if ($filter && ($flag == 'dept') && ($filter->hasFlag(Filter::FLAG_INACTIVE_DEPT) != $bool))
$filter->setFlag(Filter::FLAG_INACTIVE_DEPT, $bool);
if ($filter && ($flag == 'topic') && ($filter->hasFlag(Filter::FLAG_INACTIVE_HT) != $bool))
$filter->setFlag(Filter::FLAG_INACTIVE_HT, $bool);
}
function setFilterFlags($actions=false, $flag, $bool) {
$flag = constant($flag);
if ($actions) {
foreach ($actions as $action)
$action->setFilterFlag($flag, $bool);
} else
$this->setFilterFlag($flag, $bool);
}

function setFilterFlag($flag, $bool) {
$filter = Filter::lookup($this->filter_id);
if ($filter && ($filter->hasFlag($flag) != $bool))
$filter->setFlag($flag, $bool);
}

function apply(&$ticket, array $info) {
Expand Down Expand Up @@ -305,7 +315,7 @@ function apply(&$ticket, array $info) {
if ($config['dept_id']) {
$dept = Dept::lookup($config['dept_id']);

if ($dept->isActive())
if ($dept && $dept->isActive())
$ticket['deptId'] = $config['dept_id'];
}
}
Expand Down Expand Up @@ -450,8 +460,7 @@ function apply(&$ticket, array $info) {
$config = $this->getConfiguration();
if ($config['topic_id']) {
$topic = Topic::lookup($config['topic_id']);

if ($topic->isActive())
if ($topic && $topic->isActive())
$ticket['topicId'] = $config['topic_id'];
}
}
Expand Down
1 change: 1 addition & 0 deletions include/class.format.php
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ function strip_emoticons($text) {
'/[\x{23F0}-\x{23FF}]/u', # Clock/Buttons
'/[\x{23E0}-\x{23EF}]/u', # More Buttons
'/[\x{2310}-\x{231F}]/u', # Hourglass/Watch
'/[\x{1000B6}]/u', # Private Use Area (Plane 16)
'/[\x{2322}-\x{232F}]/u' # Keyboard
), '', $text);
}
Expand Down
68 changes: 53 additions & 15 deletions include/class.forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -1341,7 +1341,6 @@ function getLocal($subtag, $default=false) {
}

function getEditForm($source=null) {

$fields = array(
'field' => $this,
'comments' => new TextareaField(array(
Expand Down Expand Up @@ -1457,6 +1456,9 @@ function validateEntry($value) {
$config = $this->getConfiguration();
$validators = array(
'' => '',
'noop' => array(
function($a, &$b) { return true; }
),
'formula' => array(array('Validator', 'is_formula'),
__('Content cannot start with the following characters: = - + @')),
'email' => array(array('Validator', 'is_valid_email'),
Expand All @@ -1465,7 +1467,10 @@ function validateEntry($value) {
__('Enter a valid phone number')),
'ip' => array(array('Validator', 'is_ip'),
__('Enter a valid IP address')),
'number' => array('is_numeric', __('Enter a number')),
'number' => array(array('Validator', 'is_numeric'),
__('Enter a number')),
'password' => array(array('Validator', 'check_passwd'),
__('Invalid Password')),
'regex' => array(
function($v) use ($config) {
$regex = $config['regex'];
Expand All @@ -1485,16 +1490,19 @@ function($v) use ($config) {
if (!$valid && !($this->getForm() instanceof AdvancedSearchForm))
$valid = 'formula';
$func = $validators[$valid];
$error = $func[1];
$error = $err = null;
// If validator is number and the value is &#48 set to 0 (int) for is_numeric
if ($valid == 'number' && $value == '&#48')
$value = 0;
if ($config['validator-error'])
$error = $this->getLocal('validator-error', $config['validator-error']);
if (is_array($func) && is_callable($func[0]))
if (!call_user_func($func[0], $value))
$this->_errors[] = $error;
if (!call_user_func_array($func[0], array($value, &$err)))
$this->_errors[] = $error ?: $err ?: $func[1];
}

function parse($value) {
return Format::striptags($value);
return Format::strip_emoticons(Format::striptags($value));
}

function display($value) {
Expand All @@ -1505,6 +1513,11 @@ function display($value) {
class PasswordField extends TextboxField {
static $widget = 'PasswordWidget';

function __construct($options=array()) {
parent::__construct($options);
$this->set('validator', 'password');
}

function parse($value) {
// Don't trim the value
return $value;
Expand Down Expand Up @@ -1818,7 +1831,7 @@ function to_php($value) {
if (is_string($value) && strpos($value, ',')) {
$values = array();
$choices = $this->getChoices();
$vals = explode(',', $value);
$vals = array_map('trim', explode(',', $value));
foreach ($vals as $V) {
if (isset($choices[$V]))
$values[$V] = $choices[$V];
Expand Down Expand Up @@ -2068,6 +2081,28 @@ function getSearchMethodWidgets() {
)),
);
}

function getSearchQ($method, $value, $name=false) {
switch ($method) {
case 'equal':
return new Q(array(
"{$name}__exact" => intval($value)
));
break;
case 'greater':
return Q::any(array(
"{$name}__gt" => intval($value)
));
break;
case 'less':
return Q::any(array(
"{$name}__lt" => intval($value)
));
break;
default:
return parent::getSearchQ($method, $value, $name);
}
}
}

class DatetimeField extends FormField {
Expand Down Expand Up @@ -2699,7 +2734,7 @@ function isAttachmentsEnabled() {

function getWidget($widgetClass=false) {
if ($hint = $this->getLocal('hint'))
$this->set('placeholder', $hint);
$this->set('placeholder', Format::striptags($hint));
$this->set('hint', null);
$widget = parent::getWidget($widgetClass);
return $widget;
Expand Down Expand Up @@ -3862,8 +3897,8 @@ function whatChanged($before, $after) {
$A = (array) $after;
$added = array_diff($A, $B);
$deleted = array_diff($B, $A);
$added = Format::htmlchars(array_keys($added));
$deleted = Format::htmlchars(array_keys($deleted));
$added = Format::htmlchars(array_values($added));
$deleted = Format::htmlchars(array_values($deleted));

if ($added && $deleted) {
$desc = sprintf(
Expand Down Expand Up @@ -4221,7 +4256,7 @@ function render($options=array()) {
<span style="display:inline-block;width:100%">
<textarea <?php echo $rows." ".$cols." ".$maxlength." ".$class
.' '.Format::array_implode('=', ' ', $attrs)
.' placeholder="'.$config['placeholder'].'"'; ?>
.' placeholder="'.$this->field->getLocal('placeholder', $config['placeholder']).'"'; ?>
id="<?php echo $this->id; ?>"
name="<?php echo $this->name; ?>"><?php
echo Format::htmlchars($this->value);
Expand Down Expand Up @@ -4615,7 +4650,7 @@ function render($options=array()) {

function getValue() {
$data = $this->field->getSource();
if (count($data)) {
if (is_array($data)) {
if (isset($data[$this->name]))
return @in_array($this->field->get('id'),
$data[$this->name]);
Expand All @@ -4624,7 +4659,7 @@ function getValue() {
return $data[$this->field->get('id')];
}

if (isset($this->value))
if (!$data && isset($this->value))
return $this->value;


Expand All @@ -4649,6 +4684,9 @@ function render($options=array()) {
if (!isset($this->value) && ($default=$this->field->get('default')))
$this->value = $default;

if ($this->value == 0)
$this->value = '';

if ($this->value) {
$datetime = Format::parseDateTime($this->value);
if ($config['time'])
Expand Down Expand Up @@ -4848,7 +4886,7 @@ function render($options=array()) {
class="<?php if ($config['html']) echo 'richtext';
?> draft draft-delete" <?php echo $attrs; ?>
cols="21" rows="8" style="width:80%;"><?php echo
Format::htmlchars($this->value) ?: $draft; ?></textarea>
ThreadEntryBody::clean($this->value ?: $draft); ?></textarea>
<?php
if (!$config['attachments'])
return;
Expand Down Expand Up @@ -5102,7 +5140,7 @@ function render($options=array()) {
}
if ($hint = $this->field->getLocal('hint')) { ?>
<em><?php
echo Format::htmlchars($hint);
echo Format::display($hint);
?></em><?php
} ?>
<div><?php
Expand Down
16 changes: 11 additions & 5 deletions include/class.list.php
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ function getLocal($subtag) {
}

function update($vars, &$errors) {
$vars = Format::htmlchars($vars);
$required = array();
if ($this->isEditable())
$required = array('name');
Expand Down Expand Up @@ -463,7 +464,7 @@ private function createForm() {
}

static function add($vars, &$errors) {

$vars = Format::htmlchars($vars);
$required = array('name');
$ht = array();
foreach (static::$fields as $f) {
Expand All @@ -477,15 +478,18 @@ static function add($vars, &$errors) {
return false;

// Create the list && form
if (!($list = self::create($ht))
if (!($list = self::create($ht, $errors, false))
|| !$list->save(true)
|| !$list->createConfigurationForm())
return false;

return $list;
}

static function create($ht=false, &$errors=array()) {
static function create($ht=false, &$errors=array(), $sanitize=true) {
if ($ht && $sanitize)
$ht = Format::htmlchars($ht);

if (isset($ht['configuration'])) {
$ht['configuration'] = JsonDataEncoder::encode($ht['configuration']);
}
Expand Down Expand Up @@ -1455,10 +1459,12 @@ function update($vars, &$errors) {
function delete() {

// Statuses with tickets are not deletable
if (!$this->isDeletable())
if (!$this->isDeletable() || !parent::delete())
return false;

return parent::delete();
Signal::send('object.deleted', $this);

return true;
}

function __toString() {
Expand Down
18 changes: 13 additions & 5 deletions include/class.mailfetch.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,17 @@ function __construct($email, $charset='UTF-8') {
if ($this->ht) {
// Support Exchange shared mailbox auth
// (eg. user@domain.com\shared@domain.com)
$usernames = explode('\\', $this->ht['username'], 2);
if (count($usernames) == 2) {
$this->authuser = $usernames[0];
$this->username = $usernames[1];
$usernames = explode('\\', $this->ht['username']);
$count = count($usernames);
if ($count == 3) {
$this->authuser = $usernames[0].'\\'.$usernames[1];
$this->username = $usernames[2];
} elseif ($count == 2) {
if (strpos($usernames[0], '@') !== false) {
$this->authuser = $usernames[0];
$this->username = $usernames[1];
} else
$this->username = $usernames[0].'\\'.$usernames[1];
} else {
$this->username = $this->ht['username'];
}
Expand Down Expand Up @@ -724,7 +731,8 @@ function createTicket($mid) {
}

// Process overloaded attachments
if (($struct = imap_fetchstructure($this->mbox, $mid))
$attachments = array();
if (($struct = @imap_fetchstructure($this->mbox, $mid))
&& ($attachments = $this->getAttachments($struct))) {
foreach ($attachments as $i=>$info) {
switch (strtolower($info['type'])) {
Expand Down
26 changes: 22 additions & 4 deletions include/class.mailparse.php
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ function mime_encode($text, $charset=null, $encoding='utf-8') {
}

function getAttachments($part=null){
$files=array();
$files = $matches = array();

/* Consider this part as an attachment if
* * It has a Content-Disposition header
Expand Down Expand Up @@ -417,18 +417,36 @@ function getAttachments($part=null){
elseif (isset($part->ctype_parameters['name*']))
$filename = Format::decodeRfc5987(
$part->ctype_parameters['name*']);

elseif (isset($part->headers['content-disposition'])
&& $part->headers['content-disposition']
&& preg_match('/filename="([^"]+)"/', $part->headers['content-disposition'], $matches))
$filename = Format::mimedecode($matches[1], $this->charset);
// Some mail clients / servers (like Lotus Notes / Domino) will
// send images without a filename. For such a case, generate a
// random filename for the image
elseif (isset($part->headers['content-id'])
&& $part->headers['content-id']
&& 0 === strcasecmp($part->ctype_primary, 'image'))
&& 0 === strcasecmp($part->ctype_primary, 'image')) {
$filename = 'image-'.Misc::randCode(4).'.'
.strtolower($part->ctype_secondary);
else
// Attachment of type message/rfc822 without name!!!
} elseif (strcasecmp($part->ctype_primary, 'message') === 0) {
$struct = $part->parts[0];
if ($struct && isset($struct->headers['subject']))
$filename = Format::mimedecode($struct->headers['subject'],
$this->charset);
else
$filename = 'email-message-'.Misc::randCode(4);

$filename .='.eml';
} elseif (isset($part->headers['content-disposition'])
&& $part->headers['content-disposition']
&& preg_match('/filename="([^"]+)"/', $part->headers['content-disposition'], $matches)) {
$filename = Format::mimedecode($matches[1], $this->charset);
} else {
// Not an attachment?
return false;
}

$file=array(
'name' => $filename,
Expand Down
6 changes: 3 additions & 3 deletions include/class.organization.php
Original file line number Diff line number Diff line change
Expand Up @@ -645,9 +645,9 @@ function getTicketsQueue() {
'staff_id' => $thisstaff->getId(),
'title' => $name
));
$this->_queue->filter(array(
'user__org__name' => $name
));
$this->_queue->config = [[
'user__org__name', 'equal', $name
]];
}

return $this->_queue;
Expand Down
13 changes: 10 additions & 3 deletions include/class.orm.php
Original file line number Diff line number Diff line change
Expand Up @@ -2262,10 +2262,11 @@ class SqlCompiler {
);

function __construct($options=false) {
if ($options)
if (is_array($options)) {
$this->options = array_merge($this->options, $options);
if ($options['subquery'])
$this->alias_num += 150;
if (isset($options['subquery']))
$this->alias_num += 150;
}
}

function getParent() {
Expand Down Expand Up @@ -2539,6 +2540,7 @@ function compileQ(Q $Q, $model, $parens=true) {
$filter = array();
$type = CompiledExpression::TYPE_WHERE;
foreach ($Q->constraints as $field=>$value) {
$fieldName = $field;
// Handle nested constraints
if ($value instanceof Q) {
$filter[] = $T = $this->compileQ($value, $model,
Expand Down Expand Up @@ -2580,7 +2582,12 @@ function compileQ(Q $Q, $model, $parens=true) {
// This constraint has to go in the HAVING clause
$field = $field->toSql($this, $model);
$type = CompiledExpression::TYPE_HAVING;
} elseif ($field instanceof QuerySet) {
// Constraint on a subquery goes to HAVING clause
list($field) = static::splitCriteria($fieldName);
$type = CompiledExpression::TYPE_HAVING;
}

if ($value === null)
$filter[] = sprintf('%s IS NULL', $field);
elseif ($value instanceof SqlField)
Expand Down
11 changes: 11 additions & 0 deletions include/class.osticket.php
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,17 @@ function is_https() {
&& !strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https'));
}

/**
* Returns TRUE if the current browser is IE and FALSE otherwise
*/
function is_ie() {
if (preg_match('/MSIE|Internet Explorer|Trident\/[\d]{1}\.[\d]{1,2}/',
$_SERVER['HTTP_USER_AGENT']))
return true;

return false;
}

/* returns true if script is being executed via commandline */
static function is_cli() {
return (!strcasecmp(substr(php_sapi_name(), 0, 3), 'cli')
Expand Down
4 changes: 2 additions & 2 deletions include/class.ostsession.php
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ function update($id, $data){
}

function destroy($id){
return SessionData::objects()->filter(['session_id' => $id])->delete();
return SessionData::objects()->filter(['session_id' => $id])->delete() ? true : false;
}

function cleanup() {
Expand Down Expand Up @@ -303,7 +303,7 @@ function read($id) {

function update($id, $data) {
if (defined('DISABLE_SESSION') && $this->isnew)
return;
return true;

$key = $this->getKey($id);
foreach ($this->servers as $S) {
Expand Down
6 changes: 3 additions & 3 deletions include/class.page.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,9 @@ function delete() {
if (!parent::delete())
return false;

return Topic::objects()
->filter(array('page_id'=>$this->getId()))
->update(array('page_id'=>0));
$type = array('type' => 'deleted');
Signal::send('object.deleted', $this, $type);
return true;
}

function save($refetch=false) {
Expand Down
18 changes: 12 additions & 6 deletions include/class.pagenate.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ function __construct($total,$page,$limit=20,$url='') {
}

function setTotal($total, $approx=false) {
$this->total = intval($total);
$this->pages = ceil( $this->total / $this->limit );
$this->total = is_string($total) ? '-' : intval($total);
$total = is_string($total) ? 500 : $total;
$this->pages = ceil( $total / $this->limit );

if (($this->limit > $this->total) || ($this->page>ceil($this->total/$this->limit))) {
if (($this->limit > $total) || ($this->page>ceil($total/$this->limit))) {
$this->start = 0;
}
if (($this->limit-1)*$this->start > $this->total) {
if (($this->limit-1)*$this->start > $total) {
$this->start -= $this->start % $this->limit;
}
$this->approx = $approx;
Expand Down Expand Up @@ -91,14 +92,18 @@ function getPage() {
function showing() {
$html = '';
$start = $this->getStart() + 1;
$end = min($start + $this->limit + $this->slack - 1, $this->total);
$end = min($start + $this->limit + $this->slack - 1,
is_string($this->total) ? 500 : $this->total);
if ($end < $this->total) {
$to= $end;
} else {
$to= $this->total;
}
$html=__('Showing')."&nbsp;";
if ($this->total > 0) {
if (is_string($this->total))
$html .= sprintf(__('%1$d - %2$d' /* Used in pagination output */),
$start, $end);
elseif ($this->total > 0) {
if ($this->approx)
$html .= sprintf(__('%1$d - %2$d of about %3$d' /* Used in pagination output */),
$start, $end, $this->total);
Expand All @@ -112,6 +117,7 @@ function showing() {
}

function getPageLinks($hash=false, $pjax=false) {
$this->total = is_string($this->total) ? 500 : $this->total; //placeholder if no total
$html = '';
$file =$this->url;
$displayed_span = 5;
Expand Down
4 changes: 2 additions & 2 deletions include/class.pdf.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function getTicket() {
}

function _print() {
global $thisstaff, $thisclient, $cfg;
global $thisstaff, $thisclient, $cfg, $ost;

if(!($ticket=$this->getTicket()))
return;
Expand Down Expand Up @@ -119,7 +119,7 @@ function __construct($task, $options=array()) {
}

function _print() {
global $thisstaff, $cfg;
global $thisstaff, $cfg, $ost;

if (!($task=$this->task) || !$thisstaff)
return;
Expand Down
3 changes: 3 additions & 0 deletions include/class.plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@ function install($path) {
.', install_path='.db_input($path)
.', name='.db_input($info['name'])
.', isphar='.db_input($is_phar);
if ($info['version'])
$sql.=', version='.db_input($info['version']);
if (!db_query($sql) || !db_affected_rows())
return false;
static::clearCache();
Expand Down Expand Up @@ -402,6 +404,7 @@ function getId() { return $this->id; }
function getName() { return $this->__($this->info['name']); }
function isActive() { return $this->ht['isactive']; }
function isPhar() { return $this->ht['isphar']; }
function getVersion() { return $this->ht['version'] ?: $this->info['version']; }
function getInstallDate() { return $this->ht['installed']; }
function getInstallPath() { return $this->ht['install_path']; }

Expand Down
17 changes: 13 additions & 4 deletions include/class.queue.php
Original file line number Diff line number Diff line change
Expand Up @@ -973,9 +973,14 @@ function mangleQuerySet(QuerySet $qs, $form=false) {
}

// Fetch a criteria Q for the query
if (list(,$field) = $searchable[$name])
if (list(,$field) = $searchable[$name]) {
// Add annotation if the field supports it.
if (is_subclass_of($field, 'AnnotatedField'))
$qs = $field->annotate($qs, $name);

if ($q = $field->getSearchQ($method, $value, $name))
$qs = $qs->filter($q);
}
}
}

Expand Down Expand Up @@ -1026,7 +1031,8 @@ function inheritColumns() {
}

function useStandardColumns() {
return !count($this->columns);
return ($this->hasFlag(self::FLAG_INHERIT_COLUMNS) ||
!count($this->columns));
}

function inheritExport() {
Expand Down Expand Up @@ -1205,14 +1211,14 @@ function update($vars, &$errors=array()) {
if (!$vars['queue-name'])
$errors['queue-name'] = __('A title is required');
elseif (($q=CustomQueue::lookup(array(
'title' => $vars['queue-name'],
'title' => Format::htmlchars($vars['queue-name']),
'parent_id' => $vars['parent_id'] ?: 0,
'staff_id' => $this->staff_id)))
&& $q->getId() != $this->id
)
$errors['queue-name'] = __('Saved queue with same name exists');

$this->title = $vars['queue-name'];
$this->title = Format::htmlchars($vars['queue-name']);
$this->parent_id = @$vars['parent_id'] ?: 0;
if ($this->parent_id && !$this->parent)
$errors['parent_id'] = __('Select a valid queue');
Expand Down Expand Up @@ -1249,6 +1255,9 @@ function update($vars, &$errors=array()) {
$this->setFlag(self::FLAG_INHERIT_SORTING,
$this->parent_id > 0 && isset($vars['inherit-sorting']));

// Saved Search - Use standard columns
if ($this instanceof SavedSearch && isset($vars['inherit-columns']))
$this->setFlag(self::FLAG_INHERIT_COLUMNS);
// Update queue columns (but without save)
if (!isset($vars['columns']) && $this->parent) {
// No columns -- imply column inheritance
Expand Down
30 changes: 18 additions & 12 deletions include/class.schedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ function save($refetch=false) {
if (count($this->dirty))
$this->set('updated', new SqlFunction('NOW'));
if (isset($this->dirty['description']))
$this->description = Format::sanitize($this->notes);
$this->description = Format::sanitize($this->description);

return parent::save($refetch);
}
Expand Down Expand Up @@ -788,15 +788,19 @@ function getIntervalSpec(Datetime $dt) {

function getCurrent($from=null) {
if (!isset($this->_current) || $from) {
// Figure out starting point (from)
$from = is_object($from) ? clone $from : Format::parseDateTime($from ?: 'now');
$start = $this->getStartsDatetime();
if ($start->getTimestamp() > $from->getTimestamp())
$from = clone $start;
// Check to make sure we're still in scope
$start = is_object($from) ? clone $from : Format::parseDateTime($from ?: 'now');
$stop = $this->getStopsDatetime();
if ($stop && $stop->getTimestamp() < $start->getTimestamp())
if ($stop && $stop->getTimestamp() < $from->getTimestamp())
return null;

// Figure out start time for the entry.
$start->modify($this->getIntervalSpec($start));
$this->_current = clone $start;
$from->modify($this->getIntervalSpec($from));
$this->_current = clone $from;
}
return $this->_current;
}
Expand Down Expand Up @@ -842,16 +846,18 @@ function next() {
return $current;
}

function getOccurrences($start=null, $end=null, $num=2) {
function getOccurrences($start=null, $end=null, $num=5) {
$occurrences = array();
if (($current = $this->getCurrent($start))) {
$occurrences[$current->format('Y-m-d')] = $this;
$start = $start ?: $current;
while (count($occurrences) < $num) {
if (!($next=$this->next()))
break;
$date = $current->format('Y-m-d');
$occurrences[$date] = $this;
if ($end && strtotime($date) >= strtotime($end))
if ($end && strtotime($date) > strtotime($end))
break;
if (strtotime($date) >= strtotime($start->format('Y-m-d')))
$occurrences[$date] = $this;

if (!($current=$this->next()))
break;
}
}
Expand Down Expand Up @@ -1418,7 +1424,7 @@ function buildFields() {
'gmt' => false,
'future' => false,
'max' => time(),
'showtimezone' => false,
'showtimezone' => true,
),
)),
'hours' => new TextboxField(array(
Expand Down
86 changes: 71 additions & 15 deletions include/class.search.php
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,13 @@ function useStandardColumns() {
return parent::useStandardColumns();
}

function inheritColumns() {
if ($this->getSettings() && isset($this->_settings['inherit-columns']))
return $this->_settings['inherit-columns'];

return parent::inheritColumns();
}

function getStandardColumns() {
return parent::getColumns(is_null($this->parent));
}
Expand Down Expand Up @@ -952,21 +959,35 @@ static function counts($agent, $cached=true, $criteria=array()) {
$Q->constraints[] = $reg;
}

$expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), new SqlField('ticket_id'));
$query->aggregate(array(
"q{$queue->id}" => SqlAggregate::COUNT($expr, true)
));
if ($Q->constraints) {
$empty = false;
if (count($Q->constraints) > 1) {
foreach ($Q->constraints as $value) {
if (!$value->constraints)
$empty = true;
}
}
}

// Add extra tables joins (if any)
if ($Q->extra && isset($Q->extra['tables'])) {
$counts['q'.$queue->getId()] = 500;
// skip counting keyword searches. Display them as '-'
$counts['q'.$queue->getId()] = '-';
continue;
$contraints = array();
if ($Q->constraints)
$constraints = new Q($Q->constraints);
foreach ($Q->extra['tables'] as $T)
$query->addExtraJoin(array($T, $constraints, ''));
$contraints = array();
if ($Q->constraints)
$constraints = new Q($Q->constraints);
foreach ($Q->extra['tables'] as $T)
$query->addExtraJoin(array($T, $constraints, ''));
}

if ($Q->constraints && !$empty) {
$expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), new SqlField('ticket_id'));
$query->aggregate(array(
"q{$queue->id}" => SqlAggregate::COUNT($expr, true)
));
} else //display skipped counts as '-'
$counts['q'.$queue->getId()] = '-';
}

try {
Expand Down Expand Up @@ -1735,7 +1756,18 @@ function applyOrderBy($query, $reverse=false, $name=false) {
}
}

class TicketThreadCountField extends NumericField {
/*
* Implemented by annotated fields
*
*/

interface AnnotatedField {
// Add the annotation to a QuerySet
function annotate($query, $name);
}

class TicketThreadCountField extends NumericField
implements AnnotatedField {

function addToQuery($query, $name=false) {
return TicketThreadCount::addToQuery($query, $name);
Expand All @@ -1744,9 +1776,14 @@ function addToQuery($query, $name=false) {
function from_query($row, $name=false) {
return TicketThreadCount::from_query($row, $name);
}

function annotate($query, $name) {
return TicketThreadCount::annotate($query, $name);
}
}

class TicketReopenCountField extends NumericField {
class TicketReopenCountField extends NumericField
implements AnnotatedField {

function addToQuery($query, $name=false) {
return TicketReopenCount::addToQuery($query, $name);
Expand All @@ -1755,9 +1792,14 @@ function addToQuery($query, $name=false) {
function from_query($row, $name=false) {
return TicketReopenCount::from_query($row, $name);
}

function annotate($query, $name) {
return TicketReopenCount::annotate($query, $name);
}
}

class ThreadAttachmentCountField extends NumericField {
class ThreadAttachmentCountField extends NumericField
implements AnnotatedField {

function addToQuery($query, $name=false) {
return ThreadAttachmentCount::addToQuery($query, $name);
Expand All @@ -1766,9 +1808,14 @@ function addToQuery($query, $name=false) {
function from_query($row, $name=false) {
return ThreadAttachmentCount::from_query($row, $name);
}

function annotate($query, $name) {
return ThreadAttachmentCount::annotate($query, $name);
}
}

class ThreadCollaboratorCountField extends NumericField {
class ThreadCollaboratorCountField extends NumericField
implements AnnotatedField {

function addToQuery($query, $name=false) {
return ThreadCollaboratorCount::addToQuery($query, $name);
Expand All @@ -1777,9 +1824,14 @@ function addToQuery($query, $name=false) {
function from_query($row, $name=false) {
return ThreadCollaboratorCount::from_query($row, $name);
}

function annotate($query, $name) {
return ThreadCollaboratorCount::annotate($query, $name);
}
}

class TicketTasksCountField extends NumericField {
class TicketTasksCountField extends NumericField
implements AnnotatedField {

function addToQuery($query, $name=false) {
return TicketTasksCount::addToQuery($query, $name);
Expand All @@ -1788,6 +1840,10 @@ function addToQuery($query, $name=false) {
function from_query($row, $name=false) {
return TicketTasksCount::from_query($row, $name);
}

function annotate($query, $name) {
return TicketTasksCount::annotate($query, $name);
}
}

interface Searchable {
Expand Down
7 changes: 7 additions & 0 deletions include/class.sla.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,16 @@ static function getVarScope() {
}

function update($vars, &$errors) {
$vars = Format::htmlchars($vars);
if (!$vars['grace_period'])
$errors['grace_period'] = __('Grace period required');
elseif (!is_numeric($vars['grace_period']))
$errors['grace_period'] = __('Numeric value required (in hours)');
elseif ($vars['grace_period'] > 8760)
$errors['grace_period'] = sprintf(
__('%s cannot be more than 8760 hours'),
__('Grace period')
);

if (!$vars['name'])
$errors['name'] = __('Name is required');
Expand Down Expand Up @@ -273,6 +279,7 @@ function __toString() {
}

static function create($vars=false, &$errors=array()) {
$vars = Format::htmlchars($vars);
$sla = new static($vars);
$sla->created = SqlFunction::NOW();
return $sla;
Expand Down
56 changes: 32 additions & 24 deletions include/class.staff.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function getConfig() {
'default_ticket_queue_id' => 0,
'reply_redirect' => 'Ticket',
'img_att_view' => 'download',
'editor_spacing' => 'double',
));
$this->_config = $_config->getInfo();
}
Expand Down Expand Up @@ -358,6 +359,10 @@ function getImageAttachmentView() {
return $this->img_att_view;
}

function editorSpacing() {
return $this->editor_spacing;
}

function forcePasswdChange() {
return $this->change_passwd;
}
Expand Down Expand Up @@ -446,6 +451,8 @@ function setDepartmentId($dept_id, $eavesdrop=false) {
) {
$this->dept_access->remove($da);
}

$this->save();
}

function usePrimaryRoleOnAssignment() {
Expand Down Expand Up @@ -776,6 +783,7 @@ function updateProfile($vars, &$errors) {
'default_ticket_queue_id' => $vars['default_ticket_queue_id'],
'reply_redirect' => ($vars['reply_redirect'] == 'Queue') ? 'Queue' : 'Ticket',
'img_att_view' => ($vars['img_att_view'] == 'inline') ? 'inline' : 'download',
'editor_spacing' => ($vars['editor_spacing'] == 'double') ? 'double' : 'single'
)
);
$this->_config = $_config->getInfo();
Expand Down Expand Up @@ -1118,30 +1126,6 @@ function update($vars, &$errors) {
}
}

// Update some things for ::updateAccess to inspect
$this->setDepartmentId($vars['dept_id']);

// Format access update as [array(dept_id, role_id, alerts?)]
$access = array();
if (isset($vars['dept_access'])) {
foreach (@$vars['dept_access'] as $dept_id) {
$access[] = array($dept_id, $vars['dept_access_role'][$dept_id],
@$vars['dept_access_alerts'][$dept_id]);
}
}
$this->updateAccess($access, $errors);
$this->setExtraAttr('def_assn_role',
isset($vars['assign_use_pri_role']), false);

// Format team membership as [array(team_id, alerts?)]
$teams = array();
if (isset($vars['teams'])) {
foreach (@$vars['teams'] as $team_id) {
$teams[] = array($team_id, @$vars['team_alerts'][$team_id]);
}
}
$this->updateTeams($teams, $errors);

// Update the local permissions
$this->updatePerms($vars['perms'], $errors);

Expand Down Expand Up @@ -1178,6 +1162,30 @@ function update($vars, &$errors) {
return false;

if ($this->save()) {
// Update some things for ::updateAccess to inspect
$this->setDepartmentId($vars['dept_id']);

// Format access update as [array(dept_id, role_id, alerts?)]
$access = array();
if (isset($vars['dept_access'])) {
foreach (@$vars['dept_access'] as $dept_id) {
$access[] = array($dept_id, $vars['dept_access_role'][$dept_id],
@$vars['dept_access_alerts'][$dept_id]);
}
}
$this->updateAccess($access, $errors);
$this->setExtraAttr('def_assn_role',
isset($vars['assign_use_pri_role']), false);

// Format team membership as [array(team_id, alerts?)]
$teams = array();
if (isset($vars['teams'])) {
foreach (@$vars['teams'] as $team_id) {
$teams[] = array($team_id, @$vars['team_alerts'][$team_id]);
}
}
$this->updateTeams($teams, $errors);

if ($vars['welcome_email'])
$this->sendResetEmail('registration-staff', false);
return true;
Expand Down
186 changes: 185 additions & 1 deletion include/class.task.php
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,37 @@ function getLastRespondent() {
return $this->lastrespondent;
}

function getField($fid) {
if (is_numeric($fid))
return $this->getDymanicFieldById($fid);

// Special fields
switch ($fid) {
case 'duedate':
return DateTimeField::init(array(
'id' => $fid,
'name' => $fid,
'default' => Misc::db2gmtime($this->getDueDate()),
'label' => __('Due Date'),
'configuration' => array(
'min' => Misc::gmtime(),
'time' => true,
'gmt' => false,
'future' => true,
)
));
}
}

function getDymanicFieldById($fid) {
foreach (DynamicFormEntry::forObject($this->getId(),
ObjectModel::OBJECT_TYPE_TASK) as $form) {
foreach ($form->getFields() as $field)
if ($field->getId() == $fid)
return $field;
}
}

function getDynamicFields($criteria=array()) {

$fields = DynamicFormField::objects()->filter(array(
Expand Down Expand Up @@ -1094,6 +1125,10 @@ function getVar($tag) {
break;
case 'last_update':
return new FormattedDate($this->last_update);
case 'description':
return Format::display($this->getThread()->getVar('original') ?: '');
case 'subject':
return Format::htmlchars($this->getTitle());
default:
if (isset($this->_answers[$tag]))
// The answer object is retrieved here which will
Expand All @@ -1116,6 +1151,7 @@ static function getVarScope() {
'dept' => array(
'class' => 'Dept', 'desc' => __('Department'),
),
'description' => __('Description'),
'due_date' => array(
'class' => 'FormattedDate', 'desc' => __('Due Date'),
),
Expand Down Expand Up @@ -1333,6 +1369,84 @@ function update($forms, $vars, &$errors) {
return $this->save();
}

function updateField($form, &$errors) {
global $thisstaff, $cfg;

if (!($field = $form->getField('field')))
return null;

if (!($changes = $field->getChanges()))
$errors['field'] = sprintf(__('%s is already assigned this value'),
__($field->getLabel()));
else {
if ($field->answer) {
if (!$field->isEditableToStaff())
$errors['field'] = sprintf(__('%s can not be edited'),
__($field->getLabel()));
elseif (!$field->save(true))
$errors['field'] = __('Unable to update field');
$changes['fields'] = array($field->getId() => $changes);
} else {
$val = $field->getClean();
$fid = $field->get('name');

// Convert duedate to DB timezone.
if ($fid == 'duedate') {
if (empty($val))
$val = null;
elseif ($dt = Format::parseDateTime($val)) {
// Make sure the due date is valid
if (Misc::user2gmtime($val) <= Misc::user2gmtime())
$errors['field']=__('Due date must be in the future');
else {
$dt->setTimezone(new DateTimeZone($cfg->getDbTimezone()));
$val = $dt->format('Y-m-d H:i:s');
}
}
} elseif (is_object($val))
$val = $val->getId();

$changes = array();
$this->{$fid} = $val;
foreach ($this->dirty as $F=>$old) {
switch ($F) {
case 'sla_id':
case 'topic_id':
case 'user_id':
case 'source':
$changes[$F] = array($old, $this->{$F});
}
}

if (!$errors && !$this->save())
$errors['field'] = __('Unable to update field');
}
}

if ($errors)
return false;

// Record the changes
$this->logEvent('edited', $changes);

// Log comments (if any)
if (($comments = $form->getField('comments')->getClean())) {
$title = sprintf(__('%s updated'), __($field->getLabel()));
$_errors = array();
$this->postNote(
array('note' => $comments, 'title' => $title),
$_errors, $thisstaff, false);
}

$this->lastupdate = SqlFunction::NOW();

$this->save();

Signal::send('model.updated', $this);

return true;
}

/* static routines */
static function lookupIdByNumber($number) {

Expand Down Expand Up @@ -1375,7 +1489,12 @@ static function create($vars=false) {

// Create a thread + message.
$thread = TaskThread::create($task);
$thread->addDescription($vars);
$desc = $thread->addDescription($vars);
// Set the ORIGINAL_MESSAGE Flag if Description is added
if ($desc) {
$desc->setFlag(ThreadEntry::FLAG_ORIGINAL_MESSAGE);
$desc->save();
}

$task->logEvent('created', null, $thisstaff);

Expand All @@ -1396,11 +1515,76 @@ static function create($vars=false) {
$task->assign($form, $_errors);
}

$task->onNewTask();

Signal::send('task.created', $task);

return $task;
}

function onNewTask($vars=array()) {
global $cfg, $thisstaff;

if (!$cfg->alertONNewTask() // Check if alert is enabled
|| !($dept=$this->getDept())
|| ($dept->isGroupMembershipEnabled() == Dept::ALERTS_DISABLED)
|| !($email=$cfg->getAlertEmail())
|| !($tpl = $dept->getTemplate())
|| !($msg=$tpl->getNewTaskAlertMsgTemplate())
) {
return;
}

// Check if Dept recipients is Admin Only
$adminOnly = ($dept->isGroupMembershipEnabled() == Dept::ALERTS_ADMIN_ONLY);

// Alert recipients
$recipients = array();

// Department Manager
if ($cfg->alertDeptManagerONNewTask()
&& $dept->getManagerId()
&& !$adminOnly)
$recipients[] = $dept->getManager();

// Department Members
if ($cfg->alertDeptMembersONNewTask() && !$adminOnly)
foreach ($dept->getMembersForAlerts() as $M)
$recipients[] = $M;

$options = array();
$staffId = $thisstaff ? $thisstaff->getId() : 0;

$msg = $this->replaceVars($msg->asArray(), $vars);

$sentlist=array();
foreach ($recipients as $k=>$staff) {
if (!is_object($staff)
// Don't bother vacationing staff.
|| !$staff->isAvailable()
// No need to alert the poster!
|| $staffId == $staff->getId()
// No duplicates.
|| isset($sentlist[$staff->getEmail()])
// Make sure staff has access to task
|| !$this->checkStaffPerm($staff)
) {
continue;
}
$alert = $this->replaceVars($msg, array('recipient' => $staff));
$email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
$sentlist[$staff->getEmail()] = 1;
}

// Alert Admin ONLY if not already a staff
if ($cfg->alertAdminONNewTask()
&& !in_array($cfg->getAdminEmail(), $sentlist)) {
$alert = $this->replaceVars($msg, array('recipient' => __('Admin')));
$email->sendAlert($cfg->getAdminEmail(), $alert['subj'],
$alert['body'], null, $options);
}
}

function delete($comments='') {
global $ost, $thisstaff;

Expand Down
72 changes: 43 additions & 29 deletions include/class.thread.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ function isCollaborator($user) {
}

function addCollaborator($user, $vars, &$errors, $event=true) {
global $cfg, $thisstaff;

if (!$user)
return null;

Expand All @@ -176,27 +178,22 @@ function addCollaborator($user, $vars, &$errors, $event=true) {

$vars = array_merge(array(
'threadId' => $this->getId(),
'userId' => $user->getId()), $vars);
'userId' => $user->getId()), $vars ?: array());
if (!($c=Collaborator::add($vars, $errors)))
return null;

$c->active = true;
// Disable Agent Collabs (if configured) for User created tickets
if (!$thisstaff && $this->object_type === 'T'
&& $cfg->disableAgentCollaborators()
&& Staff::lookup($user->getDefaultEmailAddress()))
$c->active = false;

$this->_collaborators = null;

if ($event) {
$this->getEvents()->log($this->getObject(),
'collab',
array('add' => array($user->getId() => array(
'name' => $user->getName()->getOriginal(),
'src' => @$vars['source'],
))
)
);

$type = array('type' => 'collab', 'add' => array($user->getId() => array(
'name' => $user->getName()->name,
'src' => @$vars['source'],
)));
Signal::send('object.created', $this->getObject(), $type);
$vars['add'] = true;
$this->logCollaboratorEvents($user, $vars);
}


Expand All @@ -217,14 +214,7 @@ function updateCollaborators($vars, &$errors) {
&& $c->delete())
$collabs[] = $c;

$this->getEvents()->log($this->getObject(), 'collab', array(
'del' => array($c->user_id => array('name' => $c->getName()->getOriginal()))
));
$type = array('type' => 'collab', 'del' => array($c->user_id => array(
'name' => $c->getName()->getOriginal(),
'src' => @$vars['source'],
)));
Signal::send('object.deleted', $this->getObject(), $type);
$this->logCollaboratorEvents($c, $vars);
}
}

Expand Down Expand Up @@ -267,6 +257,23 @@ function updateCollaborators($vars, &$errors) {
return true;
}

function logCollaboratorEvents($collaborator, $vars) {
$name = $collaborator->getName()->getOriginal();
$userId = (get_class($collaborator) == 'User')
? $collaborator->getId() : $collaborator->user_id;
$action = $vars['del'] ? 'object.deleted' : 'object.created';
$addDel = $vars['del'] ? 'del' : 'add';

$this->getEvents()->log($this->getObject(), 'collab', array(
$addDel => array($userId => array('name' => $name))
));
$type = array('type' => 'collab', $addDel => array($userId => array(
'name' => $name,
'src' => @$vars['source'],
)));
Signal::send($action, $this->getObject(), $type);
}

//UserList of participants (collaborators)
function getParticipants() {

Expand Down Expand Up @@ -1556,11 +1563,17 @@ function lookupByRefMessageId($mid, $from) {

function setExtra($entries, $info=NULL, $thread_id=NULL) {
foreach ($entries as $entry) {
$mergeInfo = new ThreadEntryMergeInfo(array(
'thread_entry_id' => $entry->getId(),
'data' => json_encode($info),
));
$mergeInfo->save();
$mergeInfo = ThreadEntryMergeInfo::objects()
->filter(array('thread_entry_id'=>$entry->getId()))
->values_flat('thread_entry_id')
->first();
if (!$mergeInfo) {
$mergeInfo = new ThreadEntryMergeInfo(array(
'thread_entry_id' => $entry->getId(),
'data' => json_encode($info),
));
$mergeInfo->save();
}
$entry->saveExtra($info, $thread_id);
}

Expand Down Expand Up @@ -1642,7 +1655,7 @@ static function create($vars=false) {
'created' => SqlFunction::NOW(),
'type' => $vars['type'],
'thread_id' => $vars['threadId'],
'title' => Format::sanitize($vars['title'], true),
'title' => Format::strip_emoticons(Format::sanitize($vars['title'], true)),
'format' => $vars['body']->getType(),
'staff_id' => $vars['staffId'],
'user_id' => $vars['userId'],
Expand Down Expand Up @@ -3351,6 +3364,7 @@ interface Threadable {
function getThreadId();
function getThread();
function postThreadEntry($type, $vars, $options=array());
function addCollaborator($user, $vars, &$errors, $event=true);
}

/**
Expand Down
272 changes: 158 additions & 114 deletions include/class.ticket.php

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions include/class.topic.php
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ function delete() {
'topic_id' => $this->getId()
))->delete();
db_query('UPDATE '.TICKET_TABLE.' SET topic_id=0 WHERE topic_id='.db_input($this->getId()));

$type = array('type' => 'deleted');
Signal::send('object.deleted', $this, $type);
}

return true;
Expand Down Expand Up @@ -462,16 +465,16 @@ function update($vars, &$errors) {
$this->isactive = $vars['isactive'];
$this->ispublic = $vars['ispublic'];
$this->sequence_id = $vars['custom-numbers'] ? $vars['sequence_id'] : 0;
$this->number_format = $vars['custom-numbers'] ? $vars['number_format'] : '';
$this->flags = $vars['custom-numbers'] ? self::FLAG_CUSTOM_NUMBERS : $this->flags;
$this->number_format = $vars['number_format'];
$this->setFlag(self::FLAG_CUSTOM_NUMBERS, ($vars['custom-numbers']));
$this->noautoresp = $vars['noautoresp'];
$this->notes = Format::sanitize($vars['notes']);

$filter_actions = FilterAction::objects()->filter(array('type' => 'topic', 'configuration' => '{"topic_id":'. $this->getId().'}'));
if ($filter_actions && $vars['status'] == 'active')
FilterAction::setFilterFlag($filter_actions, 'topic', false);
FilterAction::setFilterFlags($filter_actions, 'Filter::FLAG_INACTIVE_HT', false);
else
FilterAction::setFilterFlag($filter_actions, 'topic', true);
FilterAction::setFilterFlags($filter_actions, 'Filter::FLAG_INACTIVE_HT', true);

switch ($vars['status']) {
case 'active':
Expand Down
21 changes: 12 additions & 9 deletions include/class.user.php
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ function getEmail() {

if (!isset($this->_email))
$this->_email = new EmailAddress(sprintf('"%s" <%s>',
$this->getName(),
addcslashes($this->getName(), '"'),
$this->default_email->address));

return $this->_email;
Expand Down Expand Up @@ -528,6 +528,9 @@ static function importCsv($stream, $defaults=array()) {
}

function importFromPost($stream, $extra=array()) {
if (!is_array($stream))
$stream = sprintf('name, email%s %s',PHP_EOL, $stream);

return User::importCsv($stream, $extra);
}

Expand Down Expand Up @@ -697,21 +700,21 @@ function getTicketsQueue($collabs=true) {

if (!$this->_queue) {
$email = $this->getDefaultEmailAddress();
$filter = new Q(array(
'user__id' => $this->getId()
));
$filter = [
['user__id', 'equal', $this->getId()],
];
if ($collabs)
$filter = Q::any(array(
'user__emails__address' => $email,
'thread__collaborators__user__emails__address' => $email,
));
$filter = [
['user__emails__address', 'equal', $email],
['thread__collaborators__user__emails__address', 'equal', $email],
];
$this->_queue = new AdhocSearch(array(
'id' => 'adhoc,uid'.$this->getId(),
'root' => 'T',
'staff_id' => $thisstaff->getId(),
'title' => $this->getName()
));
$this->_queue->filter($filter);
$this->_queue->config = $filter;
}

return $this->_queue;
Expand Down
25 changes: 20 additions & 5 deletions include/class.validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,19 +184,25 @@ static function is_email($email, $list=false, $verify=false) {
// MX if no MX records exist for the domain. Also, include a
// full-stop trailing char so that the default domain of the server
// is not added automatically
if ($verify and !count(dns_get_record($m->host.'.', DNS_MX)))
return 0 < count(dns_get_record($m->host.'.', DNS_A|DNS_AAAA));
if ($verify and !dns_get_record($m->host.'.', DNS_MX))
return 0 < @count(dns_get_record($m->host.'.', DNS_A|DNS_AAAA));

return true;
}

static function is_valid_email($email) {
static function is_numeric($number, &$error='') {
if (!is_numeric($number))
$error = __('Enter a Number');
return $error == '';
}

static function is_valid_email($email, &$error='') {
global $cfg;
// Default to FALSE for installation
return self::is_email($email, false, $cfg && $cfg->verifyEmailAddrs());
}

static function is_phone($phone) {
static function is_phone($phone, &$error='') {
/* We're not really validating the phone number but just making sure it doesn't contain illegal chars and of acceptable len */
$stripped=preg_replace("(\(|\)|\-|\.|\+|[ ]+)","",$phone);
return (!is_numeric($stripped) || ((strlen($stripped)<7) || (strlen($stripped)>16)))?false:true;
Expand All @@ -207,7 +213,7 @@ static function is_url($url) {
return ($url && ($info=parse_url($url)) && $info['host']);
}

static function is_ip($ip) {
static function is_ip($ip, &$error='') {
return filter_var(trim($ip), FILTER_VALIDATE_IP) !== false;
}

Expand All @@ -225,6 +231,15 @@ static function is_formula($text, &$error='') {
return $error == '';
}

static function check_passwd($passwd, &$error='') {
try {
PasswordPolicy::checkPassword($passwd, null);
} catch (BadPassword $ex) {
$error = $ex->getMessage();
}
return $error == '';
}

/*
* check_ip
* Checks if an IP (IPv4 or IPv6) address is contained in the list of given IPs or subnets.
Expand Down
12 changes: 12 additions & 0 deletions include/client/header.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
if ($lang) {
echo ' lang="' . $lang . '"';
}

// Dropped IE Support Warning
if (osTicket::is_ie())
$ost->setWarning(__('osTicket no longer supports Internet Explorer.'));
?>>
<head>
<meta charset="utf-8">
Expand Down Expand Up @@ -85,6 +89,14 @@
</head>
<body>
<div id="container">
<?php
if($ost->getError())
echo sprintf('<div class="error_bar">%s</div>', $ost->getError());
elseif($ost->getWarning())
echo sprintf('<div class="warning_bar">%s</div>', $ost->getWarning());
elseif($ost->getNotice())
echo sprintf('<div class="notice_bar">%s</div>', $ost->getNotice());
?>
<div id="header">
<div class="pull-right flush-right">
<p>
Expand Down
2 changes: 1 addition & 1 deletion include/client/open.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
$('.richtext').each(function() {
var redactor = $(this).data('redactor');
if (redactor && redactor.opts.draftDelete)
redactor.draft.deleteDraft();
redactor.plugin.draft.deleteDraft();
});
window.location.href='index.php';">
</p>
Expand Down
1 change: 1 addition & 0 deletions include/i18n/en_US/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ core:
show_assigned_tickets: 1
show_answered_tickets: 0
hide_staff_name: 0
disable_agent_collabs: 0
overlimit_notice_active: 0
email_attachments: 1
ticket_number_format: '######'
Expand Down
4 changes: 2 additions & 2 deletions include/i18n/en_US/form.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,15 @@
fields:
- type: text # notrans
name: title # notrans
flags: 0x470B1
flags: 0x770B1
sort: 1
label: Title
configuration:
size: 40
length: 50
- type: thread # notrans
name: description # notrans
flags: 0x450F3
flags: 0x650F3
sort: 2
label: Description
hint: Details on the reason(s) for creating the task.
10 changes: 10 additions & 0 deletions include/i18n/en_US/help/tips/emails.email.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ auto_response:
You may disable the Auto-Response sent to the User when a new ticket
is created via this Email Address.
userid:
title: Username
content: >
The Username is utilized in the email authentication process. We accept
single address strings, shared mailbox strings, servername + username
strings, and a combination of all.
links:
- title: More Information
href: https://docs.osticket.com/en/latest/Admin/Emails/Emails.html

username:
title: Username
content: >
Expand Down
11 changes: 11 additions & 0 deletions include/i18n/en_US/help/tips/settings.agents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ staff_identity_masking:
If enabled, this will hide the Agent’s name from the Client during any
communication.
disable_agent_collabs:
title: Disable Agent Collaborators
content: >
If Enabled, Agents that are added as Collaborators by Users will be automatically
Disabled. This is helpful when Users are blindly adding Agents to the CC field
causing the Agents to receive all of the Participant Alerts.
<br><br>
<strong>Note:</strong>
<br>
This setting is global for all User created Tickets via API, Piping, and Fetching.
# Authentication settings
password_reset:
title: Password Expiration Policy
Expand Down
12 changes: 12 additions & 0 deletions include/i18n/en_US/help/tips/settings.system.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ default_schedule:
- title: Manage Schedules
href: /scp/schedules.php

force_https:
title: Force HTTPS
content: >
This setting allows Admins to configure wether or not they want to Force
HTTPS system-wide. If enabled, any request that is using the HTTP protocol
will be redirected to the HTTPS protocol. Note, this will only work if you
have an SSL certificate installed and have HTTPS configured on the server.
<br/><br/>
<b>Note:</b><rb/>
This might affect remote piping scripts. Reference new scripts included in
<code>setup/scripts/</code> for updates.
default_page_size:
title: Default Page Size
content: >
Expand Down
6 changes: 6 additions & 0 deletions include/i18n/en_US/help/tips/tickets.queue.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ child_status:
content: >
All Child Tickets will be set to a closed status since thread entries will all be moved to the Parent Ticket.
parent_status:
title: Parent Ticket Status
content: >
If you choose to set a Parent Status, the Parent Ticket will be changed to the status you select.
The Ticket on top of the list will be the Parent Ticket.
reply_types:
title: Reply Types
content: >
Expand Down
6 changes: 6 additions & 0 deletions include/i18n/en_US/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
permissions: [
ticket.create,
ticket.edit,
ticket.merge,
ticket.link,
ticket.assign,
ticket.release,
ticket.transfer,
Expand All @@ -45,6 +47,8 @@
permissions: [
ticket.create,
ticket.edit,
ticket.merge,
ticket.link,
ticket.assign,
ticket.release,
ticket.transfer,
Expand All @@ -67,6 +71,8 @@
permissions: [
ticket.create,
ticket.merge,
ticket.link,
ticket.assign,
ticket.release,
ticket.transfer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ public function __construct()
'packTableData' => false,

'ignore_table_percents' => false,
'ignore_table_widths' => false,
'ignore_table_widths' => true,
// If table width set > page width, force resizing but keep relative sizes
// Also forces respect of cell widths set by %
'keep_table_proportions' => true,
Expand Down
1 change: 1 addition & 0 deletions include/pear/Mail/mimeDecode.php
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ function _decode(&$headers, &$body, $default_ctype = 'text/plain')

case 'message/rfc822':
$obj = new Mail_mimeDecode($body);
$return->body = $body;
$return->parts[] = $obj->decode(array('include_bodies' => $this->_include_bodies,
'decode_bodies' => $this->_decode_bodies,
'decode_headers' => $this->_decode_headers));
Expand Down
7 changes: 2 additions & 5 deletions include/staff/department.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
if(!array_key_exists($info['pid'], $depts) && $info['pid'])
{
$depts[$info['pid']] = $current_name;
$warn = sprintf(__('%s selected must be active'), __('Parent Department'));
$errors['pid'] = sprintf(__('%s selected must be active'), __('Parent Department'));
}
foreach ($depts as $id=>$name) {
$selected=($info['pid'] && $id==$info['pid'])?'selected="selected"':'';
Expand All @@ -76,10 +76,7 @@
}
?>
</select>
<?php
if($warn) { ?>
&nbsp;<span class="error">*&nbsp;<?php echo $warn; ?></span>
<?php } ?>
&nbsp;<span class="error">*&nbsp;<?php echo $errors['pid']; ?></span>
</td>
</tr>
<tr>
Expand Down
1 change: 1 addition & 0 deletions include/staff/email.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
<input type="text" size="35" name="userid" value="<?php echo $info['userid']; ?>"
autocomplete="off" autocorrect="off">
&nbsp;<span class="error">&nbsp;<?php echo $errors['userid']; ?>&nbsp;</span>
<i class="help-tip icon-question-sign" href="#userid"></i>
</td>
</tr>
<tr>
Expand Down
5 changes: 4 additions & 1 deletion include/staff/filters.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@
if ($row['flags'] & Filter::FLAG_INACTIVE_HT)
echo '<a data-placement="bottom" data-toggle="tooltip" title="Inactive Help Topic Selected"
<i class="pull-right icon-warning-sign"></a>';

if ($row['flags'] & Filter::FLAG_DELETED_OBJECT)
echo '<a data-placement="bottom" data-toggle="tooltip" title="Deleted Object Selected"
<i class="pull-right icon-warning-sign"></a>';
?>
</td>
<td><?php echo $row['isactive']?__('Active'):'<b>'.__('Disabled').'</b>'; ?></td>
Expand Down Expand Up @@ -190,4 +194,3 @@
</p>
<div class="clear"></div>
</div>

4 changes: 4 additions & 0 deletions include/staff/header.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
if ($lang) {
echo ' lang="' . Internationalization::rfc1766($lang) . '"';
}

// Dropped IE Support Warning
if (osTicket::is_ie())
$ost->setWarning(__('osTicket no longer supports Internet Explorer.'));
?>>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
Expand Down
6 changes: 5 additions & 1 deletion include/staff/orgs.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,11 @@
$form.find('#selected-count').val(ids.length);
$form.submit();
};
$.confirm(__('You sure?')).then(submit);
$.confirm(__('You sure?')).then(function(promise) {
if (promise === false)
return false;
submit();
});
}
else if (!ids.length) {
$.sysAlert(__('Oops'),
Expand Down
10 changes: 6 additions & 4 deletions include/staff/plugins.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
<thead>
<tr>
<th width="4%">&nbsp;</th>
<th width="66%"><?php echo __('Plugin Name'); ?></th>
<th width="56%"><?php echo __('Plugin Name'); ?></th>
<th width="10%"><?php echo __('Version'); ?></th>
<th width="10%"><?php echo __('Status'); ?></th>
<th width="20%"><?php echo __('Date Installed'); ?></th>
</tr>
Expand All @@ -67,8 +68,9 @@
<tr>
<td align="center"><input type="checkbox" class="ckb" name="ids[]" value="<?php echo $p->getId(); ?>"
<?php echo $sel?'checked="checked"':''; ?>></td>
<td><a href="plugins.php?id=<?php echo $p->getId(); ?>"
><?php echo $p->getName(); ?></a></td>
<td><a href="plugins.php?id=<?php echo $p->getId(); ?>">
<?php echo $p->getName(); ?></a></td>
<td><?php echo $p->getVersion(); ?></a></td>
<td><?php echo ($p->isActive())
? 'Enabled' : '<strong>Disabled</strong>'; ?></td>
<td><?php echo Format::datetime($p->getInstallDate()); ?></td>
Expand All @@ -78,7 +80,7 @@
</tbody>
<tfoot>
<tr>
<td colspan="4">
<td colspan="5">
<?php if($count){ ?>
<?php echo __('Select'); ?>:&nbsp;
<a id="selectAll" href="#ckb"><?php echo __('All'); ?></a>&nbsp;&nbsp;
Expand Down
18 changes: 18 additions & 0 deletions include/staff/profile.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,24 @@ class="staff-username typeahead"
<div class="error"><?php echo $errors['img_att_view']; ?></div>
</td>
</tr>
<tr>
<td><?php echo __('Editor Spacing'); ?>:
<div class="faded"><?php echo __('Set the editor spacing to Single or Double when pressing Enter.');?></div>
</td>
<td>
<select name="editor_spacing">
<?php
$options=array('double'=>__('Double'),'single'=>__('Single'));
$spacing = $staff->editor_spacing;
foreach($options as $key=>$opt) {
echo sprintf('<option value="%s" %s>%s</option>',
$key,($spacing==$key)?'selected="selected"':'',$opt);
}
?>
</select>
<div class="error"><?php echo $errors['editor_spacing']; ?></div>
</td>
</tr>
</tbody>
<tbody>
<tr class="header">
Expand Down
4 changes: 3 additions & 1 deletion include/staff/queues-ticket.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@
ids.push($(this).val());
});
if (ids.length) {
$.confirm(__('You sure?')).then(function() {
$.confirm(__('You sure?')).then(function(promise) {
if (promise === false)
return false;
$.each(ids, function() { $form.append($('<input type="hidden" name="ids[]">').val(this)); });
$form.append($('<input type="hidden" name="a" />')
.val($(that).data('action')));
Expand Down
9 changes: 9 additions & 0 deletions include/staff/settings-agents.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@
<div class="error"><?php echo Format::htmlchars($errors['agent_avatar']); ?></div>
</td>
</tr>
<tr>
<td><?php echo __('Disable Agent Collaborators'); ?>:</td>
<td>
<input type="checkbox" name="disable_agent_collabs"
<?php echo $config['disable_agent_collabs']?'checked="checked"':''; ?>>
<?php echo __('Enable'); ?>&nbsp;<i class="help-tip icon-question-sign"
href="#disable_agent_collabs"></i>
</td>
</tr>
<tr>
<th colspan="2">
<em><b><?php echo __('Authentication Settings'); ?></b></em>
Expand Down
10 changes: 10 additions & 0 deletions include/staff/settings-system.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@
<i class="help-tip icon-question-sign" href="#default_department"></i>
</td>
</tr>
<tr>
<td><?php echo __('Force HTTPS'); ?>:</td>
<td>
<input type="checkbox" name="force_https" <?php
echo $config['force_https'] ? 'checked="checked"' : ''; ?>>
<?php echo __('Force all requests through HTTPS.'); ?>
<font class="error"><?php echo $errors['force_https']; ?></font>
<i class="help-tip icon-question-sign" href="#force_https"></i>
</td>
</tr>
<tr>
<td><?php echo __('Collision Avoidance Duration'); ?>:</td>
<td>
Expand Down
2 changes: 1 addition & 1 deletion include/staff/tasks.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
$sort_options = array(
'updated' => __('Most Recently Updated'),
'created' => __('Most Recently Created'),
'due' => __('Due Soon'),
'due' => __('Due Date'),
'number' => __('Task Number'),
'closed' => __('Most Recently Closed'),
'hot' => __('Longest Thread'),
Expand Down
2 changes: 1 addition & 1 deletion include/staff/templates/dynamic-field-config.tmpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
</div>
<div style="width:100%">
<textarea style="width:90%; width:calc(100% - 20px)" name="hint" rows="2" cols="40"
class="richtext small no-bar"
class="richtext small"
data-translate-tag="<?php echo $field->getTranslateTag('hint'); ?>"><?php
echo Format::htmlchars($field->get('hint')); ?></textarea>
</div>
Expand Down
2 changes: 1 addition & 1 deletion include/staff/templates/dynamic-form.tmpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
?>" data-entry-id="<?php echo $field->getAnswer()->get('entry_id');
?>"> <i class="icon-trash"></i> </a></div><?php
}
if ($a && !$a->getValue() && $field->isRequiredForClose()) {
if ($a && !$a->getValue() && $field->isRequiredForClose() && get_class($field) != 'BooleanField') {
?><i class="icon-warning-sign help-tip warning"
data-title="<?php echo __('Required to close ticket'); ?>"
data-content="<?php echo __('Data is required in this field in order to close the related ticket'); ?>"
Expand Down
Loading