diff --git a/WHATSNEW.md b/WHATSNEW.md index 9bbfbbb64682f294d5e07e842916fcbecbb20173..7bc3553b8031dd6bec76213c5daa8b1050114eea 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,19 +1,85 @@ +osTicket 1.11.0 +================== +## Major New Features +- Custom Columns/Custom Queues +- Inline Edit +- Ticket Referral +- CC/BCC +- Export Agent CSV +- Department Access CSV +- Archive Help Topics/Departments +- Nested Knowledgebase Categories + +### Enhancements +- Dashboard Statistics +- Fix Vimeo iFrames +- Fix randNumber() +- Section Break Hint +- List & Choice Searching (#3703, #3493, #2625) +- Adds osTicket Favicons (#4112) +- Fix Most Redactor Issues (#3849) +- Send Login Errors Still Sends (#4073) +- Private FAQs In Sidebar Search +- User Password Reset (#4030) +- Disabled & Private Help Topic (#3538) +- Helpdesk Status Help Tip +- Local Names In Validation Errors +- User Registration Form (#4043) +- Organization User List Pages Link (#4116) +- Ticket Edit Internal Note (#4028) +- Disable Canned Responses On New Ticket (#3971) +- Canned Response Margin +- Ticket Preview Custom Fields +- Help Topic SLA (#3979) +- Fix Agent Identity Masking (#2955, #3524) +- Force Keys For Choice Field Options (#4071) +- Check Missing Required Fields +- Task Action Button Styling +- Add Fullscreen To Embedded Videos +- Fix Serbian Flag Icon (#3952) +- Optimize Lock Table +- Fix Outdated Alerts Link (#3935) +- Fix Default Dept. Private Error (#3934) +- Mailto TLD Length (#4063) +- Remove Primary Contacts (#3903) +- Fix Reset Button(s) (#3670) +- Newsletter Link +- Offline Page Images (#3869) +- User Login Page Translation (#3860) +- Translate Special Characters (#3842) +- Custom Form Deletion (#3542, #4059) +- Client Side Long FAQ Title (#3380) +- Client FAQ Last Updated Time (#3475) +- Email Banlist Sorting (#3452) +- Fix New Ticket Cancel Button (#2624, #2881) +- SQL Error Unknown column 'relevance' (#2655) +- Fixes issue with last_update ticket variable +- Ticket Notice Alert +- Fix CSRF fail + shake effect (#3928, #3546) +- Issue/ticket preview collabs +- Allowing translation of copyrights in footers +- User/Organization are not translated (#3650) +- Fix DatePicker on client side (#3625, #3817, #3804, 0fbc09a) +- Add Custom Forms to Ticket Filter Data +- Fix for LDAP/AD auth plugin (#4198, #3460, #3544, #3549) + + osTicket v1.10.1 ================ ### Enhancements -- Users: Support search by phone number -- i18n: Fix getPrimaryLanguage() on non-object (#3799) -- Add TimezoneField (#3786) -- Chunk long text body (#3757, 7b68c994) -- Spyc: convert hex strings to INTs under PHP 7 (#3621) -- forms: Proper Field Deletion -- Move orphaned tasks on department deletion to the default department (42e2c55a) -- List: Save List Item Abbreviation (8513f137) +* Users: Support search by phone number +* i18n: Fix getPrimaryLanguage() on non-object (#3799) +* Add TimezoneField (#3786) +* Chunk long text body (#3757, 7b68c994) +* Spyc: convert hex strings to INTs under PHP 7 (#3621) +* forms: Proper Field Deletion +* Move orphaned tasks on department deletion to the default department (42e2c55a) +* List: Save List Item Abbreviation (8513f137) ### Performance and Security -- XSS: Encode html entities of advanced search title (#3919) -- XSS: Encode html entities of cached form data (#3960, bcd58e8) -- ORM: Addresses an SQL injection vulnerability in ORM lookup function (#3959, 1eaa6910) +* XSS: Encode html entities of advanced search title (#3919) +* XSS: Encode html entities of cached form data (#3960, bcd58e8) +* ORM: Addresses an SQL injection vulnerability in ORM lookup function (#3959, 1eaa6910) osTicket v1.10 diff --git a/bootstrap.php b/bootstrap.php index 794613840f288b9f3dde80176777593f5c4243c2..4b64227a839c1e9cd864db6cc74a1930e9906f98 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -139,6 +139,7 @@ class Bootstrap { define('QUEUE_SORT_TABLE', $prefix.'queue_sort'); define('QUEUE_SORTING_TABLE', $prefix.'queue_sorts'); define('QUEUE_EXPORT_TABLE', $prefix.'queue_export'); + define('QUEUE_CONFIG_TABLE', $prefix.'queue_config'); define('API_KEY_TABLE',$prefix.'api_key'); define('TIMEZONE_TABLE',$prefix.'timezone'); diff --git a/file.php b/file.php index 33ffec5ff1cf038dbfb999b64ce8af5d1c1db3b7..994b77a0c2256a0218a27ffebb01b1c7d77a344b 100644 --- a/file.php +++ b/file.php @@ -26,21 +26,34 @@ if (!$_GET['key'] Http::response(404, __('Unknown or invalid file')); } -// Enforce security settings -if ($cfg->isAuthRequiredForFiles() && !$thisclient) { - if (!($U = StaffAuthenticationBackend::getUser())) { - // Try and determine if a staff is viewing this page - if (strpos($_SERVER['HTTP_REFERRER'], ROOT_PATH . 'scp/') !== false) { - $_SESSION['_staff']['auth']['dest'] = - '/' . ltrim($_SERVER['REQUEST_URI'], '/'); - Http::redirect(ROOT_PATH.'scp/login.php'); - } - else { - require 'secure.inc.php'; - } +// Get the object type the file is attached to +$type = ''; +if ($_GET['id'] + && ($a=$file->attachments->findFirst(array( + 'id' => $_GET['id'])))) + $type = $a->type; + +// Enforce security settings if enabled. +if ($cfg->isAuthRequiredForFiles() + // FAQ & Page files allowed without login. + && !in_array($type, ['P', 'F']) + // Check user login + && !$thisuser + // Check staff login + && !StaffAuthenticationBackend::getUser() + ) { + + // Try and determine if an agent is viewing the page / file + if (strpos($_SERVER['HTTP_REFERRER'], ROOT_PATH . 'scp/') !== false) { + $_SESSION['_staff']['auth']['dest'] = + '/' . ltrim($_SERVER['REQUEST_URI'], '/'); + Http::redirect(ROOT_PATH.'scp/login.php'); + } else { + require 'secure.inc.php'; } } + // Validate session access hash - we want to make sure the link is FRESH! // and the user has access to the parent ticket!! if ($file->verifySignature($_GET['signature'], $_GET['expires'])) { diff --git a/include/ajax.draft.php b/include/ajax.draft.php index e1bb78a0435b619f9bba99973b806543e6fd4a23..43e7f98b8651001ac5416b35c6f0563f84821f49 100644 --- a/include/ajax.draft.php +++ b/include/ajax.draft.php @@ -133,7 +133,8 @@ class DraftAjaxAPI extends AjaxController { 'content_id' => 'cid:'.$f->getKey(), // Return draft_id to connect the auto draft creation 'draft_id' => $draft->getId(), - 'filelink' => $f->getDownloadUrl(false, 'inline'), + 'filelink' => $f->getDownloadUrl( + ['type' => 'D', 'deposition' => 'inline']), )); } @@ -339,14 +340,14 @@ class DraftAjaxAPI extends AjaxController { && ($object = $thread->getObject()) && ($thisstaff->canAccess($object)) ) { - $union = ' UNION SELECT f.id, a.`type`, a.`name` FROM '.THREAD_TABLE.' t + $union = ' UNION SELECT f.id, a.id as aid, a.`type`, a.`name` FROM '.THREAD_TABLE.' t JOIN '.THREAD_ENTRY_TABLE.' th ON (th.thread_id = t.id) JOIN '.ATTACHMENT_TABLE.' a ON (a.object_id = th.id AND a.`type` = \'H\') JOIN '.FILE_TABLE.' f ON (a.file_id = f.id) WHERE a.`inline` = 1 AND t.id='.db_input($_GET['threadId']); } - $sql = 'SELECT distinct f.id, COALESCE(a.type, f.ft), a.`name` FROM '.FILE_TABLE + $sql = 'SELECT distinct f.id, a.id as aid, COALESCE(a.type, f.ft), a.`name` FROM '.FILE_TABLE .' f LEFT JOIN '.ATTACHMENT_TABLE.' a ON (a.file_id = f.id) WHERE ((a.`type` IN (\'C\', \'F\', \'T\', \'P\') AND a.`inline` = 1) OR f.ft = \'L\')' .' AND f.`type` LIKE \'image/%\''; @@ -354,9 +355,11 @@ class DraftAjaxAPI extends AjaxController { Http::response(500, 'Unable to lookup files'); $files = array(); - while (list($id, $type, $name) = db_fetch_row($res)) { - $f = AttachmentFile::lookup((int) $id); - $url = $f->getDownloadUrl(); + while (list($id, $aid, $type, $name) = db_fetch_row($res)) { + if (!($f = AttachmentFile::lookup((int) $id))) + continue; + + $url = $f->getDownloadUrl(['id' => $aid]); $files[] = array( // Don't send special sizing for thread items 'cause they // should be cached already by the client diff --git a/include/ajax.forms.php b/include/ajax.forms.php index 41506c872f076bcc66d8582e1ef38bb4a462e4c0..9ca601e33020d9c0f3ea1e669bbc4d405217df5e 100644 --- a/include/ajax.forms.php +++ b/include/ajax.forms.php @@ -15,6 +15,9 @@ class DynamicFormsAjaxAPI extends AjaxController { } function getFormsForHelpTopic($topic_id, $client=false) { + if (!$_SERVER['HTTP_REFERER']) + Http::response(403, 'Forbidden.'); + if (!($topic = Topic::lookup($topic_id))) Http::response(404, 'No such help topic'); diff --git a/include/ajax.search.php b/include/ajax.search.php index 80ebd621ca61503df97026c1f196d8c7e199ba87..d8ce8dbab1b4ef7443b5a3b664cdac63f3ce4425 100644 --- a/include/ajax.search.php +++ b/include/ajax.search.php @@ -28,8 +28,9 @@ class SearchAjaxAPI extends AjaxController { if (!$thisstaff) Http::response(403, 'Agent login required'); - $search = new SavedSearch(array( + $search = new AdhocSearch(array( 'root' => 'T', + 'staff_id' => $thisstaff->getId(), 'parent_id' => @$_GET['parent_id'] ?: 0, )); if ($search->parent_id) { @@ -39,7 +40,7 @@ class SearchAjaxAPI extends AjaxController { if (isset($_SESSION[$context]) && $key && $_SESSION[$context][$key]) $search->config = $_SESSION[$context][$key]; - $this->_tryAgain($search, $search->getForm()); + $this->_tryAgain($search); } function editSearch($id) { @@ -51,7 +52,7 @@ class SearchAjaxAPI extends AjaxController { elseif (!$search || !$search->checkAccess($thisstaff)) Http::response(404, 'No such saved search'); - $this->_tryAgain($search, $search->getForm()); + $this->_tryAgain($search); } function addField($name) { @@ -60,7 +61,9 @@ class SearchAjaxAPI extends AjaxController { if (!$thisstaff) Http::response(403, 'Agent login required'); - $search = new SavedSearch(array('root'=>'T')); + $search = new SavedSearch(array( + 'root'=>'T' + )); $searchable = $search->getSupportedMatches(); if (!($F = $searchable[$name])) Http::response(404, 'No such field: ', print_r($name, true)); @@ -82,7 +85,15 @@ class SearchAjaxAPI extends AjaxController { } function doSearch() { - $search = new SavedSearch(array('root' => 'T')); + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Agent login is required'); + + $search = new AdhocSearch(array( + 'root' => 'T', + 'staff_id' => $thisstaff->getId())); + $form = $search->getForm($_POST); if (false === $this->_setupSearch($search, $form)) { return; @@ -139,11 +150,28 @@ class SearchAjaxAPI extends AjaxController { -$size); } - function _tryAgain($search, $form, $errors=array(), $info=array()) { - $matches = $search->getSupportedMatches(); + function _tryAgain($search, $form=null, $errors=array(), $info=array()) { + if (!$form) + $form = $search->getForm(); include STAFFINC_DIR . 'templates/advanced-search.tmpl.php'; } + function createSearch() { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Agent login is required'); + + + $search = SavedSearch::create(array( + 'title' => __('Add Queue'), + 'root' => 'T', + 'staff_id' => $thisstaff->getId(), + 'parent_id' => $_GET['pid'], + )); + $this->_tryAgain($search); + } + function saveSearch($id=0) { global $thisstaff; @@ -151,15 +179,16 @@ class SearchAjaxAPI extends AjaxController { Http::response(403, 'Agent login is required'); if ($id) { // update - $search = SavedSearch::lookup($id); + if (!($search = SavedSearch::lookup($id)) + || !$search->checkAccess($thisstaff)) + Http::response(404, 'No such saved search'); } else { // new search - $search = SavedSearch::create(array('root' => 'T')); - $search->staff_id = $thisstaff->getId(); + $search = SavedSearch::create(array( + 'root' => 'T', + 'staff_id' => $thisstaff->getId() + )); } - if (!$search || !$search->checkAccess($thisstaff)) - Http::response(404, 'No such saved search'); - if (false === $this->_saveSearch($search)) return; @@ -169,16 +198,23 @@ class SearchAjaxAPI extends AjaxController { $id ? __('updated') : __('created'), __('successfully')), ); - - $this->_tryAgain($search, $search->getForm(), null, $info); + $this->_tryAgain($search, null, null, $info); } function _saveSearch(SavedSearch $search) { + + // Validate the form. $form = $search->getForm($_POST); + if ($this->_hasErrors($search, $form)) + return false; + $errors = array(); if (!$search->update($_POST, $errors) - || !$search->save(true) - ) { + || !$search->save(true)) { + + $form->addError(sprintf( + __('Unable to update %s. Correct error(s) below and try again.'), + __('queue'))); $this->_tryAgain($search, $form, $errors); return false; } @@ -352,43 +388,15 @@ class SearchAjaxAPI extends AjaxController { function collectQueueCounts($ids=null) { global $thisstaff; - if (!$thisstaff) { + if (!$thisstaff) Http::response(403, 'Agent login is required'); - } - - $queues = CustomQueue::objects() - ->filter(Q::any(array( - 'flags__hasbit' => CustomQueue::FLAG_PUBLIC, - 'staff_id' => $thisstaff->getId(), - ))); + $criteria = array(); if ($ids && is_array($ids)) - $queues->filter(array('id__in' => $ids)); - - $query = Ticket::objects(); - - // Visibility contraints ------------------ - // TODO: Consider SavedSearch::ignoreVisibilityConstraints() - $visibility = $thisstaff->getTicketsVisibility(); - $query->filter($visibility); - foreach ($queues as $queue) { - $Q = $queue->getBasicQuery(); - if (count($Q->extra) || $Q->isWindowed()) { - // XXX: This doesn't work - $query->annotate(array( - 'q'.$queue->id => $Q->values_flat() - ->aggregate(array('count' => SqlAggregate::COUNT('ticket_id'))) - )); - } - else { - $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), new SqlField('ticket_id')); - $query->aggregate(array( - 'q'.$queue->id => SqlAggregate::COUNT($expr, true) - )); - } - } + $criteria = array('id__in' => $ids); + $counts = SavedQueue::ticketsCount($thisstaff, $criteria, 'q'); Http::response(200, false, 'application/json'); - return $this->encode($query->values()->one()); + return $this->encode($counts); } } diff --git a/include/ajax.thread.php b/include/ajax.thread.php index a66062e0b933607397803545f98c384d46b5b53c..0369548ed5deb1b855c6f2f9c3127d4eb9b4ff22 100644 --- a/include/ajax.thread.php +++ b/include/ajax.thread.php @@ -74,11 +74,11 @@ class ThreadAjaxAPI extends AjaxController { if (!$user_info) $info['error'] = __('Unable to find user in directory'); - return self::_addcollaborator($thread, null, $form, $info); + return self::_addcollaborator($thread, null, $form, 'addcc', $info); } //Collaborators utils - function addCollaborator($tid, $uid=0) { + function addCollaborator($tid, $type=null, $uid=0) { global $thisstaff; if (!($thread=Thread::lookup($tid)) @@ -90,7 +90,7 @@ class ThreadAjaxAPI extends AjaxController { //If not a post then assume new collaborator form if(!$_POST) - return self::_addcollaborator($thread, $user); + return self::_addcollaborator($thread, $user, null, $type); $user = $form = null; if (isset($_POST['id']) && $_POST['id']) { //Existing user/ @@ -107,7 +107,7 @@ class ThreadAjaxAPI extends AjaxController { array(), $errors))) { $info = array('msg' => sprintf(__('%s added as a collaborator'), Format::htmlchars($c->getName()))); - $c->setCc(); + $type == 'addbcc' ? $c->setBcc() : $c->setCc(); $c->save(); return self::_collaborators($thread, $info); } @@ -119,7 +119,7 @@ class ThreadAjaxAPI extends AjaxController { $info +=array('error' =>__('Unable to add collaborator.').' '.__('Internal error occurred')); } - return self::_addcollaborator($thread, $user, $form, $info); + return self::_addcollaborator($thread, $user, $form, $type, $info); } function updateCollaborator($tid, $cid) { @@ -194,15 +194,15 @@ class ThreadAjaxAPI extends AjaxController { return $resp; } - function _addcollaborator($thread, $user=null, $form=null, $info=array()) { + function _addcollaborator($thread, $user=null, $form=null, $type=null, $info=array()) { global $thisstaff; $info += array( 'title' => __('Add a collaborator'), - 'action' => sprintf('#thread/%d/add-collaborator', - $thread->getId()), - 'onselect' => sprintf('ajax.php/thread/%d/add-collaborator/', - $thread->getId()), + 'action' => sprintf('#thread/%d/add-collaborator/%s', + $thread->getId(), $type), + 'onselect' => sprintf('ajax.php/thread/%d/add-collaborator/%s/', + $thread->getId(), $type), ); ob_start(); diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 3f53acd180bb79283d90de1c7766e674ec20c55b..6825556a98ad07028029130e195d7eaa3d7447d5 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -523,6 +523,10 @@ function refer($tid, $target=null) { __($field->getLabel()) ) ); + + $impl = $field->getImpl(); + if ($impl instanceof FileUploadField) + $field->save(); Http::response(201, $field->getClean()); } $form->addErrors($errors); @@ -938,7 +942,7 @@ function refer($tid, $target=null) { || !$ticket->checkStaffPerm($thisstaff)) Http::response(404, 'Unknown ticket #'); - $role = $thisstaff->getRole($ticket->getDeptId()); + $role = $ticket->getRole($thisstaff); $info = array(); $state = null; @@ -991,7 +995,7 @@ function refer($tid, $target=null) { elseif ($status->getId() == $ticket->getStatusId()) $errors['err'] = sprintf(__('Ticket already set to %s status'), __($status->getName())); - elseif (($role = $thisstaff->getRole($ticket->getDeptId()))) { + elseif (($role = $ticket->getRole($thisstaff))) { // Make sure the agent has permission to set the status switch(mb_strtolower($status->getState())) { case 'open': diff --git a/include/ajax.users.php b/include/ajax.users.php index d42349924e89430df12309c7159b1c7eba839fa6..328b93cf0e1d1167797ad29fcda7a0d6a1b8da8b 100644 --- a/include/ajax.users.php +++ b/include/ajax.users.php @@ -73,12 +73,16 @@ class UsersAjaxAPI extends AjaxController { return $this->search($type, $fulltext); } } else { - $users->filter(Q::any(array( + $filter = Q::any(array( 'emails__address__contains' => $q, 'name__contains' => $q, 'org__name__contains' => $q, - 'cdata__phone__contains' => $q, - ))); + 'account__username__contains' => $q, + )); + if (UserForm::getInstance()->getField('phone')) + $filter->add(array('cdata__phone__contains' => $q)); + + $users->filter($filter); } // Omit already-imported remote users diff --git a/include/api.tickets.php b/include/api.tickets.php index e2ee066aefff68921a26dc4bfbd28b62008b7a8e..c209086a9d57a682462f2d2966c5fd41db69b176 100644 --- a/include/api.tickets.php +++ b/include/api.tickets.php @@ -40,7 +40,7 @@ class TicketApiController extends ApiController { if(!strcasecmp($format, 'email')) { $supported = array_merge($supported, array('header', 'mid', 'emailId', 'to-email-id', 'ticketId', 'reply-to', 'reply-to-name', - 'in-reply-to', 'references', 'thread-type', + 'in-reply-to', 'references', 'thread-type', 'system_emails', 'mailflags' => array('bounce', 'auto-reply', 'spam', 'viral'), 'recipients' => array('*' => array('name', 'email', 'source')) )); diff --git a/include/class.category.php b/include/class.category.php index 3eeedfd144b09721f4004a31a3aa26c9be2e5108..c4b1ff0d1ab8f2d2c40db18c8ff5fc5980facccc 100644 --- a/include/class.category.php +++ b/include/class.category.php @@ -55,7 +55,9 @@ class Category extends VerySimpleModel { return $count; } function getDescription() { return $this->description; } - function getDescriptionWithImages() { return Format::viewableImages($this->description); } + function getDescriptionWithImages() { + return Format::viewableImages($this->description); + } function getNotes() { return $this->notes; } function getCreateDate() { return $this->created; } function getUpdateDate() { return $this->updated; } diff --git a/include/class.dept.php b/include/class.dept.php index a1a87164f924da3965f1ff77be5e31cbaf4fc4ed..4eb0b8e2229d906bbce2031f6690c8d54af23778 100644 --- a/include/class.dept.php +++ b/include/class.dept.php @@ -107,7 +107,7 @@ implements TemplateVariable, Searchable { 'name' => new TextboxField(array( 'label' => __('Name'), )), - 'manager' => new AgentSelectionField(array( + 'manager' => new DepartmentManagerSelectionField(array( 'label' => __('Manager'), )), ); diff --git a/include/class.export.php b/include/class.export.php index 6a25afc4fe760931e1b41f72689cd0063c45e883..3a47bcc942e358d51fa6615bbd9d065a887182ba 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -390,7 +390,7 @@ class ResultSetExporter { function dump() { # Useful for debug output while ($row=$this->nextArray()) { - var_dump($row); + var_dump($row); //nolint } } } diff --git a/include/class.faq.php b/include/class.faq.php index 4eb1c2d83df46c4953ed0712c1cb03c5a8d913ba..555fd3aee7f6f01fb485259e338c13c394dc826b 100644 --- a/include/class.faq.php +++ b/include/class.faq.php @@ -74,7 +74,7 @@ class FAQ extends VerySimpleModel { function getQuestion() { return $this->question; } function getAnswer() { return $this->answer; } function getAnswerWithImages() { - return Format::viewableImages($this->answer); + return Format::viewableImages($this->answer, ['type' => 'F']); } function getTeaser() { return Format::truncate(Format::striptags($this->answer), 150); @@ -194,7 +194,8 @@ class FAQ extends VerySimpleModel { return $this->_getLocal('answer', $lang); } function getLocalAnswerWithImages($lang=false) { - return Format::viewableImages($this->getLocalAnswer($lang)); + return Format::viewableImages($this->getLocalAnswer($lang), + ['type' => 'F']); } function _getLocal($what, $lang=false) { if (!$lang) { diff --git a/include/class.file.php b/include/class.file.php index bcdb9e0ed68f0ce7a944a9ae866ea5626dd4b90b..d880840861b09065a34cd275ebb726773d82e105 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -184,25 +184,31 @@ class AttachmentFile extends VerySimpleModel { exit(); } - function getDownloadUrl($minage=false, $disposition=false, $handler=false) { - // XXX: Drop this when AttachmentFile goes to ORM + function getDownloadUrl($options=array()) { + // Add attachment ref id if object type is set + if (isset($options['type']) + && !isset($options['id']) + && ($a=$this->attachments->findFirst(array( + 'type' => $options['type'])))) + $options['id'] = $a->getId(); + return static::generateDownloadUrl($this->getId(), - strtolower($this->getKey()), $this->getSignature(), $minage, - $disposition, $handler); + strtolower($this->getKey()), $this->getSignature(), + $options); } - static function generateDownloadUrl($id, $key, $hash, $minage=false, - $disposition=false, $handler=false - ) { - // Expire at the nearest midnight, allowing at least 12 hours access - $minage = $minage ?: 43200; - $gmnow = Misc::gmtime() + $minage; + static function generateDownloadUrl($id, $key, $hash, $options = array()) { + + // Expire at the nearest midnight, allow at least12 hrs access + $minage = @$options['minage'] ?: 43200; + $gmnow = Misc::gmtime() + $options['minage']; $expires = $gmnow + 86400 - ($gmnow % 86400); // Generate a signature based on secret content $signature = static::_genUrlSignature($id, $key, $hash, $expires); - $handler = $handler ?: ROOT_PATH . 'file.php'; + // Handler / base url + $handler = @$options['handler'] ?: ROOT_PATH . 'file.php'; // Return sanitized query string $args = array( @@ -211,10 +217,13 @@ class AttachmentFile extends VerySimpleModel { 'signature' => $signature, ); - if ($disposition) - $args['disposition'] = $disposition; + if (isset($options['disposition'])) + $args['disposition'] = $options['disposition']; + + if (isset($options['id'])) + $args['id'] = $options['id']; - return $handler . '?' . http_build_query($args); + return sprintf('%s?%s', $handler, http_build_query($args)); } function verifySignature($signature, $expires) { @@ -635,7 +644,7 @@ class AttachmentFile extends VerySimpleModel { ->filter(array( 'attachments__object_id__isnull' => true, 'ft' => 'T', - 'created__gt' => new DateTime('now -1 day'), + 'created__lt' => SqlFunction::NOW()->minus(SqlInterval::DAY(1)), )); foreach ($files as $f) { diff --git a/include/class.filter.php b/include/class.filter.php index 7178aece5eba1d0540020b9898f2602bdcdd55df..1827342284a3bab796bc1db2244dd53c527e0e0b 100644 --- a/include/class.filter.php +++ b/include/class.filter.php @@ -471,20 +471,28 @@ class Filter { } function save($id,$vars,&$errors) { + //get current filter actions (they're validated before saving) + self::save_actions($id, $vars, $errors); + if ($this) { foreach ($this->getActions() as $A) { - if ($A->type == 'dept') - $dept = Dept::lookup($A->parseConfiguration($vars)['dept_id']); - - if ($A->type == 'topic') - $topic = Topic::lookup($A->parseConfiguration($vars)['topic_id']); + $config = JsonDataParser::parse($A->configuration); + if ($A->type == 'dept') { + $dept = Dept::lookup($config['dept_id']); + $dept_action = $A->getId(); + } + + if ($A->type == 'topic') { + $topic = Topic::lookup($config['topic_id']); + $topic_action = $A->getId(); + } } } - if($dept && !$dept->isActive()) + if($dept && !$dept->isActive() && (is_array($vars['actions']) && !in_array('D' . $dept_action,$vars['actions']))) $errors['err'] = sprintf(__('%s selected for %s must be active'), __('Department'), __('Filter Action')); - if($topic && !$topic->isActive()) + if($topic && !$topic->isActive() && (is_array($vars['actions']) && !in_array('D' . $topic_action,$vars['actions']))) $errors['err'] = sprintf(__('%s selected for %s must be active'), __('Help Topic'), __('Filter Action')); if(!$vars['execorder']) @@ -522,7 +530,7 @@ class Filter { .',execorder='.db_input($vars['execorder']) .',email_id='.db_input($emailId) .',match_all_rules='.db_input($vars['match_all_rules']) - .',stop_onmatch='.db_input(isset($vars['stop_onmatch'])?1:0) + .',stop_onmatch='.db_input($vars['stop_onmatch']) .',notes='.db_input(Format::sanitize($vars['notes'])); if($id) { @@ -543,7 +551,6 @@ class Filter { # Don't care about errors stashed in $xerrors $xerrors = array(); self::save_rules($id,$vars,$xerrors); - self::save_actions($id, $vars, $errors); return count($errors) == 0; } @@ -551,20 +558,31 @@ class Filter { function validate_actions($action) { $errors = array(); $config = json_decode($action->ht['configuration'], true); - if ($action->ht['type'] == 'dept') { - $dept = Dept::lookup($config['dept_id']); - if (!$dept || !$dept->isActive()) { - $errors['err'] = sprintf(__('Unable to save: Please choose an active %s'), 'Department'); - return $errors; - } - } - - if ($action->ht['type'] == 'topic') { - $topic = Topic::lookup($config['topic_id']); - if (!$topic || !$topic->isActive()) { - $errors['err'] = sprintf(__('Unable to save: Please choose an active %s'), 'Help Topic'); - return $errors; - } + switch ($action->ht['type']) { + case 'dept': + $dept = Dept::lookup($config['dept_id']); + if (!$dept || !$dept->isActive()) { + $errors['err'] = sprintf(__('Unable to save: Please choose an active %s'), 'Department'); + return $errors; + } + break; + + case 'topic': + $topic = Topic::lookup($config['topic_id']); + if (!$topic || !$topic->isActive()) { + $errors['err'] = sprintf(__('Unable to save: Please choose an active %s'), 'Help Topic'); + return $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'])); + return $errors; + } + } + break; } return false; @@ -593,7 +611,6 @@ class Filter { 'sort' => (int) $sort, )); $I->setConfiguration($errors, $vars); - $config = json_decode($I->ht['configuration'], true); $invalid = self::validate_actions($I); if ($invalid) { @@ -607,8 +624,6 @@ class Filter { if ($I = FilterAction::lookup($info)) { $I->setConfiguration($errors, $vars); - $config = json_decode($I->ht['configuration'], true); - $invalid = self::validate_actions($I); if ($invalid) { $errors['err'] = sprintf($invalid['err']); diff --git a/include/class.filter_action.php b/include/class.filter_action.php index c963e355c1ab0905f49d10dcf6926718851a8c71..59bafbf57ebfe40205adbfe2dbecd60b33525083 100644 --- a/include/class.filter_action.php +++ b/include/class.filter_action.php @@ -41,8 +41,7 @@ class FilterAction extends VerySimpleModel { return $this->_config; } - function parseConfiguration($source, &$errors=array()) - { + function parseConfiguration($source, &$errors=array()) { if (!$source) return $this->getConfiguration(); diff --git a/include/class.format.php b/include/class.format.php index 50c8a7fdf144c12d23f9f59ff4c1b3dc87cd4609..2463271c875b24e85b6a50039690096cddfb2127 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -306,8 +306,10 @@ class Format { ':<!DOCTYPE[^>]+>:', # <!DOCTYPE ... > ':<\?[^>]+>:', # <?xml version="1.0" ... > ':<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 ), - array('', '', '', '', '<html'), + array('', '', '', '', '<html', '$4', '$2 $3'), $html); // HtmLawed specific config only @@ -455,14 +457,17 @@ class Format { } - function viewableImages($html, $script=false) { + function viewableImages($html, $options=array()) { $cids = $images = array(); + $options +=array( + 'deposition' => 'inline'); return preg_replace_callback('/"cid:([\w._-]{32})"/', - function($match) use ($script, $images) { + function($match) use ($options, $images) { if (!($file = AttachmentFile::lookup($match[1]))) return $match[0]; + return sprintf('"%s" data-cid="%s"', - $file->getDownloadUrl(false, 'inline', $script), $match[1]); + $file->getDownloadUrl($options), $match[1]); }, $html); } diff --git a/include/class.forms.php b/include/class.forms.php index 2f8582855c64596e1f364476b4a449f0b0185d04..9ef27efb5e8385b313159ef4505edd625ffa14b0 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -1650,6 +1650,15 @@ class BooleanField extends FormField { ); } + function describeSearchMethod($method) { + + $methods = $this->get('descsearchmethods'); + if (isset($methods[$method])) + return $methods[$method]; + + return parent::describeSearchMethod($method); + } + function getSearchMethodWidgets() { return array( 'set' => null, @@ -2690,15 +2699,12 @@ class DepartmentField extends ChoiceField { else { $current_id = $selected->value; $current_name = Dept::getNameById($current_id); - $addNew = true; } } - $active_depts = array(); - if($current_id) - $active_depts = Dept::objects() - ->filter(array('flags__hasbit' => Dept::FLAG_ACTIVE)) - ->values('id', 'name'); + $active_depts = Dept::objects() + ->filter(array('flags__hasbit' => Dept::FLAG_ACTIVE)) + ->values('id', 'name'); $choices = array(); if ($depts = Dept::getDepartments(null, true, Dept::DISPLAY_DISABLED)) { @@ -2708,18 +2714,17 @@ class DepartmentField extends ChoiceField { $active[$dept['id']] = $dept['name']; //add selected dept to list - $active[$current_id] = $current_name; - + if($current_id) + $active[$current_id] = $current_name; + else + return $active; foreach ($depts as $id => $name) { $choices[$id] = $name; if(!array_key_exists($id, $active) && $current_id) - unset($choices[$id]); + unset($choices[$id]); } - } - if($addNew) - $choices[':new:'] = '— '.__('Add New').' —'; return $choices; } @@ -3933,6 +3938,8 @@ class ChoicesWidget extends Widget { $values[$v] = $choices[$v]; elseif (($i=$this->field->lookupChoice($v))) $values += $i; + elseif (!$k && $v) + return $v; } } } @@ -4513,13 +4520,15 @@ class FreeTextWidget extends Widget { if (($attachments = $this->field->getFiles()) && count($attachments)) { ?> <section class="freetext-files"> <div class="title"><?php echo __('Related Resources'); ?></div> - <?php foreach ($attachments as $attach) { ?> + <?php foreach ($attachments as $attach) { + $filename = Format::htmlchars($attach->getFilename()); + ?> <div class="file"> <a href="<?php echo $attach->file->getDownloadUrl(); ?>" - target="_blank" download="<?php echo $attach->file->getDownloadUrl(); - ?>" class="truncate no-pjax"> + target="_blank" download="<?php echo $filename; ?>" + class="truncate no-pjax"> <i class="icon-file"></i> - <?php echo Format::htmlchars($attach->getFilename()); ?> + <?php echo $filename; ?> </a> </div> <?php } ?> @@ -4914,7 +4923,7 @@ class ReferralForm extends Form { ), ) ), - 'dept' => new ChoiceField(array( + 'dept' => new DepartmentField(array( 'id'=>4, 'label' => '', 'flags' => hexdec(0X450F3), diff --git a/include/class.json.php b/include/class.json.php index ad5ac65c12e3da2c939faecdb67dfec3f049c635..66edb7623e4944cc4ab30b763740993294652f5a 100644 --- a/include/class.json.php +++ b/include/class.json.php @@ -21,25 +21,39 @@ include_once "JSON.php"; class JsonDataParser { - function parse($stream) { + function parse($stream, $tidy=false) { if (is_resource($stream)) { $contents = ''; while (!feof($stream)) $contents .= fread($stream, 8192); } else $contents = $stream; + + if ($contents && $tidy) + $contents = self::tidy($contents); + return self::decode($contents); } - function decode($contents) { - if (function_exists("json_decode")) { - return json_decode($contents, true); - } else { - # Create associative arrays rather than 'objects' - $decoder = new Services_JSON(SERVICES_JSON_LOOSE_TYPE); - return $decoder->decode($contents); - } + static function decode($contents, $assoc=true) { + if (function_exists("json_decode")) + return json_decode($contents, $assoc); + + $decoder = new Services_JSON($assoc ? SERVICES_JSON_LOOSE_TYPE : 0); + return $decoder->decode($contents); } + + static function tidy($content) { + + // Clean up doubly quoted JSON + $content = str_replace( + array(':"{', '}"', '\"'), + array(':{', '}', '"'), + $content); + // return trimmed content. + return trim($content); + } + function lastError() { if (function_exists("json_last_error")) { $errors = array( diff --git a/include/class.orm.php b/include/class.orm.php index 08c2f209c324ab32f7272435d29e6855abfa3e64..acb94853467f21c5442447029bc9b3f354d2b3ae 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -1737,7 +1737,8 @@ implements ArrayAccess { function sort($key=false, $reverse=false) { // Fetch all records into the cache $this->asArray(); - return parent::sort($key, $reverse); + parent::sort($key, $reverse); + return $this; } /** diff --git a/include/class.page.php b/include/class.page.php index 3ea5ca0f98839dc5fcdc4638c5de515b492bc245..a430fc2e66cd20da6b5a1711b1739094bbc8fdbc 100644 --- a/include/class.page.php +++ b/include/class.page.php @@ -70,7 +70,7 @@ class Page extends VerySimpleModel { return $this->_getLocal('body', $lang); } function getBodyWithImages() { - return Format::viewableImages($this->getLocalBody()); + return Format::viewableImages($this->getLocalBody(), ['type' => 'P']); } function _getLocal($what, $lang=false) { diff --git a/include/class.pdf.php b/include/class.pdf.php index c12d071e82dd48058513fad2d1a89dcf5efb25a7..cc395f388999d1230b0da195747e9a9990ee22df 100644 --- a/include/class.pdf.php +++ b/include/class.pdf.php @@ -45,6 +45,10 @@ class mPDFWithLocalImages extends mPDF { ); return call_user_func_array(array('parent', 'WriteHtml'), $args); } + + function output($name, $dest) { + return parent::Output($name, $dest); + } } class Ticket2PDF extends mPDFWithLocalImages diff --git a/include/class.queue.php b/include/class.queue.php index b037d4eee0769d5e7ad4d291e1cc5e24d1b9537e..189a7a84654a2bda283c60e0a49eae443ab142e8 100644 --- a/include/class.queue.php +++ b/include/class.queue.php @@ -28,6 +28,7 @@ class CustomQueue extends VerySimpleModel { ), 'columns' => array( 'reverse' => 'QueueColumnGlue.queue', + 'constrain' => array('staff_id' =>'QueueColumnGlue.staff_id'), 'broker' => 'QueueColumnListBroker', ), 'sorts' => array( @@ -111,6 +112,10 @@ class CustomQueue extends VerySimpleModel { return $this->path ?: $this->buildPath(); } + function criteriaRequired() { + return true; + } + function getCriteria($include_parent=false) { if (!isset($this->criteria)) { $old = @$this->config[0] === '{'; @@ -123,7 +128,9 @@ class CustomQueue extends VerySimpleModel { && !isset($this->criteria['conditions']) ) { // TODO: Upgrade old ORM path names - $this->criteria = $this->isolateCriteria($this->criteria); + // Parse criteria out of JSON if any. + $this->criteria = self::isolateCriteria($this->criteria, + $this->getRoot()); } } $criteria = $this->criteria ?: array(); @@ -162,38 +169,40 @@ class CustomQueue extends VerySimpleModel { * Parameters: * $search - <array> Request parameters ($_POST) used to update the * search beyond the current configuration of the search criteria + * $searchables - search fields - default to current if not provided */ - function getForm($source=null) { - $searchable = $this->getCurrentSearchFields($source); - $fields = array( - ':keywords' => new TextboxField(array( - 'id' => 3001, - 'configuration' => array( - 'size' => 40, - 'length' => 400, - 'autofocus' => true, - 'classes' => 'full-width headline', - 'placeholder' => __('Keywords — Optional'), - ), - )), - ); - foreach ($searchable as $path=>$field) { - $fields = array_merge($fields, static::getSearchField($field, $path)); + function getForm($source=null, $searchable=null) { + $fields = array(); + $validator = false; + if (!isset($searchable)) { + $searchable = $this->getCurrentSearchFields($source); + $validator = true; + $fields = array( + ':keywords' => new TextboxField(array( + 'id' => 3001, + 'configuration' => array( + 'size' => 40, + 'length' => 400, + 'autofocus' => true, + 'classes' => 'full-width headline', + 'placeholder' => __('Keywords — Optional'), + ), + )), + ); } + foreach ($searchable as $path=>$field) + $fields = array_merge($fields, static::getSearchField($field, $path)); + $form = new AdvancedSearchForm($fields, $source); - $form->addValidator(function($form) { - $selected = 0; - foreach ($form->getFields() as $F) { - if (substr($F->get('name'), -7) == '+search' && $F->getClean()) - $selected += 1; - // Consider keyword searches - elseif ($F->get('name') == ':keywords' && $F->getClean()) - $selected += 1; - } - if (!$selected) - $form->addError(__('No fields selected for searching')); - }); + + // Field selection validator + if ($this->criteriaRequired()) { + $form->addValidator(function($form) { + if (!$form->getNumFieldsSelected()) + $form->addError(__('No fields selected for searching')); + }); + } // Load state from current configuraiton if (!$source) { @@ -233,7 +242,7 @@ class CustomQueue extends VerySimpleModel { * to contain a list extra fields by ORM path, of newly added * fields not yet saved in this object's getCriteria(). */ - function getCurrentSearchFields($source=array()) { + function getCurrentSearchFields($source=array(), $criteria=array()) { static $basic = array( 'Ticket' => array( 'status__state', @@ -255,7 +264,7 @@ class CustomQueue extends VerySimpleModel { $core[$path] = $all[$path]; // Add others from current configuration - foreach ($this->getCriteria() as $C) { + foreach ($criteria ?: $this->getCriteria() as $C) { list($path) = $C; if (isset($all[$path])) $core[$path] = $all[$path]; @@ -269,6 +278,27 @@ class CustomQueue extends VerySimpleModel { return $core; } + /** + * Fetch all supported ORM fields filterable by this search object. + */ + function getSupportedFilters() { + return static::getFilterableFields($this->getRoot()); + } + + + /** + * Get get supplemental matches for public queues. + * + */ + + function getSupplementalMatches() { + return array(); + } + + function getSupplementalCriteria() { + return array(); + } + /** * Fetch all supported ORM fields searchable by this search object. The * returned list represents searchable fields, keyed by the ORM path. @@ -374,6 +404,20 @@ class CustomQueue extends VerySimpleModel { return $fields; } + /** + * Fetch all searchable fileds, for the base object which support quick filters. + */ + function getFilterableFields($object) { + $filters = array(); + foreach (static::getSearchableFields($object) as $p => $f) { + list($label, $field) = $f; + if ($field && $field->supportsQuickFilter()) + $filters[$p] = $f; + } + + return $filters; + } + /** * Fetch the FormField instances used when for configuring a searchable * field in the user interface. This is the glue between a field @@ -460,11 +504,13 @@ class CustomQueue extends VerySimpleModel { * field name being search, the method used for searhing, and the method- * specific data entered in the UI. */ - function isolateCriteria($criteria, $root=null) { - $searchable = static::getSearchableFields($root ?: $this->getRoot()); - $items = array(); + static function isolateCriteria($criteria, $base='Ticket') { + if (!is_array($criteria)) return null; + + $items = array(); + $searchable = static::getSearchableFields($base); foreach ($criteria as $k=>$v) { if (substr($k, -7) === '+method') { list($name,) = explode('+', $k, 2); @@ -477,9 +523,8 @@ class CustomQueue extends VerySimpleModel { // Lookup the field to search this condition list($label, $field) = $searchable[$name]; - - // Get the search method and value - $method = $v; + // Get the search method + $method = is_array($v) ? key($v) : $v; // Not all search methods require a value $value = $criteria["{$name}+{$method}"]; @@ -574,6 +619,10 @@ class CustomQueue extends VerySimpleModel { return $fields; } + function getStandardColumns() { + return $this->getColumns(); + } + function getColumns($use_template=false) { if ($this->columns_id && ($q = CustomQueue::lookup($this->columns_id)) @@ -818,6 +867,7 @@ class CustomQueue extends VerySimpleModel { * array retrieved in ::getSearchFields() */ function describeField($info, $name=false) { + $name = $name ?: $info['field']->get('label'); return $info['field']->describeSearch($info['method'], $info['value'], $name); } @@ -862,17 +912,25 @@ class CustomQueue extends VerySimpleModel { } function checkAccess(Staff $agent) { - return $agent->getId() == $this->staff_id - || $this->hasFlag(self::FLAG_PUBLIC); + return $this->isPublic() || $this->checkOwnership($agent); } - function ignoreVisibilityConstraints() { - global $thisstaff; + function checkOwnership(Staff $agent) { + + return ($agent->getId() == $this->staff_id && + !$this->isAQueue()); + } + + function isOwner(Staff $agent) { + return $agent && $this->isPrivate() && $this->checkOwnership($agent); + } - // For saved searches (not queues), staff can have a permission to + function ignoreVisibilityConstraints(Staff $agent) { + // For saved searches (not queues), some staff can have a permission to // see all records - return !$this->isAQueue() - && $thisstaff->hasPerm(SearchBackend::PERM_EVERYTHING); + return (!$this->isASubQueue() + && $this->isOwner($agent) + && $agent->canSearchEverything()); } function inheritCriteria() { @@ -883,6 +941,10 @@ class CustomQueue extends VerySimpleModel { return $this->hasFlag(self::FLAG_INHERIT_COLUMNS); } + function useStandardColumns() { + return !count($this->columns); + } + function inheritExport() { return ($this->hasFlag(self::FLAG_INHERIT_EXPORT) || !count($this->exports)); @@ -911,12 +973,22 @@ class CustomQueue extends VerySimpleModel { return $base; } + function isASubQueue() { + return $this->parent ? $this->parent->isASubQueue() : + $this->isAQueue(); + } + function isAQueue() { return $this->hasFlag(self::FLAG_QUEUE); } function isPrivate() { - return !$this->isAQueue() && !$this->hasFlag(self::FLAG_PUBLIC); + return !$this->isAQueue() && !$this->isPublic() && + $this->staff_id; + } + + function isPublic() { + return $this->hasFlag(self::FLAG_PUBLIC); } protected function hasFlag($flag) { @@ -995,18 +1067,19 @@ class CustomQueue extends VerySimpleModel { } function update($vars, &$errors=array()) { + // Set basic search information - if (!$vars['name']) - $errors['name'] = __('A title is required'); + if (!$vars['queue-name']) + $errors['queue-name'] = __('A title is required'); elseif (($q=CustomQueue::lookup(array( - 'title' => $vars['name'], + 'title' => $vars['queue-name'], 'parent_id' => $vars['parent_id'] ?: 0, 'staff_id' => $this->staff_id))) && $q->getId() != $this->id ) - $errors['name'] = __('Saved queue with same name exists'); + $errors['queue-name'] = __('Saved queue with same name exists'); - $this->title = $vars['name']; + $this->title = $vars['queue-name']; $this->parent_id = @$vars['parent_id'] ?: 0; if ($this->parent_id && !$this->parent) $errors['parent_id'] = __('Select a valid queue'); @@ -1038,7 +1111,7 @@ class CustomQueue extends VerySimpleModel { $this->setFlag(self::FLAG_INHERIT_CRITERIA, $this->parent_id > 0 && isset($vars['inherit'])); $this->setFlag(self::FLAG_INHERIT_COLUMNS, - $this->parent_id > 0 && isset($vars['inherit-columns'])); + isset($vars['inherit-columns'])); $this->setFlag(self::FLAG_INHERIT_EXPORT, $this->parent_id > 0 && isset($vars['inherit-exports'])); $this->setFlag(self::FLAG_INHERIT_SORTING, @@ -1049,37 +1122,17 @@ class CustomQueue extends VerySimpleModel { // No columns -- imply column inheritance $this->setFlag(self::FLAG_INHERIT_COLUMNS); } - if (isset($vars['columns']) && !$this->hasFlag(self::FLAG_INHERIT_COLUMNS)) { - $new = $vars['columns']; - $order = array_keys($new); - foreach ($this->columns as $col) { - $key = $col->column_id; - if (!isset($vars['columns'][$key])) { - $this->columns->remove($col); - continue; - } - $info = $vars['columns'][$key]; - $col->set('sort', array_search($key, $order)); - $col->set('heading', $info['heading']); - $col->set('width', $info['width']); - $col->setSortable($info['sortable']); - unset($new[$key]); - } - // Add new columns - foreach ($new as $info) { - $glue = new QueueColumnGlue(array( - 'column_id' => $info['column_id'], - 'sort' => array_search($info['column_id'], $order), - 'heading' => $info['heading'], - 'width' => $info['width'] ?: 100, - 'bits' => $info['sortable'] ? QueueColumn::FLAG_SORTABLE : 0, - )); - $glue->queue = $this; - $this->columns->add( - QueueColumn::lookup($info['column_id']), $glue); - } - // Re-sort the in-memory columns array - $this->columns->sort(function($c) { return $c->sort; }); + + + if ($this->getId() + && isset($vars['columns']) + && !$this->hasFlag(self::FLAG_INHERIT_COLUMNS)) { + + + if ($this->columns->update($vars['columns'], $errors, array( + 'queue_id' => $this->getId(), + 'staff_id' => $this->staff_id))) + $this->columns->reset(); } // Update export fields for the queue @@ -1154,7 +1207,8 @@ class CustomQueue extends VerySimpleModel { } else { $this->config = JsonDataEncoder::encode([ - 'criteria' => $this->isolateCriteria($form->getClean()), + 'criteria' => self::isolateCriteria($form->getClean(), + $this->getRoot()), 'conditions' => $conditions, ]); // Clear currently set criteria.and conditions. @@ -1228,13 +1282,10 @@ class CustomQueue extends VerySimpleModel { static function create($vars=false) { - global $thisstaff; $queue = new static($vars); $queue->created = SqlFunction::NOW(); $queue->setFlag(self::FLAG_QUEUE); - if ($thisstaff) - $queue->staff_id = $thisstaff->getId(); return $queue; } @@ -1650,8 +1701,8 @@ class QueueColumnCondition { * field name being search, the method used for searhing, and the method- * specific data entered in the UI. */ - static function isolateCriteria($criteria, $root='Ticket') { - $searchable = CustomQueue::getSearchableFields($root); + static function isolateCriteria($criteria, $base='Ticket') { + $searchable = CustomQueue::getSearchableFields($base); foreach ($criteria as $k=>$v) { if (substr($k, -7) === '+method') { list($name,) = explode('+', $k, 2); @@ -2166,6 +2217,71 @@ extends VerySimpleModel { } } + +class QueueConfig +extends VerySimpleModel { + static $meta = array( + 'table' => QUEUE_CONFIG_TABLE, + 'pk' => array('queue_id', 'staff_id'), + 'joins' => array( + 'queue' => array( + 'constraint' => array( + 'queue_id' => 'CustomQueue.id'), + ), + 'staff' => array( + 'constraint' => array( + 'staff_id' => 'Staff.staff_id', + ) + ), + 'columns' => array( + 'reverse' => 'QueueColumnGlue.config', + 'constrain' => array('staff_id' =>'QueueColumnGlue.staff_id'), + 'broker' => 'QueueColumnListBroker', + ), + ), + ); + + function getSettings() { + return JsonDataParser::decode($this->setting); + } + + + function update($vars, &$errors) { + + // settings of interest + $setting = array( + 'sort_id' => (int) $vars['sort_id'], + 'filter' => $vars['filter'], + 'inherit-columns' => isset($vars['inherit-columns']), + 'criteria' => $vars['criteria'] ?: array(), + ); + + if (!$setting['inherit-columns'] && $vars['columns']) { + if (!$this->columns->update($vars['columns'], $errors, array( + 'queue_id' => $this->queue_id, + 'staff_id' => $this->staff_id))) + $setting['inherit-columns'] = true; + $this->columns->reset(); + } + + $this->setting = JsonDataEncoder::encode($setting); + + return $this->save(); + } + + function save($refetch=false) { + if ($this->dirty) + $this->updated = SqlFunction::NOW(); + return parent::save($refetch || $this->dirty); + } + + static function create($vars=false) { + $inst = new static($vars); + return $inst; + } +} + + class QueueExport extends VerySimpleModel { static $meta = array( @@ -2203,16 +2319,23 @@ class QueueColumnGlue extends VerySimpleModel { static $meta = array( 'table' => QUEUE_COLUMN_TABLE, - 'pk' => array('queue_id', 'column_id'), + 'pk' => array('queue_id', 'staff_id', 'column_id'), 'joins' => array( 'column' => array( 'constraint' => array('column_id' => 'QueueColumn.id'), ), 'queue' => array( - 'constraint' => array('queue_id' => 'CustomQueue.id'), + 'constraint' => array( + 'queue_id' => 'CustomQueue.id', + 'staff_id' => 'CustomQueue.staff_id'), + ), + 'config' => array( + 'constraint' => array( + 'queue_id' => 'QueueConfig.queue_id', + 'staff_id' => 'QueueConfig.staff_id'), ), ), - 'select_related' => array('column', 'queue'), + 'select_related' => array('column'), 'ordering' => array('sort'), ); } @@ -2244,6 +2367,44 @@ extends InstrumentedList { parent::add($anno, false); return $anno; } + + function update($columns, &$errors, $options=array()) { + + + $new = $columns; + $order = array_keys($new); + foreach ($this as $col) { + $key = $col->column_id; + if (!isset($columns[$key])) { + $this->remove($col); + continue; + } + $info = $columns[$key]; + $col->set('sort', array_search($key, $order)); + $col->set('heading', $info['heading']); + $col->set('width', $info['width']); + $col->setSortable($info['sortable']); + unset($new[$key]); + } + // Add new columns + foreach ($new as $info) { + $glue = new QueueColumnGlue(array( + 'staff_id' => $options['staff_id'] ?: 0 , + 'queue_id' => $options['queue_id'] ?: 0, + 'column_id' => $info['column_id'], + 'sort' => array_search($info['column_id'], $order), + 'heading' => $info['heading'], + 'width' => $info['width'] ?: 100, + 'bits' => $info['sortable'] ? QueueColumn::FLAG_SORTABLE : 0, + )); + + $this->add(QueueColumn::lookup($info['column_id']), $glue); + } + // Re-sort the in-memory columns array + $this->sort(function($c) { return $c->sort; }); + + return $this->saveAll(); + } } class QueueSort diff --git a/include/class.report.php b/include/class.report.php index 999ae22fe0ac6c6e12a70edbb1bf6fc340f2611c..c39ec47cc5ea595aaddcaf46d25323929d58c1cc 100644 --- a/include/class.report.php +++ b/include/class.report.php @@ -211,9 +211,10 @@ class OverviewReport { $headers = array(__('Help Topic')); $header = function($row) { return Topic::getLocalNameById($row['topic_id'], $row['topic__topic']); }; $pk = 'topic_id'; + $topics = Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED); $stats = $stats ->values('topic_id', 'topic__topic', 'topic__flags') - ->filter(array('dept_id__in' => $thisstaff->getDepts(), 'topic_id__gt' => 0)); + ->filter(array('dept_id__in' => $thisstaff->getDepts(), 'topic_id__gt' => 0, 'topic_id__in' => array_keys($topics))); $times = $times ->values('topic_id') ->filter(array('topic_id__gt' => 0)); @@ -223,7 +224,10 @@ class OverviewReport { $header = function($row) { return new AgentsName(array( 'first' => $row['staff__firstname'], 'last' => $row['staff__lastname'])); }; $pk = 'staff_id'; - $stats = $stats->values('staff_id', 'staff__firstname', 'staff__lastname'); + $staff = Staff::getStaffMembers(); + $stats = $stats + ->values('staff_id', 'staff__firstname', 'staff__lastname') + ->filter(array('staff_id__in' => array_keys($staff))); $times = $times->values('staff_id'); $depts = $thisstaff->getManagedDepartments(); if ($thisstaff->hasPerm(ReportModel::PERM_AGENTS)) diff --git a/include/class.search.php b/include/class.search.php index d305ab16fee05a17d2de4c0c808d9f37230582f1..8b6ec005915d1055f1543ec0526363f34d163b9a 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -646,14 +646,29 @@ MysqlSearchBackend::register(); // Saved search system /** - * A special case of the custom queues used to represent an advanced search. + * Custom Queue truly represent a saved advanced search. */ -class SavedSearch extends CustomQueue { +class SavedQueue extends CustomQueue { // Override the ORM relationship to force no children private $children = false; + private $_config; + private $_criteria; + private $_columns; + private $_settings; + private $_form; - function isSaved() { - return true; + + + function __onload() { + global $thisstaff; + + // Load custom settings for this staff + if ($thisstaff) { + $this->_config = QueueConfig::lookup(array( + 'queue_id' => $this->getId(), + 'staff_id' => $thisstaff->getId()) + ); + } } static function forStaff(Staff $agent) { @@ -664,14 +679,216 @@ class SavedSearch extends CustomQueue { ->exclude(array('flags__hasbit'=>self::FLAG_QUEUE)); } + private function getSettings() { + if (!isset($this->_settings)) { + $this->_settings = array(); + if ($this->_config) + $this->_settings = $this->_config->getSettings(); + } + + return $this->_settings; + } + + private function getCustomColumns() { + + if (!isset($this->_columns)) { + $this->_columns = array(); + if ($this->_config + && $this->_config->columns->count()) + $this->_columns = $this->_config->columns; + } + + return $this->_columns; + } + + /** + * Fetch an AdvancedSearchForm instance for use in displaying or + * configuring this search in the user interface. + * + */ + function getForm($source=null, $searchable=array()) { + global $thisstaff; + + if (!$this->isAQueue()) + $searchable = $this->getCurrentSearchFields($source, + parent::getCriteria()); + else // Only allow supplemental matches. + $searchable = array_intersect_key($this->getCurrentSearchFields($source), + $this->getSupplementalMatches()); + + return parent::getForm($source, $searchable); + } + + /** + * Get get supplemental matches for public queues. + * + */ + function getSupplementalMatches() { + // Target flags + $flags = array('isoverdue', 'isassigned', 'isreopened', 'isanswered'); + $current = array(); + // Check for closed state - whih disables above flags + foreach (parent::getCriteria() as $c) { + if (!strcasecmp($c[0], 'status__state') + && isset($c[2]['closed'])) + return array(); + + $current[] = $c[0]; + } + + // Filter out fields already in criteria + $matches = array_intersect_key($this->getSupportedMatches(), + array_flip(array_diff($flags, $current))); + + return $matches; + } + + function criteriaRequired() { + return !$this->isAQueue(); + } + + function describeCriteria($criteria=false){ + $criteria = $criteria ?: parent::getCriteria(); + return parent::describeCriteria($criteria); + } + + function getCriteria($include_parent=true) { + + if (!isset($this->_criteria)) { + $this->getSettings(); + $this->_criteria = $this->_settings['criteria'] ?: array(); + } + + $criteria = $this->_criteria; + if ($include_parent) + $criteria = array_merge($criteria, + parent::getCriteria($include_parent)); + + + return $criteria; + } + + function getSupplementalCriteria() { + return $this->getCriteria(false); + } + + function useStandardColumns() { + + $this->getSettings(); + if ($this->getCustomColumns() + && isset($this->_settings['inherit-columns'])) + return $this->_settings['inherit-columns']; + + // owner?? edit away. + if ($this->_config + && $this->_config->staff_id == $this->staff_id) + return false; + + return parent::useStandardColumns(); + } + + function getStandardColumns() { + return parent::getColumns(is_null($this->parent)); + } + + function getColumns($use_template=false) { + + if (!$this->useStandardColumns() && ($columns=$this->getCustomColumns())) + return $columns; + + return parent::getColumns($use_template); + } + function update($vars, &$errors=array()) { - if (!parent::update($vars, $errors)) + global $thisstaff; + + if (!$this->checkAccess($thisstaff)) return false; - // Personal queues _always_ inherit from their parent - $this->setFlag(self::FLAG_INHERIT_CRITERIA, $this->parent_id > 0); + if ($this->checkOwnership($thisstaff)) { + // Owner of the queue - can update everything + if (!parent::update($vars, $errors)) + return false; + + // Personal queues _always_ inherit from their parent + $this->setFlag(self::FLAG_INHERIT_CRITERIA, $this->parent_id > + 0); + + return true; + } + + // Agent's config for public queue. + if (!$this->_config) + $this->_config = QueueConfig::create(array( + 'queue_id' => $this->getId(), + 'staff_id' => $thisstaff->getId())); + + // Validate & isolate supplemental criteria (if any) + $vars['criteria'] = array(); + if (isset($vars['fields'])) { + $form = $this->getForm($vars, $thisstaff); + if ($form->isValid()) { + $criteria = self::isolateCriteria($form->getClean(), + $this->getRoot()); + $allowed = $this->getSupplementalMatches(); + foreach ($criteria as $k => $c) + if (!isset($allowed[$c[0]])) + unset($criteria[$k]); + + $vars['criteria'] = $criteria ?: array(); + } else { + $errors['criteria'] = __('Validation errors exist on supplimental criteria'); + } + } + + if (!$errors && $this->_config->update($vars, $errors)) + $this->_settings = $this->_criteria = null; - return count($errors) === 0; + return (!$errors); + } + + static function ticketsCount($agent, $criteria=array(), + $prefix='') { + + if (!$agent instanceof Staff) + return array(); + + $queues = SavedQueue::objects() + ->filter(Q::any(array( + 'flags__hasbit' => CustomQueue::FLAG_PUBLIC, + 'staff_id' => $agent->getId(), + ))); + + if ($criteria) + $queues->filter($criteria); + + $query = Ticket::objects(); + // Apply tickets visibility for the agent + $query = $agent->applyVisibility($query); + // Aggregate constraints + foreach ($queues as $queue) { + $Q = $queue->getBasicQuery(); + $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), new SqlField('ticket_id')); + $query->aggregate(array( + "$prefix{$queue->id}" => SqlAggregate::COUNT($expr, true) + )); + } + + return $query->values()->one(); + } + + static function lookup($criteria) { + $queue = parent::lookup($criteria); + // Annoted cusom settings (if any) + if (($c=$queue->_config)) { + $queue->_settings = $c->getSettings() ?: array(); + $queue = AnnotatedModel::wrap($queue, + array_intersect_key($queue->_settings, + array_flip(array('sort_id', 'filter')))); + $queue->_config = $c; + } + + return $queue; } static function create($vars=false) { @@ -681,6 +898,13 @@ class SavedSearch extends CustomQueue { } } +class SavedSearch extends SavedQueue { + + function isSaved() { + return (!$this->__new__); + } +} + class AdhocSearch extends SavedSearch { @@ -717,13 +941,63 @@ extends SavedSearch { } } +// AdvacedSearchForm class AdvancedSearchForm extends SimpleForm { static $id = 1337; + + function getNumFieldsSelected() { + $selected = 0; + foreach ($this->getFields() as $F) { + if (substr($F->get('name'), -7) == '+search' + && $F->getClean()) + $selected += 1; + // Consider keyword searches + elseif ($F->get('name') == ':keywords' + && $F->getClean()) + $selected += 1; + } + return $selected; + } } // Advanced search special fields -class HelpTopicChoiceField extends ChoiceField { +class AdvancedSearchSelectionField extends ChoiceField { + + function hasIdValue() { + return false; + } + + function getSearchQ($method, $value, $name=false) { + switch ($method) { + case 'includes': + case '!includes': + $Q = new Q(); + if (count($value) > 1) + $Q->add(array("{$name}__in" => array_keys($value))); + else + $Q->add(array($name => key($value))); + + if ($method == '!includes') + $Q->negate(); + return $Q; + break; + // osTicket commonly uses `0` to represent an unset state, so + // the set and unset checks need to check for both not null and + // nonzero + case 'nset': + return new Q([$name => 0]); + case 'set': + return Q::not([$name => 0]); + default: + return parent::getSearchQ($method, $value, $name); + } + + } + +} + +class HelpTopicChoiceField extends AdvancedSearchSelectionField { function hasIdValue() { return true; } @@ -734,7 +1008,7 @@ class HelpTopicChoiceField extends ChoiceField { } require_once INCLUDE_DIR . 'class.dept.php'; -class DepartmentChoiceField extends ChoiceField { +class DepartmentChoiceField extends AdvancedSearchSelectionField { var $_choices = null; function getChoices($verbose=false) { @@ -764,6 +1038,7 @@ class DepartmentChoiceField extends ChoiceField { } } + class AssigneeChoiceField extends ChoiceField { function getChoices($verbose=false) { global $thisstaff; @@ -877,11 +1152,30 @@ class AssigneeChoiceField extends ChoiceField { function applyOrderBy($query, $reverse=false, $name=false) { $reverse = $reverse ? '-' : ''; - return $query->order_by("{$reverse}staff__firstname", - "{$reverse}staff__lastname", "{$reverse}team__name"); + return Staff::nsort($query, $reverse); } } +class AssignedField extends AssigneeChoiceField { + + function getSearchMethods() { + return array( + 'assigned' => __('assigned'), + '!assigned' => __('unassigned'), + ); + } + + function addToQuery($query, $name=false) { + return $query->values('staff_id', 'team_id'); + } + + function from_query($row, $name=false) { + return ($row['staff_id'] || $row['staff_id']) + ? __('Yes') : __('No'); + } + +} + /** * Simple trait which changes the SQL for "has a value" and "does not have a * value" to check for zero or non-zero. Useful for not nullable fields. @@ -902,11 +1196,11 @@ trait ZeroMeansUnset { } } -class AgentSelectionField extends ChoiceField { +class AgentSelectionField extends AdvancedSearchSelectionField { use ZeroMeansUnset; function getChoices($verbose=false) { - return Staff::getStaffMembers(); + return array('M' => __('Me')) + Staff::getStaffMembers(); } function toString($value) { @@ -920,31 +1214,52 @@ class AgentSelectionField extends ChoiceField { parent::toString($value); } - function applyOrderBy($query, $reverse=false, $name=false) { - global $cfg; + function getSearchQ($method, $value, $name=false) { + global $thisstaff; + // unpack me + if (isset($value['M']) && $thisstaff) { + $value[$thisstaff->getId()] = $thisstaff->getName(); + unset($value['M']); + } + + return parent::getSearchQ($method, $value, $name); + } + + function applyOrderBy($query, $reverse=false, $name=false) { $reverse = $reverse ? '-' : ''; - switch ($cfg->getAgentNameFormat()) { - case 'last': - case 'lastfirst': - case 'legal': - $query->order_by("{$reverse}staff__lastname", - "{$reverse}staff__firstname"); - break; + return Staff::nsort($query, $reverse); + } +} - default: - $query->order_by("{$reverse}staff__firstname", - "{$reverse}staff__lastname"); - } - return $query; +class DepartmentManagerSelectionField extends AgentSelectionField { + + function getChoices($verbose=false) { + return Staff::getStaffMembers(); + } + + function getSearchQ($method, $value, $name=false) { + return parent::getSearchQ($method, $value, 'dept__manager_id'); } } -class TeamSelectionField extends ChoiceField { - use ZeroMeansUnset; +class TeamSelectionField extends AdvancedSearchSelectionField { function getChoices($verbose=false) { - return Team::getTeams(); + return array('T' => __('One of my teams')) + Team::getTeams(); + } + + function getSearchQ($method, $value, $name=false) { + global $thisstaff; + + // Unpack my teams + if (isset($value['T']) && $thisstaff + && ($teams = $thisstaff->getTeams())) { + unset($value['T']); + $value = $value + array_flip($teams); + } + + return parent::getSearchQ($method, $value, $name); } function applyOrderBy($query, $reverse=false, $name=false) { @@ -953,7 +1268,7 @@ class TeamSelectionField extends ChoiceField { } } -class TicketStateChoiceField extends ChoiceField { +class TicketStateChoiceField extends AdvancedSearchSelectionField { function getChoices($verbose=false) { return array( 'open' => __('Open'), diff --git a/include/class.staff.php b/include/class.staff.php index 965b7d429910614015045bf49ead08db9e616237..0c35a0c1c4b5fb33f9b327a811d68d4aa624e4d4 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -359,7 +359,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { $depts = array(); if (($res=db_query($sql)) && db_num_rows($res)) { while(list($id)=db_fetch_row($res)) - $depts[] = $id; + $depts[] = (int) $id; } /* ORM method — about 2.0ms slower @@ -459,13 +459,21 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { return $this->_roles; } - function getRole($dept=null) { - $deptId = is_object($dept) ? $dept->getId() : $dept; + function getRole($dept=null, $assigned=false) { + + if (is_null($dept)) + return $this->role; + + if ((!$dept instanceof Dept) && !($dept=Dept::lookup($dept))) + return null; + + $deptId = $dept->getId(); $roles = $this->getRoles(); if (isset($roles[$deptId])) return $roles[$deptId]; - if ($this->usePrimaryRoleOnAssignment()) + // Default to primary role. + if ($assigned && $this->usePrimaryRoleOnAssignment()) return $this->role; // View only access @@ -483,6 +491,10 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { return false; } + function canSearchEverything() { + return $this->hasPerm(SearchBackend::PERM_EVERYTHING); + } + function canManageTickets() { return $this->hasPerm(Ticket::PERM_DELETE, false) || $this->hasPerm(Ticket::PERM_TRANSFER, false) @@ -534,8 +546,13 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { return ($teamId && in_array($teamId, $this->getTeams())); } - function canAccessDept($deptId) { - return ($deptId && in_array($deptId, $this->getDepts()) && !$this->isAccessLimited()); + function canAccessDept($dept) { + + if (!$dept instanceof Dept) + return false; + + return (!$this->isAccessLimited() + && in_array($dept->getId(), $this->getDepts())); } function getTeams() { @@ -543,7 +560,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { if (!isset($this->_teams)) { $this->_teams = array(); foreach ($this->teams as $team) - $this->_teams[] = $team->team_id; + $this->_teams[] = (int) $team->team_id; } return $this->_teams; @@ -559,7 +576,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { $assigned->add(array('thread__referrals__agent__staff_id' => $this->getId())); // -- Open and assigned to a team of mine - if ($teams = array_filter($this->getTeams())) { + if (($teams = array_filter($this->getTeams()))) { $assigned->add(array('team_id__in' => $teams)); $assigned->add(array('thread__referrals__team__team_id__in' => $teams)); } @@ -575,6 +592,10 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { return $visibility; } + function applyVisibility($query) { + return $query->filter($this->getTicketsVisibility()); + } + /* stats */ function resetStats() { $this->stats = array(); diff --git a/include/class.task.php b/include/class.task.php index 1a5860b0b87595d6a045058a511f79a78c903ef6..5e8ae0fe45c6af4d5fca52895d54e6eecd0f87f5 100644 --- a/include/class.task.php +++ b/include/class.task.php @@ -267,7 +267,7 @@ class Task extends TaskModel implements RestrictedAccess, Threadable { return false; // Check access based on department or assignment - if (!$staff->canAccessDept($this->getDeptId()) + if (!$staff->canAccessDept($this->getDept()) && $this->isOpen() && $staff->getId() != $this->getStaffId() && !$staff->isTeamMember($this->getTeamId())) @@ -279,7 +279,7 @@ class Task extends TaskModel implements RestrictedAccess, Threadable { return true; // Permission check requested -- get role. - if (!($role=$staff->getRole($this->getDeptId()))) + if (!($role=$staff->getRole($this->getDept()))) return false; // Check permission based on the effective role @@ -995,7 +995,7 @@ class Task extends TaskModel implements RestrictedAccess, Threadable { $pdf = new Task2PDF($this, $options); $name = 'Task-'.$this->getNumber().'.pdf'; - Http::download($name, 'application/pdf', $pdf->Output($name, 'S')); + Http::download($name, 'application/pdf', $pdf->output($name, 'S')); //Remember what the user selected - for autoselect on the next print. $_SESSION['PAPER_SIZE'] = $options['psize']; exit; @@ -1330,7 +1330,7 @@ class Task extends TaskModel implements RestrictedAccess, Threadable { $task->logEvent('created', null, $thisstaff); // Get role for the dept - $role = $thisstaff->getRole($task->dept_id); + $role = $thisstaff->getRole($task->getDept()); // Assignment $assignee = $vars['internal_formdata']['assignee']; if ($assignee diff --git a/include/class.team.php b/include/class.team.php index d6963eb9213272959dc5fe0da0d319b59376a784..a4fd4f8c7af6eb8cbac70a3d27f2f82488cc59ea 100644 --- a/include/class.team.php +++ b/include/class.team.php @@ -208,13 +208,19 @@ implements TemplateVariable { } $member->setAlerts($alerts); } - if (!$errors && $dropped) { + + if ($errors) + return false; + + $this->members->saveAll(); + if ($dropped) { $this->members ->filter(array('staff_id__in' => array_keys($dropped))) ->delete(); $this->members->reset(); } - return !$errors; + + return true; } function save($refetch=false) { diff --git a/include/class.thread.php b/include/class.thread.php index b06bd0432458f9e062c0a7877274da191d0fb26e..017d7db353c3fbd5740935138f2fc26222a03dcb 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -284,7 +284,7 @@ implements Searchable { function isReferred($to=null, $strict=false) { - if (is_null($to)) + if (is_null($to) || !$this->referrals) return ($this->referrals); switch (true) { @@ -299,12 +299,12 @@ implements Searchable { return false; // Referred to staff's department - if ($this->referrals->findFirst(array( + if ($to->getDepts() && $this->referrals->filter(array( 'object_id__in' => $to->getDepts(), 'object_type' => ObjectModel::OBJECT_TYPE_DEPT))) return true; // Referred to staff's team - if ($this->referrals->findFirst(array( + if ($to->getTeams() && $this->referrals->filter(array( 'object_id__in' => $to->getTeams(), 'object_type' => ObjectModel::OBJECT_TYPE_TEAM))) return true; @@ -778,6 +778,18 @@ implements TemplateVariable { ), ); + // Thread entry types + static protected $types = array( + 'M' => 'message', + 'R' => 'response', + 'N' => 'note', + 'B' => 'bccmessage', + ); + + function getTypeName() { + return self::$types[$this->type]; + } + function postEmail($mailinfo) { global $ost; @@ -1720,6 +1732,10 @@ implements TemplateVariable { static function getPermissions() { return self::$perms; } + + static function getTypes() { + return self::$types; + } } RolePermission::register(/* @trans */ 'Tickets', ThreadEntry::getPermissions()); diff --git a/include/class.ticket.php b/include/class.ticket.php index a6ed0915df61141e13fd89f210f7a69805cf785e..167703aaf49e2d1ae32d405f33210892b29b548c 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -207,9 +207,8 @@ implements RestrictedAccess, Threadable, Searchable { } function isReopenable() { - return ($this->getStatus()->isReopenable() - && $this->getDept()->allowsReopen() - && $this->getTopic()->allowsReopen()); + return ($this->getStatus()->isReopenable() && $this->getDept()->allowsReopen() + && ($this->getTopic() ? $this->getTopic()->allowsReopen() : null)); } function isClosed() { @@ -248,7 +247,7 @@ implements RestrictedAccess, Threadable, Searchable { if (!$this->isOpen()) return false; - if (!$to) + if (is_null($to)) return ($this->getStaffId() || $this->getTeamId()); switch (true) { @@ -276,30 +275,36 @@ implements RestrictedAccess, Threadable, Searchable { return null !== $this->getLock(); } + function getRole($staff) { + if (!$staff instanceof Staff) + return null; + + return $staff->getRole($this->getDept(), $this->isAssigned($staff)); + } + function checkStaffPerm($staff, $perm=null) { + // Must be a valid staff - if (!$staff instanceof Staff && !($staff=Staff::lookup($staff))) + if ((!$staff instanceof Staff) && !($staff=Staff::lookup($staff))) return false; - // Check access based on department or assignment - if (($staff->showAssignedOnly() - || !$staff->canAccessDept($this->getDeptId())) - // only open tickets can be considered assigned - && $this->isOpen() - && $staff->getId() != $this->getStaffId() - && !$staff->isTeamMember($this->getTeamId()) - && !$this->thread->isReferred($staff) - ) { + // check department access first + if (!$staff->canAccessDept($this->getDept()) + // no restrictions + && !$staff->isAccessLimited() + // check assignment + && !$this->isAssigned($staff) + // check referral + && !$this->thread->isReferred($staff)) return false; - } // At this point staff has view access unless a specific permission is // requested if ($perm === null) return true; - // Permission check requested -- get role. - if (!($role=$staff->getRole($this->getDeptId()))) + // Permission check requested -- get role if any + if (!($role=$this->getRole($staff))) return false; // Check permission based on the effective role @@ -962,7 +967,7 @@ implements RestrictedAccess, Threadable, Searchable { 'name' => "{$fid}_id", 'label' => __('Help Topic'), 'default' => $this->getTopicId(), - 'choices' => Topic::getHelpTopics() + 'choices' => Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED) )); break; case 'source': @@ -1022,16 +1027,22 @@ implements RestrictedAccess, Threadable, Searchable { } } - static function getMissingRequiredFields($ticket) { + //if ids passed, function returns only the ids of fields disabled by help topic + static function getMissingRequiredFields($ticket, $ids=false) { // Check for fields disabled by Help Topic $disabled = array(); - foreach ($ticket->entries as $entry) { - $extra = JsonDataParser::decode($entry->extra); + foreach (($ticket->getTopic() ? $ticket->getTopic()->forms : $ticket->entries) as $f) { + $extra = JsonDataParser::decode($f->extra); + if (!empty($extra['disable'])) $disabled[] = $extra['disable']; } + $disabled = !empty($disabled) ? call_user_func_array('array_merge', $disabled) : NULL; + if ($ids) + return $disabled; + $criteria = array( 'answers__field__flags__hasbit' => DynamicFormField::FLAG_ENABLED, 'answers__field__flags__hasbit' => DynamicFormField::FLAG_CLOSE_REQUIRED, @@ -1042,7 +1053,7 @@ implements RestrictedAccess, Threadable, Searchable { if ($disabled) array_push($criteria, Q::not(array('answers__field__id__in' => $disabled))); - return $ticket->getDynamicFields($criteria, $ticket); + return $ticket->getDynamicFields($criteria); } function getMissingRequiredField() { @@ -1237,7 +1248,7 @@ implements RestrictedAccess, Threadable, Searchable { function setStatus($status, $comments='', &$errors=array(), $set_closing_agent=true) { global $thisstaff; - if ($thisstaff && !($role = $thisstaff->getRole($this->getDeptId()))) + if ($thisstaff && !($role=$this->getRole($thisstaff))) return false; if ($status && is_numeric($status)) @@ -1603,7 +1614,6 @@ implements RestrictedAccess, Threadable, Searchable { if(get_class($recipient) == 'Collaborator') { if ($recipient->isCc()) { $collabsCc[] = $recipient->getEmail()->address; - $cnotice = $this->replaceVars($msg, array('recipient.name.first' => __('Collaborator'), 'recipient' => $recipient)); } else $collabsBcc[] = $recipient; @@ -1614,11 +1624,14 @@ implements RestrictedAccess, Threadable, Searchable { } } - foreach ($collabsBcc as $recipient) { - $notice = $this->replaceVars($msg, array('recipient' => $recipient)); - if ($posterEmail != $recipient->getEmail()->address) - $email->send($recipient, $notice['subj'], $notice['body'], $attachments, - $options); + //send bcc messages seperately for privacy + if ($collabsBcc) { + foreach ($collabsBcc as $recipient) { + $notice = $this->replaceVars($msg, array('recipient' => $recipient)); + if ($posterEmail != $recipient->getEmail()->address) + $email->send($recipient, $notice['subj'], $notice['body'], $attachments, + $options); + } } foreach ($collabsCc as $cc) { @@ -1628,14 +1641,33 @@ implements RestrictedAccess, Threadable, Searchable { $collaborators[] = $cc; } + //the ticket user is a recipient if ($owner->getEmail()->address != $poster->getEmail()->address && !in_array($owner->getEmail()->address, $skip)) - $collaborators[] = $owner->getEmail()->address; + $owner_recip = $owner->getEmail()->address; $collaborators['cc'] = $collaborators; //collaborator email sent out - $email->send('', $cnotice['subj'], $cnotice['body'], $attachments, - $options, $collaborators); + if ($collaborators['cc'] || $owner_recip) { + //say dear collaborator if the ticket user is not a recipient + if (!$owner_recip) { + $nameFormats = array_keys(PersonsName::allFormats()); + $names = array(); + foreach ($nameFormats as $key => $value) { + $names['recipient.name.' . $value] = __('Collaborator'); + } + $names = array_merge($names, array('recipient' => $recipient)); + $cnotice = $this->replaceVars($msg, $names); + } + + //otherwise address email to ticket user + else + $cnotice = $this->replaceVars($msg, array('recipient' => $owner)); + + //if the ticket user is a recipient, put them in to address otherwise, cc all recipients + $email->send($owner_recip ? $owner_recip : '', $cnotice['subj'], $cnotice['body'], $attachments, + $options, $collaborators); + } } function onMessage($message, $autorespond=true, $reopen=true) { @@ -1663,7 +1695,7 @@ implements RestrictedAccess, Threadable, Searchable { // Is agent on vacation ? && $staff->isAvailable() // Does the agent have access to dept? - && $staff->canAccessDept($dept->getId())) + && $staff->canAccessDept($dept)) $this->setStaffId($staff->getId()); else $this->setStaffId(0); // Clear assignment @@ -2059,10 +2091,14 @@ implements RestrictedAccess, Threadable, Searchable { 'label' => __('Create Date'), 'configuration' => array('fromdb' => true), )), - 'est_duedate' => new DatetimeField(array( + 'duedate' => new DatetimeField(array( 'label' => __('Due Date'), 'configuration' => array('fromdb' => true), )), + 'est_duedate' => new DatetimeField(array( + 'label' => __('SLA Due Date'), + 'configuration' => array('fromdb' => true), + )), 'reopened' => new DatetimeField(array( 'label' => __('Reopen Date'), 'configuration' => array('fromdb' => true), @@ -2095,9 +2131,20 @@ implements RestrictedAccess, Threadable, Searchable { )), 'isoverdue' => new BooleanField(array( 'label' => __('Overdue'), + 'descsearchmethods' => array( + 'set' => '%s', + 'nset' => 'Not %s' + ), )), 'isanswered' => new BooleanField(array( 'label' => __('Answered'), + 'descsearchmethods' => array( + 'set' => '%s', + 'nset' => 'Not %s' + ), + )), + 'isassigned' => new AssignedField(array( + 'label' => __('Assigned'), )), 'ip_address' => new TextboxField(array( 'label' => __('IP Address'), @@ -2920,47 +2967,40 @@ implements RestrictedAccess, Threadable, Searchable { $user = $this->getOwner(); if (($email=$email) && ($tpl = $dept->getTemplate()) - && ($msg=$tpl->getReplyMsgTemplate()) - ) { + && ($msg=$tpl->getReplyMsgTemplate())) { + $msg = $this->replaceVars($msg->asArray(), $variables + array('recipient' => $user) ); - $attachments = $cfg->emailAttachments()?$response->getAttachments():array(); - } - if ($vars['emailcollab'] == 1) { + $attachments = $cfg->emailAttachments() ? $response->getAttachments() : array(); //Cc collaborators - if($vars['ccs']) { - $collabsCc = array(); - $collabsCc[] = Collaborator::getCollabList($vars['ccs']); - $collabsCc['cc'] = $collabsCc; - $email->send($user, $msg['subj'], $msg['body'], $attachments, - $options, $collabsCc); - } - else { - $email->send($user, $msg['subj'], $msg['body'], $attachments, - $options); + $collabsCc = array(); + if ($vars['ccs'] && $vars['emailcollab']) { + $collabsCc[] = Collaborator::getCollabList($vars['ccs']); + $collabsCc['cc'] = $collabsCc[0]; } + $email->send($user, $msg['subj'], $msg['body'], $attachments, + $options, $collabsCc); + //Bcc Collaborators - if($vars['bccs']) { - foreach ($vars['bccs'] as $uid) { - $recipient = User::lookup($uid); - if (($bcctpl = $dept->getTemplate()) + if ($vars['bccs'] + && $vars['emailcollab'] + && ($bcctpl = $dept->getTemplate()) && ($bccmsg=$bcctpl->getReplyMsgTemplate())) { - $bccmsg = $this->replaceVars($bccmsg->asArray(), $variables + - array('recipient' => $user, 'recipient.name.first' => $recipient->getName()->getFirst()) - ); + foreach ($vars['bccs'] as $uid) { + if (!($recipient = User::lookup($uid))) + continue; - $email->send($recipient, $bccmsg['subj'], $bccmsg['body'], $attachments, - $options); + $extraVars = UsersName::getNameFormats($recipient, 'recipient'); + $extraVars = array_merge($extraVars, array('recipient' => $user)); + $msg = $this->replaceVars($bccmsg->asArray(), $variables + $extraVars); + + $email->send($recipient, $msg['subj'], $msg['body'], $attachments, $options); } - } } - } - else - $email->send($user, $msg['subj'], $msg['body'], $attachments, - $options); + } return $response; } @@ -3070,7 +3110,7 @@ implements RestrictedAccess, Threadable, Searchable { $pdf = new Ticket2PDF($this, $psize, $notes); $name = 'Ticket-'.$this->getNumber().'.pdf'; - Http::download($name, 'application/pdf', $pdf->Output($name, 'S')); + Http::download($name, 'application/pdf', $pdf->output($name, 'S')); //Remember what the user selected - for autoselect on the next print. $_SESSION['PAPER_SIZE'] = $psize; exit; @@ -3942,7 +3982,8 @@ implements RestrictedAccess, Threadable, Searchable { return false; if ($vars['deptId'] - && ($role = $thisstaff->getRole($vars['deptId'])) + && ($dept=Dept::lookup($vars['deptId'])) + && ($role = $thisstaff->getRole($dept)) && !$role->hasPerm(Ticket::PERM_CREATE) ) { $errors['err'] = sprintf(__('You do not have permission to create a ticket in %s'), __('this department')); @@ -4017,7 +4058,7 @@ implements RestrictedAccess, Threadable, Searchable { $vars['msgId']=$ticket->getLastMsgId(); // Effective role for the department - $role = $thisstaff->getRole($ticket->getDeptId()); + $role = $ticket->getRole($thisstaff); // post response - if any $response = null; @@ -4095,15 +4136,13 @@ implements RestrictedAccess, Threadable, Searchable { && ($bccmsg=$tpl->getNewTicketNoticeMsgTemplate()) && ($email=$dept->getEmail()) ) - $bccmsg = $ticket->replaceVars($bccmsg->asArray(), - array( - 'message' => $message, - 'signature' => $signature, - 'response' => ($response) ? $response->getBody() : '', - 'recipient' => $ticket->getOwner(), - 'recipient.name.first' => $recipient->getName()->getFirst(), - ) - ); + $extraVars = UsersName::getNameFormats($recipient, 'recipient'); + $extraVars = array_merge($extraVars, array( + 'message' => $message, + 'signature' => $signature, + 'response' => ($response) ? $response->getBody() : '', + 'recipient' => $ticket->getOwner())); + $bccmsg = $ticket->replaceVars($bccmsg->asArray(), $extraVars); $email->send($recipient, $bccmsg['subj'], $bccmsg['body'], $attachments, $options); diff --git a/include/class.topic.php b/include/class.topic.php index 84a1c9d47d85892d62ef4c44976ade79c33afcee..245b3c39f88535ab0afe34a24e8d189d126ca01e 100644 --- a/include/class.topic.php +++ b/include/class.topic.php @@ -191,8 +191,7 @@ implements TemplateVariable, Searchable { return $this->isActive(); } - function isActive() - { + function isActive() { return !!($this->flags & self::FLAG_ACTIVE); } diff --git a/include/class.upgrader.php b/include/class.upgrader.php index 87fb63c89be95b0cab01f8beb838a111e1b06841..562dcb27957c948a639942cf654fd1450e899d3c 100644 --- a/include/class.upgrader.php +++ b/include/class.upgrader.php @@ -362,6 +362,10 @@ class StreamUpgrader extends SetupWizard { if(!($max_time = ini_get('max_execution_time'))) $max_time = 300; //Apache/IIS defaults. + // Drop any model meta cache to ensure model changes do not cause + // crashes + ModelMeta::flushModelCache(); + // Apply up to five patches at a time foreach (array_slice($patches, 0, 5) as $patch) { //TODO: check time used vs. max execution - break if need be diff --git a/include/class.user.php b/include/class.user.php index dcd04b9c5d59f2bc6c02f4e54c8e928a26c647d5..083e0d5bf2b1853e82d6804a53641d353dbf8fd9 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -813,6 +813,16 @@ implements TemplateVariable { return $this; } + function getNameFormats($user, $type) { + $nameFormats = array(); + + foreach (PersonsName::allFormats() as $format => $func) { + $nameFormats[$type . '.name.' . $format] = $user->getName()->$func[1](); + } + + return $nameFormats; + } + function asVar() { return $this->__toString(); } diff --git a/include/client/faq.inc.php b/include/client/faq.inc.php index 9bc723a6e61c4dfbdf9571ce215c8f132e22b0e7..176ae429a5065d545fb4361faee019b015232971 100644 --- a/include/client/faq.inc.php +++ b/include/client/faq.inc.php @@ -43,7 +43,8 @@ $category=$faq->getCategory(); <strong><?php echo __('Attachments');?>:</strong> <?php foreach ($attachments as $att) { ?> <div> - <a href="<?php echo $att->file->getDownloadUrl(); ?>" class="no-pjax"> + <a href="<?php echo $att->file->getDownloadUrl(['id' => $att->getId()]); + ?>" class="no-pjax"> <i class="icon-file"></i> <?php echo Format::htmlchars($att->getFilename()); ?> </a> diff --git a/include/client/templates/thread-entry.tmpl.php b/include/client/templates/thread-entry.tmpl.php index fed92a5c657b6bba249f6f365cae8462b62c895d..e49b3b0389bf1c9d2f7c330ebac9039b536c0fc9 100644 --- a/include/client/templates/thread-entry.tmpl.php +++ b/include/client/templates/thread-entry.tmpl.php @@ -1,6 +1,6 @@ <?php global $cfg; -$entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note', 'B' => 'bccmessage'); +$entryTypes = ThreadEntry::getTypes(); $user = $entry->getUser() ?: $entry->getStaff(); if ($entry->staff && $cfg->hideStaffName()) $name = __('Staff'); @@ -11,13 +11,14 @@ if ($cfg->isAvatarsEnabled() && $user) $avatar = $user->getAvatar(); ?> <?php - if ($entryTypes[$entry->type] == 'note') { - $entryTypes[$entry->type] = 'bccmessage'; +$type = $entryTypes[$entry->type]; +if ($type == 'note') { + $type = 'bccmessage'; $entry->type = 'B'; - } +} ?> -<div class="thread-entry <?php echo $entryTypes[$entry->type]; ?> <?php if ($avatar) echo 'avatar'; ?>"> +<div class="thread-entry <?php echo $type; ?> <?php if ($avatar) echo 'avatar'; ?>"> <?php if ($avatar) { ?> <span class="<?php echo ($entry->type == 'M' || $entry->type == 'B') ? 'pull-left' : 'pull-right'; ?> avatar"> <?php echo $avatar; ?> @@ -60,7 +61,8 @@ if ($cfg->isAvatarsEnabled() && $user) ?> <span class="attachment-info"> <i class="icon-paperclip icon-flip-horizontal"></i> - <a class="no-pjax truncate filename" href="<?php echo $A->file->getDownloadUrl(); + <a class="no-pjax truncate filename" + href="<?php echo $A->file->getDownloadUrl(['id' => $A->getId()]); ?>" download="<?php echo Format::htmlchars($A->getFilename()); ?>" target="_blank"><?php echo Format::htmlchars($A->getFilename()); ?></a><?php echo $size;?> diff --git a/include/client/view.inc.php b/include/client/view.inc.php index bc39255ab0e02367b43f151e1e1b9a05c5c41bd7..fa48bdc3eaf2bb3aaf44ca37690fa64ff509eed3 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -208,7 +208,7 @@ foreach (AttachmentFile::objects()->filter(array( 'attachments__inline' => true, )) as $file) { $urls[strtolower($file->getKey())] = array( - 'download_url' => $file->getDownloadUrl(), + 'download_url' => $file->getDownloadUrl(['type' => 'H']), 'filename' => $file->name, ); } ?> diff --git a/include/i18n/en_US/department.yaml b/include/i18n/en_US/department.yaml index 3de7de70b6a2965679f39a870e80460516d04206..aa0e6ddc71d8b62befb546d69e390cce165a2f93 100644 --- a/include/i18n/en_US/department.yaml +++ b/include/i18n/en_US/department.yaml @@ -3,6 +3,7 @@ # # Fields: # id - (int:optional) id number in the database +# flags - (bitmask: 0x0004 Active 0x0008 Archived) # name - (string) Short name of the department # signature - (string) Descriptive name of the department # @@ -17,6 +18,7 @@ signature: | Support Department ispublic: 1 + flags: 0x0004 group_membership: 1 - id: 2 @@ -24,6 +26,7 @@ signature: | Sales and Customer Retention ispublic: 1 + flags: 0x0004 sla_id: 1 group_membership: 1 @@ -32,4 +35,5 @@ signature: | Maintenance Department ispublic: 0 + flags: 0x0004 group_membership: 0 diff --git a/include/i18n/en_US/help_topic.yaml b/include/i18n/en_US/help_topic.yaml index 76faba0c0e3638a0dc4b4b6b8b8f6deb9262dc70..acf674a3b0cd9f0e8162955e4dda8b1346acf595 100644 --- a/include/i18n/en_US/help_topic.yaml +++ b/include/i18n/en_US/help_topic.yaml @@ -4,7 +4,7 @@ # Fields: # id - (int:optional) id number in the database # topic - (string) descriptive name of the help topic -# isactive - (bool:0|1) if the help topic should be initially usable +# flags - (bitmask: Active | Disabled | Archived) # ispublic - (bool:0|1) true or false if end users should be able to see the # help topic. In other words, true or false if the help topic is _not_ # for internal use only @@ -19,7 +19,7 @@ # --- - topic_id: 1 - isactive: 1 + flags: 0x02 ispublic: 1 priority_id: 2 forms: [2] @@ -27,7 +27,8 @@ notes: | Questions about products or services -- isactive: 1 +- topic_id: 2 + flags: 0x02 ispublic: 1 priority_id: 1 forms: [2] @@ -36,7 +37,7 @@ Tickets that primarily concern the sales and billing departments - topic_id: 10 - isactive: 1 + flags: 0x02 ispublic: 1 dept_id: 3 priority_id: 2 @@ -46,7 +47,7 @@ Product, service, or equipment related issues - topic_pid: 10 - isactive: 1 + flags: 0x02 ispublic: 1 sla_id: 1 priority_id: 3 diff --git a/include/i18n/en_US/queue.yaml b/include/i18n/en_US/queue.yaml index 0d572c5c7d3ea24779aceaa3b6fc3ae07312ff2b..ab2b1a4bbb3453e14c7843b81a75d65751b94d9e 100644 --- a/include/i18n/en_US/queue.yaml +++ b/include/i18n/en_US/queue.yaml @@ -75,7 +75,7 @@ parent_id: 1 flags: 0x03 root: T - sort: 4 + sort: 2 config: '[["isanswered","set",null]]' columns: - column_id: 1 @@ -114,8 +114,6 @@ - sort_id: 3 - sort_id: 4 - - - id: 3 title: My Tickets parent_id: 0 @@ -205,55 +203,12 @@ - sort_id: 1 - sort_id: 2 -- title: Unassigned - parent_id: 1 - flags: 0x2b - root: T - sort: 1 - config: '[["assignee","!assigned",null]]' - columns: - - column_id: 1 - bits: 1 - sort: 1 - sort: 1 - width: 100 - heading: Ticket - - column_id: 10 - bits: 1 - sort: 1 - sort: 2 - width: 150 - heading: Last Update - - column_id: 3 - bits: 1 - sort: 1 - sort: 3 - width: 300 - heading: Subject - - column_id: 4 - bits: 1 - sort: 1 - sort: 4 - width: 185 - heading: From - - column_id: 5 - bits: 1 - sort: 1 - sort: 5 - width: 85 - heading: Priority - - column_id: 11 - bits: 1 - sort: 1 - sort: 6 - width: 160 - heading: Department - -- title: Assigned +- id: 5 + title: Assigned parent_id: 1 flags: 0x03 root: T - sort: 2 + sort: 3 config: '[["assignee","assigned",null]]' columns: - column_id: 1 @@ -287,11 +242,12 @@ width: 160 heading: Assigned To -- title: Overdue +- id: 6 + title: Overdue parent_id: 1 flags: 0x2b root: T - sort: 3 + sort: 4 sort_id: 4 config: '[["isoverdue","set",null]]' columns: @@ -300,7 +256,7 @@ sort: 1 width: 100 heading: Ticket - - column_id: 10 + - column_id: 9 bits: 1 sort: 1 sort: 9 @@ -330,18 +286,3 @@ sort: 6 width: 160 heading: Assigned To - -- title: Personal Tickets - parent_id: 3 - flags: 0x2b - root: T - sort: 1 - config: '{"criteria":[["assignee","includes",{"M":"Me"}]]}' - -- title: Teams Tickets - parent_id: 3 - flags: 0x2b - root: T - sort: 2 - config: '{"criteria":[["team_id","set",null]],"conditions":[]}' - filter: team_id diff --git a/include/i18n/en_US/queue_column.yaml b/include/i18n/en_US/queue_column.yaml index 1c1d011f5df1cef9b1cc08eb3f002c7124778113..6f7419a4af0e0074293b957b07f813668d1889e4 100644 --- a/include/i18n/en_US/queue_column.yaml +++ b/include/i18n/en_US/queue_column.yaml @@ -92,7 +92,8 @@ - id: 9 name: "Due Date" - primary: "est_duedate" + primary: "duedate" + secondary: "est_duedate" filter: "date:human" truncate: "wrap" annotations: "[]" diff --git a/include/i18n/en_US/templates/email/ticket.autoreply.yaml b/include/i18n/en_US/templates/email/ticket.autoreply.yaml index 39e8e4aa3fc7a1e56f1903096d5fa7a7a91b0cd9..824e2e5e1e862ce3fd238ae1d94c033a0354e323 100644 --- a/include/i18n/en_US/templates/email/ticket.autoreply.yaml +++ b/include/i18n/en_US/templates/email/ticket.autoreply.yaml @@ -33,7 +33,7 @@ body: | <hr> <div style="color: rgb(127, 127, 127); font-size: small;"><em>We hope this response has sufficiently answered your questions. If you wish to - provide additional comments or informatione, please reply to this email + provide additional comments or information, please reply to this email or <a href="%{recipient.ticket_link}"><span style="color: rgb(84, 141, 212);" >login to your account</span></a> for a complete archive of your support requests.</em></div> diff --git a/include/mysqli.php b/include/mysqli.php index 998d286590d08ff8f8ea6c78ad072b28ee7600f2..efb459f334b9d8a6ffc7899fca51caf08d2f25cc 100644 --- a/include/mysqli.php +++ b/include/mysqli.php @@ -234,7 +234,7 @@ function db_fetch_field($res) { return ($res) ? $res->fetch_field() : NULL; } -function db_assoc_array($res, $mode=false) { +function db_assoc_array($res, $mode=MYSQLI_ASSOC) { $result = array(); if($res && db_num_rows($res)) { while ($row=db_fetch_array($res, $mode)) diff --git a/include/staff/faq-view.inc.php b/include/staff/faq-view.inc.php index a5dc4e56010c41ca059d7701d1bd19345c533661..531fbc2cd44bb603b860f5de9e4e833bcbaf2a88 100644 --- a/include/staff/faq-view.inc.php +++ b/include/staff/faq-view.inc.php @@ -41,7 +41,8 @@ if ($thisstaff->hasPerm(FAQ::PERM_MANAGE)) { ?> <?php foreach ($attachments as $att) { ?> <div> <i class="icon-paperclip pull-left"></i> - <a target="_blank" href="<?php echo $att->file->getDownloadUrl(); ?>" + <a target="_blank" href="<?php echo $att->file->getDownloadUrl(['id' => + $att->getId()]); ?>" class="attachment no-pjax"> <?php echo Format::htmlchars($att->getFilename()); ?> </a> diff --git a/include/staff/filter.inc.php b/include/staff/filter.inc.php index ba0c06ce58ccdd35b8dadcdc9001362477da7b83..5816b4bf388cb147566688be145c9d5f8d72e6c8 100644 --- a/include/staff/filter.inc.php +++ b/include/staff/filter.inc.php @@ -240,18 +240,16 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); if ($filter) { foreach ($filter->getActions() as $A) { $_warn = ''; $existing[] = $A->type; + $config = JsonDataParser::parse($A->configuration); if($A->type == 'dept') { $errors['topic_id'] = ''; - // $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); - $dept_config = $A->parseConfiguration($_POST); - $dept = Dept::lookup($dept_config['dept_id']); + $dept = Dept::lookup($config['dept_id']); if($dept && !$dept->isActive()) $_warn = sprintf(__('%s must be active'), __('Department')); } elseif($A->type == 'topic') { $errors['dept_id'] = ''; - $topic_config = $A->parseConfiguration($_POST); - $topic = Topic::lookup($topic_config['topic_id']); + $topic = Topic::lookup($config['topic_id']); if($topic && !$topic->isActive()) $_warn = sprintf(__('%s must be active'), __('Help Topic')); } diff --git a/include/staff/queue.inc.php b/include/staff/queue.inc.php index bf41bd1a48b42204c95fbec0b7fda987229c032a..8506fbee9d1ce3d78b429fe74c969f7f2771f158 100644 --- a/include/staff/queue.inc.php +++ b/include/staff/queue.inc.php @@ -57,11 +57,11 @@ else { <table class="table"> <td style="width:60%; vertical-align:top"> <div><strong><?php echo __('Queue Name'); ?>:</strong></div> - <input type="text" name="name" value="<?php + <input type="text" name="queue-name" value="<?php echo Format::htmlchars($queue->getName()); ?>" style="width:100%" /> - <br/> + <div class="error"><?php echo $errors['queue-name']; ?></div> <br/> <div><strong><?php echo __("Queue Search Criteria"); ?></strong></div> <label class="checkbox" style="line-height:1.3em"> @@ -120,10 +120,8 @@ else { if ($queue->parent && ($qf = $queue->parent->getQuickFilterField())) echo sprintf(' (%s)', $qf->getLabel()); ?> —</option> -<?php foreach (CustomQueue::getSearchableFields('Ticket') as $path=>$f) { +<?php foreach ($queue->getSupportedFilters() as $path => $f) { list($label, $field) = $f; - if (!$field->supportsQuickFilter()) - continue; ?> <option value="<?php echo $path; ?>" <?php if ($path == $queue->filter) echo 'selected="selected"'; ?> @@ -314,8 +312,8 @@ var Q = setInterval(function() { }(); </script> </table> - </div> - + </div> + <div class="hidden tab_content" id="preview-tab"> <div id="preview"> </div> @@ -339,7 +337,7 @@ var Q = setInterval(function() { <div class="hidden tab_content" id="conditions-tab"> <div style="margin-bottom: 15px"><?php echo __("Conditions are used to change the view of the data in a row based on some conditions of the data. For instance, a column might be shown bold if some condition is met."); - ?> <?php echo __("These conditions apply to an entire row in the queue."); + ?> <?php echo __("These conditions apply to an entire row in the queue."); ?></div> <div class="conditions"> <?php diff --git a/include/staff/team.inc.php b/include/staff/team.inc.php index 254d1559d6407d5e15d522185e2c62778ff567f4..9362db27d55bd3022505be2f6900907e3dd21a81 100644 --- a/include/staff/team.inc.php +++ b/include/staff/team.inc.php @@ -231,7 +231,7 @@ $(document).on('click', 'a.drop-membership', function() { }); <?php -if ($team) { +if ($team && $team->members) { foreach ($team->members->sort(function($a) { return $a->staff->getName(); }) as $member) { echo sprintf('addMember(%d, %s, %d, %s);', $member->staff_id, diff --git a/include/staff/templates/advanced-search-criteria.tmpl.php b/include/staff/templates/advanced-search-criteria.tmpl.php index 136c7d139bb7a35d382757fe2e534230f79d997a..9348cf79be9a81e3cb762f1bcaf96604acedf437 100644 --- a/include/staff/templates/advanced-search-criteria.tmpl.php +++ b/include/staff/templates/advanced-search-criteria.tmpl.php @@ -1,9 +1,29 @@ <?php -foreach ($form->errors(true) ?: array() as $message) { - ?><div class="error-banner"><?php echo $message;?></div><?php +// Display errors if any +foreach ($form->errors(true) ?: array() as $message) + echo sprintf('<div class="error-banner">%s</div>', + Format::htmlchars($message)); +// Current search fields. +$info = $search->getSearchFields($form) ?: array(); +if (($search instanceof SavedQueue) && !$search->checkOwnership($thisstaff)) { + $matches = $search->getSupplementalMatches(); + // Uneditable core criteria for the queue + echo '<div class="faded">'. nl2br(Format::htmlchars($search->describeCriteria())). + '</div><br>'; + // Show any supplemental filters + if ($matches && count($info)) { + ?> + <div id="ticket-flags" + style="padding:5px; border-top: 1px dotted #777;"> + <strong><i class="icon-caret-down"></i> <?php + echo __('Supplemental Filters'); ?></strong> + </div> +<?php + } +} else { + $matches = $search->getSupportedMatches(); } -$info = $search->getSearchFields($form); foreach (array_keys($info) as $F) { ?><input type="hidden" name="fields[]" value="<?php echo $F; ?>"/><?php } @@ -51,8 +71,7 @@ foreach ($form->getFields() as $name=>$field) { $this.closest('.adv-search-field-container').find('.adv-search-field-body').slideDown('fast'); $this.find('span.faded').hide(); $this.find('i').removeClass('icon-caret-right').addClass('icon-caret-down'); - return false; -"><i class="icon-caret-right"></i> + return false; "><i class="icon-caret-right"></i> <span class="faded"><?php echo $search->describeField($info[$name]); ?></span> </a> </span> @@ -62,21 +81,20 @@ foreach ($form->getFields() as $name=>$field) { } ?> </fieldset> <?php if ($name[0] == ':' && substr($name, -7) == '+search') { - list($N,) = explode('+', $name, 2); -?> + list($N,) = explode('+', $name, 2); ?> <input type="hidden" name="fields[]" value="<?php echo $N; ?>"/> <?php } } if (!$first_field) echo '</div></div>'; -?> + +if ($matches && is_array($matches)) { ?> <div id="extra-fields"></div> <hr/> <i class="icon-plus-sign"></i> <select id="search-add-new-field" name="new-field" style="max-width: 300px;"> <option value="">— <?php echo __('Add Other Field'); ?> —</option> <?php -if (is_array($matches)) { foreach ($matches as $path => $F) { # Skip fields already listed above the drop-down if (isset($already_listed[$path])) @@ -86,7 +104,7 @@ foreach ($matches as $path => $F) { if (isset($state[$path])) echo 'disabled="disabled"'; ?>><?php echo $label; ?></option> <?php } -} ?> +?> </select> <script> $(function() { @@ -100,9 +118,12 @@ $(function() { if (!json.success) return false; $(that).find(':selected').prop('disabled', true); + $(that).find('option:eq("")').prop('selected', true); $('#extra-fields').append($(json.html)); } }); }); }); </script> +<?php +} ?> diff --git a/include/staff/templates/advanced-search.tmpl.php b/include/staff/templates/advanced-search.tmpl.php index 71d2deedcb0314a1dd4355bad643fb42a68d363d..0fe2b99dcb420a7d6297e18e8c8426afbed7b727 100644 --- a/include/staff/templates/advanced-search.tmpl.php +++ b/include/staff/templates/advanced-search.tmpl.php @@ -1,11 +1,15 @@ <?php +global $thisstaff; + $parent_id = $_REQUEST['parent_id'] ?: $search->parent_id; if ($parent_id - && (!($parent = CustomQueue::lookup($parent_id))) + && is_numeric($parent_id) + && (!($parent = SavedQueue::lookup($parent_id))) ) { $parent_id = 0; } +$editable = $search->checkOwnership($thisstaff); $queues = array(); foreach (CustomQueue::queues() as $q) $queues[$q->id] = $q->getFullName(); @@ -26,10 +30,21 @@ if ($info['error']) { echo sprintf('<p id="msg_warning">%s</p>', $info['warn']); } elseif ($info['msg']) { echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); -} ?> -<form action="#tickets/search" method="post" name="search" id="advsearch" +} + +// Form action +$action = '#tickets/search'; +if ($search->isSaved() && $search->getId()) + $action .= sprintf('/%s/save', $search->getId()); +elseif (!$search instanceof AdhocSearch) + $action .= '/save'; +?> +<form action="<?php echo $action; ?>" method="post" name="search" id="advsearch" class="<?php echo ($search->isSaved() || $parent) ? 'savedsearch' : 'adhocsearch'; ?>"> <input type="hidden" name="id" value="<?php echo $search->getId(); ?>"> +<?php +if ($editable) { + ?> <div class="flex row"> <div class="span12"> <select id="parent" name="parent_id" > @@ -43,10 +58,16 @@ foreach ($queues as $id => $name) { </select> </div> </div> +<?php +} ?> <ul class="clean tabs"> <li class="active"><a href="#criteria"><i class="icon-search"></i> <?php echo __('Criteria'); ?></a></li> <li><a href="#columns"><i class="icon-columns"></i> <?php echo __('Columns'); ?></a></li> - <li><a href="#fields"><i class="icon-download"></i> <?php echo __('Export'); ?></a></li> + <?php + if ($search->isSaved()) { ?> + <li><a href="#settings"><i class="icon-cog"></i> <?php echo __('Settings'); ?></a></li> + <?php + } ?> </ul> <div class="tab_content" id="criteria"> @@ -68,51 +89,80 @@ foreach ($queues as $id => $name) { </div> </div> <input type="hidden" name="a" value="search"> - <?php include STAFFINC_DIR . 'templates/advanced-search-criteria.tmpl.php'; ?> + <?php + include STAFFINC_DIR . 'templates/advanced-search-criteria.tmpl.php'; + ?> </div> </div> </div> -<div class="tab_content hidden" id="columns" style="overflow-y: auto; -height:auto;"> +<div class="tab_content hidden" id="columns"> <?php include STAFFINC_DIR . "templates/queue-columns.tmpl.php"; ?> </div> -<div class="tab_content hidden" id="fields"> +<?php +if ($search->isSaved()) { ?> +<div class="tab_content hidden" id="settings"> <?php - include STAFFINC_DIR . "templates/queue-fields.tmpl.php"; ?> + include STAFFINC_DIR . "templates/savedqueue-settings.tmpl.php"; + ?> </div> - <?php - $save = (($parent && !$search->isSaved()) || $errors); ?> +<?php +} else { // Not saved. + $save = (($parent && !$search->isSaved()) || isset($_POST['queue-name'])); +?> +<div> <div style="margin-top:10px;"><a href="#" id="save"><i class="icon-caret-<?php echo $save ? 'down' : 'right'; ?>"></i> <span><?php echo __('Save Search'); ?></span></a></div> <div id="save-changes" class="<?php echo $save ? '' : 'hidden'; ?>" style="padding:5px; border-top: 1px dotted #777;"> - <div><input name="name" type="text" size="40" - value="<?php echo $search->isSaved() ? Format::htmlchars($search->getName()) : ''; ?>" + <div><input name="queue-name" type="text" size="40" + value="<?php echo Format::htmlchars($search->isSaved() ? $search->getName() : + $_POST['queue-name']); ?>" placeholder="<?php echo __('Search Title'); ?>"> + <?php + if ($search instanceof AdhocSearch && !$search->isSaved()) { ?> <span class="buttons"> - <button class="button" type="button" name="save" + <button class="save button" type="button" name="save-search" value="save"><i class="icon-save"></i> <?php echo $search->id ? __('Save Changes') : __('Save'); ?></button> </span> + <?php + } ?> </div> - <div class="error" id="name-error"><?php echo Format::htmlchars($errors['name']); ?></div> + <div class="error" id="name-error"><?php echo + Format::htmlchars($errors['queue-name']); ?></div> </div> + </div> +<?php +} ?> <hr/> <div> <p class="full-width"> <span class="buttons pull-left"> - <input type="reset" id="reset" value="<?php echo __('Reset'); ?>"> - <input type="button" name="cancel" class="close" - value="<?php echo __('Cancel'); ?>"> + <input type="button" name="cancel" class="close" value="<?php echo __('Cancel'); ?>"> + <?php + if ($search->isSaved()) { ?> + <input type="button" name="done" class="done" value="<?php echo + __('Done'); ?>" > + <?php + } ?> </span> <span class="buttons pull-right"> + <?php + if (!$search instanceof AdhocSearch) { ?> + <button class="save button" type="submit" name="save" value="save" + id="do_save"><i class="icon-save"></i> + <?php echo __('Save'); ?></button> + <?php + } else { ?> <button class="button" type="submit" name="submit" value="search" id="do_search"><i class="icon-search"></i> <?php echo __('Search'); ?></button> + <?php + } ?> </span> </p> </div> @@ -185,32 +235,45 @@ height:auto;"> return false; }); - $('form.savedsearch').on('keyup change paste', 'input, select, textarea', function() { - var form = $(this).closest('form'); - $this = $('#save-changes', form); - if ($this.is(":hidden")) - $this.fadeIn(); - $('a#save').find('i').removeClass('icon-caret-right').addClass('icon-caret-down'); - $('button[name=save]', form).addClass('save pending'); - $('div.error', form).html(''); + $('form#advsearch').on('keyup change paste', 'input, select, textarea', function() { + + var form = $(this).closest('form'); + $this = $('#save-changes', form); + $('button.save', form).addClass('save pending'); + $('div.error, div.error-banner', form).html('').hide(); }); $(document).on('click', 'form#advsearch input#reset', function(e) { var f = $(this).closest('form'); - $('button[name=save]', f).removeClass('save pending'); + $('button.save', f).removeClass('save pending'); $('div#save-changes', f).hide(); }); - $('button[name=save]').click(function() { + $('button[name=save-search]').click(function() { var $form = $(this).closest('form'); var id = parseInt($('input[name=id]', $form).val(), 10) || 0; - var action = '#tickets/search'; - if (id > 0) - action = action + '/'+id; + var name = $('input[name=queue-name]', $form).val(); + if (name.length) { + var action = '#tickets/search'; + if (id > 0) + action = action + '/'+id; + $form.prop('action', action+'/save'); + $form.submit(); + } else { + $('div#name-error', $form).html('<?php echo __('Name required'); + ?>').show(); + } - $form.prop('action', action+'/save'); - $form.submit(); + return false; }); + $('input.done').click(function() { + var $form = $(this).closest('form'); + var id = parseInt($('input[name=id]', $form).val(), 10) || 0; + if ($('button.save', $form).hasClass('pending')) + alert('Unsaved Changes - save or cancel to discard!'); + else + window.location.href = 'tickets.php?queue='+id; + }); }(); </script> diff --git a/include/staff/templates/collaborators.tmpl.php b/include/staff/templates/collaborators.tmpl.php index 7f80a5804053f2c9b9ecf23d9880f4d5555f7b75..1a50164f1f78421050098dfef38f89de93d9cf0b 100644 --- a/include/staff/templates/collaborators.tmpl.php +++ b/include/staff/templates/collaborators.tmpl.php @@ -65,7 +65,7 @@ if(($users=$thread->getCollaborators())) {?> ?> <td> <div><a class="collaborator" id="addcollaborator" - href="#thread/<?php echo $thread->getId(); ?>/add-collaborator" + href="#thread/<?php echo $thread->getId(); ?>/add-collaborator/addcc" ><i class="icon-plus-sign"></i> <?php echo __('Add Collaborator'); ?></a></div> </td> </table> diff --git a/include/staff/templates/note.tmpl.php b/include/staff/templates/note.tmpl.php index f36f1d81a38fdb3ff33861d4fe22f54953bdf10a..4ceae8f1bd7d68655cdef2b451691fb45e60a701 100644 --- a/include/staff/templates/note.tmpl.php +++ b/include/staff/templates/note.tmpl.php @@ -7,7 +7,8 @@ </div> <div class="header-right"> <?php - echo $note->getStaff()->getName(); +$staff = $note->getStaff(); +echo $staff ? $staff->getName() : _('Staff'); if (isset($show_options) && $show_options) { ?> <div class="options no-pjax"> <a href="#" class="action edit-note" title="edit"><i class="icon-pencil"></i></a> diff --git a/include/staff/templates/queue-columns.tmpl.php b/include/staff/templates/queue-columns.tmpl.php index 233a7c411defb2f853a22d3563502719efa4f262..feaf7cd4ef6163d89d99289bd23b294c01515540 100644 --- a/include/staff/templates/queue-columns.tmpl.php +++ b/include/staff/templates/queue-columns.tmpl.php @@ -1,3 +1,4 @@ +<div style="overflow-y: auto; height:auto; max-height: 350px;"> <table class="table"> <?php if ($queue->parent) { ?> @@ -12,24 +13,22 @@ if ($queue->parent) { ?> </td> </tr> </tbody> -<?php } - // Adhoc Advanced search does not have customizable columns, but saved - // ones do - elseif ($queue->__new__) { ?> +<?php } elseif ($queue instanceof SavedQueue) { ?> <tbody> <tr> <td colspan="3"> <input type="checkbox" name="inherit-columns" <?php - if (count($queue->columns) == 0) echo 'checked="checked"'; - if ($queue instanceof SavedSearch) echo 'disabled="disabled"'; ?> - onchange="javascript:$(this).closest('table').find('.if-not-inherited').toggle(!$(this).prop('checked'));" /> + if ($queue->useStandardColumns()) echo 'checked="checked"'; + if ($queue instanceof SavedSearch && $queue->__new__) echo 'disabled="disabled"'; ?> + onchange="javascript:$(this).closest('table').find('.if-not-inherited').toggle(!$(this).prop('checked')); + $(this).closest('table').find('.standard-columns').toggle($(this).prop('checked'));" /> <?php echo __('Use standard columns'); ?> <br /><br /> </td> </tr> </tbody> <?php } -$hidden_cols = $queue->inheritColumns() || count($queue->columns) === 0; +$hidden_cols = $queue->inheritColumns() || $queue->useStandardColumns(); ?> <tbody class="if-not-inherited <?php if ($hidden_cols) echo 'hidden'; ?>"> <tr class="header"> @@ -92,17 +91,34 @@ $hidden_cols = $queue->inheritColumns() || count($queue->columns) === 0; </td> </tr> </tbody> + <tbody class="standard-columns <?php if (!$hidden_cols) echo 'hidden'; ?>"> + <?php + foreach ($queue->getStandardColumns() as $c) { ?> + <tr> + <td nowrap><?php echo Format::htmlchars($c->heading); ?></td> + <td nowrap><?php echo Format::htmlchars($c->name); ?></td> + <td> </td> + </tr> + <?php + } ?> + </tbody> </table> - +</div> <script> +function() { +$('[name=inherit-columns]').on('click', function() { + $('.standard-columns').toggle(); +}); var Q = setInterval(function() { if ($('#append-column').length == 0) return; clearInterval(Q); var addColumn = function(colid, info) { - if (!colid) return; + + if (!colid || $('tr#column-'+colid).length) + return; + var copy = $('#column-template').clone(), name_prefix = 'columns[' + colid + ']'; info['column_id'] = colid; @@ -119,7 +135,7 @@ var Q = setInterval(function() { $this.attr('name', name_prefix + '[' + name + ']'); }); copy.find('span').text(info['name']); - copy.attr('id', '').show().insertBefore($('#column-template')); + copy.attr('id', 'column-'+colid).show().insertBefore($('#column-template')); copy.removeClass('hidden'); if (info['trans'] !== undefined) { var input = copy.find('input[data-translate-tag]') diff --git a/include/staff/templates/queue-savedsearches-nav.tmpl.php b/include/staff/templates/queue-savedsearches-nav.tmpl.php index d6c57fdaa6d4569199f6dfeb2ebe41f809568e17..4899b794a2edce13e0cc6009f6f23beb883cb1e4 100644 --- a/include/staff/templates/queue-savedsearches-nav.tmpl.php +++ b/include/staff/templates/queue-savedsearches-nav.tmpl.php @@ -18,7 +18,6 @@ <!-- Start Dropdown and child queues --> <?php foreach ($searches->findAll(array( 'staff_id' => $thisstaff->getId(), - 'parent_id' => 0, Q::not(array( 'flags__hasbit' => CustomQueue::FLAG_PUBLIC )) diff --git a/include/staff/templates/queue-tickets.tmpl.php b/include/staff/templates/queue-tickets.tmpl.php index c03cfacdb2de879337a7640926d3c5acd7f880fd..37d7cced74789b85fd6aa1f6cf6b0dc720eacc30 100644 --- a/include/staff/templates/queue-tickets.tmpl.php +++ b/include/staff/templates/queue-tickets.tmpl.php @@ -3,9 +3,10 @@ // $tickets - <QuerySet> with all columns and annotations necessary to // render the full page + // Impose visibility constraints // ------------------------------------------------------------ -if (!($queue->ignoreVisibilityConstraints())) +if (!$queue->ignoreVisibilityConstraints($thisstaff)) $tickets->filter($thisstaff->getTicketsVisibility()); // Make sure the cdata materialized view is available @@ -121,62 +122,37 @@ return false;"> <div class="pull-left flush-left"> <h2><a href="<?php echo $refresh_url; ?>" title="<?php echo __('Refresh'); ?>"><i class="icon-refresh"></i> <?php echo - $queue->getName(); ?></a></h2> + $queue->getName(); ?></a> + <?php + if (($crit=$queue->getSupplementalCriteria())) + echo sprintf('<i class="icon-filter" + data-placement="bottom" data-toggle="tooltip" + title="%s"></i> ', + Format::htmlchars($queue->describeCriteria($crit))); + ?> + </h2> </div> <div class="configureQ"> <i class="icon-cog"></i> <div class="noclick-dropdown anchor-left"> <ul> -<?php -if ($queue->isPrivate()) { ?> <li> <a class="no-pjax" href="#" data-dialog="ajax.php/tickets/search/<?php echo urlencode($queue->getId()); ?>"><i - class="icon-fixed-width icon-save"></i> - <?php echo __('Edit'); ?></a> - </li> -<?php } -else { - if ($thisstaff->isAdmin()) { ?> - <li> - <a class="no-pjax" - href="queues.php?id=<?php echo $queue->id; ?>"><i class="icon-fixed-width icon-pencil"></i> <?php echo __('Edit'); ?></a> </li> -<?php } -# Anyone has permission to create personal sub-queues -?> <li> <a class="no-pjax" href="#" - data-dialog="ajax.php/tickets/search?parent_id=<?php - echo $queue->id; ?>"><i + data-dialog="ajax.php/tickets/search/create?pid=<?php + echo $queue->getId(); ?>"><i class="icon-fixed-width icon-plus-sign"></i> - <?php echo __('Add Personal Queue'); ?></a> - </li> -<?php -} -if ($thisstaff->isAdmin()) { ?> - <li> - <a class="no-pjax" - href="queues.php?a=sub&id=<?php echo $queue->id; ?>"><i - class="icon-fixed-width icon-level-down"></i> <?php echo __('Add Sub Queue'); ?></a> </li> - <li> - <a class="no-pjax" - href="queues.php?a=clone&id=<?php echo $queue->id; ?>"><i - class="icon-fixed-width icon-copy"></i> - <?php echo __('Clone'); ?></a> - </li> -<?php } -if ( - $queue->id > 0 - && ( - ($thisstaff->isAdmin() && $queue->parent_id) - || $queue->isPrivate() -)) { ?> +<?php + +if ($queue->id > 0 && $queue->isOwner($thisstaff)) { ?> <li class="danger"> <a class="no-pjax confirm-action" href="#" data-dialog="ajax.php/queue/<?php diff --git a/include/staff/templates/savedqueue-settings.tmpl.php b/include/staff/templates/savedqueue-settings.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..42d36d57bf1006021c67e219c57e357a83cf5568 --- /dev/null +++ b/include/staff/templates/savedqueue-settings.tmpl.php @@ -0,0 +1,63 @@ +<div style="overflow-y: auto; height:auto; max-height: 350px;"> + <div> + <div class="faded"><strong><?php echo __('Name'); ?></strong></div> + <div> + <?php + if ($queue->checkOwnership($thisstaff)) { ?> + <input name="queue-name" type="text" size="40" + value="<?php echo Format::htmlchars($queue->getName()); ?>" + placeholder="<?php echo __('Search Title'); ?>"> + <?php + } else { + echo Format::htmlchars($queue->getName()); + } ?> + </div> + <div class="error" id="name-error"><?php echo + Format::htmlchars($errors['queue-name']); ?></div> + </div> + <div> + <div class="faded"><strong><?php echo __("Quick Filter"); ?></strong></div> + <div> + <select name="filter"> + <option value="" <?php if ($queue->filter == "") + echo 'selected="selected"'; ?>>— <?php echo __('None'); ?> —</option> + <option value="::" <?php if ($queue->filter == "::") + echo 'selected="selected"'; ?>>— <?php echo __('Inherit from parent'); + if ($queue->parent + && ($qf = $queue->parent->getQuickFilterField())) + echo sprintf(' (%s)', $qf->getLabel()); ?> —</option> +<?php foreach ($queue->getSupportedFilters() as $path => $f) { + list($label, $field) = $f; +?> + <option value="<?php echo $path; ?>" + <?php if ($path == $queue->filter) echo 'selected="selected"'; ?> + ><?php echo Format::htmlchars($label); ?></option> +<?php } ?> + </select> + </div> + <div class="error"><?php + echo Format::htmlchars($errors['filter']); ?></div> + </div> + <div> + <div class="faded"><strong><?php echo __("Defaut Sorting"); ?></strong></div> + <div> + <select name="sort_id"> + <option value="" <?php if ($queue->sort_id == 0) + echo 'selected="selected"'; ?>>— <?php echo __('None'); ?> —</option> + <option value="::" <?php if ($queue->isDefaultSortInherited() && + $queue->parent) + echo 'selected="selected"'; ?>>— <?php echo __('Inherit from parent'); + if ($queue->parent + && ($sort = $queue->parent->getDefaultSort())) + echo sprintf(' (%s)', $sort->getName()); ?> —</option> +<?php foreach ($queue->getSortOptions() as $sort) { ?> + <option value="<?php echo $sort->id; ?>" + <?php if ($sort->id == $queue->sort_id) echo 'selected="selected"'; ?> + ><?php echo Format::htmlchars($sort->getName()); ?></option> +<?php } ?> + </select> + </div> + <div class="error"><?php + echo Format::htmlchars($errors['sort_id']); ?></div> + </div> +</div> diff --git a/include/staff/templates/status-options.tmpl.php b/include/staff/templates/status-options.tmpl.php index c8cff18a94cc8b06535f0ecd017a2b87df9fd841..16d0f885bf3bb4813865d8ebbdc3171cf5be49b1 100644 --- a/include/staff/templates/status-options.tmpl.php +++ b/include/staff/templates/status-options.tmpl.php @@ -1,5 +1,10 @@ <?php global $thisstaff, $ticket; + +$role = $ticket ? $ticket->getRole($thisstaff) : $thisstaff->getRole(); +if ($role && !$role->hasPerm(Ticket::PERM_CLOSE)) + return; + // Map states to actions $actions= array( 'closed' => array( @@ -14,9 +19,8 @@ $actions= array( ); $states = array('open'); -if ($thisstaff->getRole($ticket ? $ticket->getDeptId() : null)->hasPerm(Ticket::PERM_CLOSE) - && (!$ticket || !Ticket::getMissingRequiredFields($ticket))) - $states = array_merge($states, array('closed')); +if (!$ticket || $ticket->isCloseable()) + $states[] = 'closed'; $statusId = $ticket ? $ticket->getStatusId() : 0; $nextStatuses = array(); diff --git a/include/staff/templates/task-view.tmpl.php b/include/staff/templates/task-view.tmpl.php index 0f6d44adf483efa54507354218d700bdb1c9b1eb..adb250728e1ae3bd19c355e78974120bc39237a5 100644 --- a/include/staff/templates/task-view.tmpl.php +++ b/include/staff/templates/task-view.tmpl.php @@ -1,7 +1,7 @@ <?php if (!defined('OSTSCPINC') || !$thisstaff || !$task - || !($role = $thisstaff->getRole($task->getDeptId()))) + || !($role = $thisstaff->getRole($task->getDept()))) die('Invalid path'); global $cfg; diff --git a/include/staff/templates/thread-entries.tmpl.php b/include/staff/templates/thread-entries.tmpl.php index 0d15aa0c9f15dfd410a9c60914a5c17ef67d3ab5..9b267bbae835c20484b484db1e963a1d4e5db2a0 100644 --- a/include/staff/templates/thread-entries.tmpl.php +++ b/include/staff/templates/thread-entries.tmpl.php @@ -80,7 +80,8 @@ foreach (Attachment::objects()->filter(array( if (!$A->inline) continue; $urls[strtolower($A->file->getKey())] = array( - 'download_url' => $A->file->getDownloadUrl(), + 'download_url' => $A->file->getDownloadUrl(['id' => + $A->getId()]), 'filename' => $A->getFilename(), ); } diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php index d36d2b7c9ab08e1e50e14829fa7726cbe5ee2050..5781db359fb9fe1383bd8f2235f536bb2f910774 100644 --- a/include/staff/templates/thread-entry.tmpl.php +++ b/include/staff/templates/thread-entry.tmpl.php @@ -101,7 +101,8 @@ if ($entry->flags & ThreadEntry::FLAG_COLLABORATOR && $entry->type == 'N') { ?> <span class="attachment-info"> <i class="icon-paperclip icon-flip-horizontal"></i> - <a class="no-pjax truncate filename" href="<?php echo $A->file->getDownloadUrl(); + <a class="no-pjax truncate filename" href="<?php echo + $A->file->getDownloadUrl(['id' => $A->getId()]); ?>" download="<?php echo Format::htmlchars($A->getFilename()); ?>" target="_blank"><?php echo Format::htmlchars($A->getFilename()); ?></a><?php echo $size;?> diff --git a/include/staff/templates/ticket-preview.tmpl.php b/include/staff/templates/ticket-preview.tmpl.php index b615e37971f7fe9b7f4be23bc547192f752f2893..4c3bbc888249a0e9526f017d6028b7e7d9477a6d 100644 --- a/include/staff/templates/ticket-preview.tmpl.php +++ b/include/staff/templates/ticket-preview.tmpl.php @@ -6,7 +6,7 @@ $staff=$ticket->getStaff(); $lock=$ticket->getLock(); -$role=$thisstaff->getRole($ticket->getDeptId()); +$role=$ticket->getRole($thisstaff); $error=$msg=$warn=null; $thread = $ticket->getThread(); diff --git a/include/staff/templates/users.tmpl.php b/include/staff/templates/users.tmpl.php index b34fa61111e05dac6726e6f379a1b67b6d88a83c..22eb67f25746a58937dd64f2d690f5666e1e6cda 100644 --- a/include/staff/templates/users.tmpl.php +++ b/include/staff/templates/users.tmpl.php @@ -1,16 +1,18 @@ <?php $qs = array(); -$select = 'SELECT user.*, email.address as email '; +$select = 'SELECT user.*, email.address as email, account.status as status, account.id as account_id '; $from = 'FROM '.USER_TABLE.' user ' - . 'LEFT JOIN '.USER_EMAIL_TABLE.' email ON (user.id = email.user_id) '; + . 'LEFT JOIN '.USER_EMAIL_TABLE.' email ON (user.id = email.user_id) ' + . 'LEFT JOIN '.USER_ACCOUNT_TABLE.' account ON (user.id = account.user_id) '; $where = ' WHERE user.org_id='.db_input($org->getId()); $sortOptions = array('name' => 'user.name', 'email' => 'email.address', 'create' => 'user.created', - 'update' => 'user.updated'); + 'update' => 'user.updated', + 'status' => 'account.status'); $orderWays = array('DESC'=>'DESC','ASC'=>'ASC'); $sort= ($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])]) ? strtolower($_REQUEST['sort']) : 'name'; //Sorting options... @@ -83,9 +85,9 @@ if ($num) { ?> <thead> <tr> <th width="4%"> </th> - <th width="38%"><?php echo __('Name'); ?></th> - <th width="35%"><?php echo __('Email'); ?></th> - <th width="8%"><?php echo __('Status'); ?></th> + <th width="30%"><?php echo __('Name'); ?></th> + <th width="33%"><?php echo __('Email'); ?></th> + <th width="18%"><?php echo __('Status'); ?></th> <th width="15%"><?php echo __('Created'); ?></th> </tr> </thead> @@ -96,7 +98,10 @@ if ($num) { ?> while ($row = db_fetch_array($res)) { $name = new UsersName($row['name']); - $status = 'Active'; + if (!$row['account_id']) + $status = __('Guest'); + else + $status = new UserAccountStatus($row['status']); $sel=false; if($ids && in_array($row['id'], $ids)) $sel=true; diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index d19138676399500753db5d7ff641711999031295..591442462ffc72772c684e119a35295506a93581 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -152,18 +152,8 @@ if ($_POST) <tr class="no_border" id="ccRow"> <td width="160"><?php echo __('Cc'); ?>:</td> <td> - <select name="ccs[]" id="cc_users_open" multiple="multiple" + <select class="collabSelections" name="ccs[]" id="cc_users_open" multiple="multiple" data-placeholder="<?php echo __('Select Contacts'); ?>"> - <option value=""></option> - <?php - $users = User::objects(); - foreach ($users as $u) { - if($user && $u->id != $user->getId()) { - ?> - <option value="<?php echo $u->id; ?>" - ><?php echo $u->getName(); ?> - </option> - <?php } } ?> </select> <br/><span class="error"><?php echo $errors['ccs']; ?></span> </td> @@ -171,18 +161,8 @@ if ($_POST) <tr class="no_border" id="bccRow"> <td width="160"><?php echo __('Bcc'); ?>:</td> <td> - <select name="bccs[]" id="bcc_users_open" multiple="multiple" + <select class="collabSelections" name="bccs[]" id="bcc_users_open" multiple="multiple" data-placeholder="<?php echo __('Select Contacts'); ?>"> - <option value=""></option> - <?php - $users = User::objects(); - foreach ($users as $u) { - if($user && $u->id != $user->getId()) { - ?> - <option value="<?php echo $u->id; ?>" - ><?php echo $u->getName(); ?> - </option> - <?php } } ?> </select> <br/><span class="error"><?php echo $errors['ccs']; ?></span> </td> @@ -534,9 +514,33 @@ $(function() { $('div#org-profile').fadeIn(); return false; }); - $("#cc_users_open").select2({width: '300px'}); - $("#bcc_users_open").select2({width: '300px'}); -}); + + $('.collabSelections').select2({ + width: '350px', + minimumInputLength: 3, + ajax: { + url: "ajax.php/users/local", + dataType: 'json', + data: function (params) { + return { + q: params.term, + }; + }, + processResults: function (data) { + return { + results: $.map(data, function (item) { + return { + text: item.name, + slug: item.slug, + id: item.id + } + }) + }; + } + } + }); + + }); $(document).ready(function () { $('#emailcollab').on('change', function(){ diff --git a/include/staff/ticket-tasks.inc.php b/include/staff/ticket-tasks.inc.php index a260f0f05b07e2427b973ccfd68f44ea9ce0e30c..aa765aef5cd39eebf3f68f53f4d02227f5ca4641 100644 --- a/include/staff/ticket-tasks.inc.php +++ b/include/staff/ticket-tasks.inc.php @@ -1,7 +1,7 @@ <?php global $thisstaff; -$role = $thisstaff->getRole($ticket->getDeptId()); +$role = $ticket->getRole($thisstaff); $tasks = Task::objects() ->select_related('dept', 'staff', 'team') diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index f5d7ae5f7ba3bf740000ed119c2e1429b2e3324e..b5ededef80a34f4bd784568e908c2edf1b8ef8fe 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -10,7 +10,7 @@ $info=($_POST && $errors)?Format::input($_POST):array(); //Get the goodies. $dept = $ticket->getDept(); //Dept -$role = $thisstaff->getRole($dept, $ticket->isAssigned($thisstaff)); +$role = $ticket->getRole($thisstaff); $staff = $ticket->getStaff(); //Assigned or closed by.. $user = $ticket->getOwner(); //Ticket User (EndUser) $team = $ticket->getTeam(); //Assigned team. @@ -186,8 +186,10 @@ if($ticket->isOverdue()) return false" ><i class="icon-paste"></i> <?php echo __('Manage Forms'); ?></a></li> <?php - } ?> + } + if ($role->hasPerm(Ticket::PERM_REPLY)) { + ?> <li> <?php @@ -199,9 +201,12 @@ if($ticket->isOverdue()) $recipients); ?> </li> + <?php + } ?> -<?php if ($thisstaff->hasPerm(Email::PERM_BANLIST)) { +<?php if ($thisstaff->hasPerm(Email::PERM_BANLIST) + && $role->hasPerm(Ticket::PERM_REPLY)) { if(!$emailBanned) {?> <li><a class="confirm-action" id="ticket-banemail" href="#banemail"><i class="icon-ban-circle"></i> <?php echo sprintf( @@ -576,13 +581,17 @@ if($ticket->isOverdue()) <br> <?php foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { + //Find fields to exclude if disabled by help topic + $disabled = Ticket::getMissingRequiredFields($ticket, true); + // Skip core fields shown earlier in the ticket view // TODO: Rewrite getAnswers() so that one could write // ->getAnswers()->filter(not(array('field__name__in'=> // array('email', ...)))); $answers = $form->getAnswers()->exclude(Q::any(array( 'field__flags__hasbit' => DynamicFormField::FLAG_EXT_STORED, - 'field__name__in' => array('subject', 'priority') + 'field__name__in' => array('subject', 'priority'), + 'field__id__in' => $disabled, ))); $displayed = array(); foreach($answers as $a) { @@ -607,7 +616,14 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { <a class="ticket-action" id="inline-update" data-placement="bottom" data-toggle="tooltip" title="<?php echo __('Update'); ?>" data-redirect="tickets.php?id=<?php echo $ticket->getId(); ?>" href="#tickets/<?php echo $ticket->getId(); ?>/field/<?php echo $id; ?>/edit"> - <?php echo $v; ?> + <?php + if (strlen($v) > 200) { + echo Format::truncate($v, 200); + echo "<br><i class=\"icon-edit\"></i>"; + } + else + echo $v; + ?> </a> <?php } else { echo $v; @@ -695,15 +711,15 @@ if ($errors['err'] && isset($_POST['a'])) { <?php }?> <tbody id="to_sec"> + <?php + # XXX: Add user-to-name and user-to-email HTML ID#s + if ($addresses = Email::getAddresses(array('smtp' => true))){ + ?> <tr> <td width="120"> <label><strong><?php echo __('From'); ?>:</strong></label> </td> <td> - <?php - # XXX: Add user-to-name and user-to-email HTML ID#s - $addresses = Email::getAddresses(); - ?> <select id="from_name" name="from_name"> <?php $sql=' SELECT email_id, email, name, smtp_host ' @@ -719,6 +735,7 @@ if ($errors['err'] && isset($_POST['a'])) { </select> </td> </tr> + <?php } ?> <tr> <td width="120"> <label><strong><?php echo __('To'); ?>:</strong></label> @@ -772,20 +789,18 @@ if ($errors['err'] && isset($_POST['a'])) { <tr> <td width="160"><b><?php echo __('Cc'); ?>:</b></td> <td> - <select name="ccs[]" id="cc_users" multiple="multiple" + <select class="collabSelections" name="ccs[]" id="cc_users" multiple="multiple" data-placeholder="<?php echo __('Select Contacts'); ?>"> - <option value=""></option> - <option value="NEW">— <?php echo __('Add New');?> —</option> <?php - $users = User::objects(); - foreach ($users as $u) { - if($u->id != $ticket->user_id && !in_array($u->getId(), $bcc_cids)) { + foreach ($cc_cids as $u) { + if($u != $ticket->user_id && !in_array($u, $bcc_cids)) { + ?> + <option value="<?php echo $u; ?>" <?php + if (in_array($u, $cc_cids)) + echo 'selected="selected"'; ?>><?php echo User::lookup($u); ?> + </option> + <?php } } ?> ?> - <option value="<?php echo $u->id; ?>" <?php - if (in_array($u->getId(), $cc_cids)) - echo 'selected="selected"'; ?>><?php echo $u->getName(); ?> - </option> - <?php } } ?> </select> <br/><span class="error"><?php echo $errors['ccs']; ?></span> </td> @@ -793,20 +808,18 @@ if ($errors['err'] && isset($_POST['a'])) { <tr> <td width="160"><b><?php echo __('Bcc'); ?>:</b></td> <td> - <select name="bccs[]" id="bcc_users" multiple="multiple" + <select class="collabSelections" name="bccs[]" id="bcc_users" multiple="multiple" data-placeholder="<?php echo __('Select Contacts'); ?>"> - <option value=""></option> - <option value="NEW">— <?php echo __('Add New');?> —</option> <?php - $users = User::objects(); - foreach ($users as $u) { - if($u->id != $ticket->user_id && !in_array($u->getId(), $cc_cids)) { + foreach ($bcc_cids as $u) { + if($u != $ticket->user_id && !in_array($u, $cc_cids)) { + ?> + <option value="<?php echo $u; ?>" <?php + if (in_array($u, $bcc_cids)) + echo 'selected="selected"'; ?>><?php echo User::lookup($u); ?> + </option> + <?php } } ?> ?> - <option value="<?php echo $u->id; ?>" <?php - if (in_array($u->getId(), $bcc_cids)) - echo 'selected="selected"'; ?>><?php echo $u->getName(); ?> - </option> - <?php } } ?> </select> <br/><span class="error"><?php echo $errors['bccs']; ?></span> </td> @@ -1176,76 +1189,72 @@ $(function() { }); $(function() { - $("#cc_users").select2({width: '350px'}); - $("#bcc_users").select2({width: '350px'}); -}); - -$(function() { - $('#cc_users').on("select2:select", function(e) { - var el = $(this); - var tid = <?php echo $ticket->getThreadId(); ?>; - - if(el.val().includes("NEW")) { - $("li[title='— Add New —']").remove(); - var url = 'ajax.php/thread/' + tid + '/add-collaborator' ; - $.userLookup(url, function(user) { - e.preventDefault(); - if($('.dialog#confirm-action').length) { - $('.dialog#confirm-action #action').val('addcc'); - $('#confirm-form').append('<input type=hidden name=user_id value='+user.id+' />'); - $('#overlay').show(); - } - }); - var arr = el.val(); - var removeStr = "NEW"; - - arr.splice($.inArray(removeStr, arr),1); - $(this).val(arr); - } - }); - - $('#bcc_users').on("select2:select", function(e) { - var el = $(this); - var tid = <?php echo $ticket->getThreadId(); ?>; + $('.collabSelections').on("select2:select", function(e) { + var el = $(this); + var tid = <?php echo $ticket->getThreadId(); ?>; + var target = e.currentTarget.id; + var addTo = (target == 'cc_users') ? 'addcc' : 'addbcc'; - if(el.val().includes("NEW")) { - $("li[title='— Add New —']").remove(); - var url = 'ajax.php/thread/' + tid + '/add-collaborator' ; - $.userLookup(url, function(user) { - e.preventDefault(); - if($('.dialog#confirm-action').length) { - $('.dialog#confirm-action #action').val('addbcc'); - $('#confirm-form').append('<input type=hidden name=user_id value='+user.id+' />'); - $('#overlay').show(); - } - }); + if(el.val().includes("NEW")) { + $("li[title='— Add New —']").remove(); + var url = 'ajax.php/thread/' + tid + '/add-collaborator/' + addTo ; + $.userLookup(url, function(user) { + e.preventDefault(); + if($('.dialog#confirm-action').length) { + $('.dialog#confirm-action #action').val(addTo); + $('#confirm-form').append('<input type=hidden name=user_id value='+user.id+' />'); + $('#overlay').show(); + } + }); var arr = el.val(); var removeStr = "NEW"; arr.splice($.inArray(removeStr, arr),1); $(this).val(arr); - } - }); - - $('#cc_users').on("select2:unselecting", function(e) { - var confirmation = confirm(__("Are you sure you want to remove the collaborator from receiving this reply?")); - if (confirmation == false) { - $('#cc_users').on("select2:opening", function(e) { - return false; - }); - return false; - } - + } }); - $('#bcc_users').on("select2:unselecting", function(e) { + $('.collabSelections').on("select2:unselecting", function(e) { + var el = $(this); + var target = '#' + e.currentTarget.id; var confirmation = confirm(__("Are you sure you want to remove the collaborator from receiving this reply?")); if (confirmation == false) { - $('#bcc_users').on("select2:opening", function(e) { - return false; - }); + $(target).on("select2:opening", function(e) { return false; + }); + return false; + } + +}); + + $('.collabSelections').select2({ + width: '350px', + minimumInputLength: 3, + ajax: { + url: "ajax.php/users/local", + dataType: 'json', + data: function (params) { + if (!params) { + params.term = 'test'; } + return { + q: params.term, + }; + }, + processResults: function (data) { + data[0] = {name: "\u2014 Add New \u2014", id: "NEW"}; + return { + results: $.map(data, function (item) { + return { + text: item.name, + slug: item.slug, + id: item.id + } + }) + }; + } + } }); + }); </script> diff --git a/include/staff/users.inc.php b/include/staff/users.inc.php index 5d35e3f277a50c1d6dfabaabe2005cbeb1631cb6..04c292a0bbf5fbc66b47f99564eb94f1fae8abdc 100644 --- a/include/staff/users.inc.php +++ b/include/staff/users.inc.php @@ -312,6 +312,11 @@ $(function() { goBaby($(this).attr('href').substr(1)); return false; }); + + // Remove CSRF Token From GET Request + document.querySelector("form[action='users.php']").onsubmit = function() { + document.getElementsByName("__CSRFToken__")[0].remove(); + }; }); </script> diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index 4ad11e1f415bd953c9e8a80441aa12cdb7bbf5dc..5c4c675cad5ffbfde918a49029645fc955d772dd 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -e7dfe82131b906a14f6a13163943855f +70921d5c3920ab240b08bdd55bc894c8 diff --git a/include/upgrader/streams/core/934b8db8-ad9d0a5f.task.php b/include/upgrader/streams/core/934b8db8-ad9d0a5f.task.php index f83f7c8b352f04e987b964dfdd57003742a32d28..886bcf6ebb0a7f53102efc66672e18073a032e29 100644 --- a/include/upgrader/streams/core/934b8db8-ad9d0a5f.task.php +++ b/include/upgrader/streams/core/934b8db8-ad9d0a5f.task.php @@ -21,10 +21,18 @@ class QueueSortCreator extends MigrationTask { // Re-insert old saved searches foreach ($old ?: array() as $row) { + // Only save entries with "valid" criteria + if (!$row['title'] + || !($config = JsonDataParser::parse($row['config'], true)) + || !($criteria = CustomQueue::isolateCriteria($criteria))) + continue; + + $row['config'] = JsonDataEncoder::encode(array( + 'criteria' => $criteria, 'conditions' => array())); $row['root'] = 'T'; CustomQueue::__create(array_intersect_key($row, array_flip( array('staff_id', 'title', 'config', 'flags', - 'created', 'updated')))); + 'root', 'created', 'updated')))); } $columns = $i18n->getTemplate('queue_sort.yaml')->getData(); diff --git a/include/upgrader/streams/core/cce1ba43-e7dfe821.patch.sql b/include/upgrader/streams/core/cce1ba43-e7dfe821.patch.sql index 660400dde7260d323533cfb805b7b8a94b8ec68c..233b1e1749703bae43e4d8b89664e97ee03d6a8f 100644 --- a/include/upgrader/streams/core/cce1ba43-e7dfe821.patch.sql +++ b/include/upgrader/streams/core/cce1ba43-e7dfe821.patch.sql @@ -9,6 +9,11 @@ ALTER TABLE `%TABLE_PREFIX%faq_category` ADD `category_pid` int(10) unsigned DEFAULT NULL AFTER `category_id`; +-- Phone Field `name` and `flags` +UPDATE `%TABLE_PREFIX%form_field` + SET `flags` = `flags` + 262144 + WHERE `type` = 'phone' AND `name` = 'phone' AND `form_id` = 1 AND `id` < 10; + -- Finished with patch UPDATE `%TABLE_PREFIX%config` SET `value` = 'e7dfe82131b906a14f6a13163943855f' diff --git a/include/upgrader/streams/core/e7dfe821-70921d5c.patch.sql b/include/upgrader/streams/core/e7dfe821-70921d5c.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..3547e6698f15cb0adcff927510968cb168bb1f08 --- /dev/null +++ b/include/upgrader/streams/core/e7dfe821-70921d5c.patch.sql @@ -0,0 +1,43 @@ +/** +* @signature 70921d5c3920ab240b08bdd55bc894c8 +* @version v1.11.0 +* @title Make Public CustomQueues Configurable +* +* This patch adds staff_id to queue_columns table and queue_config table to +* allow for ability to customize public queue columns as well as additional +* settings +* +*/ + +-- Add staff_id to queue_columns table +ALTER TABLE `%TABLE_PREFIX%queue_columns` + ADD `staff_id` int(11) unsigned NOT NULL AFTER `column_id`; + +-- Set staff_id to 0 for default columns +UPDATE `%TABLE_PREFIX%queue_columns` + SET `staff_id` = 0; + +-- Add staff_id to PRIMARY KEY +ALTER TABLE `%TABLE_PREFIX%queue_columns` + DROP PRIMARY KEY, + ADD PRIMARY KEY (`queue_id`, `column_id`, `staff_id`); + +-- Set staff_id to 0 for public queues +UPDATE `%TABLE_PREFIX%queue` + SET `staff_id` = 0 + WHERE (`flags` & 1) >0; + +-- Add bridge table for public Queues staff configuration & settings +DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_config`; +CREATE TABLE `%TABLE_PREFIX%queue_config` ( + `queue_id` int(11) unsigned NOT NULL, + `staff_id` int(11) unsigned NOT NULL, + `setting` text, + `updated` datetime NOT NULL, + PRIMARY KEY (`queue_id`,`staff_id`) +) DEFAULT CHARSET=utf8; + + -- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `value` = '70921d5c3920ab240b08bdd55bc894c8' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/open.php b/open.php index 450b9d1f60609daaa31e77673e4898d672230403..a507bc979515181f5d46eb035a8ccaf9ade92e59 100644 --- a/open.php +++ b/open.php @@ -82,7 +82,8 @@ if ($ticket echo Format::viewableImages( $ticket->replaceVars( $page->getLocalBody() - ) + ), + ['type' => 'P'] ); } else { diff --git a/scp/ajax.php b/scp/ajax.php index 039af6b3e48d838eeabd2506dffbfc67c3055ed7..85d010e92b48b47d39643715bd8d9884c4c040c8 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -177,6 +177,7 @@ $dispatcher = patterns('', url_post('^$', 'doSearch'), url_get('^/(?P<id>\d+)$', 'editSearch'), url_get('^/adhoc,(?P<key>[\w=/+]+)$', 'getAdvancedSearchDialog'), + url_get('^/create$', 'createSearch'), url_post('^/(?P<id>\d+)/save$', 'saveSearch'), url_post('^/save$', 'saveSearch'), url_delete('^/(?P<id>\d+)$', 'deleteSearch'), @@ -210,9 +211,9 @@ $dispatcher = patterns('', url_get('^(?P<tid>\d+)/collaborators/preview$', 'previewCollaborators'), url_get('^(?P<tid>\d+)/collaborators$', 'showCollaborators'), url_post('^(?P<tid>\d+)/collaborators$', 'updateCollaborators'), - url_get('^(?P<tid>\d+)/add-collaborator/(?P<uid>\d+)$', 'addCollaborator'), + url_get('^(?P<tid>\d+)/add-collaborator/(?P<type>\w+)/(?P<uid>\d+)$', 'addCollaborator'), url_get('^(?P<tid>\d+)/add-collaborator/auth:(?P<bk>\w+):(?P<id>.+)$', 'addRemoteCollaborator'), - url('^(?P<tid>\d+)/add-collaborator$', 'addCollaborator'), + url('^(?P<tid>\d+)/add-collaborator/(?P<type>\w+)$', 'addCollaborator'), url_get('^(?P<tid>\d+)/collaborators/(?P<cid>\d+)/view$', 'viewCollaborator'), url_post('^(?P<tid>\d+)/collaborators/(?P<cid>\d+)$', 'updateCollaborator') )), diff --git a/scp/emailtest.php b/scp/emailtest.php index ae8d68579d88768ee3034c856d89ace84c5b6283..8228de5c3507906057c6d7a5f1a7ac70b067edd2 100644 --- a/scp/emailtest.php +++ b/scp/emailtest.php @@ -16,8 +16,6 @@ require('admin.inc.php'); include_once(INCLUDE_DIR.'class.email.php'); include_once(INCLUDE_DIR.'class.csrf.php'); -$info=array(); -$info['subj']='osTicket test email'; if($_POST){ $errors=array(); @@ -53,6 +51,8 @@ $ost->addExtraHeader('<meta name="tip-namespace" content="emails.diagnostic" />' "$('#content').data('tipNamespace', '".$tip_namespace."');"); require(STAFFINC_DIR.'header.inc.php'); +$info=array(); +$info['subj']='osTicket test email'; $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); ?> <form action="emailtest.php" method="post" class="save"> diff --git a/scp/filters.php b/scp/filters.php index 23bbe23e37ae0721eccbf43288dbfc6a77ab0e75..c83f9eac074175b908b3cea6cf3eaaa75778f64a 100644 --- a/scp/filters.php +++ b/scp/filters.php @@ -120,11 +120,12 @@ $tip_namespace = 'manage.filter'; if($filter || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) { if($filter) { foreach ($filter->getActions() as $A) { + $config = JsonDataParser::parse($A->configuration); if($A->type == 'dept') - $dept = Dept::lookup($A->parseConfiguration($_POST)['dept_id']); + $dept = Dept::lookup($config['dept_id']); if($A->type == 'topic') - $topic = Topic::lookup($A->parseConfiguration($_POST)['topic_id']); + $topic = Topic::lookup($config['topic_id']); } } diff --git a/scp/js/scp.js b/scp/js/scp.js index fb2a6428a9c8e1884186968236b252f9a197eda7..508123366f03b860cf906dd727bb26b9ff95fc4d 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -506,7 +506,7 @@ var scp_prep = function() { // Auto fetch queue counts $(function() { var fired = false; - $('li.top-queue.item').hover(function() { + $('#customQ_nav li.item').hover(function() { if (fired) return; fired = true; $.ajax({ @@ -711,7 +711,9 @@ $.dialog = function (url, codes, cb, options) { } catch (e) { } $('div.body', $popup).html(resp); - $popup.effect('shake'); + if ($('#msg_error, .error-banner', $popup).length) { + $popup.effect('shake'); + } $('#msg_notice, #msg_error', $popup).delay(5000).slideUp(); $('div.tab_content[id] div.error:not(:empty)', $popup).each(function() { var div = $(this).closest('.tab_content'); diff --git a/scp/queues.php b/scp/queues.php index c2a641614e47116bcd2cd9efb0ebc38b331d20eb..ba5f33b78baf9bdaec51b1da074993030703030c 100644 --- a/scp/queues.php +++ b/scp/queues.php @@ -20,7 +20,7 @@ require('admin.inc.php'); $nav->setTabActive('settings', 'settings.php?t='.urlencode($_GET['t'])); $errors = array(); -if ($_REQUEST['id']) { +if ($_REQUEST['id'] && is_numeric($_REQUEST['id'])) { $queue = CustomQueue::lookup($_REQUEST['id']); } @@ -43,11 +43,14 @@ if ($_POST) { case 'create': $queue = CustomQueue::create(array( 'flags' => CustomQueue::FLAG_PUBLIC, - 'root' => $_POST['root'] ?: 'Ticket' + 'staff_id' => 0, + 'title' => $_POST['queue-name'], + 'root' => $_POST['root'] ?: 'T' )); if ($queue->update($_POST, $errors) && $queue->save(true)) { - $msg = sprintf(__('Successfully added %s'), Format::htmlchars($_POST['name'])); + $msg = sprintf(__('Successfully added %s'), + Format::htmlchars($queue->getName())); } elseif (!$errors['err']) { $errors['err']=sprintf(__('Unable to add %s. Correct error(s) below and try again.'), diff --git a/scp/tasks.php b/scp/tasks.php index 5cd77777f3714b6ff592878ab25ca6ee35a212a1..63e4b87ca1b35831e54f6bfcde6ce0efe07620db 100644 --- a/scp/tasks.php +++ b/scp/tasks.php @@ -44,7 +44,7 @@ if($_POST && !$errors): if ($task) { //More coffee please. $errors=array(); - $role = $thisstaff->getRole($task->getDeptId()); + $role = $thisstaff->getRole($task->getDept()); switch(strtolower($_POST['a'])): case 'postnote': /* Post Internal Note */ $vars = $_POST; diff --git a/scp/tickets.php b/scp/tickets.php index 112a7f7d581212a1c16ccaab830468af560e6164..2d04b89efc48ccedab6099f58bee837d72ac2b9d 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -110,10 +110,10 @@ if (!$ticket) { $_SESSION[$queue_key] = $queue_id; if ((int) $queue_id && !$queue) { - $queue = CustomQueue::lookup($queue_id); + $queue = SavedQueue::lookup($queue_id); } if (!$queue) { - $queue = CustomQueue::lookup($cfg->getDefaultTicketQueueId()); + $queue = SavedQueue::lookup($cfg->getDefaultTicketQueueId()); } // Set the queue_id for navigation to turn a top-level item bold @@ -139,7 +139,7 @@ if($_POST && !$errors): //More coffee please. $errors=array(); $lock = $ticket->getLock(); //Ticket lock if any - $role = $thisstaff->getRole($ticket->getDeptId()); + $role = $ticket->getRole($thisstaff); switch(strtolower($_POST['a'])): case 'reply': if (!$role || !$role->hasPerm(Ticket::PERM_REPLY)) { @@ -456,7 +456,7 @@ $nav->setTabActive('tickets'); $nav->addSubNavInfo('jb-overflowmenu', 'customQ_nav'); // Fetch ticket queues organized by root and sub-queues -$queues = CustomQueue::queues() +$queues = SavedQueue::queues() ->filter(Q::any(array( 'flags__hasbit' => CustomQueue::FLAG_PUBLIC, 'staff_id' => $thisstaff->getId(), diff --git a/setup/inc/class.installer.php b/setup/inc/class.installer.php index 1a198325548faafda964b654f26423e7ad828844..d5aff5044cbf99b2a8b6460b6251b215c4bc2911 100644 --- a/setup/inc/class.installer.php +++ b/setup/inc/class.installer.php @@ -210,6 +210,18 @@ class Installer extends SetupWizard { return false; } + // Extended Access + foreach (Dept::objects() + ->filter(Q::not(array('id' => $dept_id))) + ->values_flat('id') as $row) { + $da = new StaffDeptAccess(array( + 'dept_id' => $row[0], + 'role_id' => $role_id + )); + $staff->dept_access->add($da); + } + $staff->dept_access->saveAll(); + // Create default emails! $email = $vars['email']; list(,$domain) = explode('@', $vars['email']); diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 45482f83f8a448904a1e054406cd9a25c83c9c61..5c1ddc7ea1a46c736dd7d05f2037925a1f75131f 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -876,11 +876,12 @@ DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_columns`; CREATE TABLE `%TABLE_PREFIX%queue_columns` ( `queue_id` int(11) unsigned NOT NULL, `column_id` int(11) unsigned NOT NULL, + `staff_id` int(11) unsigned NOT NULL, `bits` int(10) unsigned NOT NULL DEFAULT '0', `sort` int(10) unsigned NOT NULL DEFAULT '1', `heading` varchar(64) DEFAULT NULL, `width` int(10) unsigned NOT NULL DEFAULT '100', - PRIMARY KEY (`queue_id`, `column_id`) + PRIMARY KEY (`queue_id`, `column_id`, `staff_id`) ) DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_sort`; @@ -913,6 +914,15 @@ CREATE TABLE `%TABLE_PREFIX%queue_export` ( KEY `queue_id` (`queue_id`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_config`; +CREATE TABLE `%TABLE_PREFIX%queue_config` ( + `queue_id` int(11) unsigned NOT NULL, + `staff_id` int(11) unsigned NOT NULL, + `setting` text, + `updated` datetime NOT NULL, + PRIMARY KEY (`queue_id`,`staff_id`) +) DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%translation`; CREATE TABLE `%TABLE_PREFIX%translation` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, diff --git a/setup/test/run-tests.php b/setup/test/run-tests.php index fe55e681e06a947511848e8d3a9a669e339485bb..9834aa90a684456a246cc7dc9fa3d07e892cbc2c 100644 --- a/setup/test/run-tests.php +++ b/setup/test/run-tests.php @@ -43,7 +43,7 @@ if (function_exists('pcntl_signal')) { foreach (glob_recursive(dirname(__file__)."/tests/test.*.php") as $t) { if (strpos($t,"class.") !== false) continue; - $class = (include $t); + $class = @(include $t); if (!is_string($class)) continue; if($selected_test && ($class != $selected_test)) diff --git a/setup/test/tests/class.test.php b/setup/test/tests/class.test.php index 5dd6097dcb127a1a3391d36361872ed0e6b5dec2..db05c2c16a88c069b55ddace79e641430d5398cd 100644 --- a/setup/test/tests/class.test.php +++ b/setup/test/tests/class.test.php @@ -31,10 +31,15 @@ class Test { function teardown() { } - static function getAllScripts($excludes=true, $root=false) { + function ignore3rdparty() { + return true; + } + + function getAllScripts($pattern='*.php', $root=false, $excludes=true) { $root = $root ?: get_osticket_root_path(); + $excludes = $excludes ?: $this->ignore3rdparty(); $scripts = array(); - foreach (glob_recursive("$root/*.php") as $s) { + foreach (glob_recursive("$root/$pattern") as $s) { $found = false; if ($excludes) { foreach (self::$third_party_paths as $p) { @@ -88,20 +93,20 @@ class Test { foreach ($rc->getMethods() as $m) { if (stripos($m->name, 'test') === 0) { $this->setup(); - call_user_func(array($this, $m->name)); + @call_user_func(array($this, $m->name)); $this->teardown(); } } } - function line_number_for_offset($filename, $offset) { - $lines = file($filename); - $bytes = $line = 0; - while ($bytes < $offset) { - $bytes += strlen(array_shift($lines)); - $line += 1; - } - return $line; + function line_number_for_offset($file, $offset) { + + if (is_file($file)) + $content = file_get_contents($file, false, null, 0, $offset); + else + $content = @substr($file, 0, $offset); + + return count(explode("\n", $content)); } } diff --git a/setup/test/tests/stubs.php b/setup/test/tests/stubs.php index 0ad23392642a6e1d4cedd18e7876e44fd1972d35..6a7b0d285ea0e9898f600365ca692f29c768f1c0 100644 --- a/setup/test/tests/stubs.php +++ b/setup/test/tests/stubs.php @@ -104,6 +104,7 @@ class Phar { function startBuffering() {} function stopBuffering() {} function setSignatureAlgorithm() {} + function compress() {} } class ZipArchive { @@ -114,6 +115,10 @@ class ZipArchive { function setExternalAttributesName() {} } +class Spyc { + function YAMLLoad() {} +} + class finfo { function file() {} function buffer() {} @@ -176,6 +181,7 @@ class NumberFormatter { class Collator { function setStrength() {} + function compare() {} } class Aws_Route53_Client { @@ -189,4 +195,43 @@ class Memcache { function set() {} function get() {} } + +class Crypt_Hash { + function setKey() {} + function setIV() {} +} + +class Crypt_AES { + function setKey() {} + function setIV() {} + function enableContinuousBuffer() {} +} + +class PEAR { + function isError() {} + function mail() {} +} + +class mail { + function factory() {} + function connect() {} + function disconnect() {} +} + +class Mail_mime { + function headers() {} + function setTXTBody() {} + function setHTMLBody() {} + function addCc() {} +} + +class mPDF { + function Output() {} +} + +class HashPassword { + function CheckPassword() {} + function HashPassword() {} +} + ?> diff --git a/setup/test/tests/test.extra-whitespace.php b/setup/test/tests/test.extra-whitespace.php index 96d31ac8425e40a38e66ff90f7a59e9717697bb9..ff3fc7be0721cd981de6550ba051a540565c6b28 100644 --- a/setup/test/tests/test.extra-whitespace.php +++ b/setup/test/tests/test.extra-whitespace.php @@ -3,16 +3,17 @@ require_once "class.test.php"; class ExtraWhitespace extends Test { var $name = "PHP Leading and Trailing Whitespace"; - + function testFindWhitespace() { foreach ($this->getAllScripts() as $s) { $matches = array(); + $content = file_get_contents($s); if (preg_match_all('/^\s+<\?php|\?>\n\s+$/s', - file_get_contents($s), $matches, + $content, $matches, PREG_OFFSET_CAPTURE) > 0) { foreach ($matches[0] as $match) $this->fail( - $s, $this->line_number_for_offset($s, $match[1]), + $s, $this->line_number_for_offset($content, $match[1]), (strpos('?>', $match[0]) !== false) ? 'Leading whitespace' : 'Trailing whitespace'); diff --git a/setup/test/tests/test.jslint.php b/setup/test/tests/test.jslint.php index 5f8fcfca9378b8eef6441e0f71e5d7853dcb28dc..e916715c663587d14f20be9dc213011c00055d1a 100644 --- a/setup/test/tests/test.jslint.php +++ b/setup/test/tests/test.jslint.php @@ -6,8 +6,7 @@ class JsSyntaxTest extends Test { function testLintErrors() { $exit = 0; - $root = get_osticket_root_path(); - foreach (glob_recursive("$root/*.js") as $s) { + foreach ($this->getAllScripts('*.js') as $s) { ob_start(); system("jsl -process $s", $exit); $line = ob_get_contents(); diff --git a/setup/test/tests/test.shortopentags.php b/setup/test/tests/test.shortopentags.php index 571fc08e15e1c3af5c0c60216eeda7d494a6cfa6..ec5b8669e454c6ae735aeb81fbfaba94f0f4e016 100644 --- a/setup/test/tests/test.shortopentags.php +++ b/setup/test/tests/test.shortopentags.php @@ -7,13 +7,14 @@ class ShortOpenTag extends Test { function testFindShortOpens() { foreach ($this->getAllScripts() as $s) { $matches = array(); + $content = file_get_contents($s); if (preg_match_all('/<\?\s*(?!php|xml).*$/m', - file_get_contents($s), $matches, + $content, $matches, PREG_OFFSET_CAPTURE) > 0) { foreach ($matches[0] as $match) $this->fail( $s, - $this->line_number_for_offset($s, $match[1]), + $this->line_number_for_offset($content, $match[1]), $match[0]); } else $this->pass(); diff --git a/setup/test/tests/test.signals.php b/setup/test/tests/test.signals.php index 7ce888383ab0aee781450f3f908d8c4a36d96ae1..0f6287a0a9e0a8414d39cecacdf2a2ed99704964 100644 --- a/setup/test/tests/test.signals.php +++ b/setup/test/tests/test.signals.php @@ -16,14 +16,15 @@ class SignalsTest extends Test { foreach ($matches as $match) $published_signals[] = $match[1]; foreach ($scripts as $s) { + $content = file_get_contents($s); if (preg_match_all("/^ *Signal::connect\('([^']+)'/m", - file_get_contents($s), $matches, + $content, $matches, PREG_OFFSET_CAPTURE|PREG_SET_ORDER) > 0) { foreach ($matches as $match) { $match = $match[1]; if (!in_array($match[0], $published_signals)) $this->fail( - $s, self::line_number_for_offset($s, $match[1]), + $s, $this->line_number_for_offset($content, $match[1]), "Signal '{$match[0]}' is never sent"); else $this->pass(); @@ -31,16 +32,6 @@ class SignalsTest extends Test { } } } - - function line_number_for_offset($filename, $offset) { - $lines = file($filename); - $bytes = $line = 0; - while ($bytes < $offset) { - $bytes += strlen(array_shift($lines)); - $line += 1; - } - return $line; - } } return 'SignalsTest'; diff --git a/setup/test/tests/test.syntax.php b/setup/test/tests/test.syntax.php index 0adf1465a3fcca066262efe308222e8da1e48720..cc528f52fb53b8174caf96a61652c11f29bde818 100644 --- a/setup/test/tests/test.syntax.php +++ b/setup/test/tests/test.syntax.php @@ -4,9 +4,13 @@ require_once "class.test.php"; class SyntaxTest extends Test { var $name = "PHP Syntax Checks"; + function ignore3rdparty() { + return false; + } + function testCompileErrors() { $exit = 0; - foreach ($this->getAllScripts(false) as $s) { + foreach ($this->getAllScripts() as $s) { ob_start(); system("php -l $s", $exit); $line = ob_get_contents(); diff --git a/setup/test/tests/test.undefinedmethods.php b/setup/test/tests/test.undefinedmethods.php index 83a227eeeae0da98b146908f50055c2b2387ff97..16f4e76e1678ac5e755e4c23235a5279e348cb03 100644 --- a/setup/test/tests/test.undefinedmethods.php +++ b/setup/test/tests/test.undefinedmethods.php @@ -4,8 +4,12 @@ require_once "class.test.php"; class UndefinedMethods extends Test { var $name = "Access to undefined object methods"; - function testFindShortOpen() { - $scripts = $this->getAllScripts(false); + function ignore3rdparty() { + return false; + } + + function testUndefinedMethods() { + $scripts = $this->getAllScripts(); $function_defs = array(); foreach ($scripts as $s) { $matches = array(); diff --git a/setup/test/tests/test.var-dump.php b/setup/test/tests/test.var-dump.php new file mode 100644 index 0000000000000000000000000000000000000000..1244ec7b5e1c1e95e037946d105ae70cf8a92fdf --- /dev/null +++ b/setup/test/tests/test.var-dump.php @@ -0,0 +1,27 @@ +<?php +require_once "class.test.php"; + +class VarDump extends Test { + var $name = "var_dump Checks"; + + function testFindShortOpens() { + $re = '/^(([\t ]*?)var_dump\(.*[\)|,|;])((?!nolint).)*$/m'; + foreach ($this->getAllScripts() as $s) { + $matches = array(); + $content = file_get_contents($s); + if (preg_match_all($re, + $content, $matches, + PREG_OFFSET_CAPTURE) > 0) { + foreach ($matches[0] as $match) { + $this->fail( + $s, + $this->line_number_for_offset($content, $match[1]), + trim($match[0])); + } + } + else $this->pass(); + } + } +} +return 'VarDump'; +?>