MigrationWizardChild.sys.mjs (13163B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { MigrationWizardConstants } from "chrome://browser/content/migration/migration-wizard-constants.mjs"; 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 8 9 const lazy = {}; 10 XPCOMUtils.defineLazyPreferenceGetter( 11 lazy, 12 "SHOW_IMPORT_ALL_PREF", 13 "browser.migrate.content-modal.import-all.enabled", 14 false 15 ); 16 17 /** 18 * This class is responsible for updating the state of a <migration-wizard> 19 * component, and for listening for events from that component to perform 20 * various migration functions. 21 */ 22 export class MigrationWizardChild extends JSWindowActorChild { 23 #wizardEl = null; 24 25 /** 26 * Retrieves the list of browsers and profiles from the parent process, and then 27 * puts the migration wizard onto the selection page showing the list that they 28 * can import from. 29 * 30 * @param {boolean} [allowOnlyFileMigrators=null] 31 * Set to true if showing the selection page is allowed if no browser migrators 32 * are found. If not true, and no browser migrators are found, then the wizard 33 * will be sent to the NO_BROWSERS_FOUND page. 34 * @param {string} [migratorKey=null] 35 * If set, this will automatically select the first associated migrator with that 36 * migratorKey in the selector. If not set, the first item in the retrieved list 37 * of migrators will be selected. 38 * @param {string} [fileImportErrorMessage=null] 39 * If set, this will display an error message below the browser / profile selector 40 * indicating that something had previously gone wrong with an import of type 41 * MIGRATOR_TYPES.FILE. 42 */ 43 async #populateMigrators( 44 allowOnlyFileMigrators, 45 migratorKey, 46 fileImportErrorMessage 47 ) { 48 let migrators = await this.sendQuery("GetAvailableMigrators"); 49 let hasBrowserMigrators = migrators.some(migrator => { 50 return migrator.type == MigrationWizardConstants.MIGRATOR_TYPES.BROWSER; 51 }); 52 let hasFileMigrators = migrators.some(migrator => { 53 return migrator.type == MigrationWizardConstants.MIGRATOR_TYPES.FILE; 54 }); 55 if (!hasBrowserMigrators && !allowOnlyFileMigrators) { 56 this.setComponentState({ 57 page: MigrationWizardConstants.PAGES.NO_BROWSERS_FOUND, 58 hasFileMigrators, 59 }); 60 this.#sendTelemetryEvent("noBrowsersFound"); 61 } else { 62 this.setComponentState({ 63 migrators, 64 page: MigrationWizardConstants.PAGES.SELECTION, 65 showImportAll: lazy.SHOW_IMPORT_ALL_PREF, 66 migratorKey, 67 fileImportErrorMessage, 68 }); 69 } 70 } 71 72 /** 73 * General event handler function for events dispatched from the 74 * <migration-wizard> component. 75 * 76 * @param {Event} event 77 * The DOM event being handled. 78 * @returns {Promise} 79 */ 80 async handleEvent(event) { 81 this.#wizardEl = event.target; 82 83 switch (event.type) { 84 case "MigrationWizard:RequestState": { 85 this.#sendTelemetryEvent("opened"); 86 await this.#requestState(event.detail?.allowOnlyFileMigrators); 87 break; 88 } 89 90 case "MigrationWizard:BeginMigration": { 91 let extraArgs = this.#recordBeginMigrationEvent(event.detail); 92 93 let hasPermissions = await this.sendQuery("CheckPermissions", { 94 key: event.detail.key, 95 type: event.detail.type, 96 }); 97 98 if (!hasPermissions) { 99 if (event.detail.key == "safari") { 100 this.#sendTelemetryEvent("safariPerms"); 101 this.setComponentState({ 102 page: MigrationWizardConstants.PAGES.SAFARI_PERMISSION, 103 }); 104 } else { 105 console.error( 106 `A migrator with key ${event.detail.key} needs permissions, ` + 107 "and no UI exists for that right now." 108 ); 109 } 110 return; 111 } 112 113 await this.beginMigration(event.detail, extraArgs); 114 break; 115 } 116 117 case "MigrationWizard:RequestSafariPermissions": { 118 let success = await this.sendQuery("RequestSafariPermissions"); 119 if (success) { 120 let extraArgs = this.#constructExtraArgs(event.detail); 121 await this.beginMigration(event.detail, extraArgs); 122 } 123 break; 124 } 125 126 case "MigrationWizard:SelectManualPasswordFile": { 127 let path = await this.sendQuery("SelectManualPasswordFile"); 128 if (path) { 129 event.detail.manualPasswordFilePath = path; 130 131 let passwordResourceIndex = event.detail.resourceTypes.indexOf( 132 MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS 133 ); 134 event.detail.resourceTypes.splice(passwordResourceIndex, 1); 135 136 let extraArgs = this.#constructExtraArgs(event.detail); 137 await this.beginMigration(event.detail, extraArgs); 138 } 139 break; 140 } 141 142 case "MigrationWizard:OpenAboutAddons": { 143 this.sendAsyncMessage("OpenAboutAddons"); 144 break; 145 } 146 147 case "MigrationWizard:PermissionsNeeded": { 148 // In theory, the migrator permissions might be requested on any 149 // platform - but in practice, this only happens on Linux, so that's 150 // why the event is named linux_perms. 151 this.#sendTelemetryEvent("linuxPerms", { 152 migrator_key: event.detail.key, 153 }); 154 break; 155 } 156 157 case "MigrationWizard:GetPermissions": { 158 let success = await this.sendQuery("GetPermissions", { 159 key: event.detail.key, 160 }); 161 if (success) { 162 await this.#requestState(true /* allowOnlyFileMigrators */); 163 } 164 break; 165 } 166 167 case "MigrationWizard:OpenURL": { 168 this.sendAsyncMessage("OpenURL", { 169 url: event.detail.url, 170 where: event.detail.where, 171 }); 172 break; 173 } 174 } 175 } 176 177 async #requestState(allowOnlyFileMigrators) { 178 this.setComponentState({ 179 page: MigrationWizardConstants.PAGES.LOADING, 180 }); 181 182 await this.#populateMigrators(allowOnlyFileMigrators); 183 184 this.#wizardEl.dispatchEvent( 185 new this.contentWindow.CustomEvent("MigrationWizard:Ready", { 186 bubbles: true, 187 }) 188 ); 189 } 190 191 /** 192 * Sends a message to the parent actor to record Event Telemetry. 193 * 194 * @param {string} type 195 * The type of event being recorded. 196 * @param {object} [args=null] 197 * Optional extra_args to supply for the event. 198 */ 199 #sendTelemetryEvent(type, args) { 200 this.sendAsyncMessage("RecordEvent", { type, args }); 201 } 202 203 /** 204 * Constructs extra arguments to pass to some Event Telemetry based 205 * on the MigrationDetails passed up from the MigrationWizard. 206 * 207 * See migration-wizard.mjs for a definition of MigrationDetails. 208 * 209 * @param {object} migrationDetails 210 * A MigrationDetails object. 211 * @returns {object} 212 */ 213 #constructExtraArgs(migrationDetails) { 214 let extraArgs = { 215 migrator_key: migrationDetails.key, 216 history: "0", 217 formdata: "0", 218 passwords: "0", 219 bookmarks: "0", 220 payment_methods: "0", 221 extensions: "0", 222 other: 0, 223 }; 224 225 for (let type of migrationDetails.resourceTypes) { 226 switch (type) { 227 case MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY: { 228 extraArgs.history = "1"; 229 break; 230 } 231 232 case MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA: { 233 extraArgs.formdata = "1"; 234 break; 235 } 236 237 case MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS: { 238 extraArgs.passwords = "1"; 239 break; 240 } 241 242 case MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS: { 243 extraArgs.bookmarks = "1"; 244 break; 245 } 246 247 case MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS: { 248 extraArgs.extensions = "1"; 249 break; 250 } 251 252 case MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES 253 .PAYMENT_METHODS: { 254 extraArgs.payment_methods = "1"; 255 break; 256 } 257 258 default: { 259 extraArgs.other++; 260 } 261 } 262 } 263 264 return extraArgs; 265 } 266 267 /** 268 * This migration wizard combines a lot of steps (selecting the browser, profile, 269 * resources, and starting the migration) into a single page. This helper method 270 * records Event Telemetry for each of those actions at the same time when a 271 * migration begins. 272 * 273 * This method returns the extra_args object that was constructed for the 274 * resources_selected and migration_started event so that a 275 * "migration_finished" event can use the same extra_args without 276 * regenerating it. 277 * 278 * See migration-wizard.mjs for a definition of MigrationDetails. 279 * 280 * @param {object} migrationDetails 281 * A MigrationDetails object. 282 * @returns {object} 283 */ 284 #recordBeginMigrationEvent(migrationDetails) { 285 this.#sendTelemetryEvent("browserSelected", { 286 migrator_key: migrationDetails.key, 287 }); 288 289 if (migrationDetails.profile) { 290 this.#sendTelemetryEvent("profileSelected", { 291 migrator_key: migrationDetails.key, 292 }); 293 } 294 295 let extraArgs = this.#constructExtraArgs(migrationDetails); 296 297 extraArgs.configured = String(Number(migrationDetails.expandedDetails)); 298 this.#sendTelemetryEvent("resourcesSelected", extraArgs); 299 delete extraArgs.configured; 300 301 this.#sendTelemetryEvent("migrationStarted", extraArgs); 302 return extraArgs; 303 } 304 305 /** 306 * Sends a message to the parent actor to attempt a migration. 307 * 308 * See migration-wizard.mjs for a definition of MigrationDetails. 309 * 310 * @param {object} migrationDetails 311 * A MigrationDetails object. 312 * @param {object} extraArgs 313 * Extra argument object to pass to the Event Telemetry for finishing 314 * the migration. 315 * @returns {Promise<undefined>} 316 * Returns a Promise that resolves after the parent responds to the migration 317 * message. 318 */ 319 async beginMigration(migrationDetails, extraArgs) { 320 // We redirect to manual password import for Safari and Chrome on Windows. 321 if ( 322 migrationDetails.resourceTypes.includes( 323 MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS 324 ) && 325 !migrationDetails.manualPasswordFilePath 326 ) { 327 if (migrationDetails.key == "safari") { 328 this.#sendTelemetryEvent("safariPasswordFile"); 329 this.setComponentState({ 330 page: MigrationWizardConstants.PAGES.SAFARI_PASSWORD_PERMISSION, 331 }); 332 return; 333 } else if ( 334 migrationDetails.key == "chrome" && 335 AppConstants.platform == "win" 336 ) { 337 this.#sendTelemetryEvent("chromePasswordFile"); 338 this.setComponentState({ 339 page: MigrationWizardConstants.PAGES 340 .CHROME_WINDOWS_PASSWORD_PERMISSION, 341 }); 342 return; 343 } 344 } 345 346 extraArgs = await this.sendQuery("Migrate", { 347 migrationDetails, 348 extraArgs, 349 }); 350 this.#sendTelemetryEvent("migrationFinished", extraArgs); 351 352 this.#wizardEl.dispatchEvent( 353 new this.contentWindow.CustomEvent("MigrationWizard:DoneMigration", { 354 bubbles: true, 355 }) 356 ); 357 } 358 359 /** 360 * General message handler function for messages received from the 361 * associated MigrationWizardParent JSWindowActor. 362 * 363 * @param {ReceiveMessageArgument} message 364 * The message received from the MigrationWizardParent. 365 */ 366 receiveMessage(message) { 367 switch (message.name) { 368 case "UpdateProgress": { 369 this.setComponentState({ 370 page: MigrationWizardConstants.PAGES.PROGRESS, 371 progress: message.data.progress, 372 key: message.data.key, 373 }); 374 break; 375 } 376 case "UpdateFileImportProgress": { 377 this.setComponentState({ 378 page: MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS, 379 progress: message.data.progress, 380 title: message.data.title, 381 }); 382 break; 383 } 384 case "FileImportProgressError": { 385 this.#populateMigrators( 386 true, 387 message.data.migratorKey, 388 message.data.fileImportErrorMessage 389 ); 390 break; 391 } 392 } 393 } 394 395 /** 396 * Calls the `setState` method on the <migration-wizard> component. The 397 * state is cloned into the execution scope of this.#wizardEl. 398 * 399 * @param {object} state The state object that a <migration-wizard> 400 * component expects. See the documentation for the element's setState 401 * method for more details. 402 */ 403 setComponentState(state) { 404 if (!this.#wizardEl) { 405 return; 406 } 407 // We waive XrayWrappers in the event that the element is embedded in 408 // a document without system privileges, like about:welcome. 409 Cu.waiveXrays(this.#wizardEl).setState( 410 Cu.cloneInto( 411 state, 412 // ownerGlobal doesn't exist in content windows. 413 // eslint-disable-next-line mozilla/use-ownerGlobal 414 this.#wizardEl.ownerDocument.defaultView 415 ) 416 ); 417 } 418 }