Bug 1822676 - Have the new Migration Wizard request permissions if necessary for Safari import. r=NeilDeakin

Differential Revision: https://phabricator.services.mozilla.com/D172840
This commit is contained in:
Mike Conley 2023-03-27 16:51:12 +00:00
Родитель c4778b21fe
Коммит 46215877ca
7 изменённых файлов: 294 добавлений и 15 удалений

Просмотреть файл

@ -606,7 +606,8 @@ let JSWINDOWACTORS = {
esModuleURI: "resource:///actors/MigrationWizardChild.sys.mjs",
events: {
"MigrationWizard:RequestState": { wantUntrusted: true },
"MigrationWizard:BeginMigration": { wantsUntrusted: true },
"MigrationWizard:BeginMigration": { wantUntrusted: true },
"MigrationWizard:RequestSafariPermissions": { wantUntrusted: true },
},
},

Просмотреть файл

@ -59,17 +59,57 @@ export class MigrationWizardChild extends JSWindowActorChild {
}
case "MigrationWizard:BeginMigration": {
await this.sendQuery("Migrate", event.detail);
this.#wizardEl.dispatchEvent(
new this.contentWindow.CustomEvent("MigrationWizard:DoneMigration", {
bubbles: true,
})
);
let hasPermissions = await this.sendQuery("CheckPermissions", {
key: event.detail.key,
});
if (!hasPermissions) {
if (event.detail.key == "safari") {
this.setComponentState({
page: MigrationWizardConstants.PAGES.SAFARI_PERMISSION,
});
} else {
console.error(
`A migrator with key ${event.detail.key} needs permissions, ` +
"and no UI exists for that right now."
);
}
return;
}
await this.beginMigration(event.detail);
break;
}
case "MigrationWizard:RequestSafariPermissions": {
let success = await this.sendQuery("RequestSafariPermissions");
if (success) {
await this.beginMigration(event.detail);
}
break;
}
}
}
/**
* Sends a message to the parent actor to attempt a migration.
*
* See migration-wizard.mjs for a definition of MigrationDetails.
*
* @param {object} migrationDetails
* A MigrationDetails object.
* @returns {Promise<undefined>}
* Returns a Promise that resolves after the parent responds to the migration
* message.
*/
async beginMigration(migrationDetails) {
await this.sendQuery("Migrate", migrationDetails);
this.#wizardEl.dispatchEvent(
new this.contentWindow.CustomEvent("MigrationWizard:DoneMigration", {
bubbles: true,
})
);
}
/**
* General message handler function for messages received from the
* associated MigrationWizardParent JSWindowActor.

Просмотреть файл

@ -77,6 +77,19 @@ export class MigrationWizardParent extends JSWindowActorParent {
message.data.resourceTypes,
message.data.profile
);
break;
}
case "CheckPermissions": {
let migrator = await MigrationUtils.getMigrator(message.data.key);
return migrator.hasPermissions();
}
case "RequestSafariPermissions": {
let safariMigrator = await MigrationUtils.getMigrator("safari");
return safariMigrator.getPermissions(
this.browsingContext.topChromeWindow
);
}
}
@ -237,8 +250,11 @@ export class MigrationWizardParent extends JSWindowActorParent {
* @returns {Promise<MigratorProfileInstance>}
*/
async #serializeMigratorAndProfile(migrator, profileObj) {
let profileMigrationData = await migrator.getMigrateData(profileObj);
let lastModifiedDate = await migrator.getLastUsedDate();
let [profileMigrationData, lastModifiedDate] = await Promise.all([
migrator.getMigrateData(profileObj),
migrator.getLastUsedDate(),
]);
let availableResourceTypes = [];
for (let resourceType in MigrationUtils.resourceTypes) {

Просмотреть файл

@ -196,6 +196,40 @@ export class MigratorBase {
return Services.prefs.getBoolPref(`browser.migrate.${key}.enabled`, false);
}
/**
* Subclasses should implement this if special checks need to be made to determine
* if certain permissions need to be requested before data can be imported.
* The returned Promise resolves to true if the required permissions have
* been granted and a migration could proceed.
*
* @returns {Promise<boolean>}
*/
async hasPermissions() {
return Promise.resolve(true);
}
/**
* Subclasses should implement this if special permissions need to be
* requested from the user or the operating system in order to perform
* a migration with this MigratorBase. This will be called only if
* hasPermissions resolves to false.
*
* The returned Promise will resolve to true if permissions were successfully
* obtained, and false otherwise. Implementors should ensure that if a call
* to getPermissions resolves to true, that the MigratorBase will be able to
* get read access to all of the resources it needs to do a migration.
*
* @param {DOMWindow} win
* The top-level DOM window hosting the UI that is requesting the permission.
* This can be used to, for example, anchor a file picker window to the
* same window that is hosting the migration UI.
* @returns {Promise<boolean>}
*/
// eslint-disable-next-line no-unused-vars
async getPermissions(win) {
return Promise.resolve(true);
}
/**
* This method returns a number that is the bitwise OR of all resource
* types that are available in aProfile. See MigrationUtils.resourceTypes

Просмотреть файл

@ -21,6 +21,7 @@ export class MigrationWizard extends HTMLElement {
#resourceTypeList = null;
#shadowRoot = null;
#importButton = null;
#safariPermissionButton = null;
static get markup() {
return `
@ -215,6 +216,11 @@ export class MigrationWizard extends HTMLElement {
this.#resourceTypeList = shadow.querySelector("#resource-type-list");
this.#resourceTypeList.addEventListener("change", this);
this.#safariPermissionButton = shadow.querySelector(
"#safari-request-permissions"
);
this.#safariPermissionButton.addEventListener("click", this);
this.#shadowRoot = shadow;
}
@ -353,6 +359,7 @@ export class MigrationWizard extends HTMLElement {
opt.profile = migrator.profile;
opt.displayName = migrator.displayName;
opt.resourceTypes = migrator.resourceTypes;
opt.hasPermissions = migrator.hasPermissions;
// Bug 1823489 - since the panel-list and panel-items are slotted, we
// cannot style them directly from migration-wizard.css. We use inline
@ -495,9 +502,45 @@ export class MigrationWizard extends HTMLElement {
* externally to perform the actual migration.
*/
#doImport() {
let migrationEventDetail = this.#gatherMigrationEventDetails();
this.dispatchEvent(
new CustomEvent("MigrationWizard:BeginMigration", {
bubbles: true,
detail: migrationEventDetail,
})
);
}
/**
* @typedef {object} MigrationDetails
* @property {string} key
* The key for a MigratorBase subclass.
* @property {object|null} profile
* A representation of a browser profile. This is serialized and originally
* sent down from the parent via the GetAvailableMigrators message.
* @property {string[]} resourceTypes
* An array of resource types that the user is attempted to import. These
* strings should be from MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.
* @property {boolean} hasPermissions
* True if this MigrationWizardChild told us that the associated
* MigratorBase subclass for the key has enough permission to read
* the requested resources.
*/
/**
* Pulls information from the DOM state of the MigrationWizard and constructs
* and returns an object that can be used to begin migration via and event
* sent to the MigrationWizardChild.
*
* @returns {MigrationDetails} details
*/
#gatherMigrationEventDetails() {
let panelItem = this.#browserProfileSelector.selectedPanelItem;
let key = panelItem.getAttribute("key");
let profile = panelItem.profile;
let hasPermissions = panelItem.hasPermissions;
let resourceTypeFields = this.#resourceTypeList.querySelectorAll(
"label[data-resource-type]"
);
@ -508,14 +551,25 @@ export class MigrationWizard extends HTMLElement {
}
}
return {
key,
profile,
resourceTypes,
hasPermissions,
};
}
/**
* Sends a request to gain read access to the Safari profile folder on
* macOS, and upon gaining access, performs a migration using the current
* settings as gathered by #gatherMigrationEventDetails
*/
#requestSafariPermissions() {
let migrationEventDetail = this.#gatherMigrationEventDetails();
this.dispatchEvent(
new CustomEvent("MigrationWizard:BeginMigration", {
new CustomEvent("MigrationWizard:RequestSafariPermissions", {
bubbles: true,
detail: {
key,
profile,
resourceTypes,
},
detail: migrationEventDetail,
})
);
}
@ -629,6 +683,8 @@ export class MigrationWizard extends HTMLElement {
event.target != this.#browserProfileSelectorList
) {
this.#onBrowserProfileSelectionChanged(event.target);
} else if (event.target == this.#safariPermissionButton) {
this.#requestSafariPermissions();
}
break;
}

