зеркало из https://github.com/mozilla/gecko-dev.git
Merge mozilla-central to mozilla-inbound
This commit is contained in:
Коммит
997eb39e9d
|
@ -1388,9 +1388,6 @@ pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox
|
|||
// The remote URL of the FxA OAuth Server
|
||||
pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
|
||||
|
||||
// Whether we display profile images in the UI or not.
|
||||
pref("identity.fxaccounts.profile_image.enabled", true);
|
||||
|
||||
// Token server used by the FxA Sync identity.
|
||||
pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
|
||||
|
||||
|
|
|
@ -285,7 +285,7 @@
|
|||
accesskey="&sendPageToDevice.accesskey;"
|
||||
hidden="true">
|
||||
<menupopup id="context-sendpagetodevice-popup"
|
||||
onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gFxAccounts.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/>
|
||||
onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gSync.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/>
|
||||
</menu>
|
||||
<menuseparator id="context-sep-viewbgimage"/>
|
||||
<menuitem id="context-viewbgimage"
|
||||
|
@ -332,7 +332,7 @@
|
|||
accesskey="&sendLinkToDevice.accesskey;"
|
||||
hidden="true">
|
||||
<menupopup id="context-sendlinktodevice-popup"
|
||||
onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
|
||||
onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
|
||||
</menu>
|
||||
<menuitem id="context-shareselect"
|
||||
label="&shareSelect.label;"
|
||||
|
|
|
@ -1,430 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var gFxAccounts = {
|
||||
|
||||
_initialized: false,
|
||||
_inCustomizationMode: false,
|
||||
_cachedProfile: null,
|
||||
|
||||
get weave() {
|
||||
delete this.weave;
|
||||
return this.weave = Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
},
|
||||
|
||||
get topics() {
|
||||
// Do all this dance to lazy-load FxAccountsCommon.
|
||||
delete this.topics;
|
||||
return this.topics = [
|
||||
"weave:service:ready",
|
||||
"weave:service:login:change",
|
||||
"weave:service:setup-complete",
|
||||
"weave:service:sync:error",
|
||||
"weave:ui:login:error",
|
||||
this.FxAccountsCommon.ONLOGIN_NOTIFICATION,
|
||||
this.FxAccountsCommon.ONLOGOUT_NOTIFICATION,
|
||||
this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION,
|
||||
];
|
||||
},
|
||||
|
||||
get panelUIFooter() {
|
||||
delete this.panelUIFooter;
|
||||
return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa");
|
||||
},
|
||||
|
||||
get panelUIStatus() {
|
||||
delete this.panelUIStatus;
|
||||
return this.panelUIStatus = document.getElementById("PanelUI-fxa-status");
|
||||
},
|
||||
|
||||
get panelUIAvatar() {
|
||||
delete this.panelUIAvatar;
|
||||
return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar");
|
||||
},
|
||||
|
||||
get panelUILabel() {
|
||||
delete this.panelUILabel;
|
||||
return this.panelUILabel = document.getElementById("PanelUI-fxa-label");
|
||||
},
|
||||
|
||||
get panelUIIcon() {
|
||||
delete this.panelUIIcon;
|
||||
return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon");
|
||||
},
|
||||
|
||||
get strings() {
|
||||
delete this.strings;
|
||||
return this.strings = Services.strings.createBundle(
|
||||
"chrome://browser/locale/accounts.properties"
|
||||
);
|
||||
},
|
||||
|
||||
get loginFailed() {
|
||||
// Referencing Weave.Service will implicitly initialize sync, and we don't
|
||||
// want to force that - so first check if it is ready.
|
||||
if (!this.weaveService.ready) {
|
||||
return false;
|
||||
}
|
||||
// LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
|
||||
// All other login failures are assumed to be transient and should go
|
||||
// away by themselves, so aren't reflected here.
|
||||
return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
|
||||
},
|
||||
|
||||
get sendTabToDeviceEnabled() {
|
||||
return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
|
||||
},
|
||||
|
||||
isSendableURI(aURISpec) {
|
||||
if (!aURISpec) {
|
||||
return false;
|
||||
}
|
||||
// Disallow sending tabs with more than 65535 characters.
|
||||
if (aURISpec.length > 65535) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// Filter out un-sendable URIs -- things like local files, object urls, etc.
|
||||
const unsendableRegexp = new RegExp(
|
||||
Services.prefs.getCharPref("services.sync.engine.tabs.filteredUrls"), "i");
|
||||
return !unsendableRegexp.test(aURISpec);
|
||||
} catch (e) {
|
||||
// The preference has been removed, or is an invalid regexp, so we log an
|
||||
// error and treat it as a valid URI -- and the more problematic case is
|
||||
// the length, which we've already addressed.
|
||||
Cu.reportError(`Failed to build url filter regexp for send tab: ${e}`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
get remoteClients() {
|
||||
return Weave.Service.clientsEngine.remoteClients
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
},
|
||||
|
||||
init() {
|
||||
// Bail out if we're already initialized and for pop-up windows.
|
||||
if (this._initialized || !window.toolbar.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let topic of this.topics) {
|
||||
Services.obs.addObserver(this, topic);
|
||||
}
|
||||
|
||||
gNavToolbox.addEventListener("customizationstarting", this);
|
||||
gNavToolbox.addEventListener("customizationending", this);
|
||||
|
||||
EnsureFxAccountsWebChannel();
|
||||
this._initialized = true;
|
||||
|
||||
this.updateUI();
|
||||
},
|
||||
|
||||
uninit() {
|
||||
if (!this._initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let topic of this.topics) {
|
||||
Services.obs.removeObserver(this, topic);
|
||||
}
|
||||
|
||||
this._initialized = false;
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION:
|
||||
this._cachedProfile = null;
|
||||
// Fallthrough intended
|
||||
default:
|
||||
this.updateUI();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
this._inCustomizationMode = event.type == "customizationstarting";
|
||||
this.updateUI();
|
||||
},
|
||||
|
||||
// Note that updateUI() returns a Promise that's only used by tests.
|
||||
updateUI() {
|
||||
let profileInfoEnabled = Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled", false);
|
||||
|
||||
this.panelUIFooter.hidden = false;
|
||||
|
||||
// Make sure the button is disabled in customization mode.
|
||||
if (this._inCustomizationMode) {
|
||||
this.panelUIStatus.setAttribute("disabled", "true");
|
||||
this.panelUILabel.setAttribute("disabled", "true");
|
||||
this.panelUIAvatar.setAttribute("disabled", "true");
|
||||
this.panelUIIcon.setAttribute("disabled", "true");
|
||||
} else {
|
||||
this.panelUIStatus.removeAttribute("disabled");
|
||||
this.panelUILabel.removeAttribute("disabled");
|
||||
this.panelUIAvatar.removeAttribute("disabled");
|
||||
this.panelUIIcon.removeAttribute("disabled");
|
||||
}
|
||||
|
||||
let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel");
|
||||
let errorLabel = this.panelUIStatus.getAttribute("errorlabel");
|
||||
let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel");
|
||||
// The localization string is for the signed in text, but it's the default text as well
|
||||
let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext");
|
||||
|
||||
let updateWithUserData = (userData) => {
|
||||
// Window might have been closed while fetching data.
|
||||
if (window.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the button to its original state.
|
||||
this.panelUILabel.setAttribute("label", defaultLabel);
|
||||
this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext);
|
||||
this.panelUIFooter.removeAttribute("fxastatus");
|
||||
this.panelUIFooter.removeAttribute("fxaprofileimage");
|
||||
this.panelUIAvatar.style.removeProperty("list-style-image");
|
||||
let showErrorBadge = false;
|
||||
if (userData) {
|
||||
// At this point we consider the user as logged-in (but still can be in an error state)
|
||||
if (this.loginFailed) {
|
||||
let tooltipDescription = this.strings.formatStringFromName("reconnectDescription", [userData.email], 1);
|
||||
this.panelUIFooter.setAttribute("fxastatus", "error");
|
||||
this.panelUILabel.setAttribute("label", errorLabel);
|
||||
this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
|
||||
showErrorBadge = true;
|
||||
} else if (!userData.verified) {
|
||||
let tooltipDescription = this.strings.formatStringFromName("verifyDescription", [userData.email], 1);
|
||||
this.panelUIFooter.setAttribute("fxastatus", "error");
|
||||
this.panelUIFooter.setAttribute("unverified", "true");
|
||||
this.panelUILabel.setAttribute("label", unverifiedLabel);
|
||||
this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
|
||||
showErrorBadge = true;
|
||||
} else {
|
||||
this.panelUIFooter.setAttribute("fxastatus", "signedin");
|
||||
this.panelUILabel.setAttribute("label", userData.email);
|
||||
}
|
||||
if (profileInfoEnabled) {
|
||||
this.panelUIFooter.setAttribute("fxaprofileimage", "enabled");
|
||||
}
|
||||
}
|
||||
if (showErrorBadge) {
|
||||
PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
|
||||
} else {
|
||||
PanelUI.removeNotification("fxa-needs-authentication");
|
||||
}
|
||||
}
|
||||
|
||||
let updateWithProfile = (profile) => {
|
||||
if (profileInfoEnabled) {
|
||||
if (profile.displayName) {
|
||||
this.panelUILabel.setAttribute("label", profile.displayName);
|
||||
}
|
||||
if (profile.avatar) {
|
||||
this.panelUIFooter.setAttribute("fxaprofileimage", "set");
|
||||
let bgImage = "url(\"" + profile.avatar + "\")";
|
||||
this.panelUIAvatar.style.listStyleImage = bgImage;
|
||||
|
||||
let img = new Image();
|
||||
img.onerror = () => {
|
||||
// Clear the image if it has trouble loading. Since this callback is asynchronous
|
||||
// we check to make sure the image is still the same before we clear it.
|
||||
if (this.panelUIAvatar.style.listStyleImage === bgImage) {
|
||||
this.panelUIFooter.removeAttribute("fxaprofileimage");
|
||||
this.panelUIAvatar.style.removeProperty("list-style-image");
|
||||
}
|
||||
};
|
||||
img.src = profile.avatar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fxAccounts.getSignedInUser().then(userData => {
|
||||
// userData may be null here when the user is not signed-in, but that's expected
|
||||
updateWithUserData(userData);
|
||||
// unverified users cause us to spew log errors fetching an OAuth token
|
||||
// to fetch the profile, so don't even try in that case.
|
||||
if (!userData || !userData.verified || !profileInfoEnabled) {
|
||||
return null; // don't even try to grab the profile.
|
||||
}
|
||||
if (this._cachedProfile) {
|
||||
return this._cachedProfile;
|
||||
}
|
||||
return fxAccounts.getSignedInUserProfile().catch(err => {
|
||||
// Not fetching the profile is sad but the FxA logs will already have noise.
|
||||
return null;
|
||||
});
|
||||
}).then(profile => {
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
updateWithProfile(profile);
|
||||
this._cachedProfile = profile; // Try to avoid fetching the profile on every UI update
|
||||
}).catch(error => {
|
||||
// This is most likely in tests, were we quickly log users in and out.
|
||||
// The most likely scenario is a user logged out, so reflect that.
|
||||
// Bug 995134 calls for better errors so we could retry if we were
|
||||
// sure this was the failure reason.
|
||||
this.FxAccountsCommon.log.error("Error updating FxA account info", error);
|
||||
updateWithUserData(null);
|
||||
});
|
||||
},
|
||||
|
||||
onMenuPanelCommand() {
|
||||
|
||||
switch (this.panelUIFooter.getAttribute("fxastatus")) {
|
||||
case "signedin":
|
||||
this.openPreferences();
|
||||
break;
|
||||
case "error":
|
||||
if (this.panelUIFooter.getAttribute("unverified")) {
|
||||
this.openPreferences();
|
||||
} else {
|
||||
this.openSignInAgainPage("menupanel");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.openPreferences();
|
||||
break;
|
||||
}
|
||||
|
||||
PanelUI.hide();
|
||||
},
|
||||
|
||||
openPreferences() {
|
||||
openPreferences("paneSync", { urlParams: { entrypoint: "menupanel" } });
|
||||
},
|
||||
|
||||
openAccountsPage(action, urlParams = {}) {
|
||||
let params = new URLSearchParams();
|
||||
if (action) {
|
||||
params.set("action", action);
|
||||
}
|
||||
for (let name in urlParams) {
|
||||
if (urlParams[name] !== undefined) {
|
||||
params.set(name, urlParams[name]);
|
||||
}
|
||||
}
|
||||
let url = "about:accounts?" + params;
|
||||
switchToTabHavingURI(url, true, {
|
||||
replaceQueryString: true
|
||||
});
|
||||
},
|
||||
|
||||
openSignInAgainPage(entryPoint) {
|
||||
this.openAccountsPage("reauth", { entrypoint: entryPoint });
|
||||
},
|
||||
|
||||
async openDevicesManagementPage(entryPoint) {
|
||||
let url = await fxAccounts.promiseAccountsManageDevicesURI(entryPoint);
|
||||
switchToTabHavingURI(url, true, {
|
||||
replaceQueryString: true
|
||||
});
|
||||
},
|
||||
|
||||
sendTabToDevice(url, clientId, title) {
|
||||
Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
|
||||
},
|
||||
|
||||
populateSendTabToDevicesMenu(devicesPopup, url, title) {
|
||||
// remove existing menu items
|
||||
while (devicesPopup.hasChildNodes()) {
|
||||
devicesPopup.firstChild.remove();
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
const onTargetDeviceCommand = (event) => {
|
||||
let clients = event.target.getAttribute("clientId") ?
|
||||
[event.target.getAttribute("clientId")] :
|
||||
this.remoteClients.map(client => client.id);
|
||||
|
||||
clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
|
||||
}
|
||||
|
||||
function addTargetDevice(clientId, name) {
|
||||
const targetDevice = document.createElement("menuitem");
|
||||
targetDevice.addEventListener("command", onTargetDeviceCommand, true);
|
||||
targetDevice.setAttribute("class", "sendtab-target");
|
||||
targetDevice.setAttribute("clientId", clientId);
|
||||
targetDevice.setAttribute("label", name);
|
||||
fragment.appendChild(targetDevice);
|
||||
}
|
||||
|
||||
const clients = this.remoteClients;
|
||||
for (let client of clients) {
|
||||
addTargetDevice(client.id, client.name);
|
||||
}
|
||||
|
||||
// "All devices" menu item
|
||||
if (clients.length > 1) {
|
||||
const separator = document.createElement("menuseparator");
|
||||
fragment.appendChild(separator);
|
||||
const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem");
|
||||
addTargetDevice("", allDevicesLabel);
|
||||
}
|
||||
|
||||
devicesPopup.appendChild(fragment);
|
||||
},
|
||||
|
||||
updateTabContextMenu(aPopupMenu, aTargetTab) {
|
||||
if (!this.sendTabToDeviceEnabled ||
|
||||
!this.weaveService.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetURI = aTargetTab.linkedBrowser.currentURI.spec;
|
||||
const showSendTab = this.remoteClients.length > 0 && this.isSendableURI(targetURI);
|
||||
|
||||
["context_sendTabToDevice", "context_sendTabToDevice_separator"]
|
||||
.forEach(id => { document.getElementById(id).hidden = !showSendTab });
|
||||
},
|
||||
|
||||
initPageContextMenu(contextMenu) {
|
||||
if (!this.sendTabToDeviceEnabled ||
|
||||
!this.weaveService.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteClientPresent = this.remoteClients.length > 0;
|
||||
// showSendLink and showSendPage are mutually exclusive
|
||||
let showSendLink = remoteClientPresent
|
||||
&& (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
|
||||
const showSendPage = !showSendLink && remoteClientPresent
|
||||
&& !(contextMenu.isContentSelected ||
|
||||
contextMenu.onImage || contextMenu.onCanvas ||
|
||||
contextMenu.onVideo || contextMenu.onAudio ||
|
||||
contextMenu.onLink || contextMenu.onTextInput)
|
||||
&& this.isSendableURI(contextMenu.browser.currentURI.spec);
|
||||
|
||||
if (showSendLink) {
|
||||
// This isn't part of the condition above since we don't want to try and
|
||||
// send the page if a link is clicked on or selected but is not sendable.
|
||||
showSendLink = this.isSendableURI(contextMenu.linkURL);
|
||||
}
|
||||
|
||||
["context-sendpagetodevice", "context-sep-sendpagetodevice"]
|
||||
.forEach(id => contextMenu.showItem(id, showSendPage));
|
||||
["context-sendlinktodevice", "context-sep-sendlinktodevice"]
|
||||
.forEach(id => contextMenu.showItem(id, showSendLink));
|
||||
}
|
||||
};
|
||||
|
||||
XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function() {
|
||||
return Cu.import("resource://gre/modules/FxAccountsCommon.js", {});
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
|
||||
"resource://gre/modules/FxAccountsWebChannel.jsm");
|
||||
|
||||
|
||||
XPCOMUtils.defineLazyGetter(gFxAccounts, "weaveService", function() {
|
||||
return Components.classes["@mozilla.org/weave/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
});
|
|
@ -475,12 +475,7 @@
|
|||
label="&toolsMenu.label;"
|
||||
accesskey="&toolsMenu.accesskey;"
|
||||
onpopupshowing="mirrorShow(this)">
|
||||
<menupopup id="menu_ToolsPopup"
|
||||
# We have to use setTimeout() here to avoid a flickering menu bar when opening
|
||||
# the Tools menu, see bug 970769. This can be removed once we got rid of the
|
||||
# event loop spinning in Weave.Status._authManager.
|
||||
onpopupshowing="setTimeout(() => gSyncUI.updateUI());"
|
||||
>
|
||||
<menupopup id="menu_ToolsPopup">
|
||||
<menuitem id="menu_openDownloads"
|
||||
label="&downloads.label;"
|
||||
accesskey="&downloads.accesskey;"
|
||||
|
@ -497,17 +492,17 @@
|
|||
label="&syncSignIn.label;"
|
||||
accesskey="&syncSignIn.accesskey;"
|
||||
observes="sync-setup-state"
|
||||
oncommand="gSyncUI.openPrefs('menubar')"/>
|
||||
oncommand="gSync.openPrefs('menubar')"/>
|
||||
<menuitem id="sync-syncnowitem"
|
||||
label="&syncSyncNowItem.label;"
|
||||
accesskey="&syncSyncNowItem.accesskey;"
|
||||
observes="sync-syncnow-state"
|
||||
oncommand="gSyncUI.doSync(event);"/>
|
||||
oncommand="gSync.doSync(event);"/>
|
||||
<menuitem id="sync-reauthitem"
|
||||
label="&syncReAuthItem.label;"
|
||||
accesskey="&syncReAuthItem.accesskey;"
|
||||
observes="sync-reauth-state"
|
||||
oncommand="gSyncUI.openSignInAgainPage('menubar');"/>
|
||||
oncommand="gSync.openSignInAgainPage('menubar');"/>
|
||||
<menuseparator id="devToolsSeparator"/>
|
||||
<menu id="webDeveloperMenu"
|
||||
label="&webDeveloperMenu.label;"
|
||||
|
|
|
@ -0,0 +1,574 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
Cu.import("resource://services-sync/UIState.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
|
||||
"resource://gre/modules/FxAccountsWebChannel.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Weave",
|
||||
"resource://services-sync/main.js");
|
||||
if (AppConstants.MOZ_SERVICES_CLOUDSYNC) {
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
|
||||
"resource://gre/modules/CloudSync.jsm");
|
||||
}
|
||||
|
||||
const MIN_STATUS_ANIMATION_DURATION = 1600;
|
||||
|
||||
var gSync = {
|
||||
_initialized: false,
|
||||
// The last sync start time. Used to calculate the leftover animation time
|
||||
// once syncing completes (bug 1239042).
|
||||
_syncStartTime: 0,
|
||||
_syncAnimationTimer: 0,
|
||||
|
||||
_obs: [
|
||||
"weave:engine:sync:finish",
|
||||
"quit-application",
|
||||
UIState.ON_UPDATE
|
||||
],
|
||||
|
||||
get panelUIFooter() {
|
||||
delete this.panelUIFooter;
|
||||
return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa");
|
||||
},
|
||||
|
||||
get panelUIStatus() {
|
||||
delete this.panelUIStatus;
|
||||
return this.panelUIStatus = document.getElementById("PanelUI-fxa-status");
|
||||
},
|
||||
|
||||
get panelUIAvatar() {
|
||||
delete this.panelUIAvatar;
|
||||
return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar");
|
||||
},
|
||||
|
||||
get panelUILabel() {
|
||||
delete this.panelUILabel;
|
||||
return this.panelUILabel = document.getElementById("PanelUI-fxa-label");
|
||||
},
|
||||
|
||||
get panelUIIcon() {
|
||||
delete this.panelUIIcon;
|
||||
return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon");
|
||||
},
|
||||
|
||||
get fxaStrings() {
|
||||
delete this.fxaStrings;
|
||||
return this.fxaStrings = Services.strings.createBundle(
|
||||
"chrome://browser/locale/accounts.properties"
|
||||
);
|
||||
},
|
||||
|
||||
get syncStrings() {
|
||||
delete this.syncStrings;
|
||||
// XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
|
||||
// but for now just make it work
|
||||
return this.syncStrings = Services.strings.createBundle(
|
||||
"chrome://weave/locale/sync.properties"
|
||||
);
|
||||
},
|
||||
|
||||
get sendTabToDeviceEnabled() {
|
||||
return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
|
||||
},
|
||||
|
||||
get remoteClients() {
|
||||
return Weave.Service.clientsEngine.remoteClients
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
},
|
||||
|
||||
init() {
|
||||
// Bail out if we're already initialized or for pop-up windows.
|
||||
if (this._initialized || !window.toolbar.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let topic of this._obs) {
|
||||
Services.obs.addObserver(this, topic, true);
|
||||
}
|
||||
|
||||
// initial label for the sync buttons.
|
||||
let broadcaster = document.getElementById("sync-status");
|
||||
broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncnow.label"));
|
||||
|
||||
// Update the UI
|
||||
if (UIState.isReady()) {
|
||||
const state = UIState.get();
|
||||
// If we are not configured, the UI is already in the right state when
|
||||
// we open the window. We can avoid a repaint.
|
||||
if (state.status != UIState.STATUS_NOT_CONFIGURED) {
|
||||
this.updateAllUI(state);
|
||||
}
|
||||
}
|
||||
|
||||
this.maybeMoveSyncedTabsButton();
|
||||
|
||||
EnsureFxAccountsWebChannel();
|
||||
|
||||
this._initialized = true;
|
||||
},
|
||||
|
||||
uninit() {
|
||||
if (!this._initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let topic of this._obs) {
|
||||
Services.obs.removeObserver(this, topic);
|
||||
}
|
||||
|
||||
this._initialized = false;
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
if (!this._initialized) {
|
||||
Cu.reportError("browser-sync observer called after unload: " + topic);
|
||||
return;
|
||||
}
|
||||
switch (topic) {
|
||||
case UIState.ON_UPDATE:
|
||||
const state = UIState.get();
|
||||
this.updateAllUI(state);
|
||||
break;
|
||||
case "quit-application":
|
||||
// Stop the animation timer on shutdown, since we can't update the UI
|
||||
// after this.
|
||||
clearTimeout(this._syncAnimationTimer);
|
||||
break;
|
||||
case "weave:engine:sync:finish":
|
||||
if (data != "clients") {
|
||||
return;
|
||||
}
|
||||
this.onClientsSynced();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
updateAllUI(state) {
|
||||
this.updatePanelBadge(state);
|
||||
this.updatePanelPopup(state);
|
||||
this.updateStateBroadcasters(state);
|
||||
this.updateSyncButtonsTooltip(state);
|
||||
this.updateSyncStatus(state);
|
||||
},
|
||||
|
||||
updatePanelPopup(state) {
|
||||
let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel");
|
||||
// The localization string is for the signed in text, but it's the default text as well
|
||||
let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext");
|
||||
|
||||
const status = state.status;
|
||||
// Reset the status bar to its original state.
|
||||
this.panelUILabel.setAttribute("label", defaultLabel);
|
||||
this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext);
|
||||
this.panelUIFooter.removeAttribute("fxastatus");
|
||||
this.panelUIAvatar.style.removeProperty("list-style-image");
|
||||
|
||||
if (status == UIState.STATUS_NOT_CONFIGURED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point we consider sync to be configured (but still can be in an error state).
|
||||
if (status == UIState.STATUS_LOGIN_FAILED) {
|
||||
let tooltipDescription = this.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
|
||||
let errorLabel = this.panelUIStatus.getAttribute("errorlabel");
|
||||
this.panelUIFooter.setAttribute("fxastatus", "login-failed");
|
||||
this.panelUILabel.setAttribute("label", errorLabel);
|
||||
this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
|
||||
return;
|
||||
} else if (status == UIState.STATUS_NOT_VERIFIED) {
|
||||
let tooltipDescription = this.fxaStrings.formatStringFromName("verifyDescription", [state.email], 1);
|
||||
let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel");
|
||||
this.panelUIFooter.setAttribute("fxastatus", "unverified");
|
||||
this.panelUILabel.setAttribute("label", unverifiedLabel);
|
||||
this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point we consider sync to be logged-in.
|
||||
this.panelUIFooter.setAttribute("fxastatus", "signedin");
|
||||
this.panelUILabel.setAttribute("label", state.displayName || state.email);
|
||||
|
||||
if (state.avatarURL) {
|
||||
let bgImage = "url(\"" + state.avatarURL + "\")";
|
||||
this.panelUIAvatar.style.listStyleImage = bgImage;
|
||||
|
||||
let img = new Image();
|
||||
img.onerror = () => {
|
||||
// Clear the image if it has trouble loading. Since this callback is asynchronous
|
||||
// we check to make sure the image is still the same before we clear it.
|
||||
if (this.panelUIAvatar.style.listStyleImage === bgImage) {
|
||||
this.panelUIAvatar.style.removeProperty("list-style-image");
|
||||
}
|
||||
};
|
||||
img.src = state.avatarURL;
|
||||
}
|
||||
},
|
||||
|
||||
updatePanelBadge(state) {
|
||||
if (state.status == UIState.STATUS_LOGIN_FAILED ||
|
||||
state.status == UIState.STATUS_NOT_VERIFIED) {
|
||||
PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
|
||||
} else {
|
||||
PanelUI.removeNotification("fxa-needs-authentication");
|
||||
}
|
||||
},
|
||||
|
||||
updateStateBroadcasters(state) {
|
||||
const status = state.status;
|
||||
|
||||
// Start off with a clean slate
|
||||
document.getElementById("sync-reauth-state").hidden = true;
|
||||
document.getElementById("sync-setup-state").hidden = true;
|
||||
document.getElementById("sync-syncnow-state").hidden = true;
|
||||
|
||||
if (CloudSync && CloudSync.ready && CloudSync().adapters.count) {
|
||||
document.getElementById("sync-syncnow-state").hidden = false;
|
||||
} else if (status == UIState.STATUS_LOGIN_FAILED) {
|
||||
// unhiding this element makes the menubar show the login failure state.
|
||||
document.getElementById("sync-reauth-state").hidden = false;
|
||||
} else if (status == UIState.STATUS_NOT_CONFIGURED ||
|
||||
status == UIState.STATUS_NOT_VERIFIED) {
|
||||
document.getElementById("sync-setup-state").hidden = false;
|
||||
} else {
|
||||
document.getElementById("sync-syncnow-state").hidden = false;
|
||||
}
|
||||
},
|
||||
|
||||
updateSyncStatus(state) {
|
||||
const broadcaster = document.getElementById("sync-status");
|
||||
const syncingUI = broadcaster.getAttribute("syncstatus") == "active";
|
||||
if (state.syncing != syncingUI) { // Do we need to update the UI?
|
||||
state.syncing ? this.onActivityStart() : this.onActivityStop();
|
||||
}
|
||||
},
|
||||
|
||||
onMenuPanelCommand() {
|
||||
switch (this.panelUIFooter.getAttribute("fxastatus")) {
|
||||
case "signedin":
|
||||
this.openPrefs("menupanel");
|
||||
break;
|
||||
case "error":
|
||||
if (this.panelUIFooter.getAttribute("fxastatus") == "unverified") {
|
||||
this.openPrefs("menupanel");
|
||||
} else {
|
||||
this.openSignInAgainPage("menupanel");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.openPrefs("menupanel");
|
||||
break;
|
||||
}
|
||||
|
||||
PanelUI.hide();
|
||||
},
|
||||
|
||||
openAccountsPage(action, urlParams = {}) {
|
||||
let params = new URLSearchParams();
|
||||
if (action) {
|
||||
params.set("action", action);
|
||||
}
|
||||
for (let name in urlParams) {
|
||||
if (urlParams[name] !== undefined) {
|
||||
params.set(name, urlParams[name]);
|
||||
}
|
||||
}
|
||||
let url = "about:accounts?" + params;
|
||||
switchToTabHavingURI(url, true, {
|
||||
replaceQueryString: true
|
||||
});
|
||||
},
|
||||
|
||||
openSignInAgainPage(entryPoint) {
|
||||
this.openAccountsPage("reauth", { entrypoint: entryPoint });
|
||||
},
|
||||
|
||||
async openDevicesManagementPage(entryPoint) {
|
||||
let url = await fxAccounts.promiseAccountsManageDevicesURI(entryPoint);
|
||||
switchToTabHavingURI(url, true, {
|
||||
replaceQueryString: true
|
||||
});
|
||||
},
|
||||
|
||||
sendTabToDevice(url, clientId, title) {
|
||||
Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
|
||||
},
|
||||
|
||||
populateSendTabToDevicesMenu(devicesPopup, url, title) {
|
||||
// remove existing menu items
|
||||
while (devicesPopup.hasChildNodes()) {
|
||||
devicesPopup.firstChild.remove();
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
const onTargetDeviceCommand = (event) => {
|
||||
let clients = event.target.getAttribute("clientId") ?
|
||||
[event.target.getAttribute("clientId")] :
|
||||
this.remoteClients.map(client => client.id);
|
||||
|
||||
clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
|
||||
}
|
||||
|
||||
function addTargetDevice(clientId, name) {
|
||||
const targetDevice = document.createElement("menuitem");
|
||||
targetDevice.addEventListener("command", onTargetDeviceCommand, true);
|
||||
targetDevice.setAttribute("class", "sendtab-target");
|
||||
targetDevice.setAttribute("clientId", clientId);
|
||||
targetDevice.setAttribute("label", name);
|
||||
fragment.appendChild(targetDevice);
|
||||
}
|
||||
|
||||
const clients = this.remoteClients;
|
||||
for (let client of clients) {
|
||||
addTargetDevice(client.id, client.name);
|
||||
}
|
||||
|
||||
// "All devices" menu item
|
||||
if (clients.length > 1) {
|
||||
const separator = document.createElement("menuseparator");
|
||||
fragment.appendChild(separator);
|
||||
const allDevicesLabel = this.fxaStrings.GetStringFromName("sendTabToAllDevices.menuitem");
|
||||
addTargetDevice("", allDevicesLabel);
|
||||
}
|
||||
|
||||
devicesPopup.appendChild(fragment);
|
||||
},
|
||||
|
||||
isSendableURI(aURISpec) {
|
||||
if (!aURISpec) {
|
||||
return false;
|
||||
}
|
||||
// Disallow sending tabs with more than 65535 characters.
|
||||
if (aURISpec.length > 65535) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// Filter out un-sendable URIs -- things like local files, object urls, etc.
|
||||
const unsendableRegexp = new RegExp(
|
||||
Services.prefs.getCharPref("services.sync.engine.tabs.filteredUrls"), "i");
|
||||
return !unsendableRegexp.test(aURISpec);
|
||||
} catch (e) {
|
||||
// The preference has been removed, or is an invalid regexp, so we log an
|
||||
// error and treat it as a valid URI -- and the more problematic case is
|
||||
// the length, which we've already addressed.
|
||||
Cu.reportError(`Failed to build url filter regexp for send tab: ${e}`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
updateTabContextMenu(aPopupMenu, aTargetTab) {
|
||||
if (!this.sendTabToDeviceEnabled || !this.weaveService.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetURI = aTargetTab.linkedBrowser.currentURI.spec;
|
||||
const showSendTab = this.remoteClients.length > 0 && this.isSendableURI(targetURI);
|
||||
|
||||
["context_sendTabToDevice", "context_sendTabToDevice_separator"]
|
||||
.forEach(id => { document.getElementById(id).hidden = !showSendTab });
|
||||
},
|
||||
|
||||
initPageContextMenu(contextMenu) {
|
||||
if (!this.sendTabToDeviceEnabled || !this.weaveService.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteClientPresent = this.remoteClients.length > 0;
|
||||
// showSendLink and showSendPage are mutually exclusive
|
||||
let showSendLink = remoteClientPresent
|
||||
&& (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
|
||||
const showSendPage = !showSendLink && remoteClientPresent
|
||||
&& !(contextMenu.isContentSelected ||
|
||||
contextMenu.onImage || contextMenu.onCanvas ||
|
||||
contextMenu.onVideo || contextMenu.onAudio ||
|
||||
contextMenu.onLink || contextMenu.onTextInput)
|
||||
&& this.isSendableURI(contextMenu.browser.currentURI.spec);
|
||||
|
||||
if (showSendLink) {
|
||||
// This isn't part of the condition above since we don't want to try and
|
||||
// send the page if a link is clicked on or selected but is not sendable.
|
||||
showSendLink = this.isSendableURI(contextMenu.linkURL);
|
||||
}
|
||||
|
||||
["context-sendpagetodevice", "context-sep-sendpagetodevice"]
|
||||
.forEach(id => contextMenu.showItem(id, showSendPage));
|
||||
["context-sendlinktodevice", "context-sep-sendlinktodevice"]
|
||||
.forEach(id => contextMenu.showItem(id, showSendLink));
|
||||
},
|
||||
|
||||
// Functions called by observers
|
||||
onActivityStart() {
|
||||
clearTimeout(this._syncAnimationTimer);
|
||||
this._syncStartTime = Date.now();
|
||||
|
||||
let broadcaster = document.getElementById("sync-status");
|
||||
broadcaster.setAttribute("syncstatus", "active");
|
||||
broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncing2.label"));
|
||||
broadcaster.setAttribute("disabled", "true");
|
||||
},
|
||||
|
||||
_onActivityStop() {
|
||||
if (!gBrowser)
|
||||
return;
|
||||
let broadcaster = document.getElementById("sync-status");
|
||||
broadcaster.removeAttribute("syncstatus");
|
||||
broadcaster.removeAttribute("disabled");
|
||||
broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncnow.label"));
|
||||
Services.obs.notifyObservers(null, "test:browser-sync:activity-stop");
|
||||
},
|
||||
|
||||
onActivityStop() {
|
||||
let now = Date.now();
|
||||
let syncDuration = now - this._syncStartTime;
|
||||
|
||||
if (syncDuration < MIN_STATUS_ANIMATION_DURATION) {
|
||||
let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration;
|
||||
clearTimeout(this._syncAnimationTimer);
|
||||
this._syncAnimationTimer = setTimeout(() => this._onActivityStop(), animationTime);
|
||||
} else {
|
||||
this._onActivityStop();
|
||||
}
|
||||
},
|
||||
|
||||
// doSync forces a sync - it *does not* return a promise as it is called
|
||||
// via the various UI components.
|
||||
doSync() {
|
||||
if (!UIState.isReady()) {
|
||||
return;
|
||||
}
|
||||
const state = UIState.get();
|
||||
if (state.status == UIState.STATUS_SIGNED_IN) {
|
||||
setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0);
|
||||
}
|
||||
Services.obs.notifyObservers(null, "cloudsync:user-sync");
|
||||
},
|
||||
|
||||
openPrefs(entryPoint = "syncbutton") {
|
||||
window.openPreferences("paneSync", { urlParams: { entrypoint: entryPoint } });
|
||||
},
|
||||
|
||||
openSyncedTabsPanel() {
|
||||
let placement = CustomizableUI.getPlacementOfWidget("sync-button");
|
||||
let area = placement ? placement.area : CustomizableUI.AREA_NAVBAR;
|
||||
let anchor = document.getElementById("sync-button") ||
|
||||
document.getElementById("PanelUI-menu-button");
|
||||
if (area == CustomizableUI.AREA_PANEL) {
|
||||
// The button is in the panel, so we need to show the panel UI, then our
|
||||
// subview.
|
||||
PanelUI.show().then(() => {
|
||||
PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
|
||||
}).catch(Cu.reportError);
|
||||
} else {
|
||||
// It is placed somewhere else - just try and show it.
|
||||
PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
|
||||
}
|
||||
},
|
||||
|
||||
/* After we are initialized we perform a once-only check for the sync
|
||||
button being in "customize purgatory" and if so, move it to the panel.
|
||||
This is done primarily for profiles created before SyncedTabs landed,
|
||||
where the button defaulted to being in that purgatory.
|
||||
We use a preference to ensure we only do it once, so people can still
|
||||
customize it away and have it stick.
|
||||
*/
|
||||
maybeMoveSyncedTabsButton() {
|
||||
const prefName = "browser.migrated-sync-button";
|
||||
let migrated = Services.prefs.getBoolPref(prefName, false);
|
||||
if (migrated) {
|
||||
return;
|
||||
}
|
||||
if (!CustomizableUI.getPlacementOfWidget("sync-button")) {
|
||||
CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
|
||||
}
|
||||
Services.prefs.setBoolPref(prefName, true);
|
||||
},
|
||||
|
||||
/* Update the tooltip for the sync-status broadcaster (which will update the
|
||||
Sync Toolbar button and the Sync spinner in the FxA hamburger area.)
|
||||
If Sync is configured, the tooltip is when the last sync occurred,
|
||||
otherwise the tooltip reflects the fact that Sync needs to be
|
||||
(re-)configured.
|
||||
*/
|
||||
updateSyncButtonsTooltip(state) {
|
||||
const status = state.status;
|
||||
|
||||
// This is a little messy as the Sync buttons are 1/2 Sync related and
|
||||
// 1/2 FxA related - so for some strings we use Sync strings, but for
|
||||
// others we reach into gSync for strings.
|
||||
let tooltiptext;
|
||||
if (status == UIState.STATUS_NOT_VERIFIED) {
|
||||
// "needs verification"
|
||||
tooltiptext = this.fxaStrings.formatStringFromName("verifyDescription", [state.email], 1);
|
||||
} else if (status == UIState.STATUS_NOT_CONFIGURED) {
|
||||
// "needs setup".
|
||||
tooltiptext = this.syncStrings.GetStringFromName("signInToSync.description");
|
||||
} else if (status == UIState.STATUS_LOGIN_FAILED) {
|
||||
// "need to reconnect/re-enter your password"
|
||||
tooltiptext = this.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
|
||||
} else {
|
||||
// Sync appears configured - format the "last synced at" time.
|
||||
tooltiptext = this.formatLastSyncDate(state.lastSync);
|
||||
}
|
||||
|
||||
let broadcaster = document.getElementById("sync-status");
|
||||
if (broadcaster) {
|
||||
if (tooltiptext) {
|
||||
broadcaster.setAttribute("tooltiptext", tooltiptext);
|
||||
} else {
|
||||
broadcaster.removeAttribute("tooltiptext");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get withinLastWeekFormat() {
|
||||
delete this.withinLastWeekFormat;
|
||||
return this.withinLastWeekFormat = new Intl.DateTimeFormat(undefined,
|
||||
{weekday: "long", hour: "numeric", minute: "numeric"});
|
||||
},
|
||||
|
||||
get oneWeekOrOlderFormat() {
|
||||
delete this.oneWeekOrOlderFormat;
|
||||
return this.oneWeekOrOlderFormat = new Intl.DateTimeFormat(undefined,
|
||||
{month: "long", day: "numeric"});
|
||||
},
|
||||
|
||||
formatLastSyncDate(date) {
|
||||
let sixDaysAgo = (() => {
|
||||
let tempDate = new Date();
|
||||
tempDate.setDate(tempDate.getDate() - 6);
|
||||
tempDate.setHours(0, 0, 0, 0);
|
||||
return tempDate;
|
||||
})();
|
||||
|
||||
// It may be confusing for the user to see "Last Sync: Monday" when the last
|
||||
// sync was indeed a Monday, but 3 weeks ago.
|
||||
let dateFormat = date < sixDaysAgo ? this.oneWeekOrOlderFormat : this.withinLastWeekFormat;
|
||||
|
||||
let lastSyncDateString = dateFormat.format(date);
|
||||
return this.syncStrings.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
|
||||
},
|
||||
|
||||
onClientsSynced() {
|
||||
let broadcaster = document.getElementById("sync-syncnow-state");
|
||||
if (broadcaster) {
|
||||
if (Weave.Service.clientsEngine.stats.numClients > 1) {
|
||||
broadcaster.setAttribute("devices-status", "multi");
|
||||
} else {
|
||||
broadcaster.setAttribute("devices-status", "single");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([
|
||||
Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference
|
||||
])
|
||||
};
|
||||
|
||||
XPCOMUtils.defineLazyGetter(gSync, "weaveService", function() {
|
||||
return Components.classes["@mozilla.org/weave/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
});
|
|
@ -1,498 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
if (AppConstants.MOZ_SERVICES_CLOUDSYNC) {
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
|
||||
"resource://gre/modules/CloudSync.jsm");
|
||||
}
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
|
||||
"resource://gre/modules/FxAccounts.jsm");
|
||||
|
||||
const MIN_STATUS_ANIMATION_DURATION = 1600;
|
||||
|
||||
// gSyncUI handles updating the tools menu and displaying notifications.
|
||||
var gSyncUI = {
|
||||
_obs: ["weave:service:sync:start",
|
||||
"weave:service:sync:finish",
|
||||
"weave:service:sync:error",
|
||||
"weave:service:setup-complete",
|
||||
"weave:service:login:start",
|
||||
"weave:service:login:finish",
|
||||
"weave:service:login:error",
|
||||
"weave:service:logout:finish",
|
||||
"weave:service:start-over",
|
||||
"weave:service:start-over:finish",
|
||||
"weave:ui:login:error",
|
||||
"weave:ui:sync:error",
|
||||
"weave:ui:sync:finish",
|
||||
"weave:ui:clear-error",
|
||||
"weave:engine:sync:finish"
|
||||
],
|
||||
|
||||
_unloaded: false,
|
||||
// The last sync start time. Used to calculate the leftover animation time
|
||||
// once syncing completes (bug 1239042).
|
||||
_syncStartTime: 0,
|
||||
_syncAnimationTimer: 0,
|
||||
_withinLastWeekFormat: null,
|
||||
_oneWeekOrOlderFormat: null,
|
||||
|
||||
init() {
|
||||
// Proceed to set up the UI if Sync has already started up.
|
||||
// Otherwise we'll do it when Sync is firing up.
|
||||
if (this.weaveService.ready) {
|
||||
this.initUI();
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync isn't ready yet, but we can still update the UI with an initial
|
||||
// state - we haven't called initUI() yet, but that's OK - that's more
|
||||
// about observers for state changes, and will be called once Sync is
|
||||
// ready to start sending notifications.
|
||||
this.updateUI();
|
||||
|
||||
Services.obs.addObserver(this, "weave:service:ready", true);
|
||||
Services.obs.addObserver(this, "quit-application", true);
|
||||
|
||||
// Remove the observer if the window is closed before the observer
|
||||
// was triggered.
|
||||
window.addEventListener("unload", () => {
|
||||
this._unloaded = true;
|
||||
Services.obs.removeObserver(this, "weave:service:ready");
|
||||
Services.obs.removeObserver(this, "quit-application");
|
||||
|
||||
if (this.weaveService.ready) {
|
||||
this._obs.forEach(topic => {
|
||||
Services.obs.removeObserver(this, topic);
|
||||
});
|
||||
}
|
||||
}, { once: true });
|
||||
},
|
||||
|
||||
initUI: function SUI_initUI() {
|
||||
// If this is a browser window?
|
||||
if (gBrowser) {
|
||||
this._obs.push("weave:notification:added");
|
||||
}
|
||||
|
||||
this._obs.forEach(function(topic) {
|
||||
Services.obs.addObserver(this, topic, true);
|
||||
}, this);
|
||||
|
||||
// initial label for the sync buttons.
|
||||
let broadcaster = document.getElementById("sync-status");
|
||||
broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label"));
|
||||
|
||||
this.maybeMoveSyncedTabsButton();
|
||||
|
||||
this.updateUI();
|
||||
},
|
||||
|
||||
|
||||
// Returns a promise that resolves with true if Sync needs to be configured,
|
||||
// false otherwise.
|
||||
_needsSetup() {
|
||||
return fxAccounts.getSignedInUser().then(user => {
|
||||
// We want to treat "account needs verification" as "needs setup".
|
||||
return !(user && user.verified);
|
||||
});
|
||||
},
|
||||
|
||||
// Returns a promise that resolves with true if the user currently signed in
|
||||
// to Sync needs to be verified, false otherwise.
|
||||
_needsVerification() {
|
||||
return fxAccounts.getSignedInUser().then(user => {
|
||||
// If there is no user, they can't be in a "needs verification" state.
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return !user.verified;
|
||||
});
|
||||
},
|
||||
|
||||
// Note that we don't show login errors in a notification bar here, but do
|
||||
// still need to track a login-failed state so the "Tools" menu updates
|
||||
// with the correct state.
|
||||
loginFailed() {
|
||||
// If Sync isn't already ready, we don't want to force it to initialize
|
||||
// by referencing Weave.Status - and it isn't going to be accurate before
|
||||
// Sync is ready anyway.
|
||||
if (!this.weaveService.ready) {
|
||||
this.log.debug("loginFailed has sync not ready, so returning false");
|
||||
return false;
|
||||
}
|
||||
this.log.debug("loginFailed has sync state=${sync}",
|
||||
{ sync: Weave.Status.login});
|
||||
return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
|
||||
},
|
||||
|
||||
// Kick off an update of the UI - does *not* return a promise.
|
||||
updateUI() {
|
||||
this._promiseUpdateUI().catch(err => {
|
||||
this.log.error("updateUI failed", err);
|
||||
})
|
||||
},
|
||||
|
||||
// Updates the UI - returns a promise.
|
||||
_promiseUpdateUI() {
|
||||
return this._needsSetup().then(needsSetup => {
|
||||
if (!gBrowser)
|
||||
return Promise.resolve();
|
||||
|
||||
let loginFailed = this.loginFailed();
|
||||
|
||||
// Start off with a clean slate
|
||||
document.getElementById("sync-reauth-state").hidden = true;
|
||||
document.getElementById("sync-setup-state").hidden = true;
|
||||
document.getElementById("sync-syncnow-state").hidden = true;
|
||||
|
||||
if (CloudSync && CloudSync.ready && CloudSync().adapters.count) {
|
||||
document.getElementById("sync-syncnow-state").hidden = false;
|
||||
} else if (loginFailed) {
|
||||
// unhiding this element makes the menubar show the login failure state.
|
||||
document.getElementById("sync-reauth-state").hidden = false;
|
||||
} else if (needsSetup) {
|
||||
document.getElementById("sync-setup-state").hidden = false;
|
||||
} else {
|
||||
document.getElementById("sync-syncnow-state").hidden = false;
|
||||
}
|
||||
|
||||
return this._updateSyncButtonsTooltip();
|
||||
});
|
||||
},
|
||||
|
||||
// Functions called by observers
|
||||
onActivityStart() {
|
||||
if (!gBrowser)
|
||||
return;
|
||||
|
||||
this.log.debug("onActivityStart");
|
||||
|
||||
clearTimeout(this._syncAnimationTimer);
|
||||
this._syncStartTime = Date.now();
|
||||
|
||||
let broadcaster = document.getElementById("sync-status");
|
||||
broadcaster.setAttribute("syncstatus", "active");
|
||||
broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncing2.label"));
|
||||
broadcaster.setAttribute("disabled", "true");
|
||||
|
||||
this.updateUI();
|
||||
},
|
||||
|
||||
_updateSyncStatus() {
|
||||
if (!gBrowser)
|
||||
return;
|
||||
let broadcaster = document.getElementById("sync-status");
|
||||
broadcaster.removeAttribute("syncstatus");
|
||||
broadcaster.removeAttribute("disabled");
|
||||
broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label"));
|
||||
this.updateUI();
|
||||
},
|
||||
|
||||
onActivityStop() {
|
||||
if (!gBrowser)
|
||||
return;
|
||||
this.log.debug("onActivityStop");
|
||||
|
||||
let now = Date.now();
|
||||
let syncDuration = now - this._syncStartTime;
|
||||
|
||||
if (syncDuration < MIN_STATUS_ANIMATION_DURATION) {
|
||||
let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration;
|
||||
clearTimeout(this._syncAnimationTimer);
|
||||
this._syncAnimationTimer = setTimeout(() => this._updateSyncStatus(), animationTime);
|
||||
} else {
|
||||
this._updateSyncStatus();
|
||||
}
|
||||
},
|
||||
|
||||
onLoginError: function SUI_onLoginError() {
|
||||
this.log.debug("onLoginError: login=${login}, sync=${sync}", Weave.Status);
|
||||
|
||||
// We don't show any login errors here; browser-fxaccounts shows them in
|
||||
// the hamburger menu.
|
||||
this.updateUI();
|
||||
},
|
||||
|
||||
onLogout: function SUI_onLogout() {
|
||||
this.updateUI();
|
||||
},
|
||||
|
||||
_getAppName() {
|
||||
let brand = Services.strings.createBundle(
|
||||
"chrome://branding/locale/brand.properties");
|
||||
return brand.GetStringFromName("brandShortName");
|
||||
},
|
||||
|
||||
// Commands
|
||||
// doSync forces a sync - it *does not* return a promise as it is called
|
||||
// via the various UI components.
|
||||
doSync() {
|
||||
this._needsSetup().then(needsSetup => {
|
||||
if (!needsSetup) {
|
||||
setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0);
|
||||
}
|
||||
Services.obs.notifyObservers(null, "cloudsync:user-sync");
|
||||
}).catch(err => {
|
||||
this.log.error("Failed to force a sync", err);
|
||||
});
|
||||
},
|
||||
|
||||
// Handle clicking the toolbar button - which either opens the Sync setup
|
||||
// pages or forces a sync now. Does *not* return a promise as it is called
|
||||
// via the UI.
|
||||
handleToolbarButton() {
|
||||
this._needsSetup().then(needsSetup => {
|
||||
if (needsSetup || this.loginFailed()) {
|
||||
this.openPrefs();
|
||||
} else {
|
||||
this.doSync();
|
||||
}
|
||||
}).catch(err => {
|
||||
this.log.error("Failed to handle toolbar button command", err);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the Sync preferences.
|
||||
*
|
||||
* @param entryPoint
|
||||
* Indicates the entrypoint from where this method was called.
|
||||
*/
|
||||
openPrefs(entryPoint = "syncbutton") {
|
||||
openPreferences("paneSync", { urlParams: { entrypoint: entryPoint } });
|
||||
},
|
||||
|
||||
openSignInAgainPage(entryPoint = "syncbutton") {
|
||||
gFxAccounts.openSignInAgainPage(entryPoint);
|
||||
},
|
||||
|
||||
openSyncedTabsPanel() {
|
||||
let placement = CustomizableUI.getPlacementOfWidget("sync-button");
|
||||
let area = placement ? placement.area : CustomizableUI.AREA_NAVBAR;
|
||||
let anchor = document.getElementById("sync-button") ||
|
||||
document.getElementById("PanelUI-menu-button");
|
||||
if (area == CustomizableUI.AREA_PANEL) {
|
||||
// The button is in the panel, so we need to show the panel UI, then our
|
||||
// subview.
|
||||
PanelUI.show().then(() => {
|
||||
PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
|
||||
}).catch(Cu.reportError);
|
||||
} else {
|
||||
// It is placed somewhere else - just try and show it.
|
||||
PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
|
||||
}
|
||||
},
|
||||
|
||||
/* After Sync is initialized we perform a once-only check for the sync
|
||||
button being in "customize purgatory" and if so, move it to the panel.
|
||||
This is done primarily for profiles created before SyncedTabs landed,
|
||||
where the button defaulted to being in that purgatory.
|
||||
We use a preference to ensure we only do it once, so people can still
|
||||
customize it away and have it stick.
|
||||
*/
|
||||
maybeMoveSyncedTabsButton() {
|
||||
const prefName = "browser.migrated-sync-button";
|
||||
let migrated = Services.prefs.getBoolPref(prefName, false);
|
||||
if (migrated) {
|
||||
return;
|
||||
}
|
||||
if (!CustomizableUI.getPlacementOfWidget("sync-button")) {
|
||||
CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
|
||||
}
|
||||
Services.prefs.setBoolPref(prefName, true);
|
||||
},
|
||||
|
||||
/* Update the tooltip for the sync-status broadcaster (which will update the
|
||||
Sync Toolbar button and the Sync spinner in the FxA hamburger area.)
|
||||
If Sync is configured, the tooltip is when the last sync occurred,
|
||||
otherwise the tooltip reflects the fact that Sync needs to be
|
||||
(re-)configured.
|
||||
*/
|
||||
_updateSyncButtonsTooltip: Task.async(function* () {
|
||||
if (!gBrowser)
|
||||
return;
|
||||
|
||||
let email;
|
||||
let user = yield fxAccounts.getSignedInUser();
|
||||
if (user) {
|
||||
email = user.email;
|
||||
}
|
||||
|
||||
let needsSetup = yield this._needsSetup();
|
||||
let needsVerification = yield this._needsVerification();
|
||||
let loginFailed = this.loginFailed();
|
||||
// This is a little messy as the Sync buttons are 1/2 Sync related and
|
||||
// 1/2 FxA related - so for some strings we use Sync strings, but for
|
||||
// others we reach into gFxAccounts for strings.
|
||||
let tooltiptext;
|
||||
if (needsVerification) {
|
||||
// "needs verification"
|
||||
tooltiptext = gFxAccounts.strings.formatStringFromName("verifyDescription", [email], 1);
|
||||
} else if (needsSetup) {
|
||||
// "needs setup".
|
||||
tooltiptext = this._stringBundle.GetStringFromName("signInToSync.description");
|
||||
} else if (loginFailed) {
|
||||
// "need to reconnect/re-enter your password"
|
||||
tooltiptext = gFxAccounts.strings.formatStringFromName("reconnectDescription", [email], 1);
|
||||
} else {
|
||||
// Sync appears configured - format the "last synced at" time.
|
||||
try {
|
||||
let lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync"));
|
||||
tooltiptext = this.formatLastSyncDate(lastSync);
|
||||
} catch (e) {
|
||||
// pref doesn't exist (which will be the case until we've seen the
|
||||
// first successful sync) or is invalid (which should be impossible!)
|
||||
// Just leave tooltiptext as the empty string in these cases, which
|
||||
// will cause the tooltip to be removed below.
|
||||
}
|
||||
}
|
||||
|
||||
// We've done all our promise-y work and ready to update the UI - make
|
||||
// sure it hasn't been torn down since we started.
|
||||
if (!gBrowser)
|
||||
return;
|
||||
|
||||
let broadcaster = document.getElementById("sync-status");
|
||||
if (broadcaster) {
|
||||
if (tooltiptext) {
|
||||
broadcaster.setAttribute("tooltiptext", tooltiptext);
|
||||
} else {
|
||||
broadcaster.removeAttribute("tooltiptext");
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
getWithinLastWeekFormat() {
|
||||
return this._withinLastWeekFormat ||
|
||||
(this._withinLastWeekFormat =
|
||||
new Intl.DateTimeFormat(undefined, {weekday: "long", hour: "numeric", minute: "numeric"}));
|
||||
},
|
||||
|
||||
getOneWeekOrOlderFormat() {
|
||||
return this._oneWeekOrOlderFormat ||
|
||||
(this._oneWeekOrOlderFormat =
|
||||
new Intl.DateTimeFormat(undefined, {month: "long", day: "numeric"}));
|
||||
},
|
||||
|
||||
formatLastSyncDate(date) {
|
||||
let sixDaysAgo = (() => {
|
||||
let tempDate = new Date();
|
||||
tempDate.setDate(tempDate.getDate() - 6);
|
||||
tempDate.setHours(0, 0, 0, 0);
|
||||
return tempDate;
|
||||
})();
|
||||
|
||||
// It may be confusing for the user to see "Last Sync: Monday" when the last
|
||||
// sync was indeed a Monday, but 3 weeks ago.
|
||||
let dateFormat = date < sixDaysAgo ? this.getOneWeekOrOlderFormat() : this.getWithinLastWeekFormat();
|
||||
|
||||
let lastSyncDateString = dateFormat.format(date);
|
||||
return this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
|
||||
},
|
||||
|
||||
onClientsSynced() {
|
||||
let broadcaster = document.getElementById("sync-syncnow-state");
|
||||
if (broadcaster) {
|
||||
if (Weave.Service.clientsEngine.stats.numClients > 1) {
|
||||
broadcaster.setAttribute("devices-status", "multi");
|
||||
} else {
|
||||
broadcaster.setAttribute("devices-status", "single");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
observe: function SUI_observe(subject, topic, data) {
|
||||
this.log.debug("observed", topic);
|
||||
if (this._unloaded) {
|
||||
Cu.reportError("SyncUI observer called after unload: " + topic);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unwrap, just like Svc.Obs, but without pulling in that dependency.
|
||||
if (subject && typeof subject == "object" &&
|
||||
("wrappedJSObject" in subject) &&
|
||||
("observersModuleSubjectWrapper" in subject.wrappedJSObject)) {
|
||||
subject = subject.wrappedJSObject.object;
|
||||
}
|
||||
|
||||
// First handle "activity" only.
|
||||
switch (topic) {
|
||||
case "weave:service:sync:start":
|
||||
this.onActivityStart();
|
||||
break;
|
||||
case "weave:service:sync:finish":
|
||||
case "weave:service:sync:error":
|
||||
this.onActivityStop();
|
||||
break;
|
||||
}
|
||||
// Now non-activity state (eg, enabled, errors, etc)
|
||||
// Note that sync uses the ":ui:" notifications for errors because sync.
|
||||
switch (topic) {
|
||||
case "weave:ui:sync:finish":
|
||||
// Do nothing.
|
||||
break;
|
||||
case "weave:ui:sync:error":
|
||||
case "weave:service:setup-complete":
|
||||
case "weave:service:login:finish":
|
||||
case "weave:service:login:start":
|
||||
case "weave:service:start-over":
|
||||
this.updateUI();
|
||||
break;
|
||||
case "weave:ui:login:error":
|
||||
case "weave:service:login:error":
|
||||
this.onLoginError();
|
||||
break;
|
||||
case "weave:service:logout:finish":
|
||||
this.onLogout();
|
||||
break;
|
||||
case "weave:service:start-over:finish":
|
||||
this.updateUI();
|
||||
break;
|
||||
case "weave:service:ready":
|
||||
this.initUI();
|
||||
break;
|
||||
case "weave:notification:added":
|
||||
this.initNotifications();
|
||||
break;
|
||||
case "weave:engine:sync:finish":
|
||||
if (data != "clients") {
|
||||
return;
|
||||
}
|
||||
this.onClientsSynced();
|
||||
break;
|
||||
case "quit-application":
|
||||
// Stop the animation timer on shutdown, since we can't update the UI
|
||||
// after this.
|
||||
clearTimeout(this._syncAnimationTimer);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([
|
||||
Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference
|
||||
])
|
||||
};
|
||||
|
||||
XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() {
|
||||
// XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
|
||||
// but for now just make it work
|
||||
return Services.strings.createBundle(
|
||||
"chrome://weave/locale/sync.properties");
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(gSyncUI, "log", function() {
|
||||
return Log.repository.getLogger("browserwindow.syncui");
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(gSyncUI, "weaveService", function() {
|
||||
return Components.classes["@mozilla.org/weave/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
});
|
|
@ -1503,8 +1503,7 @@ var gBrowserInit = {
|
|||
PointerLock.init();
|
||||
|
||||
// initialize the sync UI
|
||||
gSyncUI.init();
|
||||
gFxAccounts.init();
|
||||
gSync.init();
|
||||
|
||||
if (AppConstants.MOZ_DATA_REPORTING)
|
||||
gDataNotificationInfoBar.init();
|
||||
|
@ -1651,7 +1650,7 @@ var gBrowserInit = {
|
|||
|
||||
FullScreen.uninit();
|
||||
|
||||
gFxAccounts.uninit();
|
||||
gSync.uninit();
|
||||
|
||||
gExtensionsNotifications.uninit();
|
||||
|
||||
|
@ -1814,7 +1813,7 @@ if (AppConstants.platform == "macosx") {
|
|||
gPrivateBrowsingUI.init();
|
||||
|
||||
// initialize the sync UI
|
||||
gSyncUI.init();
|
||||
gSync.init();
|
||||
|
||||
if (AppConstants.E10S_TESTING_ONLY) {
|
||||
gRemoteTabsUI.init();
|
||||
|
@ -6799,7 +6798,7 @@ function checkEmptyPageOrigin(browser = gBrowser.selectedBrowser,
|
|||
}
|
||||
|
||||
function BrowserOpenSyncTabs() {
|
||||
gSyncUI.openSyncedTabsPanel();
|
||||
gSync.openSyncedTabsPanel();
|
||||
}
|
||||
|
||||
function ReportFalseDeceptiveSite() {
|
||||
|
@ -8173,7 +8172,7 @@ var TabContextMenu = {
|
|||
this.contextTab.addEventListener("TabAttrModified", this);
|
||||
aPopupMenu.addEventListener("popuphiding", this);
|
||||
|
||||
gFxAccounts.updateTabContextMenu(aPopupMenu, this.contextTab);
|
||||
gSync.updateTabContextMenu(aPopupMenu, this.contextTab);
|
||||
},
|
||||
handleEvent(aEvent) {
|
||||
switch (aEvent.type) {
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
<menu id="context_sendTabToDevice" label="&sendTabToDevice.label;"
|
||||
accesskey="&sendTabToDevice.accesskey;" hidden="true">
|
||||
<menupopup id="context_sendTabToDevicePopupMenu"
|
||||
onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
|
||||
onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
|
||||
</menu>
|
||||
<menuseparator/>
|
||||
<menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;"
|
||||
|
@ -492,7 +492,7 @@
|
|||
<menuitem label="&syncedTabs.context.managedevices.label;"
|
||||
accesskey="&syncedTabs.context.managedevices.accesskey;"
|
||||
id="syncedTabsManageDevices"
|
||||
oncommand="gFxAccounts.openDevicesManagementPage('syncedtabs-sidebar');"/>
|
||||
oncommand="gSync.openDevicesManagementPage('syncedtabs-sidebar');"/>
|
||||
<menuitem label="&syncSyncNowItem.label;"
|
||||
accesskey="&syncSyncNowItem.accesskey;"
|
||||
id="syncedTabsRefresh"/>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<script type="application/javascript" src="chrome://browser/content/browser-safebrowsing.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-sidebar.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-syncui.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-sync.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-tabsintitlebar.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-thumbnails.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-trackingprotection.js"/>
|
||||
|
@ -38,5 +38,3 @@
|
|||
#ifdef MOZ_DATA_REPORTING
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-data-submission-info-bar.js"/>
|
||||
#endif
|
||||
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
|
||||
|
|
|
@ -121,9 +121,6 @@ with Files("browser-feeds.js"):
|
|||
with Files("browser-fullZoom.js"):
|
||||
BUG_COMPONENT = ("Firefox", "Tabbed Browsing")
|
||||
|
||||
with Files("browser-fxaccounts.js"):
|
||||
BUG_COMPONENT = ("Core", "FxAccounts")
|
||||
|
||||
with Files("browser-gestureSupport.js"):
|
||||
BUG_COMPONENT = ("Core", "Widget: Cocoa")
|
||||
|
||||
|
@ -145,7 +142,7 @@ with Files("browser-safebrowsing.js"):
|
|||
with Files("*social*"):
|
||||
BUG_COMPONENT = ("Firefox", "SocialAPI")
|
||||
|
||||
with Files("browser-syncui.js"):
|
||||
with Files("browser-sync.js"):
|
||||
BUG_COMPONENT = ("Firefox", "Sync")
|
||||
|
||||
with Files("browser-tabPreviews.xml"):
|
||||
|
|
|
@ -601,7 +601,7 @@ nsContextMenu.prototype = {
|
|||
},
|
||||
|
||||
initSyncItems() {
|
||||
gFxAccounts.initPageContextMenu(this);
|
||||
gSync.initPageContextMenu(this);
|
||||
},
|
||||
|
||||
openPasswordManager() {
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
[DEFAULT]
|
||||
support-files =
|
||||
POSTSearchEngine.xml
|
||||
accounts_testRemoteCommands.html
|
||||
alltabslistener.html
|
||||
app_bug575561.html
|
||||
app_subframe_bug575561.html
|
||||
|
@ -18,7 +17,6 @@ support-files =
|
|||
browser_bug678392-1.html
|
||||
browser_bug678392-2.html
|
||||
browser_bug970746.xhtml
|
||||
browser_fxa_web_channel.html
|
||||
browser_registerProtocolHandler_notification.html
|
||||
browser_star_hsts.sjs
|
||||
browser_tab_dragdrop2_frame1.xul
|
||||
|
@ -94,11 +92,6 @@ support-files =
|
|||
!/toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs
|
||||
|
||||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
[browser_aboutAccounts.js]
|
||||
skip-if = os == "linux" # Bug 958026
|
||||
support-files =
|
||||
content_aboutAccounts.js
|
||||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
[browser_aboutCertError.js]
|
||||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
[browser_aboutNetError.js]
|
||||
|
@ -376,11 +369,6 @@ skip-if = true # browser_drag.js is disabled, as it needs to be updated for the
|
|||
tags = fullscreen
|
||||
skip-if = os == "linux" # Linux: Intermittent failures - bug 941575.
|
||||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
[browser_fxaccounts.js]
|
||||
support-files = fxa_profile_handler.sjs
|
||||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
[browser_fxa_web_channel.js]
|
||||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
[browser_gestureSupport.js]
|
||||
skip-if = e10s # Bug 863514 - no gesture support.
|
||||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
|
@ -497,9 +485,6 @@ support-files =
|
|||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
[browser_subframe_favicons_not_used.js]
|
||||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
[browser_syncui.js]
|
||||
skip-if = os == 'linux' # Bug 1304272
|
||||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
[browser_tab_close_dependent_window.js]
|
||||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
[browser_tabDrop.js]
|
||||
|
|
|
@ -903,7 +903,7 @@ add_task(function* test_input_spell_false() {
|
|||
const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
|
||||
|
||||
add_task(function* test_plaintext_sendpagetodevice() {
|
||||
if (!gFxAccounts.sendTabToDeviceEnabled) {
|
||||
if (!gSync.sendTabToDeviceEnabled) {
|
||||
return;
|
||||
}
|
||||
yield ensureSyncReady();
|
||||
|
@ -942,7 +942,7 @@ add_task(function* test_plaintext_sendpagetodevice() {
|
|||
});
|
||||
|
||||
add_task(function* test_link_sendlinktodevice() {
|
||||
if (!gFxAccounts.sendTabToDeviceEnabled) {
|
||||
if (!gSync.sendTabToDeviceEnabled) {
|
||||
return;
|
||||
}
|
||||
yield ensureSyncReady();
|
||||
|
|
|
@ -1,258 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
var {Log} = Cu.import("resource://gre/modules/Log.jsm", {});
|
||||
var {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
var {fxAccounts} = Cu.import("resource://gre/modules/FxAccounts.jsm", {});
|
||||
var FxAccountsCommon = {};
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js", FxAccountsCommon);
|
||||
|
||||
const TEST_ROOT = "http://example.com/browser/browser/base/content/test/general/";
|
||||
|
||||
// instrument gFxAccounts to send observer notifications when it's done
|
||||
// what it does.
|
||||
(function() {
|
||||
let unstubs = {}; // The original functions we stub out.
|
||||
|
||||
// The stub functions.
|
||||
let stubs = {
|
||||
updateUI() {
|
||||
return unstubs["updateUI"].call(gFxAccounts).then(() => {
|
||||
Services.obs.notifyObservers(null, "test:browser_fxaccounts:updateUI");
|
||||
});
|
||||
},
|
||||
// Opening preferences is trickier than it should be as leaks are reported
|
||||
// due to the promises it fires off at load time and there's no clear way to
|
||||
// know when they are done.
|
||||
// So just ensure openPreferences is called rather than whether it opens.
|
||||
openPreferences() {
|
||||
Services.obs.notifyObservers(null, "test:browser_fxaccounts:openPreferences");
|
||||
}
|
||||
};
|
||||
|
||||
for (let name in stubs) {
|
||||
unstubs[name] = gFxAccounts[name];
|
||||
gFxAccounts[name] = stubs[name];
|
||||
}
|
||||
// and undo our damage at the end.
|
||||
registerCleanupFunction(() => {
|
||||
for (let name in unstubs) {
|
||||
gFxAccounts[name] = unstubs[name];
|
||||
}
|
||||
stubs = unstubs = null;
|
||||
});
|
||||
})();
|
||||
|
||||
// Other setup/cleanup
|
||||
var newTab;
|
||||
|
||||
Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri",
|
||||
TEST_ROOT + "accounts_testRemoteCommands.html");
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
|
||||
Services.prefs.clearUserPref("identity.fxaccounts.remote.profile.uri");
|
||||
gBrowser.removeTab(newTab);
|
||||
});
|
||||
|
||||
add_task(function* initialize() {
|
||||
// Set a new tab with something other than about:blank, so it doesn't get reused.
|
||||
// We must wait for it to load or the promiseTabOpen() call in the next test
|
||||
// gets confused.
|
||||
newTab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla", {animate: false});
|
||||
yield promiseTabLoaded(newTab);
|
||||
});
|
||||
|
||||
// The elements we care about.
|
||||
var panelUILabel = document.getElementById("PanelUI-fxa-label");
|
||||
var panelUIStatus = document.getElementById("PanelUI-fxa-status");
|
||||
var panelUIFooter = document.getElementById("PanelUI-footer-fxa");
|
||||
|
||||
// The tests
|
||||
add_task(function* test_nouser() {
|
||||
let user = yield fxAccounts.getSignedInUser();
|
||||
Assert.strictEqual(user, null, "start with no user signed in");
|
||||
let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI");
|
||||
Services.obs.notifyObservers(null, this.FxAccountsCommon.ONLOGOUT_NOTIFICATION);
|
||||
yield promiseUpdateDone;
|
||||
|
||||
// Check the world - the FxA footer area is visible as it is offering a signin.
|
||||
Assert.ok(isFooterVisible())
|
||||
|
||||
Assert.equal(panelUILabel.getAttribute("label"), panelUIStatus.getAttribute("defaultlabel"));
|
||||
Assert.equal(panelUIStatus.getAttribute("tooltiptext"), panelUIStatus.getAttribute("signedinTooltiptext"));
|
||||
Assert.ok(!panelUIFooter.hasAttribute("fxastatus"), "no fxsstatus when signed out");
|
||||
Assert.ok(!panelUIFooter.hasAttribute("fxaprofileimage"), "no fxaprofileimage when signed out");
|
||||
|
||||
let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
|
||||
panelUIStatus.click();
|
||||
yield promisePreferencesOpened;
|
||||
});
|
||||
|
||||
/*
|
||||
XXX - Bug 1191162 - need a better hawk mock story or this will leak in debug builds.
|
||||
|
||||
add_task(function* test_unverifiedUser() {
|
||||
let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI");
|
||||
yield setSignedInUser(false); // this will fire the observer that does the update.
|
||||
yield promiseUpdateDone;
|
||||
|
||||
// Check the world.
|
||||
Assert.ok(isFooterVisible())
|
||||
|
||||
Assert.equal(panelUILabel.getAttribute("label"), "foo@example.com");
|
||||
Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
|
||||
panelUIStatus.getAttribute("signedinTooltiptext"));
|
||||
Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
|
||||
let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
|
||||
panelUIStatus.click();
|
||||
yield promisePreferencesOpened
|
||||
yield signOut();
|
||||
});
|
||||
*/
|
||||
|
||||
add_task(function* test_verifiedUserEmptyProfile() {
|
||||
// We see 2 updateUI() calls - one for the signedInUser and one after
|
||||
// we first fetch the profile. We want them both to fire or we aren't testing
|
||||
// the state we think we are testing.
|
||||
let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI", 2);
|
||||
gFxAccounts._cachedProfile = null;
|
||||
configureProfileURL({}); // successful but empty profile.
|
||||
yield setSignedInUser(true); // this will fire the observer that does the update.
|
||||
yield promiseUpdateDone;
|
||||
|
||||
// Check the world.
|
||||
Assert.ok(isFooterVisible())
|
||||
Assert.equal(panelUILabel.getAttribute("label"), "foo@example.com");
|
||||
Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
|
||||
panelUIStatus.getAttribute("signedinTooltiptext"));
|
||||
Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
|
||||
|
||||
let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
|
||||
panelUIStatus.click();
|
||||
yield promisePreferencesOpened;
|
||||
yield signOut();
|
||||
});
|
||||
|
||||
add_task(function* test_verifiedUserDisplayName() {
|
||||
let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI", 2);
|
||||
gFxAccounts._cachedProfile = null;
|
||||
configureProfileURL({ displayName: "Test User Display Name" });
|
||||
yield setSignedInUser(true); // this will fire the observer that does the update.
|
||||
yield promiseUpdateDone;
|
||||
|
||||
Assert.ok(isFooterVisible())
|
||||
Assert.equal(panelUILabel.getAttribute("label"), "Test User Display Name");
|
||||
Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
|
||||
panelUIStatus.getAttribute("signedinTooltiptext"));
|
||||
Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
|
||||
yield signOut();
|
||||
});
|
||||
|
||||
add_task(function* test_profileNotificationsClearsCache() {
|
||||
let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI", 1);
|
||||
gFxAccounts._cachedProfile = { foo: "bar" };
|
||||
Services.obs.notifyObservers(null, this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION);
|
||||
Assert.ok(!gFxAccounts._cachedProfile);
|
||||
yield promiseUpdateDone;
|
||||
});
|
||||
|
||||
add_task(function* test_verifiedUserProfileFailure() {
|
||||
// profile failure means only one observer fires.
|
||||
let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI", 1);
|
||||
gFxAccounts._cachedProfile = null;
|
||||
configureProfileURL(null, 500);
|
||||
yield setSignedInUser(true); // this will fire the observer that does the update.
|
||||
yield promiseUpdateDone;
|
||||
|
||||
Assert.ok(isFooterVisible());
|
||||
Assert.equal(panelUILabel.getAttribute("label"), "foo@example.com");
|
||||
Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
|
||||
panelUIStatus.getAttribute("signedinTooltiptext"));
|
||||
Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
|
||||
yield signOut();
|
||||
});
|
||||
|
||||
// Helpers.
|
||||
function isFooterVisible() {
|
||||
let style = window.getComputedStyle(panelUIFooter);
|
||||
return style.getPropertyValue("display") == "flex";
|
||||
}
|
||||
|
||||
function configureProfileURL(profile, responseStatus = 200) {
|
||||
let responseBody = profile ? JSON.stringify(profile) : "";
|
||||
let url = TEST_ROOT + "fxa_profile_handler.sjs?" +
|
||||
"responseStatus=" + responseStatus +
|
||||
"responseBody=" + responseBody +
|
||||
// This is a bit cheeky - the FxA code will just append "/profile"
|
||||
// to the preference value. We arrange for this to be seen by our
|
||||
// .sjs as part of the query string.
|
||||
"&path=";
|
||||
|
||||
Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", url);
|
||||
}
|
||||
|
||||
function promiseObserver(topic, count = 1) {
|
||||
return new Promise(resolve => {
|
||||
let obs = (aSubject, aTopic, aData) => {
|
||||
if (--count == 0) {
|
||||
Services.obs.removeObserver(obs, aTopic);
|
||||
resolve(aSubject);
|
||||
}
|
||||
}
|
||||
Services.obs.addObserver(obs, topic);
|
||||
});
|
||||
}
|
||||
|
||||
var promiseTabOpen = Task.async(function*(urlBase) {
|
||||
info("Waiting for tab to open...");
|
||||
let event = yield promiseWaitForEvent(gBrowser.tabContainer, "TabOpen", true);
|
||||
let tab = event.target;
|
||||
yield promiseTabLoadEvent(tab);
|
||||
ok(tab.linkedBrowser.currentURI.spec.startsWith(urlBase),
|
||||
"Got " + tab.linkedBrowser.currentURI.spec + ", expecting " + urlBase);
|
||||
let whenUnloaded = promiseTabUnloaded(tab);
|
||||
gBrowser.removeTab(tab);
|
||||
yield whenUnloaded;
|
||||
});
|
||||
|
||||
function promiseTabUnloaded(tab) {
|
||||
return new Promise(resolve => {
|
||||
info("Wait for tab to unload");
|
||||
function handle(event) {
|
||||
tab.linkedBrowser.removeEventListener("unload", handle, true);
|
||||
info("Got unload event");
|
||||
resolve(event);
|
||||
}
|
||||
tab.linkedBrowser.addEventListener("unload", handle, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
// FxAccounts helpers.
|
||||
function setSignedInUser(verified) {
|
||||
let data = {
|
||||
email: "foo@example.com",
|
||||
uid: "1234@lcip.org",
|
||||
assertion: "foobar",
|
||||
sessionToken: "dead",
|
||||
kA: "beef",
|
||||
kB: "cafe",
|
||||
verified,
|
||||
|
||||
oauthTokens: {
|
||||
// a token for the profile server.
|
||||
profile: "key value",
|
||||
}
|
||||
}
|
||||
return fxAccounts.setSignedInUser(data);
|
||||
}
|
||||
|
||||
var signOut = Task.async(function* () {
|
||||
// This test needs to make sure that any updates for the logout have
|
||||
// completed before starting the next test, or we see the observer
|
||||
// notifications get out of sync.
|
||||
let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI");
|
||||
// we always want a "localOnly" signout here...
|
||||
yield fxAccounts.signOut(true);
|
||||
yield promiseUpdateDone;
|
||||
});
|
|
@ -1,205 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
var {Log} = Cu.import("resource://gre/modules/Log.jsm", {});
|
||||
var {Weave} = Cu.import("resource://services-sync/main.js", {});
|
||||
|
||||
var stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]
|
||||
.getService(Ci.nsIStringBundleService)
|
||||
.createBundle("chrome://weave/locale/sync.properties");
|
||||
|
||||
// ensure test output sees log messages.
|
||||
Log.repository.getLogger("browserwindow.syncui").addAppender(new Log.DumpAppender());
|
||||
|
||||
// Send the specified sync-related notification and return a promise that
|
||||
// resolves once gSyncUI._promiseUpateUI is complete and the UI is ready to check.
|
||||
function notifyAndPromiseUIUpdated(topic) {
|
||||
return new Promise(resolve => {
|
||||
// Instrument gSyncUI so we know when the update is complete.
|
||||
let oldPromiseUpdateUI = gSyncUI._promiseUpdateUI.bind(gSyncUI);
|
||||
gSyncUI._promiseUpdateUI = function() {
|
||||
return oldPromiseUpdateUI().then(() => {
|
||||
// Restore our override.
|
||||
gSyncUI._promiseUpdateUI = oldPromiseUpdateUI;
|
||||
// Resolve the promise so the caller knows the update is done.
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
// Now send the notification.
|
||||
Services.obs.notifyObservers(null, topic);
|
||||
});
|
||||
}
|
||||
|
||||
// Sync manages 3 broadcasters so the menus correctly reflect the Sync state.
|
||||
// Only one of these 3 should ever be visible - pass the ID of the broadcaster
|
||||
// you expect to be visible and it will check it's the only one that is.
|
||||
function checkBroadcasterVisible(broadcasterId) {
|
||||
let all = ["sync-reauth-state", "sync-setup-state", "sync-syncnow-state"];
|
||||
Assert.ok(all.indexOf(broadcasterId) >= 0, "valid id");
|
||||
for (let check of all) {
|
||||
let eltHidden = document.getElementById(check).hidden;
|
||||
Assert.equal(eltHidden, check == broadcasterId ? false : true, check);
|
||||
}
|
||||
}
|
||||
|
||||
function promiseObserver(topic) {
|
||||
return new Promise(resolve => {
|
||||
let obs = (aSubject, aTopic, aData) => {
|
||||
Services.obs.removeObserver(obs, aTopic);
|
||||
resolve(aSubject);
|
||||
}
|
||||
Services.obs.addObserver(obs, topic);
|
||||
});
|
||||
}
|
||||
|
||||
function checkButtonTooltips(stringPrefix) {
|
||||
for (let butId of ["PanelUI-remotetabs-syncnow", "PanelUI-fxa-icon"]) {
|
||||
let text = document.getElementById(butId).getAttribute("tooltiptext");
|
||||
let desc = `Text is "${text}", expecting it to start with "${stringPrefix}"`
|
||||
Assert.ok(text.startsWith(stringPrefix), desc);
|
||||
}
|
||||
}
|
||||
|
||||
add_task(function* prepare() {
|
||||
// add the Sync button to the toolbar so we can get it!
|
||||
CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_NAVBAR);
|
||||
registerCleanupFunction(() => {
|
||||
CustomizableUI.removeWidgetFromArea("sync-button");
|
||||
});
|
||||
|
||||
let xps = Components.classes["@mozilla.org/weave/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
yield xps.whenLoaded();
|
||||
// Put Sync and the UI into a known state.
|
||||
Weave.Status.login = Weave.LOGIN_FAILED_NO_USERNAME;
|
||||
yield notifyAndPromiseUIUpdated("weave:service:login:error");
|
||||
|
||||
checkBroadcasterVisible("sync-setup-state");
|
||||
checkButtonTooltips("Sign In To Sync");
|
||||
// mock out the "_needsSetup()" function so we don't short-circuit.
|
||||
let oldNeedsSetup = window.gSyncUI._needsSetup;
|
||||
window.gSyncUI._needsSetup = () => Promise.resolve(false);
|
||||
registerCleanupFunction(() => {
|
||||
window.gSyncUI._needsSetup = oldNeedsSetup;
|
||||
// and an observer to set the state back to what it should be now we've
|
||||
// restored the stub.
|
||||
Services.obs.notifyObservers(null, "weave:service:login:finish");
|
||||
});
|
||||
// and a notification to have the state change away from "needs setup"
|
||||
yield notifyAndPromiseUIUpdated("weave:service:login:finish");
|
||||
checkBroadcasterVisible("sync-syncnow-state");
|
||||
// open the sync-button panel so we can check elements in that.
|
||||
document.getElementById("sync-button").click();
|
||||
});
|
||||
|
||||
add_task(function* testSyncNeedsVerification() {
|
||||
// mock out the "_needsVerification()" function
|
||||
let oldNeedsVerification = window.gSyncUI._needsVerification;
|
||||
window.gSyncUI._needsVerification = () => true;
|
||||
try {
|
||||
// a notification for the state change
|
||||
yield notifyAndPromiseUIUpdated("weave:service:login:finish");
|
||||
checkButtonTooltips("Verify");
|
||||
} finally {
|
||||
window.gSyncUI._needsVerification = oldNeedsVerification;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
add_task(function* testSyncLoginError() {
|
||||
checkBroadcasterVisible("sync-syncnow-state");
|
||||
|
||||
// Pretend we are in a "login failed" error state
|
||||
Weave.Status.sync = Weave.LOGIN_FAILED;
|
||||
Weave.Status.login = Weave.LOGIN_FAILED_LOGIN_REJECTED;
|
||||
yield notifyAndPromiseUIUpdated("weave:ui:sync:error");
|
||||
|
||||
// But the menu *should* reflect the login error.
|
||||
checkBroadcasterVisible("sync-reauth-state");
|
||||
// The tooltips for the buttons should also reflect it.
|
||||
checkButtonTooltips("Reconnect");
|
||||
|
||||
// Now pretend we just had a successful login - the error notification should go away.
|
||||
Weave.Status.sync = Weave.STATUS_OK;
|
||||
Weave.Status.login = Weave.LOGIN_SUCCEEDED;
|
||||
yield notifyAndPromiseUIUpdated("weave:service:login:start");
|
||||
yield notifyAndPromiseUIUpdated("weave:service:login:finish");
|
||||
// The menus should be back to "all good"
|
||||
checkBroadcasterVisible("sync-syncnow-state");
|
||||
});
|
||||
|
||||
function checkButtonsStatus(shouldBeActive) {
|
||||
for (let eid of [
|
||||
"sync-status", // the broadcaster itself.
|
||||
"sync-button", // the main sync button which observes the broadcaster
|
||||
"PanelUI-fxa-icon", // the sync icon in the fxa footer that observes it.
|
||||
]) {
|
||||
let elt = document.getElementById(eid);
|
||||
if (shouldBeActive) {
|
||||
Assert.equal(elt.getAttribute("syncstatus"), "active", `${eid} should be active`);
|
||||
} else {
|
||||
Assert.ok(!elt.hasAttribute("syncstatus"), `${eid} should have no status attr`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* testButtonActions(startNotification, endNotification, expectActive = true) {
|
||||
checkButtonsStatus(false);
|
||||
// pretend a sync is starting.
|
||||
yield notifyAndPromiseUIUpdated(startNotification);
|
||||
checkButtonsStatus(expectActive);
|
||||
// and has stopped
|
||||
yield notifyAndPromiseUIUpdated(endNotification);
|
||||
checkButtonsStatus(false);
|
||||
}
|
||||
|
||||
function *doTestButtonActivities() {
|
||||
// logins do not "activate" the spinner/button as they may block and make
|
||||
// the UI look like Sync is never completing.
|
||||
yield testButtonActions("weave:service:login:start", "weave:service:login:finish", false);
|
||||
yield testButtonActions("weave:service:login:start", "weave:service:login:error", false);
|
||||
|
||||
// But notifications for Sync itself should activate it.
|
||||
yield testButtonActions("weave:service:sync:start", "weave:service:sync:finish");
|
||||
yield testButtonActions("weave:service:sync:start", "weave:service:sync:error");
|
||||
|
||||
// and ensure the counters correctly handle multiple in-flight syncs
|
||||
yield notifyAndPromiseUIUpdated("weave:service:sync:start");
|
||||
checkButtonsStatus(true);
|
||||
// sync stops.
|
||||
yield notifyAndPromiseUIUpdated("weave:service:sync:finish");
|
||||
// Button should not be active.
|
||||
checkButtonsStatus(false);
|
||||
}
|
||||
|
||||
add_task(function* testButtonActivitiesInNavBar() {
|
||||
// check the button's functionality while the button is in the NavBar - which
|
||||
// it already is.
|
||||
yield doTestButtonActivities();
|
||||
});
|
||||
|
||||
add_task(function* testFormatLastSyncDateNow() {
|
||||
let now = new Date();
|
||||
let nowString = gSyncUI.formatLastSyncDate(now);
|
||||
Assert.equal(nowString, "Last sync: " + now.toLocaleDateString(undefined, {weekday: "long", hour: "numeric", minute: "numeric"}));
|
||||
});
|
||||
|
||||
add_task(function* testFormatLastSyncDateMonthAgo() {
|
||||
let monthAgo = new Date();
|
||||
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
||||
let monthAgoString = gSyncUI.formatLastSyncDate(monthAgo);
|
||||
Assert.equal(monthAgoString, "Last sync: " + monthAgo.toLocaleDateString(undefined, {month: "long", day: "numeric"}));
|
||||
});
|
||||
|
||||
add_task(function* testButtonActivitiesInPanel() {
|
||||
// check the button's functionality while the button is in the panel - it's
|
||||
// currently in the NavBar - move it to the panel and open it.
|
||||
CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
|
||||
yield PanelUI.show();
|
||||
try {
|
||||
yield doTestButtonActivities();
|
||||
} finally {
|
||||
PanelUI.hide();
|
||||
}
|
||||
});
|
|
@ -17,9 +17,9 @@ add_task(function* test() {
|
|||
is(document.getElementById("context_reloadAllTabs").disabled, false, "Reload All Tabs is enabled");
|
||||
|
||||
|
||||
if (gFxAccounts.sendTabToDeviceEnabled) {
|
||||
const origIsSendableURI = gFxAccounts.isSendableURI;
|
||||
gFxAccounts.isSendableURI = () => true;
|
||||
if (gSync.sendTabToDeviceEnabled) {
|
||||
const origIsSendableURI = gSync.isSendableURI;
|
||||
gSync.isSendableURI = () => true;
|
||||
// Check the send tab to device menu item
|
||||
yield ensureSyncReady();
|
||||
const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
|
||||
|
@ -31,11 +31,11 @@ add_task(function* test() {
|
|||
is(targets[0].getAttribute("label"), "Foo", "Foo target is present");
|
||||
is(targets[1].getAttribute("label"), "Bar", "Bar target is present");
|
||||
is(targets[3].getAttribute("label"), "All Devices", "All Devices target is present");
|
||||
gFxAccounts.isSendableURI = () => false;
|
||||
gSync.isSendableURI = () => false;
|
||||
updateTabContextMenu(origTab);
|
||||
is(document.getElementById("context_sendTabToDevice").hidden, true, "Send tab to device is hidden");
|
||||
restoreRemoteClients(oldGetter);
|
||||
gFxAccounts.isSendableURI = origIsSendableURI;
|
||||
gSync.isSendableURI = origIsSendableURI;
|
||||
}
|
||||
|
||||
// Hide the original tab.
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// This is basically an echo server!
|
||||
// We just grab responseStatus and responseBody query params!
|
||||
|
||||
function reallyHandleRequest(request, response) {
|
||||
var query = "?" + request.queryString;
|
||||
|
||||
var responseStatus = 200;
|
||||
var match = /responseStatus=([^&]*)/.exec(query);
|
||||
if (match) {
|
||||
responseStatus = parseInt(match[1]);
|
||||
}
|
||||
|
||||
var responseBody = "";
|
||||
match = /responseBody=([^&]*)/.exec(query);
|
||||
if (match) {
|
||||
responseBody = decodeURIComponent(match[1]);
|
||||
}
|
||||
|
||||
response.setStatusLine("1.0", responseStatus, "OK");
|
||||
response.write(responseBody);
|
||||
}
|
||||
|
||||
function handleRequest(request, response)
|
||||
{
|
||||
try {
|
||||
reallyHandleRequest(request, response);
|
||||
} catch (e) {
|
||||
response.setStatusLine("1.0", 500, "NotOK");
|
||||
response.write("Error handling request: " + e);
|
||||
}
|
||||
}
|
|
@ -836,16 +836,16 @@ function getCertExceptionDialog(aLocation) {
|
|||
|
||||
function setupRemoteClientsFixture(fixture) {
|
||||
let oldRemoteClientsGetter =
|
||||
Object.getOwnPropertyDescriptor(gFxAccounts, "remoteClients").get;
|
||||
Object.getOwnPropertyDescriptor(gSync, "remoteClients").get;
|
||||
|
||||
Object.defineProperty(gFxAccounts, "remoteClients", {
|
||||
Object.defineProperty(gSync, "remoteClients", {
|
||||
get() { return fixture; }
|
||||
});
|
||||
return oldRemoteClientsGetter;
|
||||
}
|
||||
|
||||
function restoreRemoteClients(getter) {
|
||||
Object.defineProperty(gFxAccounts, "remoteClients", {
|
||||
Object.defineProperty(gSync, "remoteClients", {
|
||||
get: getter
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
"extends": [
|
||||
"plugin:mozilla/browser-test"
|
||||
]
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
[browser_sync.js]
|
||||
[browser_fxa_web_channel.js]
|
||||
support-files=
|
||||
browser_fxa_web_channel.html
|
||||
[browser_aboutAccounts.js]
|
||||
skip-if = os == "linux" # Bug 958026
|
||||
support-files =
|
||||
content_aboutAccounts.js
|
||||
accounts_testRemoteCommands.html
|
|
@ -17,7 +17,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
|
||||
const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
|
||||
const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/sync/";
|
||||
// Preference helpers.
|
||||
var changedPrefs = new Set();
|
||||
|
||||
|
@ -42,7 +42,7 @@ var gTests = [
|
|||
},
|
||||
*run() {
|
||||
setPref("identity.fxaccounts.remote.signup.uri",
|
||||
"https://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
|
||||
"https://example.com/browser/browser/base/content/test/sync/accounts_testRemoteCommands.html");
|
||||
let tab = yield promiseNewTabLoadEvent("about:accounts");
|
||||
let mm = tab.linkedBrowser.messageManager;
|
||||
|
|
@ -17,7 +17,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
|
|||
var {FxAccountsWebChannel} = Components.utils.import("resource://gre/modules/FxAccountsWebChannel.jsm", {});
|
||||
|
||||
const TEST_HTTP_PATH = "http://example.com";
|
||||
const TEST_BASE_URL = TEST_HTTP_PATH + "/browser/browser/base/content/test/general/browser_fxa_web_channel.html";
|
||||
const TEST_BASE_URL = TEST_HTTP_PATH + "/browser/browser/base/content/test/sync/browser_fxa_web_channel.html";
|
||||
const TEST_CHANNEL_ID = "account_updates_test";
|
||||
|
||||
var gTests = [
|
|
@ -0,0 +1,241 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(async function test_ui_state_notification_calls_updateAllUI() {
|
||||
let called = false;
|
||||
let updateAllUI = gSync.updateAllUI;
|
||||
gSync.updateAllUI = () => { called = true; };
|
||||
|
||||
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
|
||||
ok(called);
|
||||
|
||||
gSync.updateAllUI = updateAllUI;
|
||||
});
|
||||
|
||||
add_task(async function test_ui_state_signedin() {
|
||||
let state = {
|
||||
status: UIState.STATUS_SIGNED_IN,
|
||||
email: "foo@bar.com",
|
||||
displayName: "Foo Bar",
|
||||
avatarURL: "https://foo.bar",
|
||||
lastSync: new Date(),
|
||||
syncing: false
|
||||
};
|
||||
|
||||
gSync.updateAllUI(state);
|
||||
|
||||
checkFxABadge(false);
|
||||
let statusBarTooltip = gSync.panelUIStatus.getAttribute("signedinTooltiptext");
|
||||
let lastSyncTooltip = gSync.formatLastSyncDate(new Date(state.lastSync));
|
||||
checkPanelUIStatusBar({
|
||||
label: "Foo Bar",
|
||||
tooltip: statusBarTooltip,
|
||||
fxastatus: "signedin",
|
||||
avatarURL: "https://foo.bar",
|
||||
syncing: false,
|
||||
syncNowTooltip: lastSyncTooltip
|
||||
});
|
||||
checkRemoteTabsPanel("PanelUI-remotetabs-main", false);
|
||||
checkMenuBarItem("sync-syncnowitem");
|
||||
});
|
||||
|
||||
add_task(async function test_ui_state_syncing() {
|
||||
let state = {
|
||||
status: UIState.STATUS_SIGNED_IN,
|
||||
email: "foo@bar.com",
|
||||
displayName: "Foo Bar",
|
||||
avatarURL: "https://foo.bar",
|
||||
lastSync: new Date(),
|
||||
syncing: true
|
||||
};
|
||||
|
||||
gSync.updateAllUI(state);
|
||||
|
||||
checkSyncNowButton("PanelUI-fxa-icon", true);
|
||||
checkSyncNowButton("PanelUI-remotetabs-syncnow", true);
|
||||
|
||||
// Be good citizens and remove the "syncing" state.
|
||||
gSync.updateAllUI({
|
||||
status: UIState.STATUS_SIGNED_IN,
|
||||
email: "foo@bar.com",
|
||||
lastSync: new Date(),
|
||||
syncing: false
|
||||
});
|
||||
// Because we switch from syncing to non-syncing, and there's a timeout involved.
|
||||
await promiseObserver("test:browser-sync:activity-stop");
|
||||
});
|
||||
|
||||
add_task(async function test_ui_state_unconfigured() {
|
||||
let state = {
|
||||
status: UIState.STATUS_NOT_CONFIGURED
|
||||
};
|
||||
|
||||
gSync.updateAllUI(state);
|
||||
|
||||
checkFxABadge(false);
|
||||
let signedOffLabel = gSync.panelUIStatus.getAttribute("defaultlabel");
|
||||
let statusBarTooltip = gSync.panelUIStatus.getAttribute("signedinTooltiptext");
|
||||
checkPanelUIStatusBar({
|
||||
label: signedOffLabel,
|
||||
tooltip: statusBarTooltip
|
||||
});
|
||||
checkRemoteTabsPanel("PanelUI-remotetabs-setupsync");
|
||||
checkMenuBarItem("sync-setup");
|
||||
});
|
||||
|
||||
add_task(async function test_ui_state_unverified() {
|
||||
let state = {
|
||||
status: UIState.STATUS_NOT_VERIFIED,
|
||||
email: "foo@bar.com",
|
||||
lastSync: new Date(),
|
||||
syncing: false
|
||||
};
|
||||
|
||||
gSync.updateAllUI(state);
|
||||
|
||||
checkFxABadge(true);
|
||||
let expectedLabel = gSync.panelUIStatus.getAttribute("unverifiedlabel");
|
||||
let tooltipText = gSync.fxaStrings.formatStringFromName("verifyDescription", [state.email], 1);
|
||||
checkPanelUIStatusBar({
|
||||
label: expectedLabel,
|
||||
tooltip: tooltipText,
|
||||
fxastatus: "unverified",
|
||||
avatarURL: null,
|
||||
syncing: false,
|
||||
syncNowTooltip: tooltipText
|
||||
});
|
||||
checkRemoteTabsPanel("PanelUI-remotetabs-setupsync", false);
|
||||
checkMenuBarItem("sync-setup");
|
||||
});
|
||||
|
||||
add_task(async function test_ui_state_loginFailed() {
|
||||
let state = {
|
||||
status: UIState.STATUS_LOGIN_FAILED,
|
||||
email: "foo@bar.com"
|
||||
};
|
||||
|
||||
gSync.updateAllUI(state);
|
||||
|
||||
checkFxABadge(true);
|
||||
let expectedLabel = gSync.panelUIStatus.getAttribute("errorlabel");
|
||||
let tooltipText = gSync.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
|
||||
checkPanelUIStatusBar({
|
||||
label: expectedLabel,
|
||||
tooltip: tooltipText,
|
||||
fxastatus: "login-failed",
|
||||
avatarURL: null,
|
||||
syncing: false,
|
||||
syncNowTooltip: tooltipText
|
||||
});
|
||||
checkRemoteTabsPanel("PanelUI-remotetabs-reauthsync", false);
|
||||
checkMenuBarItem("sync-reauthitem");
|
||||
});
|
||||
|
||||
add_task(async function test_FormatLastSyncDateNow() {
|
||||
let now = new Date();
|
||||
let nowString = gSync.formatLastSyncDate(now);
|
||||
is(nowString, "Last sync: " + now.toLocaleDateString(undefined, {weekday: "long", hour: "numeric", minute: "numeric"}),
|
||||
"The date is correctly formatted");
|
||||
});
|
||||
|
||||
add_task(async function test_FormatLastSyncDateMonthAgo() {
|
||||
let monthAgo = new Date();
|
||||
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
||||
let monthAgoString = gSync.formatLastSyncDate(monthAgo);
|
||||
is(monthAgoString, "Last sync: " + monthAgo.toLocaleDateString(undefined, {month: "long", day: "numeric"}),
|
||||
"The date is correctly formatted");
|
||||
});
|
||||
|
||||
function checkFxABadge(shouldBeShown) {
|
||||
let isShown = false;
|
||||
for (let notification of PanelUI.notifications) {
|
||||
if (notification.id == "fxa-needs-authentication") {
|
||||
isShown = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
is(isShown, shouldBeShown, "the fxa badge has the right visibility");
|
||||
}
|
||||
|
||||
function checkPanelUIStatusBar({label, tooltip, fxastatus, avatarURL, syncing, syncNowTooltip}) {
|
||||
let labelNode = document.getElementById("PanelUI-fxa-label");
|
||||
let tooltipNode = document.getElementById("PanelUI-fxa-status");
|
||||
let statusNode = document.getElementById("PanelUI-footer-fxa");
|
||||
let avatar = document.getElementById("PanelUI-fxa-avatar");
|
||||
|
||||
is(labelNode.getAttribute("label"), label, "panelUI-fxa label has the right value");
|
||||
is(tooltipNode.getAttribute("tooltiptext"), tooltip, "panelUI-fxa tooltip has the right value");
|
||||
if (fxastatus) {
|
||||
is(statusNode.getAttribute("fxastatus"), fxastatus, "panelUI-fxa fxastatus has the right value");
|
||||
} else {
|
||||
ok(!statusNode.hasAttribute("fxastatus"), "panelUI-fxa fxastatus is unset")
|
||||
}
|
||||
if (avatarURL) {
|
||||
is(avatar.style.listStyleImage, `url("${avatarURL}")`, "panelUI-fxa avatar URL is set");
|
||||
} else {
|
||||
ok(!statusNode.style.listStyleImage, "panelUI-fxa avatar URL is unset");
|
||||
}
|
||||
|
||||
if (syncing != undefined && syncNowTooltip != undefined) {
|
||||
checkSyncNowButton("PanelUI-fxa-icon", syncing, syncNowTooltip);
|
||||
}
|
||||
}
|
||||
|
||||
function checkRemoteTabsPanel(expectedShownItemId, syncing, syncNowTooltip) {
|
||||
checkItemsVisiblities(["PanelUI-remotetabs-main",
|
||||
"PanelUI-remotetabs-setupsync",
|
||||
"PanelUI-remotetabs-reauthsync"],
|
||||
expectedShownItemId);
|
||||
|
||||
if (syncing != undefined && syncNowTooltip != undefined) {
|
||||
checkSyncNowButton("PanelUI-remotetabs-syncnow", syncing, syncNowTooltip);
|
||||
}
|
||||
}
|
||||
|
||||
function checkMenuBarItem(expectedShownItemId) {
|
||||
checkItemsVisiblities(["sync-setup", "sync-syncnowitem", "sync-reauthitem"],
|
||||
expectedShownItemId);
|
||||
}
|
||||
|
||||
function checkSyncNowButton(buttonId, syncing, tooltip = null) {
|
||||
const remoteTabsButton = document.getElementById(buttonId);
|
||||
|
||||
is(remoteTabsButton.getAttribute("syncstatus"), syncing ? "active" : "", "button active has the right value");
|
||||
if (tooltip) {
|
||||
is(remoteTabsButton.getAttribute("tooltiptext"), tooltip, "button tooltiptext is set to the right value");
|
||||
}
|
||||
|
||||
if (buttonId == "PanelUI-fxa-icon") {
|
||||
return;
|
||||
}
|
||||
|
||||
is(remoteTabsButton.hasAttribute("disabled"), syncing, "disabled has the right value");
|
||||
if (syncing) {
|
||||
is(remoteTabsButton.getAttribute("label"), gSync.syncStrings.GetStringFromName("syncing2.label"), "label is set to the right value");
|
||||
} else {
|
||||
is(remoteTabsButton.getAttribute("label"), gSync.syncStrings.GetStringFromName("syncnow.label"), "label is set to the right value");
|
||||
}
|
||||
}
|
||||
|
||||
// Only one item visible at a time.
|
||||
function checkItemsVisiblities(itemsIds, expectedShownItemId) {
|
||||
for (let id of itemsIds) {
|
||||
if (id == expectedShownItemId) {
|
||||
ok(!document.getElementById(id).hidden, "menuitem " + id + " should be visible");
|
||||
} else {
|
||||
ok(document.getElementById(id).hidden, "menuitem " + id + " should be hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function promiseObserver(topic) {
|
||||
return new Promise(resolve => {
|
||||
let obs = (aSubject, aTopic, aData) => {
|
||||
Services.obs.removeObserver(obs, aTopic);
|
||||
resolve(aSubject);
|
||||
}
|
||||
Services.obs.addObserver(obs, topic);
|
||||
});
|
||||
}
|
|
@ -24,7 +24,7 @@
|
|||
<script type="application/javascript" src="chrome://browser/content/browser.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-sync.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/web-panels.js"/>
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<script type="application/javascript" src="chrome://browser/content/browser.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser-sync.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/webext-panels.js"/>
|
||||
|
||||
|
|
|
@ -73,7 +73,6 @@ browser.jar:
|
|||
content/browser/browser-feeds.js (content/browser-feeds.js)
|
||||
content/browser/browser-fullScreenAndPointerLock.js (content/browser-fullScreenAndPointerLock.js)
|
||||
content/browser/browser-fullZoom.js (content/browser-fullZoom.js)
|
||||
content/browser/browser-fxaccounts.js (content/browser-fxaccounts.js)
|
||||
content/browser/browser-gestureSupport.js (content/browser-gestureSupport.js)
|
||||
content/browser/browser-media.js (content/browser-media.js)
|
||||
content/browser/browser-places.js (content/browser-places.js)
|
||||
|
@ -82,7 +81,7 @@ browser.jar:
|
|||
content/browser/browser-safebrowsing.js (content/browser-safebrowsing.js)
|
||||
content/browser/browser-sidebar.js (content/browser-sidebar.js)
|
||||
content/browser/browser-social.js (content/browser-social.js)
|
||||
content/browser/browser-syncui.js (content/browser-syncui.js)
|
||||
content/browser/browser-sync.js (content/browser-sync.js)
|
||||
* content/browser/browser-tabPreviews.xml (content/browser-tabPreviews.xml)
|
||||
#ifdef CAN_DRAW_IN_TITLEBAR
|
||||
content/browser/browser-tabsintitlebar.js (content/browser-tabsintitlebar.js)
|
||||
|
|
|
@ -29,6 +29,7 @@ BROWSER_CHROME_MANIFESTS += [
|
|||
'content/test/siteIdentity/browser.ini',
|
||||
'content/test/social/browser.ini',
|
||||
'content/test/static/browser.ini',
|
||||
'content/test/sync/browser.ini',
|
||||
'content/test/tabcrashed/browser.ini',
|
||||
'content/test/tabPrompts/browser.ini',
|
||||
'content/test/tabs/browser.ini',
|
||||
|
|
|
@ -472,7 +472,7 @@ const CustomizableWidgets = [
|
|||
clientItem.setAttribute("itemtype", "client");
|
||||
let window = doc.defaultView;
|
||||
clientItem.setAttribute("tooltiptext",
|
||||
window.gSyncUI.formatLastSyncDate(new Date(client.lastModified)));
|
||||
window.gSync.formatLastSyncDate(new Date(client.lastModified)));
|
||||
clientItem.textContent = client.name;
|
||||
|
||||
attachFragment.appendChild(clientItem);
|
||||
|
|
|
@ -31,19 +31,21 @@
|
|||
hidden="true"/>
|
||||
<hbox id="PanelUI-footer-fxa">
|
||||
<hbox id="PanelUI-fxa-status"
|
||||
label="&fxaSignedIn.tooltip;"
|
||||
defaultlabel="&fxaSignIn.label;"
|
||||
signedinTooltiptext="&fxaSignedIn.tooltip;"
|
||||
tooltiptext="&fxaSignedIn.tooltip;"
|
||||
errorlabel="&fxaSignInError.label;"
|
||||
unverifiedlabel="&fxaUnverified.label;"
|
||||
onclick="if (event.which == 1) gFxAccounts.onMenuPanelCommand();">
|
||||
onclick="if (event.which == 1) gSync.onMenuPanelCommand();">
|
||||
<image id="PanelUI-fxa-avatar"/>
|
||||
<toolbarbutton id="PanelUI-fxa-label"
|
||||
label="&fxaSignIn.label;"
|
||||
fxabrandname="&syncBrand.fxAccount.label;"/>
|
||||
</hbox>
|
||||
<toolbarseparator/>
|
||||
<toolbarbutton id="PanelUI-fxa-icon"
|
||||
oncommand="gSyncUI.doSync();"
|
||||
oncommand="gSync.doSync();"
|
||||
closemenu="none">
|
||||
<observes element="sync-status" attribute="syncstatus"/>
|
||||
<observes element="sync-status" attribute="tooltiptext"/>
|
||||
|
@ -125,11 +127,11 @@
|
|||
<toolbarbutton id="PanelUI-remotetabs-view-managedevices"
|
||||
class="subviewbutton"
|
||||
label="&appMenuRemoteTabs.managedevices.label;"
|
||||
oncommand="gFxAccounts.openDevicesManagementPage('syncedtabs-menupanel');"/>
|
||||
oncommand="gSync.openDevicesManagementPage('syncedtabs-menupanel');"/>
|
||||
<toolbarbutton id="PanelUI-remotetabs-syncnow"
|
||||
observes="sync-status"
|
||||
class="subviewbutton"
|
||||
oncommand="gSyncUI.doSync();"
|
||||
oncommand="gSync.doSync();"
|
||||
closemenu="none"/>
|
||||
<menuseparator id="PanelUI-remotetabs-separator"/>
|
||||
</vbox>
|
||||
|
@ -154,7 +156,7 @@
|
|||
<hbox pack="center">
|
||||
<toolbarbutton class="PanelUI-remotetabs-prefs-button"
|
||||
label="&appMenuRemoteTabs.openprefs.label;"
|
||||
oncommand="gSyncUI.openPrefs('synced-tabs');"/>
|
||||
oncommand="gSync.openPrefs('synced-tabs');"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</hbox>
|
||||
|
@ -188,7 +190,7 @@
|
|||
<label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label>
|
||||
<toolbarbutton class="PanelUI-remotetabs-prefs-button"
|
||||
label="&appMenuRemoteTabs.signin.label;"
|
||||
oncommand="gSyncUI.openPrefs('synced-tabs');"/>
|
||||
oncommand="gSync.openPrefs('synced-tabs');"/>
|
||||
</vbox>
|
||||
<!-- When Sync needs re-authentication. This uses the exact same messaging
|
||||
as "Sync is not configured" but remains a separate box so we get
|
||||
|
@ -202,7 +204,7 @@
|
|||
<label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label>
|
||||
<toolbarbutton class="PanelUI-remotetabs-prefs-button"
|
||||
label="&appMenuRemoteTabs.signin.label;"
|
||||
oncommand="gSyncUI.openPrefs('synced-tabs');"/>
|
||||
oncommand="gSync.openPrefs('synced-tabs');"/>
|
||||
</vbox>
|
||||
</hbox>
|
||||
</vbox>
|
||||
|
|
|
@ -122,7 +122,6 @@ skip-if = os == "linux"
|
|||
[browser_985815_propagate_setToolbarVisibility.js]
|
||||
[browser_987177_destroyWidget_xul.js]
|
||||
[browser_987177_xul_wrapper_updating.js]
|
||||
[browser_987185_syncButton.js]
|
||||
[browser_987492_window_api.js]
|
||||
[browser_987640_charEncoding.js]
|
||||
[browser_988072_sidebar_events.js]
|
||||
|
@ -157,3 +156,4 @@ skip-if = os == "mac"
|
|||
[browser_check_tooltips_in_navbar.js]
|
||||
[browser_editcontrols_update.js]
|
||||
subsuite = clipboard
|
||||
[browser_remote_tabs_button.js]
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
var syncService = {};
|
||||
Components.utils.import("resource://services-sync/service.js", syncService);
|
||||
|
||||
var needsSetup;
|
||||
var originalSync;
|
||||
var service = syncService.Service;
|
||||
var syncWasCalled = false;
|
||||
|
||||
add_task(function* testSyncButtonFunctionality() {
|
||||
info("Check Sync button functionality");
|
||||
storeInitialValues();
|
||||
mockFunctions();
|
||||
|
||||
// add the Sync button to the panel
|
||||
CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
|
||||
|
||||
// check the button's functionality
|
||||
yield PanelUI.show();
|
||||
info("The panel menu was opened");
|
||||
|
||||
let syncButton = document.getElementById("sync-button");
|
||||
ok(syncButton, "The Sync button was added to the Panel Menu");
|
||||
// click the button - the panel should open.
|
||||
syncButton.click();
|
||||
let syncPanel = document.getElementById("PanelUI-remotetabs");
|
||||
ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
|
||||
|
||||
// Find and click the "setup" button.
|
||||
let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
|
||||
syncNowButton.click();
|
||||
|
||||
info("The sync button was clicked");
|
||||
|
||||
yield waitForCondition(() => syncWasCalled);
|
||||
});
|
||||
|
||||
add_task(function* asyncCleanup() {
|
||||
// reset the panel UI to the default state
|
||||
yield resetCustomization();
|
||||
ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
|
||||
|
||||
if (isPanelUIOpen()) {
|
||||
let panelHidePromise = promisePanelHidden(window);
|
||||
PanelUI.hide();
|
||||
yield panelHidePromise;
|
||||
}
|
||||
|
||||
restoreValues();
|
||||
});
|
||||
|
||||
function mockFunctions() {
|
||||
// mock needsSetup
|
||||
gSyncUI._needsSetup = () => Promise.resolve(false);
|
||||
|
||||
// mock service.errorHandler.syncAndReportErrors()
|
||||
service.errorHandler.syncAndReportErrors = mocked_syncAndReportErrors;
|
||||
}
|
||||
|
||||
function mocked_syncAndReportErrors() {
|
||||
syncWasCalled = true;
|
||||
}
|
||||
|
||||
function restoreValues() {
|
||||
gSyncUI._needsSetup = needsSetup;
|
||||
service.sync = originalSync;
|
||||
}
|
||||
|
||||
function storeInitialValues() {
|
||||
needsSetup = gSyncUI._needsSetup;
|
||||
originalSync = service.sync;
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
let syncService = {};
|
||||
Components.utils.import("resource://services-sync/service.js", syncService);
|
||||
const service = syncService.Service;
|
||||
Components.utils.import("resource://services-sync/UIState.jsm");
|
||||
|
||||
let getState;
|
||||
let originalSync;
|
||||
let syncWasCalled = false;
|
||||
|
||||
// TODO: This test should probably be re-written, we don't really test much here.
|
||||
add_task(async function testSyncRemoteTabsButtonFunctionality() {
|
||||
info("Test the Sync Remote Tabs button in the PanelUI");
|
||||
storeInitialValues();
|
||||
mockFunctions();
|
||||
|
||||
// Force UI update.
|
||||
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
|
||||
|
||||
// add the sync remote tabs button to the panel
|
||||
CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
|
||||
|
||||
// check the button's functionality
|
||||
await PanelUI.show();
|
||||
info("The panel menu was opened");
|
||||
|
||||
let syncRemoteTabsBtn = document.getElementById("sync-button");
|
||||
ok(syncRemoteTabsBtn, "The sync remote tabs button was added to the Panel Menu");
|
||||
// click the button - the panel should open.
|
||||
syncRemoteTabsBtn.click();
|
||||
let remoteTabsPanel = document.getElementById("PanelUI-remotetabs");
|
||||
ok(remoteTabsPanel.getAttribute("current"), "Sync Panel is in view");
|
||||
|
||||
// Find and click the "setup" button.
|
||||
let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
|
||||
syncNowButton.click();
|
||||
info("The sync now button was clicked");
|
||||
|
||||
await waitForCondition(() => syncWasCalled);
|
||||
});
|
||||
|
||||
add_task(async function asyncCleanup() {
|
||||
// reset the panel UI to the default state
|
||||
await resetCustomization();
|
||||
ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
|
||||
|
||||
if (isPanelUIOpen()) {
|
||||
let panelHidePromise = promisePanelHidden(window);
|
||||
PanelUI.hide();
|
||||
await panelHidePromise;
|
||||
}
|
||||
|
||||
restoreValues();
|
||||
});
|
||||
|
||||
function mockFunctions() {
|
||||
// mock UIState.get()
|
||||
UIState.get = () => ({
|
||||
status: UIState.STATUS_SIGNED_IN,
|
||||
email: "user@mozilla.com"
|
||||
});
|
||||
|
||||
// mock service.errorHandler.syncAndReportErrors()
|
||||
service.errorHandler.syncAndReportErrors = mocked_syncAndReportErrors;
|
||||
}
|
||||
|
||||
function mocked_syncAndReportErrors() {
|
||||
syncWasCalled = true;
|
||||
}
|
||||
|
||||
function restoreValues() {
|
||||
UIState.get = getState;
|
||||
service.syncAndReportErrors = originalSync;
|
||||
}
|
||||
|
||||
function storeInitialValues() {
|
||||
getState = UIState.get;
|
||||
originalSync = service.syncAndReportErrors;
|
||||
}
|
|
@ -229,11 +229,11 @@ add_task(function* () {
|
|||
let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
|
||||
|
||||
let didSync = false;
|
||||
let oldDoSync = gSyncUI.doSync;
|
||||
gSyncUI.doSync = function() {
|
||||
let oldDoSync = gSync.doSync;
|
||||
gSync.doSync = function() {
|
||||
didSync = true;
|
||||
mockedInternal.hasSyncedThisSession = true;
|
||||
gSyncUI.doSync = oldDoSync;
|
||||
gSync.doSync = oldDoSync;
|
||||
}
|
||||
syncNowButton.click();
|
||||
ok(didSync, "clicking the button called the correct function");
|
||||
|
|
|
@ -129,8 +129,6 @@ var gSyncPane = {
|
|||
});
|
||||
|
||||
this.updateWeavePrefs();
|
||||
|
||||
this._initProfileImageUI();
|
||||
},
|
||||
|
||||
_toggleComputerNameControls(editMode) {
|
||||
|
@ -225,14 +223,6 @@ var gSyncPane = {
|
|||
});
|
||||
},
|
||||
|
||||
_initProfileImageUI() {
|
||||
try {
|
||||
if (Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled")) {
|
||||
document.getElementById("fxaProfileImage").hidden = false;
|
||||
}
|
||||
} catch (e) { }
|
||||
},
|
||||
|
||||
updateWeavePrefs() {
|
||||
let service = Components.classes["@mozilla.org/weave/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
|
@ -243,8 +233,6 @@ var gSyncPane = {
|
|||
fxaEmailAddress1Label.hidden = false;
|
||||
displayNameLabel.hidden = true;
|
||||
|
||||
let profileInfoEnabled = Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled", false);
|
||||
|
||||
// determine the fxa status...
|
||||
this._showLoadPage(service);
|
||||
|
||||
|
@ -300,7 +288,7 @@ var gSyncPane = {
|
|||
return null;
|
||||
}).then(data => {
|
||||
let fxaLoginStatus = document.getElementById("fxaLoginStatus");
|
||||
if (data && profileInfoEnabled) {
|
||||
if (data) {
|
||||
if (data.displayName) {
|
||||
fxaLoginStatus.setAttribute("hasName", true);
|
||||
displayNameLabel.hidden = false;
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
<vbox align="center" pack="center">
|
||||
<image id="fxaProfileImage" class="actionable"
|
||||
role="button"
|
||||
onclick="gSyncPane.openChangeProfileImage(event);" hidden="true"
|
||||
onclick="gSyncPane.openChangeProfileImage(event);"
|
||||
onkeypress="gSyncPane.openChangeProfileImage(event);"
|
||||
tooltiptext="&profilePicture.tooltip;"/>
|
||||
</vbox>
|
||||
|
|
|
@ -129,8 +129,6 @@ var gSyncPane = {
|
|||
});
|
||||
|
||||
this.updateWeavePrefs();
|
||||
|
||||
this._initProfileImageUI();
|
||||
},
|
||||
|
||||
_toggleComputerNameControls(editMode) {
|
||||
|
@ -225,14 +223,6 @@ var gSyncPane = {
|
|||
});
|
||||
},
|
||||
|
||||
_initProfileImageUI() {
|
||||
try {
|
||||
if (Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled")) {
|
||||
document.getElementById("fxaProfileImage").hidden = false;
|
||||
}
|
||||
} catch (e) { }
|
||||
},
|
||||
|
||||
updateWeavePrefs() {
|
||||
let service = Components.classes["@mozilla.org/weave/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
|
@ -243,8 +233,6 @@ var gSyncPane = {
|
|||
fxaEmailAddress1Label.hidden = false;
|
||||
displayNameLabel.hidden = true;
|
||||
|
||||
let profileInfoEnabled = Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled", false);
|
||||
|
||||
// determine the fxa status...
|
||||
this._showLoadPage(service);
|
||||
|
||||
|
@ -300,7 +288,7 @@ var gSyncPane = {
|
|||
return null;
|
||||
}).then(data => {
|
||||
let fxaLoginStatus = document.getElementById("fxaLoginStatus");
|
||||
if (data && profileInfoEnabled) {
|
||||
if (data) {
|
||||
if (data.displayName) {
|
||||
fxaLoginStatus.setAttribute("hasName", true);
|
||||
displayNameLabel.hidden = false;
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
<vbox align="center" pack="center">
|
||||
<image id="fxaProfileImage" class="actionable"
|
||||
role="button"
|
||||
onclick="gSyncPane.openChangeProfileImage(event);" hidden="true"
|
||||
onclick="gSyncPane.openChangeProfileImage(event);"
|
||||
onkeypress="gSyncPane.openChangeProfileImage(event);"
|
||||
tooltiptext="&profilePicture.tooltip;"/>
|
||||
</vbox>
|
||||
|
|
|
@ -122,7 +122,7 @@ SyncedTabsDeckComponent.prototype = {
|
|||
|
||||
getPanelStatus() {
|
||||
return this._accountStatus().then(exists => {
|
||||
if (!exists || this._getChromeWindow(this._window).gSyncUI.loginFailed()) {
|
||||
if (!exists || this._SyncedTabs.loginFailed) {
|
||||
return this.PANELS.NOT_AUTHED_INFO;
|
||||
}
|
||||
if (!this._SyncedTabs.isConfiguredToSyncTabs) {
|
||||
|
@ -166,7 +166,7 @@ SyncedTabsDeckComponent.prototype = {
|
|||
},
|
||||
|
||||
openSyncPrefs() {
|
||||
this._getChromeWindow(this._window).gSyncUI.openPrefs("tabs-sidebar");
|
||||
this._getChromeWindow(this._window).gSync.openPrefs("tabs-sidebar");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -213,7 +213,7 @@ TabListView.prototype = {
|
|||
_updateClient(item, itemNode) {
|
||||
itemNode.setAttribute("id", "item-" + item.id);
|
||||
let lastSync = new Date(item.lastModified);
|
||||
let lastSyncTitle = getChromeWindow(this._window).gSyncUI.formatLastSyncDate(lastSync);
|
||||
let lastSyncTitle = getChromeWindow(this._window).gSync.formatLastSyncDate(lastSync);
|
||||
itemNode.setAttribute("title", lastSyncTitle);
|
||||
if (item.closed) {
|
||||
itemNode.classList.add("closed");
|
||||
|
|
|
@ -140,16 +140,6 @@ add_task(function* testPanelStatus() {
|
|||
let SyncedTabsMock = {
|
||||
getTabClients() {}
|
||||
};
|
||||
let loginFailed = false;
|
||||
let chromeWindowMock = {
|
||||
gSyncUI: {
|
||||
loginFailed() {
|
||||
return loginFailed;
|
||||
}
|
||||
}
|
||||
};
|
||||
let getChromeWindowMock = sinon.stub();
|
||||
getChromeWindowMock.returns(chromeWindowMock);
|
||||
|
||||
sinon.stub(listStore, "getData");
|
||||
|
||||
|
@ -158,8 +148,7 @@ add_task(function* testPanelStatus() {
|
|||
fxAccounts,
|
||||
deckStore,
|
||||
listComponent,
|
||||
SyncedTabs: SyncedTabsMock,
|
||||
getChromeWindowMock
|
||||
SyncedTabs: SyncedTabsMock
|
||||
});
|
||||
|
||||
let isAuthed = false;
|
||||
|
@ -169,10 +158,10 @@ add_task(function* testPanelStatus() {
|
|||
|
||||
isAuthed = true;
|
||||
|
||||
loginFailed = true;
|
||||
SyncedTabsMock.loginFailed = true;
|
||||
result = yield component.getPanelStatus();
|
||||
Assert.equal(result, component.PANELS.NOT_AUTHED_INFO);
|
||||
loginFailed = false;
|
||||
SyncedTabsMock.loginFailed = false;
|
||||
|
||||
SyncedTabsMock.isConfiguredToSyncTabs = false;
|
||||
result = yield component.getPanelStatus();
|
||||
|
@ -211,12 +200,12 @@ add_task(function* testActions() {
|
|||
openUILink() {},
|
||||
};
|
||||
let chromeWindowMock = {
|
||||
gSyncUI: {
|
||||
gSync: {
|
||||
openPrefs() {}
|
||||
}
|
||||
};
|
||||
sinon.spy(windowMock, "openUILink");
|
||||
sinon.spy(chromeWindowMock.gSyncUI, "openPrefs");
|
||||
sinon.spy(chromeWindowMock.gSync, "openPrefs");
|
||||
|
||||
let getChromeWindowMock = sinon.stub();
|
||||
getChromeWindowMock.returns(chromeWindowMock);
|
||||
|
@ -236,5 +225,5 @@ add_task(function* testActions() {
|
|||
|
||||
component.openSyncPrefs();
|
||||
Assert.ok(getChromeWindowMock.calledWith(windowMock));
|
||||
Assert.ok(chromeWindowMock.gSyncUI.openPrefs.called);
|
||||
Assert.ok(chromeWindowMock.gSync.openPrefs.called);
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ function test() {
|
|||
|
||||
registerCleanupFunction(function*() {
|
||||
yield signOut();
|
||||
gFxAccounts.updateUI();
|
||||
gSync.updateAllUI(UIState.get());
|
||||
});
|
||||
|
||||
var tests = [
|
||||
|
@ -35,7 +35,7 @@ var tests = [
|
|||
yield setSignedInUser();
|
||||
let userData = yield fxAccounts.getSignedInUser();
|
||||
isnot(userData, null, "Logged in now");
|
||||
gFxAccounts.updateUI(); // Causes a leak (see bug 1332985)
|
||||
gSync.updateAllUI(UIState.get());
|
||||
yield showMenuPromise("appMenu");
|
||||
yield showHighlightPromise("accountStatus");
|
||||
let highlight = document.getElementById("UITourHighlightContainer");
|
||||
|
|
|
@ -571,12 +571,12 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton {
|
|||
}
|
||||
|
||||
#PanelUI-footer-fxa:not([fxastatus="signedin"]) > toolbarseparator,
|
||||
#PanelUI-footer-fxa:not([fxastatus="signedin"]) > #PanelUI-fxa-icon,
|
||||
#PanelUI-footer-fxa:not([fxaprofileimage]) > #PanelUI-fxa-status > #PanelUI-fxa-avatar {
|
||||
#PanelUI-footer-fxa:not([fxastatus="signedin"]) > #PanelUI-fxa-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status::after {
|
||||
#PanelUI-footer-fxa[fxastatus="login-failed"] > #PanelUI-fxa-status::after,
|
||||
#PanelUI-footer-fxa[fxastatus="unverified"] > #PanelUI-fxa-status::after {
|
||||
content: url(chrome://browser/skin/warning.svg);
|
||||
filter: drop-shadow(0 1px 0 hsla(206,50%,10%,.15));
|
||||
width: 47px;
|
||||
|
@ -725,8 +725,7 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton {
|
|||
border-inline-start-style: none;
|
||||
}
|
||||
|
||||
#PanelUI-footer-fxa[fxaprofileimage="set"] > #PanelUI-fxa-status > #PanelUI-fxa-label,
|
||||
#PanelUI-footer-fxa[fxaprofileimage="enabled"]:not([fxastatus="error"]) > #PanelUI-fxa-status > #PanelUI-fxa-label {
|
||||
#PanelUI-footer-fxa[fxastatus="signedin"] > #PanelUI-fxa-status > #PanelUI-fxa-label {
|
||||
padding-inline-start: 0px;
|
||||
}
|
||||
|
||||
|
@ -856,7 +855,7 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton {
|
|||
display: none;
|
||||
}
|
||||
|
||||
#PanelUI-fxa-icon[syncstatus="active"]:not([disabled]) {
|
||||
#PanelUI-fxa-icon[syncstatus="active"] {
|
||||
list-style-image: url(chrome://browser/skin/syncProgress-horizontalbar.png);
|
||||
}
|
||||
|
||||
|
@ -887,19 +886,10 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton {
|
|||
}
|
||||
|
||||
#PanelUI-footer-fxa[fxastatus="signedin"] > #PanelUI-fxa-status > #PanelUI-fxa-label > .toolbarbutton-icon,
|
||||
#PanelUI-footer-fxa[fxastatus="error"][fxaprofileimage="set"] > #PanelUI-fxa-status > #PanelUI-fxa-label > .toolbarbutton-icon {
|
||||
#PanelUI-footer-fxa:not([fxastatus="signedin"]) > #PanelUI-fxa-status > #PanelUI-fxa-avatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#PanelUI-footer-fxa[fxastatus="error"]:not([fxaprofileimage="set"]) > #PanelUI-fxa-status > #PanelUI-fxa-avatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#PanelUI-fxa-status[disabled],
|
||||
#PanelUI-fxa-icon[disabled] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#PanelUI-fxa-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
@ -914,7 +904,7 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton {
|
|||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
#PanelUI-footer-fxa[fxaprofileimage="enabled"] > #PanelUI-fxa-status > #PanelUI-fxa-avatar {
|
||||
#PanelUI-footer-fxa > #PanelUI-fxa-status > #PanelUI-fxa-avatar {
|
||||
list-style-image: url(chrome://browser/skin/fxa/default-avatar.svg);
|
||||
}
|
||||
|
||||
|
@ -935,16 +925,12 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton {
|
|||
}
|
||||
|
||||
#PanelUI-help[disabled],
|
||||
#PanelUI-quit[disabled],
|
||||
#PanelUI-fxa-icon[disabled],
|
||||
#PanelUI-fxa-avatar[disabled],
|
||||
#PanelUI-fxa-label[disabled] > .toolbarbutton-icon,
|
||||
#PanelUI-fxa-status::after {
|
||||
#PanelUI-quit[disabled] {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
#PanelUI-fxa-status:not([disabled]):hover,
|
||||
#PanelUI-fxa-icon:not([disabled]):hover,
|
||||
#PanelUI-fxa-status:hover,
|
||||
#PanelUI-fxa-icon:hover,
|
||||
#PanelUI-help:not([disabled]):hover,
|
||||
#PanelUI-customize:hover,
|
||||
#PanelUI-quit:not([disabled]):hover {
|
||||
|
@ -952,8 +938,8 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton {
|
|||
background-color: var(--arrowpanel-dimmed);
|
||||
}
|
||||
|
||||
#PanelUI-fxa-status:not([disabled]):hover:active,
|
||||
#PanelUI-fxa-icon:not([disabled]):hover:active,
|
||||
#PanelUI-fxa-status:hover:active,
|
||||
#PanelUI-fxa-icon:hover:active,
|
||||
#PanelUI-help:not([disabled]):hover:active,
|
||||
#PanelUI-customize:hover:active,
|
||||
#PanelUI-quit:not([disabled]):hover:active {
|
||||
|
@ -962,23 +948,26 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton {
|
|||
box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
|
||||
}
|
||||
|
||||
#PanelUI-fxa-status:not([disabled]):hover,
|
||||
#PanelUI-fxa-status:not([disabled]):hover:active,
|
||||
#PanelUI-fxa-icon:not([disabled]):hover,
|
||||
#PanelUI-fxa-icon:not([disabled]):hover:active {
|
||||
#PanelUI-fxa-status:hover,
|
||||
#PanelUI-fxa-status:hover:active,
|
||||
#PanelUI-fxa-icon:hover,
|
||||
#PanelUI-fxa-icon:hover:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#PanelUI-footer-fxa[fxastatus="error"] {
|
||||
#PanelUI-footer-fxa[fxastatus="login-failed"],
|
||||
#PanelUI-footer-fxa[fxastatus="unverified"] {
|
||||
background-color: hsl(42,94%,88%);
|
||||
border-top: 1px solid hsl(42,94%,70%);
|
||||
}
|
||||
|
||||
#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status:hover {
|
||||
#PanelUI-footer-fxa[fxastatus="login-failed"] > #PanelUI-fxa-status:hover,
|
||||
#PanelUI-footer-fxa[fxastatus="unverified"] > #PanelUI-fxa-status:hover {
|
||||
background-color: hsl(42,94%,85%);
|
||||
}
|
||||
|
||||
#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status:hover:active {
|
||||
#PanelUI-footer-fxa[fxastatus="login-failed"] > #PanelUI-fxa-status:hover:active,
|
||||
#PanelUI-footer-fxa[fxastatus="unverified"] > #PanelUI-fxa-status:hover:active {
|
||||
background-color: hsl(42,94%,82%);
|
||||
box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
|
||||
}
|
||||
|
@ -1719,7 +1708,7 @@ menuitem[checked="true"].subviewbutton > .menu-iconic-left {
|
|||
list-style-image: url(chrome://browser/skin/sync-horizontalbar@2x.png);
|
||||
}
|
||||
|
||||
#PanelUI-fxa-icon[syncstatus="active"]:not([disabled]) {
|
||||
#PanelUI-fxa-icon[syncstatus="active"] {
|
||||
list-style-image: url(chrome://browser/skin/syncProgress-horizontalbar@2x.png);
|
||||
}
|
||||
|
||||
|
|
|
@ -96,7 +96,9 @@ void
|
|||
VRDisplayPresentation::DestroyLayers()
|
||||
{
|
||||
for (VRLayerChild* layer : mLayers) {
|
||||
Unused << layer->SendDestroy();
|
||||
if (layer->IsIPCOpen()) {
|
||||
Unused << layer->SendDestroy();
|
||||
}
|
||||
}
|
||||
mLayers.Clear();
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ VRLayerChild::VRLayerChild(uint32_t aVRDisplayID, VRManagerChild* aVRManagerChil
|
|||
, mCanvasElement(nullptr)
|
||||
, mShSurfClient(nullptr)
|
||||
, mFront(nullptr)
|
||||
, mIPCOpen(true)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -74,6 +75,12 @@ VRLayerChild::SubmitFrame()
|
|||
SendSubmitFrame(mFront->GetIPDLActor());
|
||||
}
|
||||
|
||||
bool
|
||||
VRLayerChild::IsIPCOpen()
|
||||
{
|
||||
return mIPCOpen;
|
||||
}
|
||||
|
||||
void
|
||||
VRLayerChild::ClearSurfaces()
|
||||
{
|
||||
|
@ -81,5 +88,11 @@ VRLayerChild::ClearSurfaces()
|
|||
mShSurfClient = nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
VRLayerChild::ActorDestroy(ActorDestroyReason aWhy)
|
||||
{
|
||||
mIPCOpen = false;
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -35,16 +35,19 @@ public:
|
|||
VRLayerChild(uint32_t aVRDisplayID, VRManagerChild* aVRManagerChild);
|
||||
void Initialize(dom::HTMLCanvasElement* aCanvasElement);
|
||||
void SubmitFrame();
|
||||
bool IsIPCOpen();
|
||||
|
||||
protected:
|
||||
virtual ~VRLayerChild();
|
||||
void ClearSurfaces();
|
||||
virtual void ActorDestroy(ActorDestroyReason aWhy) override;
|
||||
|
||||
uint32_t mVRDisplayID;
|
||||
|
||||
RefPtr<dom::HTMLCanvasElement> mCanvasElement;
|
||||
RefPtr<layers::SharedSurfaceTextureClient> mShSurfClient;
|
||||
RefPtr<layers::TextureClient> mFront;
|
||||
bool mIPCOpen;
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
|
|
|
@ -85,7 +85,7 @@ fails-if(stylo) HTTP(..) != kerning-sanity-check-nokern.html kerning-sanity-chec
|
|||
HTTP(..) == font-features-across-space-1.html font-features-across-space-1-ref.html
|
||||
fails-if(stylo) HTTP(..) == spacelookups.html spacelookups-ref.html
|
||||
# tests whether word cache is in use by testing for ignored space kerns
|
||||
fails-if(stylo) HTTP(..) == spacelookups-wordcache.html spacelookups-wordcache-ref.html
|
||||
HTTP(..) == spacelookups-wordcache.html spacelookups-wordcache-ref.html
|
||||
# requires Japanese font with feature support, WinXP lacks one
|
||||
random-if(!winWidget&&!cocoaWidget) HTTP(..) == fwid-spaces.html fwid-spaces-ref.html
|
||||
# Arial/Times New Roman on Win7+/OSX 10.6+ have kerning pairs that include spaces
|
||||
|
|
|
@ -43,7 +43,7 @@ skip css-values-3/vh_not_refreshing_on_chrome_iframe.html
|
|||
skip css-values-3/vh-support-transform-origin.html
|
||||
skip css-values-3/vh-support-transform-translate.html
|
||||
|
||||
fails-if(stylo) css-values-3/calc-in-calc.html
|
||||
css-values-3/calc-in-calc.html
|
||||
fails-if(stylo) css-values-3/vh-support-atviewport.html
|
||||
|
||||
#### CSS Writing Modes 3 #############################################
|
||||
|
|
|
@ -218,7 +218,7 @@ fails-if(!stylo) == css-values-3/attr-length-valid.html css-values-3/reference/2
|
|||
fails-if(!stylo) == css-values-3/attr-px-invalid-cast.html css-values-3/reference/200-200-green.html
|
||||
== css-values-3/attr-px-invalid-fallback.html css-values-3/reference/200-200-green.html
|
||||
fails-if(!stylo) == css-values-3/attr-px-valid.html css-values-3/reference/200-200-green.html
|
||||
fails-if(stylo) == css-values-3/calc-in-calc.html css-values-3/reference/all-green.html
|
||||
== css-values-3/calc-in-calc.html css-values-3/reference/all-green.html
|
||||
fails == css-values-3/calc-in-media-queries-001.html css-values-3/reference/all-green.html
|
||||
fails == css-values-3/calc-in-media-queries-002.html css-values-3/reference/all-green.html
|
||||
== css-values-3/calc-invalid-range-clamping.html css-values-3/reference/200-200-green.html
|
||||
|
|
|
@ -117,6 +117,8 @@ to mochitest command.
|
|||
* test_initial_storage.html `grid` [*]
|
||||
* test_property_syntax_errors.html `grid`: actually there are issues with this [*]
|
||||
* test_value_storage.html `'grid` [*]
|
||||
* Some issue with font-feature-settings:
|
||||
* test_inherit_computation.html `value for 'font'` [8]
|
||||
* Unimplemented prefixed properties:
|
||||
* -moz-transform: need different parsing rules servo/servo#16003
|
||||
* test_value_storage.html `-moz-transform`: need different parsing rules bug 1357906 [70]
|
||||
|
@ -130,12 +132,9 @@ to mochitest command.
|
|||
* test_specified_value_serialization.html `bug-721136` [1]
|
||||
* Properties implemented but not in geckolib:
|
||||
* font-feature-settings property servo/servo#15975
|
||||
* test_compute_data_with_start_struct.html `font-feature-settings` [2]
|
||||
* test_inherit_computation.html `font-feature-settings` [8]
|
||||
* test_inherit_storage.html `font-feature-settings` [12]
|
||||
* test_initial_computation.html `font-feature-settings` [4]
|
||||
* test_initial_storage.html `font-feature-settings` [6]
|
||||
* test_value_storage.html `font-feature-settings` [112]
|
||||
* test_inherit_storage.html `font-feature-settings` [2]
|
||||
* test_initial_storage.html `font-feature-settings` [1]
|
||||
* test_value_storage.html `font-feature-settings` [118]
|
||||
* image-orientation property bug 1341758
|
||||
* test_value_storage.html `image-orientation` [40]
|
||||
* Stylesheet cloning is somehow busted bug 1348481
|
||||
|
|
|
@ -321,6 +321,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
|||
}
|
||||
}
|
||||
|
||||
private String mName;
|
||||
private volatile ICodecCallbacks mCallbacks;
|
||||
private AsyncCodec mCodec;
|
||||
private InputProcessor mInputProcessor;
|
||||
|
@ -362,16 +363,9 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
|||
|
||||
if (DEBUG) { Log.d(LOGTAG, "configure " + this); }
|
||||
|
||||
MediaFormat fmt = format.asFormat();
|
||||
String codecName = getCodecForFormat(fmt, flags == MediaCodec.CONFIGURE_FLAG_ENCODE ? true : false);
|
||||
if (codecName == null) {
|
||||
Log.e(LOGTAG, "FAIL: cannot find codec");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
AsyncCodec codec = AsyncCodecFactory.create(codecName);
|
||||
|
||||
MediaFormat fmt = format.asFormat();
|
||||
AsyncCodec codec = createCodecForFormat(fmt, flags == MediaCodec.CONFIGURE_FLAG_ENCODE ? true : false);
|
||||
MediaCrypto crypto = RemoteMediaDrmBridgeStub.getMediaCrypto(drmStubId);
|
||||
if (DEBUG) {
|
||||
boolean hasCrypto = crypto != null;
|
||||
|
@ -397,12 +391,11 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
|||
mCodec = codec;
|
||||
mInputProcessor = new InputProcessor();
|
||||
mOutputProcessor = new OutputProcessor(renderToSurface);
|
||||
mSamplePool = new SamplePool(codecName, renderToSurface);
|
||||
mSamplePool = new SamplePool(mName, renderToSurface);
|
||||
if (DEBUG) { Log.d(LOGTAG, codec.toString() + " created. Render to surface?" + renderToSurface); }
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "FAIL: cannot create codec -- " + codecName);
|
||||
e.printStackTrace();
|
||||
Log.e(LOGTAG, "codec creation error", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -425,28 +418,34 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
|||
mCodec = null;
|
||||
}
|
||||
|
||||
private String getCodecForFormat(MediaFormat format, boolean isEncoder) {
|
||||
private AsyncCodec createCodecForFormat(MediaFormat format, boolean isEncoder) throws IOException {
|
||||
String mime = format.getString(MediaFormat.KEY_MIME);
|
||||
if (mime == null) {
|
||||
return null;
|
||||
if (mime == null || mime.isEmpty()) {
|
||||
throw new IllegalArgumentException("invalid MIME type: " + mime);
|
||||
}
|
||||
|
||||
int numCodecs = MediaCodecList.getCodecCount();
|
||||
for (int i = 0; i < numCodecs; i++) {
|
||||
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
|
||||
if (info.isEncoder() == !isEncoder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] types = info.getSupportedTypes();
|
||||
for (String t : types) {
|
||||
if (t.equalsIgnoreCase(mime)) {
|
||||
return info.getName();
|
||||
if (!t.equalsIgnoreCase(mime)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
AsyncCodec codec = AsyncCodecFactory.create(info.getName());
|
||||
mName = info.getName();
|
||||
return codec;
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(LOGTAG, "unable to create " + info.getName() + ". Try next codec.");
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
// TODO: API 21+ is simpler.
|
||||
//static MediaCodecList sCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
|
||||
//return sCodecList.findDecoderForFormat(format);
|
||||
throw new RuntimeException("cannot create codec for " + mime);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -14,7 +14,6 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
|||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
|
||||
Cu.importGlobalProperties(["fetch"]);
|
||||
|
||||
|
@ -119,56 +118,53 @@ class BlocklistClient {
|
|||
* in order to leverage the updateJSONBlocklist() below, which writes a new
|
||||
* dump each time the collection changes.
|
||||
*/
|
||||
loadDumpFile() {
|
||||
async loadDumpFile() {
|
||||
// Replace OS specific path separator by / for URI.
|
||||
const { components: folderFile } = OS.Path.split(this.filename);
|
||||
const fileURI = `resource://app/defaults/${folderFile.join("/")}`;
|
||||
return Task.spawn(function* loadFile() {
|
||||
const response = yield fetch(fileURI);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not read from '${fileURI}'`);
|
||||
}
|
||||
// Will be rejected if JSON is invalid.
|
||||
return yield response.json();
|
||||
});
|
||||
const response = await fetch(fileURI);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not read from '${fileURI}'`);
|
||||
}
|
||||
// Will be rejected if JSON is invalid.
|
||||
return response.json();
|
||||
}
|
||||
|
||||
validateCollectionSignature(remote, payload, collection, options = {}) {
|
||||
async validateCollectionSignature(remote, payload, collection, options = {}) {
|
||||
const {ignoreLocal} = options;
|
||||
|
||||
return Task.spawn((function* () {
|
||||
// this is a content-signature field from an autograph response.
|
||||
const {x5u, signature} = yield fetchCollectionMetadata(remote, collection);
|
||||
const certChain = yield fetch(x5u).then((res) => res.text());
|
||||
// this is a content-signature field from an autograph response.
|
||||
const {x5u, signature} = await fetchCollectionMetadata(remote, collection);
|
||||
const certChainResponse = await fetch(x5u)
|
||||
const certChain = await certChainResponse.text();
|
||||
|
||||
const verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
|
||||
.createInstance(Ci.nsIContentSignatureVerifier);
|
||||
const verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
|
||||
.createInstance(Ci.nsIContentSignatureVerifier);
|
||||
|
||||
let toSerialize;
|
||||
if (ignoreLocal) {
|
||||
toSerialize = {
|
||||
last_modified: `${payload.last_modified}`,
|
||||
data: payload.data
|
||||
};
|
||||
} else {
|
||||
const {data: localRecords} = yield collection.list();
|
||||
const records = mergeChanges(collection, localRecords, payload.changes);
|
||||
toSerialize = {
|
||||
last_modified: `${payload.lastModified}`,
|
||||
data: records
|
||||
};
|
||||
}
|
||||
let toSerialize;
|
||||
if (ignoreLocal) {
|
||||
toSerialize = {
|
||||
last_modified: `${payload.last_modified}`,
|
||||
data: payload.data
|
||||
};
|
||||
} else {
|
||||
const {data: localRecords} = await collection.list();
|
||||
const records = mergeChanges(collection, localRecords, payload.changes);
|
||||
toSerialize = {
|
||||
last_modified: `${payload.lastModified}`,
|
||||
data: records
|
||||
};
|
||||
}
|
||||
|
||||
const serialized = CanonicalJSON.stringify(toSerialize);
|
||||
const serialized = CanonicalJSON.stringify(toSerialize);
|
||||
|
||||
if (verifier.verifyContentSignature(serialized, "p384ecdsa=" + signature,
|
||||
certChain,
|
||||
this.signerName)) {
|
||||
// In case the hash is valid, apply the changes locally.
|
||||
return payload;
|
||||
}
|
||||
throw new Error(INVALID_SIGNATURE);
|
||||
}).bind(this));
|
||||
if (verifier.verifyContentSignature(serialized, "p384ecdsa=" + signature,
|
||||
certChain,
|
||||
this.signerName)) {
|
||||
// In case the hash is valid, apply the changes locally.
|
||||
return payload;
|
||||
}
|
||||
throw new Error(INVALID_SIGNATURE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -181,7 +177,7 @@ class BlocklistClient {
|
|||
* @param {bool} options.loadDump load initial dump from disk on first sync (default: true)
|
||||
* @return {Promise} which rejects on sync or process failure.
|
||||
*/
|
||||
maybeSync(lastModified, serverTime, options = {loadDump: true}) {
|
||||
async maybeSync(lastModified, serverTime, options = {loadDump: true}) {
|
||||
const {loadDump} = options;
|
||||
const remote = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
|
||||
const enforceCollectionSigning =
|
||||
|
@ -198,78 +194,76 @@ class BlocklistClient {
|
|||
}
|
||||
}
|
||||
|
||||
return Task.spawn((function* syncCollection() {
|
||||
let sqliteHandle;
|
||||
try {
|
||||
// Synchronize remote data into a local Sqlite DB.
|
||||
sqliteHandle = yield FirefoxAdapter.openConnection({path: KINTO_STORAGE_PATH});
|
||||
const options = {
|
||||
hooks,
|
||||
adapterOptions: {sqliteHandle},
|
||||
};
|
||||
const collection = this._kinto.collection(this.collectionName, options);
|
||||
let sqliteHandle;
|
||||
try {
|
||||
// Synchronize remote data into a local Sqlite DB.
|
||||
sqliteHandle = await FirefoxAdapter.openConnection({path: KINTO_STORAGE_PATH});
|
||||
const options = {
|
||||
hooks,
|
||||
adapterOptions: {sqliteHandle},
|
||||
};
|
||||
const collection = this._kinto.collection(this.collectionName, options);
|
||||
|
||||
let collectionLastModified = yield collection.db.getLastModified();
|
||||
let collectionLastModified = await collection.db.getLastModified();
|
||||
|
||||
// If there is no data currently in the collection, attempt to import
|
||||
// initial data from the application defaults.
|
||||
// This allows to avoid synchronizing the whole collection content on
|
||||
// cold start.
|
||||
if (!collectionLastModified && loadDump) {
|
||||
try {
|
||||
const initialData = yield this.loadDumpFile();
|
||||
yield collection.db.loadDump(initialData.data);
|
||||
collectionLastModified = yield collection.db.getLastModified();
|
||||
} catch (e) {
|
||||
// Report but go-on.
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// If the data is up to date, there's no need to sync. We still need
|
||||
// to record the fact that a check happened.
|
||||
if (lastModified <= collectionLastModified) {
|
||||
this.updateLastCheck(serverTime);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch changes from server.
|
||||
// If there is no data currently in the collection, attempt to import
|
||||
// initial data from the application defaults.
|
||||
// This allows to avoid synchronizing the whole collection content on
|
||||
// cold start.
|
||||
if (!collectionLastModified && loadDump) {
|
||||
try {
|
||||
const {ok} = yield collection.sync({remote});
|
||||
if (!ok) {
|
||||
throw new Error("Sync failed");
|
||||
}
|
||||
const initialData = await this.loadDumpFile();
|
||||
await collection.db.loadDump(initialData.data);
|
||||
collectionLastModified = await collection.db.getLastModified();
|
||||
} catch (e) {
|
||||
if (e.message == INVALID_SIGNATURE) {
|
||||
// if sync fails with a signature error, it's likely that our
|
||||
// local data has been modified in some way.
|
||||
// We will attempt to fix this by retrieving the whole
|
||||
// remote collection.
|
||||
const payload = yield fetchRemoteCollection(remote, collection);
|
||||
yield this.validateCollectionSignature(remote, payload, collection, {ignoreLocal: true});
|
||||
// if the signature is good (we haven't thrown), and the remote
|
||||
// last_modified is newer than the local last_modified, replace the
|
||||
// local data
|
||||
const localLastModified = yield collection.db.getLastModified();
|
||||
if (payload.last_modified >= localLastModified) {
|
||||
yield collection.clear();
|
||||
yield collection.loadDump(payload.data);
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
// Report but go-on.
|
||||
Cu.reportError(e);
|
||||
}
|
||||
// Read local collection of records.
|
||||
const {data} = yield collection.list();
|
||||
|
||||
yield this.processCallback(data);
|
||||
|
||||
// Track last update.
|
||||
this.updateLastCheck(serverTime);
|
||||
} finally {
|
||||
yield sqliteHandle.close();
|
||||
}
|
||||
}).bind(this));
|
||||
|
||||
// If the data is up to date, there's no need to sync. We still need
|
||||
// to record the fact that a check happened.
|
||||
if (lastModified <= collectionLastModified) {
|
||||
this.updateLastCheck(serverTime);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch changes from server.
|
||||
try {
|
||||
const {ok} = await collection.sync({remote});
|
||||
if (!ok) {
|
||||
throw new Error("Sync failed");
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message == INVALID_SIGNATURE) {
|
||||
// if sync fails with a signature error, it's likely that our
|
||||
// local data has been modified in some way.
|
||||
// We will attempt to fix this by retrieving the whole
|
||||
// remote collection.
|
||||
const payload = await fetchRemoteCollection(remote, collection);
|
||||
await this.validateCollectionSignature(remote, payload, collection, {ignoreLocal: true});
|
||||
// if the signature is good (we haven't thrown), and the remote
|
||||
// last_modified is newer than the local last_modified, replace the
|
||||
// local data
|
||||
const localLastModified = await collection.db.getLastModified();
|
||||
if (payload.last_modified >= localLastModified) {
|
||||
await collection.clear();
|
||||
await collection.loadDump(payload.data);
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
// Read local collection of records.
|
||||
const {data} = await collection.list();
|
||||
|
||||
await this.processCallback(data);
|
||||
|
||||
// Track last update.
|
||||
this.updateLastCheck(serverTime);
|
||||
} finally {
|
||||
await sqliteHandle.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -288,7 +282,7 @@ class BlocklistClient {
|
|||
*
|
||||
* @param {Object} records current records in the local db.
|
||||
*/
|
||||
function* updateCertBlocklist(records) {
|
||||
async function updateCertBlocklist(records) {
|
||||
const certList = Cc["@mozilla.org/security/certblocklist;1"]
|
||||
.getService(Ci.nsICertBlocklist);
|
||||
for (let item of records) {
|
||||
|
@ -316,7 +310,7 @@ function* updateCertBlocklist(records) {
|
|||
*
|
||||
* @param {Object} records current records in the local db.
|
||||
*/
|
||||
function* updatePinningList(records) {
|
||||
async function updatePinningList(records) {
|
||||
if (!Services.prefs.getBoolPref(PREF_BLOCKLIST_PINNING_ENABLED)) {
|
||||
return;
|
||||
}
|
||||
|
@ -361,16 +355,16 @@ function* updatePinningList(records) {
|
|||
* @param {String} filename path relative to profile dir.
|
||||
* @param {Object} records current records in the local db.
|
||||
*/
|
||||
function* updateJSONBlocklist(filename, records) {
|
||||
async function updateJSONBlocklist(filename, records) {
|
||||
// Write JSON dump for synchronous load at startup.
|
||||
const path = OS.Path.join(OS.Constants.Path.profileDir, filename);
|
||||
const blocklistFolder = OS.Path.dirname(path);
|
||||
|
||||
yield OS.File.makeDir(blocklistFolder, {from: OS.Constants.Path.profileDir});
|
||||
await OS.File.makeDir(blocklistFolder, {from: OS.Constants.Path.profileDir});
|
||||
|
||||
const serialized = JSON.stringify({data: records}, null, 2);
|
||||
try {
|
||||
yield OS.File.writeAtomic(path, serialized, {tmpPath: path + ".tmp"});
|
||||
await OS.File.writeAtomic(path, serialized, {tmpPath: path + ".tmp"});
|
||||
// Notify change to `nsBlocklistService`
|
||||
const eventData = {filename};
|
||||
Services.cpmm.sendAsyncMessage("Blocklist:reload-from-disk", eventData);
|
||||
|
|
|
@ -7,7 +7,6 @@ this.EXPORTED_SYMBOLS = ["checkVersions", "addTestBlocklistClient"];
|
|||
const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.importGlobalProperties(["fetch"]);
|
||||
const BlocklistClients = Cu.import("resource://services-common/blocklist-clients.js", {});
|
||||
|
||||
|
@ -33,98 +32,95 @@ this.addTestBlocklistClient = (name, client) => { gBlocklistClients[name] = clie
|
|||
|
||||
// This is called by the ping mechanism.
|
||||
// returns a promise that rejects if something goes wrong
|
||||
this.checkVersions = function() {
|
||||
return Task.spawn(function* syncClients() {
|
||||
|
||||
// Check if the server backoff time is elapsed.
|
||||
if (Services.prefs.prefHasUserValue(PREF_SETTINGS_SERVER_BACKOFF)) {
|
||||
const backoffReleaseTime = Services.prefs.getCharPref(PREF_SETTINGS_SERVER_BACKOFF);
|
||||
const remainingMilliseconds = parseInt(backoffReleaseTime, 10) - Date.now();
|
||||
if (remainingMilliseconds > 0) {
|
||||
throw new Error(`Server is asking clients to back off; retry in ${Math.ceil(remainingMilliseconds / 1000)}s.`);
|
||||
} else {
|
||||
Services.prefs.clearUserPref(PREF_SETTINGS_SERVER_BACKOFF);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch a versionInfo object that looks like:
|
||||
// {"data":[
|
||||
// {
|
||||
// "host":"kinto-ota.dev.mozaws.net",
|
||||
// "last_modified":1450717104423,
|
||||
// "bucket":"blocklists",
|
||||
// "collection":"certificates"
|
||||
// }]}
|
||||
// Right now, we only use the collection name and the last modified info
|
||||
const kintoBase = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
|
||||
const changesEndpoint = kintoBase + Services.prefs.getCharPref(PREF_BLOCKLIST_CHANGES_PATH);
|
||||
|
||||
// Use ETag to obtain a `304 Not modified` when no change occurred.
|
||||
const headers = {};
|
||||
if (Services.prefs.prefHasUserValue(PREF_BLOCKLIST_LAST_ETAG)) {
|
||||
const lastEtag = Services.prefs.getCharPref(PREF_BLOCKLIST_LAST_ETAG);
|
||||
if (lastEtag) {
|
||||
headers["If-None-Match"] = lastEtag;
|
||||
}
|
||||
}
|
||||
|
||||
const response = yield fetch(changesEndpoint, {headers});
|
||||
|
||||
// Check if the server asked the clients to back off.
|
||||
if (response.headers.has("Backoff")) {
|
||||
const backoffSeconds = parseInt(response.headers.get("Backoff"), 10);
|
||||
if (!isNaN(backoffSeconds)) {
|
||||
const backoffReleaseTime = Date.now() + backoffSeconds * 1000;
|
||||
Services.prefs.setCharPref(PREF_SETTINGS_SERVER_BACKOFF, backoffReleaseTime);
|
||||
}
|
||||
}
|
||||
|
||||
let versionInfo;
|
||||
// No changes since last time. Go on with empty list of changes.
|
||||
if (response.status == 304) {
|
||||
versionInfo = {data: []};
|
||||
this.checkVersions = async function() {
|
||||
// Check if the server backoff time is elapsed.
|
||||
if (Services.prefs.prefHasUserValue(PREF_SETTINGS_SERVER_BACKOFF)) {
|
||||
const backoffReleaseTime = Services.prefs.getCharPref(PREF_SETTINGS_SERVER_BACKOFF);
|
||||
const remainingMilliseconds = parseInt(backoffReleaseTime, 10) - Date.now();
|
||||
if (remainingMilliseconds > 0) {
|
||||
throw new Error(`Server is asking clients to back off; retry in ${Math.ceil(remainingMilliseconds / 1000)}s.`);
|
||||
} else {
|
||||
versionInfo = yield response.json();
|
||||
Services.prefs.clearUserPref(PREF_SETTINGS_SERVER_BACKOFF);
|
||||
}
|
||||
}
|
||||
|
||||
// If the server is failing, the JSON response might not contain the
|
||||
// expected data (e.g. error response - Bug 1259145)
|
||||
if (!versionInfo.hasOwnProperty("data")) {
|
||||
throw new Error("Polling for changes failed.");
|
||||
// Fetch a versionInfo object that looks like:
|
||||
// {"data":[
|
||||
// {
|
||||
// "host":"kinto-ota.dev.mozaws.net",
|
||||
// "last_modified":1450717104423,
|
||||
// "bucket":"blocklists",
|
||||
// "collection":"certificates"
|
||||
// }]}
|
||||
// Right now, we only use the collection name and the last modified info
|
||||
const kintoBase = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
|
||||
const changesEndpoint = kintoBase + Services.prefs.getCharPref(PREF_BLOCKLIST_CHANGES_PATH);
|
||||
|
||||
// Use ETag to obtain a `304 Not modified` when no change occurred.
|
||||
const headers = {};
|
||||
if (Services.prefs.prefHasUserValue(PREF_BLOCKLIST_LAST_ETAG)) {
|
||||
const lastEtag = Services.prefs.getCharPref(PREF_BLOCKLIST_LAST_ETAG);
|
||||
if (lastEtag) {
|
||||
headers["If-None-Match"] = lastEtag;
|
||||
}
|
||||
}
|
||||
|
||||
// Record new update time and the difference between local and server time
|
||||
const serverTimeMillis = Date.parse(response.headers.get("Date"));
|
||||
const response = await fetch(changesEndpoint, {headers});
|
||||
|
||||
// negative clockDifference means local time is behind server time
|
||||
// by the absolute of that value in seconds (positive means it's ahead)
|
||||
const clockDifference = Math.floor((Date.now() - serverTimeMillis) / 1000);
|
||||
Services.prefs.setIntPref(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS, clockDifference);
|
||||
Services.prefs.setIntPref(PREF_BLOCKLIST_LAST_UPDATE, serverTimeMillis / 1000);
|
||||
// Check if the server asked the clients to back off.
|
||||
if (response.headers.has("Backoff")) {
|
||||
const backoffSeconds = parseInt(response.headers.get("Backoff"), 10);
|
||||
if (!isNaN(backoffSeconds)) {
|
||||
const backoffReleaseTime = Date.now() + backoffSeconds * 1000;
|
||||
Services.prefs.setCharPref(PREF_SETTINGS_SERVER_BACKOFF, backoffReleaseTime);
|
||||
}
|
||||
}
|
||||
|
||||
let firstError;
|
||||
for (let collectionInfo of versionInfo.data) {
|
||||
const {bucket, collection, last_modified: lastModified} = collectionInfo;
|
||||
const client = gBlocklistClients[collection];
|
||||
if (client && client.bucketName == bucket) {
|
||||
try {
|
||||
yield client.maybeSync(lastModified, serverTimeMillis);
|
||||
} catch (e) {
|
||||
if (!firstError) {
|
||||
firstError = e;
|
||||
}
|
||||
let versionInfo;
|
||||
// No changes since last time. Go on with empty list of changes.
|
||||
if (response.status == 304) {
|
||||
versionInfo = {data: []};
|
||||
} else {
|
||||
versionInfo = await response.json();
|
||||
}
|
||||
|
||||
// If the server is failing, the JSON response might not contain the
|
||||
// expected data (e.g. error response - Bug 1259145)
|
||||
if (!versionInfo.hasOwnProperty("data")) {
|
||||
throw new Error("Polling for changes failed.");
|
||||
}
|
||||
|
||||
// Record new update time and the difference between local and server time
|
||||
const serverTimeMillis = Date.parse(response.headers.get("Date"));
|
||||
|
||||
// negative clockDifference means local time is behind server time
|
||||
// by the absolute of that value in seconds (positive means it's ahead)
|
||||
const clockDifference = Math.floor((Date.now() - serverTimeMillis) / 1000);
|
||||
Services.prefs.setIntPref(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS, clockDifference);
|
||||
Services.prefs.setIntPref(PREF_BLOCKLIST_LAST_UPDATE, serverTimeMillis / 1000);
|
||||
|
||||
let firstError;
|
||||
for (let collectionInfo of versionInfo.data) {
|
||||
const {bucket, collection, last_modified: lastModified} = collectionInfo;
|
||||
const client = gBlocklistClients[collection];
|
||||
if (client && client.bucketName == bucket) {
|
||||
try {
|
||||
await client.maybeSync(lastModified, serverTimeMillis);
|
||||
} catch (e) {
|
||||
if (!firstError) {
|
||||
firstError = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (firstError) {
|
||||
// cause the promise to reject by throwing the first observed error
|
||||
throw firstError;
|
||||
}
|
||||
}
|
||||
if (firstError) {
|
||||
// cause the promise to reject by throwing the first observed error
|
||||
throw firstError;
|
||||
}
|
||||
|
||||
// Save current Etag for next poll.
|
||||
if (response.headers.has("ETag")) {
|
||||
const currentEtag = response.headers.get("ETag");
|
||||
Services.prefs.setCharPref(PREF_BLOCKLIST_LAST_ETAG, currentEtag);
|
||||
}
|
||||
});
|
||||
// Save current Etag for next poll.
|
||||
if (response.headers.has("ETag")) {
|
||||
const currentEtag = response.headers.get("ETag");
|
||||
Services.prefs.setCharPref(PREF_BLOCKLIST_LAST_ETAG, currentEtag);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -31,7 +31,7 @@ var {interfaces: Ci, utils: Cu} = Components;
|
|||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://services-common/hawkrequest.js");
|
||||
Cu.import("resource://services-common/observers.js");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/PromiseUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
@ -207,7 +207,7 @@ this.HawkClient.prototype = {
|
|||
retryOK = true) {
|
||||
method = method.toLowerCase();
|
||||
|
||||
let deferred = Promise.defer();
|
||||
let deferred = PromiseUtils.defer();
|
||||
let uri = this.host + path;
|
||||
let self = this;
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
*/
|
||||
const { utils: Cu } = Components;
|
||||
const { Sqlite } = Cu.import("resource://gre/modules/Sqlite.jsm", {});
|
||||
const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js", {});
|
||||
|
||||
const SQLITE_PATH = "kinto.sqlite";
|
||||
|
@ -212,24 +211,22 @@ class FirefoxAdapter extends Kinto.adapters.BaseAdapter {
|
|||
*
|
||||
* This will be called automatically by open().
|
||||
*/
|
||||
static _init(connection) {
|
||||
return Task.spawn(function* () {
|
||||
yield connection.executeTransaction(function* doSetup() {
|
||||
const schema = yield connection.getSchemaVersion();
|
||||
static async _init(connection) {
|
||||
await connection.executeTransaction(function* doSetup() {
|
||||
const schema = yield connection.getSchemaVersion();
|
||||
|
||||
if (schema == 0) {
|
||||
if (schema == 0) {
|
||||
|
||||
for (let statementName of createStatements) {
|
||||
yield connection.execute(statements[statementName]);
|
||||
}
|
||||
|
||||
yield connection.setSchemaVersion(currentSchemaVersion);
|
||||
} else if (schema != 1) {
|
||||
throw new Error("Unknown database schema: " + schema);
|
||||
for (let statementName of createStatements) {
|
||||
yield connection.execute(statements[statementName]);
|
||||
}
|
||||
});
|
||||
return connection;
|
||||
|
||||
yield connection.setSchemaVersion(currentSchemaVersion);
|
||||
} else if (schema != 1) {
|
||||
throw new Error("Unknown database schema: " + schema);
|
||||
}
|
||||
});
|
||||
return connection;
|
||||
}
|
||||
|
||||
_executeStatement(statement, params) {
|
||||
|
@ -327,39 +324,37 @@ class FirefoxAdapter extends Kinto.adapters.BaseAdapter {
|
|||
* @param {Array} records.
|
||||
* @return {Array} imported records.
|
||||
*/
|
||||
loadDump(records) {
|
||||
async loadDump(records) {
|
||||
const connection = this._connection;
|
||||
const collection_name = this.collection;
|
||||
return Task.spawn(function* () {
|
||||
yield connection.executeTransaction(function* doImport() {
|
||||
for (let record of records) {
|
||||
const params = {
|
||||
collection_name,
|
||||
record_id: record.id,
|
||||
record: JSON.stringify(record),
|
||||
};
|
||||
yield connection.execute(statements.importData, params);
|
||||
}
|
||||
const lastModified = Math.max(...records.map(record => record.last_modified));
|
||||
await connection.executeTransaction(function* doImport() {
|
||||
for (let record of records) {
|
||||
const params = {
|
||||
collection_name,
|
||||
record_id: record.id,
|
||||
record: JSON.stringify(record),
|
||||
};
|
||||
const previousLastModified = yield connection.execute(
|
||||
statements.getLastModified, params).then(result => {
|
||||
return result.length > 0
|
||||
? result[0].getResultByName("last_modified")
|
||||
: -1;
|
||||
});
|
||||
if (lastModified > previousLastModified) {
|
||||
const params = {
|
||||
collection_name,
|
||||
last_modified: lastModified,
|
||||
};
|
||||
yield connection.execute(statements.saveLastModified, params);
|
||||
}
|
||||
});
|
||||
return records;
|
||||
yield connection.execute(statements.importData, params);
|
||||
}
|
||||
const lastModified = Math.max(...records.map(record => record.last_modified));
|
||||
const params = {
|
||||
collection_name,
|
||||
};
|
||||
const previousLastModified = yield connection.execute(
|
||||
statements.getLastModified, params).then(result => {
|
||||
return result.length > 0
|
||||
? result[0].getResultByName("last_modified")
|
||||
: -1;
|
||||
});
|
||||
if (lastModified > previousLastModified) {
|
||||
const params = {
|
||||
collection_name,
|
||||
last_modified: lastModified,
|
||||
};
|
||||
yield connection.execute(statements.saveLastModified, params);
|
||||
}
|
||||
});
|
||||
return records;
|
||||
}
|
||||
|
||||
saveLastModified(lastModified) {
|
||||
|
|
|
@ -18,7 +18,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
|
|||
"resource://services-common/utils.js");
|
||||
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"LogManager",
|
||||
|
@ -71,7 +70,7 @@ FlushableStorageAppender.prototype = {
|
|||
|
||||
// Flush the current stream to a file. Somewhat counter-intuitively, you
|
||||
// must pass a log which will be written to with details of the operation.
|
||||
flushToFile: Task.async(function* (subdirArray, filename, log) {
|
||||
async flushToFile(subdirArray, filename, log) {
|
||||
let inStream = this.getInputStream();
|
||||
this.reset();
|
||||
if (!inStream) {
|
||||
|
@ -81,12 +80,12 @@ FlushableStorageAppender.prototype = {
|
|||
log.debug("Flushing file log");
|
||||
log.trace("Beginning stream copy to " + filename + ": " + Date.now());
|
||||
try {
|
||||
yield this._copyStreamToFile(inStream, subdirArray, filename, log);
|
||||
await this._copyStreamToFile(inStream, subdirArray, filename, log);
|
||||
log.trace("onCopyComplete", Date.now());
|
||||
} catch (ex) {
|
||||
log.error("Failed to copy log stream to file", ex);
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy an input stream to the named file, doing everything off the main
|
||||
|
@ -96,7 +95,7 @@ FlushableStorageAppender.prototype = {
|
|||
* outputFileName is the filename to create.
|
||||
* Returns a promise that is resolved on completion or rejected with an error.
|
||||
*/
|
||||
_copyStreamToFile: Task.async(function* (inputStream, subdirArray, outputFileName, log) {
|
||||
async _copyStreamToFile(inputStream, subdirArray, outputFileName, log) {
|
||||
// The log data could be large, so we don't want to pass it all in a single
|
||||
// message, so use BUFFER_SIZE chunks.
|
||||
const BUFFER_SIZE = 8192;
|
||||
|
@ -106,9 +105,9 @@ FlushableStorageAppender.prototype = {
|
|||
binaryStream.setInputStream(inputStream);
|
||||
|
||||
let outputDirectory = OS.Path.join(OS.Constants.Path.profileDir, ...subdirArray);
|
||||
yield OS.File.makeDir(outputDirectory, { ignoreExisting: true, from: OS.Constants.Path.profileDir });
|
||||
await OS.File.makeDir(outputDirectory, { ignoreExisting: true, from: OS.Constants.Path.profileDir });
|
||||
let fullOutputFileName = OS.Path.join(outputDirectory, outputFileName);
|
||||
let output = yield OS.File.open(fullOutputFileName, { write: true} );
|
||||
let output = await OS.File.open(fullOutputFileName, { write: true} );
|
||||
try {
|
||||
while (true) {
|
||||
let available = binaryStream.available();
|
||||
|
@ -116,18 +115,18 @@ FlushableStorageAppender.prototype = {
|
|||
break;
|
||||
}
|
||||
let chunk = binaryStream.readByteArray(Math.min(available, BUFFER_SIZE));
|
||||
yield output.write(new Uint8Array(chunk));
|
||||
await output.write(new Uint8Array(chunk));
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
binaryStream.close(); // inputStream is closed by the binaryStream
|
||||
yield output.close();
|
||||
await output.close();
|
||||
} catch (ex) {
|
||||
log.error("Failed to close the input stream", ex);
|
||||
}
|
||||
}
|
||||
log.trace("finished copy to", fullOutputFileName);
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
// The public LogManager object.
|
||||
|
@ -242,7 +241,7 @@ LogManager.prototype = {
|
|||
* file written or on error), SUCCESS_LOG_WRITTEN if a "success" log was
|
||||
* written, or ERROR_LOG_WRITTEN if an "error" log was written.
|
||||
*/
|
||||
resetFileLog: Task.async(function* () {
|
||||
async resetFileLog() {
|
||||
try {
|
||||
let flushToFile;
|
||||
let reasonPrefix;
|
||||
|
@ -266,7 +265,7 @@ LogManager.prototype = {
|
|||
// We have reasonPrefix at the start of the filename so all "error"
|
||||
// logs are grouped in about:sync-log.
|
||||
let filename = reasonPrefix + "-" + this.logFilePrefix + "-" + Date.now() + ".txt";
|
||||
yield this._fileAppender.flushToFile(this._logFileSubDirectoryEntries, filename, this._log);
|
||||
await this._fileAppender.flushToFile(this._logFileSubDirectoryEntries, filename, this._log);
|
||||
|
||||
// It's not completely clear to markh why we only do log cleanups
|
||||
// for errors, but for now the Sync semantics have been copied...
|
||||
|
@ -275,7 +274,7 @@ LogManager.prototype = {
|
|||
// there are occasional errors - let's address this later!)
|
||||
if (reason == this.ERROR_LOG_WRITTEN && !this._cleaningUpFileLogs) {
|
||||
this._log.trace("Scheduling cleanup.");
|
||||
// Note we don't return/yield or otherwise wait on this promise - it
|
||||
// Note we don't return/await or otherwise wait on this promise - it
|
||||
// continues in the background
|
||||
this.cleanupLogs().catch(err => {
|
||||
this._log.error("Failed to cleanup logs", err);
|
||||
|
@ -286,12 +285,12 @@ LogManager.prototype = {
|
|||
this._log.error("Failed to resetFileLog", ex);
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds all logs older than maxErrorAge and deletes them using async I/O.
|
||||
*/
|
||||
cleanupLogs: Task.async(function* () {
|
||||
async cleanupLogs() {
|
||||
this._cleaningUpFileLogs = true;
|
||||
let logDir = FileUtils.getDir("ProfD", this._logFileSubDirectoryEntries);
|
||||
let iterator = new OS.File.DirectoryIterator(logDir.path);
|
||||
|
@ -299,7 +298,7 @@ LogManager.prototype = {
|
|||
let threshold = Date.now() - 1000 * maxAge;
|
||||
|
||||
this._log.debug("Log cleanup threshold time: " + threshold);
|
||||
yield iterator.forEach(Task.async(function* (entry) {
|
||||
await iterator.forEach(async (entry) => {
|
||||
// Note that we don't check this.logFilePrefix is in the name - we cleanup
|
||||
// all files in this directory regardless of that prefix so old logfiles
|
||||
// for prefixes no longer in use are still cleaned up. See bug 1279145.
|
||||
|
@ -309,23 +308,23 @@ LogManager.prototype = {
|
|||
}
|
||||
try {
|
||||
// need to call .stat() as the enumerator doesn't give that to us on *nix.
|
||||
let info = yield OS.File.stat(entry.path);
|
||||
let info = await OS.File.stat(entry.path);
|
||||
if (info.lastModificationDate.getTime() >= threshold) {
|
||||
return;
|
||||
}
|
||||
this._log.trace(" > Cleanup removing " + entry.name +
|
||||
" (" + info.lastModificationDate.getTime() + ")");
|
||||
yield OS.File.remove(entry.path);
|
||||
await OS.File.remove(entry.path);
|
||||
this._log.trace("Deleted " + entry.name);
|
||||
} catch (ex) {
|
||||
this._log.debug("Encountered error trying to clean up old log file "
|
||||
+ entry.name, ex);
|
||||
}
|
||||
}.bind(this)));
|
||||
});
|
||||
iterator.close();
|
||||
this._cleaningUpFileLogs = false;
|
||||
this._log.debug("Done deleting files.");
|
||||
// This notification is used only for tests.
|
||||
Services.obs.notifyObservers(null, "services-tests:common:log-manager:cleanup-logs");
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -54,22 +54,17 @@ add_task(function* test_check_maybeSync() {
|
|||
|
||||
let updater = Cu.import("resource://services-common/blocklist-updater.js", {});
|
||||
|
||||
let syncPromise = new Promise(function(resolve, reject) {
|
||||
// add a test kinto client that will respond to lastModified information
|
||||
// for a collection called 'test-collection'
|
||||
updater.addTestBlocklistClient("test-collection", {
|
||||
bucketName: "blocklists",
|
||||
maybeSync(lastModified, serverTime) {
|
||||
do_check_eq(lastModified, 1000);
|
||||
do_check_eq(serverTime, 2000);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
updater.checkVersions();
|
||||
});
|
||||
|
||||
// ensure we get the maybeSync call
|
||||
yield syncPromise;
|
||||
// add a test kinto client that will respond to lastModified information
|
||||
// for a collection called 'test-collection'
|
||||
updater.addTestBlocklistClient("test-collection", {
|
||||
bucketName: "blocklists",
|
||||
maybeSync(lastModified, serverTime) {
|
||||
do_check_eq(lastModified, 1000);
|
||||
do_check_eq(serverTime, 2000);
|
||||
}
|
||||
});
|
||||
yield updater.checkVersions();
|
||||
|
||||
// check the last_update is updated
|
||||
do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-common/hawkclient.js");
|
||||
|
||||
const SECOND_MS = 1000;
|
||||
|
@ -490,9 +489,9 @@ function getTimestampDelta(authHeader, now = Date.now()) {
|
|||
}
|
||||
|
||||
function deferredStop(server) {
|
||||
let deferred = Promise.defer();
|
||||
server.stop(deferred.resolve);
|
||||
return deferred.promise;
|
||||
return new Promise(resolve => {
|
||||
server.stop(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
|
|
|
@ -17,7 +17,6 @@ const {utils: Cu, interfaces: Ci} = Components;
|
|||
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
|
@ -94,42 +93,41 @@ this.Credentials = Object.freeze({
|
|||
},
|
||||
|
||||
setup(emailInput, passwordInput, options = {}) {
|
||||
let deferred = Promise.defer();
|
||||
log.debug("setup credentials for " + emailInput);
|
||||
return new Promise(resolve => {
|
||||
log.debug("setup credentials for " + emailInput);
|
||||
|
||||
let hkdfSalt = options.hkdfSalt || HKDF_SALT;
|
||||
let hkdfLength = options.hkdfLength || HKDF_LENGTH;
|
||||
let hmacLength = options.hmacLength || HMAC_LENGTH;
|
||||
let hmacAlgorithm = options.hmacAlgorithm || HMAC_ALGORITHM;
|
||||
let stretchedPWLength = options.stretchedPassLength || STRETCHED_PW_LENGTH_BYTES;
|
||||
let pbkdf2Rounds = options.pbkdf2Rounds || PBKDF2_ROUNDS;
|
||||
let hkdfSalt = options.hkdfSalt || HKDF_SALT;
|
||||
let hkdfLength = options.hkdfLength || HKDF_LENGTH;
|
||||
let hmacLength = options.hmacLength || HMAC_LENGTH;
|
||||
let hmacAlgorithm = options.hmacAlgorithm || HMAC_ALGORITHM;
|
||||
let stretchedPWLength = options.stretchedPassLength || STRETCHED_PW_LENGTH_BYTES;
|
||||
let pbkdf2Rounds = options.pbkdf2Rounds || PBKDF2_ROUNDS;
|
||||
|
||||
let result = {};
|
||||
let result = {};
|
||||
|
||||
let password = CommonUtils.encodeUTF8(passwordInput);
|
||||
let salt = this.keyWordExtended("quickStretch", emailInput);
|
||||
let password = CommonUtils.encodeUTF8(passwordInput);
|
||||
let salt = this.keyWordExtended("quickStretch", emailInput);
|
||||
|
||||
let runnable = () => {
|
||||
let start = Date.now();
|
||||
let quickStretchedPW = CryptoUtils.pbkdf2Generate(
|
||||
password, salt, pbkdf2Rounds, stretchedPWLength, hmacAlgorithm, hmacLength);
|
||||
let runnable = () => {
|
||||
let start = Date.now();
|
||||
let quickStretchedPW = CryptoUtils.pbkdf2Generate(
|
||||
password, salt, pbkdf2Rounds, stretchedPWLength, hmacAlgorithm, hmacLength);
|
||||
|
||||
result.quickStretchedPW = quickStretchedPW;
|
||||
result.quickStretchedPW = quickStretchedPW;
|
||||
|
||||
result.authPW =
|
||||
CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("authPW"), hkdfLength);
|
||||
result.authPW =
|
||||
CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("authPW"), hkdfLength);
|
||||
|
||||
result.unwrapBKey =
|
||||
CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("unwrapBkey"), hkdfLength);
|
||||
result.unwrapBKey =
|
||||
CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("unwrapBkey"), hkdfLength);
|
||||
|
||||
log.debug("Credentials set up after " + (Date.now() - start) + " ms");
|
||||
deferred.resolve(result);
|
||||
}
|
||||
log.debug("Credentials set up after " + (Date.now() - start) + " ms");
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
Services.tm.dispatchToMainThread(runnable);
|
||||
log.debug("Dispatched thread for credentials setup crypto work");
|
||||
|
||||
return deferred.promise;
|
||||
Services.tm.dispatchToMainThread(runnable);
|
||||
log.debug("Dispatched thread for credentials setup crypto work");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -10,14 +10,13 @@ const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
|||
Cu.importGlobalProperties(["URL"]);
|
||||
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/PromiseUtils.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-common/rest.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsStorage.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
|
||||
|
@ -894,7 +893,7 @@ FxAccountsInternal.prototype = {
|
|||
return userData;
|
||||
}
|
||||
if (!currentState.whenKeysReadyDeferred) {
|
||||
currentState.whenKeysReadyDeferred = Promise.defer();
|
||||
currentState.whenKeysReadyDeferred = PromiseUtils.defer();
|
||||
if (userData.keyFetchToken) {
|
||||
this.fetchAndUnwrapKeys(userData.keyFetchToken).then(
|
||||
(dataWithKeys) => {
|
||||
|
@ -920,60 +919,58 @@ FxAccountsInternal.prototype = {
|
|||
).then(result => currentState.resolve(result));
|
||||
},
|
||||
|
||||
fetchAndUnwrapKeys(keyFetchToken) {
|
||||
async fetchAndUnwrapKeys(keyFetchToken) {
|
||||
if (logPII) {
|
||||
log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
|
||||
}
|
||||
let currentState = this.currentAccountState;
|
||||
return Task.spawn(function* task() {
|
||||
// Sign out if we don't have a key fetch token.
|
||||
if (!keyFetchToken) {
|
||||
log.warn("improper fetchAndUnwrapKeys() call: token missing");
|
||||
yield this.signOut();
|
||||
return null;
|
||||
}
|
||||
// Sign out if we don't have a key fetch token.
|
||||
if (!keyFetchToken) {
|
||||
log.warn("improper fetchAndUnwrapKeys() call: token missing");
|
||||
await this.signOut();
|
||||
return currentState.resolve(null);
|
||||
}
|
||||
|
||||
let {kA, wrapKB} = yield this.fetchKeys(keyFetchToken);
|
||||
let {kA, wrapKB} = await this.fetchKeys(keyFetchToken);
|
||||
|
||||
let data = yield currentState.getUserAccountData();
|
||||
let data = await currentState.getUserAccountData();
|
||||
|
||||
// Sanity check that the user hasn't changed out from under us
|
||||
if (data.keyFetchToken !== keyFetchToken) {
|
||||
throw new Error("Signed in user changed while fetching keys!");
|
||||
}
|
||||
// Sanity check that the user hasn't changed out from under us
|
||||
if (data.keyFetchToken !== keyFetchToken) {
|
||||
throw new Error("Signed in user changed while fetching keys!");
|
||||
}
|
||||
|
||||
// Next statements must be synchronous until we setUserAccountData
|
||||
// so that we don't risk getting into a weird state.
|
||||
let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey),
|
||||
wrapKB);
|
||||
// Next statements must be synchronous until we setUserAccountData
|
||||
// so that we don't risk getting into a weird state.
|
||||
let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey),
|
||||
wrapKB);
|
||||
|
||||
if (logPII) {
|
||||
log.debug("kB_hex: " + kB_hex);
|
||||
}
|
||||
let updateData = {
|
||||
kA: CommonUtils.bytesAsHex(kA),
|
||||
kB: CommonUtils.bytesAsHex(kB_hex),
|
||||
keyFetchToken: null, // null values cause the item to be removed.
|
||||
unwrapBKey: null,
|
||||
}
|
||||
if (logPII) {
|
||||
log.debug("kB_hex: " + kB_hex);
|
||||
}
|
||||
let updateData = {
|
||||
kA: CommonUtils.bytesAsHex(kA),
|
||||
kB: CommonUtils.bytesAsHex(kB_hex),
|
||||
keyFetchToken: null, // null values cause the item to be removed.
|
||||
unwrapBKey: null,
|
||||
}
|
||||
|
||||
log.debug("Keys Obtained: kA=" + !!updateData.kA + ", kB=" + !!updateData.kB);
|
||||
if (logPII) {
|
||||
log.debug("Keys Obtained: kA=" + updateData.kA + ", kB=" + updateData.kB);
|
||||
}
|
||||
log.debug("Keys Obtained: kA=" + !!updateData.kA + ", kB=" + !!updateData.kB);
|
||||
if (logPII) {
|
||||
log.debug("Keys Obtained: kA=" + updateData.kA + ", kB=" + updateData.kB);
|
||||
}
|
||||
|
||||
yield currentState.updateUserAccountData(updateData);
|
||||
// We are now ready for business. This should only be invoked once
|
||||
// per setSignedInUser(), regardless of whether we've rebooted since
|
||||
// setSignedInUser() was called.
|
||||
this.notifyObservers(ONVERIFIED_NOTIFICATION);
|
||||
return currentState.getUserAccountData();
|
||||
}.bind(this)).then(result => currentState.resolve(result));
|
||||
await currentState.updateUserAccountData(updateData);
|
||||
// We are now ready for business. This should only be invoked once
|
||||
// per setSignedInUser(), regardless of whether we've rebooted since
|
||||
// setSignedInUser() was called.
|
||||
this.notifyObservers(ONVERIFIED_NOTIFICATION);
|
||||
data = await currentState.getUserAccountData();
|
||||
return currentState.resolve(data);
|
||||
},
|
||||
|
||||
getAssertionFromCert(data, keyPair, cert, audience) {
|
||||
async getAssertionFromCert(data, keyPair, cert, audience) {
|
||||
log.debug("getAssertionFromCert");
|
||||
let d = Promise.defer();
|
||||
let options = {
|
||||
duration: ASSERTION_LIFETIME,
|
||||
localtimeOffsetMsec: this.localtimeOffsetMsec,
|
||||
|
@ -982,19 +979,21 @@ FxAccountsInternal.prototype = {
|
|||
let currentState = this.currentAccountState;
|
||||
// "audience" should look like "http://123done.org".
|
||||
// The generated assertion will expire in two minutes.
|
||||
jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => {
|
||||
if (err) {
|
||||
log.error("getAssertionFromCert: " + err);
|
||||
d.reject(err);
|
||||
} else {
|
||||
log.debug("getAssertionFromCert returning signed: " + !!signed);
|
||||
if (logPII) {
|
||||
log.debug("getAssertionFromCert returning signed: " + signed);
|
||||
let assertion = await new Promise((resolve, reject) => {
|
||||
jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => {
|
||||
if (err) {
|
||||
log.error("getAssertionFromCert: " + err);
|
||||
reject(err);
|
||||
} else {
|
||||
log.debug("getAssertionFromCert returning signed: " + !!signed);
|
||||
if (logPII) {
|
||||
log.debug("getAssertionFromCert returning signed: " + signed);
|
||||
}
|
||||
resolve(signed);
|
||||
}
|
||||
d.resolve(signed);
|
||||
}
|
||||
});
|
||||
});
|
||||
return d.promise.then(result => currentState.resolve(result));
|
||||
return currentState.resolve(assertion);
|
||||
},
|
||||
|
||||
getCertificateSigned(sessionToken, serializedPublicKey, lifetime) {
|
||||
|
@ -1012,7 +1011,7 @@ FxAccountsInternal.prototype = {
|
|||
/**
|
||||
* returns a promise that fires with {keyPair, certificate}.
|
||||
*/
|
||||
getKeypairAndCertificate: Task.async(function* (currentState) {
|
||||
async getKeypairAndCertificate(currentState) {
|
||||
// If the debugging pref to ignore cached authentication credentials is set for Sync,
|
||||
// then don't use any cached key pair/certificate, i.e., generate a new
|
||||
// one and get it signed.
|
||||
|
@ -1021,7 +1020,7 @@ FxAccountsInternal.prototype = {
|
|||
// password.
|
||||
let ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials", false);
|
||||
let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD;
|
||||
let accountData = yield currentState.getUserAccountData(["cert", "keyPair", "sessionToken"]);
|
||||
let accountData = await currentState.getUserAccountData(["cert", "keyPair", "sessionToken"]);
|
||||
|
||||
let keyPairValid = !ignoreCachedAuthCredentials &&
|
||||
accountData.keyPair &&
|
||||
|
@ -1055,7 +1054,7 @@ FxAccountsInternal.prototype = {
|
|||
keyPair = accountData.keyPair;
|
||||
} else {
|
||||
let keyWillBeValidUntil = this.now() + KEY_LIFETIME;
|
||||
keyPair = yield new Promise((resolve, reject) => {
|
||||
keyPair = await new Promise((resolve, reject) => {
|
||||
jwcrypto.generateKeyPair("DS160", (err, kp) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
|
@ -1072,7 +1071,7 @@ FxAccountsInternal.prototype = {
|
|||
|
||||
// and generate the cert.
|
||||
let certWillBeValidUntil = this.now() + CERT_LIFETIME;
|
||||
let certificate = yield this.getCertificateSigned(accountData.sessionToken,
|
||||
let certificate = await this.getCertificateSigned(accountData.sessionToken,
|
||||
keyPair.rawKeyPair.serializedPublicKey,
|
||||
CERT_LIFETIME);
|
||||
log.debug("getCertificate got a new one: " + !!certificate);
|
||||
|
@ -1085,13 +1084,13 @@ FxAccountsInternal.prototype = {
|
|||
validUntil: certWillBeValidUntil,
|
||||
},
|
||||
};
|
||||
yield currentState.updateUserAccountData(toUpdate);
|
||||
await currentState.updateUserAccountData(toUpdate);
|
||||
}
|
||||
return {
|
||||
keyPair: keyPair.rawKeyPair,
|
||||
certificate,
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
getUserAccountData() {
|
||||
return this.currentAccountState.getUserAccountData();
|
||||
|
@ -1175,7 +1174,7 @@ FxAccountsInternal.prototype = {
|
|||
// were already polling for receipt of an earlier email.
|
||||
this.pollStartDate = Date.now();
|
||||
if (!currentState.whenVerifiedDeferred) {
|
||||
currentState.whenVerifiedDeferred = Promise.defer();
|
||||
currentState.whenVerifiedDeferred = PromiseUtils.defer();
|
||||
// This deferred might not end up with any handlers (eg, if sync
|
||||
// is yet to start up.) This might cause "A promise chain failed to
|
||||
// handle a rejection" messages, so add an error handler directly
|
||||
|
@ -1348,7 +1347,7 @@ FxAccountsInternal.prototype = {
|
|||
* AUTH_ERROR
|
||||
* UNKNOWN_ERROR
|
||||
*/
|
||||
getOAuthToken: Task.async(function* (options = {}) {
|
||||
async getOAuthToken(options = {}) {
|
||||
log.debug("getOAuthToken enter");
|
||||
let scope = options.scope;
|
||||
if (typeof scope === "string") {
|
||||
|
@ -1359,7 +1358,7 @@ FxAccountsInternal.prototype = {
|
|||
throw this._error(ERROR_INVALID_PARAMETER, "Missing or invalid 'scope' option");
|
||||
}
|
||||
|
||||
yield this._getVerifiedAccountOrReject();
|
||||
await this._getVerifiedAccountOrReject();
|
||||
|
||||
// Early exit for a cached token.
|
||||
let currentState = this.currentAccountState;
|
||||
|
@ -1388,8 +1387,8 @@ FxAccountsInternal.prototype = {
|
|||
|
||||
try {
|
||||
log.debug("getOAuthToken fetching new token from", oAuthURL);
|
||||
let assertion = yield this.getAssertion(oAuthURL);
|
||||
let result = yield client.getTokenFromAssertion(assertion, scopeString);
|
||||
let assertion = await this.getAssertion(oAuthURL);
|
||||
let result = await client.getTokenFromAssertion(assertion, scopeString);
|
||||
let token = result.access_token;
|
||||
// If we got one, cache it.
|
||||
if (token) {
|
||||
|
@ -1410,7 +1409,7 @@ FxAccountsInternal.prototype = {
|
|||
} catch (err) {
|
||||
throw this._errorToErrorClass(err);
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an OAuth token from the token cache. Callers should call this
|
||||
|
@ -1424,7 +1423,7 @@ FxAccountsInternal.prototype = {
|
|||
* @return Promise.<undefined> This function will always resolve, even if
|
||||
* an unknown token is passed.
|
||||
*/
|
||||
removeCachedOAuthToken: Task.async(function* (options) {
|
||||
async removeCachedOAuthToken(options) {
|
||||
if (!options.token || typeof options.token !== "string") {
|
||||
throw this._error(ERROR_INVALID_PARAMETER, "Missing or invalid 'token' option");
|
||||
}
|
||||
|
@ -1436,7 +1435,7 @@ FxAccountsInternal.prototype = {
|
|||
log.warn("FxA failed to revoke a cached token", err);
|
||||
});
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
async _getVerifiedAccountOrReject() {
|
||||
let data = await this.currentAccountState.getUserAccountData();
|
||||
|
|
|
@ -7,7 +7,6 @@ this.EXPORTED_SYMBOLS = ["FxAccountsClient"];
|
|||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-common/hawkclient.js");
|
||||
|
@ -564,45 +563,36 @@ this.FxAccountsClient.prototype = {
|
|||
* "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error
|
||||
* }
|
||||
*/
|
||||
_request: function hawkRequest(path, method, credentials, jsonPayload) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
async _request(path, method, credentials, jsonPayload) {
|
||||
// We were asked to back off.
|
||||
if (this.backoffError) {
|
||||
log.debug("Received new request during backoff, re-rejecting.");
|
||||
deferred.reject(this.backoffError);
|
||||
return deferred.promise;
|
||||
throw this.backoffError;
|
||||
}
|
||||
|
||||
this.hawk.request(path, method, credentials, jsonPayload).then(
|
||||
(response) => {
|
||||
try {
|
||||
let responseObj = JSON.parse(response.body);
|
||||
deferred.resolve(responseObj);
|
||||
} catch (err) {
|
||||
log.error("json parse error on response: " + response.body);
|
||||
deferred.reject({error: err});
|
||||
}
|
||||
},
|
||||
|
||||
(error) => {
|
||||
log.error("error " + method + "ing " + path + ": " + JSON.stringify(error));
|
||||
if (error.retryAfter) {
|
||||
log.debug("Received backoff response; caching error as flag.");
|
||||
this.backoffError = error;
|
||||
// Schedule clearing of cached-error-as-flag.
|
||||
CommonUtils.namedTimer(
|
||||
this._clearBackoff,
|
||||
error.retryAfter * 1000,
|
||||
this,
|
||||
"fxaBackoffTimer"
|
||||
);
|
||||
}
|
||||
deferred.reject(error);
|
||||
let response;
|
||||
try {
|
||||
response = await this.hawk.request(path, method, credentials, jsonPayload);
|
||||
} catch (error) {
|
||||
log.error("error " + method + "ing " + path + ": " + JSON.stringify(error));
|
||||
if (error.retryAfter) {
|
||||
log.debug("Received backoff response; caching error as flag.");
|
||||
this.backoffError = error;
|
||||
// Schedule clearing of cached-error-as-flag.
|
||||
CommonUtils.namedTimer(
|
||||
this._clearBackoff,
|
||||
error.retryAfter * 1000,
|
||||
this,
|
||||
"fxaBackoffTimer"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(response.body);
|
||||
} catch (error) {
|
||||
log.error("json parse error on response: " + response.body);
|
||||
throw {error};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ Cu.import("resource://services-common/rest.js");
|
|||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
|
||||
"resource://gre/modules/FxAccounts.jsm");
|
||||
|
@ -34,24 +33,24 @@ const CONFIG_PREFS = [
|
|||
this.FxAccountsConfig = {
|
||||
|
||||
// Returns a promise that resolves with the URI of the remote UI flows.
|
||||
promiseAccountsSignUpURI: Task.async(function*() {
|
||||
yield this.ensureConfigured();
|
||||
async promiseAccountsSignUpURI() {
|
||||
await this.ensureConfigured();
|
||||
let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signup.uri");
|
||||
if (fxAccounts.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
|
||||
throw new Error("Firefox Accounts server must use HTTPS");
|
||||
}
|
||||
return url;
|
||||
}),
|
||||
},
|
||||
|
||||
// Returns a promise that resolves with the URI of the remote UI flows.
|
||||
promiseAccountsSignInURI: Task.async(function*() {
|
||||
yield this.ensureConfigured();
|
||||
async promiseAccountsSignInURI() {
|
||||
await this.ensureConfigured();
|
||||
let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signin.uri");
|
||||
if (fxAccounts.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
|
||||
throw new Error("Firefox Accounts server must use HTTPS");
|
||||
}
|
||||
return url;
|
||||
}),
|
||||
},
|
||||
|
||||
resetConfigURLs() {
|
||||
let autoconfigURL = this.getAutoConfigURL();
|
||||
|
@ -96,25 +95,25 @@ this.FxAccountsConfig = {
|
|||
return rootURL;
|
||||
},
|
||||
|
||||
ensureConfigured: Task.async(function*() {
|
||||
let isSignedIn = !!(yield fxAccounts.getSignedInUser());
|
||||
async ensureConfigured() {
|
||||
let isSignedIn = !!(await fxAccounts.getSignedInUser());
|
||||
if (!isSignedIn) {
|
||||
yield this.fetchConfigURLs();
|
||||
await this.fetchConfigURLs();
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
// Read expected client configuration from the fxa auth server
|
||||
// (from `identity.fxaccounts.autoconfig.uri`/.well-known/fxa-client-configuration)
|
||||
// and replace all the relevant our prefs with the information found there.
|
||||
// This is only done before sign-in and sign-up, and even then only if the
|
||||
// `identity.fxaccounts.autoconfig.uri` preference is set.
|
||||
fetchConfigURLs: Task.async(function*() {
|
||||
async fetchConfigURLs() {
|
||||
let rootURL = this.getAutoConfigURL();
|
||||
if (!rootURL) {
|
||||
return;
|
||||
}
|
||||
let configURL = rootURL + "/.well-known/fxa-client-configuration";
|
||||
let jsonStr = yield new Promise((resolve, reject) => {
|
||||
let jsonStr = await new Promise((resolve, reject) => {
|
||||
let request = new RESTRequest(configURL);
|
||||
request.setHeader("Accept", "application/json");
|
||||
request.get(error => {
|
||||
|
@ -170,6 +169,6 @@ this.FxAccountsConfig = {
|
|||
log.error("Failed to initialize configuration preferences from autoconfig object", e);
|
||||
throw e;
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
};
|
||||
|
|
|
@ -18,7 +18,6 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
|||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccounts.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "permissionManager",
|
||||
|
|
|
@ -12,7 +12,6 @@ this.EXPORTED_SYMBOLS = ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClie
|
|||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://services-common/rest.js");
|
||||
|
|
|
@ -11,11 +11,9 @@ this.EXPORTED_SYMBOLS = ["FxAccountsProfileClient", "FxAccountsProfileClientErro
|
|||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/FxAccounts.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://services-common/rest.js");
|
||||
|
||||
Cu.importGlobalProperties(["URL"]);
|
||||
|
@ -82,14 +80,14 @@ this.FxAccountsProfileClient.prototype = {
|
|||
* Rejects: {FxAccountsProfileClientError} Profile client error.
|
||||
* @private
|
||||
*/
|
||||
_createRequest: Task.async(function* (path, method = "GET", etag = null) {
|
||||
async _createRequest(path, method = "GET", etag = null) {
|
||||
let token = this.token;
|
||||
if (!token) {
|
||||
// tokens are cached, so getting them each request is cheap.
|
||||
token = yield this.fxa.getOAuthToken(this.oauthOptions);
|
||||
token = await this.fxa.getOAuthToken(this.oauthOptions);
|
||||
}
|
||||
try {
|
||||
return (yield this._rawRequest(path, method, token, etag));
|
||||
return (await this._rawRequest(path, method, token, etag));
|
||||
} catch (ex) {
|
||||
if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
|
||||
throw ex;
|
||||
|
@ -100,22 +98,22 @@ this.FxAccountsProfileClient.prototype = {
|
|||
}
|
||||
// it's an auth error - assume our token expired and retry.
|
||||
log.info("Fetching the profile returned a 401 - revoking our token and retrying");
|
||||
yield this.fxa.removeCachedOAuthToken({token});
|
||||
token = yield this.fxa.getOAuthToken(this.oauthOptions);
|
||||
await this.fxa.removeCachedOAuthToken({token});
|
||||
token = await this.fxa.getOAuthToken(this.oauthOptions);
|
||||
// and try with the new token - if that also fails then we fail after
|
||||
// revoking the token.
|
||||
try {
|
||||
return (yield this._rawRequest(path, method, token, etag));
|
||||
return (await this._rawRequest(path, method, token, etag));
|
||||
} catch (ex) {
|
||||
if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
|
||||
throw ex;
|
||||
}
|
||||
log.info("Retry fetching the profile still returned a 401 - revoking our token and failing");
|
||||
yield this.fxa.removeCachedOAuthToken({token});
|
||||
await this.fxa.removeCachedOAuthToken({token});
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
/**
|
||||
* Remote "raw" request helper - doesn't handle auth errors and tokens.
|
||||
|
|
|
@ -10,7 +10,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
/**
|
||||
* FxAccountsPushService manages Push notifications for Firefox Accounts in the browser
|
||||
|
@ -194,12 +193,12 @@ FxAccountsPushService.prototype = {
|
|||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
_onPasswordChanged: Task.async(function* () {
|
||||
if (!(yield this.fxAccounts.sessionStatus())) {
|
||||
yield this.fxAccounts.resetCredentials();
|
||||
async _onPasswordChanged() {
|
||||
if (!(await this.fxAccounts.sessionStatus())) {
|
||||
await this.fxAccounts.resetCredentials();
|
||||
Services.obs.notifyObservers(null, ON_ACCOUNT_STATE_CHANGE_NOTIFICATION);
|
||||
}
|
||||
}),
|
||||
},
|
||||
/**
|
||||
* Fired when the Push server drops a subscription, or the subscription identifier changes.
|
||||
*
|
||||
|
|
|
@ -12,7 +12,6 @@ const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
|||
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
@ -65,7 +64,7 @@ this.FxAccountsStorageManager.prototype = {
|
|||
this._promiseInitialized = this._initialize(accountData);
|
||||
},
|
||||
|
||||
_initialize: Task.async(function* (accountData) {
|
||||
async _initialize(accountData) {
|
||||
log.trace("initializing new storage manager");
|
||||
try {
|
||||
if (accountData) {
|
||||
|
@ -88,20 +87,20 @@ this.FxAccountsStorageManager.prototype = {
|
|||
}
|
||||
}
|
||||
// write it out and we are done.
|
||||
yield this._write();
|
||||
await this._write();
|
||||
return;
|
||||
}
|
||||
// So we were initialized without account data - that means we need to
|
||||
// read the state from storage. We try and read plain storage first and
|
||||
// only attempt to read secure storage if the plain storage had a user.
|
||||
this._needToReadSecure = yield this._readPlainStorage();
|
||||
this._needToReadSecure = await this._readPlainStorage();
|
||||
if (this._needToReadSecure && this.secureStorage) {
|
||||
yield this._doReadAndUpdateSecure();
|
||||
await this._doReadAndUpdateSecure();
|
||||
}
|
||||
} finally {
|
||||
log.trace("initializing of new storage manager done");
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
finalize() {
|
||||
// We can't throw this instance away while it is still writing or we may
|
||||
|
@ -145,8 +144,8 @@ this.FxAccountsStorageManager.prototype = {
|
|||
// data is returned except for "in memory" fields. Note that not specifying
|
||||
// field names will soon be deprecated/removed - we want all callers to
|
||||
// specify the fields they care about.
|
||||
getAccountData: Task.async(function* (fieldNames = null) {
|
||||
yield this._promiseInitialized;
|
||||
async getAccountData(fieldNames = null) {
|
||||
await this._promiseInitialized;
|
||||
// We know we are initialized - this means our .cachedPlain is accurate
|
||||
// and doesn't need to be read (it was read if necessary by initialize).
|
||||
// So if there's no uid, there's no user signed in.
|
||||
|
@ -160,7 +159,7 @@ this.FxAccountsStorageManager.prototype = {
|
|||
result[name] = value;
|
||||
}
|
||||
// But the secure data may not have been read, so try that now.
|
||||
yield this._maybeReadAndUpdateSecure();
|
||||
await this._maybeReadAndUpdateSecure();
|
||||
// .cachedSecure now has as much as it possibly can (which is possibly
|
||||
// nothing if (a) secure storage remains locked and (b) we've never updated
|
||||
// a field to be stored in secure storage.)
|
||||
|
@ -188,7 +187,7 @@ this.FxAccountsStorageManager.prototype = {
|
|||
} else if (FXA_PWDMGR_SECURE_FIELDS.has(fieldName)) {
|
||||
// We may not have read secure storage yet.
|
||||
if (!checkedSecure) {
|
||||
yield this._maybeReadAndUpdateSecure();
|
||||
await this._maybeReadAndUpdateSecure();
|
||||
checkedSecure = true;
|
||||
}
|
||||
if (this.cachedSecure[fieldName] !== undefined) {
|
||||
|
@ -199,12 +198,12 @@ this.FxAccountsStorageManager.prototype = {
|
|||
}
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
},
|
||||
|
||||
// Update just the specified fields. This DOES NOT allow you to change to
|
||||
// a different user, nor to set the user as signed-out.
|
||||
updateAccountData: Task.async(function* (newFields) {
|
||||
yield this._promiseInitialized;
|
||||
async updateAccountData(newFields) {
|
||||
await this._promiseInitialized;
|
||||
if (!("uid" in this.cachedPlain)) {
|
||||
// If this storage instance shows no logged in user, then you can't
|
||||
// update fields.
|
||||
|
@ -244,12 +243,12 @@ this.FxAccountsStorageManager.prototype = {
|
|||
}
|
||||
// If we haven't yet read the secure data, do so now, else we may write
|
||||
// out partial data.
|
||||
yield this._maybeReadAndUpdateSecure();
|
||||
await this._maybeReadAndUpdateSecure();
|
||||
// Now save it - but don't wait on the _write promise - it's queued up as
|
||||
// a storage operation, so .finalize() will wait for completion, but no need
|
||||
// for us to.
|
||||
this._write();
|
||||
}),
|
||||
},
|
||||
|
||||
_clearCachedData() {
|
||||
this.cachedMemory = {};
|
||||
|
@ -270,10 +269,10 @@ this.FxAccountsStorageManager.prototype = {
|
|||
Note: _readPlainStorage is only called during initialize, so isn't
|
||||
protected via _queueStorageOperation() nor _promiseInitialized.
|
||||
*/
|
||||
_readPlainStorage: Task.async(function* () {
|
||||
async _readPlainStorage() {
|
||||
let got;
|
||||
try {
|
||||
got = yield this.plainStorage.get();
|
||||
got = await this.plainStorage.get();
|
||||
} catch (err) {
|
||||
// File hasn't been created yet. That will be done
|
||||
// when write is called.
|
||||
|
@ -299,12 +298,12 @@ this.FxAccountsStorageManager.prototype = {
|
|||
this.cachedPlain[name] = value;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
},
|
||||
|
||||
/* If we haven't managed to read the secure storage, try now, so
|
||||
we can merge our cached data with the data that's already been set.
|
||||
*/
|
||||
_maybeReadAndUpdateSecure: Task.async(function* () {
|
||||
_maybeReadAndUpdateSecure() {
|
||||
if (this.secureStorage == null || !this._needToReadSecure) {
|
||||
return null;
|
||||
}
|
||||
|
@ -314,13 +313,13 @@ this.FxAccountsStorageManager.prototype = {
|
|||
}
|
||||
return null;
|
||||
});
|
||||
}),
|
||||
},
|
||||
|
||||
/* Unconditionally read the secure storage and merge our cached data (ie, data
|
||||
which has already been set while the secure storage was locked) with
|
||||
the read data
|
||||
*/
|
||||
_doReadAndUpdateSecure: Task.async(function* () {
|
||||
async _doReadAndUpdateSecure() {
|
||||
let { uid, email } = this.cachedPlain;
|
||||
try {
|
||||
log.debug("reading secure storage with existing", Object.keys(this.cachedSecure));
|
||||
|
@ -328,7 +327,7 @@ this.FxAccountsStorageManager.prototype = {
|
|||
// updated cachedSecure before we've read it. That means that after we do
|
||||
// manage to read we must write back the merged data.
|
||||
let needWrite = Object.keys(this.cachedSecure).length != 0;
|
||||
let readSecure = yield this.secureStorage.get(uid, email);
|
||||
let readSecure = await this.secureStorage.get(uid, email);
|
||||
// and update our cached data with it - anything already in .cachedSecure
|
||||
// wins (including the fact it may be null or undefined, the latter
|
||||
// which means it will be removed from storage.
|
||||
|
@ -345,7 +344,7 @@ this.FxAccountsStorageManager.prototype = {
|
|||
}
|
||||
if (needWrite) {
|
||||
log.debug("successfully read secure data; writing updated data back")
|
||||
yield this._doWriteSecure();
|
||||
await this._doWriteSecure();
|
||||
}
|
||||
}
|
||||
this._needToReadSecure = false;
|
||||
|
@ -357,7 +356,7 @@ this.FxAccountsStorageManager.prototype = {
|
|||
throw ex;
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
_write() {
|
||||
// We don't want multiple writes happening concurrently, and we also need to
|
||||
|
@ -365,7 +364,7 @@ this.FxAccountsStorageManager.prototype = {
|
|||
return this._queueStorageOperation(() => this.__write());
|
||||
},
|
||||
|
||||
__write: Task.async(function* () {
|
||||
async __write() {
|
||||
// Write everything back - later we could track what's actually dirty,
|
||||
// but for now we write it all.
|
||||
log.debug("writing plain storage", Object.keys(this.cachedPlain));
|
||||
|
@ -373,7 +372,7 @@ this.FxAccountsStorageManager.prototype = {
|
|||
version: DATA_FORMAT_VERSION,
|
||||
accountData: this.cachedPlain,
|
||||
}
|
||||
yield this.plainStorage.set(toWritePlain);
|
||||
await this.plainStorage.set(toWritePlain);
|
||||
|
||||
// If we have no secure storage manager we are done.
|
||||
if (this.secureStorage == null) {
|
||||
|
@ -382,14 +381,14 @@ this.FxAccountsStorageManager.prototype = {
|
|||
// and only attempt to write to secure storage if we've managed to read it,
|
||||
// otherwise we might clobber data that's already there.
|
||||
if (!this._needToReadSecure) {
|
||||
yield this._doWriteSecure();
|
||||
await this._doWriteSecure();
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
/* Do the actual write of secure data. Caller is expected to check if we actually
|
||||
need to write and to ensure we are in a queued storage operation.
|
||||
*/
|
||||
_doWriteSecure: Task.async(function* () {
|
||||
async _doWriteSecure() {
|
||||
// We need to remove null items here.
|
||||
for (let [name, value] of Object.entries(this.cachedSecure)) {
|
||||
if (value == null) {
|
||||
|
@ -402,7 +401,7 @@ this.FxAccountsStorageManager.prototype = {
|
|||
accountData: this.cachedSecure,
|
||||
}
|
||||
try {
|
||||
yield this.secureStorage.set(this.cachedPlain.uid, toWriteSecure);
|
||||
await this.secureStorage.set(this.cachedPlain.uid, toWriteSecure);
|
||||
} catch (ex) {
|
||||
if (!(ex instanceof this.secureStorage.STORAGE_LOCKED)) {
|
||||
throw ex;
|
||||
|
@ -412,23 +411,23 @@ this.FxAccountsStorageManager.prototype = {
|
|||
// read.
|
||||
log.error("setAccountData: secure storage is locked trying to write");
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
// Delete the data for an account - ie, called on "sign out".
|
||||
deleteAccountData() {
|
||||
return this._queueStorageOperation(() => this._deleteAccountData());
|
||||
},
|
||||
|
||||
_deleteAccountData: Task.async(function* () {
|
||||
async _deleteAccountData() {
|
||||
log.debug("removing account data");
|
||||
yield this._promiseInitialized;
|
||||
yield this.plainStorage.set(null);
|
||||
await this._promiseInitialized;
|
||||
await this.plainStorage.set(null);
|
||||
if (this.secureStorage) {
|
||||
yield this.secureStorage.set(null);
|
||||
await this.secureStorage.set(null);
|
||||
}
|
||||
this._clearCachedData();
|
||||
log.debug("account data reset");
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -495,9 +494,9 @@ LoginManagerStorage.prototype = {
|
|||
// Clear any data from the login manager. Returns true if the login manager
|
||||
// was unlocked (even if no existing logins existed) or false if it was
|
||||
// locked (meaning we don't even know if it existed or not.)
|
||||
_clearLoginMgrData: Task.async(function* () {
|
||||
async _clearLoginMgrData() {
|
||||
try { // Services.logins might be third-party and broken...
|
||||
yield Services.logins.initializationPromise;
|
||||
await Services.logins.initializationPromise;
|
||||
if (!this._isLoggedIn) {
|
||||
return false;
|
||||
}
|
||||
|
@ -510,12 +509,12 @@ LoginManagerStorage.prototype = {
|
|||
log.error("Failed to clear login data: ${}", ex);
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
set: Task.async(function* (uid, contents) {
|
||||
async set(uid, contents) {
|
||||
if (!contents) {
|
||||
// Nuke it from the login manager.
|
||||
let cleared = yield this._clearLoginMgrData();
|
||||
let cleared = await this._clearLoginMgrData();
|
||||
if (!cleared) {
|
||||
// just log a message - we verify that the uid matches when
|
||||
// we reload it, so having a stale entry doesn't really hurt.
|
||||
|
@ -529,7 +528,7 @@ LoginManagerStorage.prototype = {
|
|||
log.trace("starting write of user data to the login manager");
|
||||
try { // Services.logins might be third-party and broken...
|
||||
// and the stuff into the login manager.
|
||||
yield Services.logins.initializationPromise;
|
||||
await Services.logins.initializationPromise;
|
||||
// If MP is locked we silently fail - the user may need to re-auth
|
||||
// next startup.
|
||||
if (!this._isLoggedIn) {
|
||||
|
@ -563,14 +562,14 @@ LoginManagerStorage.prototype = {
|
|||
// manager replacement that's simply broken.
|
||||
log.error("Failed to save data to the login manager", ex);
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
get: Task.async(function* (uid, email) {
|
||||
async get(uid, email) {
|
||||
log.trace("starting fetch of user data from the login manager");
|
||||
|
||||
try { // Services.logins might be third-party and broken...
|
||||
// read the data from the login manager and merge it for return.
|
||||
yield Services.logins.initializationPromise;
|
||||
await Services.logins.initializationPromise;
|
||||
|
||||
if (!this._isLoggedIn) {
|
||||
log.info("returning partial account data as the login manager is locked.");
|
||||
|
@ -590,7 +589,7 @@ LoginManagerStorage.prototype = {
|
|||
return JSON.parse(login.password);
|
||||
}
|
||||
log.info("username in the login manager doesn't match - ignoring it");
|
||||
yield this._clearLoginMgrData();
|
||||
await this._clearLoginMgrData();
|
||||
} catch (ex) {
|
||||
if (ex instanceof this.STORAGE_LOCKED) {
|
||||
throw ex;
|
||||
|
@ -600,7 +599,7 @@ LoginManagerStorage.prototype = {
|
|||
log.error("Failed to get data from the login manager", ex);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
// A global variable to indicate if the login manager is available - it doesn't
|
||||
|
|
|
@ -85,7 +85,9 @@ this.FxAccountsWebChannel = function(options) {
|
|||
this._webChannelId = options.channel_id;
|
||||
|
||||
// options.helpers is only specified by tests.
|
||||
this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options);
|
||||
XPCOMUtils.defineLazyGetter(this, "_helpers", () => {
|
||||
return options.helpers || new FxAccountsWebChannelHelpers(options);
|
||||
});
|
||||
|
||||
this._setupChannel();
|
||||
};
|
||||
|
|
|
@ -25,7 +25,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=963835
|
|||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
Components.utils.import("resource://gre/modules/Promise.jsm");
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
Components.utils.import("resource://gre/modules/FxAccounts.jsm");
|
||||
Components.utils.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
|
|
|
@ -8,7 +8,7 @@ Cu.import("resource://gre/modules/FxAccounts.jsm");
|
|||
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/FxAccountsOAuthGrantClient.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/PromiseUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
// We grab some additional stuff via backstage passes.
|
||||
|
@ -96,31 +96,27 @@ function MockFxAccountsClient() {
|
|||
|
||||
// mock calls up to the auth server to determine whether the
|
||||
// user account has been verified
|
||||
this.recoveryEmailStatus = function(sessionToken) {
|
||||
this.recoveryEmailStatus = async function(sessionToken) {
|
||||
// simulate a call to /recovery_email/status
|
||||
return Promise.resolve({
|
||||
return {
|
||||
email: this._email,
|
||||
verified: this._verified
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
this.accountStatus = function(uid) {
|
||||
let deferred = Promise.defer();
|
||||
deferred.resolve(!!uid && (!this._deletedOnServer));
|
||||
return deferred.promise;
|
||||
this.accountStatus = async function(uid) {
|
||||
return !!uid && (!this._deletedOnServer);
|
||||
};
|
||||
|
||||
this.accountKeys = function(keyFetchToken) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
do_timeout(50, () => {
|
||||
let response = {
|
||||
kA: expandBytes("11"),
|
||||
wrapKB: expandBytes("22")
|
||||
};
|
||||
deferred.resolve(response);
|
||||
return new Promise(resolve => {
|
||||
do_timeout(50, () => {
|
||||
resolve({
|
||||
kA: expandBytes("11"),
|
||||
wrapKB: expandBytes("22")
|
||||
});
|
||||
});
|
||||
});
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
this.resendVerificationEmail = function(sessionToken) {
|
||||
|
@ -150,7 +146,7 @@ function MockFxAccounts() {
|
|||
VERIFICATION_POLL_TIMEOUT_INITIAL: 100, // 100ms
|
||||
|
||||
_getCertificateSigned_calls: [],
|
||||
_d_signCertificate: Promise.defer(),
|
||||
_d_signCertificate: PromiseUtils.defer(),
|
||||
_now_is: new Date(),
|
||||
now() {
|
||||
return this._now_is;
|
||||
|
@ -792,7 +788,7 @@ add_task(function* test_getAssertion() {
|
|||
do_check_eq(exp, now + ASSERTION_LIFETIME);
|
||||
|
||||
// Reset for next call.
|
||||
fxa.internal._d_signCertificate = Promise.defer();
|
||||
fxa.internal._d_signCertificate = PromiseUtils.defer();
|
||||
|
||||
// Getting a new assertion "soon" (i.e., w/o incrementing "now"), even for
|
||||
// a new audience, should not provoke key generation or a signing request.
|
||||
|
|
|
@ -7,7 +7,6 @@ Cu.import("resource://services-common/utils.js");
|
|||
Cu.import("resource://gre/modules/FxAccounts.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
initTestLogging("Trace");
|
||||
|
@ -77,9 +76,7 @@ function MockFxAccountsClient(device) {
|
|||
};
|
||||
|
||||
this.accountStatus = function(uid) {
|
||||
let deferred = Promise.defer();
|
||||
deferred.resolve(!!uid && (!this._deletedOnServer));
|
||||
return deferred.promise;
|
||||
return Promise.resolve(!!uid && (!this._deletedOnServer));
|
||||
};
|
||||
|
||||
const { id: deviceId, name: deviceName, type: deviceType, sessionToken } = device;
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-common/hawkrequest.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
|
@ -35,9 +34,9 @@ var ACCOUNT_KEYS = {
|
|||
};
|
||||
|
||||
function deferredStop(server) {
|
||||
let deferred = Promise.defer();
|
||||
server.stop(deferred.resolve);
|
||||
return deferred.promise;
|
||||
return new Promise(resolve => {
|
||||
server.stop(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function* test_authenticated_get_request() {
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://gre/modules/Credentials.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
|
||||
|
|
|
@ -64,9 +64,7 @@ function MockFxAccountsClient() {
|
|||
this._verified = false;
|
||||
|
||||
this.accountStatus = function(uid) {
|
||||
let deferred = Promise.defer();
|
||||
deferred.resolve(!!uid && (!this._deletedOnServer));
|
||||
return deferred.promise;
|
||||
return Promise.resolve(!!uid && (!this._deletedOnServer));
|
||||
};
|
||||
|
||||
this.signOut = function() { return Promise.resolve(); };
|
||||
|
|
|
@ -61,9 +61,7 @@ function MockFxAccountsClient() {
|
|||
this._verified = false;
|
||||
|
||||
this.accountStatus = function(uid) {
|
||||
let deferred = Promise.defer();
|
||||
deferred.resolve(!!uid && (!this._deletedOnServer));
|
||||
return deferred.promise;
|
||||
return Promise.resolve(!!uid && (!this._deletedOnServer));
|
||||
};
|
||||
|
||||
this.signOut = function() { return Promise.resolve(); };
|
||||
|
@ -166,11 +164,12 @@ add_task(function* testRevoke() {
|
|||
ok(token1, "got a token");
|
||||
equal(token1, "token0");
|
||||
|
||||
// FxA fires an observer when the "background" revoke is complete.
|
||||
let revokeComplete = promiseNotification("testhelper-fxa-revoke-complete");
|
||||
// drop the new token from our cache.
|
||||
yield fxa.removeCachedOAuthToken({token: token1});
|
||||
yield revokeComplete;
|
||||
|
||||
// FxA fires an observer when the "background" revoke is complete.
|
||||
yield promiseNotification("testhelper-fxa-revoke-complete");
|
||||
// the revoke should have been successful.
|
||||
equal(client.activeTokens.size, 0);
|
||||
// fetching it again hits the server.
|
||||
|
@ -198,10 +197,11 @@ add_task(function* testSignOutDestroysTokens() {
|
|||
ok(token2, "got a token");
|
||||
notEqual(token1, token2, "got a different token");
|
||||
|
||||
// FxA fires an observer when the "background" signout is complete.
|
||||
let signoutComplete = promiseNotification("testhelper-fxa-signout-complete");
|
||||
// now sign out - they should be removed.
|
||||
yield fxa.signOut();
|
||||
// FxA fires an observer when the "background" signout is complete.
|
||||
yield promiseNotification("testhelper-fxa-signout-complete");
|
||||
yield signoutComplete;
|
||||
// No active tokens left.
|
||||
equal(client.activeTokens.size, 0);
|
||||
});
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/FxAccountsProfileClient.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsProfile.jsm");
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
|
||||
// Tests for the FxA push service.
|
||||
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
// Tests for the FxA storage manager.
|
||||
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsStorage.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
@ -28,15 +27,15 @@ function MockedPlainStorage(accountData) {
|
|||
this.numReads = 0;
|
||||
}
|
||||
MockedPlainStorage.prototype = {
|
||||
get: Task.async(function* () {
|
||||
async get() {
|
||||
this.numReads++;
|
||||
Assert.equal(this.numReads, 1, "should only ever be 1 read of acct data");
|
||||
return this.data;
|
||||
}),
|
||||
},
|
||||
|
||||
set: Task.async(function* (data) {
|
||||
async set(data) {
|
||||
this.data = data;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
function MockedSecureStorage(accountData) {
|
||||
|
@ -60,7 +59,7 @@ MockedSecureStorage.prototype = {
|
|||
// "TypeError: this.STORAGE_LOCKED is not a constructor"
|
||||
STORAGE_LOCKED: function() {},
|
||||
/* eslint-enable object-shorthand */
|
||||
get: Task.async(function* (uid, email) {
|
||||
async get(uid, email) {
|
||||
this.fetchCount++;
|
||||
if (this.locked) {
|
||||
throw new this.STORAGE_LOCKED();
|
||||
|
@ -68,11 +67,11 @@ MockedSecureStorage.prototype = {
|
|||
this.numReads++;
|
||||
Assert.equal(this.numReads, 1, "should only ever be 1 read of unlocked data");
|
||||
return this.data;
|
||||
}),
|
||||
},
|
||||
|
||||
set: Task.async(function* (uid, contents) {
|
||||
async set(uid, contents) {
|
||||
this.data = contents;
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
function add_storage_task(testFunction) {
|
||||
|
@ -336,12 +335,12 @@ add_task(function* checkLockedUpdates() {
|
|||
// A helper for our queued tests. It creates a StorageManager and then queues
|
||||
// an unresolved promise. The tests then do additional setup and checks, then
|
||||
// resolves or rejects the blocked promise.
|
||||
var setupStorageManagerForQueueTest = Task.async(function* () {
|
||||
async function setupStorageManagerForQueueTest() {
|
||||
let sm = new FxAccountsStorageManager();
|
||||
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
|
||||
sm.secureStorage = new MockedSecureStorage({kA: "kA"});
|
||||
sm.secureStorage.locked = true;
|
||||
yield sm.initialize();
|
||||
await sm.initialize();
|
||||
|
||||
let resolveBlocked, rejectBlocked;
|
||||
let blockedPromise = new Promise((resolve, reject) => {
|
||||
|
@ -351,7 +350,7 @@ var setupStorageManagerForQueueTest = Task.async(function* () {
|
|||
|
||||
sm._queueStorageOperation(() => blockedPromise);
|
||||
return {sm, blockedPromise, resolveBlocked, rejectBlocked}
|
||||
});
|
||||
}
|
||||
|
||||
// First the general functionality.
|
||||
add_task(function* checkQueueSemantics() {
|
||||
|
|
|
@ -220,6 +220,9 @@ this.configureIdentity = async function(identityOverrides, server) {
|
|||
|
||||
configureFxAccountIdentity(ns.Service.identity, config);
|
||||
await ns.Service.identity.initializeWithCurrentIdentity();
|
||||
// The token is fetched in the background, whenReadyToAuthenticate is resolved
|
||||
// when we are ready.
|
||||
await ns.Service.identity.whenReadyToAuthenticate.promise;
|
||||
// and cheat to avoid requiring each test do an explicit login - give it
|
||||
// a cluster URL.
|
||||
if (config.fxaccount.token.endpoint) {
|
||||
|
|
|
@ -11,7 +11,6 @@ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
|
|||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://services-sync/main.js");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
|
@ -56,14 +55,14 @@ let log = Log.repository.getLogger("Sync.RemoteTabs");
|
|||
// A private singleton that does the work.
|
||||
let SyncedTabsInternal = {
|
||||
/* Make a "tab" record. Returns a promise */
|
||||
_makeTab: Task.async(function* (client, tab, url, showRemoteIcons) {
|
||||
async _makeTab(client, tab, url, showRemoteIcons) {
|
||||
let icon;
|
||||
if (showRemoteIcons) {
|
||||
icon = tab.icon;
|
||||
}
|
||||
if (!icon) {
|
||||
try {
|
||||
icon = (yield PlacesUtils.promiseFaviconLinkUrl(url)).spec;
|
||||
icon = (await PlacesUtils.promiseFaviconLinkUrl(url)).spec;
|
||||
} catch (ex) { /* no favicon avaiable */ }
|
||||
}
|
||||
if (!icon) {
|
||||
|
@ -77,10 +76,10 @@ let SyncedTabsInternal = {
|
|||
client: client.id,
|
||||
lastUsed: tab.lastUsed,
|
||||
};
|
||||
}),
|
||||
},
|
||||
|
||||
/* Make a "client" record. Returns a promise for consistency with _makeTab */
|
||||
_makeClient: Task.async(function* (client) {
|
||||
async _makeClient(client) {
|
||||
return {
|
||||
id: client.id,
|
||||
type: "client",
|
||||
|
@ -89,14 +88,14 @@ let SyncedTabsInternal = {
|
|||
lastModified: client.lastModified * 1000, // sec to ms
|
||||
tabs: []
|
||||
};
|
||||
}),
|
||||
},
|
||||
|
||||
_tabMatchesFilter(tab, filter) {
|
||||
let reFilter = new RegExp(escapeRegExp(filter), "i");
|
||||
return tab.url.match(reFilter) || tab.title.match(reFilter);
|
||||
},
|
||||
|
||||
getTabClients: Task.async(function* (filter) {
|
||||
async getTabClients(filter) {
|
||||
log.info("Generating tab list with filter", filter);
|
||||
let result = [];
|
||||
|
||||
|
@ -118,7 +117,7 @@ let SyncedTabsInternal = {
|
|||
if (!Weave.Service.clientsEngine.remoteClientExists(client.id)) {
|
||||
continue;
|
||||
}
|
||||
let clientRepr = yield this._makeClient(client);
|
||||
let clientRepr = await this._makeClient(client);
|
||||
log.debug("Processing client", clientRepr);
|
||||
|
||||
for (let tab of client.tabs) {
|
||||
|
@ -137,7 +136,7 @@ let SyncedTabsInternal = {
|
|||
if (!url || seenURLs.has(url)) {
|
||||
continue;
|
||||
}
|
||||
let tabRepr = yield this._makeTab(client, tab, url, showRemoteIcons);
|
||||
let tabRepr = await this._makeTab(client, tab, url, showRemoteIcons);
|
||||
if (filter && !this._tabMatchesFilter(tabRepr, filter)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -151,7 +150,7 @@ let SyncedTabsInternal = {
|
|||
}
|
||||
log.info(`Final tab list has ${result.length} clients with ${ntabs} tabs.`);
|
||||
return result;
|
||||
}),
|
||||
},
|
||||
|
||||
syncTabs(force) {
|
||||
if (!force) {
|
||||
|
@ -213,6 +212,14 @@ let SyncedTabsInternal = {
|
|||
}
|
||||
},
|
||||
|
||||
get loginFailed() {
|
||||
if (!weaveXPCService.ready) {
|
||||
log.debug("Sync isn't yet ready; assuming the login didn't fail");
|
||||
return false;
|
||||
}
|
||||
return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
|
||||
},
|
||||
|
||||
// Returns true if Sync is configured to Sync tabs, false otherwise
|
||||
get isConfiguredToSyncTabs() {
|
||||
if (!weaveXPCService.ready) {
|
||||
|
|
|
@ -0,0 +1,263 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @typedef {Object} UIState
|
||||
* @property {string} status The Sync/FxA status, see STATUS_* constants.
|
||||
* @property {string} [email] The FxA email configured to log-in with Sync.
|
||||
* @property {string} [displayName] The user's FxA display name.
|
||||
* @property {string} [avatarURL] The user's FxA avatar URL.
|
||||
* @property {Date} [lastSync] The last sync time.
|
||||
* @property {boolean} [syncing] Whether or not we are currently syncing.
|
||||
*/
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["UIState"];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Weave",
|
||||
"resource://services-sync/main.js");
|
||||
|
||||
const TOPICS = [
|
||||
"weave:service:login:change",
|
||||
"weave:service:login:error",
|
||||
"weave:service:ready",
|
||||
"weave:service:sync:start",
|
||||
"weave:service:sync:finish",
|
||||
"weave:service:sync:error",
|
||||
"fxaccounts:onlogin", // Defined in FxAccountsCommon, pulling it is expensive.
|
||||
"fxaccounts:onlogout",
|
||||
"fxaccounts:profilechange",
|
||||
];
|
||||
|
||||
const ON_UPDATE = "sync-ui-state:update"
|
||||
|
||||
const STATUS_NOT_CONFIGURED = "not_configured";
|
||||
const STATUS_LOGIN_FAILED = "login_failed";
|
||||
const STATUS_NOT_VERIFIED = "not_verified";
|
||||
const STATUS_SIGNED_IN = "signed_in";
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
status: STATUS_NOT_CONFIGURED
|
||||
};
|
||||
|
||||
const UIStateInternal = {
|
||||
_initialized: false,
|
||||
_state: null,
|
||||
|
||||
// We keep _syncing out of the state object because we can only track it
|
||||
// using sync events and we can't determine it at any point in time.
|
||||
_syncing: false,
|
||||
|
||||
get state() {
|
||||
if (!this._state) {
|
||||
return DEFAULT_STATE;
|
||||
}
|
||||
return Object.assign({}, this._state, { syncing: this._syncing });
|
||||
},
|
||||
|
||||
isReady() {
|
||||
if (!this._initialized) {
|
||||
this.init();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
init() {
|
||||
this._initialized = true;
|
||||
// Refresh the state in the background.
|
||||
this.refreshState().catch(e => {
|
||||
Cu.reportError(e);
|
||||
});
|
||||
},
|
||||
|
||||
// Used for testing.
|
||||
reset() {
|
||||
this._state = null;
|
||||
this._syncing = false;
|
||||
this._initialized = false;
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "weave:service:sync:start":
|
||||
this.toggleSyncActivity(true);
|
||||
break;
|
||||
case "weave:service:sync:finish":
|
||||
case "weave:service:sync:error":
|
||||
this.toggleSyncActivity(false);
|
||||
break;
|
||||
default:
|
||||
this.refreshState().catch(e => {
|
||||
Cu.reportError(e);
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// Builds a new state from scratch.
|
||||
async refreshState() {
|
||||
this._state = {};
|
||||
await this._refreshFxAState();
|
||||
this._setLastSyncTime(this._state); // We want this in case we change accounts.
|
||||
|
||||
this.notifyStateUpdated();
|
||||
return this.state;
|
||||
},
|
||||
|
||||
// Update the current state with the last sync time/currently syncing status.
|
||||
toggleSyncActivity(syncing) {
|
||||
this._syncing = syncing;
|
||||
this._setLastSyncTime(this._state);
|
||||
|
||||
this.notifyStateUpdated();
|
||||
},
|
||||
|
||||
notifyStateUpdated() {
|
||||
Services.obs.notifyObservers(null, ON_UPDATE);
|
||||
},
|
||||
|
||||
async _refreshFxAState() {
|
||||
let userData = await this._getUserData();
|
||||
this._populateWithUserData(this._state, userData);
|
||||
if (this.state.status != STATUS_SIGNED_IN) {
|
||||
return;
|
||||
}
|
||||
let profile = await this._getProfile();
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
this._populateWithProfile(this._state, profile);
|
||||
},
|
||||
|
||||
_populateWithUserData(state, userData) {
|
||||
let status;
|
||||
if (!userData) {
|
||||
status = STATUS_NOT_CONFIGURED;
|
||||
} else {
|
||||
if (this._loginFailed()) {
|
||||
status = STATUS_LOGIN_FAILED;
|
||||
} else if (!userData.verified) {
|
||||
status = STATUS_NOT_VERIFIED;
|
||||
} else {
|
||||
status = STATUS_SIGNED_IN;
|
||||
}
|
||||
state.email = userData.email;
|
||||
}
|
||||
state.status = status;
|
||||
},
|
||||
|
||||
_populateWithProfile(state, profile) {
|
||||
state.displayName = profile.displayName;
|
||||
state.avatarURL = profile.avatar;
|
||||
},
|
||||
|
||||
async _getUserData() {
|
||||
try {
|
||||
return await this.fxAccounts.getSignedInUser();
|
||||
} catch (e) {
|
||||
// This is most likely in tests, where we quickly log users in and out.
|
||||
// The most likely scenario is a user logged out, so reflect that.
|
||||
// Bug 995134 calls for better errors so we could retry if we were
|
||||
// sure this was the failure reason.
|
||||
Cu.reportError("Error updating FxA account info: " + e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async _getProfile() {
|
||||
try {
|
||||
return await this.fxAccounts.getSignedInUserProfile();
|
||||
} catch (e) {
|
||||
// Not fetching the profile is sad but the FxA logs will already have noise.
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
_setLastSyncTime(state) {
|
||||
if (state.status == UIState.STATUS_SIGNED_IN) {
|
||||
try {
|
||||
state.lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync", null));
|
||||
} catch (_) {
|
||||
state.lastSync = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_loginFailed() {
|
||||
// Referencing Weave.Service will implicitly initialize sync, and we don't
|
||||
// want to force that - so first check if it is ready.
|
||||
let service = Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
if (!service.ready) {
|
||||
return false;
|
||||
}
|
||||
// LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
|
||||
// All other login failures are assumed to be transient and should go
|
||||
// away by themselves, so aren't reflected here.
|
||||
return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
|
||||
},
|
||||
|
||||
set fxAccounts(mockFxAccounts) {
|
||||
delete this.fxAccounts;
|
||||
this.fxAccounts = mockFxAccounts;
|
||||
}
|
||||
};
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(UIStateInternal, "fxAccounts",
|
||||
"resource://gre/modules/FxAccounts.jsm");
|
||||
|
||||
for (let topic of TOPICS) {
|
||||
Services.obs.addObserver(UIStateInternal, topic);
|
||||
}
|
||||
|
||||
this.UIState = {
|
||||
_internal: UIStateInternal,
|
||||
|
||||
ON_UPDATE,
|
||||
|
||||
STATUS_NOT_CONFIGURED,
|
||||
STATUS_LOGIN_FAILED,
|
||||
STATUS_NOT_VERIFIED,
|
||||
STATUS_SIGNED_IN,
|
||||
|
||||
/**
|
||||
* Returns true if the module has been initialized and the state set.
|
||||
* If not, return false and trigger an init in the background.
|
||||
*/
|
||||
isReady() {
|
||||
return this._internal.isReady();
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {UIState} The current Sync/FxA UI State.
|
||||
*/
|
||||
get() {
|
||||
return this._internal.state;
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh the state. Used for testing, don't call this directly since
|
||||
* UIState already listens to Sync/FxA notifications to determine if the state
|
||||
* needs to be refreshed. ON_UPDATE will be fired once the state is refreshed.
|
||||
*
|
||||
* @returns {Promise<UIState>} Resolved once the state is refreshed.
|
||||
*/
|
||||
refresh() {
|
||||
return this._internal.refreshState();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the state of the whole module. Used for testing.
|
||||
*/
|
||||
reset() {
|
||||
this._internal.reset();
|
||||
}
|
||||
};
|
|
@ -11,7 +11,7 @@ var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
|||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/PromiseUtils.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccounts.jsm");
|
||||
Cu.import("resource://services-common/async.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
@ -170,7 +170,7 @@ this.BrowserIDManager.prototype = {
|
|||
this._log.trace("initializeWithCurrentIdentity");
|
||||
|
||||
// Reset the world before we do anything async.
|
||||
this.whenReadyToAuthenticate = Promise.defer();
|
||||
this.whenReadyToAuthenticate = PromiseUtils.defer();
|
||||
this.whenReadyToAuthenticate.promise.catch(err => {
|
||||
this._log.error("Could not authenticate", err);
|
||||
});
|
||||
|
@ -559,7 +559,7 @@ this.BrowserIDManager.prototype = {
|
|||
|
||||
let getToken = assertion => {
|
||||
log.debug("Getting a token");
|
||||
let deferred = Promise.defer();
|
||||
let deferred = PromiseUtils.defer();
|
||||
let cb = function(err, token) {
|
||||
if (err) {
|
||||
return deferred.reject(err);
|
||||
|
|
|
@ -17,7 +17,6 @@ Cu.import("resource://services-sync/constants.js");
|
|||
Cu.import("resource://services-sync/engines.js");
|
||||
Cu.import("resource://services-sync/record.js");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "BookmarkValidator",
|
||||
"resource://services-sync/bookmark_validator.js");
|
||||
|
@ -438,14 +437,14 @@ BookmarksEngine.prototype = {
|
|||
SyncEngine.prototype._syncStartup.call(this);
|
||||
|
||||
let cb = Async.makeSpinningCallback();
|
||||
Task.spawn(function* () {
|
||||
(async () => {
|
||||
// For first-syncs, make a backup for the user to restore
|
||||
if (this.lastSync == 0) {
|
||||
this._log.debug("Bookmarks backup starting.");
|
||||
yield PlacesBackups.create(null, true);
|
||||
await PlacesBackups.create(null, true);
|
||||
this._log.debug("Bookmarks backup done.");
|
||||
}
|
||||
}.bind(this)).then(
|
||||
})().then(
|
||||
cb, ex => {
|
||||
// Failure to create a backup is somewhat bad, but probably not bad
|
||||
// enough to prevent syncing of bookmarks - so just log the error and
|
||||
|
@ -781,11 +780,11 @@ BookmarksStore.prototype = {
|
|||
//
|
||||
// See `PlacesSyncUtils.bookmarks.remove` for the implementation.
|
||||
|
||||
deletePending: Task.async(function* deletePending() {
|
||||
let guidsToUpdate = yield PlacesSyncUtils.bookmarks.remove([...this._itemsToDelete]);
|
||||
async deletePending() {
|
||||
let guidsToUpdate = await PlacesSyncUtils.bookmarks.remove([...this._itemsToDelete]);
|
||||
this.clearPendingDeletions();
|
||||
return guidsToUpdate;
|
||||
}),
|
||||
},
|
||||
|
||||
clearPendingDeletions() {
|
||||
this._itemsToDelete.clear();
|
||||
|
@ -850,11 +849,11 @@ BookmarksStore.prototype = {
|
|||
|
||||
wipe: function BStore_wipe() {
|
||||
this.clearPendingDeletions();
|
||||
Async.promiseSpinningly(Task.spawn(function* () {
|
||||
Async.promiseSpinningly((async () => {
|
||||
// Save a backup before clearing out all bookmarks.
|
||||
yield PlacesBackups.create(null, true);
|
||||
yield PlacesSyncUtils.bookmarks.wipe();
|
||||
}));
|
||||
await PlacesBackups.create(null, true);
|
||||
await PlacesSyncUtils.bookmarks.wipe();
|
||||
})());
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -927,8 +926,8 @@ BookmarksTracker.prototype = {
|
|||
|
||||
// Migrates tracker entries from the old JSON-based tracker to Places. This
|
||||
// is called the first time we start tracking changes.
|
||||
_migrateOldEntries: Task.async(function* () {
|
||||
let existingIDs = yield Utils.jsonLoad("changes/" + this.file, this);
|
||||
async _migrateOldEntries() {
|
||||
let existingIDs = await Utils.jsonLoad("changes/" + this.file, this);
|
||||
if (existingIDs === null) {
|
||||
// If the tracker file doesn't exist, we don't need to migrate, even if
|
||||
// the engine is enabled. It's possible we're upgrading before the first
|
||||
|
@ -962,9 +961,9 @@ BookmarksTracker.prototype = {
|
|||
modified: timestamp * 1000,
|
||||
});
|
||||
}
|
||||
yield PlacesSyncUtils.bookmarks.migrateOldTrackerEntries(entries);
|
||||
await PlacesSyncUtils.bookmarks.migrateOldTrackerEntries(entries);
|
||||
return Utils.jsonRemove("changes/" + this.file, this);
|
||||
}),
|
||||
},
|
||||
|
||||
_needsMigration() {
|
||||
return this.engine && this.engineIsEnabled() && this.engine.lastSync > 0;
|
||||
|
|
|
@ -36,6 +36,7 @@ EXTRA_JS_MODULES['services-sync'] += [
|
|||
'modules/status.js',
|
||||
'modules/SyncedTabs.jsm',
|
||||
'modules/telemetry.js',
|
||||
'modules/UIState.jsm',
|
||||
'modules/util.js',
|
||||
]
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ Cu.import("resource://services-sync/engines.js");
|
|||
Cu.import("resource://services-sync/service.js");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://testing-common/PlacesTestUtils.jsm");
|
||||
Cu.import("resource:///modules/PlacesUIUtils.jsm");
|
||||
|
||||
|
|
|
@ -0,0 +1,260 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// ================================================
|
||||
// Load mocking/stubbing library, sinon
|
||||
// docs: http://sinonjs.org/docs/
|
||||
/* global sinon */
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
let window = {
|
||||
document: {},
|
||||
location: {},
|
||||
setTimeout,
|
||||
setInterval,
|
||||
clearTimeout,
|
||||
clearInterval,
|
||||
};
|
||||
let self = window;
|
||||
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
|
||||
loader.loadSubScript("resource://testing-common/sinon-1.16.1.js");
|
||||
// ================================================
|
||||
|
||||
Cu.import("resource://services-sync/UIState.jsm");
|
||||
|
||||
const UIStateInternal = UIState._internal;
|
||||
|
||||
add_task(async function test_isReady() {
|
||||
UIState.reset();
|
||||
|
||||
let refreshState = sinon.spy(UIStateInternal, "refreshState");
|
||||
|
||||
// On the first call, returns false and triggers a refresh of the state
|
||||
ok(!UIState.isReady());
|
||||
ok(refreshState.calledOnce);
|
||||
refreshState.reset();
|
||||
|
||||
// On subsequent calls, only return true
|
||||
ok(UIState.isReady());
|
||||
ok(!refreshState.called);
|
||||
|
||||
refreshState.restore();
|
||||
});
|
||||
|
||||
add_task(async function test_refreshState_signedin() {
|
||||
UIState.reset();
|
||||
const fxAccountsOrig = UIStateInternal.fxAccounts;
|
||||
|
||||
const now = new Date().toString();
|
||||
Services.prefs.setCharPref("services.sync.lastSync", now);
|
||||
UIStateInternal.syncing = false;
|
||||
|
||||
UIStateInternal.fxAccounts = {
|
||||
getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
|
||||
getSignedInUserProfile: () => Promise.resolve({ displayName: "Foo Bar", avatar: "https://foo/bar" })
|
||||
}
|
||||
|
||||
let state = await UIState.refresh();
|
||||
|
||||
equal(state.status, UIState.STATUS_SIGNED_IN);
|
||||
equal(state.email, "foo@bar.com");
|
||||
equal(state.displayName, "Foo Bar");
|
||||
equal(state.avatarURL, "https://foo/bar");
|
||||
equal(state.lastSync, now);
|
||||
equal(state.syncing, false);
|
||||
|
||||
UIStateInternal.fxAccounts = fxAccountsOrig;
|
||||
});
|
||||
|
||||
add_task(async function test_refreshState_signedin_profile_unavailable() {
|
||||
UIState.reset();
|
||||
const fxAccountsOrig = UIStateInternal.fxAccounts;
|
||||
|
||||
const now = new Date().toString();
|
||||
Services.prefs.setCharPref("services.sync.lastSync", now);
|
||||
UIStateInternal.syncing = false;
|
||||
|
||||
UIStateInternal.fxAccounts = {
|
||||
getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
|
||||
getSignedInUserProfile: () => Promise.reject(new Error("Profile unavailable"))
|
||||
}
|
||||
|
||||
let state = await UIState.refresh();
|
||||
|
||||
equal(state.status, UIState.STATUS_SIGNED_IN);
|
||||
equal(state.email, "foo@bar.com");
|
||||
equal(state.displayName, undefined);
|
||||
equal(state.avatarURL, undefined);
|
||||
equal(state.lastSync, now);
|
||||
equal(state.syncing, false);
|
||||
|
||||
UIStateInternal.fxAccounts = fxAccountsOrig;
|
||||
});
|
||||
|
||||
add_task(async function test_refreshState_unconfigured() {
|
||||
UIState.reset();
|
||||
const fxAccountsOrig = UIStateInternal.fxAccounts;
|
||||
|
||||
let getSignedInUserProfile = sinon.spy();
|
||||
UIStateInternal.fxAccounts = {
|
||||
getSignedInUser: () => Promise.resolve(null),
|
||||
getSignedInUserProfile
|
||||
}
|
||||
|
||||
let state = await UIState.refresh();
|
||||
|
||||
equal(state.status, UIState.STATUS_NOT_CONFIGURED);
|
||||
equal(state.email, undefined);
|
||||
equal(state.displayName, undefined);
|
||||
equal(state.avatarURL, undefined);
|
||||
equal(state.lastSync, undefined);
|
||||
|
||||
ok(!getSignedInUserProfile.called);
|
||||
|
||||
UIStateInternal.fxAccounts = fxAccountsOrig;
|
||||
});
|
||||
|
||||
add_task(async function test_refreshState_unverified() {
|
||||
UIState.reset();
|
||||
const fxAccountsOrig = UIStateInternal.fxAccounts;
|
||||
|
||||
let getSignedInUserProfile = sinon.spy();
|
||||
UIStateInternal.fxAccounts = {
|
||||
getSignedInUser: () => Promise.resolve({ verified: false, email: "foo@bar.com" }),
|
||||
getSignedInUserProfile
|
||||
}
|
||||
|
||||
let state = await UIState.refresh();
|
||||
|
||||
equal(state.status, UIState.STATUS_NOT_VERIFIED);
|
||||
equal(state.email, "foo@bar.com");
|
||||
equal(state.displayName, undefined);
|
||||
equal(state.avatarURL, undefined);
|
||||
equal(state.lastSync, undefined);
|
||||
|
||||
ok(!getSignedInUserProfile.called);
|
||||
|
||||
UIStateInternal.fxAccounts = fxAccountsOrig;
|
||||
});
|
||||
|
||||
add_task(async function test_refreshState_loginFailed() {
|
||||
UIState.reset();
|
||||
const fxAccountsOrig = UIStateInternal.fxAccounts;
|
||||
|
||||
let loginFailed = sinon.stub(UIStateInternal, "_loginFailed");
|
||||
loginFailed.returns(true);
|
||||
|
||||
let getSignedInUserProfile = sinon.spy();
|
||||
UIStateInternal.fxAccounts = {
|
||||
getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
|
||||
getSignedInUserProfile
|
||||
}
|
||||
|
||||
let state = await UIState.refresh();
|
||||
|
||||
equal(state.status, UIState.STATUS_LOGIN_FAILED);
|
||||
equal(state.email, "foo@bar.com");
|
||||
equal(state.displayName, undefined);
|
||||
equal(state.avatarURL, undefined);
|
||||
equal(state.lastSync, undefined);
|
||||
|
||||
ok(!getSignedInUserProfile.called);
|
||||
|
||||
loginFailed.restore();
|
||||
UIStateInternal.fxAccounts = fxAccountsOrig;
|
||||
});
|
||||
|
||||
add_task(async function test_observer_refreshState() {
|
||||
let refreshState = sinon.spy(UIStateInternal, "refreshState");
|
||||
|
||||
let shouldRefresh = ["weave:service:login:change", "weave:service:login:error",
|
||||
"weave:service:ready", "fxaccounts:onlogin",
|
||||
"fxaccounts:onlogout", "fxaccounts:profilechange"];
|
||||
|
||||
for (let topic of shouldRefresh) {
|
||||
let uiUpdateObserved = observeUIUpdate();
|
||||
Services.obs.notifyObservers(null, topic);
|
||||
await uiUpdateObserved;
|
||||
ok(refreshState.calledOnce);
|
||||
refreshState.reset();
|
||||
}
|
||||
|
||||
refreshState.restore();
|
||||
});
|
||||
|
||||
// Drive the UIState in a configured state.
|
||||
async function configureUIState(syncing, lastSync = new Date()) {
|
||||
UIState.reset();
|
||||
const fxAccountsOrig = UIStateInternal.fxAccounts;
|
||||
|
||||
UIStateInternal._syncing = syncing;
|
||||
Services.prefs.setCharPref("services.sync.lastSync", lastSync.toString());
|
||||
|
||||
UIStateInternal.fxAccounts = {
|
||||
getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
|
||||
getSignedInUserProfile: () => Promise.resolve({ displayName: "Foo Bar", avatar: "https://foo/bar" })
|
||||
}
|
||||
await UIState.refresh();
|
||||
UIStateInternal.fxAccounts = fxAccountsOrig;
|
||||
}
|
||||
|
||||
add_task(async function test_syncStarted() {
|
||||
await configureUIState(false);
|
||||
|
||||
const oldState = Object.assign({}, UIState.get());
|
||||
ok(!oldState.syncing);
|
||||
|
||||
let uiUpdateObserved = observeUIUpdate();
|
||||
Services.obs.notifyObservers(null, "weave:service:sync:start");
|
||||
await uiUpdateObserved;
|
||||
|
||||
const newState = Object.assign({}, UIState.get());
|
||||
ok(newState.syncing);
|
||||
});
|
||||
|
||||
add_task(async function test_syncFinished() {
|
||||
let yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
await configureUIState(true, yesterday);
|
||||
|
||||
const oldState = Object.assign({}, UIState.get());
|
||||
ok(oldState.syncing);
|
||||
|
||||
let uiUpdateObserved = observeUIUpdate();
|
||||
Services.prefs.setCharPref("services.sync.lastSync", new Date().toString());
|
||||
Services.obs.notifyObservers(null, "weave:service:sync:finish");
|
||||
await uiUpdateObserved;
|
||||
|
||||
const newState = Object.assign({}, UIState.get());
|
||||
ok(!newState.syncing);
|
||||
ok(new Date(newState.lastSync) > new Date(oldState.lastSync));
|
||||
});
|
||||
|
||||
add_task(async function test_syncError() {
|
||||
let yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
await configureUIState(true, yesterday);
|
||||
|
||||
const oldState = Object.assign({}, UIState.get());
|
||||
ok(oldState.syncing);
|
||||
|
||||
let uiUpdateObserved = observeUIUpdate();
|
||||
Services.obs.notifyObservers(null, "weave:service:sync:error");
|
||||
await uiUpdateObserved;
|
||||
|
||||
const newState = Object.assign({}, UIState.get());
|
||||
ok(!newState.syncing);
|
||||
deepEqual(newState.lastSync, oldState.lastSync);
|
||||
});
|
||||
|
||||
function observeUIUpdate() {
|
||||
return new Promise(resolve => {
|
||||
let obs = (aSubject, aTopic, aData) => {
|
||||
Services.obs.removeObserver(obs, aTopic);
|
||||
const state = UIState.get();
|
||||
resolve(state);
|
||||
}
|
||||
Services.obs.addObserver(obs, UIState.ON_UPDATE);
|
||||
});
|
||||
}
|
|
@ -184,3 +184,5 @@ support-files = prefs_test_prefs_store.js
|
|||
|
||||
[test_telemetry.js]
|
||||
requesttimeoutfactor = 4
|
||||
|
||||
[test_uistate.js]
|
||||
|
|
|
@ -2799,6 +2799,7 @@ name = "style_tests"
|
|||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"app_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cssparser 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"euclid 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"html5ever 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -3193,7 +3194,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "webrender"
|
||||
version = "0.36.0"
|
||||
source = "git+https://github.com/servo/webrender#964df2fa00f330daf4f233669e37133f93113792"
|
||||
source = "git+https://github.com/servo/webrender#e44a079266a181ddaae2fd27298cec1094b54b7e"
|
||||
dependencies = [
|
||||
"app_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bincode 1.0.0-alpha6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -3222,7 +3223,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "webrender_traits"
|
||||
version = "0.36.0"
|
||||
source = "git+https://github.com/servo/webrender#964df2fa00f330daf4f233669e37133f93113792"
|
||||
source = "git+https://github.com/servo/webrender#e44a079266a181ddaae2fd27298cec1094b54b7e"
|
||||
dependencies = [
|
||||
"app_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bincode 1.0.0-alpha6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
|
|
@ -35,7 +35,7 @@ impl ExtendableMessageEvent {
|
|||
-> Root<ExtendableMessageEvent> {
|
||||
let ev = box ExtendableMessageEvent {
|
||||
event: ExtendableEvent::new_inherited(),
|
||||
data: Heap::new(data.get()),
|
||||
data: Heap::default(),
|
||||
origin: origin,
|
||||
lastEventId: lastEventId,
|
||||
};
|
||||
|
@ -44,6 +44,8 @@ impl ExtendableMessageEvent {
|
|||
let event = ev.upcast::<Event>();
|
||||
event.init_event(type_, bubbles, cancelable);
|
||||
}
|
||||
ev.data.set(data.get());
|
||||
|
||||
ev
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ use dom_struct::dom_struct;
|
|||
use js::jsapi::{Heap, JSContext, JSObject};
|
||||
use js::typedarray::{Float64Array, CreateWith};
|
||||
use std::cell::Cell;
|
||||
use std::ptr;
|
||||
use webvr_traits::{WebVRGamepadData, WebVRGamepadHand, WebVRGamepadState};
|
||||
|
||||
#[dom_struct]
|
||||
|
@ -47,7 +46,6 @@ impl Gamepad {
|
|||
connected: bool,
|
||||
timestamp: f64,
|
||||
mapping_type: String,
|
||||
axes: *mut JSObject,
|
||||
buttons: &GamepadButtonList,
|
||||
pose: Option<&VRPose>,
|
||||
hand: WebVRGamepadHand,
|
||||
|
@ -60,7 +58,7 @@ impl Gamepad {
|
|||
connected: Cell::new(connected),
|
||||
timestamp: Cell::new(timestamp),
|
||||
mapping_type: mapping_type,
|
||||
axes: Heap::new(axes),
|
||||
axes: Heap::default(),
|
||||
buttons: JS::from_ref(buttons),
|
||||
pose: pose.map(JS::from_ref),
|
||||
hand: hand,
|
||||
|
@ -75,28 +73,24 @@ impl Gamepad {
|
|||
state: &WebVRGamepadState) -> Root<Gamepad> {
|
||||
let buttons = GamepadButtonList::new_from_vr(&global, &state.buttons);
|
||||
let pose = VRPose::new(&global, &state.pose);
|
||||
let cx = global.get_cx();
|
||||
rooted!(in (cx) let mut axes = ptr::null_mut());
|
||||
|
||||
let gamepad = reflect_dom_object(box Gamepad::new_inherited(state.gamepad_id,
|
||||
data.name.clone(),
|
||||
index,
|
||||
state.connected,
|
||||
state.timestamp,
|
||||
"".into(),
|
||||
&buttons,
|
||||
Some(&pose),
|
||||
data.hand.clone(),
|
||||
data.display_id),
|
||||
global,
|
||||
GamepadBinding::Wrap);
|
||||
unsafe {
|
||||
let _ = Float64Array::create(cx,
|
||||
CreateWith::Slice(&state.axes),
|
||||
axes.handle_mut());
|
||||
let _ = Float64Array::create(global.get_cx(), CreateWith::Slice(&state.axes), gamepad.axes.handle_mut());
|
||||
}
|
||||
|
||||
reflect_dom_object(box Gamepad::new_inherited(state.gamepad_id,
|
||||
data.name.clone(),
|
||||
index,
|
||||
state.connected,
|
||||
state.timestamp,
|
||||
"".into(),
|
||||
axes.get(),
|
||||
&buttons,
|
||||
Some(&pose),
|
||||
data.hand.clone(),
|
||||
data.display_id),
|
||||
global,
|
||||
GamepadBinding::Wrap)
|
||||
|
||||
gamepad
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,11 +41,14 @@ impl MessageEvent {
|
|||
lastEventId: DOMString) -> Root<MessageEvent> {
|
||||
let ev = box MessageEvent {
|
||||
event: Event::new_inherited(),
|
||||
data: Heap::new(data.get()),
|
||||
data: Heap::default(),
|
||||
origin: origin,
|
||||
lastEventId: lastEventId,
|
||||
};
|
||||
reflect_dom_object(ev, global, MessageEventBinding::Wrap)
|
||||
let ev = reflect_dom_object(ev, global, MessageEventBinding::Wrap);
|
||||
ev.data.set(data.get());
|
||||
|
||||
ev
|
||||
}
|
||||
|
||||
pub fn new(global: &GlobalScope, type_: Atom,
|
||||
|
|
|
@ -28,29 +28,29 @@ pub struct VREyeParameters {
|
|||
unsafe_no_jsmanaged_fields!(WebVREyeParameters);
|
||||
|
||||
impl VREyeParameters {
|
||||
#[allow(unsafe_code)]
|
||||
#[allow(unrooted_must_root)]
|
||||
fn new_inherited(parameters: WebVREyeParameters, global: &GlobalScope) -> VREyeParameters {
|
||||
let fov = VRFieldOfView::new(&global, parameters.field_of_view.clone());
|
||||
let result = VREyeParameters {
|
||||
fn new_inherited(parameters: WebVREyeParameters, fov: &VRFieldOfView) -> VREyeParameters {
|
||||
VREyeParameters {
|
||||
reflector_: Reflector::new(),
|
||||
parameters: DOMRefCell::new(parameters),
|
||||
offset: Heap::default(),
|
||||
fov: JS::from_ref(&*fov)
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let _ = Float32Array::create(global.get_cx(),
|
||||
CreateWith::Slice(&result.parameters.borrow().offset),
|
||||
result.offset.handle_mut());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
pub fn new(parameters: WebVREyeParameters, global: &GlobalScope) -> Root<VREyeParameters> {
|
||||
reflect_dom_object(box VREyeParameters::new_inherited(parameters, global),
|
||||
global,
|
||||
VREyeParametersBinding::Wrap)
|
||||
let fov = VRFieldOfView::new(&global, parameters.field_of_view.clone());
|
||||
|
||||
let eye_parameters = reflect_dom_object(box VREyeParameters::new_inherited(parameters, &fov),
|
||||
global,
|
||||
VREyeParametersBinding::Wrap);
|
||||
unsafe {
|
||||
let _ = Float32Array::create(global.get_cx(),
|
||||
CreateWith::Slice(&eye_parameters.parameters.borrow().offset),
|
||||
eye_parameters.offset.handle_mut());
|
||||
}
|
||||
|
||||
eye_parameters
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,16 +31,8 @@ pub struct VRFrameData {
|
|||
}
|
||||
|
||||
impl VRFrameData {
|
||||
#[allow(unsafe_code)]
|
||||
#[allow(unrooted_must_root)]
|
||||
fn new(global: &GlobalScope) -> Root<VRFrameData> {
|
||||
let matrix = [1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0f32];
|
||||
let pose = VRPose::new(&global, &Default::default());
|
||||
|
||||
let framedata = VRFrameData {
|
||||
fn new_inherited(pose: &VRPose) -> VRFrameData {
|
||||
VRFrameData {
|
||||
reflector_: Reflector::new(),
|
||||
left_proj: Heap::default(),
|
||||
left_view: Heap::default(),
|
||||
|
@ -49,23 +41,25 @@ impl VRFrameData {
|
|||
pose: JS::from_ref(&*pose),
|
||||
timestamp: Cell::new(0.0),
|
||||
first_timestamp: Cell::new(0.0)
|
||||
};
|
||||
|
||||
let root = reflect_dom_object(box framedata,
|
||||
global,
|
||||
VRFrameDataBinding::Wrap);
|
||||
|
||||
unsafe {
|
||||
let ref framedata = *root;
|
||||
let _ = Float32Array::create(global.get_cx(), CreateWith::Slice(&matrix),
|
||||
framedata.left_proj.handle_mut());
|
||||
let _ = Float32Array::create(global.get_cx(), CreateWith::Slice(&matrix),
|
||||
framedata.left_view.handle_mut());
|
||||
let _ = Float32Array::create(global.get_cx(), CreateWith::Slice(&matrix),
|
||||
framedata.right_proj.handle_mut());
|
||||
let _ = Float32Array::create(global.get_cx(), CreateWith::Slice(&matrix),
|
||||
framedata.right_view.handle_mut());
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn new(global: &GlobalScope) -> Root<VRFrameData> {
|
||||
let matrix = [1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0f32];
|
||||
let pose = VRPose::new(&global, &Default::default());
|
||||
|
||||
let root = reflect_dom_object(box VRFrameData::new_inherited(&pose),
|
||||
global,
|
||||
VRFrameDataBinding::Wrap);
|
||||
let cx = global.get_cx();
|
||||
create_typed_array(cx, &matrix, &root.left_proj);
|
||||
create_typed_array(cx, &matrix, &root.left_view);
|
||||
create_typed_array(cx, &matrix, &root.right_proj);
|
||||
create_typed_array(cx, &matrix, &root.right_view);
|
||||
|
||||
root
|
||||
}
|
||||
|
@ -76,6 +70,13 @@ impl VRFrameData {
|
|||
}
|
||||
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn create_typed_array(cx: *mut JSContext, src: &[f32], dst: &Heap<*mut JSObject>) {
|
||||
unsafe {
|
||||
let _ = Float32Array::create(cx, CreateWith::Slice(src), dst.handle_mut());
|
||||
}
|
||||
}
|
||||
|
||||
impl VRFrameData {
|
||||
#[allow(unsafe_code)]
|
||||
pub fn update(&self, data: &WebVRFrameData) {
|
||||
|
|
|
@ -32,9 +32,7 @@ unsafe fn update_or_create_typed_array(cx: *mut JSContext,
|
|||
match src {
|
||||
Some(data) => {
|
||||
if dst.get().is_null() {
|
||||
rooted!(in (cx) let mut array = ptr::null_mut());
|
||||
let _ = Float32Array::create(cx, CreateWith::Slice(data), array.handle_mut());
|
||||
(*dst).set(array.get());
|
||||
let _ = Float32Array::create(cx, CreateWith::Slice(data), dst.handle_mut());
|
||||
} else {
|
||||
typedarray!(in(cx) let array: Float32Array = dst.get());
|
||||
if let Ok(mut array) = array {
|
||||
|
|
|
@ -26,28 +26,28 @@ pub struct VRStageParameters {
|
|||
unsafe_no_jsmanaged_fields!(WebVRStageParameters);
|
||||
|
||||
impl VRStageParameters {
|
||||
#[allow(unsafe_code)]
|
||||
#[allow(unrooted_must_root)]
|
||||
fn new_inherited(parameters: WebVRStageParameters, global: &GlobalScope) -> VRStageParameters {
|
||||
let stage = VRStageParameters {
|
||||
fn new_inherited(parameters: WebVRStageParameters) -> VRStageParameters {
|
||||
VRStageParameters {
|
||||
reflector_: Reflector::new(),
|
||||
parameters: DOMRefCell::new(parameters),
|
||||
transform: Heap::default()
|
||||
};
|
||||
// XXX unsound!
|
||||
unsafe {
|
||||
let _ = Float32Array::create(global.get_cx(),
|
||||
CreateWith::Slice(&stage.parameters.borrow().sitting_to_standing_transform),
|
||||
stage.transform.handle_mut());
|
||||
}
|
||||
|
||||
stage
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
pub fn new(parameters: WebVRStageParameters, global: &GlobalScope) -> Root<VRStageParameters> {
|
||||
reflect_dom_object(box VRStageParameters::new_inherited(parameters, global),
|
||||
global,
|
||||
VRStageParametersBinding::Wrap)
|
||||
let cx = global.get_cx();
|
||||
let stage_parameters = reflect_dom_object(box VRStageParameters::new_inherited(parameters),
|
||||
global,
|
||||
VRStageParametersBinding::Wrap);
|
||||
unsafe {
|
||||
let source = &stage_parameters.parameters.borrow().sitting_to_standing_transform;
|
||||
let _ = Float32Array::create(cx,
|
||||
CreateWith::Slice(source),
|
||||
stage_parameters.transform.handle_mut());
|
||||
}
|
||||
|
||||
stage_parameters
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
|
|
|
@ -921,13 +921,13 @@ fn static_assert() {
|
|||
need_clone=True) %>
|
||||
% endfor
|
||||
|
||||
pub fn set_border_image_source(&mut self, v: longhands::border_image_source::computed_value::T) {
|
||||
pub fn set_border_image_source(&mut self, image: longhands::border_image_source::computed_value::T) {
|
||||
unsafe {
|
||||
// Prevent leaking of the last elements we did set
|
||||
Gecko_SetNullImageValue(&mut self.gecko.mBorderImageSource);
|
||||
}
|
||||
|
||||
if let Some(image) = v.0 {
|
||||
if let Some(image) = image.0 {
|
||||
self.gecko.mBorderImageSource.set(image, &mut false)
|
||||
}
|
||||
}
|
||||
|
@ -1296,12 +1296,47 @@ fn static_assert() {
|
|||
skip_font_longhands = """font-family font-size font-size-adjust font-weight
|
||||
font-synthesis -x-lang font-variant-alternates
|
||||
font-variant-east-asian font-variant-ligatures
|
||||
font-variant-numeric font-language-override"""
|
||||
font-variant-numeric font-language-override
|
||||
font-feature-settings"""
|
||||
%>
|
||||
<%self:impl_trait style_struct_name="Font"
|
||||
skip_longhands="${skip_font_longhands}"
|
||||
skip_additionals="*">
|
||||
|
||||
pub fn set_font_feature_settings(&mut self, v: longhands::font_feature_settings::computed_value::T) {
|
||||
use properties::longhands::font_feature_settings::computed_value::T;
|
||||
|
||||
let current_settings = &mut self.gecko.mFont.fontFeatureSettings;
|
||||
current_settings.clear_pod();
|
||||
|
||||
match v {
|
||||
T::Normal => unsafe { current_settings.set_len_pod(0) },
|
||||
|
||||
T::Tag(feature_settings) => {
|
||||
unsafe { current_settings.set_len_pod(feature_settings.len() as u32) };
|
||||
|
||||
for (current, feature) in current_settings.iter_mut().zip(feature_settings) {
|
||||
current.mTag = feature.tag;
|
||||
current.mValue = feature.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn copy_font_feature_settings_from(&mut self, other: &Self ) {
|
||||
let current_settings = &mut self.gecko.mFont.fontFeatureSettings;
|
||||
let feature_settings = &other.gecko.mFont.fontFeatureSettings;
|
||||
let settings_length = feature_settings.len() as u32;
|
||||
|
||||
current_settings.clear_pod();
|
||||
unsafe { current_settings.set_len_pod(settings_length) };
|
||||
|
||||
for (current, feature) in current_settings.iter_mut().zip(feature_settings.iter()) {
|
||||
current.mTag = feature.mTag;
|
||||
current.mValue = feature.mValue;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_font_family(&mut self, v: longhands::font_family::computed_value::T) {
|
||||
use properties::longhands::font_family::computed_value::FontFamily;
|
||||
use gecko_bindings::structs::FontFamilyType;
|
||||
|
@ -2756,18 +2791,9 @@ fn static_assert() {
|
|||
|
||||
for (image, geckoimage) in images.0.into_iter().zip(self.gecko.${image_layers_field}
|
||||
.mLayers.iter_mut()) {
|
||||
% if shorthand == "background":
|
||||
if let Some(image) = image.0 {
|
||||
geckoimage.mImage.set(image, cacheable)
|
||||
}
|
||||
% else:
|
||||
use properties::longhands::mask_image::single_value::computed_value::T;
|
||||
match image {
|
||||
T::Image(image) => geckoimage.mImage.set(image, cacheable),
|
||||
_ => ()
|
||||
}
|
||||
% endif
|
||||
|
||||
if let Some(image) = image.0 {
|
||||
geckoimage.mImage.set(image, cacheable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -309,7 +309,7 @@
|
|||
computed,
|
||||
inherited_style.get_font());
|
||||
% else:
|
||||
% if property.has_uncacheable_values:
|
||||
% if property.has_uncacheable_values == "True":
|
||||
context.mutate_style().mutate_${data.current_style_struct.name_lower}()
|
||||
.set_${property.ident}(computed, cacheable ${maybe_wm});
|
||||
% else:
|
||||
|
|
|
@ -12,82 +12,13 @@ ${helpers.predefined_type("background-color", "CSSColor",
|
|||
spec="https://drafts.csswg.org/css-backgrounds/#background-color",
|
||||
animation_value_type="IntermediateColor", complex_color=True)}
|
||||
|
||||
<%helpers:vector_longhand name="background-image" animation_value_type="none"
|
||||
spec="https://drafts.csswg.org/css-backgrounds/#the-background-image"
|
||||
has_uncacheable_values="${product == 'gecko'}">
|
||||
use std::fmt;
|
||||
use style_traits::ToCss;
|
||||
use values::HasViewportPercentage;
|
||||
use values::specified::Image;
|
||||
|
||||
pub mod computed_value {
|
||||
use values::computed;
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
|
||||
pub struct T(pub Option<computed::Image>);
|
||||
}
|
||||
|
||||
impl ToCss for computed_value::T {
|
||||
fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
|
||||
match self.0 {
|
||||
None => dest.write_str("none"),
|
||||
Some(ref image) => image.to_css(dest),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
no_viewport_percentage!(SpecifiedValue);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
|
||||
pub struct SpecifiedValue(pub Option<Image>);
|
||||
|
||||
impl ToCss for SpecifiedValue {
|
||||
fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
|
||||
match *self {
|
||||
SpecifiedValue(Some(ref image)) => image.to_css(dest),
|
||||
SpecifiedValue(None) => dest.write_str("none"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_initial_value() -> computed_value::T {
|
||||
computed_value::T(None)
|
||||
}
|
||||
#[inline]
|
||||
pub fn get_initial_specified_value() -> SpecifiedValue {
|
||||
SpecifiedValue(None)
|
||||
}
|
||||
pub fn parse(context: &ParserContext, input: &mut Parser) -> Result<SpecifiedValue, ()> {
|
||||
if input.try(|input| input.expect_ident_matching("none")).is_ok() {
|
||||
Ok(SpecifiedValue(None))
|
||||
} else {
|
||||
Ok(SpecifiedValue(Some(try!(Image::parse(context, input)))))
|
||||
}
|
||||
}
|
||||
impl ToComputedValue for SpecifiedValue {
|
||||
type ComputedValue = computed_value::T;
|
||||
|
||||
#[inline]
|
||||
fn to_computed_value(&self, context: &Context) -> computed_value::T {
|
||||
match *self {
|
||||
SpecifiedValue(None) => computed_value::T(None),
|
||||
SpecifiedValue(Some(ref image)) =>
|
||||
computed_value::T(Some(image.to_computed_value(context))),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn from_computed_value(computed: &computed_value::T) -> Self {
|
||||
match *computed {
|
||||
computed_value::T(None) => SpecifiedValue(None),
|
||||
computed_value::T(Some(ref image)) =>
|
||||
SpecifiedValue(Some(ToComputedValue::from_computed_value(image))),
|
||||
}
|
||||
}
|
||||
}
|
||||
</%helpers:vector_longhand>
|
||||
${helpers.predefined_type("background-image", "LayerImage",
|
||||
initial_value="computed_value::T(None)",
|
||||
initial_specified_value="SpecifiedValue(None)",
|
||||
spec="https://drafts.csswg.org/css-backgrounds/#the-background-image",
|
||||
vector="True",
|
||||
animation_value_type="none",
|
||||
has_uncacheable_values="True" if product == "gecko" else "False")}
|
||||
|
||||
<%helpers:predefined_type name="background-position-x" type="position::HorizontalPosition"
|
||||
initial_value="computed::position::HorizontalPosition::zero()"
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче