diff --git a/include/ajax.orgs.php b/include/ajax.orgs.php index c4d73187bf6d2c85c2bf1dc4a7c0f6650174e233..0cc098a50ea44a9bb3863b243c2617690cdbdee4 100644 --- a/include/ajax.orgs.php +++ b/include/ajax.orgs.php @@ -26,26 +26,32 @@ class OrgsAjaxAPI extends AjaxController { Http::response(400, 'Query argument is required'); } + $q = $_REQUEST['q']; $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25; - $orgs=array(); - - $escaped = db_input(strtolower($_REQUEST['q']), false); - $sql='SELECT DISTINCT org.id, org.name ' - .' FROM '.ORGANIZATION_TABLE.' org ' - .' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON (entry.object_type=\'O\' AND entry.object_id = org.id) - LEFT JOIN '.FORM_ANSWER_TABLE.' value ON (value.entry_id=entry.id) ' - .' WHERE org.name LIKE \'%'.$escaped.'%\' OR value.value LIKE \'%'.$escaped.'%\'' - .' ORDER BY org.created ' - .' LIMIT '.$limit; - - if(($res=db_query($sql)) && db_num_rows($res)){ - while(list($id, $name)=db_fetch_row($res)) { - $orgs[] = array('name' => Format::htmlchars($name), 'info' => $name, - 'id' => $id, '/bin/true' => $_REQUEST['q']); - } + + $orgs = Organization::objects() + ->values_flat('id', 'name') + ->limit($limit); + + global $ost; + $orgs = $ost->searcher->find($q, $orgs); + $orgs->order_by(new SqlCode('__relevance__'), QuerySet::DESC) + ->distinct('id'); + + if (!count($orgs) && substr($q, strlen($q)-1) != '*') { + // Do wildcard full-text search + $_REQUEST['q'] = $q."*"; + return $this->search($type); + } + + $matched = array(); + foreach ($orgs as $O) { + list($id, $name) = $O; + $matched[] = array('name' => Format::htmlchars($name), 'info' => $name, + 'id' => $id, '/bin/true' => $_REQUEST['q']); } - return $this->json_encode(array_values($orgs)); + return $this->json_encode(array_values($matched)); } diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 36e0f6aa8dd209427c2d896621be6985c05ffdfc..88410826294050e92b8ef09c1f4648e231eb6a94 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -79,17 +79,25 @@ class TicketsAjaxAPI extends AjaxController { } $hits = TicketModel::objects() - ->filter(Q::any(array( - 'user__emails__address__contains' => $_REQUEST['q'], - 'user__name__contains' => $_REQUEST['q'], - 'user__account__username' => $_REQUEST['q'], - 'user__org__name__contains' => $_REQUEST['q'], - ))) ->filter($visibility) ->values('user__emails__address') ->annotate(array('tickets' => SqlAggregate::COUNT('ticket_id'))) ->limit($limit); + $q = $_REQUEST['q']; + // Drop at sign in email addresses + $q = str_replace('@', ' ', $q); + + global $ost; + $hits = $ost->searcher->find($q, $hits) + ->order_by(new SqlCode('__relevance__'), QuerySet::DESC); + + if (!count($hits) && $q[strlen($q)-1] != '*') { + // Do wild-card fulltext search + $_REQUEST['q'] = $q.'*'; + return $this->lookupByEmail(); + } + foreach ($hits as $T) { $email = $T['user__emails__address']; $count = $T['tickets']; diff --git a/include/ajax.users.php b/include/ajax.users.php index 6cfed17c8c09ddc6d2c0ccffe7bc9f5822e7bf53..0dd6da74b3384f14d16800c6c36ca657220cf7d6 100644 --- a/include/ajax.users.php +++ b/include/ajax.users.php @@ -29,54 +29,61 @@ class UsersAjaxAPI extends AjaxController { Http::response(400, __('Query argument is required')); } + $q = $_REQUEST['q']; $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25; $users=array(); $emails=array(); if (!$type || !strcasecmp($type, 'remote')) { - foreach (AuthenticationBackend::searchUsers($_REQUEST['q']) as $u) { + foreach (AuthenticationBackend::searchUsers($q) as $u) { $name = new UsersName(array('first' => $u['first'], 'last' => $u['last'])); $users[] = array('email' => $u['email'], 'name'=>(string) $name, 'info' => "{$u['email']} - $name (remote)", - 'id' => "auth:".$u['id'], "/bin/true" => $_REQUEST['q']); + 'id' => "auth:".$u['id'], "/bin/true" => $q); $emails[] = $u['email']; } } if (!$type || !strcasecmp($type, 'local')) { - $remote_emails = ($emails = array_filter($emails)) - ? ' OR email.address IN ('.implode(',',db_input($emails)).') ' - : ''; - - $q = str_replace(' ', '%', $_REQUEST['q']); - $escaped = db_input($q, false); - $sql='SELECT DISTINCT user.id, email.address, name ' - .' FROM '.USER_TABLE.' user ' - .' JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id ' - .' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON (entry.object_type=\'U\' AND entry.object_id = user.id) - LEFT JOIN '.FORM_ANSWER_TABLE.' value ON (value.entry_id=entry.id) ' - .' WHERE email.address LIKE \'%'.$escaped.'%\' - OR user.name LIKE \'%'.$escaped.'%\' - OR value.value LIKE \'%'.$escaped.'%\''.$remote_emails - .' LIMIT '.$limit; - - if(($res=db_query($sql)) && db_num_rows($res)){ - while(list($id,$email,$name)=db_fetch_row($res)) { - foreach ($users as $i=>$u) { - if ($u['email'] == $email) { - unset($users[$i]); - break; - } + + $users = User::objects() + ->values_flat('id', 'emails__address', 'name') + ->limit($limit); + + global $ost; + $users = $ost->searcher->find($q, $users); + $users->order_by(new SqlCode('__relevance__'), QuerySet::DESC) + ->distinct('id'); + + if (!count($emails) && !count($users) && substr($q, strlen($q)-1) != '*') { + // Do wildcard full-text search + $_REQUEST['q'] = $q."*"; + return $this->search($type); + } + + if ($emails = array_filter($emails)) { + $users->chain(User::objects()->filter(array( + 'emails__address__in' => $emails + ))); + } + + $matches = array(); + foreach ($users as $U) { + list($id,$email,$name) = $U; + foreach ($matches as $i=>$u) { + if ($u['email'] == $email) { + unset($matches[$i]); + break; } - $name = Format::htmlchars(new UsersName($name)); - $users[] = array('email'=>$email, 'name'=>$name, 'info'=>"$email - $name", - "id" => $id, "/bin/true" => $_REQUEST['q']); } + $name = Format::htmlchars(new UsersName($name)); + $matches[] = array('email'=>$email, 'name'=>$name, 'info'=>"$email - $name", + "id" => $id, "/bin/true" => $_REQUEST['q']); } - usort($users, function($a, $b) { return strcmp($a['name'], $b['name']); }); + usort($matches, function($a, $b) { return strcmp($a['name'], $b['name']); }); } - return $this->json_encode(array_values($users)); + return $this->json_encode(array_values($matches)); } diff --git a/include/class.search.php b/include/class.search.php index a9ca110035d90bbd8b5153e26ba85f1ed202e3d3..4cf233fab8581c7a2c0e656b988750d79d460167 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -333,11 +333,11 @@ class MysqlSearchBackend extends SearchBackend { // If using boolean operators, search in boolean mode. This regex // will ensure proper placement of operators, whitespace, and quotes // in an effort to avoid crashing the query at MySQL - if (preg_match('/^(?:[(+<>~-]*(\w+[*]?|"[^"]+")[)]?(\s+|$))+$/u', $query, $T = array())) + $query = $this->quote($query); + if (preg_match('/(?=(?:^|\s)[()"+<>~-])\s*(?:[(+<>~-]*(\w+[*]?|"[^"]+")[)]?(\s+|$))+$|\w[*]/u', $query, $T = array())) $mode = ' IN BOOLEAN MODE'; #elseif (count(explode(' ', $query)) == 1) # $mode = ' WITH QUERY EXPANSION'; - $query = $this->quote($query); $search = 'MATCH (Z1.title, Z1.content) AGAINST ('.db_input($query).$mode.')'; switch ($criteria->model) { @@ -349,10 +349,37 @@ class MysqlSearchBackend extends SearchBackend { ), 'tables' => array( str_replace(array(':', '{}'), array(TABLE_PREFIX, $search), - "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`) as `ticket_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND Z3.`object_type` = 'T') LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) WHERE {}) Z1"), + "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`, Z8.`ticket_id`) as `ticket_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND Z3.`object_type` = 'T') LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') LEFT JOIN :ticket Z8 ON (Z8.`user_id` = Z6.`id`) WHERE {}) Z1"), + ) + )); + $criteria->filter(array('ticket_id'=>new SqlCode('Z1.`ticket_id`'))); + break; + + case 'User': + $criteria->extra(array( + 'select' => array( + '__relevance__' => 'Z1.`relevance`', + ), + 'tables' => array( + str_replace(array(':', '{}'), array(TABLE_PREFIX, $search), + "(SELECT Z6.`id` as `user_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') WHERE {}) Z1"), ) )); - $criteria->filter(array('ticket_id'=>new SqlCode('Z1.`ticket_id`')))->distinct('ticket_id'); + $criteria->filter(array('id'=>new SqlCode('Z1.`user_id`'))); + break; + + case 'Organization': + $criteria->extra(array( + 'select' => array( + '__relevance__' => 'Z1.`relevance`', + ), + 'tables' => array( + str_replace(array(':', '{}'), array(TABLE_PREFIX, $search), + "(SELECT Z2.`id` as `org_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:organization` Z2 ON (Z2.`id` = Z1.`object_id` AND Z1.`object_type` = 'O') WHERE {}) Z1"), + ) + )); + $criteria->filter(array('id'=>new SqlCode('Z1.`org_id`'))); + break; } // TODO: Ensure search table exists; diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index 2de6870684e91b2dab0be14d1bb03182c3f9d643..cf19f45a36db6dcbee8303203c95e84109e2107c 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -81,31 +81,30 @@ case 'search': if ($_REQUEST['query']) { $results_type=__('Search Results'); // Use an index if possible - if ($_REQUEST['search-type'] == 'typeahead' && Validator::is_email($_REQUEST['query'])) { - $tickets = $tickets->filter(array( - 'user__emails__address' => $_REQUEST['query'], - )); + if ($_REQUEST['search-type'] == 'typeahead') { + if (Validator::is_email($_REQUEST['query'])) { + $tickets = $tickets->filter(array( + 'user__emails__address' => $_REQUEST['query'], + )); + } + elseif (is_numeric($_REQUEST['query'])) { + $tickets = $tickets->filter(array( + 'number' => $_REQUEST['query'], + )); + } } - else { - $basic_search = Q::any(array( - 'number__startswith' => $_REQUEST['query'], - 'user__name__contains' => $_REQUEST['query'], - 'user__emails__address__contains' => $_REQUEST['query'], - 'user__org__name__contains' => $_REQUEST['query'], - )); - $tickets->filter($basic_search); - if (!$_REQUEST['search-type']) { - // [Search] click, consider keywords too. This is a - // relatively ugly hack. SearchBackend::find() add in a - // constraint for the search. We need to pop that off and - // include it as an OR with the above constraints - $keywords = TicketModel::objects(); - $keywords->extra(array('select' => array('ticket_id' => 'Z1.ticket_id'))); - $keywords = $ost->searcher->find($_REQUEST['query'], $keywords); - $tickets->values('ticket_id')->annotate(array('__relevance__' => new SqlCode(0.5))); - $keywords->aggregated = true; // Hack to prevent select ticket.* - $tickets->union($keywords)->order_by(new SqlCode('__relevance__'), QuerySet::DESC); + elseif ($_REQUEST['query']) { + // [Search] click, consider keywords + $__tickets = $ost->searcher->find($_REQUEST['query'], $tickets); + if (!count($__tickets)) { + // Do wildcard search if no hits + $__tickets = $ost->searcher->find($_REQUEST['query'].'*', $tickets); } + $tickets = $__tickets->distinct('ticket_id'); + $has_relevance = true; + } + if (count($tickets) == 1) { + // Redirect to ticket page } // Clear sticky search queue unset($_SESSION[$queue_key]); diff --git a/scp/js/scp.js b/scp/js/scp.js index 8408453d0dc465d5e33c6097397039a29595ecc9..0b0425e1f8874c93b5abb010412d8e353d36568d 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -746,7 +746,7 @@ $.confirm = function(message, title, options) { .append($('<input type="button" class="close"/>') .attr('value', __('Cancel')) .click(function() { hide(); }) - )).append($('<span class="buttons pull-right">test</span>') + )).append($('<span class="buttons pull-right"></span>') .append($('<input type="button"/>') .attr('value', __('OK')) .click(function() { hide(); D.resolve(body.find('input').serializeArray()); })