1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
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 "ScheduledTask.h"
8 
9 #include <string>
10 #include <time.h>
11 
12 #include <comutil.h>
13 #include <taskschd.h>
14 
15 #include "readstrings.h"
16 #include "mozilla/RefPtr.h"
17 #include "mozilla/ScopeExit.h"
18 #include "mozilla/UniquePtr.h"
19 #include "mozilla/WinHeaderOnlyUtils.h"
20 #include "WindowsDefaultBrowser.h"
21 
22 #include "DefaultBrowser.h"
23 
24 const wchar_t* kTaskVendor = L"" MOZ_APP_VENDOR;
25 // kTaskName should have the unique token appended before being used.
26 const wchar_t* kTaskName = L"" MOZ_APP_DISPLAYNAME " Default Browser Agent ";
27 
28 // The task scheduler requires its time values to come in the form of a string
29 // in the format YYYY-MM-DDTHH:MM:SSZ. This format string is used to get that
30 // out of the C library wcsftime function.
31 const wchar_t* kTimeFormat = L"%Y-%m-%dT%H:%M:%SZ";
32 // The expanded time string should always be this length, for example:
33 // 2020-02-12T16:59:32Z
34 const size_t kTimeStrMaxLen = 20;
35 
36 #define ENSURE(x)         \
37   if (FAILED(hr = (x))) { \
38     LOG_ERROR(hr);        \
39     return hr;            \
40   }
41 
42 struct SysFreeStringDeleter {
operator ()SysFreeStringDeleter43   void operator()(BSTR aPtr) { ::SysFreeString(aPtr); }
44 };
45 using BStrPtr = mozilla::UniquePtr<OLECHAR, SysFreeStringDeleter>;
46 
GetTaskDescription(mozilla::UniquePtr<wchar_t[]> & description)47 bool GetTaskDescription(mozilla::UniquePtr<wchar_t[]>& description) {
48   mozilla::UniquePtr<wchar_t[]> installPath;
49   bool success = GetInstallDirectory(installPath);
50   if (!success) {
51     LOG_ERROR_MESSAGE(L"Failed to get install directory");
52     return false;
53   }
54   const wchar_t* iniFormat = L"%s\\defaultagent_localized.ini";
55   int bufferSize = _scwprintf(iniFormat, installPath.get());
56   ++bufferSize;  // Extra character for terminating null
57   mozilla::UniquePtr<wchar_t[]> iniPath =
58       mozilla::MakeUnique<wchar_t[]>(bufferSize);
59   _snwprintf_s(iniPath.get(), bufferSize, _TRUNCATE, iniFormat,
60                installPath.get());
61 
62   IniReader reader(iniPath.get());
63   reader.AddKey("DefaultBrowserAgentTaskDescription", &description);
64   int status = reader.Read();
65   if (status != OK) {
66     LOG_ERROR_MESSAGE(L"Failed to read task description: %d", status);
67     return false;
68   }
69   return true;
70 }
71 
RegisterTask(const wchar_t * uniqueToken,BSTR startTime)72 HRESULT RegisterTask(const wchar_t* uniqueToken,
73                      BSTR startTime /* = nullptr */) {
74   // Do data migration during the task installation. This might seem like it
75   // belongs in UpdateTask, but we want to be able to call
76   //    RemoveTasks();
77   //    RegisterTask();
78   // and still have data migration happen. Also, UpdateTask calls this function,
79   // so migration will still get run in that case.
80   MaybeMigrateCurrentDefault();
81 
82   // Make sure we don't try to register a task that already exists.
83   RemoveTasks(uniqueToken, WhichTasks::WdbaTaskOnly);
84 
85   // If we create a folder and then fail to create the task, we need to
86   // remember to delete the folder so that whatever set of permissions it ends
87   // up with doesn't interfere with trying to create the task again later, and
88   // so that we don't just leave an empty folder behind.
89   bool createdFolder = false;
90 
91   HRESULT hr = S_OK;
92   RefPtr<ITaskService> scheduler;
93   ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
94                           IID_ITaskService, getter_AddRefs(scheduler)));
95 
96   ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
97 
98   RefPtr<ITaskFolder> rootFolder;
99   BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\"));
100   ENSURE(
101       scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder)));
102 
103   RefPtr<ITaskFolder> taskFolder;
104   BStrPtr vendorBStr = BStrPtr(SysAllocString(kTaskVendor));
105   if (FAILED(rootFolder->GetFolder(vendorBStr.get(),
106                                    getter_AddRefs(taskFolder)))) {
107     hr = rootFolder->CreateFolder(vendorBStr.get(), VARIANT{},
108                                   getter_AddRefs(taskFolder));
109     if (SUCCEEDED(hr)) {
110       createdFolder = true;
111     } else if (hr != HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) {
112       // The folder already existing isn't an error, but anything else is.
113       LOG_ERROR(hr);
114       return hr;
115     }
116   }
117 
118   auto cleanupFolder =
119       mozilla::MakeScopeExit([hr, createdFolder, &rootFolder, &vendorBStr] {
120         if (createdFolder && FAILED(hr)) {
121           // If this fails, we can't really handle that intelligently, so
122           // don't even bother to check the return code.
123           rootFolder->DeleteFolder(vendorBStr.get(), 0);
124         }
125       });
126 
127   RefPtr<ITaskDefinition> newTask;
128   ENSURE(scheduler->NewTask(0, getter_AddRefs(newTask)));
129 
130   mozilla::UniquePtr<wchar_t[]> description;
131   if (!GetTaskDescription(description)) {
132     return E_FAIL;
133   }
134   BStrPtr descriptionBstr = BStrPtr(SysAllocString(description.get()));
135 
136   RefPtr<IRegistrationInfo> taskRegistration;
137   ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(taskRegistration)));
138   ENSURE(taskRegistration->put_Description(descriptionBstr.get()));
139 
140   RefPtr<ITaskSettings> taskSettings;
141   ENSURE(newTask->get_Settings(getter_AddRefs(taskSettings)));
142   ENSURE(taskSettings->put_DisallowStartIfOnBatteries(VARIANT_FALSE));
143   ENSURE(taskSettings->put_MultipleInstances(TASK_INSTANCES_IGNORE_NEW));
144   ENSURE(taskSettings->put_StartWhenAvailable(VARIANT_TRUE));
145   ENSURE(taskSettings->put_StopIfGoingOnBatteries(VARIANT_FALSE));
146   // This cryptic string means "35 minutes". So, if the task runs for longer
147   // than that, the process will be killed, because that should never happen.
148   BStrPtr execTimeLimitBStr = BStrPtr(SysAllocString(L"PT35M"));
149   ENSURE(taskSettings->put_ExecutionTimeLimit(execTimeLimitBStr.get()));
150 
151   RefPtr<IRegistrationInfo> regInfo;
152   ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(regInfo)));
153 
154   ENSURE(regInfo->put_Author(vendorBStr.get()));
155 
156   RefPtr<ITriggerCollection> triggers;
157   ENSURE(newTask->get_Triggers(getter_AddRefs(triggers)));
158 
159   RefPtr<ITrigger> newTrigger;
160   ENSURE(triggers->Create(TASK_TRIGGER_DAILY, getter_AddRefs(newTrigger)));
161 
162   RefPtr<IDailyTrigger> dailyTrigger;
163   ENSURE(newTrigger->QueryInterface(IID_IDailyTrigger,
164                                     getter_AddRefs(dailyTrigger)));
165 
166   if (startTime) {
167     ENSURE(dailyTrigger->put_StartBoundary(startTime));
168   } else {
169     // The time that the task is scheduled to run at every day is taken from the
170     // time in the trigger's StartBoundary property. We'll set this to the
171     // current time, on the theory that the time at which we're being installed
172     // is a time that the computer is likely to be on other days. If our
173     // theory is wrong and the computer is offline at the scheduled time, then
174     // because we've set StartWhenAvailable above, the task will run whenever
175     // it wakes up. Since our task is entirely in the background and doesn't use
176     // a lot of resources, we're not concerned about it bothering the user if it
177     // runs while they're actively using this computer.
178     time_t now_t = time(nullptr);
179     // Subtract a minute from the current time, to avoid "winning" a potential
180     // race with the scheduler that might have it start the task immediately
181     // after we register it, if we finish doing that and then it evaluates the
182     // trigger during the same second. We haven't seen this happen in practice,
183     // but there's no documented guarantee that it won't, so let's be sure.
184     now_t -= 60;
185 
186     tm now_tm;
187     errno_t errno_rv = gmtime_s(&now_tm, &now_t);
188     if (errno_rv != 0) {
189       // The C runtime has a (private) function to convert Win32 error codes to
190       // errno values, but there's nothing that goes the other way, and it
191       // isn't worth including one here for something that's this unlikely to
192       // fail anyway. So just return a generic error.
193       hr = HRESULT_FROM_WIN32(ERROR_INVALID_TIME);
194       LOG_ERROR(hr);
195       return hr;
196     }
197 
198     mozilla::UniquePtr<wchar_t[]> timeStr =
199         mozilla::MakeUnique<wchar_t[]>(kTimeStrMaxLen + 1);
200 
201     if (wcsftime(timeStr.get(), kTimeStrMaxLen + 1, kTimeFormat, &now_tm) ==
202         0) {
203       hr = E_NOT_SUFFICIENT_BUFFER;
204       LOG_ERROR(hr);
205       return hr;
206     }
207 
208     BStrPtr startTimeBStr = BStrPtr(SysAllocString(timeStr.get()));
209     ENSURE(dailyTrigger->put_StartBoundary(startTimeBStr.get()));
210   }
211 
212   ENSURE(dailyTrigger->put_DaysInterval(1));
213 
214   RefPtr<IActionCollection> actions;
215   ENSURE(newTask->get_Actions(getter_AddRefs(actions)));
216 
217   RefPtr<IAction> action;
218   ENSURE(actions->Create(TASK_ACTION_EXEC, getter_AddRefs(action)));
219 
220   RefPtr<IExecAction> execAction;
221   ENSURE(action->QueryInterface(IID_IExecAction, getter_AddRefs(execAction)));
222 
223   BStrPtr binaryPathBStr =
224       BStrPtr(SysAllocString(mozilla::GetFullBinaryPath().get()));
225   ENSURE(execAction->put_Path(binaryPathBStr.get()));
226 
227   std::wstring taskArgs = L"do-task \"";
228   taskArgs += uniqueToken;
229   taskArgs += L"\"";
230   BStrPtr argsBStr = BStrPtr(SysAllocString(taskArgs.c_str()));
231   ENSURE(execAction->put_Arguments(argsBStr.get()));
232 
233   std::wstring taskName(kTaskName);
234   taskName += uniqueToken;
235   BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str()));
236 
237   RefPtr<IRegisteredTask> registeredTask;
238   ENSURE(taskFolder->RegisterTaskDefinition(
239       taskNameBStr.get(), newTask, TASK_CREATE_OR_UPDATE, VARIANT{}, VARIANT{},
240       TASK_LOGON_INTERACTIVE_TOKEN, VARIANT{}, getter_AddRefs(registeredTask)));
241 
242   return hr;
243 }
244 
UpdateTask(const wchar_t * uniqueToken)245 HRESULT UpdateTask(const wchar_t* uniqueToken) {
246   RefPtr<ITaskService> scheduler;
247   HRESULT hr = S_OK;
248   ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
249                           IID_ITaskService, getter_AddRefs(scheduler)));
250 
251   ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
252 
253   RefPtr<ITaskFolder> taskFolder;
254   BStrPtr folderBStr = BStrPtr(SysAllocString(kTaskVendor));
255 
256   if (FAILED(
257           scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder)))) {
258     // If our folder doesn't exist, create it and the task.
259     return RegisterTask(uniqueToken);
260   }
261 
262   std::wstring taskName(kTaskName);
263   taskName += uniqueToken;
264   BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str()));
265 
266   RefPtr<IRegisteredTask> task;
267   if (FAILED(taskFolder->GetTask(taskNameBStr.get(), getter_AddRefs(task)))) {
268     // If our task doesn't exist at all, just create one.
269     return RegisterTask(uniqueToken);
270   }
271 
272   // If we have a task registered already, we need to recreate it because
273   // something might have changed that we need to update. But we don't
274   // want to restart the schedule from now, because that might mean the
275   // task never runs at all for e.g. Nightly. So create a new task, but
276   // first get and preserve the existing trigger.
277   RefPtr<ITaskDefinition> definition;
278   if (FAILED(task->get_Definition(getter_AddRefs(definition)))) {
279     // This task is broken, make a new one.
280     return RegisterTask(uniqueToken);
281   }
282 
283   RefPtr<ITriggerCollection> triggerList;
284   if (FAILED(definition->get_Triggers(getter_AddRefs(triggerList)))) {
285     // This task is broken, make a new one.
286     return RegisterTask(uniqueToken);
287   }
288 
289   RefPtr<ITrigger> trigger;
290   if (FAILED(triggerList->get_Item(1, getter_AddRefs(trigger)))) {
291     // This task is broken, make a new one.
292     return RegisterTask(uniqueToken);
293   }
294 
295   BSTR startTimeBstr;
296   if (FAILED(trigger->get_StartBoundary(&startTimeBstr))) {
297     // This task is broken, make a new one.
298     return RegisterTask(uniqueToken);
299   }
300   BStrPtr startTime(startTimeBstr);
301 
302   return RegisterTask(uniqueToken, startTime.get());
303 }
304 
EndsWith(const wchar_t * string,const wchar_t * suffix)305 bool EndsWith(const wchar_t* string, const wchar_t* suffix) {
306   size_t string_len = wcslen(string);
307   size_t suffix_len = wcslen(suffix);
308   if (suffix_len > string_len) {
309     return false;
310   }
311   const wchar_t* substring = string + string_len - suffix_len;
312   return wcscmp(substring, suffix) == 0;
313 }
314 
RemoveTasks(const wchar_t * uniqueToken,WhichTasks tasksToRemove)315 HRESULT RemoveTasks(const wchar_t* uniqueToken, WhichTasks tasksToRemove) {
316   if (!uniqueToken || wcslen(uniqueToken) == 0) {
317     return E_INVALIDARG;
318   }
319 
320   RefPtr<ITaskService> scheduler;
321   HRESULT hr = S_OK;
322   ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
323                           IID_ITaskService, getter_AddRefs(scheduler)));
324 
325   ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
326 
327   RefPtr<ITaskFolder> taskFolder;
328   BStrPtr folderBStr(SysAllocString(kTaskVendor));
329 
330   hr = scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder));
331   if (FAILED(hr)) {
332     if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) {
333       // Don't return an error code if our folder doesn't exist,
334       // because that just means it's been removed already.
335       return S_OK;
336     } else {
337       return hr;
338     }
339   }
340 
341   RefPtr<IRegisteredTaskCollection> tasksInFolder;
342   ENSURE(taskFolder->GetTasks(TASK_ENUM_HIDDEN, getter_AddRefs(tasksInFolder)));
343 
344   LONG numTasks = 0;
345   ENSURE(tasksInFolder->get_Count(&numTasks));
346 
347   std::wstring WdbaTaskName(kTaskName);
348   WdbaTaskName += uniqueToken;
349 
350   // This will be set to the last error that we encounter while deleting tasks.
351   // This allows us to keep attempting to remove the remaining tasks, even if
352   // we encounter an error, while still preserving what error we encountered so
353   // we can return it from this function.
354   HRESULT deleteResult = S_OK;
355   // Set to true if we intentionally skip any tasks.
356   bool tasksSkipped = false;
357 
358   for (LONG i = 0; i < numTasks; ++i) {
359     RefPtr<IRegisteredTask> task;
360     // IRegisteredTaskCollection's are 1-indexed.
361     hr = tasksInFolder->get_Item(_variant_t(i + 1), getter_AddRefs(task));
362     if (FAILED(hr)) {
363       deleteResult = hr;
364       continue;
365     }
366 
367     BSTR taskName;
368     hr = task->get_Name(&taskName);
369     if (FAILED(hr)) {
370       deleteResult = hr;
371       continue;
372     }
373     // Automatically free taskName when we are done with it.
374     BStrPtr uniqueTaskName(taskName);
375 
376     if (tasksToRemove == WhichTasks::WdbaTaskOnly) {
377       if (WdbaTaskName.compare(taskName) != 0) {
378         tasksSkipped = true;
379         continue;
380       }
381     } else {  // tasksToRemove == WhichTasks::AllTasksForInstallation
382       if (!EndsWith(taskName, uniqueToken)) {
383         tasksSkipped = true;
384         continue;
385       }
386     }
387 
388     hr = taskFolder->DeleteTask(taskName, 0 /* flags */);
389     if (FAILED(hr)) {
390       deleteResult = hr;
391     }
392   }
393 
394   // If we successfully removed all the tasks, delete the folder too.
395   if (!tasksSkipped && SUCCEEDED(deleteResult)) {
396     RefPtr<ITaskFolder> rootFolder;
397     BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\"));
398     ENSURE(
399         scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder)));
400     ENSURE(rootFolder->DeleteFolder(folderBStr.get(), 0));
401   }
402 
403   return deleteResult;
404 }
405