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 "nsWindowsShellService.h"
7 #include "nsIServiceManager.h"
8 #include "nsICategoryManager.h"
9 #include "nsNativeCharsetUtils.h"
10 #include "nsIPrefService.h"
11 #include "windows.h"
12 #include "shellapi.h"
13 #include "nsIFile.h"
14 #include "nsDirectoryServiceDefs.h"
15 #include "nsUnicharUtils.h"
16 #include "nsComponentManagerUtils.h"
17 #include "nsServiceManagerUtils.h"
18 #include "nsIProperties.h"
19 #include "nsString.h"
20 
21 #ifdef _WIN32_WINNT
22 #  undef _WIN32_WINNT
23 #endif
24 #define _WIN32_WINNT 0x0600
25 #define INITGUID
26 #include <shlobj.h>
27 
28 #include <mbstring.h>
29 
30 #ifndef MAX_BUF
31 #  define MAX_BUF 4096
32 #endif
33 
34 #define REG_FAILED(val) (val != ERROR_SUCCESS)
35 
NS_IMPL_ISUPPORTS(nsWindowsShellService,nsIShellService,nsIToolkitShellService)36 NS_IMPL_ISUPPORTS(nsWindowsShellService, nsIShellService,
37                   nsIToolkitShellService)
38 
39 static nsresult OpenKeyForReading(HKEY aKeyRoot, const nsAString& aKeyName,
40                                   HKEY* aKey) {
41   const nsString& flatName = PromiseFlatString(aKeyName);
42 
43   DWORD res = ::RegOpenKeyExW(aKeyRoot, flatName.get(), 0, KEY_READ, aKey);
44   switch (res) {
45     case ERROR_SUCCESS:
46       break;
47     case ERROR_ACCESS_DENIED:
48       return NS_ERROR_FILE_ACCESS_DENIED;
49     case ERROR_FILE_NOT_FOUND:
50       return NS_ERROR_NOT_AVAILABLE;
51   }
52 
53   return NS_OK;
54 }
55 
56 ///////////////////////////////////////////////////////////////////////////////
57 // Default Mail Registry Settings
58 ///////////////////////////////////////////////////////////////////////////////
59 
60 typedef enum {
61   NO_SUBSTITUTION = 0x00,
62   APP_PATH_SUBSTITUTION = 0x01
63 } SettingFlags;
64 
65 // APP_REG_NAME_MAIL and APP_REG_NAME_NEWS should be kept in synch with
66 // AppRegNameMail and AppRegNameNews in the installer file: defines.nsi.in
67 #define APP_REG_NAME_MAIL L"Thunderbird"
68 #define APP_REG_NAME_NEWS L"Thunderbird (News)"
69 #define APP_REG_NAME_CALENDAR L"Thunderbird (Calendar)"
70 #define CLS_EML "ThunderbirdEML"
71 #define CLS_MAILTOURL "Thunderbird.Url.mailto"
72 #define CLS_MIDURL "Thunderbird.Url.mid"
73 #define CLS_NEWSURL "Thunderbird.Url.news"
74 #define CLS_FEEDURL "Thunderbird.Url.feed"
75 #define CLS_WEBCALURL "Thunderbird.Url.webcal"
76 #define CLS_ICS "ThunderbirdICS"
77 #define SOP "\\shell\\open\\command"
78 #define VAL_OPEN "\"%APPPATH%\" \"%1\""
79 #define VAL_MAIL_OPEN "\"%APPPATH%\" -osint -mail \"%1\""
80 #define VAL_COMPOSE_OPEN "\"%APPPATH%\" -osint -compose \"%1\""
81 
82 #define MAKE_KEY_NAME1(PREFIX, MID) PREFIX MID
83 
84 static SETTING gMailSettings[] = {
85     // File Extension Class
86     {".eml", "", CLS_EML, NO_SUBSTITUTION},
87 
88     // File Extension Class
89     {MAKE_KEY_NAME1(CLS_EML, SOP), "", VAL_OPEN, APP_PATH_SUBSTITUTION},
90 
91     // Protocol Handler Class - for Vista and above
92     {MAKE_KEY_NAME1(CLS_MAILTOURL, SOP), "", VAL_COMPOSE_OPEN,
93      APP_PATH_SUBSTITUTION},
94     {MAKE_KEY_NAME1(CLS_MIDURL, SOP), "", VAL_OPEN, APP_PATH_SUBSTITUTION},
95 
96     // Protocol Handlers
97     {MAKE_KEY_NAME1("mailto", SOP), "", VAL_COMPOSE_OPEN,
98      APP_PATH_SUBSTITUTION},
99     {MAKE_KEY_NAME1("mid", SOP), "", VAL_OPEN, APP_PATH_SUBSTITUTION},
100 };
101 
102 static SETTING gNewsSettings[] = {
103     // Protocol Handler Class - for Vista and above
104     {MAKE_KEY_NAME1(CLS_NEWSURL, SOP), "", VAL_MAIL_OPEN,
105      APP_PATH_SUBSTITUTION},
106 
107     // Protocol Handlers
108     {MAKE_KEY_NAME1("news", SOP), "", VAL_MAIL_OPEN, APP_PATH_SUBSTITUTION},
109     {MAKE_KEY_NAME1("nntp", SOP), "", VAL_MAIL_OPEN, APP_PATH_SUBSTITUTION},
110 };
111 
112 static SETTING gCalendarSettings[] = {
113     // File Extension Class
114     {".ics", "", CLS_ICS, NO_SUBSTITUTION},
115 
116     // File Extension Class
117     {MAKE_KEY_NAME1(CLS_ICS, SOP), "", VAL_OPEN, APP_PATH_SUBSTITUTION},
118 
119     // Protocol Handlers
120     {MAKE_KEY_NAME1(CLS_WEBCALURL, SOP), "", VAL_OPEN, APP_PATH_SUBSTITUTION},
121     {MAKE_KEY_NAME1("webcal", SOP), "", VAL_OPEN, APP_PATH_SUBSTITUTION},
122     {MAKE_KEY_NAME1("webcals", SOP), "", VAL_OPEN, APP_PATH_SUBSTITUTION},
123 };
124 
GetHelperPath(nsAutoString & aPath)125 nsresult GetHelperPath(nsAutoString& aPath) {
126   nsresult rv;
127   nsCOMPtr<nsIProperties> directoryService =
128       do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv);
129   NS_ENSURE_SUCCESS(rv, rv);
130 
131   nsCOMPtr<nsIFile> appHelper;
132   rv = directoryService->Get(NS_XPCOM_CURRENT_PROCESS_DIR, NS_GET_IID(nsIFile),
133                              getter_AddRefs(appHelper));
134   NS_ENSURE_SUCCESS(rv, rv);
135 
136   rv = appHelper->Append(u"uninstall"_ns);
137   NS_ENSURE_SUCCESS(rv, rv);
138 
139   rv = appHelper->Append(u"helper.exe"_ns);
140   NS_ENSURE_SUCCESS(rv, rv);
141 
142   return appHelper->GetPath(aPath);
143 }
144 
LaunchHelper(nsAutoString & aPath,nsAutoString & aParams)145 nsresult LaunchHelper(nsAutoString& aPath, nsAutoString& aParams) {
146   SHELLEXECUTEINFOW executeInfo = {0};
147 
148   executeInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
149   executeInfo.hwnd = NULL;
150   executeInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
151   executeInfo.lpDirectory = NULL;
152   executeInfo.lpFile = aPath.get();
153   executeInfo.lpParameters = aParams.get();
154   executeInfo.nShow = SW_SHOWNORMAL;
155 
156   if (ShellExecuteExW(&executeInfo))
157     // Block until the program exits
158     WaitForSingleObject(executeInfo.hProcess, INFINITE);
159   else
160     return NS_ERROR_ABORT;
161 
162   // We're going to ignore errors here since there's nothing we can do about
163   // them, and helper.exe seems to return non-zero ret on success.
164   return NS_OK;
165 }
166 
Init()167 nsresult nsWindowsShellService::Init() {
168   WCHAR appPath[MAX_BUF];
169   if (!::GetModuleFileNameW(0, appPath, MAX_BUF)) return NS_ERROR_FAILURE;
170 
171   // Convert the path to a long path since GetModuleFileNameW returns the path
172   // that was used to launch the app which is not necessarily a long path.
173   if (!::GetLongPathNameW(appPath, appPath, MAX_BUF)) return NS_ERROR_FAILURE;
174 
175   mAppLongPath = appPath;
176 
177   return NS_OK;
178 }
179 
nsWindowsShellService()180 nsWindowsShellService::nsWindowsShellService() : mCheckedThisSession(false) {}
181 
182 NS_IMETHODIMP
IsDefaultClient(bool aStartupCheck,uint16_t aApps,bool * aIsDefaultClient)183 nsWindowsShellService::IsDefaultClient(bool aStartupCheck, uint16_t aApps,
184                                        bool* aIsDefaultClient) {
185   // If this is the first mail window, maintain internal state that we've
186   // checked this session (so that subsequent window opens don't show the
187   // default client dialog).
188   if (aStartupCheck) mCheckedThisSession = true;
189 
190   *aIsDefaultClient = true;
191 
192   // for each type,
193   if (aApps & nsIShellService::MAIL) {
194     *aIsDefaultClient &=
195         TestForDefault(gMailSettings, sizeof(gMailSettings) / sizeof(SETTING));
196     // Only check if this app is default on Vista if the previous checks
197     // indicate that this app is the default.
198     if (*aIsDefaultClient)
199       IsDefaultClientVista(nsIShellService::MAIL, aIsDefaultClient);
200   }
201   if (aApps & nsIShellService::NEWS) {
202     *aIsDefaultClient &=
203         TestForDefault(gNewsSettings, sizeof(gNewsSettings) / sizeof(SETTING));
204     // Only check if this app is default on Vista if the previous checks
205     // indicate that this app is the default.
206     if (*aIsDefaultClient)
207       IsDefaultClientVista(nsIShellService::NEWS, aIsDefaultClient);
208   }
209   if (aApps & nsIShellService::CALENDAR) {
210     *aIsDefaultClient &= TestForDefault(
211         gCalendarSettings, sizeof(gCalendarSettings) / sizeof(SETTING));
212     // Only check if this app is default on Vista if the previous checks
213     // indicate that this app is the default.
214     if (*aIsDefaultClient)
215       IsDefaultClientVista(nsIShellService::CALENDAR, aIsDefaultClient);
216   }
217   // RSS / feed protocol shell integration is not working so return true
218   // until it is fixed (bug 445823).
219   if (aApps & nsIShellService::RSS) *aIsDefaultClient &= true;
220 
221   return NS_OK;
222 }
223 
224 NS_IMETHODIMP
SetDefaultClient(bool aForAllUsers,uint16_t aApps)225 nsWindowsShellService::SetDefaultClient(bool aForAllUsers, uint16_t aApps) {
226   nsAutoString appHelperPath;
227   if (NS_FAILED(GetHelperPath(appHelperPath))) return NS_ERROR_FAILURE;
228 
229   nsAutoString params;
230   if (aForAllUsers) {
231     params.AppendLiteral(" /SetAsDefaultAppGlobal");
232   } else {
233     params.AppendLiteral(" /SetAsDefaultAppUser");
234     if (aApps & nsIShellService::MAIL) params.AppendLiteral(" Mail");
235 
236     if (aApps & nsIShellService::NEWS) params.AppendLiteral(" News");
237 
238     if (aApps & nsIShellService::CALENDAR) params.AppendLiteral(" Calendar");
239   }
240 
241   return LaunchHelper(appHelperPath, params);
242 }
243 
244 NS_IMETHODIMP
GetShouldCheckDefaultClient(bool * aResult)245 nsWindowsShellService::GetShouldCheckDefaultClient(bool* aResult) {
246   if (mCheckedThisSession) {
247     *aResult = false;
248     return NS_OK;
249   }
250 
251   nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
252   return prefs->GetBoolPref("mail.shell.checkDefaultClient", aResult);
253 }
254 
255 NS_IMETHODIMP
SetShouldCheckDefaultClient(bool aShouldCheck)256 nsWindowsShellService::SetShouldCheckDefaultClient(bool aShouldCheck) {
257   nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
258   return prefs->SetBoolPref("mail.shell.checkDefaultClient", aShouldCheck);
259 }
260 
261 /* helper routine. Iterate over the passed in settings object. */
TestForDefault(SETTING aSettings[],int32_t aSize)262 bool nsWindowsShellService::TestForDefault(SETTING aSettings[], int32_t aSize) {
263   bool isDefault = true;
264   char16_t currValue[MAX_BUF];
265   SETTING* end = aSettings + aSize;
266   for (SETTING* settings = aSettings; settings < end; ++settings) {
267     NS_ConvertUTF8toUTF16 dataLongPath(settings->valueData);
268     NS_ConvertUTF8toUTF16 key(settings->keyName);
269     NS_ConvertUTF8toUTF16 value(settings->valueName);
270     if (settings->flags & APP_PATH_SUBSTITUTION) {
271       int32_t offset = dataLongPath.Find("%APPPATH%");
272       dataLongPath.Replace(offset, 9, mAppLongPath);
273     }
274 
275     ::ZeroMemory(currValue, sizeof(currValue));
276     HKEY theKey;
277     nsresult rv = OpenKeyForReading(HKEY_CLASSES_ROOT, key, &theKey);
278     if (NS_FAILED(rv)) {
279       // Key doesn't exist
280       isDefault = false;
281       break;
282     }
283 
284     DWORD len = sizeof currValue;
285     DWORD result = ::RegQueryValueExW(theKey, value.get(), NULL, NULL,
286                                       (LPBYTE)currValue, &len);
287     // Close the key we opened.
288     ::RegCloseKey(theKey);
289     if (REG_FAILED(result) ||
290         !dataLongPath.Equals(currValue, nsCaseInsensitiveStringComparator)) {
291       // Key wasn't set, or was set to something else (something else became the
292       // default client)
293       isDefault = false;
294       break;
295     }
296   }  // for each registry key we want to look at
297 
298   return isDefault;
299 }
300 
IsDefaultClientVista(uint16_t aApps,bool * aIsDefaultClient)301 bool nsWindowsShellService::IsDefaultClientVista(uint16_t aApps,
302                                                  bool* aIsDefaultClient) {
303   IApplicationAssociationRegistration* pAAR;
304 
305   HRESULT hr = CoCreateInstance(
306       CLSID_ApplicationAssociationRegistration, NULL, CLSCTX_INPROC,
307       IID_IApplicationAssociationRegistration, (void**)&pAAR);
308 
309   if (SUCCEEDED(hr)) {
310     BOOL isDefaultMail = true;
311     BOOL isDefaultNews = true;
312     BOOL isDefaultCalendar = true;
313     if (aApps & nsIShellService::MAIL)
314       pAAR->QueryAppIsDefaultAll(AL_EFFECTIVE, APP_REG_NAME_MAIL,
315                                  &isDefaultMail);
316     if (aApps & nsIShellService::NEWS)
317       pAAR->QueryAppIsDefaultAll(AL_EFFECTIVE, APP_REG_NAME_NEWS,
318                                  &isDefaultNews);
319     if (aApps & nsIShellService::CALENDAR)
320       pAAR->QueryAppIsDefaultAll(AL_EFFECTIVE, APP_REG_NAME_CALENDAR,
321                                  &isDefaultCalendar);
322 
323     *aIsDefaultClient = isDefaultNews && isDefaultMail && isDefaultCalendar;
324 
325     pAAR->Release();
326     return true;
327   }
328   return false;
329 }
330