1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6 #include "msgCore.h"
7 #include "nsMsgTagService.h"
8 #include "nsMsgBaseCID.h"
9 #include "nsIPrefService.h"
10 #include "nsISupportsPrimitives.h"
11 #include "nsMsgI18N.h"
12 #include "nsIPrefLocalizedString.h"
13 #include "nsMsgDBView.h" // for labels migration
14 #include "nsQuickSort.h"
15 #include "nsMsgUtils.h"
16 #include "nsComponentManagerUtils.h"
17 #include "nsServiceManagerUtils.h"
18 #include "nsMemory.h"
19
20 #define STRLEN(s) (sizeof(s) - 1)
21
22 #define TAG_PREF_VERSION "version"
23 #define TAG_PREF_SUFFIX_TAG ".tag"
24 #define TAG_PREF_SUFFIX_COLOR ".color"
25 #define TAG_PREF_SUFFIX_ORDINAL ".ordinal"
26
27 static bool gMigratingKeys = false;
28
29 // Comparator to set sort order in GetAllTags().
30 struct CompareMsgTags {
31 private:
cmpCompareMsgTags32 int cmp(RefPtr<nsIMsgTag> element1, RefPtr<nsIMsgTag> element2) const {
33 // Sort nsMsgTag objects by ascending order, using their ordinal or key.
34 // The "smallest" value will be first in the sorted array,
35 // thus being the most important element.
36
37 // Only use the key if the ordinal is not defined or empty.
38 nsAutoCString value1, value2;
39 element1->GetOrdinal(value1);
40 if (value1.IsEmpty()) element1->GetKey(value1);
41 element2->GetOrdinal(value2);
42 if (value2.IsEmpty()) element2->GetKey(value2);
43
44 return strcmp(value1.get(), value2.get());
45 }
46
47 public:
EqualsCompareMsgTags48 bool Equals(RefPtr<nsIMsgTag> element1, RefPtr<nsIMsgTag> element2) const {
49 return cmp(element1, element2) == 0;
50 }
LessThanCompareMsgTags51 bool LessThan(RefPtr<nsIMsgTag> element1, RefPtr<nsIMsgTag> element2) const {
52 return cmp(element1, element2) < 0;
53 }
54 };
55
56 //
57 // nsMsgTag
58 //
NS_IMPL_ISUPPORTS(nsMsgTag,nsIMsgTag)59 NS_IMPL_ISUPPORTS(nsMsgTag, nsIMsgTag)
60
61 nsMsgTag::nsMsgTag(const nsACString& aKey, const nsAString& aTag,
62 const nsACString& aColor, const nsACString& aOrdinal)
63 : mTag(aTag), mKey(aKey), mColor(aColor), mOrdinal(aOrdinal) {}
64
~nsMsgTag()65 nsMsgTag::~nsMsgTag() {}
66
67 /* readonly attribute ACString key; */
GetKey(nsACString & aKey)68 NS_IMETHODIMP nsMsgTag::GetKey(nsACString& aKey) {
69 aKey = mKey;
70 return NS_OK;
71 }
72
73 /* readonly attribute AString tag; */
GetTag(nsAString & aTag)74 NS_IMETHODIMP nsMsgTag::GetTag(nsAString& aTag) {
75 aTag = mTag;
76 return NS_OK;
77 }
78
79 /* readonly attribute ACString color; */
GetColor(nsACString & aColor)80 NS_IMETHODIMP nsMsgTag::GetColor(nsACString& aColor) {
81 aColor = mColor;
82 return NS_OK;
83 }
84
85 /* readonly attribute ACString ordinal; */
GetOrdinal(nsACString & aOrdinal)86 NS_IMETHODIMP nsMsgTag::GetOrdinal(nsACString& aOrdinal) {
87 aOrdinal = mOrdinal;
88 return NS_OK;
89 }
90
91 //
92 // nsMsgTagService
93 //
NS_IMPL_ISUPPORTS(nsMsgTagService,nsIMsgTagService)94 NS_IMPL_ISUPPORTS(nsMsgTagService, nsIMsgTagService)
95
96 nsMsgTagService::nsMsgTagService() {
97 m_tagPrefBranch = nullptr;
98 nsCOMPtr<nsIPrefService> prefService(
99 do_GetService(NS_PREFSERVICE_CONTRACTID));
100 if (prefService)
101 prefService->GetBranch("mailnews.tags.", getter_AddRefs(m_tagPrefBranch));
102 // need to figure out how to migrate the tags only once.
103 MigrateLabelsToTags();
104 RefreshKeyCache();
105 }
106
~nsMsgTagService()107 nsMsgTagService::~nsMsgTagService() {} /* destructor code */
108
109 /* wstring getTagForKey (in string key); */
GetTagForKey(const nsACString & key,nsAString & _retval)110 NS_IMETHODIMP nsMsgTagService::GetTagForKey(const nsACString& key,
111 nsAString& _retval) {
112 nsAutoCString prefName(key);
113 if (!gMigratingKeys) ToLowerCase(prefName);
114 prefName.AppendLiteral(TAG_PREF_SUFFIX_TAG);
115 return GetUnicharPref(prefName.get(), _retval);
116 }
117
118 /* void setTagForKey (in string key); */
SetTagForKey(const nsACString & key,const nsAString & tag)119 NS_IMETHODIMP nsMsgTagService::SetTagForKey(const nsACString& key,
120 const nsAString& tag) {
121 nsAutoCString prefName(key);
122 ToLowerCase(prefName);
123 prefName.AppendLiteral(TAG_PREF_SUFFIX_TAG);
124 return SetUnicharPref(prefName.get(), tag);
125 }
126
127 /* void getKeyForTag (in wstring tag); */
GetKeyForTag(const nsAString & aTag,nsACString & aKey)128 NS_IMETHODIMP nsMsgTagService::GetKeyForTag(const nsAString& aTag,
129 nsACString& aKey) {
130 nsTArray<nsCString> prefList;
131 nsresult rv = m_tagPrefBranch->GetChildList("", prefList);
132 NS_ENSURE_SUCCESS(rv, rv);
133 // traverse the list, and look for a pref with the desired tag value.
134 // XXXbz is there a good reason to reverse the list here, or did the
135 // old code do it just to be clever and save some characters in the
136 // for loop header?
137 for (auto& prefName : mozilla::Reversed(prefList)) {
138 // We are returned the tag prefs in the form "<key>.<tag_data_type>", but
139 // since we only want the tags, just check that the string ends with "tag".
140 if (StringEndsWith(prefName, nsLiteralCString(TAG_PREF_SUFFIX_TAG))) {
141 nsAutoString curTag;
142 GetUnicharPref(prefName.get(), curTag);
143 if (aTag.Equals(curTag)) {
144 aKey = Substring(prefName, 0,
145 prefName.Length() - STRLEN(TAG_PREF_SUFFIX_TAG));
146 break;
147 }
148 }
149 }
150 ToLowerCase(aKey);
151 return NS_OK;
152 }
153
154 /* ACString getTopKey (in ACString keylist); */
GetTopKey(const nsACString & keyList,nsACString & _retval)155 NS_IMETHODIMP nsMsgTagService::GetTopKey(const nsACString& keyList,
156 nsACString& _retval) {
157 _retval.Truncate();
158 // find the most important key
159 nsTArray<nsCString> keyArray;
160 ParseString(keyList, ' ', keyArray);
161 uint32_t keyCount = keyArray.Length();
162 nsCString *topKey = nullptr, *key, topOrdinal, ordinal;
163 for (uint32_t i = 0; i < keyCount; ++i) {
164 key = &keyArray[i];
165 if (key->IsEmpty()) continue;
166
167 // ignore unknown keywords
168 nsAutoString tagValue;
169 nsresult rv = GetTagForKey(*key, tagValue);
170 if (NS_FAILED(rv) || tagValue.IsEmpty()) continue;
171
172 // new top key, judged by ordinal order?
173 rv = GetOrdinalForKey(*key, ordinal);
174 if (NS_FAILED(rv) || ordinal.IsEmpty()) ordinal = *key;
175 if ((ordinal < topOrdinal) || topOrdinal.IsEmpty()) {
176 topOrdinal = ordinal;
177 topKey = key; // copy actual result key only once - later
178 }
179 }
180 // return the most important key - if any
181 if (topKey) _retval = *topKey;
182 return NS_OK;
183 }
184
185 /* void addTagForKey (in string key, in wstring tag, in string color, in string
186 * ordinal); */
AddTagForKey(const nsACString & key,const nsAString & tag,const nsACString & color,const nsACString & ordinal)187 NS_IMETHODIMP nsMsgTagService::AddTagForKey(const nsACString& key,
188 const nsAString& tag,
189 const nsACString& color,
190 const nsACString& ordinal) {
191 nsAutoCString prefName(key);
192 ToLowerCase(prefName);
193 prefName.AppendLiteral(TAG_PREF_SUFFIX_TAG);
194 nsresult rv = SetUnicharPref(prefName.get(), tag);
195 NS_ENSURE_SUCCESS(rv, rv);
196 rv = SetColorForKey(key, color);
197 NS_ENSURE_SUCCESS(rv, rv);
198 rv = RefreshKeyCache();
199 NS_ENSURE_SUCCESS(rv, rv);
200 return SetOrdinalForKey(key, ordinal);
201 }
202
203 /* void addTag (in wstring tag, in long color); */
AddTag(const nsAString & tag,const nsACString & color,const nsACString & ordinal)204 NS_IMETHODIMP nsMsgTagService::AddTag(const nsAString& tag,
205 const nsACString& color,
206 const nsACString& ordinal) {
207 // figure out key from tag. Apply transformation stripping out
208 // illegal characters like <SP> and then convert to imap mod utf7.
209 // Then, check if we have a tag with that key yet, and if so,
210 // make it unique by appending A, AA, etc.
211 // Should we use an iterator?
212 nsAutoString transformedTag(tag);
213 transformedTag.ReplaceChar(" ()/{%*<>\\\"", '_');
214 nsAutoCString key;
215 CopyUTF16toMUTF7(transformedTag, key);
216 // We have an imap server that converts keys to upper case so we're going
217 // to normalize all keys to lower case (upper case looks ugly in prefs.js)
218 ToLowerCase(key);
219 nsAutoCString prefName(key);
220 while (true) {
221 nsAutoString tagValue;
222 nsresult rv = GetTagForKey(prefName, tagValue);
223 if (NS_FAILED(rv) || tagValue.IsEmpty() || tagValue.Equals(tag))
224 return AddTagForKey(prefName, tag, color, ordinal);
225 prefName.Append('A');
226 }
227 NS_ASSERTION(false, "can't get here");
228 return NS_ERROR_FAILURE;
229 }
230
231 /* long getColorForKey (in string key); */
GetColorForKey(const nsACString & key,nsACString & _retval)232 NS_IMETHODIMP nsMsgTagService::GetColorForKey(const nsACString& key,
233 nsACString& _retval) {
234 nsAutoCString prefName(key);
235 if (!gMigratingKeys) ToLowerCase(prefName);
236 prefName.AppendLiteral(TAG_PREF_SUFFIX_COLOR);
237 nsCString color;
238 nsresult rv = m_tagPrefBranch->GetCharPref(prefName.get(), color);
239 if (NS_SUCCEEDED(rv)) _retval = color;
240 return NS_OK;
241 }
242
243 /* long getSelectorForKey (in ACString key, out AString selector); */
GetSelectorForKey(const nsACString & key,nsAString & _retval)244 NS_IMETHODIMP nsMsgTagService::GetSelectorForKey(const nsACString& key,
245 nsAString& _retval) {
246 // Our keys are the result of MUTF-7 encoding. For CSS selectors we need
247 // to reduce this to 0-9A-Za-z_ with a leading alpha character.
248 // We encode non-alphanumeric characters using _ as an escape character
249 // and start with a leading T in all cases. This way users defining tags
250 // "selected" or "focus" don't collide with inbuilt "selected" or "focus".
251
252 // Calculate length of selector string.
253 const char* in = key.BeginReading();
254 size_t outLen = 1;
255 while (*in) {
256 if (('0' <= *in && *in <= '9') || ('A' <= *in && *in <= 'Z') ||
257 ('a' <= *in && *in <= 'z')) {
258 outLen++;
259 } else {
260 outLen += 3;
261 }
262 in++;
263 }
264
265 // Now fill selector string.
266 _retval.SetCapacity(outLen);
267 _retval.Assign('T');
268 in = key.BeginReading();
269 while (*in) {
270 if (('0' <= *in && *in <= '9') || ('A' <= *in && *in <= 'Z') ||
271 ('a' <= *in && *in <= 'z')) {
272 _retval.Append(*in);
273 } else {
274 _retval.AppendPrintf("_%02x", *in);
275 }
276 in++;
277 }
278
279 return NS_OK;
280 }
281
282 /* void setColorForKey (in ACString key, in ACString color); */
SetColorForKey(const nsACString & key,const nsACString & color)283 NS_IMETHODIMP nsMsgTagService::SetColorForKey(const nsACString& key,
284 const nsACString& color) {
285 nsAutoCString prefName(key);
286 ToLowerCase(prefName);
287 prefName.AppendLiteral(TAG_PREF_SUFFIX_COLOR);
288 if (color.IsEmpty()) {
289 m_tagPrefBranch->ClearUserPref(prefName.get());
290 return NS_OK;
291 }
292 return m_tagPrefBranch->SetCharPref(prefName.get(), color);
293 }
294
295 /* ACString getOrdinalForKey (in ACString key); */
GetOrdinalForKey(const nsACString & key,nsACString & _retval)296 NS_IMETHODIMP nsMsgTagService::GetOrdinalForKey(const nsACString& key,
297 nsACString& _retval) {
298 nsAutoCString prefName(key);
299 if (!gMigratingKeys) ToLowerCase(prefName);
300 prefName.AppendLiteral(TAG_PREF_SUFFIX_ORDINAL);
301 nsCString ordinal;
302 nsresult rv = m_tagPrefBranch->GetCharPref(prefName.get(), ordinal);
303 _retval = ordinal;
304 return rv;
305 }
306
307 /* void setOrdinalForKey (in ACString key, in ACString ordinal); */
SetOrdinalForKey(const nsACString & key,const nsACString & ordinal)308 NS_IMETHODIMP nsMsgTagService::SetOrdinalForKey(const nsACString& key,
309 const nsACString& ordinal) {
310 nsAutoCString prefName(key);
311 ToLowerCase(prefName);
312 prefName.AppendLiteral(TAG_PREF_SUFFIX_ORDINAL);
313 if (ordinal.IsEmpty()) {
314 m_tagPrefBranch->ClearUserPref(prefName.get());
315 return NS_OK;
316 }
317 return m_tagPrefBranch->SetCharPref(prefName.get(), ordinal);
318 }
319
320 /* void deleteTag (in wstring tag); */
DeleteKey(const nsACString & key)321 NS_IMETHODIMP nsMsgTagService::DeleteKey(const nsACString& key) {
322 // clear the associated prefs
323 nsAutoCString prefName(key);
324 if (!gMigratingKeys) ToLowerCase(prefName);
325 prefName.Append('.');
326
327 nsTArray<nsCString> prefNames;
328 nsresult rv = m_tagPrefBranch->GetChildList(prefName.get(), prefNames);
329 NS_ENSURE_SUCCESS(rv, rv);
330
331 for (auto& prefName : prefNames) {
332 m_tagPrefBranch->ClearUserPref(prefName.get());
333 }
334
335 return RefreshKeyCache();
336 }
337
338 /* Array<nsIMsgTag> getAllTags(); */
GetAllTags(nsTArray<RefPtr<nsIMsgTag>> & aTagArray)339 NS_IMETHODIMP nsMsgTagService::GetAllTags(
340 nsTArray<RefPtr<nsIMsgTag>>& aTagArray) {
341 aTagArray.Clear();
342
343 // get the actual tag definitions
344 nsresult rv;
345 nsTArray<nsCString> prefList;
346 rv = m_tagPrefBranch->GetChildList("", prefList);
347 NS_ENSURE_SUCCESS(rv, rv);
348 // sort them by key for ease of processing
349 prefList.Sort();
350
351 nsString tag;
352 nsCString lastKey, color, ordinal;
353 for (auto& pref : mozilla::Reversed(prefList)) {
354 // extract just the key from <key>.<info=tag|color|ordinal>
355 int32_t dotLoc = pref.RFindChar('.');
356 if (dotLoc != kNotFound) {
357 auto& key = Substring(pref, 0, dotLoc);
358 if (key != lastKey) {
359 if (!key.IsEmpty()) {
360 // .tag MUST exist (but may be empty)
361 rv = GetTagForKey(key, tag);
362 if (NS_SUCCEEDED(rv)) {
363 // .color MAY exist
364 color.Truncate();
365 GetColorForKey(key, color);
366 // .ordinal MAY exist
367 rv = GetOrdinalForKey(key, ordinal);
368 if (NS_FAILED(rv)) ordinal.Truncate();
369 // store the tag info in our array
370 aTagArray.AppendElement(new nsMsgTag(key, tag, color, ordinal));
371 }
372 }
373 lastKey = key;
374 }
375 }
376 }
377
378 // sort the non-null entries by ordinal
379 aTagArray.Sort(CompareMsgTags());
380 return NS_OK;
381 }
382
SetUnicharPref(const char * prefName,const nsAString & val)383 nsresult nsMsgTagService::SetUnicharPref(const char* prefName,
384 const nsAString& val) {
385 nsresult rv = NS_OK;
386 if (!val.IsEmpty()) {
387 rv = m_tagPrefBranch->SetStringPref(prefName, NS_ConvertUTF16toUTF8(val));
388 } else {
389 m_tagPrefBranch->ClearUserPref(prefName);
390 }
391 return rv;
392 }
393
GetUnicharPref(const char * prefName,nsAString & prefValue)394 nsresult nsMsgTagService::GetUnicharPref(const char* prefName,
395 nsAString& prefValue) {
396 nsCString valueUtf8;
397 nsresult rv =
398 m_tagPrefBranch->GetStringPref(prefName, EmptyCString(), 0, valueUtf8);
399 CopyUTF8toUTF16(valueUtf8, prefValue);
400 return rv;
401 }
402
MigrateLabelsToTags()403 nsresult nsMsgTagService::MigrateLabelsToTags() {
404 nsCString prefString;
405
406 int32_t prefVersion = 0;
407 nsresult rv = m_tagPrefBranch->GetIntPref(TAG_PREF_VERSION, &prefVersion);
408 if (NS_SUCCEEDED(rv) && prefVersion > 1)
409 return rv;
410 else if (prefVersion == 1) {
411 gMigratingKeys = true;
412 // need to convert the keys to lower case
413 nsTArray<RefPtr<nsIMsgTag>> tagArray;
414 GetAllTags(tagArray);
415 for (auto& tag : tagArray) {
416 nsAutoCString key, color, ordinal;
417 nsAutoString tagStr;
418 tag->GetKey(key);
419 tag->GetTag(tagStr);
420 tag->GetOrdinal(ordinal);
421 tag->GetColor(color);
422 DeleteKey(key);
423 ToLowerCase(key);
424 AddTagForKey(key, tagStr, color, ordinal);
425 }
426 gMigratingKeys = false;
427 } else {
428 nsCOMPtr<nsIPrefBranch> prefRoot(do_GetService(NS_PREFSERVICE_CONTRACTID));
429 nsCOMPtr<nsIPrefLocalizedString> pls;
430 nsString ucsval;
431 nsAutoCString labelKey("$label1");
432 for (int32_t i = 0; i < PREF_LABELS_MAX;) {
433 prefString.Assign(PREF_LABELS_DESCRIPTION);
434 prefString.AppendInt(i + 1);
435 rv = prefRoot->GetComplexValue(prefString.get(),
436 NS_GET_IID(nsIPrefLocalizedString),
437 getter_AddRefs(pls));
438 NS_ENSURE_SUCCESS(rv, rv);
439 pls->ToString(getter_Copies(ucsval));
440
441 prefString.Assign(PREF_LABELS_COLOR);
442 prefString.AppendInt(i + 1);
443 nsCString csval;
444 rv = prefRoot->GetCharPref(prefString.get(), csval);
445 NS_ENSURE_SUCCESS(rv, rv);
446
447 rv = AddTagForKey(labelKey, ucsval, csval, EmptyCString());
448 NS_ENSURE_SUCCESS(rv, rv);
449 labelKey.SetCharAt(++i + '1', 6);
450 }
451 }
452 m_tagPrefBranch->SetIntPref(TAG_PREF_VERSION, 2);
453 return rv;
454 }
455
IsValidKey(const nsACString & aKey,bool * aResult)456 NS_IMETHODIMP nsMsgTagService::IsValidKey(const nsACString& aKey,
457 bool* aResult) {
458 NS_ENSURE_ARG_POINTER(aResult);
459 *aResult = m_keys.Contains(aKey);
460 return NS_OK;
461 }
462
463 // refresh the local tag key array m_keys from preferences
RefreshKeyCache()464 nsresult nsMsgTagService::RefreshKeyCache() {
465 nsTArray<RefPtr<nsIMsgTag>> tagArray;
466 nsresult rv = GetAllTags(tagArray);
467 NS_ENSURE_SUCCESS(rv, rv);
468 m_keys.Clear();
469
470 uint32_t numTags = tagArray.Length();
471 m_keys.SetCapacity(numTags);
472 for (uint32_t tagIndex = 0; tagIndex < numTags; tagIndex++) {
473 nsAutoCString key;
474 tagArray[tagIndex]->GetKey(key);
475 m_keys.InsertElementAt(tagIndex, key);
476 }
477 return rv;
478 }
479