Merge branch '1.17.x' into develop
This commit is contained in:
60
WHATSNEW.md
60
WHATSNEW.md
@@ -30,66 +30,10 @@ osTicket v1.18
|
||||
* Database: Change Plugin Name to varchar(255) (aac546d0)
|
||||
* update: MAJOR_VERSION (3eac42e5)
|
||||
|
||||
osTicket v1.17.6
|
||||
osTicket v1.17.8
|
||||
================
|
||||
### Improvements
|
||||
* Fix PR #6283: DB column value of "local backend" is 'client' and not 'local' (7805cd94)
|
||||
* i18n: Tasks Department ID Missing (00bc6b1b)
|
||||
* issue: Checkbox Template Variable (09de9587)
|
||||
* Fixed mailfetch error message (cd4717a1)
|
||||
* issue: fix square characters being printed when printing tickets that's using languages like Thai (872a6492, 25844034)
|
||||
* issue: Position Styling.. Again (7f03a64b, b843fb15)
|
||||
* mysqli: Multi-Host Failure (0a8475fb)
|
||||
* issue: Task Last Respondent (5277c7ab)
|
||||
* i18n: Schedules getDays() (f2facda3)
|
||||
* issue: Referred Closed Tickets (a7b0711d)
|
||||
* php: Update Prerequisites (d331a44c)
|
||||
* i18n: Lang URL Param (fece6ff7)
|
||||
* mysql: Update Minimum Version (317ab967)
|
||||
* Fix ajax path for AddRemoteCollaborator (6df6cd98)
|
||||
* Fix staff mobile field class (f8455c82)
|
||||
* patches: Latest 05/28/2024 (2d65cb77, 36b4c94f, 854cf47c, d9fe3ada)
|
||||
* upgrade: Laminas-Mail (2.25.1) (c0a74162, 939a278c, f658268d)
|
||||
* hotfix: Fix getRawEmail() (c26d840c, 1eb71197)
|
||||
* issue: Linked Icon annotation (6c096cce)
|
||||
* fix(email): fix case sensitivity in "Action" header retrieval (07843598)
|
||||
* Upgrade mimeDecode.php from v1.48 to v1.5.6 2016-08-28 (cbd78ac8)
|
||||
* mailer: sendmail() Failure (14e2057b)
|
||||
* php: 8.3 Support (a3931f46, b38db372, fea5e1e0, 387a1c04, 136d372e)
|
||||
* issue: Ticket Relations (9426b2dd)
|
||||
* issue: Basic Search Selector (74b8bd0e)
|
||||
* mail: Reapply Memory Optimization (eaaa64d9, ab76ffe8)
|
||||
* php: iconv Recommendation (33ecc3a0)
|
||||
* Ignore VS Code Workspace Files (0da2e0e0)
|
||||
* issue: Email Remote Backend Name (b54dd584)
|
||||
* issue: Safari Response Content Disposition (64a5df68)
|
||||
* api: DueDate and Other Errors (14814ae2)
|
||||
* issue: Multiselect List Export (f3bf8553)
|
||||
* issue: Image Annotation (aeeb2850)
|
||||
* user: List Import (fec70c4a)
|
||||
* issue: Latest SQL Warnings (9bdfde5a)
|
||||
* fields: Variable Name Validation (0724d1ad)
|
||||
* issue: Transfer Empty Comments Var (ed87b257)
|
||||
* update: Composer symfony/process (59f25918)
|
||||
* issue: Client ACL Staff File Download (8255b2e7)
|
||||
* update: PHP Deprecations & Warnings (84c14ace)
|
||||
* issue: User Lookup Umlaut (504b0bfb)
|
||||
* issue: Department Field User Import (5c2b6a5d)
|
||||
* Fix force-https auto-enabling on settings page (3805bbfd)
|
||||
* queue: getTotal Incorrect Counts (4f137dc2)
|
||||
* issue: Referred Tickets Incorrect Queue Counts (b42cad6f)
|
||||
* php: 8.4 Support (a4c0f674, b4938b99, 5e5a9ff5, fb9a39ba)
|
||||
* issue: Canned Response Access (b930a68b)
|
||||
* issue: Excessive Fetching Errors (c546a167)
|
||||
* issue: Plain-text Base64 (216ded32)
|
||||
* i18n: Crowdin API v2 (0ec6670d)
|
||||
* issue: iFrame Logins (3a5da66b)
|
||||
* issue: mimeDecode .eml Attachments (7fc3d8c2)
|
||||
* patches: Latest 11/18/2024 (416b548b, 245e7554)
|
||||
* Revert part of 0784359 commit (ec76a203)
|
||||
|
||||
### Security
|
||||
* security: Latest Vulns 01/2025 (193f5fe0, ab6672fa)
|
||||
* security: Latest Patches 06/2026 (52c366f, 5afdf54, c54a6ac, 1e39bf1, feccb6a, 6eb6b98, 078516e, 98abb05, e52e010, fd96bba, 7bbd8ab, ba6217a, 580e1c8, b535782, 5963797, d590a97, eaebe01, b4cc092, d457c14, 5600f94, 5ff9795, 119cefe, b4ede88, 2a0c388, 6558b33)
|
||||
|
||||
osTicket v1.17.7
|
||||
================
|
||||
|
||||
@@ -67,8 +67,9 @@ if($thisclient && $thisclient->getId() && $thisclient->isValid()){
|
||||
}
|
||||
|
||||
/******* CSRF Protectin *************/
|
||||
// Enforce CSRF protection for POSTS
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST' && !$ost->checkCSRFToken()) {
|
||||
// Enforce CSRF protection for state-changing methods
|
||||
if (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'PATCH', 'DELETE'])
|
||||
&& !$ost->checkCSRFToken()) {
|
||||
Http::redirect('index.php');
|
||||
//just incase redirect fails
|
||||
die('Action denied (400)!');
|
||||
|
||||
7
file.php
7
file.php
@@ -45,7 +45,7 @@ if ($cfg->isAuthRequiredForFiles()
|
||||
) {
|
||||
|
||||
// Try and determine if an agent is viewing the page / file
|
||||
if (strpos($_SERVER['HTTP_REFERRER'], ROOT_PATH . 'scp/') !== false) {
|
||||
if (strpos($_SERVER['HTTP_REFERER'], ROOT_PATH . 'scp/') !== false) {
|
||||
$_SESSION['_staff']['auth']['dest'] = Http::refresh_url();
|
||||
Http::redirect(ROOT_PATH.'scp/login.php');
|
||||
} else {
|
||||
@@ -58,7 +58,7 @@ if ($cfg->isAuthRequiredForFiles()
|
||||
// and the user has access to the parent ticket!!
|
||||
if ($file->verifySignature($_GET['signature'], $_GET['expires'])) {
|
||||
try {
|
||||
if (($s = @$_GET['s']) && strpos($file->getType(), 'image/') === 0)
|
||||
if (($s = @$_GET['s']) && $file->isInlineSafeImage())
|
||||
return $file->display($s);
|
||||
|
||||
// Download the file..
|
||||
@@ -67,7 +67,8 @@ if ($file->verifySignature($_GET['signature'], $_GET['expires'])) {
|
||||
$file->download($filename, $disposition, @$_GET['expires']);
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
Http::response(500, 'Unable to find that file: '.$ex->getMessage());
|
||||
$ost->logError(__('File download error'), $ex->getMessage());
|
||||
Http::response(500, __('Unable to retrieve the requested file'));
|
||||
}
|
||||
}
|
||||
// else
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
if(!defined('INCLUDE_DIR')) die('!');
|
||||
|
||||
require_once(INCLUDE_DIR.'class.draft.php');
|
||||
require_once(INCLUDE_DIR.'class.file.php');
|
||||
|
||||
class DraftAjaxAPI extends AjaxController {
|
||||
|
||||
@@ -84,7 +85,7 @@ class DraftAjaxAPI extends AjaxController {
|
||||
|
||||
// Check file type to ensure image
|
||||
$type = $file[0]['type'];
|
||||
if (strpos($file[0]['type'], 'image/') !== 0)
|
||||
if (!AttachmentFile::isInlineSafeImageType($file[0]['type']))
|
||||
return Http::response(403,
|
||||
JsonDataEncoder::encode(array(
|
||||
'error' => 'File type is not allowed',
|
||||
@@ -367,6 +368,9 @@ class DraftAjaxAPI extends AjaxController {
|
||||
|
||||
$files = array();
|
||||
foreach ($images as $f) {
|
||||
if (!$f->isInlineSafeImage())
|
||||
continue;
|
||||
|
||||
$url = $f->getDownloadUrl();
|
||||
$files[] = array(
|
||||
// Don't send special sizing for thread items 'cause they
|
||||
|
||||
@@ -13,6 +13,9 @@ class NoteAjaxAPI extends AjaxController {
|
||||
Http::response(403, "Login required");
|
||||
elseif (!($note = QuickNote::lookup($id)))
|
||||
Http::response(205, "Note not found");
|
||||
elseif (($note->staff_id !== $thisstaff->getId())
|
||||
&& !$thisstaff->isAdmin())
|
||||
Http::response(403, "Access Denied");
|
||||
|
||||
Http::response(200, $note->display());
|
||||
}
|
||||
@@ -24,6 +27,9 @@ class NoteAjaxAPI extends AjaxController {
|
||||
Http::response(403, "Login required");
|
||||
elseif (!($note = QuickNote::lookup($id)))
|
||||
Http::response(205, "Note not found");
|
||||
elseif (($note->staff_id !== $thisstaff->getId())
|
||||
&& !$thisstaff->isAdmin())
|
||||
Http::response(403, "Access Denied");
|
||||
elseif (!isset($_POST['note']) || !$_POST['note'])
|
||||
Http::response(422, "Send `note` parameter");
|
||||
|
||||
@@ -41,6 +47,9 @@ class NoteAjaxAPI extends AjaxController {
|
||||
Http::response(403, "Login required");
|
||||
elseif (!($note = QuickNote::lookup($id)))
|
||||
Http::response(205, "Note not found");
|
||||
elseif (($note->staff_id !== $thisstaff->getId())
|
||||
&& !$thisstaff->isAdmin())
|
||||
Http::response(403, "Access Denied");
|
||||
elseif (!$note->delete())
|
||||
Http::response(500, "Unable to remove note");
|
||||
|
||||
@@ -52,6 +61,9 @@ class NoteAjaxAPI extends AjaxController {
|
||||
|
||||
if (!$thisstaff)
|
||||
Http::response(403, "Login required");
|
||||
elseif (!in_array(substr($ext_id, 0, 1), ['O', 'U'])
|
||||
&& !$thisstaff->isAdmin())
|
||||
Http::response(403, "Access Denied");
|
||||
elseif (!isset($_POST['note']) || !$_POST['note'])
|
||||
Http::response(422, "Send `note` parameter");
|
||||
|
||||
|
||||
@@ -59,10 +59,17 @@ class TasksAjaxAPI extends AjaxController {
|
||||
}
|
||||
|
||||
function triggerThreadAction($task_id, $thread_id, $action) {
|
||||
global $thisstaff;
|
||||
|
||||
if (!($task = Task::lookup($task_id))
|
||||
|| !$task->checkStaffPerm($thisstaff))
|
||||
Http::response(404, 'No such task thread entry');
|
||||
|
||||
$thread = ThreadEntry::lookup($thread_id);
|
||||
if (!$thread)
|
||||
Http::response(404, 'No such task thread entry');
|
||||
if ($thread->getThread()->getObjectId() != $task_id)
|
||||
if ($thread->getThread()->getObjectType() != 'A'
|
||||
|| $thread->getThread()->getObjectId() != $task_id)
|
||||
Http::response(404, 'No such task thread entry');
|
||||
|
||||
$valid = false;
|
||||
|
||||
@@ -23,7 +23,7 @@ include_once INCLUDE_DIR . 'class.thread_actions.php';
|
||||
|
||||
class TicketsAjaxAPI extends AjaxController {
|
||||
|
||||
function lookup() {
|
||||
function lookup($wildcard=false) {
|
||||
global $thisstaff;
|
||||
|
||||
$limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
|
||||
@@ -53,7 +53,7 @@ class TicketsAjaxAPI extends AjaxController {
|
||||
return $this->encode(array());
|
||||
|
||||
global $ost;
|
||||
$hits = $ost->searcher->find($q, $hits, false);
|
||||
$hits = $ost->searcher->find($q, $hits, false, ['boolean' => $wildcard]);
|
||||
|
||||
$T = array();
|
||||
if (preg_match('/\d{2,}[^*]/', $q, $T)) {
|
||||
@@ -62,10 +62,10 @@ class TicketsAjaxAPI extends AjaxController {
|
||||
elseif (preg_match('/(.*@.{2,})|(.{2,}@.*)/', $q, $T)) {
|
||||
$hits = $this->lookupByEmail($limit, $visibility, $hits);
|
||||
}
|
||||
elseif (!count($hits) && preg_match('`\w$`u', $q)) {
|
||||
elseif (!$wildcard && !count($hits) && preg_match('`^[a-zA-Z0-9]+$`u', $q)) {
|
||||
// Do wild-card fulltext search
|
||||
$_REQUEST['q'] = $q.'*';
|
||||
return $this->lookup();
|
||||
return $this->lookup(true);
|
||||
}
|
||||
|
||||
// TODO: Why not aggregate based on relationship?
|
||||
@@ -777,6 +777,8 @@ class TicketsAjaxAPI extends AjaxController {
|
||||
|
||||
if (!($ticket=Ticket::lookup($tid)))
|
||||
Http::response(404, __('No such ticket'));
|
||||
elseif (!$ticket->checkStaffPerm($thisstaff))
|
||||
Http::response(403, __('Permission denied'));
|
||||
elseif (!($field=$ticket->getField($fid)))
|
||||
Http::response(404, __('No such field'));
|
||||
|
||||
@@ -1702,10 +1704,17 @@ class TicketsAjaxAPI extends AjaxController {
|
||||
}
|
||||
|
||||
function triggerThreadAction($ticket_id, $thread_id, $action) {
|
||||
global $thisstaff;
|
||||
|
||||
if (!($ticket = Ticket::lookup($ticket_id))
|
||||
|| !$ticket->checkStaffPerm($thisstaff))
|
||||
Http::response(404, 'No such ticket thread entry');
|
||||
|
||||
$thread = ThreadEntry::lookup($thread_id);
|
||||
if (!$thread)
|
||||
Http::response(404, 'No such ticket thread entry');
|
||||
if ($thread->getThread()->getObjectId() != $ticket_id)
|
||||
if ($thread->getThread()->getObjectType() != 'T'
|
||||
|| $thread->getThread()->getObjectId() != $ticket_id)
|
||||
Http::response(404, 'No such ticket thread entry');
|
||||
|
||||
$valid = false;
|
||||
|
||||
@@ -121,7 +121,12 @@ class UsersAjaxAPI extends AjaxController {
|
||||
|
||||
if(!$thisstaff)
|
||||
Http::response(403, 'Login Required');
|
||||
elseif(!($user = User::lookup($id)))
|
||||
|
||||
if (!$thisstaff->hasPerm(User::PERM_DIRECTORY)
|
||||
&& !$thisstaff->hasPerm(User::PERM_EDIT))
|
||||
Http::response(403, 'Permission Denied');
|
||||
|
||||
if(!($user = User::lookup($id)))
|
||||
Http::response(404, 'Unknown user');
|
||||
|
||||
$info = array(
|
||||
@@ -275,6 +280,14 @@ class UsersAjaxAPI extends AjaxController {
|
||||
}
|
||||
|
||||
function getUser($id=false) {
|
||||
global $thisstaff;
|
||||
|
||||
if (!$thisstaff)
|
||||
Http::response(403, 'Login Required');
|
||||
|
||||
if (!$thisstaff->hasPerm(User::PERM_DIRECTORY)
|
||||
&& !$thisstaff->hasPerm(User::PERM_EDIT))
|
||||
Http::response(403, 'Permission Denied');
|
||||
|
||||
if(($user=User::lookup(($id) ? $id : $_REQUEST['id'])))
|
||||
Http::response(201, $user->to_json(), 'application/json');
|
||||
@@ -422,6 +435,8 @@ class UsersAjaxAPI extends AjaxController {
|
||||
Http::response(403, 'Login Required');
|
||||
elseif (!($user = User::lookup($id)))
|
||||
Http::response(404, 'Unknown user');
|
||||
elseif (!$thisstaff->hasPerm(User::PERM_EDIT))
|
||||
Http::response(403, 'Access Denied');
|
||||
|
||||
$info = array();
|
||||
$info['title'] = sprintf(__('Organization for %s'),
|
||||
@@ -434,6 +449,9 @@ class UsersAjaxAPI extends AjaxController {
|
||||
if (!($org = Organization::lookup($_POST['orgid'])))
|
||||
$info['error'] = __('Unknown organization selected');
|
||||
} else { //Creating new org.
|
||||
if (!$thisstaff->hasPerm(Organization::PERM_CREATE))
|
||||
Http::response(403, 'Permission Denied');
|
||||
|
||||
$form = OrganizationForm::getDefaultForm()->getForm($_POST);
|
||||
if (!($org = Organization::fromForm($form)))
|
||||
$info['error'] = __('Unable to create organization.')
|
||||
|
||||
@@ -146,7 +146,7 @@ class API {
|
||||
$sql='INSERT INTO '.API_KEY_TABLE.' SET '.$sql
|
||||
.',created=NOW() '
|
||||
.',ipaddr='.db_input($vars['ipaddr'])
|
||||
.',apikey='.db_input(strtoupper(md5(time().$vars['ipaddr'].md5(Misc::randCode(16)))));
|
||||
.',apikey='.db_input(strtoupper(Misc::randCode(48, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')));
|
||||
|
||||
if(db_query($sql) && ($id=db_insert_id()))
|
||||
return $id;
|
||||
@@ -353,7 +353,7 @@ class ApiController extends Controller {
|
||||
// TODO: Only include API Key when in debug mode to avoid
|
||||
// potentialialy leaking a valid key in system logs
|
||||
if (($key=$this->getApiKey()))
|
||||
$msg .= "\n[$key]\n";
|
||||
$msg .= sprintf("\n[%s]\n", Format::shroud($key, 8, 16));
|
||||
|
||||
$title = sprintf('%s (%s)', $title ?: __('API Error'), $code);
|
||||
$this->logError($title, $msg, $logOnly);
|
||||
|
||||
@@ -1255,7 +1255,7 @@ class PasswordResetTokenBackend extends StaffAuthenticationBackend {
|
||||
|| $id != $staff->getId())
|
||||
$errors['msg'] = __('Invalid reset token');
|
||||
elseif (!($ts = $_config->lastModified($_POST['token']))
|
||||
&& ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
|
||||
|| ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
|
||||
$errors['msg'] = __('Invalid reset token');
|
||||
elseif (!$staff->forcePasswdRest())
|
||||
$errors['msg'] = __('Unable to reset password');
|
||||
@@ -1482,7 +1482,7 @@ class ClientPasswordResetTokenBackend extends UserAuthenticationBackend {
|
||||
|| $id != 'c'.$client->getId())
|
||||
$errors['msg'] = __('Invalid reset token');
|
||||
elseif (!($ts = $_config->lastModified($_POST['token']))
|
||||
&& ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
|
||||
|| ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
|
||||
$errors['msg'] = __('Invalid reset token');
|
||||
elseif (!$acct->forcePasswdReset())
|
||||
$errors['msg'] = __('Unable to reset password');
|
||||
|
||||
@@ -29,9 +29,11 @@ class Dispatcher {
|
||||
function resolve($url, $args=null) {
|
||||
if ($this->file) { $this->lazy_load(); }
|
||||
# Support HTTP method emulation with the _method GET argument
|
||||
if (isset($_GET['_method'])) {
|
||||
$_SERVER['REQUEST_METHOD'] = strtoupper($_GET['_method']);
|
||||
unset($_GET['_method']);
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST'
|
||||
&& isset($_POST['_method'])
|
||||
&& in_array(strtoupper($_POST['_method']), array('PUT', 'PATCH', 'DELETE'))) {
|
||||
$_SERVER['REQUEST_METHOD'] = strtoupper($_POST['_method']);
|
||||
unset($_POST['_method']);
|
||||
}
|
||||
// Decode URL for accurate matching
|
||||
$url = urldecode($url);
|
||||
|
||||
@@ -175,6 +175,10 @@ class AttachmentFile extends VerySimpleModel
|
||||
}
|
||||
|
||||
function display($scale=false, $ttl=86400) {
|
||||
// Force download if not safe viewable image
|
||||
if (!$this->isInlineSafeImage())
|
||||
return $this->download(false, 'attachment');
|
||||
|
||||
$this->makeCacheable($ttl);
|
||||
|
||||
if ($scale && extension_loaded('gd')
|
||||
@@ -214,6 +218,12 @@ class AttachmentFile extends VerySimpleModel
|
||||
'type' => $options['type']))))
|
||||
$options['id'] = $a->getId();
|
||||
|
||||
// Force attachment disposition if not inline-safe image
|
||||
if (isset($options['disposition'])
|
||||
&& strcasecmp($options['disposition'], 'inline') == 0
|
||||
&& !$this->isInlineSafeImage())
|
||||
$options['disposition'] = 'attachment';
|
||||
|
||||
return static::generateDownloadUrl($this->getId(),
|
||||
strtolower($this->getKey()), $this->getSignature(),
|
||||
$options);
|
||||
@@ -267,7 +277,8 @@ class AttachmentFile extends VerySimpleModel
|
||||
|
||||
$check = static::_genUrlSignature($this->getId(), $this->getKey(),
|
||||
$this->getSignature(), $expires);
|
||||
return $signature == $check;
|
||||
|
||||
return hash_equals($check, $signature);
|
||||
}
|
||||
|
||||
static function _genUrlSignature($id, $key, $signature, $expires) {
|
||||
@@ -287,7 +298,7 @@ class AttachmentFile extends VerySimpleModel
|
||||
$inline = ($thisstaff ? ($thisstaff->getImageAttachmentView() === 'inline') : false);
|
||||
$disposition = ((($disposition && strcasecmp($disposition, 'inline') == 0)
|
||||
|| $inline)
|
||||
&& strpos($this->getType(), 'image/') !== false)
|
||||
&& $this->isInlineSafeImage())
|
||||
? 'inline' : 'attachment';
|
||||
$ttl = ($expires) ? $expires - Misc::gmtime() : false;
|
||||
$bk = $this->open();
|
||||
@@ -721,6 +732,29 @@ class AttachmentFile extends VerySimpleModel
|
||||
->filter(array('ft' => 'B'))
|
||||
->order_by('created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a MIME type is safe to render inline as a passive image.
|
||||
*
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
static function isInlineSafeImageType($type) {
|
||||
$type = strtolower(trim((string) $type));
|
||||
|
||||
return strpos($type, 'image/') === 0
|
||||
&& $type !== 'image/svg+xml'
|
||||
&& $type !== 'image/svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this file is safe to render inline as a passive image.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function isInlineSafeImage() {
|
||||
return static::isInlineSafeImageType($this->getType());
|
||||
}
|
||||
}
|
||||
|
||||
class FileStorageBackend {
|
||||
|
||||
@@ -339,11 +339,10 @@ class Format {
|
||||
':<html[^>]+:i', # drop html attributes
|
||||
':<(a|span) (name|style)="(mso-bookmark\:)?_MailEndCompose">(.+)?<\/(a|span)>:', # Drop _MailEndCompose
|
||||
':<div dir=(3D)?"ltr">(.*?)<\/div>(.*):is', # drop Gmail "ltr" attributes
|
||||
':data-cid="[^"]*":', # drop image cid attributes
|
||||
'(position: ?(-webkit-)?(static|relative|fixed|absolute|sticky|initial|inherit);?)', # Position styling
|
||||
':[\x{2002}-\x{200B}]+:u', # unicode spaces
|
||||
),
|
||||
array('', '', '', '', '<html', '$4', '$2 $3', '', '', ' '),
|
||||
array('', '', '', '', '<html', '$4', '$2 $3', '', ' '),
|
||||
$html);
|
||||
|
||||
// HtmLawed specific config only
|
||||
@@ -353,7 +352,7 @@ class Format {
|
||||
'comment' => 1, //Remove html comments (OUTLOOK LOVE THEM)
|
||||
'tidy' => -1,
|
||||
'elements' => '*-form-input-button',
|
||||
'deny_attribute' => 'id, formaction, action, srcset, on*',
|
||||
'deny_attribute' => 'id, formaction, action, srcset, data*, on*',
|
||||
'schemes' => 'href: aim, feed, file, ftp, gopher, http, https, irc, mailto, news, nntp, sftp, ssh, telnet; *:file, http, https; src: cid, http, https, data',
|
||||
'hook_tag' => function($e, $a=0) { return Format::__html_cleanup($e, $a); },
|
||||
);
|
||||
@@ -605,6 +604,9 @@ class Format {
|
||||
if (!($file = AttachmentFile::lookup($match[2])))
|
||||
return $match[0];
|
||||
|
||||
if (!$file->isInlineSafeImage())
|
||||
return $match[0];
|
||||
|
||||
return sprintf('"%s" data-cid="%s"',
|
||||
$file->getDownloadUrl($options), $match[2]);
|
||||
}, $html);
|
||||
|
||||
@@ -973,7 +973,7 @@ class CustomQueue extends VerySimpleModel {
|
||||
// Consider keyword searching
|
||||
if ($name === ':keywords') {
|
||||
global $ost;
|
||||
$qs = $ost->searcher->find($value, $qs, false);
|
||||
$qs = $ost->searcher->find($value, $qs, false, ['boolean' => false]);
|
||||
}
|
||||
else {
|
||||
$nullable = ($method === 'nset') ? false : null;
|
||||
|
||||
@@ -44,7 +44,7 @@ abstract class SearchBackend {
|
||||
);
|
||||
|
||||
abstract function update($model, $id, $content, $new=false, $attrs=array());
|
||||
abstract function find($query, QuerySet $criteria, $addRelevance=true);
|
||||
abstract function find($query, QuerySet $criteria, $addRelevance=true, $options=array());
|
||||
|
||||
static function register($backend=false) {
|
||||
$backend = $backend ?: get_called_class();
|
||||
@@ -79,9 +79,9 @@ class SearchInterface {
|
||||
$this->bootstrap();
|
||||
}
|
||||
|
||||
function find($query, QuerySet $criteria, $addRelevance=true) {
|
||||
function find($query, QuerySet $criteria, $addRelevance=true, $options=array()) {
|
||||
$query = Format::searchable($query);
|
||||
return $this->backend->find($query, $criteria, $addRelevance);
|
||||
return $this->backend->find($query, $criteria, $addRelevance, $options);
|
||||
}
|
||||
|
||||
function update($model, $id, $content, $new=false, $attrs=array()) {
|
||||
@@ -327,7 +327,7 @@ class MysqlSearchBackend extends SearchBackend {
|
||||
return implode('', $results);
|
||||
}
|
||||
|
||||
function find($query, QuerySet $criteria, $addRelevance=true) {
|
||||
function find($query, QuerySet $criteria, $addRelevance=true, $options=array()) {
|
||||
global $thisstaff;
|
||||
|
||||
// MySQL usually doesn't handle words shorter than three letters
|
||||
@@ -339,6 +339,10 @@ class MysqlSearchBackend extends SearchBackend {
|
||||
|
||||
$mode = ' IN NATURAL LANGUAGE MODE';
|
||||
|
||||
// Allow boolean full-text syntax to be disabled
|
||||
$allow_boolean = !array_key_exists('boolean', $options)
|
||||
|| $options['boolean'];
|
||||
|
||||
// According to the MySQL full text boolean mode, this grammar is
|
||||
// assumed:
|
||||
// see http://dev.mysql.com/doc/refman/5.6/en/fulltext-boolean.html
|
||||
@@ -359,7 +363,8 @@ class MysqlSearchBackend extends SearchBackend {
|
||||
// Require the use of at least one operator and conform to the
|
||||
// boolean mode grammar
|
||||
$T = array();
|
||||
if (preg_match('`(^|\s)["()<>~+-]`u', $query, $T)
|
||||
if ($allow_boolean
|
||||
&& preg_match('`(^|\s)["()<>~+-]`u', $query, $T)
|
||||
&& preg_match("`^{$BOOLEAN}$`u", $query, $T)
|
||||
) {
|
||||
// If using boolean operators, search in boolean mode. This regex
|
||||
|
||||
@@ -1672,7 +1672,7 @@ implements TemplateVariable {
|
||||
'format' => $vars['body']->getType(),
|
||||
'staff_id' => $vars['staffId'],
|
||||
'user_id' => $vars['userId'],
|
||||
'poster' => $poster,
|
||||
'poster' => Format::sanitize($poster),
|
||||
'source' => $vars['source'],
|
||||
'flags' => $vars['flags'] ?: 0,
|
||||
));
|
||||
@@ -1757,6 +1757,10 @@ implements TemplateVariable {
|
||||
$files[$i]['inline'] = true;
|
||||
}
|
||||
foreach ($entry->normalizeFileInfo($files) as $F) {
|
||||
if (!empty($F['inline'])
|
||||
&& (!isset($F['file']) || !$F['file']->isInlineSafeImage()))
|
||||
$F['inline'] = false;
|
||||
|
||||
// Deduplicate on the `key` attribute. The key is
|
||||
// necessary for the CID rewrite below
|
||||
$attached_files[$F['key']] = $F;
|
||||
@@ -1769,6 +1773,9 @@ implements TemplateVariable {
|
||||
// discarded, only the unique hash-code (key) will be available to
|
||||
// retrieve the image later
|
||||
foreach ($attached_files as $key => $a) {
|
||||
if (empty($a['inline']))
|
||||
continue;
|
||||
|
||||
if (isset($a['cid']) && $a['cid']) {
|
||||
$body = preg_replace('/src=("|\'|\b)(?:cid:)?'
|
||||
. preg_quote($a['cid'], '/').'\1/i',
|
||||
|
||||
@@ -4651,7 +4651,6 @@ implements RestrictedAccess, Threadable, Searchable {
|
||||
// post response - if any
|
||||
$response = null;
|
||||
if ($vars['response'] && $role->hasPerm(Ticket::PERM_REPLY)) {
|
||||
$vars['response'] = $ticket->replaceVars($vars['response']);
|
||||
// $vars['cannedatachments'] contains the attachments placed on
|
||||
// the response form.
|
||||
$response = $ticket->postReply($vars, $errors, ($alert &&
|
||||
|
||||
@@ -88,7 +88,7 @@ class UserSession {
|
||||
list($hash, $expire, $ip) = explode(':', $token);
|
||||
|
||||
// Make sure the session hash is valid
|
||||
if ((md5($expire . SESSION_SECRET . $this->userID) != $hash))
|
||||
if (!hash_equals(md5($expire . SESSION_SECRET . $this->userID), $hash))
|
||||
return false;
|
||||
|
||||
// is it expired??
|
||||
|
||||
@@ -71,6 +71,15 @@ class VariableReplacer {
|
||||
$tag = mb_strtolower($tag);
|
||||
$rv = null;
|
||||
|
||||
// Deny sensitive attributes outright
|
||||
$blacklist = [
|
||||
'passwd',
|
||||
'password',
|
||||
'authkey',
|
||||
];
|
||||
if (in_array($tag, $blacklist))
|
||||
return "";
|
||||
|
||||
if (!is_object($obj)) {
|
||||
if ($tag && is_array($obj) && array_key_exists($tag, $obj))
|
||||
$rv = $obj[$tag];
|
||||
|
||||
@@ -30,7 +30,7 @@ if ($cfg->isAvatarsEnabled() && $user)
|
||||
</span>
|
||||
</div>
|
||||
<?php
|
||||
echo sprintf(__('<b>%s</b> posted %s'), $name,
|
||||
echo sprintf(__('<b>%s</b> posted %s'), Format::htmlchars($name),
|
||||
sprintf('<time datetime="%s" title="%s">%s</time>',
|
||||
date(DateTime::W3C, Misc::db2gmtime($entry->created)),
|
||||
Format::daydatetime($entry->created),
|
||||
@@ -38,7 +38,7 @@ if ($cfg->isAvatarsEnabled() && $user)
|
||||
)
|
||||
); ?>
|
||||
<span style="max-width:500px" class="faded title truncate"><?php
|
||||
echo $entry->title; ?>
|
||||
echo Format::htmlchars($entry->title); ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="thread-body" id="thread-id-<?php echo $entry->getId(); ?>">
|
||||
|
||||
@@ -42,9 +42,9 @@ $agentmasking = $cfg->hideStaffName();
|
||||
<td style=" border-top: 1px dashed #999;">
|
||||
<div style="background-color:#f7f7f7; padding:10px 20px;">
|
||||
<p style="font-family: sans-serif; padding:0; margin:0; color:<?php echo $color; ?>;">
|
||||
<strong><?php echo $name; ?></strong>
|
||||
<strong><?php echo Format::htmlchars($name); ?></strong>
|
||||
<span style="color:#888; font-size:12px; padding-left: 20px;"><?php
|
||||
echo $entry->title;
|
||||
echo Format::htmlchars($entry->title);
|
||||
?>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@@ -101,7 +101,7 @@ if ($settings['keywords']) {
|
||||
$tickets->filter(array('number__startswith'=>$q));
|
||||
} elseif (strlen($q) > 2) { //Deep search!
|
||||
// Use the search engine to perform the search
|
||||
$tickets = $ost->searcher->find($q, $tickets);
|
||||
$tickets = $ost->searcher->find($q, $tickets, true, ['boolean' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,9 @@ else
|
||||
<td align="center">
|
||||
<input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>"
|
||||
<?php echo $sel?'checked="checked"':''; ?>> </td>
|
||||
<td> <a href="apikeys.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['apikey']); ?></a></td>
|
||||
<td> <a href="apikeys.php?id=<?php echo $row['id']; ?>">
|
||||
<?php echo Format::shroud(Format::htmlchars($row['apikey']), 12, 26); ?>
|
||||
</a></td>
|
||||
<td><?php echo $row['ipaddr']; ?></td>
|
||||
<td><?php echo $row['isactive']?__('Active'):'<b>'.__('Disabled').'</b>'; ?></td>
|
||||
<td> <?php echo Format::date($row['created']); ?></td>
|
||||
|
||||
@@ -84,7 +84,7 @@ if ($_POST && $thread) {
|
||||
$c->getUserId(),
|
||||
$c->isActive() ? 'selected="selected"' : '',
|
||||
$c->isActive() ? 'active' : 'disabled',
|
||||
$c->getName());
|
||||
Format::htmlchars($c->getName()));
|
||||
}
|
||||
$recipients = sprintf(__('(%d of %d)'),
|
||||
$thread->getNumActiveCollaborators(),
|
||||
@@ -92,9 +92,9 @@ if ($_POST && $thread) {
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$('#t<?php echo $thread->getId(); ?>-recipients').html('<?php echo $recipients; ?>');
|
||||
$('#t<?php echo $thread->getId(); ?>-recipients').html('<?php echo JsonDataEncoder::encode($recipients); ?>');
|
||||
$('#t<?php echo $thread->getId(); ?>-collaborators').html('<?php echo $thread->getNumCollaborators(); ?>');
|
||||
$('#collabselection').html('<?php echo $options; ?>');
|
||||
$('#collabselection').html('<?php echo JsonDataEncoder::encode($options); ?>');
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
<br/>
|
||||
|
||||
<div class="thread-body bleed">
|
||||
<?php echo $faq->getLocalAnswer(); ?>
|
||||
<?php echo Format::stripExternalImages($faq->getLocalAnswer(), true); ?>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ foreach ($entries as $entry) {
|
||||
<div class="header">
|
||||
<div class="thread-info">
|
||||
<div class="thread-name">
|
||||
<span><?php echo $name; ?></span> <span><?php echo Format::datetime($entry->created) ?></span>
|
||||
<span><?php echo Format::htmlchars($name); ?></span> <span><?php echo Format::datetime($entry->created); ?></span>
|
||||
<?php if ($entry->number) { ?>
|
||||
<span data-toggle="tooltip" title="<?php echo sprintf(__('Ticket #%s'), $entry->number); ?>" class="label label-bare">
|
||||
<i class="icon-code-fork"></i></span>
|
||||
|
||||
@@ -27,7 +27,7 @@ do {
|
||||
echo Format::htmlchars($E->title).' — '; ?></strong>
|
||||
<em><?php if (strpos($E->updated, '0000-') === false)
|
||||
echo sprintf(__('Edited on %s by %s'), Format::datetime($E->updated),
|
||||
($editor = $E->getEditor()) ? $editor->getName() : '');
|
||||
($editor = $E->getEditor()) ? Format::htmlchars($editor->getName()) : '');
|
||||
else
|
||||
echo __('Original'); ?></em>
|
||||
</a>
|
||||
|
||||
@@ -71,7 +71,7 @@ if ($user && $cfg->isAvatarsEnabled())
|
||||
</span>
|
||||
</div>
|
||||
<?php
|
||||
echo sprintf(__('<b>%s</b> posted %s'), $name,
|
||||
echo sprintf(__('<b>%s</b> posted %s'), Format::htmlchars($name),
|
||||
sprintf('<a name="entry-%d" href="#entry-%1$s"><time %s
|
||||
datetime="%s" data-toggle="tooltip" title="%s">%s</time></a>',
|
||||
$entry->id,
|
||||
@@ -82,7 +82,7 @@ if ($user && $cfg->isAvatarsEnabled())
|
||||
)
|
||||
); ?>
|
||||
<span style="max-width:400px" class="faded title truncate"><?php
|
||||
echo $entry->title; ?>
|
||||
echo Format::htmlchars($entry->title); ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="thread-body no-pjax">
|
||||
|
||||
@@ -11,11 +11,21 @@ if ($_SESSION[':form-data'] && !$_GET['tid'])
|
||||
|
||||
// Use thread entry to seed the ticket
|
||||
if (!$user && $_GET['tid'] && ($entry = ThreadEntry::lookup($_GET['tid']))) {
|
||||
if ($entry->getThread()->getObjectType() == 'T')
|
||||
if ($entry->getThread()->getObjectType() == 'T') {
|
||||
$oldTicketId = $entry->getThread()->getObjectId();
|
||||
if ($entry->getThread()->getObjectType() == 'A')
|
||||
|
||||
if (!($oldTicket = Ticket::lookup($oldTicketId))
|
||||
|| !$oldTicket->checkStaffPerm($thisstaff))
|
||||
Http::response(403, 'Access Denied');
|
||||
}
|
||||
if ($entry->getThread()->getObjectType() == 'A') {
|
||||
$oldTaskId = $entry->getThread()->getObjectId();
|
||||
|
||||
if (!($oldTask = Task::lookup($oldTaskId))
|
||||
|| !$oldTask->checkStaffPerm($thisstaff))
|
||||
Http::response(403, 'Access Denied');
|
||||
}
|
||||
|
||||
$_SESSION[':form-data']['message'] = Format::htmlchars($entry->getBody());
|
||||
$_SESSION[':form-data']['ticketId'] = $oldTicketId;
|
||||
$_SESSION[':form-data']['taskId'] = $oldTaskId;
|
||||
|
||||
6
open.php
6
open.php
@@ -18,6 +18,12 @@ define('SOURCE','Web'); //Ticket source.
|
||||
$ticket = null;
|
||||
$errors=array();
|
||||
if ($_POST) {
|
||||
// If login is required and there is no user or if guest user then
|
||||
// banish to the shadow realm
|
||||
if ($cfg->isClientLoginRequired()
|
||||
&& (!$thisclient || $thisclient->isGuest()))
|
||||
require_once 'secure.inc.php';
|
||||
|
||||
$vars = $_POST;
|
||||
$vars['deptId']=$vars['emailId']=0; //Just Making sure we don't accept crap...only topicId is expected.
|
||||
if ($thisclient) {
|
||||
|
||||
191
scp/js/bootstrap-tooltip.js
vendored
191
scp/js/bootstrap-tooltip.js
vendored
@@ -1,16 +1,146 @@
|
||||
/* ========================================================================
|
||||
* Bootstrap: tooltip.js v3.3.4
|
||||
* http://getbootstrap.com/javascript/#tooltip
|
||||
* Bootstrap: tooltip.js v3.4.1
|
||||
* https://getbootstrap.com/docs/3.4/javascript/#tooltip
|
||||
* Inspired by the original jQuery.tipsy by Jason Frame
|
||||
* ========================================================================
|
||||
* Copyright 2011-2015 Twitter, Inc.
|
||||
* Copyright 2011-2019 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* ======================================================================== */
|
||||
|
||||
|
||||
+function ($) {
|
||||
'use strict';
|
||||
|
||||
var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']
|
||||
|
||||
var uriAttrs = [
|
||||
'background',
|
||||
'cite',
|
||||
'href',
|
||||
'itemtype',
|
||||
'longdesc',
|
||||
'poster',
|
||||
'src',
|
||||
'xlink:href'
|
||||
]
|
||||
|
||||
var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
|
||||
|
||||
var DefaultWhitelist = {
|
||||
// Global attributes allowed on any supplied element below.
|
||||
'*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
|
||||
a: ['target', 'href', 'title', 'rel'],
|
||||
area: [],
|
||||
b: [],
|
||||
br: [],
|
||||
col: [],
|
||||
code: [],
|
||||
div: [],
|
||||
em: [],
|
||||
hr: [],
|
||||
h1: [],
|
||||
h2: [],
|
||||
h3: [],
|
||||
h4: [],
|
||||
h5: [],
|
||||
h6: [],
|
||||
i: [],
|
||||
img: ['src', 'alt', 'title', 'width', 'height'],
|
||||
li: [],
|
||||
ol: [],
|
||||
p: [],
|
||||
pre: [],
|
||||
s: [],
|
||||
small: [],
|
||||
span: [],
|
||||
sub: [],
|
||||
sup: [],
|
||||
strong: [],
|
||||
u: [],
|
||||
ul: []
|
||||
}
|
||||
|
||||
/**
|
||||
* A pattern that recognizes a commonly useful subset of URLs that are safe.
|
||||
*
|
||||
* Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
|
||||
*/
|
||||
var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi
|
||||
|
||||
/**
|
||||
* A pattern that matches safe data URLs. Only matches image, video and audio types.
|
||||
*
|
||||
* Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
|
||||
*/
|
||||
var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i
|
||||
|
||||
function allowedAttribute(attr, allowedAttributeList) {
|
||||
var attrName = attr.nodeName.toLowerCase()
|
||||
|
||||
if ($.inArray(attrName, allowedAttributeList) !== -1) {
|
||||
if ($.inArray(attrName, uriAttrs) !== -1) {
|
||||
return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var regExp = $(allowedAttributeList).filter(function (index, value) {
|
||||
return value instanceof RegExp
|
||||
})
|
||||
|
||||
// Check if a regular expression validates the attribute.
|
||||
for (var i = 0, l = regExp.length; i < l; i++) {
|
||||
if (attrName.match(regExp[i])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
|
||||
if (unsafeHtml.length === 0) {
|
||||
return unsafeHtml
|
||||
}
|
||||
|
||||
if (sanitizeFn && typeof sanitizeFn === 'function') {
|
||||
return sanitizeFn(unsafeHtml)
|
||||
}
|
||||
|
||||
// IE 8 and below don't support createHTMLDocument
|
||||
if (!document.implementation || !document.implementation.createHTMLDocument) {
|
||||
return unsafeHtml
|
||||
}
|
||||
|
||||
var createdDocument = document.implementation.createHTMLDocument('sanitization')
|
||||
createdDocument.body.innerHTML = unsafeHtml
|
||||
|
||||
var whitelistKeys = $.map(whiteList, function (el, i) { return i })
|
||||
var elements = $(createdDocument.body).find('*')
|
||||
|
||||
for (var i = 0, len = elements.length; i < len; i++) {
|
||||
var el = elements[i]
|
||||
var elName = el.nodeName.toLowerCase()
|
||||
|
||||
if ($.inArray(elName, whitelistKeys) === -1) {
|
||||
el.parentNode.removeChild(el)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var attributeList = $.map(el.attributes, function (el) { return el })
|
||||
var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || [])
|
||||
|
||||
for (var j = 0, len2 = attributeList.length; j < len2; j++) {
|
||||
if (!allowedAttribute(attributeList[j], whitelistedAttributes)) {
|
||||
el.removeAttribute(attributeList[j].nodeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createdDocument.body.innerHTML
|
||||
}
|
||||
|
||||
// TOOLTIP PUBLIC CLASS DEFINITION
|
||||
// ===============================
|
||||
|
||||
@@ -26,7 +156,7 @@
|
||||
this.init('tooltip', element, options)
|
||||
}
|
||||
|
||||
Tooltip.VERSION = '3.3.4'
|
||||
Tooltip.VERSION = '3.4.1'
|
||||
|
||||
Tooltip.TRANSITION_DURATION = 150
|
||||
|
||||
@@ -43,7 +173,10 @@
|
||||
viewport: {
|
||||
selector: 'body',
|
||||
padding: 0
|
||||
}
|
||||
},
|
||||
sanitize : true,
|
||||
sanitizeFn : null,
|
||||
whiteList : DefaultWhitelist
|
||||
}
|
||||
|
||||
Tooltip.prototype.init = function (type, element, options) {
|
||||
@@ -51,7 +184,7 @@
|
||||
this.type = type
|
||||
this.$element = $(element)
|
||||
this.options = this.getOptions(options)
|
||||
this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
|
||||
this.$viewport = this.options.viewport && $(document).find($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
|
||||
this.inState = { click: false, hover: false, focus: false }
|
||||
|
||||
if (this.$element[0] instanceof document.constructor && !this.options.selector) {
|
||||
@@ -84,7 +217,15 @@
|
||||
}
|
||||
|
||||
Tooltip.prototype.getOptions = function (options) {
|
||||
options = $.extend({}, this.getDefaults(), this.$element.data(), options)
|
||||
var dataAttributes = this.$element.data()
|
||||
|
||||
for (var dataAttr in dataAttributes) {
|
||||
if (dataAttributes.hasOwnProperty(dataAttr) && $.inArray(dataAttr, DISALLOWED_ATTRIBUTES) !== -1) {
|
||||
delete dataAttributes[dataAttr]
|
||||
}
|
||||
}
|
||||
|
||||
options = $.extend({}, this.getDefaults(), dataAttributes, options)
|
||||
|
||||
if (options.delay && typeof options.delay == 'number') {
|
||||
options.delay = {
|
||||
@@ -93,6 +234,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (options.sanitize) {
|
||||
options.template = sanitizeHtml(options.template, options.whiteList, options.sanitizeFn)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -204,7 +349,7 @@
|
||||
.addClass(placement)
|
||||
.data('bs.' + this.type, this)
|
||||
|
||||
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
|
||||
this.options.container ? $tip.appendTo($(document).find(this.options.container)) : $tip.insertAfter(this.$element)
|
||||
this.$element.trigger('inserted.bs.' + this.type)
|
||||
|
||||
var pos = this.getPosition()
|
||||
@@ -306,7 +451,16 @@
|
||||
var $tip = this.tip()
|
||||
var title = this.getTitle()
|
||||
|
||||
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
|
||||
if (this.options.html) {
|
||||
if (this.options.sanitize) {
|
||||
title = sanitizeHtml(title, this.options.whiteList, this.options.sanitizeFn)
|
||||
}
|
||||
|
||||
$tip.find('.tooltip-inner').html(title)
|
||||
} else {
|
||||
$tip.find('.tooltip-inner').text(title)
|
||||
}
|
||||
|
||||
$tip.removeClass('fade in top bottom left right')
|
||||
}
|
||||
|
||||
@@ -317,9 +471,11 @@
|
||||
|
||||
function complete() {
|
||||
if (that.hoverState != 'in') $tip.detach()
|
||||
that.$element
|
||||
.removeAttr('aria-describedby')
|
||||
.trigger('hidden.bs.' + that.type)
|
||||
if (that.$element) { // TODO: Check whether guarding this code with this `if` is really necessary.
|
||||
that.$element
|
||||
.removeAttr('aria-describedby')
|
||||
.trigger('hidden.bs.' + that.type)
|
||||
}
|
||||
callback && callback()
|
||||
}
|
||||
|
||||
@@ -362,7 +518,10 @@
|
||||
// width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093
|
||||
elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })
|
||||
}
|
||||
var elOffset = isBody ? { top: 0, left: 0 } : $element.offset()
|
||||
var isSvg = window.SVGElement && el instanceof window.SVGElement
|
||||
// Avoid using $.offset() on SVGs since it gives incorrect results in jQuery 3.
|
||||
// See https://github.com/twbs/bootstrap/issues/20280
|
||||
var elOffset = isBody ? { top: 0, left: 0 } : (isSvg ? null : $element.offset())
|
||||
var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
|
||||
var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
|
||||
|
||||
@@ -478,9 +637,13 @@
|
||||
that.$tip = null
|
||||
that.$arrow = null
|
||||
that.$viewport = null
|
||||
that.$element = null
|
||||
})
|
||||
}
|
||||
|
||||
Tooltip.prototype.sanitizeHtml = function (unsafeHtml) {
|
||||
return sanitizeHtml(unsafeHtml, this.options.whiteList, this.options.sanitizeFn)
|
||||
}
|
||||
|
||||
// TOOLTIP PLUGIN DEFINITION
|
||||
// =========================
|
||||
|
||||
@@ -39,6 +39,7 @@ if($_POST) {
|
||||
}
|
||||
switch ($_POST['do']) {
|
||||
case 'sendmail':
|
||||
$start = microtime(true);
|
||||
$userid = (string) $_POST['userid'];
|
||||
if (Validator::is_userid($userid)
|
||||
&& ($staff=Staff::lookup($userid))) {
|
||||
@@ -50,6 +51,15 @@ if($_POST) {
|
||||
}
|
||||
else
|
||||
$tpl = 'pwreset.sent.php';
|
||||
|
||||
$min = 1.4;
|
||||
$jitter = random_int(0, 250) / 1000;
|
||||
$target = $min + $jitter;
|
||||
|
||||
$elapsed = microtime(true) - $start;
|
||||
if ($elapsed < $target)
|
||||
usleep((int)(($target - $elapsed) * 1_000_000));
|
||||
|
||||
break;
|
||||
case 'newpasswd':
|
||||
// TODO: Compare passwords
|
||||
|
||||
@@ -104,8 +104,9 @@ define('SESSION_MAXLIFE', $thisstaff->getMaxIdleTime());
|
||||
$thisstaff->refreshSession();
|
||||
|
||||
/******* CSRF Protectin *************/
|
||||
// Enforce CSRF protection for POSTS
|
||||
if ($_POST && !$ost->checkCSRFToken()) {
|
||||
// Enforce CSRF protection for state-changing methods
|
||||
if (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'PATCH', 'DELETE'])
|
||||
&& !$ost->checkCSRFToken()) {
|
||||
Http::response(400, __('Valid CSRF Token Required'));
|
||||
exit;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user