Просмотреть файл

@ -9,3 +9,6 @@ prefs =
[browser_dialog_resize.js]
[browser_do_migration.js]
[browser_entrypoint_telemetry.js]
[browser_safari_permissions.js]
run-if =
os == "mac"

Просмотреть файл

@ -0,0 +1,129 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
const { SafariProfileMigrator } = ChromeUtils.importESModule(
"resource:///modules/SafariProfileMigrator.sys.mjs"
);
/**
* Tests that if we don't have permission to read the contents
* of ~/Library/Safari, that we can ask for permission to do that.
*
* This involves presenting the user with some instructions, and then
* showing a native folder picker for the user to select the
* ~/Library/Safari folder. This seems to give us read access to the
* folder contents.
*
* Revoking permissions for reading the ~/Library/Safari folder is
* not something that we know how to do just yet. It seems to be
* something involving macOS's System Integrity Protection. This test
* mocks out and simulates the actual permissions mechanism to make
* this test run reliably and repeatably.
*/
add_task(async function test_safari_permissions() {
let sandbox = sinon.createSandbox();
registerCleanupFunction(() => {
sandbox.restore();
});
Assert.ok(
await MigrationUtils.getMigrator(SafariProfileMigrator.key),
"Safari migrator exists."
);
sandbox
.stub(SafariProfileMigrator.prototype, "hasPermissions")
.onFirstCall()
.resolves(false)
.onSecondCall()
.resolves(true);
sandbox
.stub(SafariProfileMigrator.prototype, "getPermissions")
.resolves(true);
sandbox
.stub(SafariProfileMigrator.prototype, "getResources")
.callsFake(() => {
return Promise.resolve([
{
type: MigrationUtils.resourceTypes.BOOKMARKS,
migrate: () => {},
},
]);
});
let didMigration = new Promise(resolve => {
sandbox
.stub(SafariProfileMigrator.prototype, "migrate")
.callsFake((aResourceTypes, aStartup, aProfile, aProgressCallback) => {
Assert.ok(
!aStartup,
"Migrator should not have been called as a startup migration."
);
aProgressCallback(MigrationUtils.resourceTypes.BOOKMARKS);
Services.obs.notifyObservers(null, "Migration:Ended");
resolve();
});
});
await withMigrationWizardDialog(async prefsWin => {
let dialogBody = prefsWin.document.body;
let wizard = dialogBody.querySelector("migration-wizard");
let wizardDone = BrowserTestUtils.waitForEvent(
wizard,
"MigrationWizard:DoneMigration"
);
let shadow = wizard.openOrClosedShadowRoot;
info("Choosing Safari");
let panelItem = wizard.querySelector(
`panel-item[key="${SafariProfileMigrator.key}"]`
);
panelItem.click();
// Let's just choose "Bookmarks" for now.
let resourceTypeList = shadow.querySelector("#resource-type-list");
let node = resourceTypeList.querySelector(
`label[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS}"]`
);
node.control.checked = true;
let deck = shadow.querySelector("#wizard-deck");
let switchedToSafariPermissionPage = BrowserTestUtils.waitForMutationCondition(
deck,
{ attributeFilter: ["selected-view"] },
() => {
return (
deck.getAttribute("selected-view") ==
"page-" + MigrationWizardConstants.PAGES.SAFARI_PERMISSION
);
}
);
let importButton = shadow.querySelector("#import");
importButton.click();
await switchedToSafariPermissionPage;
Assert.ok(true, "Went to Safari permission page after attempting import.");
let requestPermissions = shadow.querySelector(
"#safari-request-permissions"
);
requestPermissions.click();
await didMigration;
Assert.ok(true, "Completed migration");
let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
let doneButton = shadow.querySelector("#done-button");
let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
doneButton.click();
await dialogClosed;
await wizardDone;
});
});