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