diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 8246670fb05e453860e1a84426c4fb8e8a49cde5..5f273a84dcf16fbc42884d2229b7c0a801e0aa7b 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -23,6 +23,6 @@ For more information on how to write a good [bug report](https://github.com/osTi ### Versions -Admin panel -> Dashboard -> Information which also additionally gives you information about you server. +Admin panel -> Dashboard -> Information which also additionally gives you information about your server. Also, please include the OS and what version of the OS you're running. As well as your browser and browser version. diff --git a/README.md b/README.md index e231d24008f930eb0fead31fabb74006b27a0c24..41013293c930aa0f63efdac72ea53c86665fa497 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ easy to setup and use. The best part is, it's completely free. Requirements ------------ * HTTP server running Microsoft® IIS or Apache - * PHP version 5.4 or greater, 5.6 is recommended + * PHP version 5.6 to 7.2, 7.2 is recommended * mysqli extension for PHP * MySQL database version 5.0 or greater diff --git a/bootstrap.php b/bootstrap.php index 8b7ea7eb9e20e2e4e74ee554242ac110afc861ee..f306bb4550b241aa6aaca9dbbd4a71fb18918fd3 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -295,6 +295,12 @@ class Bootstrap { if (extension_loaded('iconv')) iconv_set_encoding('internal_encoding', 'UTF-8'); + if (intval(phpversion()) < 7) { + function random_int($a, $b) { + return rand($a, $b); + } + } + function mb_str_wc($str) { return count(preg_split('~[^\p{L}\p{N}\'].+~u', trim($str))); } diff --git a/client.inc.php b/client.inc.php index 2ad4d4702139d68e589dd917acfcedd4ca28f30d..63d7d12243c130ded357a7fe59f9d7a6024a5667 100644 --- a/client.inc.php +++ b/client.inc.php @@ -22,6 +22,10 @@ require_once($thisdir.'main.inc.php'); if(!defined('INCLUDE_DIR')) die('Fatal error'); +// Enforce ACL (if applicable) +if (!Validator::check_acl('client')) + die(__('Access Denied')); + /*Some more include defines specific to client only */ define('CLIENTINC_DIR',INCLUDE_DIR.'client/'); define('OSTCLIENTINC',TRUE); diff --git a/include/ajax.forms.php b/include/ajax.forms.php index fddaec16b2711f7e14c720f1ceafdbe1c8a33d4d..3217124435bbe624bd297b85738cc67b395ec717 100644 --- a/include/ajax.forms.php +++ b/include/ajax.forms.php @@ -151,6 +151,8 @@ class DynamicFormsAjaxAPI extends AjaxController { function saveListItem($list_id, $item_id) { global $thisstaff; + $errors = array(); + if (!$thisstaff) Http::response(403, 'Login required'); @@ -179,7 +181,7 @@ class DynamicFormsAjaxAPI extends AjaxController { 'name' => $basic['name'], 'value' => $basic['value'], 'abbrev' => $basic['extra'], - ]); + ], $errors); } } diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index c92789dbb1425b105e93cc368afb90edbcf8e540..2a176b9b8c7699010639d54d6dadadb19af36488 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -358,7 +358,7 @@ class TicketsAjaxAPI extends AjaxController { $format.='.plain'; $varReplacer = function (&$var) use($ticket) { - return $ticket->replaceVars($var); + return $ticket->replaceVars($var, array('recipient' => $ticket->getOwner())); }; include_once(INCLUDE_DIR.'class.canned.php'); diff --git a/include/class.config.php b/include/class.config.php index 8cdf808e8838c6e19f1d73d1c97f0dc7094df76c..c13e2617cc360f9877a448dfe77946117cc2625e 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -420,6 +420,30 @@ class OsticketConfig extends Config { return $this->get('enable_richtext'); } + function getAllowIframes() { + return str_replace(array(', ', ','), array(' ', ' '), $this->get('allow_iframes')) ?: "'self'"; + } + + function getACL() { + if (!($acl = $this->get('acl'))) + return null; + + return explode(',', str_replace(' ', '', $acl)); + } + + function getACLBackendOpts() { + return array( + 0 => __('Disabled'), + 1 => __('All'), + 2 => __('Client Portal'), + 3 => __('Staff Panel') + ); + } + + function getACLBackend() { + return $this->get('acl_backend') ?: 0; + } + function isAvatarsEnabled() { return $this->get('enable_avatars'); } @@ -1120,6 +1144,8 @@ class OsticketConfig extends Config { $f['helpdesk_title']=array('type'=>'string', 'required'=>1, 'error'=>__('Helpdesk title is required')); $f['default_dept_id']=array('type'=>'int', 'required'=>1, 'error'=>__('Default Department is required')); $f['autolock_minutes']=array('type'=>'int', 'required'=>1, 'error'=>__('Enter lock time in minutes')); + $f['allow_iframes']=array('type'=>'cs-domain', 'required'=>0, 'error'=>__('Enter comma separated list of domains')); + $f['acl']=array('type'=>'ipaddr', 'required'=>0, 'error'=>__('Enter comma separated list of IP addresses')); //Date & Time Options $f['time_format']=array('type'=>'string', 'required'=>1, 'error'=>__('Time format is required')); $f['date_format']=array('type'=>'string', 'required'=>1, 'error'=>__('Date format is required')); @@ -1130,6 +1156,18 @@ class OsticketConfig extends Config { $vars = Format::htmlchars($vars, true); + // ACL Checks + if ($vars['acl']) { + // Check if Admin's IP is in the list, if not, return error + // to avoid locking self out + if (!in_array($vars['acl_backend'], array(0,2))) { + $acl = explode(',', str_replace(' ', '', $acl)); + if (!in_array(osTicket::get_client_ip(), $acl)) + $errors['acl'] = __('Cowardly refusing to lock out active administrator'); + } + } elseif ((int) $vars['acl_backend'] !== 0) + $errors['acl'] = __('IP address required when selecting panel'); + // Make sure the selected backend is valid $storagebk = null; if (isset($vars['default_storage_bk'])) { @@ -1178,6 +1216,9 @@ class OsticketConfig extends Config { 'enable_avatars' => isset($vars['enable_avatars']) ? 1 : 0, 'enable_richtext' => isset($vars['enable_richtext']) ? 1 : 0, 'files_req_auth' => isset($vars['files_req_auth']) ? 1 : 0, + 'allow_iframes' => Format::sanitize($vars['allow_iframes']), + 'acl' => Format::sanitize($vars['acl']), + 'acl_backend' => Format::sanitize((int) $vars['acl_backend']) ?: 0, )); } diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index 75c3e9904136dcab723426ed2159f6a07007fd23..f5037503d541b97d6d07974b8cb2e60420da20d1 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -1194,11 +1194,10 @@ class DynamicFormEntry extends VerySimpleModel { $field = $a->getField(); if (!$field->hasData() || $field->isPresentationOnly()) continue; - $after = $field->to_database($field->getClean()); - $before = $field->to_database($a->getValue()); - if ($before == $after) + $changes = $field->getChanges(); + if (!$changes) continue; - $fields[$field->get('id')] = array($before, $after); + $fields[$field->get('id')] = $changes; } return $fields; } diff --git a/include/class.faq.php b/include/class.faq.php index 38e20b55cfa14b176eeae22913f96448e8b850d3..4f1085affc2e1960ecef4f1587f053cbdbe3b912 100644 --- a/include/class.faq.php +++ b/include/class.faq.php @@ -155,7 +155,8 @@ class FAQ extends VerySimpleModel { include STAFFINC_DIR . 'templates/faq-print.tmpl.php'; $html = ob_get_clean(); - $pdf = new mPDFWithLocalImages('', $paper); + $pdf = new mPDFWithLocalImages(['mode' => 'utf-8', 'format' => + $paper, 'tempDir'=>sys_get_temp_dir()]); // Setup HTML writing and load default thread stylesheet $pdf->WriteHtml( '<style> diff --git a/include/class.format.php b/include/class.format.php index 2e745a592a3083f876fb53116a60153d5eb98748..7d4877b5e9a33778d8362191b33c237154f437b8 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -424,6 +424,33 @@ class Format { return strip_tags($decode?Format::htmldecode($var):$var); } + // Strip all Emoticon/Emoji characters until we support them + function strip_emoticons($text) { + return preg_replace(array( + '/[\x{1F601}-\x{1F64F}]/u', # Emoticons + '/[\x{1F680}-\x{1F6C0}]/u', # Transport/Map + '/[\x{1F600}-\x{1F636}]/u', # Add. Emoticons + '/[\x{1F681}-\x{1F6C5}]/u', # Add. Transport/Map + '/[\x{1F30D}-\x{1F567}]/u', # Other + '/[\x{1F910}-\x{1F999}]/u', # Hands + '/[\x{1F9D0}-\x{1F9DF}]/u', # Fantasy + '/[\x{1F9E0}-\x{1F9EF}]/u', # Clothes + '/[\x{1F6F0}-\x{1F6FF}]/u', # Misc. Transport + '/[\x{1F6E0}-\x{1F6EF}]/u', # Planes/Boats + '/[\x{1F6C0}-\x{1F6CF}]/u', # Bed/Bath + '/[\x{1F9C0}-\x{1F9C2}]/u', # Misc. Food + '/[\x{1F6D0}-\x{1F6D2}]/u', # Sign/P.O.W./Cart + '/[\x{1F500}-\x{1F5FF}]/u', # Uncategorized + '/[\x{1F300}-\x{1F3FF}]/u', # Cyclone/Amphora + '/[\x{2702}-\x{27B0}]/u', # Dingbats + '/[\x{00A9}-\x{00AE}]/u', # Copyright/Registered + '/[\x{23F0}-\x{23FF}]/u', # Clock/Buttons + '/[\x{23E0}-\x{23EF}]/u', # More Buttons + '/[\x{2310}-\x{231F}]/u', # Hourglass/Watch + '/[\x{2322}-\x{232F}]/u' # Keyboard + ), '', $text); + } + //make urls clickable. Mainly for display function clickableurls($text, $target='_blank') { global $ost; diff --git a/include/class.forms.php b/include/class.forms.php index 0cbabf90b238cd0fd84fb6fa71fe4bbdca9f2f83..3cef29c99e95e1dfd1b862b75d46d54bc647faed 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -1535,7 +1535,7 @@ class TextareaField extends FormField { 'choices' => array( function($val) { $val = str_replace('"', '', JsonDataEncoder::encode($val)); - $regex = "/^(?! )[A-z0-9 _-]+:{1}[A-z0-9 _-]+$/"; + $regex = "/^(?! )[A-z0-9 _-]+:{1}[^\n]+$/"; foreach (explode('\r\n', $val) as $v) { if (!preg_match($regex, $v)) return false; @@ -1677,6 +1677,24 @@ class BooleanField extends FormField { return ($value) ? __('Yes') : __('No'); } + function getClean($validate=true) { + if (!isset($this->_clean)) { + $this->_clean = (isset($this->value)) + ? $this->value : $this->getValue(); + + if ($this->isVisible() && $validate) + $this->validateEntry($this->_clean); + } + return $this->_clean; + } + + function getChanges() { + $new = $this->getValue(); + $old = $this->answer ? $this->answer->getValue() : $this->get('default'); + + return ($old != $new) ? array($this->to_database($old), $this->to_database($new)) : false; + } + function getSearchMethods() { return array( 'set' => __('checked'), @@ -1895,7 +1913,7 @@ class ChoiceField extends FormField { $choices = explode("\n", $config['choices']); foreach ($choices as $choice) { // Allow choices to be key: value - list($key, $val) = explode(':', $choice); + list($key, $val) = explode(':', $choice, 2); if ($val == null) $val = $key; $this->_choices[trim($key)] = trim($val); @@ -1980,6 +1998,24 @@ class ChoiceField extends FormField { } function applyQuickFilter($query, $qf_value, $name=false) { + global $thisstaff; + + //special assignment quick filters + switch (true) { + case ($qf_value == 'assigned'): + case ($qf_value == '!assigned'): + $result = AssigneeChoiceField::getSearchQ($qf_value, $qf_value); + return $query->filter($result); + case (strpos($qf_value, 's') !== false): + case (strpos($qf_value, 't') !== false): + case ($qf_value == 'M'): + case ($qf_value == 'T'): + $value = array($qf_value => $qf_value); + $result = AssigneeChoiceField::getSearchQ('includes', $value); + return $query->filter($result); + break; + } + return $query->filter(array( $name ?: $this->get('name') => $qf_value, )); @@ -4563,7 +4599,7 @@ class FileUploadWidget extends Widget { allowedfiletypes: <?php echo JsonDataEncoder::encode( $mimetypes); ?>, maxfiles: <?php echo $config['max'] ?: 20; ?>, - maxfilesize: <?php echo $maxfilesize; ?>, + maxfilesize: <?php echo str_replace(',', '.', $maxfilesize); ?>, name: '<?php echo $name; ?>[]', files: <?php echo JsonDataEncoder::encode($files); ?> });}); diff --git a/include/class.mailer.php b/include/class.mailer.php index dfd65b2ab254fabbb7005ef55e30add8921ac85b..620cd84de92150beda0af5a08ff95e5db1c4d608 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -406,6 +406,8 @@ class Mailer { if (!is_array($recipients) && (!$recipients instanceof MailingList)) $recipients = array($recipients); foreach ($recipients as $recipient) { + if ($recipient instanceof ClientSession) + $recipient = $recipient->getSessionUser(); switch (true) { case $recipient instanceof EmailRecipient: $addr = sprintf('"%s" <%s>', diff --git a/include/class.queue.php b/include/class.queue.php index e336c2085e4e8cead73977a5b9ddf587c4c28f18..901fadbab3bc275178a8fb377f3e4c43fda68ef0 100644 --- a/include/class.queue.php +++ b/include/class.queue.php @@ -723,7 +723,7 @@ class CustomQueue extends VerySimpleModel { "bits" => QueueColumn::FLAG_SORTABLE, )), QueueColumn::placeholder(array( - "id" => 6, + "id" => 8, "heading" => "Assignee", "primary" => 'assignee', "width" => 100, @@ -807,9 +807,17 @@ class CustomQueue extends VerySimpleModel { // See if we have cached export preference if (isset($_SESSION['Export:Q'.$this->getId()])) { $opts = $_SESSION['Export:Q'.$this->getId()]; - if (isset($opts['fields'])) + if (isset($opts['fields'])) { $fields = array_intersect_key($fields, array_flip($opts['fields'])); + $exportableFields = CustomQueue::getExportableFields(); + foreach ($opts['fields'] as $key => $name) { + if (is_null($fields[$name]) && isset($exportableFields)) { + $fields[$name] = $exportableFields[$name]; + } + } + } + if (isset($opts['filename']) && ($parts = pathinfo($opts['filename']))) { $filename = $opts['filename']; @@ -1144,25 +1152,23 @@ class CustomQueue extends VerySimpleModel { $new = $fields; foreach ($this->exports as $f) { + $heading = $f->getHeading(); $key = $f->getPath(); if (!isset($fields[$key])) { $this->exports->remove($f); continue; } - $info = $fields[$key]; - if (is_array($info)) - $heading = $info['heading']; - else - $heading = $info; - $f->set('heading', $heading); $f->set('sort', array_search($key, $order)+1); unset($new[$key]); } + $exportableFields = CustomQueue::getExportableFields(); foreach ($new as $k => $field) { - if (is_array($field)) + if (isset($exportableFields[$k])) + $heading = $exportableFields[$k]; + elseif (is_array($field)) $heading = $field['heading']; else $heading = $field; @@ -1229,7 +1235,7 @@ class CustomQueue extends VerySimpleModel { $this->path = $this->buildPath(); $this->setFlag(self::FLAG_INHERIT_CRITERIA, $this->parent_id); $this->setFlag(self::FLAG_INHERIT_COLUMNS, - isset($vars['inherit-columns'])); + $this->parent_id > 0 && isset($vars['inherit-columns'])); $this->setFlag(self::FLAG_INHERIT_EXPORT, $this->parent_id > 0 && isset($vars['inherit-exports'])); $this->setFlag(self::FLAG_INHERIT_SORTING, diff --git a/include/class.search.php b/include/class.search.php index f2a77955b536dd165f28a333ffbd7e261a314e0b..ac7a8df2d1bd2fcdfe5024651b4b64c923b312e4 100755 --- a/include/class.search.php +++ b/include/class.search.php @@ -1308,6 +1308,13 @@ class AssigneeChoiceField extends ChoiceField { class AssignedField extends AssigneeChoiceField { + function getChoices($verbose=false) { + return array( + 'assigned' => __('Assigned'), + '!assigned' => __('Unassigned'), + ); + } + function getSearchMethods() { return array( 'assigned' => __('assigned'), @@ -1399,8 +1406,14 @@ class DepartmentManagerSelectionField extends AgentSelectionField { static $_members; function getChoices($verbose=false) { - if (isset($this->_members)) - $this->_members = Staff::getStaffMembers(); + if (!isset($this->_members)) { + $managers = array(); + $staff = Staff::objects()->filter(array('dept__manager_id__gt' => 0)); + foreach ($staff as $s) { + $managers['s'.$s->getId()] = $s->getName()->name; + } + $this->_members = $managers; + } return $this->_members; } @@ -1414,9 +1427,9 @@ class TeamSelectionField extends AdvancedSearchSelectionField { static $_teams; function getChoices($verbose=false) { - if (!isset($this->_teams)) + if (!isset($this->_teams) && $teams = Team::getTeams()) $this->_teams = array('T' => __('One of my teams')) + - Team::getTeams(); + $teams; return $this->_teams; } diff --git a/include/class.staff.php b/include/class.staff.php index 0deedfefc4568e579f211d098f07899ec8991822..2618730bcb985800fb3a46766af83738232e089c 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -597,6 +597,10 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { $visibility = Q::any(new Q(array('status__state'=>'open', $assigned))); + // -- If access is limited to assigned only, return assigned + if ($this->isAccessLimited()) + return $visibility; + // -- Routed to a department of mine if (($depts=$this->getDepts()) && count($depts)) { $visibility->add(array('dept_id__in' => $depts)); diff --git a/include/class.thread.php b/include/class.thread.php index f1c6ac988fd06715631d52fa03604da7108045fa..55be97a108a5fd22eca16543a87df79eab9d2848 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -1514,7 +1514,7 @@ implements TemplateVariable { $vars['body'] = new TextThreadEntryBody($vars['body']); } - if (!($body = $vars['body']->getClean())) + if (!($body = Format::strip_emoticons($vars['body']->getClean()))) $body = '-'; //Special tag used to signify empty message as stored. $poster = $vars['poster']; @@ -2989,7 +2989,7 @@ implements TemplateVariable { } function asVar() { - return $this->getVar('complete'); + return new ThreadEntries($this); } function getVar($name) { @@ -3013,21 +3013,14 @@ implements TemplateVariable { break; case 'complete': - $content = ''; - $thread = $this; - ob_start(); - include INCLUDE_DIR.'client/templates/thread-export.tmpl.php'; - $content = ob_get_contents(); - ob_end_clean(); - return $content; - + return $this->asVar(); break; } } static function getVarScope() { return array( - 'complete' => __('Thread Correspondence'), + 'complete' =>array('class' => 'ThreadEntries', 'desc' => __('Thread Correspondence')), 'original' => array('class' => 'MessageThreadEntry', 'desc' => __('Original Message')), 'lastmessage' => array('class' => 'MessageThreadEntry', 'desc' => __('Last Message')), ); @@ -3047,6 +3040,47 @@ implements TemplateVariable { } } +class ThreadEntries { + var $thread; + + function __construct($thread) { + $this->thread = $thread; + } + + function __tostring() { + return (string) $this->getVar(); + } + + function asVar() { + return $this->getVar(); + } + + function getVar($name='') { + + $order = ''; + switch ($name) { + case 'reversed': + $order = '-'; + default: + $content = ''; + $thread = $this->thread; + ob_start(); + include INCLUDE_DIR.'client/templates/thread-export.tmpl.php'; + $content = ob_get_contents(); + ob_end_clean(); + return $content; + break; + } + } + + static function getVarScope() { + return array( + 'reversed' => sprintf('%s %s', __('Thread Correspondence'), + __('in reversed order')), + ); + } +} + // Ticket thread class class TicketThread extends ObjectThread { static function create($ticket=false) { diff --git a/include/class.ticket.php b/include/class.ticket.php index 7c6194a62df667911ec99ca6a11f7cd8b40fd1db..a634799bfafc37787625e3089791ffc311417d09 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -261,7 +261,6 @@ implements RestrictedAccess, Threadable, Searchable { } function isAssigned($to=null) { - if (!$this->isOpen()) return false; @@ -308,8 +307,6 @@ implements RestrictedAccess, Threadable, Searchable { // check department access first if (!$staff->canAccessDept($this->getDept()) - // no restrictions - && !$staff->isAccessLimited() // check assignment && !$this->isAssigned($staff) // check referral @@ -1532,7 +1529,7 @@ implements RestrictedAccess, Threadable, Searchable { } // Account manager - if ($cfg->alertAcctManagerONNewMessage() + if ($cfg->alertAcctManagerONNewTicket() && ($org = $this->getOwner()->getOrganization()) && ($acct_manager = $org->getAccountManager()) ) { @@ -3173,7 +3170,7 @@ implements RestrictedAccess, Threadable, Searchable { function save($refetch=false) { if ($this->dirty) { $this->updated = SqlFunction::NOW(); - if (isset($this->dirty['status_id'])) + if (isset($this->dirty['status_id']) && PHP_SAPI !== 'cli') // Refetch the queue counts SavedQueue::clearCounts(); } @@ -3412,7 +3409,10 @@ implements RestrictedAccess, Threadable, Searchable { ->filter(array('number' => $number)); if ($email) - $query->filter(array('user__emails__address' => $email)); + $query->filter(Q::any(array( + 'user__emails__address' => $email, + 'thread__collaborators__user__emails__address' => $email + ))); if (!$ticket) { diff --git a/include/class.usersession.php b/include/class.usersession.php index bb113f5d3bf2f82fcff08c11c4fb933dcf48b011..cb900176c0e9d5ed1a00fc1b2ef62e9ade1c9a8c 100644 --- a/include/class.usersession.php +++ b/include/class.usersession.php @@ -123,6 +123,10 @@ class ClientSession extends EndUser { $this->session= new UserSession($user->getId()); } + function getSessionUser() { + return $this->user; + } + function isValid(){ global $_SESSION,$cfg; diff --git a/include/class.validator.php b/include/class.validator.php index 97f79bc8a0d77ef5900e9c46b13dbdf3fc05a221..2e1928f37882ea18c24e0b1500e3d29c9e0b8bd4 100644 --- a/include/class.validator.php +++ b/include/class.validator.php @@ -123,6 +123,21 @@ class Validator { if(!is_numeric($this->input[$k]) || (strlen($this->input[$k])!=5)) $this->errors[$k]=$field['error']; break; + case 'cs-domain': // Comma separated list of domains + if($values=explode(',', $this->input[$k])) + foreach($values as $v) + if(!preg_match_all( + '/^(https?:\/\/)?((\*\.|\w+\.)?[\w-]+(\.[a-zA-Z]+)?(:([0-9]+|\*))?)+$/', + ltrim($v))) + $this->errors[$k]=$field['error']; + break; + case 'ipaddr': + if($values=explode(',', $this->input[$k])){ + foreach($values as $v) + if(!preg_match_all('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', ltrim($v))) + $this->errors[$k]=$field['error']; + } + break; default://If param type is not set...or handle..error out... $this->errors[$k]=$field['error'].' '.__('(type not set)'); endswitch; @@ -299,5 +314,36 @@ class Validator { return (!$errors); } + + function check_acl($backend) { + global $cfg; + + $acl = $cfg->getACL(); + if (empty($acl)) + return true; + $ip = osTicket::get_client_ip(); + if (empty($ip)) + return false; + + $aclbk = $cfg->getACLBackend(); + switch($backend) { + case 'client': + if (in_array($aclbk, array(0,3))) + return true; + break; + case 'staff': + if (in_array($aclbk, array(0,2))) + return true; + break; + default: + return false; + break; + } + + if (!in_array($ip, $acl)) + return false; + + return true; + } } ?> diff --git a/include/client/header.inc.php b/include/client/header.inc.php index 75c2b3cbf69263c8659c9d8ffbc62aa6200ed234..e823bd01398c79b510bf8358b535b7b0ade8d3a7 100644 --- a/include/client/header.inc.php +++ b/include/client/header.inc.php @@ -6,7 +6,8 @@ $signin_url = ROOT_PATH . "login.php" $signout_url = ROOT_PATH . "logout.php?auth=".$ost->getLinkToken(); header("Content-Type: text/html; charset=UTF-8"); -header("X-Frame-Options: SAMEORIGIN"); +header("Content-Security-Policy: frame-ancestors '".$cfg->getAllowIframes()."';"); + if (($lang = Internationalization::getCurrentLanguage())) { $langs = array_unique(array($lang, $cfg->getPrimaryLanguage())); $langs = Internationalization::rfc1766($langs); diff --git a/include/client/knowledgebase.inc.php b/include/client/knowledgebase.inc.php index ac4a82f6941f7263b97bef48974865457fbf9fe1..c3895e1629bec11670cd594f1bc40b913633a073 100644 --- a/include/client/knowledgebase.inc.php +++ b/include/client/knowledgebase.inc.php @@ -18,12 +18,14 @@ if($_REQUEST['q'] || $_REQUEST['cid'] || $_REQUEST['topicId']) { //Search $faqs->filter(array('topics__topic_id'=>$_REQUEST['topicId'])); if ($_REQUEST['q']) - $faqs->filter(Q::ANY(array( - 'question__contains'=>$_REQUEST['q'], - 'answer__contains'=>$_REQUEST['q'], - 'keywords__contains'=>$_REQUEST['q'], - 'category__name__contains'=>$_REQUEST['q'], - 'category__description__contains'=>$_REQUEST['q'], + $faqs->filter(Q::all(array( + Q::ANY(array( + 'question__contains'=>$_REQUEST['q'], + 'answer__contains'=>$_REQUEST['q'], + 'keywords__contains'=>$_REQUEST['q'], + 'category__name__contains'=>$_REQUEST['q'], + 'category__description__contains'=>$_REQUEST['q'], + )) ))); include CLIENTINC_DIR . 'kb-search.inc.php'; diff --git a/include/client/templates/thread-export.tmpl.php b/include/client/templates/thread-export.tmpl.php index 5914a88da94a48a5da28c31dd1685a4e2f438ae4..2ed2288d85d1344781ad195798946b1234277f29 100644 --- a/include/client/templates/thread-export.tmpl.php +++ b/include/client/templates/thread-export.tmpl.php @@ -12,7 +12,7 @@ AttachmentFile::objects()->filter(array( ))->all(); $entries = $thread->getEntries(); -$entries->filter(array('type__in' => array_keys($entryTypes))); +$entries->filter(array('type__in' => array_keys($entryTypes)))->order_by("{$order}id");; ?> <style type="text/css"> div {font-family: sans-serif;} diff --git a/include/client/view.inc.php b/include/client/view.inc.php index 59c3baff9d4e559422c6ebf5ab494fbd378d65af..9d5ebc758ad35fc0f97d864114b3b7642faaedbe 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -99,7 +99,7 @@ if ($thisclient && $thisclient->isGuest() <td colspan="2"> <!-- Custom Data --> <?php -$sections = array(); +$sections = $forms = array(); foreach (DynamicFormEntry::forTicket($ticket->getId()) as $i=>$form) { // Skip core fields shown earlier in the ticket view $answers = $form->getAnswers()->exclude(Q::any(array( @@ -112,11 +112,13 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $i=>$form) { if ($v = $a->display()) $sections[$i][$j] = array($v, $a); } + // Set form titles + $forms[$i] = $form->getTitle(); } foreach ($sections as $i=>$answers) { ?> <table class="custom-data" cellspacing="0" cellpadding="4" width="100%" border="0"> - <tr><td colspan="2" class="headline flush-left"><?php echo $form->getTitle(); ?></th></tr> + <tr><td colspan="2" class="headline flush-left"><?php echo $forms[$i]; ?></th></tr> <?php foreach ($answers as $A) { list($v, $a) = $A; ?> <tr> diff --git a/include/i18n/en_US/help/tips/settings.system.yaml b/include/i18n/en_US/help/tips/settings.system.yaml index 1c2e3786580be78a6a3d5e69a7b2385b1b5afbbe..95e5117fd3c796047bc789260a0a2f3d3737c403 100644 --- a/include/i18n/en_US/help/tips/settings.system.yaml +++ b/include/i18n/en_US/help/tips/settings.system.yaml @@ -92,6 +92,40 @@ collision_avoidance: <br><br> Enter <span class="doc-desc-opt">0</span> to disable the lockout feature. +allow_iframes: + title: Allow iFrames + content: > + Enter comma separated list of domains for the system to be framed + in. If left empty, the system will default to 'self'. This accepts + domain wildcards, HTTP/HTTPS URL scheme, and port numbers. + <br><br> + <b>Example:</b> + <br> + https://domain.tld, sub.domain.tld:443, http://*.domain.tld + links: + - title: Syntax Information (host-source) + href: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors#Sources" + +acl: + title: ACL (Access Control List) + content: > + Enter a comma separated list of IP addresses to allow access to the system. + There are four options to choose which panel(s) to apply the ACL to. + <table border="1" cellpadding="2px" cellspacing="0" style="margin-top:7px" + ><tbody style="vertical-align:top;"> + <tr><th>Apply To</th> + <th>Description</th></tr> + <tr><td>Disabled</td> + <td>Disables ACL altogether.</td></tr> + <tr><td>All</td> + <td>Applies ACL to all Panels. (ie. Client Portal, Staff Panel, + Admin Panel)</td></tr> + <tr><td>Client Portal</td> + <td>Applies ACL to only Client Portal.</td></tr> + <tr><td>Staff Panel</td> + <td>Applies ACL to only Staff Panel and Admin Panel.</td></tr> + </tbody></table> + # Date and time options date_time_options: title: Date & Time Options diff --git a/include/staff/category.inc.php b/include/staff/category.inc.php index 20392a06739ebc02c5fa1cda008cf99a822430c1..97ab7e21d4866f864afad94a54c7bbc88f4bb8b2 100644 --- a/include/staff/category.inc.php +++ b/include/staff/category.inc.php @@ -124,6 +124,14 @@ if (count($langs) > 1) { ?> <?php } ?> </select> + <script> + $('select[name=pid]').on('change', function() { + var val = this.value; + $('select[name=pid]').each(function() { + $(this).val(val); + }); + }); + </script> </div> <div style="padding-bottom:8px;"> <b><?php echo __('Category Name');?></b>: diff --git a/include/staff/faq.inc.php b/include/staff/faq.inc.php index 830dc4645516ae229816e3a22c455369c3b02e95..80bf2241552ee6b9ae43ff931e63184f869bcd58 100644 --- a/include/staff/faq.inc.php +++ b/include/staff/faq.inc.php @@ -4,7 +4,7 @@ if (!defined('OSTSCPINC') || !$thisstaff die('Access Denied'); $info = $qs = array(); -if($faq){ +if($faq && $faq->getId()){ $title=__('Update FAQ').': '.$faq->getQuestion(); $action='update'; $submit_text=__('Save Changes'); diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php index 218da7c6b47c83da2e40c0f2e091fefa93ee63fd..d04d9f53f2c476eecdc983a58d761bcfb42ff65a 100644 --- a/include/staff/header.inc.php +++ b/include/staff/header.inc.php @@ -1,6 +1,6 @@ <?php header("Content-Type: text/html; charset=UTF-8"); -header("X-Frame-Options: SAMEORIGIN"); +header("Content-Security-Policy: frame-ancestors ".$cfg->getAllowIframes().";"); $title = ($ost && ($title=$ost->getPageTitle())) ? $title : ('osTicket :: '.__('Staff Control Panel')); diff --git a/include/staff/login.header.php b/include/staff/login.header.php index 2a4fcdbb6d234d0ffa434b838afcc065ec808e38..2f24f3a17a8ab4f4eb1c3249922d815295cebb8b 100644 --- a/include/staff/login.header.php +++ b/include/staff/login.header.php @@ -1,6 +1,6 @@ <?php defined('OSTSCPINC') or die('Invalid path'); -header("X-Frame-Options: SAMEORIGIN"); +header("Content-Security-Policy: frame-ancestors ".$cfg->getAllowIframes().";"); ?> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> diff --git a/include/staff/orgs.inc.php b/include/staff/orgs.inc.php index feb53acb399b5e934cd49bf9c1eedb5420f3e856..b8236d545e6b1d6bc29f1b4855f43e84b2129508 100644 --- a/include/staff/orgs.inc.php +++ b/include/staff/orgs.inc.php @@ -18,7 +18,7 @@ if ($_REQUEST['query']) { $sortOptions = array( 'name' => 'name', - 'users' => 'users', + 'users' => 'user_count', 'create' => 'created', 'update' => 'updated' ); diff --git a/include/staff/settings-system.inc.php b/include/staff/settings-system.inc.php index 22502ed5e54dde3ae77d17c0c8aa15f4c7204b4b..5f4703b1eac209cf355faa17b8887432a9dfb00c 100644 --- a/include/staff/settings-system.inc.php +++ b/include/staff/settings-system.inc.php @@ -131,6 +131,33 @@ $gmtime = Misc::gmtime(); <i class="help-tip icon-question-sign" href="#enable_richtext"></i> </td> </tr> + <tr> + <td><?php echo __('Allow iFrames'); ?>:</td> + <td><input type="text" size="40" name="allow_iframes" value="<?php echo $config['allow_iframes']; ?>"> + <font class="error"> <?php echo $errors['allow_iframes']; ?></font> + <i class="help-tip icon-question-sign" href="#allow_iframes"></i> + </td> + </tr> + <tr> + <td><?php echo __('ACL'); ?>:</td> + <td><input type="text" size="40" name="acl" value="<?php echo $config['acl']; ?>" + placeholder="eg. 192.168.1.1, 192.168.2.2, 192.168.3.3"> + Apply To: + <select name="acl_backend"> + <?php foreach($cfg->getACLBackendOpts() as $k=>$v) { ?> + <option <?php if ($cfg->getACLBackend() == $k) echo 'selected="selected"'; ?> + value="<?php echo $k; ?>"> + <?php echo $v; ?> + </option> + <?php } ?> + </select> + <i class="help-tip icon-question-sign" href="#acl"></i> + <?php if ($errors['acl']) { ?> + <br> + <font class="error"> <?php echo $errors['acl']; ?></font> + <?php } ?> + </td> + </tr> <tr> <th colspan="2"> <em><b><?php echo __('Date and Time Options'); ?></b> diff --git a/include/staff/templates/queue-columns.tmpl.php b/include/staff/templates/queue-columns.tmpl.php index fd8e99529a4ac346ebdfa177b100a735ae5fa9a4..5db261a8e8c4467dafc46b460b2f59f3107fee67 100644 --- a/include/staff/templates/queue-columns.tmpl.php +++ b/include/staff/templates/queue-columns.tmpl.php @@ -29,7 +29,8 @@ if ($queue->parent) { ?> </tr> </tbody> <?php } -$hidden_cols = $queue->inheritColumns() || $queue->useStandardColumns(); +$hidden_cols = $queue->inheritColumns() || ($queue->useStandardColumns() && + $queue->parent_id); ?> <tbody class="if-not-inherited <?php if ($hidden_cols) echo 'hidden'; ?>"> <tr class="header"> diff --git a/include/staff/templates/queue-quickfilter.tmpl.php b/include/staff/templates/queue-quickfilter.tmpl.php index 6046f7ae21af25a8c491a2abd774c4612eba1c10..996a6fc38c5727c162d86901f151681c8469f091 100644 --- a/include/staff/templates/queue-quickfilter.tmpl.php +++ b/include/staff/templates/queue-quickfilter.tmpl.php @@ -37,16 +37,24 @@ $.pjax({ timeout: 2000, container: '#pjax-container'}); return false;"> - <ul <?php if (count($choices) > 20) echo 'style="height:500px;overflow-x:hidden;overflow-y:scroll;"'; ?>> - <?php foreach ($choices as $k=>$desc) { - $selected = isset($quick_filter) && $quick_filter == $k; - ?> - <li <?php - if ($selected) echo 'class="active"'; - ?>> - <a href="#" data-value="<?php echo Format::htmlchars($k); ?>"> - <?php echo Format::htmlchars($desc); ?></a> - </li> - <?php } ?> - </ul> +<ul <?php if ($choices && count($choices) > 20) echo 'style="height:500px;overflow-x:hidden;overflow-y:scroll;"'; ?>> + <?php if ($choices) { + foreach ($choices as $k=>$desc) { + $selected = isset($quick_filter) && $quick_filter == $k; + ?> + <li <?php + if ($selected) echo 'class="active"'; + ?>> + <a href="#" data-value="<?php echo Format::htmlchars($k); ?>"> + <?php echo Format::htmlchars($desc); ?></a> + </li> + <?php } + } else { ?> + <li> + <a href="#" data-value="0"> + <?php echo __('None'); ?></a> + </li> + <?php } + ?> +</ul> </div> diff --git a/include/staff/templates/queue-tickets.tmpl.php b/include/staff/templates/queue-tickets.tmpl.php index dba05dea8c7278d6e9dbeb01af4af0e589bba1e8..d6e6307c1d8c3a0ab4f5f9e820a397c7ead872e5 100644 --- a/include/staff/templates/queue-tickets.tmpl.php +++ b/include/staff/templates/queue-tickets.tmpl.php @@ -90,9 +90,9 @@ if (isset($tickets->extra['tables'])) { $criteria->values_flat('ticket_id')]); # Index hint should be used on the $criteria query only $tickets->clearOption(QuerySet::OPT_INDEX_HINT); - $tickets->distinct('ticket_id'); } +$tickets->distinct('ticket_id'); $count = $queue->getCount($thisstaff) ?: (PAGE_LIMIT*3); $pageNav->setTotal($count, true); $pageNav->setURL('tickets.php', $args); diff --git a/include/upgrader/prereq.inc.php b/include/upgrader/prereq.inc.php index 8d48052c1f5fbea432de48105fc5c26f6d7bee76..6925e35105610d11e414e45f27a39fd9bfc7f318 100644 --- a/include/upgrader/prereq.inc.php +++ b/include/upgrader/prereq.inc.php @@ -39,7 +39,7 @@ if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access D <div class="sidebar pull-right"> <h3><?php echo __('Upgrade Tips');?></h3> <p>1. <?php echo __('Remember to back up your osTicket database');?></p> - <p>2. <?php echo sprintf(__('Refer to %1$s Upgrade Guide %2$s for the latest tips'), '<a href="http://osticket.com/wiki/Upgrade_and_Migration" target="_blank">', '</a>');?></p> + <p>2. <?php echo sprintf(__('Refer to %1$s Upgrade Guide %2$s for the latest tips'), '<a href="https://docs.osticket.com/en/latest/Getting%20Started/Upgrade%20and%20Migration.html" target="_blank">', '</a>');?></p> <p>3. <?php echo __('If you experience any problems, you can always restore your files/database backup.');?></p> <p>4. <?php echo sprintf(__('We can help. Feel free to %1$s contact us %2$s for professional help.'), '<a href="http://osticket.com/support/" target="_blank">', '</a>');?></p> diff --git a/scp/faq.php b/scp/faq.php index 8b948c1421ff0b51e044410ce3f9d8ba0b296cb5..164de2393c182466412e3d8c0b8f805c3b3d5213 100644 --- a/scp/faq.php +++ b/scp/faq.php @@ -143,7 +143,7 @@ if ($_POST) { } $inc='faq-categories.inc.php'; //FAQs landing page. -if($faq) { +if($faq && $faq->getId()) { $inc='faq-view.inc.php'; if ($_REQUEST['a']=='edit' && $thisstaff->hasPerm(FAQ::PERM_MANAGE)) diff --git a/scp/js/scp.js b/scp/js/scp.js index e39c2aebec4f5a150e5e6ee5f56ec88560629d1d..734cdb4cb4a73cceec16b66ce44ada1c6b56f01f 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -1167,15 +1167,16 @@ $(document).on('change', 'select[data-quick-add]', function() { }); // Quick note interface -$(document).on('click.note', '.quicknote .action.edit-note', function() { +$(document).on('click.note', '.quicknote .action.edit-note', function(e) { + // Prevent Auto-Scroll to top of page + e.preventDefault(); var note = $(this).closest('.quicknote'), body = note.find('.body'), T = $('<textarea>').text(body.html()); if (note.closest('.dialog, .tip_box').length) T.addClass('no-bar small'); body.replaceWith(T); - $.redact(T); - $(T).redactor('focus.setStart'); + $.redact(T, { focusEnd: true }); note.find('.action.edit-note').hide(); note.find('.action.save-note').show(); note.find('.action.cancel-edit').show(); @@ -1248,8 +1249,7 @@ $(document).on('click', '#new-note', function() { note.replaceWith(T); $('<p>').addClass('submit').css('text-align', 'center') .append(button).appendTo(T.parent()); - $.redact(T); - $(T).redactor('focus.setStart'); + $.redact(T, { focusEnd: true }); return false; }); diff --git a/scp/orgs.php b/scp/orgs.php index 8a70dcf2b61562fdda3fb9807410af9d5ffe9987..7111540418373924a482e09fafb7f93ebf4ef0a1 100644 --- a/scp/orgs.php +++ b/scp/orgs.php @@ -118,7 +118,7 @@ if ($org) { } elseif ($_REQUEST['a'] == 'export' && ($query=$_SESSION[':O:tickets'])) { $filename = sprintf('%s-tickets-%s.csv', $org->getName(), strftime('%Y%m%d')); - if (!Export::saveTickets($query, $filename, 'csv')) + if (!Export::saveTickets($query, NULL, $filename, 'csv')) $errors['err'] = __('Unable to dump query results.') .' '.__('Internal error occurred'); } diff --git a/scp/staff.inc.php b/scp/staff.inc.php index 8acf73ceabfe35f2e2f054eb019230ebbd5821c0..da4c5a382bb57b2d6f84cdf5d21259f826ca81cd 100644 --- a/scp/staff.inc.php +++ b/scp/staff.inc.php @@ -21,6 +21,10 @@ require_once('../main.inc.php'); if(!defined('INCLUDE_DIR')) die('Fatal error... invalid setting.'); +// Enforce ACL (if applicable) +if (!Validator::check_acl('staff')) + die(__('Access Denied')); + /*Some more include defines specific to staff only */ define('STAFFINC_DIR',INCLUDE_DIR.'staff/'); define('SCP_DIR',str_replace('//','/',dirname(__FILE__).'/')); diff --git a/setup/inc/header.inc.php b/setup/inc/header.inc.php index 57ceade2e12bad159d811881d8f8eea2022c22f7..fcb69d3ffa5ea9b21d14323c10d11953cb1b36d2 100644 --- a/setup/inc/header.inc.php +++ b/setup/inc/header.inc.php @@ -1,4 +1,7 @@ -<?php header("X-Frame-Options: SAMEORIGIN"); ?> +<?php +if ($cfg) + header("Content-Security-Policy: frame-ancestors ".$cfg->getAllowIframes().";"); +?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html <?php