diff --git a/toolkit/components/taskscheduler/TaskScheduler.sys.mjs b/toolkit/components/taskscheduler/TaskScheduler.sys.mjs index d432484a0664502448d9776ea23e8f4c53f43fee..5e01932dfa61b0103e3516722119bb243ef7aa03 100644 --- a/toolkit/components/taskscheduler/TaskScheduler.sys.mjs +++ b/toolkit/components/taskscheduler/TaskScheduler.sys.mjs @@ -77,32 +77,40 @@ export var TaskScheduler = { * @param intervalSeconds * Interval at which to run the command, in seconds. Minimum 1800 (30 minutes). * - * @param options + * @param {Object} options * Optional, as are all of its properties: * { - * args + * options.args * Array of arguments to pass on the command line. Does not include the command * itself even if that is considered part of the command line. If missing, no * argument list is generated. * - * workingDirectory + * options.workingDirectory * Working directory for the command. If missing, no working directory is set. * - * description + * options.description * A description string that will be visible to system administrators. This should * be localized. If missing, no description is set. * - * disabled + * options.disabled * If true the task will be created disabled, so that it will not be run. * Ignored on macOS: see comments in TaskSchedulerMacOSImpl.jsm. * Default false, intended for tests. * - * executionTimeoutSec + * options.executionTimeoutSec * Specifies how long (in seconds) the scheduled task can execute for before it is * automatically stopped by the task scheduler. If a value <= 0 is given, it will be * ignored. * This is not currently implemented on macOS. * On Windows, the default timeout is 72 hours. + * + * options.nameVersion + * Over time, we have needed to change the name format that tasks are registered with. + * When interacting with an up-to-date task, this value can be unspecified and the + * current version of the name format will be used by default. When interacting with + * an out-of-date task using an old naming format, this can be used to specify what + * version of the name should be used. Since the precise naming format is platform + * specific, these version numbers are also platform-specific. * } * } */ @@ -123,14 +131,37 @@ export var TaskScheduler = { /** * Delete a scheduled task previously created with registerTask. * + * @param {Object} options + * Optional, as are all of its properties: + * { + * options.nameVersion + * Over time, we have needed to change the name format that tasks are registered with. + * When interacting with an up-to-date task, this value can be unspecified and the + * current version of the name format will be used by default. When interacting with + * an out-of-date task using an old naming format, this can be used to specify what + * version of the name should be used. Since the precise naming format is platform + * specific, these version numbers are also platform-specific. + * } * @throws NS_ERROR_FILE_NOT_FOUND if the task does not exist. */ - async deleteTask(id) { - return lazy.gImpl.deleteTask(id); + async deleteTask(id, options) { + return lazy.gImpl.deleteTask(id, options); }, /** * Delete all tasks registered by this application. + * + * @param {Object} options + * Optional, as are all of its properties: + * { + * options.nameVersion + * Over time, we have needed to change the name format that tasks are registered with. + * When interacting with an up-to-date task, this value can be unspecified and the + * current version of the name format will be used by default. When interacting with + * an out-of-date task using an old naming format, this can be used to specify what + * version of the name should be used. Since the precise naming format is platform + * specific, these version numbers are also platform-specific. + * } */ async deleteAllTasks() { return lazy.gImpl.deleteAllTasks(); @@ -142,10 +173,22 @@ export var TaskScheduler = { * @param id * A string representing the identifier of the task to look for. * + * @param {Object} options + * Optional, as are all of its properties: + * { + * options.nameVersion + * Over time, we have needed to change the name format that tasks are registered with. + * When interacting with an up-to-date task, this value can be unspecified and the + * current version of the name format will be used by default. When interacting with + * an out-of-date task using an old naming format, this can be used to specify what + * version of the name should be used. Since the precise naming format is platform + * specific, these version numbers are also platform-specific. + * } + * * @return * true if the task exists, otherwise false. */ - async taskExists(id) { - return lazy.gImpl.taskExists(id); + async taskExists(id, options) { + return lazy.gImpl.taskExists(id, options); }, }; diff --git a/toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs b/toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs index 682839068d3d5de3f3d820d64b71a7fbf99c8079..5ff9c09c58678487d9e76863f874cde6c37fac16 100644 --- a/toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs +++ b/toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs @@ -51,7 +51,7 @@ export var MacOSImpl = { let uid = await this._uid(); lazy.log.debug(`registerTask: uid=${uid}`); - let label = this._formatLabelForThisApp(id); + let label = this._formatLabelForThisApp(id, options); // We ignore `options.disabled`, which is test only. // @@ -126,10 +126,10 @@ export var MacOSImpl = { return true; }, - async deleteTask(id) { + async deleteTask(id, options) { lazy.log.info(`deleteTask(${id})`); - let label = this._formatLabelForThisApp(id); + let label = this._formatLabelForThisApp(id, options); return this._deleteTaskByLabel(label); }, @@ -207,8 +207,8 @@ export var MacOSImpl = { lazy.log.debug(`deleteAllTasks: returning ${JSON.stringify(result)}`); }, - async taskExists(id) { - const label = this._formatLabelForThisApp(id); + async taskExists(id, options) { + const label = this._formatLabelForThisApp(id, options); const path = this._formatPlistPath(label); return IOUtils.exists(path); }, @@ -292,12 +292,12 @@ export var MacOSImpl = { return serializer.serializeToString(doc); }, - _formatLabelForThisApp(id) { + _formatLabelForThisApp(id, options) { let installHash = lazy.XreDirProvider.getInstallHash(); return `${AppConstants.MOZ_MACBUNDLE_ID}.${installHash}.${id}`; }, - _labelMatchesThisApp(label) { + _labelMatchesThisApp(label, options) { let installHash = lazy.XreDirProvider.getInstallHash(); return ( label && diff --git a/toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs b/toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs index b9f2716fb8d505aabd41b7031ec1f966c152210e..8d9c15c31494562a1080ff3b2291375319e0be00 100644 --- a/toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs +++ b/toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs @@ -40,16 +40,16 @@ export var WinImpl = { lazy.WinTaskSvc.registerTask( this._taskFolderName(), - this._formatTaskName(id), + this._formatTaskName(id, options), xml, updateExisting ); }, - deleteTask(id) { + deleteTask(id, options) { lazy.WinTaskSvc.deleteTask( this._taskFolderName(), - this._formatTaskName(id) + this._formatTaskName(id, options) ); }, @@ -112,7 +112,7 @@ export var WinImpl = { } }, - taskExists(id) { + taskExists(id, options) { const taskFolderName = this._taskFolderName(); let allTasks; @@ -126,7 +126,7 @@ export var WinImpl = { throw ex; } - return allTasks.includes(this._formatTaskName(id)); + return allTasks.includes(this._formatTaskName(id, options)); }, _formatTaskDefinitionXML(command, intervalSeconds, options) { @@ -270,13 +270,47 @@ export var WinImpl = { }; }, - _formatTaskName(id) { + /** + * Formats a given task id according to one of two formats. + * + * @param id + * A string representing the identifier of the task to format + * + * @param {Object} options + * Optional, as are all of its properties: + * { + * options.nameVersion + * Specifies whether to search for tasks using nameVersion 1 + * which is `${taskID} ${installHash}` or nameVersion 2 which is + * `${taskID} ${currentUserSid} ${installHash}`. Defaults to nameVersion 2. + * } + * + * @return + * Formatted task name. + */ + _formatTaskName(id, options) { const installHash = lazy.XreDirProvider.getInstallHash(); - return `${id} ${installHash}`; + if (options?.nameVersion == 1) { + return `${id} ${installHash}`; + } + const currentUserSid = lazy.WinTaskSvc.getCurrentUserSid(); + return `${id} ${currentUserSid} ${installHash}`; }, _matchAppTaskName(name) { const installHash = lazy.XreDirProvider.getInstallHash(); return name.endsWith(` ${installHash}`); }, + + _updateTaskNameFormat(id) { + const taskFolderName = this._taskFolderName(); + const allTasks = lazy.WinTaskSvc.getFolderTasks(taskFolderName); + const taskNameV1 = this._formatTaskName(id, { nameVersion: 1 }); + const taskNameV2 = this._formatTaskName(id, { nameVersion: 2 }); + if (allTasks.includes(taskNameV1)) { + const taskXML = lazy.WinTaskSvc.getTaskXML(taskFolderName, taskNameV1); + lazy.WinTaskSvc.registerTask(taskFolderName, taskNameV2, taskXML, true); + lazy.WinTaskSvc.deleteTask(taskFolderName, taskNameV1); + } + }, }; diff --git a/toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl b/toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl index 7a7c5747c0e814e081bbbc8e07f74c357c80c17d..7778360e7d5e122a8c837a009aa2779686d87a8f 100644 --- a/toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl +++ b/toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl @@ -59,6 +59,17 @@ interface nsIWinTaskSchedulerService : nsISupports */ AString getTaskXML(in wstring aFolderName, in wstring aTaskName); + /** + * Gets the sid of the current user. + * + * @throws NS_ERROR_NOT_IMPLEMENTED If called on a non-Windows OS. + * @throws NS_ERROR_FAILURE If the user token cannot be found. + * @throws NS_ERROR_ABORT If converting the sid to a string fails. + * + * @returns The sid of the current user. + */ + AString getCurrentUserSid(); + /** * Delete a task. * diff --git a/toolkit/components/taskscheduler/nsWinTaskScheduler.cpp b/toolkit/components/taskscheduler/nsWinTaskScheduler.cpp index 21b2909805afb93a69619c4890d6f590c020b1f0..1efae5c349d11d8d5ed27d186ed7ecf1af88ee6e 100644 --- a/toolkit/components/taskscheduler/nsWinTaskScheduler.cpp +++ b/toolkit/components/taskscheduler/nsWinTaskScheduler.cpp @@ -7,6 +7,8 @@ #include <windows.h> #include <comdef.h> +#include <sddl.h> +#include <securitybaseapi.h> #include <taskschd.h> #include "nsString.h" @@ -114,6 +116,30 @@ nsWinTaskSchedulerService::GetTaskXML(const char16_t* aFolderName, return NS_OK; } +NS_IMETHODIMP +nsWinTaskSchedulerService::GetCurrentUserSid(nsAString& aUserSid) { +#ifndef XP_WIN + return NS_ERROR_NOT_IMPLEMENTED; +#else // !XP_WIN + DWORD tokenLen; + LPWSTR stringSid; + BYTE tokenBuf[TOKEN_USER_MAX_SIZE]; + PTOKEN_USER tokenInfo = reinterpret_cast<PTOKEN_USER>(tokenBuf); + BOOL success = GetTokenInformation(GetCurrentProcessToken(), TokenUser, + tokenInfo, sizeof(tokenBuf), &tokenLen); + if (!success) { + return NS_ERROR_FAILURE; + } + success = ConvertSidToStringSidW(tokenInfo->User.Sid, &stringSid); + if (!success) { + return NS_ERROR_ABORT; + } + aUserSid.Assign(stringSid); + LocalFree(stringSid); + return NS_OK; +#endif +} + NS_IMETHODIMP nsWinTaskSchedulerService::RegisterTask(const char16_t* aFolderName, const char16_t* aTaskName, diff --git a/toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js b/toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js index d0c2a6c357fa522edf3207694fa39c12c41b995c..ed1be3e49b4a274e9d882f3844edc194ac708e91 100644 --- a/toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js +++ b/toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js @@ -200,3 +200,92 @@ add_task(async function test_create() { ); Assert.equal(WinSvc.validateTaskDefinition(basicXML), 0 /* S_OK */); }); + +add_task(async function test_migrate() { + // Create task name with nameVersion1 + const taskName = "test-task-1"; + const rawTaskNameV1 = WinImpl._formatTaskName(taskName, { nameVersion: 1 }); + const rawTaskNameV2 = WinImpl._formatTaskName(taskName, { nameVersion: 2 }); + const folderName = WinImpl._taskFolderName(); + const exePath = "C:\\Program Files\\XYZ\\123.exe"; + const workingDir = "C:\\Program Files\\XYZ"; + const argsIn = [ + "x.txt", + "c:\\x.txt", + 'C:\\"HELLO WORLD".txt', + "only space.txt", + ]; + const expectedArgsOutStr = [ + "x.txt", + "c:\\x.txt", + '"C:\\\\\\"HELLO WORLD\\".txt"', + '"only space.txt"', + ].join(" "); + const description = "Entities: < &. Non-ASCII: abc😀def."; + const intervalSecsIn = 2 * 60 * 60; // 2 hours + const expectedIntervalOut = "PT2H"; // 2 hours + + const queries = [ + ["Actions Exec Command", exePath], + ["Actions Exec WorkingDirectory", workingDir], + ["Actions Exec Arguments", expectedArgsOutStr], + ["RegistrationInfo Description", description], + ["RegistrationInfo Author", Services.appinfo.vendor], + ["Settings Enabled", "false"], + ["Triggers TimeTrigger Repetition Interval", expectedIntervalOut], + ]; + + await TaskScheduler.registerTask(taskName, exePath, intervalSecsIn, { + disabled: true, + args: argsIn, + description, + workingDirectory: workingDir, + nameVersion: 1, + }); + + ok( + WinImpl.taskExists(taskName, { nameVersion: 1 }), + "Task exists with nameVersion1" + ); + const originalTaskXML = WinSvc.getTaskXML(folderName, rawTaskNameV1); + const parser = new DOMParser(); + const docV1 = parser.parseFromString(originalTaskXML, "text/xml"); + + Assert.equal(docV1.documentElement.tagName, "Task"); + + // Check for the values set above + for (let [sel, expected] of queries) { + Assert.equal( + docV1.querySelector(sel).textContent, + expected, + `Task V1 ${sel} had expected textContent` + ); + } + + // Update task name format to nameVersion2 + WinImpl._updateTaskNameFormat(taskName); + ok( + WinImpl.taskExists(taskName, { nameVersion: 2 }), + "Task exists with nameVersion2" + ); + ok( + !WinImpl.taskExists(taskName, { nameVersion: 1 }), + "Task with nameVersion1 successfully deleted" + ); + + // Check that the new task XML is still valid + const newTaskXML = WinSvc.getTaskXML(folderName, rawTaskNameV2); + Assert.equal(WinSvc.validateTaskDefinition(newTaskXML), 0 /* S_OK */); + const docV2 = parser.parseFromString(newTaskXML, "text/xml"); + + Assert.equal(docV2.documentElement.tagName, "Task"); + + // Check that the updated values still match the provided ones. + for (let [sel, expected] of queries) { + Assert.equal( + docV2.querySelector(sel).textContent, + expected, + `Task V2 ${sel} had expected textContent` + ); + } +}); diff --git a/toolkit/mozapps/update/BackgroundUpdate.sys.mjs b/toolkit/mozapps/update/BackgroundUpdate.sys.mjs index fd88a33dfcd9398e531e981228593112514d6e4b..70fc8388404910d012a42f635321969ef4db3f10 100644 --- a/toolkit/mozapps/update/BackgroundUpdate.sys.mjs +++ b/toolkit/mozapps/update/BackgroundUpdate.sys.mjs @@ -56,10 +56,31 @@ XPCOMUtils.defineLazyServiceGetters(lazy, { // by storing the installed version number of the task to a pref and comparing // that version number to the current version. If they aren't equal, we know // that we have to re-register the task. -const TASK_DEF_CURRENT_VERSION = 3; +const TASK_DEF_CURRENT_VERSION = 4; const TASK_INSTALLED_VERSION_PREF = "app.update.background.lastInstalledTaskVersion"; +// This returns the version of the task naming scheme being used which +// is different from the task version used for the task definition. +function taskNameVersion(taskVersion) { + if (AppConstants.platform != "win" || taskVersion < 4) { + return 1; + } + return 2; +} + +async function deleteTasksInRange(installedVersion, currentVersion) { + for ( + let taskVersion = installedVersion; + taskVersion <= currentVersion; + taskVersion++ + ) { + await lazy.TaskScheduler.deleteTask(this.taskId, { + nameVersion: taskNameVersion(taskVersion), + }); + } +} + export var BackgroundUpdate = { QueryInterface: ChromeUtils.generateQI([ "nsINamed", @@ -504,7 +525,11 @@ export var BackgroundUpdate = { ); if (!successfullyReadPrevious || previousEnabled) { - await lazy.TaskScheduler.deleteTask(this.taskId); + let installedVersion = Services.prefs.getIntPref( + TASK_INSTALLED_VERSION_PREF, + TASK_DEF_CURRENT_VERSION + ); + await deleteTasksInRange(installedVersion, TASK_DEF_CURRENT_VERSION); lazy.log.debug( `${SLUG}: witnessed falling (enabled -> disabled) edge; deleted task ${this.taskId}.` ); @@ -533,7 +558,11 @@ export var BackgroundUpdate = { `Removing task so the new version can be registered` ); try { - await lazy.TaskScheduler.deleteTask(this.taskId); + let installedVersion = Services.prefs.getIntPref( + TASK_INSTALLED_VERSION_PREF, + TASK_DEF_CURRENT_VERSION + ); + await deleteTasksInRange(installedVersion, TASK_DEF_CURRENT_VERSION); } catch (e) { lazy.log.error(`${SLUG}: Error removing old task: ${e}`); }