summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/src/ABQueryUtils.jsm
blob: 4944971ddf9657fa79796bcb22b83460b28dc76c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * This file contains helper methods for dealing with addressbook search URIs.
 */

const EXPORTED_SYMBOLS = [
  "getSearchTokens",
  "getModelQuery",
  "modelQueryHasUserValue",
  "generateQueryURI",
  "encodeABTermValue",
];

/**
 * Parse the multiword search string to extract individual search terms
 * (separated on the basis of spaces) or quoted exact phrases to search
 * against multiple fields of the addressbook cards.
 *
 * @param {string} aSearchString - The full search string entered by the user.
 *
 * @returns {Array} Array of separated search terms from the full search string.
 */
function getSearchTokens(aSearchString) {
  // Trim leading and trailing whitespace and comma(s) to prevent empty search
  // words when splitting unquoted parts of search string below.
  let searchString = aSearchString
    .replace(/^[,\s]+/, "")
    .replace(/[,\s]+$/, "");
  if (searchString == "") {
    return [];
  }

  let quotedTerms = [];

  // Split up multiple search words to create a *foo* and *bar* search against
  // search fields, using the OR-search template from modelQuery for each word.
  // If the search query has quoted terms like "foo bar", extract them as is.
  let startIndex;
  while ((startIndex = searchString.indexOf('"')) != -1) {
    let endIndex = searchString.indexOf('"', startIndex + 1);
    if (endIndex == -1) {
      endIndex = searchString.length;
    }

    quotedTerms.push(searchString.substring(startIndex + 1, endIndex));
    let query = searchString.substring(0, startIndex);
    if (endIndex < searchString.length) {
      query += searchString.substr(endIndex + 1);
    }

    searchString = query.trim();
  }

  let searchWords = [];
  if (searchString.length != 0) {
    // Split non-quoted search terms on whitespace and comma(s): Allow flexible
    // incremental searches, and prevent false negatives for |Last, First| with
    // |View > Show Name As > Last, First|, where comma is not found in data.
    searchWords = quotedTerms.concat(searchString.split(/[,\s]+/));
  } else {
    searchWords = quotedTerms;
  }

  return searchWords;
}

/**
 * For AB quicksearch or recipient autocomplete, get the normal or phonetic model
 * query URL part from prefs, allowing users to customize these searches.
 *
 * @param {string} aBasePrefName - The full pref name of default, non-phonetic
 *   model query, e.g. mail.addr_book.quicksearchquery.format. If phonetic
 *   search is used, corresponding pref must exist:
 *   e.g. mail.addr_book.quicksearchquery.format.phonetic
 * @returns {boolean} depending on mail.addr_book.show_phonetic_fields pref,
 *   the value of aBasePrefName or aBasePrefName + ".phonetic"
 */
function getModelQuery(aBasePrefName) {
  let modelQuery = "";
  if (
    Services.prefs.getComplexValue(
      "mail.addr_book.show_phonetic_fields",
      Ci.nsIPrefLocalizedString
    ).data == "true"
  ) {
    modelQuery = Services.prefs.getCharPref(aBasePrefName + ".phonetic");
  } else {
    modelQuery = Services.prefs.getCharPref(aBasePrefName);
  }
  // remove leading "?" to migrate existing customized values for mail.addr_book.quicksearchquery.format
  // todo: could this be done in a once-off migration at install time to avoid repetitive calls?
  if (modelQuery.startsWith("?")) {
    modelQuery = modelQuery.slice(1);
  }
  return modelQuery;
}

/**
 * Check if the currently used pref with the model query was customized by user.
 *
 * @param {string} aBasePrefName - The full pref name of default, non-phonetic
 *   model query, e.g. mail.addr_book.quicksearchquery.format
 *   If phonetic search is used, corresponding pref must exist:
 *   e.g. mail.addr_book.quicksearchquery.format.phonetic
 * @returns {boolean} true or false
 */
function modelQueryHasUserValue(aBasePrefName) {
  if (
    Services.prefs.getComplexValue(
      "mail.addr_book.show_phonetic_fields",
      Ci.nsIPrefLocalizedString
    ).data == "true"
  ) {
    return Services.prefs.prefHasUserValue(aBasePrefName + ".phonetic");
  }
  return Services.prefs.prefHasUserValue(aBasePrefName);
}

/*
 * Given a database model query and a list of search tokens,
 * return query URI.
 *
 * @param aModelQuery database model query
 * @param aSearchWords an array of search tokens.
 *
 * @return query URI.
 */
function generateQueryURI(aModelQuery, aSearchWords) {
  // If there are no search tokens, we simply return an empty string.
  if (!aSearchWords || aSearchWords.length == 0) {
    return "";
  }

  let queryURI = "";
  aSearchWords.forEach(
    searchWord =>
      (queryURI += aModelQuery.replace(/@V/g, encodeABTermValue(searchWord)))
  );

  // queryURI has all the (or(...)) searches, link them up with (and(...)).
  queryURI = "?(and" + queryURI + ")";

  return queryURI;
}

/**
 * Encode the string passed as value into an addressbook search term.
 * The '(' and ')' characters are special for the addressbook
 * search query language, but are not escaped in encodeURIComponent()
 * so must be done manually on top of it.
 */
function encodeABTermValue(aString) {
  return encodeURIComponent(aString)
    .replace(/\(/g, "%28")
    .replace(/\)/g, "%29");
}