From 865230e4209f7b5c8ed96f526eb6507b384d2068 Mon Sep 17 00:00:00 2001 From: Bostjan Skufca Date: Thu, 14 Apr 2016 02:13:00 +0200 Subject: [PATCH 1/7] Autocomplete: fix undefined variable notice if no addressbook is configured for autocomplete --- program/steps/mail/autocomplete.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/steps/mail/autocomplete.inc b/program/steps/mail/autocomplete.inc index 3023ecfb7..cfa579532 100644 --- a/program/steps/mail/autocomplete.inc +++ b/program/steps/mail/autocomplete.inc @@ -58,8 +58,8 @@ else { $book_types = (array) $RCMAIL->config->get('autocomplete_addressbooks', 'sql'); } +$contacts = array(); if (!empty($book_types) && strlen($search)) { - $contacts = array(); $sort_keys = array(); $books_num = count($book_types); $search_lc = mb_strtolower($search); From ed55af4aa989a6facbb2110c3d2281b628f177ee Mon Sep 17 00:00:00 2001 From: Bostjan Skufca Date: Thu, 14 Apr 2016 02:28:19 +0200 Subject: [PATCH 2/7] Autocomplete: trigger "contacts_autocomplete_after" hook processing Provide existing list of contact suggestions as an argument - it might be unclear now what logic plugin developers decide to implement, but data for that logic should be provided upfront, and that includes list of suggestions that RC itself comes up with. Plugin logic might then replace the list entirely, or just rearrange its entries, or use part of the list when it runs out of own ideas, or do something entirely different. --- program/steps/mail/autocomplete.inc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/program/steps/mail/autocomplete.inc b/program/steps/mail/autocomplete.inc index cfa579532..61b43ef57 100644 --- a/program/steps/mail/autocomplete.inc +++ b/program/steps/mail/autocomplete.inc @@ -175,5 +175,14 @@ if (!empty($book_types) && strlen($search)) { } } + +// Allow autocomplete result optimization via plugin +$pluginResult = $RCMAIL->plugins->exec_hook('contacts_autocomplete_after', array( + 'search' => $search, + 'contacts' => $contacts, // Provide already-found contacts to plugin if they are required +)); +$contacts = $pluginResult['contacts']; + + $OUTPUT->command('ksearch_query_results', $contacts, $search, $reqid); $OUTPUT->send(); From a15b2d59984898899cc7f3fb62ec65e6d4f6a8c8 Mon Sep 17 00:00:00 2001 From: Bostjan Skufca Date: Thu, 14 Apr 2016 02:39:54 +0200 Subject: [PATCH 3/7] Autocomplete refactoring: replace variable name '$id' with '$abook_id' Reason: Having genericly named variable $id in nested loops makes code unreadable. Replacing generic name '$id' with '$ENTITIY_id' format removes all ambiguity. --- program/steps/mail/autocomplete.inc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/program/steps/mail/autocomplete.inc b/program/steps/mail/autocomplete.inc index 61b43ef57..809bd66f1 100644 --- a/program/steps/mail/autocomplete.inc +++ b/program/steps/mail/autocomplete.inc @@ -64,8 +64,8 @@ if (!empty($book_types) && strlen($search)) { $books_num = count($book_types); $search_lc = mb_strtolower($search); - foreach ($book_types as $id) { - $abook = $RCMAIL->get_address_book($id); + foreach ($book_types as $abook_id) { + $abook = $RCMAIL->get_address_book($abook_id); $abook->set_pagesize($MAXNUM); if ($result = $abook->search($RCMAIL->config->get('contactlist_fields'), $search, $mode, true, true, 'email')) { @@ -135,7 +135,7 @@ if (!empty($book_types) && strlen($search)) { 'email' => $email, 'type' => 'group', 'id' => $group['ID'], - 'source' => $id, + 'source' => $abook_id, ); if (count($contacts) >= $MAXNUM) { @@ -152,7 +152,7 @@ if (!empty($book_types) && strlen($search)) { 'name' => $group['name'] . ' (' . intval($result->count) . ')', 'type' => 'group', 'id' => $group['ID'], - 'source' => $id + 'source' => $abook_id, ); if (count($contacts) >= $MAXNUM) { From 05c7d49a37ec44c7588a1fcff71f091e8c46c27b Mon Sep 17 00:00:00 2001 From: Bostjan Skufca Date: Thu, 14 Apr 2016 02:45:21 +0200 Subject: [PATCH 4/7] Autocomplete search: add id and source (addressbook) into resulting contact data array Two reasons: - provide it to plugin backend functions that use 'contacts_autocomplete_after' hook - provide it to frontend Why to frontend? If plugin JS adds an 'autocomplete_insert' hook we need to provide it with exact autocomplete data. Providing it with name and email address only, without pinpointing exact origin of this autocomplete result, will severely limit learning capabilities of potential future autocomplete implementations. --- program/steps/mail/autocomplete.inc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/program/steps/mail/autocomplete.inc b/program/steps/mail/autocomplete.inc index 809bd66f1..31480ca76 100644 --- a/program/steps/mail/autocomplete.inc +++ b/program/steps/mail/autocomplete.inc @@ -92,7 +92,12 @@ if (!empty($book_types) && strlen($search)) { // skip duplicates if (empty($contacts[$index])) { - $contact = array('name' => $contact, 'type' => $sql_arr['_type']); + $contact = array( + 'name' => $contact, + 'type' => $sql_arr['_type'], + 'id' => $sql_arr['contact_id'], + 'source' => $abook_id, + ); if (($display = rcube_addressbook::compose_search_name($sql_arr, $email, $name)) && $display != $contact['name']) { $contact['display'] = $display; From 1791c3e3d7265a5a706e010974ebbf4bc97a1339 Mon Sep 17 00:00:00 2001 From: Bostjan Skufca Date: Thu, 14 Apr 2016 02:57:21 +0200 Subject: [PATCH 5/7] Autocomplete/frontend: provide functions that hook into 'autocomplete_insert' actual search and result data Reason: In order to enable development of sophisticated autocomplete algorithms, they will need to process all the data relevant to autocomplete - what was the search string and which result was the correct one. Example to illustrate the need: Say we are talking about these two people of interest, who are in the address book, among others: - person 1: Bostjan Skufca - person 2: Bostjan SkuBIC Our user is used to think about the first person by the first name, "Bostjan", as he is an old friend. The second one is a colleague at work where people call themselves mostly by surnames, "Skubic" in this case. Without this data provided to 'autocomplete_insert', there is no way for RC to learn that when our user enters "bos" in the To: field he thinks about person #1 (Bostjan SkuFCA), and when he starts typing "sku" he means person #2 (Bostjan SkuBIC). --- program/js/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/program/js/app.js b/program/js/app.js index 075600c3d..df9717200 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -5182,7 +5182,7 @@ function rcube_webmail() this.set_caret_pos(this.ksearch_input, p + insert.length); if (trigger) { - this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert, data:this.env.contacts[id] }); + this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert, data:this.env.contacts[id], search:this.ksearch_value, result_type:'contact' }); this.compose_type_activity++; } }; @@ -5191,7 +5191,7 @@ function rcube_webmail() { if (this.group2expand[id]) { this.group2expand[id].input.value = this.group2expand[id].input.value.replace(this.group2expand[id].name, recipients); - this.triggerEvent('autocomplete_insert', { field:this.group2expand[id].input, insert:recipients }); + this.triggerEvent('autocomplete_insert', { field:this.group2expand[id].input, insert:recipients, data:this.group2expand[id], search:this.ksearch_value, result_type:'group' }); this.group2expand[id] = null; this.compose_type_activity++; } From f919e8ffa6d911d083b606b6412146fedc4b51d9 Mon Sep 17 00:00:00 2001 From: Bostjan Skufca Date: Fri, 15 Apr 2016 02:22:19 +0200 Subject: [PATCH 6/7] Autocomplete: change type from 'contact' to 'person' to sync with what is expected from LDAP. Explanation: Alternative would be to leave type empty, as it is when contact comes form SQL source. But this feels overly ambiguous and may cause problems in the future. --- program/js/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/js/app.js b/program/js/app.js index df9717200..9a95feb31 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -5182,7 +5182,7 @@ function rcube_webmail() this.set_caret_pos(this.ksearch_input, p + insert.length); if (trigger) { - this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert, data:this.env.contacts[id], search:this.ksearch_value, result_type:'contact' }); + this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert, data:this.env.contacts[id], search:this.ksearch_value, result_type:'person' }); this.compose_type_activity++; } }; From 12756e2297b0a9e64ab961af45fe9a35838cd85b Mon Sep 17 00:00:00 2001 From: Bostjan Skufca Date: Fri, 15 Apr 2016 04:27:26 +0200 Subject: [PATCH 7/7] Autocomplete: store last searched-for string into separate variable for later consumption Reason: Autocompleting person contacts works as expected - ksearch_value is available and passed to triggerEvent. But with group autocomplete, ksearch_value is reset (to null) and triggerEvent call lacks necessary data. --- program/js/app.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/program/js/app.js b/program/js/app.js index 9a95feb31..8ffea107c 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -5182,7 +5182,8 @@ function rcube_webmail() this.set_caret_pos(this.ksearch_input, p + insert.length); if (trigger) { - this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert, data:this.env.contacts[id], search:this.ksearch_value, result_type:'person' }); + this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert, data:this.env.contacts[id], search:this.ksearch_value_last, result_type:'person' }); + this.ksearch_value_last = null; this.compose_type_activity++; } }; @@ -5191,7 +5192,8 @@ function rcube_webmail() { if (this.group2expand[id]) { this.group2expand[id].input.value = this.group2expand[id].input.value.replace(this.group2expand[id].name, recipients); - this.triggerEvent('autocomplete_insert', { field:this.group2expand[id].input, insert:recipients, data:this.group2expand[id], search:this.ksearch_value, result_type:'group' }); + this.triggerEvent('autocomplete_insert', { field:this.group2expand[id].input, insert:recipients, data:this.group2expand[id], search:this.ksearch_value_last, result_type:'group' }); + this.ksearch_value_last = null; this.group2expand[id] = null; this.compose_type_activity++; } @@ -5234,6 +5236,7 @@ function rcube_webmail() var old_value = this.ksearch_value; this.ksearch_value = q; + this.ksearch_value_last = q; // Group expansion clears ksearch_value before calling autocomplete_insert trigger, therefore store it in separate variable for later consumption. // ...string is empty if (!q.length)