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 /**
7  * This is a shared part of the OSPreferences API implementation.
8  * It defines helper methods and public methods that are calling
9  * platform-specific private methods.
10  */
11 
12 #include "OSPreferences.h"
13 
14 #include "mozilla/ClearOnShutdown.h"
15 #include "mozilla/intl/DateTimePatternGenerator.h"
16 #include "mozilla/intl/DateTimeFormat.h"
17 #include "mozilla/Result.h"
18 #include "mozilla/Services.h"
19 #include "nsIObserverService.h"
20 
21 using namespace mozilla::intl;
22 
23 NS_IMPL_ISUPPORTS(OSPreferences, mozIOSPreferences)
24 
25 mozilla::StaticRefPtr<OSPreferences> OSPreferences::sInstance;
26 
27 // Return a new strong reference to the instance, creating it if necessary.
GetInstanceAddRefed()28 already_AddRefed<OSPreferences> OSPreferences::GetInstanceAddRefed() {
29   RefPtr<OSPreferences> result = sInstance;
30   if (!result) {
31     MOZ_ASSERT(NS_IsMainThread(),
32                "OSPreferences should be initialized on main thread!");
33     if (!NS_IsMainThread()) {
34       return nullptr;
35     }
36     sInstance = new OSPreferences();
37     result = sInstance;
38 
39     DebugOnly<nsresult> rv = Preferences::RegisterPrefixCallback(
40         PreferenceChanged, "intl.date_time.pattern_override");
41     MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed.");
42 
43     ClearOnShutdown(&sInstance);
44   }
45   return result.forget();
46 }
47 
48 // Return a raw pointer to the instance: not for off-main-thread use,
49 // because ClearOnShutdown means it could go away unexpectedly.
GetInstance()50 OSPreferences* OSPreferences::GetInstance() {
51   MOZ_ASSERT(NS_IsMainThread());
52   if (!sInstance) {
53     // This will create the static instance; then we just drop the extra
54     // reference.
55     RefPtr<OSPreferences> result = GetInstanceAddRefed();
56   }
57   return sInstance;
58 }
59 
Refresh()60 void OSPreferences::Refresh() {
61   nsTArray<nsCString> newLocales;
62   ReadSystemLocales(newLocales);
63 
64   if (mSystemLocales != newLocales) {
65     mSystemLocales = std::move(newLocales);
66     nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
67     if (obs) {
68       obs->NotifyObservers(nullptr, "intl:system-locales-changed", nullptr);
69     }
70   }
71 }
72 
~OSPreferences()73 OSPreferences::~OSPreferences() {
74   Preferences::UnregisterPrefixCallback(PreferenceChanged,
75                                         "intl.date_time.pattern_override");
76   RemoveObservers();
77 }
78 
79 /*static*/
PreferenceChanged(const char * aPrefName,void *)80 void OSPreferences::PreferenceChanged(const char* aPrefName,
81                                       void* /* aClosure */) {
82   if (sInstance) {
83     sInstance->mPatternCache.Clear();
84   }
85 }
86 
87 /**
88  * This method should be called by every method of OSPreferences that
89  * retrieves a locale id from external source.
90  *
91  * It attempts to retrieve as much of the locale ID as possible, cutting
92  * out bits that are not understood (non-strict behavior of ICU).
93  *
94  * It returns true if the canonicalization was successful.
95  */
CanonicalizeLanguageTag(nsCString & aLoc)96 bool OSPreferences::CanonicalizeLanguageTag(nsCString& aLoc) {
97   return LocaleService::CanonicalizeLanguageId(aLoc);
98 }
99 
100 /**
101  * This method retrieves from mozilla::intl the best pattern for a given
102  * date/time style.
103  */
GetDateTimePatternForStyle(DateTimeFormatStyle aDateStyle,DateTimeFormatStyle aTimeStyle,const nsACString & aLocale,nsACString & aRetVal)104 bool OSPreferences::GetDateTimePatternForStyle(DateTimeFormatStyle aDateStyle,
105                                                DateTimeFormatStyle aTimeStyle,
106                                                const nsACString& aLocale,
107                                                nsACString& aRetVal) {
108   DateTimeFormat::StyleBag style;
109 
110   switch (aTimeStyle) {
111     case DateTimeFormatStyle::Short:
112       style.time = Some(DateTimeFormat::Style::Short);
113       break;
114     case DateTimeFormatStyle::Medium:
115       style.time = Some(DateTimeFormat::Style::Medium);
116       break;
117     case DateTimeFormatStyle::Long:
118       style.time = Some(DateTimeFormat::Style::Long);
119       break;
120     case DateTimeFormatStyle::Full:
121       style.time = Some(DateTimeFormat::Style::Full);
122       break;
123     case DateTimeFormatStyle::None:
124     case DateTimeFormatStyle::Invalid:
125       // Do nothing.
126       break;
127   }
128 
129   switch (aDateStyle) {
130     case DateTimeFormatStyle::Short:
131       style.date = Some(DateTimeFormat::Style::Short);
132       break;
133     case DateTimeFormatStyle::Medium:
134       style.date = Some(DateTimeFormat::Style::Medium);
135       break;
136     case DateTimeFormatStyle::Long:
137       style.date = Some(DateTimeFormat::Style::Long);
138       break;
139     case DateTimeFormatStyle::Full:
140       style.date = Some(DateTimeFormat::Style::Full);
141       break;
142     case DateTimeFormatStyle::None:
143     case DateTimeFormatStyle::Invalid:
144       // Do nothing.
145       break;
146   }
147 
148   nsAutoCString locale;
149   if (aLocale.IsEmpty()) {
150     AutoTArray<nsCString, 10> regionalPrefsLocales;
151     LocaleService::GetInstance()->GetRegionalPrefsLocales(regionalPrefsLocales);
152     locale.Assign(regionalPrefsLocales[0]);
153   } else {
154     locale.Assign(aLocale);
155   }
156 
157   auto genResult =
158       DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
159   if (genResult.isErr()) {
160     return false;
161   }
162   auto generator = genResult.unwrap();
163 
164   auto dfResult = DateTimeFormat::TryCreateFromStyle(
165       MakeStringSpan(locale.get()), style, generator.get(), Nothing());
166   if (dfResult.isErr()) {
167     return false;
168   }
169   auto df = dfResult.unwrap();
170 
171   DateTimeFormat::PatternVector pattern;
172   auto patternResult = df->GetPattern(pattern);
173   if (patternResult.isErr()) {
174     return false;
175   }
176 
177   aRetVal = NS_ConvertUTF16toUTF8(pattern.begin(), pattern.length());
178   return true;
179 }
180 
181 /**
182  * This method retrieves from mozilla::intl the best skeleton for a given
183  * date/time style.
184  *
185  * This is useful for cases where an OS does not provide its own patterns,
186  * but provide ability to customize the skeleton, like alter hourCycle setting.
187  *
188  * The returned value is a skeleton that matches the styles.
189  */
GetDateTimeSkeletonForStyle(DateTimeFormatStyle aDateStyle,DateTimeFormatStyle aTimeStyle,const nsACString & aLocale,nsACString & aRetVal)190 bool OSPreferences::GetDateTimeSkeletonForStyle(DateTimeFormatStyle aDateStyle,
191                                                 DateTimeFormatStyle aTimeStyle,
192                                                 const nsACString& aLocale,
193                                                 nsACString& aRetVal) {
194   nsAutoCString pattern;
195   if (!GetDateTimePatternForStyle(aDateStyle, aTimeStyle, aLocale, pattern)) {
196     return false;
197   }
198 
199   auto genResult =
200       DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
201   if (genResult.isErr()) {
202     return false;
203   }
204 
205   nsAutoString patternAsUtf16 = NS_ConvertUTF8toUTF16(pattern);
206   DateTimeFormat::SkeletonVector skeleton;
207   auto generator = genResult.unwrap();
208   auto skeletonResult = generator->GetSkeleton(patternAsUtf16, skeleton);
209   if (skeletonResult.isErr()) {
210     return false;
211   }
212 
213   aRetVal = NS_ConvertUTF16toUTF8(skeleton.begin(), skeleton.length());
214   return true;
215 }
216 
217 /**
218  * This method checks for preferences that override the defaults
219  */
OverrideDateTimePattern(DateTimeFormatStyle aDateStyle,DateTimeFormatStyle aTimeStyle,const nsACString & aLocale,nsACString & aRetVal)220 bool OSPreferences::OverrideDateTimePattern(DateTimeFormatStyle aDateStyle,
221                                             DateTimeFormatStyle aTimeStyle,
222                                             const nsACString& aLocale,
223                                             nsACString& aRetVal) {
224   const auto PrefToMaybeString = [](const char* pref) -> Maybe<nsAutoCString> {
225     nsAutoCString value;
226     nsresult nr = Preferences::GetCString(pref, value);
227     if (NS_FAILED(nr) || value.IsEmpty()) {
228       return Nothing();
229     }
230     return Some(std::move(value));
231   };
232 
233   Maybe<nsAutoCString> timeSkeleton;
234   switch (aTimeStyle) {
235     case DateTimeFormatStyle::Short:
236       timeSkeleton =
237           PrefToMaybeString("intl.date_time.pattern_override.time_short");
238       break;
239     case DateTimeFormatStyle::Medium:
240       timeSkeleton =
241           PrefToMaybeString("intl.date_time.pattern_override.time_medium");
242       break;
243     case DateTimeFormatStyle::Long:
244       timeSkeleton =
245           PrefToMaybeString("intl.date_time.pattern_override.time_long");
246       break;
247     case DateTimeFormatStyle::Full:
248       timeSkeleton =
249           PrefToMaybeString("intl.date_time.pattern_override.time_full");
250       break;
251     default:
252       break;
253   }
254 
255   Maybe<nsAutoCString> dateSkeleton;
256   switch (aDateStyle) {
257     case DateTimeFormatStyle::Short:
258       dateSkeleton =
259           PrefToMaybeString("intl.date_time.pattern_override.date_short");
260       break;
261     case DateTimeFormatStyle::Medium:
262       dateSkeleton =
263           PrefToMaybeString("intl.date_time.pattern_override.date_medium");
264       break;
265     case DateTimeFormatStyle::Long:
266       dateSkeleton =
267           PrefToMaybeString("intl.date_time.pattern_override.date_long");
268       break;
269     case DateTimeFormatStyle::Full:
270       dateSkeleton =
271           PrefToMaybeString("intl.date_time.pattern_override.date_full");
272       break;
273     default:
274       break;
275   }
276 
277   nsAutoCString locale;
278   if (aLocale.IsEmpty()) {
279     AutoTArray<nsCString, 10> regionalPrefsLocales;
280     LocaleService::GetInstance()->GetRegionalPrefsLocales(regionalPrefsLocales);
281     locale.Assign(regionalPrefsLocales[0]);
282   } else {
283     locale.Assign(aLocale);
284   }
285 
286   const auto FillConnectorPattern = [&locale](
287                                         const nsAutoCString& datePattern,
288                                         const nsAutoCString& timePattern) {
289     nsAutoCString pattern;
290     GetDateTimeConnectorPattern(nsDependentCString(locale.get()), pattern);
291     int32_t index = pattern.Find("{1}");
292     if (index != kNotFound) {
293       pattern.Replace(index, 3, datePattern);
294     }
295     index = pattern.Find("{0}");
296     if (index != kNotFound) {
297       pattern.Replace(index, 3, timePattern);
298     }
299     return pattern;
300   };
301 
302   if (timeSkeleton && dateSkeleton) {
303     aRetVal.Assign(FillConnectorPattern(*dateSkeleton, *timeSkeleton));
304   } else if (timeSkeleton) {
305     if (aDateStyle != DateTimeFormatStyle::None) {
306       nsAutoCString pattern;
307       if (!ReadDateTimePattern(aDateStyle, DateTimeFormatStyle::None, aLocale,
308                                pattern) &&
309           !GetDateTimePatternForStyle(aDateStyle, DateTimeFormatStyle::None,
310                                       aLocale, pattern)) {
311         return false;
312       }
313       aRetVal.Assign(FillConnectorPattern(pattern, *timeSkeleton));
314     } else {
315       aRetVal.Assign(*timeSkeleton);
316     }
317   } else if (dateSkeleton) {
318     if (aTimeStyle != DateTimeFormatStyle::None) {
319       nsAutoCString pattern;
320       if (!ReadDateTimePattern(DateTimeFormatStyle::None, aTimeStyle, aLocale,
321                                pattern) &&
322           !GetDateTimePatternForStyle(DateTimeFormatStyle::None, aTimeStyle,
323                                       aLocale, pattern)) {
324         return false;
325       }
326       aRetVal.Assign(FillConnectorPattern(*dateSkeleton, pattern));
327     } else {
328       aRetVal.Assign(*dateSkeleton);
329     }
330   } else {
331     return false;
332   }
333 
334   return true;
335 }
336 
337 /**
338  * This function is a counterpart to GetDateTimeSkeletonForStyle.
339  *
340  * It takes a skeleton and returns the best available pattern for a given locale
341  * that represents the provided skeleton.
342  *
343  * For example:
344  * "Hm" skeleton for "en-US" will return "H:m"
345  */
GetPatternForSkeleton(const nsACString & aSkeleton,const nsACString & aLocale,nsACString & aRetVal)346 bool OSPreferences::GetPatternForSkeleton(const nsACString& aSkeleton,
347                                           const nsACString& aLocale,
348                                           nsACString& aRetVal) {
349   aRetVal.Truncate();
350 
351   auto genResult =
352       DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
353   if (genResult.isErr()) {
354     return false;
355   }
356 
357   nsAutoString skeletonAsUtf16 = NS_ConvertUTF8toUTF16(aSkeleton);
358   DateTimeFormat::PatternVector pattern;
359   auto generator = genResult.unwrap();
360   auto patternResult = generator->GetBestPattern(skeletonAsUtf16, pattern);
361   if (patternResult.isErr()) {
362     return false;
363   }
364 
365   aRetVal = NS_ConvertUTF16toUTF8(pattern.begin(), pattern.length());
366   return true;
367 }
368 
369 /**
370  * This function returns a pattern that should be used to join date and time
371  * patterns into a single date/time pattern string.
372  *
373  * It's useful for OSes that do not provide an API to retrieve such combined
374  * pattern.
375  *
376  * An example output is "{1}, {0}".
377  */
GetDateTimeConnectorPattern(const nsACString & aLocale,nsACString & aRetVal)378 bool OSPreferences::GetDateTimeConnectorPattern(const nsACString& aLocale,
379                                                 nsACString& aRetVal) {
380   // Check for a valid override pref and use that if present.
381   nsAutoCString value;
382   nsresult nr = Preferences::GetCString(
383       "intl.date_time.pattern_override.connector_short", value);
384   if (NS_SUCCEEDED(nr) && value.Find("{0}") != kNotFound &&
385       value.Find("{1}") != kNotFound) {
386     aRetVal = std::move(value);
387     return true;
388   }
389 
390   auto genResult =
391       DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
392   if (genResult.isErr()) {
393     return false;
394   }
395 
396   auto generator = genResult.unwrap();
397   Span<const char16_t> result = generator->GetPlaceholderPattern();
398   aRetVal = NS_ConvertUTF16toUTF8(result.data(), result.size());
399   return true;
400 }
401 
402 /**
403  * mozIOSPreferences methods
404  */
405 NS_IMETHODIMP
GetSystemLocales(nsTArray<nsCString> & aRetVal)406 OSPreferences::GetSystemLocales(nsTArray<nsCString>& aRetVal) {
407   if (!mSystemLocales.IsEmpty()) {
408     aRetVal = mSystemLocales.Clone();
409     return NS_OK;
410   }
411 
412   if (ReadSystemLocales(aRetVal)) {
413     mSystemLocales = aRetVal.Clone();
414     return NS_OK;
415   }
416 
417   // If we failed to get the system locale, we still need
418   // to return something because there are tests out there that
419   // depend on system locale to be set.
420   aRetVal.AppendElement("en-US"_ns);
421   return NS_ERROR_FAILURE;
422 }
423 
424 NS_IMETHODIMP
GetSystemLocale(nsACString & aRetVal)425 OSPreferences::GetSystemLocale(nsACString& aRetVal) {
426   if (!mSystemLocales.IsEmpty()) {
427     aRetVal = mSystemLocales[0];
428   } else {
429     AutoTArray<nsCString, 10> locales;
430     GetSystemLocales(locales);
431     if (!locales.IsEmpty()) {
432       aRetVal = locales[0];
433     }
434   }
435   return NS_OK;
436 }
437 
438 NS_IMETHODIMP
GetRegionalPrefsLocales(nsTArray<nsCString> & aRetVal)439 OSPreferences::GetRegionalPrefsLocales(nsTArray<nsCString>& aRetVal) {
440   if (!mRegionalPrefsLocales.IsEmpty()) {
441     aRetVal = mRegionalPrefsLocales.Clone();
442     return NS_OK;
443   }
444 
445   if (ReadRegionalPrefsLocales(aRetVal)) {
446     mRegionalPrefsLocales = aRetVal.Clone();
447     return NS_OK;
448   }
449 
450   // If we failed to read regional prefs locales,
451   // use system locales as last fallback.
452   return GetSystemLocales(aRetVal);
453 }
454 
ToDateTimeFormatStyle(int32_t aTimeFormat)455 static OSPreferences::DateTimeFormatStyle ToDateTimeFormatStyle(
456     int32_t aTimeFormat) {
457   switch (aTimeFormat) {
458     // See mozIOSPreferences.idl for the integer values here.
459     case 0:
460       return OSPreferences::DateTimeFormatStyle::None;
461     case 1:
462       return OSPreferences::DateTimeFormatStyle::Short;
463     case 2:
464       return OSPreferences::DateTimeFormatStyle::Medium;
465     case 3:
466       return OSPreferences::DateTimeFormatStyle::Long;
467     case 4:
468       return OSPreferences::DateTimeFormatStyle::Full;
469   }
470   return OSPreferences::DateTimeFormatStyle::Invalid;
471 }
472 
473 NS_IMETHODIMP
GetDateTimePattern(int32_t aDateFormatStyle,int32_t aTimeFormatStyle,const nsACString & aLocale,nsACString & aRetVal)474 OSPreferences::GetDateTimePattern(int32_t aDateFormatStyle,
475                                   int32_t aTimeFormatStyle,
476                                   const nsACString& aLocale,
477                                   nsACString& aRetVal) {
478   DateTimeFormatStyle dateStyle = ToDateTimeFormatStyle(aDateFormatStyle);
479   if (dateStyle == DateTimeFormatStyle::Invalid) {
480     return NS_ERROR_INVALID_ARG;
481   }
482   DateTimeFormatStyle timeStyle = ToDateTimeFormatStyle(aTimeFormatStyle);
483   if (timeStyle == DateTimeFormatStyle::Invalid) {
484     return NS_ERROR_INVALID_ARG;
485   }
486 
487   // If the user is asking for None on both, date and time style,
488   // let's exit early.
489   if (timeStyle == DateTimeFormatStyle::None &&
490       dateStyle == DateTimeFormatStyle::None) {
491     return NS_OK;
492   }
493 
494   // If the locale is not specified, default to first regional prefs locale
495   const nsACString* locale = &aLocale;
496   AutoTArray<nsCString, 10> rpLocales;
497   if (aLocale.IsEmpty()) {
498     LocaleService::GetInstance()->GetRegionalPrefsLocales(rpLocales);
499     MOZ_ASSERT(rpLocales.Length() > 0);
500     locale = &rpLocales[0];
501   }
502 
503   // Create a cache key from the locale + style options
504   nsAutoCString key(*locale);
505   key.Append(':');
506   key.AppendInt(aDateFormatStyle);
507   key.Append(':');
508   key.AppendInt(aTimeFormatStyle);
509 
510   nsCString pattern;
511   if (mPatternCache.Get(key, &pattern)) {
512     aRetVal = pattern;
513     return NS_OK;
514   }
515 
516   if (!OverrideDateTimePattern(dateStyle, timeStyle, *locale, pattern)) {
517     if (!ReadDateTimePattern(dateStyle, timeStyle, *locale, pattern)) {
518       if (!GetDateTimePatternForStyle(dateStyle, timeStyle, *locale, pattern)) {
519         return NS_ERROR_FAILURE;
520       }
521     }
522   }
523 
524   if (mPatternCache.Count() == kMaxCachedPatterns) {
525     // Don't allow unlimited cache growth; just throw it away in the case of
526     // pathological behavior where a page keeps requesting different formats
527     // and locales.
528     NS_WARNING("flushing DateTimePattern cache");
529     mPatternCache.Clear();
530   }
531   mPatternCache.InsertOrUpdate(key, pattern);
532 
533   aRetVal = pattern;
534   return NS_OK;
535 }
536 
OverrideSkeletonHourCycle(bool aIs24Hour,nsAutoCString & aSkeleton)537 void OSPreferences::OverrideSkeletonHourCycle(bool aIs24Hour,
538                                               nsAutoCString& aSkeleton) {
539   if (aIs24Hour) {
540     // If aSkeleton contains 'h' or 'K', replace with 'H' or 'k' respectively,
541     // and delete 'a' if present.
542     if (aSkeleton.FindChar('h') == -1 && aSkeleton.FindChar('K') == -1) {
543       return;
544     }
545     for (int32_t i = 0; i < int32_t(aSkeleton.Length()); ++i) {
546       switch (aSkeleton[i]) {
547         case 'a':
548           aSkeleton.Cut(i, 1);
549           --i;
550           break;
551         case 'h':
552           aSkeleton.SetCharAt('H', i);
553           break;
554         case 'K':
555           aSkeleton.SetCharAt('k', i);
556           break;
557       }
558     }
559   } else {
560     // If skeleton contains 'H' or 'k', replace with 'h' or 'K' respectively,
561     // and add 'a' unless already present.
562     if (aSkeleton.FindChar('H') == -1 && aSkeleton.FindChar('k') == -1) {
563       return;
564     }
565     bool foundA = false;
566     for (size_t i = 0; i < aSkeleton.Length(); ++i) {
567       switch (aSkeleton[i]) {
568         case 'a':
569           foundA = true;
570           break;
571         case 'H':
572           aSkeleton.SetCharAt('h', i);
573           break;
574         case 'k':
575           aSkeleton.SetCharAt('K', i);
576           break;
577       }
578     }
579     if (!foundA) {
580       aSkeleton.Append(char16_t('a'));
581     }
582   }
583 }
584