1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  *
3  * This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "OSPreferences.h"
8 #include "mozilla/intl/Locale.h"
9 #include "mozilla/intl/LocaleService.h"
10 #include "mozilla/WindowsVersion.h"
11 #include "nsReadableUtils.h"
12 
13 #include <windows.h>
14 
15 #ifndef __MINGW32__  // WinRT headers not yet supported by MinGW
16 #  include <roapi.h>
17 #  include <wrl.h>
18 #  include <Windows.System.UserProfile.h>
19 
20 using namespace Microsoft::WRL;
21 using namespace Microsoft::WRL::Wrappers;
22 using namespace ABI::Windows::Foundation::Collections;
23 using namespace ABI::Windows::System::UserProfile;
24 #endif
25 
26 using namespace mozilla::intl;
27 
OSPreferences()28 OSPreferences::OSPreferences() {}
29 
ReadSystemLocales(nsTArray<nsCString> & aLocaleList)30 bool OSPreferences::ReadSystemLocales(nsTArray<nsCString>& aLocaleList) {
31   MOZ_ASSERT(aLocaleList.IsEmpty());
32 
33 #ifndef __MINGW32__
34   if (IsWin8OrLater()) {
35     // Try to get language list from GlobalizationPreferences; if this fails,
36     // we'll fall back to GetUserPreferredUILanguages.
37     // Per MSDN, these APIs are not available prior to Win8.
38 
39     // RoInitialize may fail with "cannot change thread mode after it is set",
40     // if the runtime was already initialized on this thread.
41     // This appears to be harmless, and we can proceed to attempt the following
42     // runtime calls.
43     HRESULT inited = RoInitialize(RO_INIT_MULTITHREADED);
44     if (SUCCEEDED(inited) || inited == RPC_E_CHANGED_MODE) {
45       ComPtr<IGlobalizationPreferencesStatics> globalizationPrefs;
46       ComPtr<IVectorView<HSTRING>> languages;
47       uint32_t count;
48       if (SUCCEEDED(RoGetActivationFactory(
49               HStringReference(
50                   RuntimeClass_Windows_System_UserProfile_GlobalizationPreferences)
51                   .Get(),
52               IID_PPV_ARGS(&globalizationPrefs))) &&
53           SUCCEEDED(globalizationPrefs->get_Languages(&languages)) &&
54           SUCCEEDED(languages->get_Size(&count))) {
55         for (uint32_t i = 0; i < count; ++i) {
56           HString lang;
57           if (SUCCEEDED(languages->GetAt(i, lang.GetAddressOf()))) {
58             unsigned int length;
59             const wchar_t* text = lang.GetRawBuffer(&length);
60             NS_LossyConvertUTF16toASCII loc(text, length);
61             if (CanonicalizeLanguageTag(loc)) {
62               if (!loc.Contains('-')) {
63                 // DirectWrite font-name code doesn't like to be given a bare
64                 // language code with no region subtag, but the
65                 // GlobalizationPreferences API may give us one (e.g. "ja").
66                 // So if there's no hyphen in the string at this point, we use
67                 // AddLikelySubtags to get a suitable region code to
68                 // go with it.
69                 Locale locale;
70                 auto result = LocaleParser::TryParse(loc, locale);
71                 if (result.isOk() && locale.AddLikelySubtags().isOk() &&
72                     locale.Region().Present()) {
73                   loc.Append('-');
74                   loc.Append(locale.Region().Span());
75                 }
76               }
77               aLocaleList.AppendElement(loc);
78             }
79           }
80         }
81       }
82     }
83     // Only close the runtime if we successfully initialized it above,
84     // otherwise we assume it was already in use and should be left as is.
85     if (SUCCEEDED(inited)) {
86       RoUninitialize();
87     }
88   }
89 #endif
90 
91   // Per MSDN, GetUserPreferredUILanguages is available from Vista onwards,
92   // so we can use it unconditionally (although it may not work well!)
93   if (aLocaleList.IsEmpty()) {
94     // Note that according to the questioner at
95     // https://stackoverflow.com/questions/52849233/getuserpreferreduilanguages-never-returns-more-than-two-languages,
96     // this may not always return the full list of languages we'd expect.
97     // We should always get at least the first-preference lang, though.
98     ULONG numLanguages = 0;
99     DWORD cchLanguagesBuffer = 0;
100     if (!GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numLanguages, nullptr,
101                                      &cchLanguagesBuffer)) {
102       return false;
103     }
104 
105     AutoTArray<WCHAR, 64> locBuffer;
106     locBuffer.SetCapacity(cchLanguagesBuffer);
107     if (!GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numLanguages,
108                                      locBuffer.Elements(),
109                                      &cchLanguagesBuffer)) {
110       return false;
111     }
112 
113     const WCHAR* start = locBuffer.Elements();
114     const WCHAR* bufEnd = start + cchLanguagesBuffer;
115     while (bufEnd - start > 1 && *start) {
116       const WCHAR* end = start + 1;
117       while (bufEnd - end > 1 && *end) {
118         end++;
119       }
120       NS_LossyConvertUTF16toASCII loc(start, end - start);
121       if (CanonicalizeLanguageTag(loc)) {
122         aLocaleList.AppendElement(loc);
123       }
124       start = end + 1;
125     }
126   }
127 
128   return !aLocaleList.IsEmpty();
129 }
130 
ReadRegionalPrefsLocales(nsTArray<nsCString> & aLocaleList)131 bool OSPreferences::ReadRegionalPrefsLocales(nsTArray<nsCString>& aLocaleList) {
132   MOZ_ASSERT(aLocaleList.IsEmpty());
133 
134   WCHAR locale[LOCALE_NAME_MAX_LENGTH];
135   if (NS_WARN_IF(!LCIDToLocaleName(LOCALE_USER_DEFAULT, locale,
136                                    LOCALE_NAME_MAX_LENGTH, 0))) {
137     return false;
138   }
139 
140   NS_LossyConvertUTF16toASCII loc(locale);
141 
142   if (CanonicalizeLanguageTag(loc)) {
143     aLocaleList.AppendElement(loc);
144     return true;
145   }
146   return false;
147 }
148 
ToDateLCType(OSPreferences::DateTimeFormatStyle aFormatStyle)149 static LCTYPE ToDateLCType(OSPreferences::DateTimeFormatStyle aFormatStyle) {
150   switch (aFormatStyle) {
151     case OSPreferences::DateTimeFormatStyle::None:
152       return LOCALE_SLONGDATE;
153     case OSPreferences::DateTimeFormatStyle::Short:
154       return LOCALE_SSHORTDATE;
155     case OSPreferences::DateTimeFormatStyle::Medium:
156       return LOCALE_SSHORTDATE;
157     case OSPreferences::DateTimeFormatStyle::Long:
158       return LOCALE_SLONGDATE;
159     case OSPreferences::DateTimeFormatStyle::Full:
160       return LOCALE_SLONGDATE;
161     case OSPreferences::DateTimeFormatStyle::Invalid:
162     default:
163       MOZ_ASSERT_UNREACHABLE("invalid date format");
164       return LOCALE_SLONGDATE;
165   }
166 }
167 
ToTimeLCType(OSPreferences::DateTimeFormatStyle aFormatStyle)168 static LCTYPE ToTimeLCType(OSPreferences::DateTimeFormatStyle aFormatStyle) {
169   switch (aFormatStyle) {
170     case OSPreferences::DateTimeFormatStyle::None:
171       return LOCALE_STIMEFORMAT;
172     case OSPreferences::DateTimeFormatStyle::Short:
173       return LOCALE_SSHORTTIME;
174     case OSPreferences::DateTimeFormatStyle::Medium:
175       return LOCALE_SSHORTTIME;
176     case OSPreferences::DateTimeFormatStyle::Long:
177       return LOCALE_STIMEFORMAT;
178     case OSPreferences::DateTimeFormatStyle::Full:
179       return LOCALE_STIMEFORMAT;
180     case OSPreferences::DateTimeFormatStyle::Invalid:
181     default:
182       MOZ_ASSERT_UNREACHABLE("invalid time format");
183       return LOCALE_STIMEFORMAT;
184   }
185 }
186 
187 /**
188  * Windows API includes regional preferences from the user only
189  * if we pass empty locale string or if the locale string matches
190  * the current locale.
191  *
192  * Since Windows API only allows us to retrieve two options - short/long
193  * we map it to our four options as:
194  *
195  *   short  -> short
196  *   medium -> short
197  *   long   -> long
198  *   full   -> long
199  *
200  * In order to produce a single date/time format, we use CLDR pattern
201  * for combined date/time string, since Windows API does not provide an
202  * option for this.
203  */
ReadDateTimePattern(DateTimeFormatStyle aDateStyle,DateTimeFormatStyle aTimeStyle,const nsACString & aLocale,nsACString & aRetVal)204 bool OSPreferences::ReadDateTimePattern(DateTimeFormatStyle aDateStyle,
205                                         DateTimeFormatStyle aTimeStyle,
206                                         const nsACString& aLocale,
207                                         nsACString& aRetVal) {
208   nsAutoString localeName;
209   CopyASCIItoUTF16(aLocale, localeName);
210 
211   bool isDate = aDateStyle != DateTimeFormatStyle::None &&
212                 aDateStyle != DateTimeFormatStyle::Invalid;
213   bool isTime = aTimeStyle != DateTimeFormatStyle::None &&
214                 aTimeStyle != DateTimeFormatStyle::Invalid;
215 
216   // If both date and time are wanted, we'll initially read them into a
217   // local string, and then insert them into the overall date+time pattern;
218   nsAutoString str;
219   if (isDate && isTime) {
220     if (!GetDateTimeConnectorPattern(aLocale, aRetVal)) {
221       NS_WARNING("failed to get date/time connector");
222       aRetVal.AssignLiteral("{1} {0}");
223     }
224   } else if (!isDate && !isTime) {
225     aRetVal.Truncate(0);
226     return true;
227   }
228 
229   if (isDate) {
230     LCTYPE lcType = ToDateLCType(aDateStyle);
231     size_t len = GetLocaleInfoEx(
232         reinterpret_cast<const wchar_t*>(localeName.BeginReading()), lcType,
233         nullptr, 0);
234     if (len == 0) {
235       return false;
236     }
237 
238     // We're doing it to ensure the terminator will fit when Windows writes the
239     // data to its output buffer. See bug 1358159 for details.
240     str.SetLength(len);
241     GetLocaleInfoEx(reinterpret_cast<const wchar_t*>(localeName.BeginReading()),
242                     lcType, (WCHAR*)str.BeginWriting(), len);
243     str.SetLength(len - 1);  // -1 because len counts the null terminator
244 
245     // Windows uses "ddd" and "dddd" for abbreviated and full day names
246     // respectively,
247     //   https://msdn.microsoft.com/en-us/library/windows/desktop/dd317787(v=vs.85).aspx
248     // but in a CLDR/ICU-style pattern these should be "EEE" and "EEEE".
249     //   http://userguide.icu-project.org/formatparse/datetime
250     // So we fix that up here.
251     nsAString::const_iterator start, pos, end;
252     start = str.BeginReading(pos);
253     str.EndReading(end);
254     if (FindInReadable(u"dddd"_ns, pos, end)) {
255       str.ReplaceLiteral(pos - start, 4, u"EEEE");
256     } else {
257       pos = start;
258       if (FindInReadable(u"ddd"_ns, pos, end)) {
259         str.ReplaceLiteral(pos - start, 3, u"EEE");
260       }
261     }
262 
263     // Also, Windows uses lowercase "g" or "gg" for era, but ICU wants uppercase
264     // "G" (it would interpret "g" as "modified Julian day"!). So fix that.
265     int32_t index = str.FindChar('g');
266     if (index >= 0) {
267       str.Replace(index, 1, 'G');
268       // If it was a double "gg", just drop the second one.
269       index++;
270       if (str.CharAt(index) == 'g') {
271         str.Cut(index, 1);
272       }
273     }
274 
275     // If time was also requested, we need to substitute the date pattern from
276     // Windows into the date+time format that we have in aRetVal.
277     if (isTime) {
278       nsACString::const_iterator start, pos, end;
279       start = aRetVal.BeginReading(pos);
280       aRetVal.EndReading(end);
281       if (FindInReadable("{1}"_ns, pos, end)) {
282         aRetVal.Replace(pos - start, 3, NS_ConvertUTF16toUTF8(str));
283       }
284     } else {
285       aRetVal = NS_ConvertUTF16toUTF8(str);
286     }
287   }
288 
289   if (isTime) {
290     LCTYPE lcType = ToTimeLCType(aTimeStyle);
291     size_t len = GetLocaleInfoEx(
292         reinterpret_cast<const wchar_t*>(localeName.BeginReading()), lcType,
293         nullptr, 0);
294     if (len == 0) {
295       return false;
296     }
297 
298     // We're doing it to ensure the terminator will fit when Windows writes the
299     // data to its output buffer. See bug 1358159 for details.
300     str.SetLength(len);
301     GetLocaleInfoEx(reinterpret_cast<const wchar_t*>(localeName.BeginReading()),
302                     lcType, (WCHAR*)str.BeginWriting(), len);
303     str.SetLength(len - 1);
304 
305     // Windows uses "t" or "tt" for a "time marker" (am/pm indicator),
306     //   https://msdn.microsoft.com/en-us/library/windows/desktop/dd318148(v=vs.85).aspx
307     // but in a CLDR/ICU-style pattern that should be "a".
308     //   http://userguide.icu-project.org/formatparse/datetime
309     // So we fix that up here.
310     int32_t index = str.FindChar('t');
311     if (index >= 0) {
312       str.Replace(index, 1, 'a');
313       index++;
314       if (str.CharAt(index) == 't') {
315         str.Cut(index, 1);
316       }
317     }
318 
319     if (isDate) {
320       nsACString::const_iterator start, pos, end;
321       start = aRetVal.BeginReading(pos);
322       aRetVal.EndReading(end);
323       if (FindInReadable("{0}"_ns, pos, end)) {
324         aRetVal.Replace(pos - start, 3, NS_ConvertUTF16toUTF8(str));
325       }
326     } else {
327       aRetVal = NS_ConvertUTF16toUTF8(str);
328     }
329   }
330 
331   return true;
332 }
333 
RemoveObservers()334 void OSPreferences::RemoveObservers() {}
335