BackupUIParent.sys.mjs (10751B)
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 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 BackupService: "resource:///modules/backup/BackupService.sys.mjs", 9 ERRORS: "chrome://browser/content/backup/backup-constants.mjs", 10 E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", 11 }); 12 13 ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { 14 return console.createInstance({ 15 prefix: "BackupUIParent", 16 maxLogLevel: Services.prefs.getBoolPref("browser.backup.log", false) 17 ? "Debug" 18 : "Warn", 19 }); 20 }); 21 22 const BACKUP_ERROR_CODE_PREF_NAME = "browser.backup.errorCode"; 23 24 /** 25 * A JSWindowActor that is responsible for marshalling information between 26 * the BackupService singleton and any registered UI widgets that need to 27 * represent data from that service. 28 */ 29 export class BackupUIParent extends JSWindowActorParent { 30 /** 31 * A reference to the BackupService singleton instance. 32 * 33 * @type {BackupService} 34 */ 35 #bs; 36 37 /** 38 * Observer for "backup-service-status-updated" notifications. 39 * We want each BackupUIParent actor instance to be notified separately and 40 * to forward the state to its child. 41 */ 42 #obs; 43 44 /** 45 * Create a BackupUIParent instance. If a BackupUIParent is instantiated 46 * before BrowserGlue has a chance to initialize the BackupService, this 47 * constructor will cause it to initialize first. 48 */ 49 constructor() { 50 super(); 51 // We use init() rather than get(), since it's possible to load 52 // about:preferences before the service has had a chance to init itself 53 // via BrowserGlue. 54 this.#bs = lazy.BackupService.init(); 55 56 // Define the observer function to capture our this. 57 this.#obs = (_subject, topic) => { 58 if (topic == "backup-service-status-updated") { 59 this.sendState(); 60 } 61 }; 62 } 63 64 /** 65 * Called once the BackupUIParent/BackupUIChild pair have been connected. 66 */ 67 actorCreated() { 68 this.#bs.addEventListener("BackupService:StateUpdate", this); 69 Services.obs.addObserver(this.#obs, "backup-service-status-updated"); 70 // Note that loadEncryptionState is an async function. 71 // This function is no-op if the encryption state was already loaded. 72 this.#bs.loadEncryptionState(); 73 } 74 75 /** 76 * Called once the BackupUIParent/BackupUIChild pair have been disconnected. 77 */ 78 didDestroy() { 79 this.#bs.removeEventListener("BackupService:StateUpdate", this); 80 Services.obs.removeObserver(this.#obs, "backup-service-status-updated"); 81 } 82 83 /** 84 * Handles events fired by the BackupService. 85 * 86 * @param {Event} event 87 * The event that the BackupService emitted. 88 */ 89 handleEvent(event) { 90 if (event.type == "BackupService:StateUpdate") { 91 this.sendState(); 92 } 93 } 94 95 /** 96 * Trigger a createBackup call. 97 * 98 * @param {...any} args 99 * Arguments to pass through to createBackup. 100 * @returns {object} Result of the backup attempt. 101 */ 102 async #triggerCreateBackup(...args) { 103 try { 104 await this.#bs.createBackup(...args); 105 return { success: true }; 106 } catch (e) { 107 lazy.logConsole.error(`Failed to retrigger backup`, e); 108 return { success: false, errorCode: e.cause || lazy.ERRORS.UNKNOWN }; 109 } 110 } 111 112 /** 113 * Handles messages sent by BackupUIChild. 114 * 115 * @param {ReceiveMessageArgument} message 116 * The message received from the BackupUIChild. 117 * @returns { 118 * null | 119 * {success: boolean, errorCode: number} | 120 * {path: string, fileName: string, iconURL: string|null} 121 * } 122 * Returns either a success object, a file details object, or null. 123 */ 124 async receiveMessage(message) { 125 let currentWindowGlobal = this.browsingContext.currentWindowGlobal; 126 // The backup spotlights can be embedded in less privileged content pages, so let's 127 // make sure that any messages from content are coming from the privileged 128 // about content process type 129 if ( 130 !currentWindowGlobal || 131 (!currentWindowGlobal.isInProcess && 132 this.browsingContext.currentRemoteType != 133 lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE) 134 ) { 135 lazy.logConsole.debug( 136 "BackupUIParent: received message from the wrong content process type." 137 ); 138 return null; 139 } 140 141 if (message.name == "RequestState") { 142 this.sendState(); 143 } else if (message.name == "TriggerCreateBackup") { 144 return await this.#triggerCreateBackup({ reason: "manual" }); 145 } else if (message.name == "EnableScheduledBackups") { 146 try { 147 let { parentDirPath, password } = message.data; 148 if (parentDirPath) { 149 this.#bs.setParentDirPath(parentDirPath); 150 } 151 152 if (password) { 153 // If the user's previously created backups were already encrypted 154 // with a password, their encryption settings are now reset to 155 // accommodate the newly supplied password. 156 if (await this.#bs.loadEncryptionState()) { 157 await this.#bs.disableEncryption(); 158 } 159 await this.#bs.enableEncryption(password); 160 Glean.browserBackup.passwordAdded.record(); 161 } 162 this.#bs.setScheduledBackups(true); 163 } catch (e) { 164 lazy.logConsole.error(`Failed to enable scheduled backups`, e); 165 return { success: false, errorCode: e.cause || lazy.ERRORS.UNKNOWN }; 166 } 167 /** 168 * TODO: (Bug 1900125) we should create a backup at the specified dir path once we turn on 169 * scheduled backups. The backup folder in the chosen directory should contain 170 * the archive file, which we create using BackupService.createArchive implemented in 171 * Bug 1897498. 172 */ 173 return { success: true }; 174 } else if (message.name == "DisableScheduledBackups") { 175 await this.#bs.cleanupBackupFiles(); 176 this.#bs.setScheduledBackups(false); 177 } else if (message.name == "ShowFilepicker") { 178 let { win, filter, existingBackupPath } = message.data; 179 180 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); 181 182 let mode = filter 183 ? Ci.nsIFilePicker.modeOpen 184 : Ci.nsIFilePicker.modeGetFolder; 185 fp.init(win || this.browsingContext, "", mode); 186 187 if (filter) { 188 fp.appendFilters(Ci.nsIFilePicker[filter]); 189 } 190 191 if (existingBackupPath) { 192 try { 193 let folder = (await IOUtils.getFile(existingBackupPath)).parent; 194 if (folder.exists()) { 195 fp.displayDirectory = folder; 196 } 197 } catch (_) { 198 // If the file can not be found we will skip setting the displayDirectory. 199 } 200 } 201 202 let result = await new Promise(resolve => fp.open(resolve)); 203 204 if (result === Ci.nsIFilePicker.returnCancel) { 205 return null; 206 } 207 208 let path = fp.file.path; 209 let iconURL = this.#bs.getIconFromFilePath(path); 210 let filename = PathUtils.filename(path); 211 212 return { 213 path, 214 filename, 215 iconURL, 216 }; 217 } else if (message.name == "GetBackupFileInfo") { 218 let { backupFile } = message.data; 219 try { 220 await this.#bs.getBackupFileInfo(backupFile); 221 } catch (e) { 222 /** 223 * TODO: (Bug 1905156) display a localized version of error in the restore dialog. 224 */ 225 } 226 } else if (message.name == "RestoreFromBackupChooseFile") { 227 const window = this.browsingContext.topChromeWindow; 228 this.#bs.filePickerForRestore(window); 229 } else if (message.name == "RestoreFromBackupFile") { 230 let { backupFile, backupPassword } = message.data; 231 try { 232 await this.#bs.recoverFromBackupArchive( 233 backupFile, 234 backupPassword, 235 true /* shouldLaunch */ 236 ); 237 } catch (e) { 238 lazy.logConsole.error(`Failed to restore file: ${backupFile}`, e); 239 this.#bs.setRecoveryError(e.cause || lazy.ERRORS.UNKNOWN); 240 return { success: false, errorCode: e.cause || lazy.ERRORS.UNKNOWN }; 241 } 242 return { success: true }; 243 } else if (message.name == "EnableEncryption") { 244 try { 245 let wasEncrypted = this.#bs.state.encryptionEnabled; 246 await this.#bs.enableEncryption(message.data.password); 247 if (wasEncrypted) { 248 Glean.browserBackup.passwordChanged.record(); 249 } else { 250 Glean.browserBackup.passwordAdded.record(); 251 } 252 } catch (e) { 253 lazy.logConsole.error(`Failed to enable encryption`, e); 254 return { success: false, errorCode: e.cause || lazy.ERRORS.UNKNOWN }; 255 } 256 257 return await this.#triggerCreateBackup({ reason: "encryption" }); 258 } else if (message.name == "DisableEncryption") { 259 try { 260 await this.#bs.disableEncryption(); 261 Glean.browserBackup.passwordRemoved.record(); 262 } catch (e) { 263 lazy.logConsole.error(`Failed to disable encryption`, e); 264 return { success: false, errorCode: e.cause || lazy.ERRORS.UNKNOWN }; 265 } 266 267 return await this.#triggerCreateBackup({ reason: "encryption" }); 268 } else if (message.name == "ShowBackupLocation") { 269 this.#bs.showBackupLocation(); 270 } else if (message.name == "EditBackupLocation") { 271 const window = this.browsingContext.topChromeWindow; 272 this.#bs.editBackupLocation(window); 273 } else if (message.name == "QuitCurrentProfile") { 274 // Notify windows that a quit has been requested. 275 let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( 276 Ci.nsISupportsPRBool 277 ); 278 Services.obs.notifyObservers(cancelQuit, "quit-application-requested"); 279 if (cancelQuit.data) { 280 // Something blocked our attempt to quit. 281 return null; 282 } 283 284 try { 285 Services.startup.quit(Services.startup.eAttemptQuit); 286 } catch (e) { 287 // let's silently resolve this error 288 lazy.logConsole.error( 289 `There was a problem while quitting the current profile: `, 290 e 291 ); 292 } 293 } else if (message.name == "SetEmbeddedComponentPersistentData") { 294 this.#bs.setEmbeddedComponentPersistentData(message.data); 295 } else if (message.name == "FlushEmbeddedComponentPersistentData") { 296 this.#bs.setEmbeddedComponentPersistentData({}); 297 } else if (message.name == "ErrorBarDismissed") { 298 Services.prefs.setIntPref(BACKUP_ERROR_CODE_PREF_NAME, lazy.ERRORS.NONE); 299 } 300 301 return null; 302 } 303 304 /** 305 * Sends the StateUpdate message to the BackupUIChild, along with the most 306 * recent state object from BackupService. 307 */ 308 sendState() { 309 this.sendAsyncMessage("StateUpdate", { 310 state: this.#bs.state, 311 }); 312 } 313 }