diff --git a/application/basilisk/app/profile/basilisk.js b/application/basilisk/app/profile/basilisk.js index 4700eac449..24f1c582cc 100644 --- a/application/basilisk/app/profile/basilisk.js +++ b/application/basilisk/app/profile/basilisk.js @@ -1293,6 +1293,17 @@ pref("privacy.trackingprotection.ui.enabled", true); pref("privacy.trackingprotection.ui.enabled", false); #endif +// Enable Contextual Identity Containers +#ifdef NIGHTLY_BUILD +pref("privacy.userContext.enabled", true); +pref("privacy.userContext.ui.enabled", true); +pref("privacy.usercontext.about_newtab_segregation.enabled", true); +#else +pref("privacy.userContext.enabled", false); +pref("privacy.userContext.ui.enabled", false); +pref("privacy.usercontext.about_newtab_segregation.enabled", false); +#endif + #ifndef RELEASE_OR_BETA // At the moment, autostart.2 is used, while autostart.1 is unused. // We leave it here set to false to reset users' defaults and allow diff --git a/application/basilisk/base/content/browser-context.inc b/application/basilisk/base/content/browser-context.inc index 2f6b19da07..36e0478af6 100644 --- a/application/basilisk/base/content/browser-context.inc +++ b/application/basilisk/base/content/browser-context.inc @@ -51,11 +51,23 @@ label="&openLinkCmdInCurrent.label;" accesskey="&openLinkCmdInCurrent.accesskey;" oncommand="gContextMenu.openLinkInCurrent();"/> -# label is dynamically set. +# label and data-usercontextid are dynamically set. + + data-usercontextid="0" + oncommand="gContextMenu.openLinkInTab(event);"/> + + - + + = 3) { let referrerURI = window.arguments[2]; if (typeof(referrerURI) == "string") { @@ -1153,11 +1184,13 @@ var gBrowserInit = { } let referrerPolicy = (window.arguments[5] != undefined ? window.arguments[5] : Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT); + let userContextId = (window.arguments[6] != undefined ? + window.arguments[6] : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID); loadURI(uriToLoad, referrerURI, window.arguments[3] || null, - window.arguments[4] || false, referrerPolicy, + window.arguments[4] || false, referrerPolicy, userContextId, // pass the origin principal (if any) and force its use to create // an initial about:blank viewer if present: - window.arguments[6], !!window.arguments[6], window.arguments[7]); + window.arguments[7], !!window.arguments[7], window.arguments[8]); window.focus(); } // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3. @@ -2042,7 +2075,7 @@ function BrowserTryToCloseWindow() } function loadURI(uri, referrer, postData, allowThirdPartyFixup, referrerPolicy, - originPrincipal, forceAboutBlankViewerInCurrent, + userContextId, originPrincipal, forceAboutBlankViewerInCurrent, triggeringPrincipal) { try { openLinkIn(uri, "current", @@ -2050,6 +2083,7 @@ function loadURI(uri, referrer, postData, allowThirdPartyFixup, referrerPolicy, referrerPolicy: referrerPolicy, postData: postData, allowThirdPartyFixup: allowThirdPartyFixup, + userContextId: userContextId, originPrincipal, triggeringPrincipal, forceAboutBlankViewerInCurrent, @@ -3923,6 +3957,66 @@ function updateEditUIVisibility() } } +/** + * Opens a new tab with the userContextId specified as an attribute of + * sourceEvent. This attribute is propagated to the top level originAttributes + * living on the tab's docShell. + * + * @param event + * A click event on a userContext File Menu option + */ +function openNewUserContextTab(event) +{ + openUILinkIn(BROWSER_NEW_TAB_URL, "tab", { + userContextId: parseInt(event.target.getAttribute('data-usercontextid')), + }); +} + +/** + * Updates File Menu User Context UI visibility depending on + * privacy.userContext.enabled pref state. + */ +function updateUserContextUIVisibility() +{ + let menu = document.getElementById("menu_newUserContext"); + menu.hidden = !Services.prefs.getBoolPref("privacy.userContext.enabled"); + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + menu.setAttribute("disabled", "true"); + } +} + +/** + * Updates the User Context UI indicators if the browser is in a non-default context + */ +function updateUserContextUIIndicator() +{ + let hbox = document.getElementById("userContext-icons"); + + let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid"); + if (!userContextId) { + hbox.setAttribute("data-identity-color", ""); + hbox.hidden = true; + return; + } + + let identity = ContextualIdentityService.getIdentityFromId(userContextId); + if (!identity) { + hbox.setAttribute("data-identity-color", ""); + hbox.hidden = true; + return; + } + + hbox.setAttribute("data-identity-color", identity.color); + + let label = document.getElementById("userContext-label"); + label.setAttribute("value", ContextualIdentityService.getUserContextLabel(userContextId)); + + let indicator = document.getElementById("userContext-indicator"); + indicator.setAttribute("data-identity-icon", identity.icon); + + hbox.hidden = false; +} + /** * Makes the Character Encoding menu enabled or disabled as appropriate. * To be called when the View menu or the app menu is opened. @@ -4603,6 +4697,7 @@ nsBrowserAccess.prototype = { _openURIInNewTab: function(aURI, aReferrer, aReferrerPolicy, aIsPrivate, aIsExternal, aForceNotRemote=false, + aUserContextId=Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID, aOpener = null, aTriggeringPrincipal = null) { let win, needToFocusWin; @@ -4631,6 +4726,7 @@ nsBrowserAccess.prototype = { triggeringPrincipal: aTriggeringPrincipal, referrerURI: aReferrer, referrerPolicy: aReferrerPolicy, + userContextId: aUserContextId, fromExternal: aIsExternal, inBackground: loadInBackground, forceNotRemote: aForceNotRemote, @@ -4707,10 +4803,13 @@ nsBrowserAccess.prototype = { // will do the job of shuttling off the newly opened browser to run in // the right process once it starts loading a URI. let forceNotRemote = !!aOpener; + let userContextId = aOpener && aOpener.document + ? aOpener.document.nodePrincipal.originAttributes.userContextId + : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID; let openerWindow = (aFlags & Ci.nsIBrowserDOMWindow.OPEN_NO_OPENER) ? null : aOpener; let browser = this._openURIInNewTab(aURI, referrer, referrerPolicy, isPrivate, isExternal, - forceNotRemote, + forceNotRemote, userContextId, openerWindow, triggeringPrincipal); if (browser) newWindow = browser.contentWindow; @@ -4742,10 +4841,16 @@ nsBrowserAccess.prototype = { var isExternal = !!(aFlags & Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + var userContextId = aParams.openerOriginAttributes && + ("userContextId" in aParams.openerOriginAttributes) + ? aParams.openerOriginAttributes.userContextId + : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID + let browser = this._openURIInNewTab(aURI, aParams.referrer, aParams.referrerPolicy, aParams.isPrivate, isExternal, false, + userContextId, null, aParams.triggeringPrincipal); if (browser) return browser.QueryInterface(Ci.nsIFrameLoaderOwner); @@ -5303,6 +5408,11 @@ function handleLinkClick(event, href, linkNode) { triggeringPrincipal: doc.nodePrincipal, }; + // The new tab/window must use the same userContextId + if (doc.nodePrincipal.originAttributes.userContextId) { + params.userContextId = doc.nodePrincipal.originAttributes.userContextId; + } + openLinkIn(href, where, params); event.preventDefault(); return true; @@ -5374,6 +5484,8 @@ function handleDroppedLink(event, urlOrLinks, name) let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange; + let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid"); + // event is null if links are dropped in content process. // inBackground should be false, as it's loading into current browser. let inBackground = false; @@ -5397,6 +5509,7 @@ function handleDroppedLink(event, urlOrLinks, name) replace: true, allowThirdPartyFixup: false, postDatas, + userContextId, }); } }); diff --git a/application/basilisk/base/content/browser.xul b/application/basilisk/base/content/browser.xul index ea7a91d505..c732c3263a 100644 --- a/application/basilisk/base/content/browser.xul +++ b/application/basilisk/base/content/browser.xul @@ -8,6 +8,7 @@ + #ifdef MOZ_DEVTOOLS #endif @@ -552,7 +553,12 @@ key="key_undoCloseTab" label="&undoCloseTab.label;" observes="History:UndoCloseTab"/> - + + + + + diff --git a/application/basilisk/base/content/content.js b/application/basilisk/base/content/content.js index d2a70ba117..5accbdf7b7 100644 --- a/application/basilisk/base/content/content.js +++ b/application/basilisk/base/content/content.js @@ -162,6 +162,9 @@ var handleContentContextMenu = function (event) { let selectionInfo = BrowserUtils.getSelectionDetails(content); + let loadContext = docShell.QueryInterface(Ci.nsILoadContext); + let userContextId = loadContext.originAttributes.userContextId; + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { let editFlags = SpellCheckHelper.isEditable(event.target, content); let spellInfo; @@ -185,7 +188,7 @@ var handleContentContextMenu = function (event) { principal, docLocation, charSet, baseURI, referrer, referrerPolicy, contentType, contentDisposition, frameOuterWindowID, selectionInfo, disableSetDesktopBg, - loginFillInfo, parentAllowsMixedContent }, + loginFillInfo, parentAllowsMixedContent, userContextId }, { event, popupNode: event.target }); } else { @@ -209,6 +212,7 @@ var handleContentContextMenu = function (event) { disableSetDesktopBackground: disableSetDesktopBg, loginFillInfo, parentAllowsMixedContent, + userContextId, }; } } diff --git a/application/basilisk/base/content/nsContextMenu.js b/application/basilisk/base/content/nsContextMenu.js index 74a2e7a8ee..3f77dcb90f 100644 --- a/application/basilisk/base/content/nsContextMenu.js +++ b/application/basilisk/base/content/nsContextMenu.js @@ -4,6 +4,7 @@ # 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/. +Components.utils.import("resource://gre/modules/ContextualIdentityService.jsm"); Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm"); Components.utils.import("resource://gre/modules/LoginManagerContextMenu.jsm"); @@ -141,11 +142,28 @@ nsContextMenu.prototype = { this.onPlainTextLink = true; } + var inContainer = false; + if (gContextMenuContentData.userContextId) { + inContainer = true; + var item = document.getElementById("context-openlinkincontainertab"); + + item.setAttribute("data-usercontextid", gContextMenuContentData.userContextId); + + var label = + ContextualIdentityService.getUserContextLabel(gContextMenuContentData.userContextId); + item.setAttribute("label", + gBrowserBundle.formatStringFromName("userContextOpenLink.label", + [label], 1)); + } + var shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink; var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); + var showContainers = Services.prefs.getBoolPref("privacy.userContext.enabled"); this.showItem("context-openlink", shouldShow && !isWindowPrivate); this.showItem("context-openlinkprivate", shouldShow); - this.showItem("context-openlinkintab", shouldShow); + this.showItem("context-openlinkintab", shouldShow && !inContainer); + this.showItem("context-openlinkincontainertab", shouldShow && inContainer); + this.showItem("context-openlinkinusercontext-menu", shouldShow && !isWindowPrivate && showContainers); this.showItem("context-openlinkincurrent", this.onPlainTextLink); this.showItem("context-sep-open", shouldShow); }, @@ -940,6 +958,13 @@ nsContextMenu.prototype = { params[p] = extra[p]; } + // If we want to change userContextId, we must be sure that we don't + // propagate the referrer. + if ("userContextId" in params && + params.userContextId != gContextMenuContentData.userContextId) { + params.noReferrer = true; + } + return params; }, @@ -957,7 +982,7 @@ nsContextMenu.prototype = { }, // Open linked-to URL in a new tab. - openLinkInTab: function() { + openLinkInTab: function(event) { urlSecurityCheck(this.linkURL, this.principal); let referrerURI = gContextMenuContentData.documentURIObject; @@ -978,6 +1003,7 @@ nsContextMenu.prototype = { let params = { allowMixedContent: persistAllowMixedContentInChildTab, + userContextId: parseInt(event.target.getAttribute('data-usercontextid')), }; openLinkIn(this.linkURL, "tab", this._openLinkInParameters(params)); @@ -1753,4 +1779,8 @@ nsContextMenu.prototype = { menuItem.label = menuLabel; menuItem.accessKey = gNavigatorBundle.getString("contextMenuSearch.accesskey"); }, + createContainerMenu: function(aEvent) { + return createUserContextMenu(aEvent, true, + gContextMenuContentData.userContextId); + }, }; diff --git a/application/basilisk/base/content/tab-content.js b/application/basilisk/base/content/tab-content.js index 5a06c316be..6d053dd2bc 100644 --- a/application/basilisk/base/content/tab-content.js +++ b/application/basilisk/base/content/tab-content.js @@ -889,6 +889,33 @@ var RefreshBlocker = { RefreshBlocker.init(); +var UserContextIdNotifier = { + init() { + addEventListener("DOMWindowCreated", this); + }, + + uninit() { + removeEventListener("DOMWindowCreated", this); + }, + + handleEvent(aEvent) { + // When the window is created, we want to inform the tabbrowser about + // the userContextId in use in order to update the UI correctly. + // Just because we cannot change the userContextId from an active docShell, + // we don't need to check DOMContentLoaded again. + this.uninit(); + + // We use the docShell because content.document can have been loaded before + // setting the originAttributes. + let loadContext = docShell.QueryInterface(Ci.nsILoadContext); + let userContextId = loadContext.originAttributes.userContextId; + + sendAsyncMessage("Browser:WindowCreated", { userContextId }); + } +}; + +UserContextIdNotifier.init(); + #ifdef MOZ_WEBEXTENSIONS ExtensionContent.init(this); addEventListener("unload", () => { diff --git a/application/basilisk/base/content/tabbrowser.xml b/application/basilisk/base/content/tabbrowser.xml index e4f0579e3e..6ac6d54c75 100644 --- a/application/basilisk/base/content/tabbrowser.xml +++ b/application/basilisk/base/content/tabbrowser.xml @@ -826,8 +826,10 @@ } let unifiedComplete = this.mTabBrowser._unifiedComplete; + let userContextId = this.mBrowser.getAttribute("usercontextid") || 0; if (this.mBrowser.registeredOpenURI) { - unifiedComplete.unregisterOpenPage(this.mBrowser.registeredOpenURI); + unifiedComplete.unregisterOpenPage(this.mBrowser.registeredOpenURI, + userContextId); delete this.mBrowser.registeredOpenURI; } // Tabs in private windows aren't registered as "Open" so @@ -835,7 +837,7 @@ if (!isBlankPageURL(aLocation.spec) && (!PrivateBrowsingUtils.isWindowPrivate(window) || PrivateBrowsingUtils.permanentPrivateBrowsing)) { - unifiedComplete.registerOpenPage(aLocation); + unifiedComplete.registerOpenPage(aLocation, userContextId); this.mBrowser.registeredOpenURI = aLocation; } } @@ -1240,6 +1242,7 @@ this._adjustFocusAfterTabSwitch(this.mCurrentTab); } + updateUserContextUIIndicator(); gIdentityHandler.updateSharingIndicator(); this.tabContainer._setPositionalAttributes(); @@ -1495,6 +1498,7 @@ var aSkipAnimation; var aForceNotRemote; var aNoReferrer; + var aUserContextId; var aRelatedBrowser; var aOriginPrincipal; var aOpener; @@ -1515,6 +1519,7 @@ aSkipAnimation = params.skipAnimation; aForceNotRemote = params.forceNotRemote; aNoReferrer = params.noReferrer; + aUserContextId = params.userContextId; aRelatedBrowser = params.relatedBrowser; aOriginPrincipal = params.originPrincipal; aOpener = params.opener; @@ -1537,6 +1542,7 @@ allowMixedContent: aAllowMixedContent, forceNotRemote: aForceNotRemote, noReferrer: aNoReferrer, + userContextId: aUserContextId, originPrincipal: aOriginPrincipal, relatedBrowser: aRelatedBrowser, opener: aOpener }); @@ -1557,6 +1563,7 @@ let aTargetTab; let aNewIndex = -1; let aPostDatas = []; + let aUserContextId; if (arguments.length == 2 && typeof arguments[1] == "object") { let params = arguments[1]; @@ -1567,6 +1574,7 @@ aNewIndex = typeof params.newIndex === "number" ? params.newIndex : aNewIndex; aPostDatas = params.postDatas || aPostDatas; + aUserContextId = params.userContextId; } if (!aURIs.length) @@ -1615,7 +1623,8 @@ ownerTab: owner, skipAnimation: multiple, allowThirdPartyFixup: aAllowThirdPartyFixup, - postData: aPostDatas[0] + postData: aPostDatas[0], + userContextId: aUserContextId }); if (aNewIndex !== -1) { this.moveTabTo(firstTabAdded, aNewIndex); @@ -1628,7 +1637,8 @@ let tab = this.addTab(aURIs[i], { skipAnimation: true, allowThirdPartyFixup: aAllowThirdPartyFixup, - postData: aPostDatas[i] + postData: aPostDatas[i], + userContextId: aUserContextId }); if (targetTabIndex !== -1) this.moveTabTo(tab, ++tabNum); @@ -1901,7 +1911,7 @@ - + + + + + document.getElementById(this.getAttribute("tabbrowser")); @@ -5259,6 +5319,55 @@ null null + + + + + + + let root = document.documentElement; @@ -6361,6 +6470,7 @@ inBackground = !inBackground; let targetTab = this._getDragTargetTab(event, true); + let userContextId = this.selectedItem.getAttribute("usercontextid"); let replace = !!targetTab; let newIndex = this._getDropIndex(event, true); let urls = links.map(link => link.url); @@ -6370,6 +6480,7 @@ allowThirdPartyFixup: true, targetTab, newIndex, + userContextId, }); } @@ -6630,6 +6741,14 @@ --> undefined + + + return this.hasAttribute("usercontextid") + ? parseInt(this.getAttribute("usercontextid")) + : 0; + + + return this.getAttribute("soundplaying") == "true"; @@ -6757,6 +6876,27 @@ ]]> + + + + + + + @@ -6933,9 +7073,30 @@ 0; i--) { @@ -6960,6 +7124,9 @@ menuItem.tab.mCorrespondingMenuitem = null; this.removeChild(menuItem); } + if (menuItem.hasAttribute("usercontextid")) { + this.removeChild(menuItem); + } } var tabcontainer = gBrowser.tabContainer; tabcontainer.mTabstrip.removeEventListener("scroll", this, false); diff --git a/application/basilisk/base/content/utilityOverlay.js b/application/basilisk/base/content/utilityOverlay.js index aeef5879c3..581a45438f 100644 --- a/application/basilisk/base/content/utilityOverlay.js +++ b/application/basilisk/base/content/utilityOverlay.js @@ -5,6 +5,7 @@ // Services = object with smart getters for common XPCOM services Components.utils.import("resource://gre/modules/AppConstants.jsm"); +Components.utils.import("resource://gre/modules/ContextualIdentityService.jsm"); Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); @@ -175,6 +176,7 @@ function whereToOpenLink( e, ignoreButton, ignoreAlt ) * skipTabAnimation (boolean) * allowPinnedTabHostChange (boolean) * allowPopups (boolean) + * userContextId (unsigned int) */ function openUILinkIn(url, where, aAllowThirdPartyFixup, aPostData, aReferrerURI) { var params; @@ -220,6 +222,7 @@ function openLinkIn(url, where, params) { var aAllowPinnedTabHostChange = !!params.allowPinnedTabHostChange; var aNoReferrer = params.noReferrer; var aAllowPopups = !!params.allowPopups; + var aUserContextId = params.userContextId; var aIndicateErrorPageLoad = params.indicateErrorPageLoad; var aPrincipal = params.originPrincipal; var aTriggeringPrincipal = params.triggeringPrincipal; @@ -265,6 +268,7 @@ function openLinkIn(url, where, params) { function useOAForPrincipal(principal) { if (principal && principal.isCodebasePrincipal) { let attrs = { + userContextId: aUserContextId, privateBrowsingId: aIsPrivate || (w && PrivateBrowsingUtils.isWindowPrivate(w)), }; return Services.scriptSecurityManager.createCodebasePrincipal(principal.URI, attrs); @@ -311,12 +315,17 @@ function openLinkIn(url, where, params) { createInstance(Ci.nsISupportsPRUint32); referrerPolicySupports.data = aReferrerPolicy; + var userContextIdSupports = Cc["@mozilla.org/supports-PRUint32;1"]. + createInstance(Ci.nsISupportsPRUint32); + userContextIdSupports.data = aUserContextId; + sa.appendElement(wuri, /* weak =*/ false); sa.appendElement(charset, /* weak =*/ false); sa.appendElement(referrerURISupports, /* weak =*/ false); sa.appendElement(aPostData, /* weak =*/ false); sa.appendElement(allowThirdPartyFixupSupports, /* weak =*/ false); sa.appendElement(referrerPolicySupports, /* weak =*/ false); + sa.appendElement(userContextIdSupports, /* weak =*/ false); sa.appendElement(aPrincipal, /* weak =*/ false); sa.appendElement(aTriggeringPrincipal, /* weak =*/ false); @@ -409,7 +418,8 @@ function openLinkIn(url, where, params) { flags: flags, referrerURI: aNoReferrer ? null : aReferrerURI, referrerPolicy: aReferrerPolicy, - postData: aPostData + postData: aPostData, + userContextId: aUserContextId }); browserUsedForLoad = aCurrentBrowser; break; @@ -428,6 +438,7 @@ function openLinkIn(url, where, params) { skipAnimation: aSkipTabAnimation, allowMixedContent: aAllowMixedContent, noReferrer: aNoReferrer, + userContextId: aUserContextId, originPrincipal: aPrincipal, triggeringPrincipal: aTriggeringPrincipal, }); @@ -471,6 +482,74 @@ function checkForMiddleClick(node, event) { } } +// Populate a menu with user-context menu items. This method should be called +// by onpopupshowing passing the event as first argument. +function createUserContextMenu(event, isContextMenu = false, excludeUserContextId = 0) { + while (event.target.hasChildNodes()) { + event.target.removeChild(event.target.firstChild); + } + + let bundle = document.getElementById("bundle_browser"); + let docfrag = document.createDocumentFragment(); + + // If we are excluding a userContextId, we want to add a 'no-container' item. + if (excludeUserContextId) { + let menuitem = document.createElement("menuitem"); + menuitem.setAttribute("data-usercontextid", "0"); + menuitem.setAttribute("label", bundle.getString("userContextNone.label")); + menuitem.setAttribute("accesskey", bundle.getString("userContextNone.accesskey")); + + // We don't set an oncommand/command attribute because if we have + // to exclude a userContextId we are generating the contextMenu and + // isContextMenu will be true. + + docfrag.appendChild(menuitem); + + let menuseparator = document.createElement("menuseparator"); + docfrag.appendChild(menuseparator); + } + + ContextualIdentityService.getIdentities().forEach(identity => { + if (identity.userContextId == excludeUserContextId) { + return; + } + + let menuitem = document.createElement("menuitem"); + menuitem.setAttribute("data-usercontextid", identity.userContextId); + menuitem.setAttribute("label", ContextualIdentityService.getUserContextLabel(identity.userContextId)); + + if (identity.accessKey) { + menuitem.setAttribute("accesskey", bundle.getString(identity.accessKey)); + } + + menuitem.classList.add("menuitem-iconic"); + menuitem.setAttribute("data-identity-color", identity.color); + + if (!isContextMenu) { + menuitem.setAttribute("command", "Browser:NewUserContextTab"); + } + + menuitem.setAttribute("data-identity-icon", identity.icon); + + docfrag.appendChild(menuitem); + }); + + if (!isContextMenu) { + docfrag.appendChild(document.createElement("menuseparator")); + + let menuitem = document.createElement("menuitem"); + menuitem.setAttribute("label", + bundle.getString("userContext.aboutPage.label")); + menuitem.setAttribute("accesskey", + bundle.getString("userContext.aboutPage.accesskey")); + menuitem.setAttribute("command", "Browser:OpenAboutContainers"); + docfrag.appendChild(menuitem); + } + + event.target.appendChild(docfrag); + return true; +} + // Closes all popups that are ancestors of the node. function closeMenus(node) { diff --git a/application/basilisk/components/contextualidentity/content/usercontext.css b/application/basilisk/components/contextualidentity/content/usercontext.css new file mode 100644 index 0000000000..728275d9f8 --- /dev/null +++ b/application/basilisk/components/contextualidentity/content/usercontext.css @@ -0,0 +1,91 @@ +[data-identity-color="blue"] { + --identity-tab-color: #0996f8; + --identity-icon-color: #00a7e0; +} + +[data-identity-color="turquoise"] { + --identity-tab-color: #01bdad; + --identity-icon-color: #01bdad; +} + +[data-identity-color="green"] { + --identity-tab-color: #57bd35; + --identity-icon-color: #7dc14c; +} + +[data-identity-color="yellow"] { + --identity-tab-color: #ffcb00; + --identity-icon-color: #ffcb00; +} + +[data-identity-color="orange"] { + --identity-tab-color: #ff9216; + --identity-icon-color: #ff9216; +} + +[data-identity-color="red"] { + --identity-tab-color: #d92215; + --identity-icon-color: #d92215; +} + +[data-identity-color="pink"] { + --identity-tab-color: #ea385e; + --identity-icon-color: #ee5195; +} + +[data-identity-color="purple"] { + --identity-tab-color: #7a2f7a; + --identity-icon-color: #7a2f7a; +} + +[data-identity-icon="fingerprint"] { + --identity-icon: url("chrome://browser/content/usercontext.svg#fingerprint"); +} + +[data-identity-icon="briefcase"] { + --identity-icon: url("chrome://browser/content/usercontext.svg#briefcase"); +} + +[data-identity-icon="dollar"] { + --identity-icon: url("chrome://browser/content/usercontext.svg#dollar"); +} + +[data-identity-icon="cart"] { + --identity-icon: url("chrome://browser/content/usercontext.svg#cart"); +} + +[data-identity-icon="circle"] { + --identity-icon: url("chrome://browser/content/usercontext.svg#circle"); +} + +#userContext-indicator { + height: 16px; + width: 16px; +} + +#userContext-label { + margin-inline-end: 3px; + color: var(--identity-tab-color); +} + +#userContext-icons { + -moz-box-align: center; +} + +.tabbrowser-tab[usercontextid] { + background-image: linear-gradient(to right, transparent 20%, var(--identity-tab-color) 30%, var(--identity-tab-color) 70%, transparent 80%); + background-size: auto 2px; + background-repeat: no-repeat; +} + +.userContext-icon, +.menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon, +.subviewbutton[usercontextid] > .toolbarbutton-icon, +#userContext-indicator { + background-image: var(--identity-icon); + filter: url(chrome://browser/skin/filters.svg#fill); + fill: var(--identity-icon-color); + background-size: contain; + background-repeat: no-repeat; + background-position: center center; +} diff --git a/application/basilisk/components/contextualidentity/jar.mn b/application/basilisk/components/contextualidentity/jar.mn new file mode 100644 index 0000000000..848245949a --- /dev/null +++ b/application/basilisk/components/contextualidentity/jar.mn @@ -0,0 +1,6 @@ +# 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/. + +browser.jar: + content/browser/usercontext/usercontext.css (content/usercontext.css) diff --git a/application/basilisk/components/contextualidentity/moz.build b/application/basilisk/components/contextualidentity/moz.build new file mode 100644 index 0000000000..aac3a838c4 --- /dev/null +++ b/application/basilisk/components/contextualidentity/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ['jar.mn'] diff --git a/application/basilisk/components/customizableui/CustomizableWidgets.jsm b/application/basilisk/components/customizableui/CustomizableWidgets.jsm index 401b7ca747..09b3f167e3 100644 --- a/application/basilisk/components/customizableui/CustomizableWidgets.jsm +++ b/application/basilisk/components/customizableui/CustomizableWidgets.jsm @@ -23,6 +23,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu", "resource://gre/modules/CharsetMenu.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService", + "resource://gre/modules/ContextualIdentityService.jsm"); XPCOMUtils.defineLazyGetter(this, "CharsetBundle", function() { const kCharsetBundle = "chrome://global/locale/charsetMenu.properties"; @@ -975,6 +977,89 @@ const CustomizableWidgets = [ let win = aEvent.view; win.MailIntegration.sendLinkForBrowser(win.gBrowser.selectedBrowser) } + }, { + id: "containers-panelmenu", + type: "view", + viewId: "PanelUI-containers", + hasObserver: false, + onCreated: function(aNode) { + let doc = aNode.ownerDocument; + let win = doc.defaultView; + let items = doc.getElementById("PanelUI-containersItems"); + + let onItemCommand = function (aEvent) { + let item = aEvent.target; + if (item.hasAttribute("usercontextid")) { + let userContextId = parseInt(item.getAttribute("usercontextid")); + win.openUILinkIn(win.BROWSER_NEW_TAB_URL, "tab", {userContextId}); + } + }; + items.addEventListener("command", onItemCommand); + + if (PrivateBrowsingUtils.isWindowPrivate(win)) { + aNode.setAttribute("disabled", "true"); + } + + this.updateVisibility(aNode); + + if (!this.hasObserver) { + Services.prefs.addObserver("privacy.userContext.enabled", this, true); + this.hasObserver = true; + } + }, + onViewShowing: function(aEvent) { + let doc = aEvent.target.ownerDocument; + + let items = doc.getElementById("PanelUI-containersItems"); + + while (items.firstChild) { + items.firstChild.remove(); + } + + let fragment = doc.createDocumentFragment(); + let bundle = doc.getElementById("bundle_browser"); + + ContextualIdentityService.getIdentities().forEach(identity => { + let label = ContextualIdentityService.getUserContextLabel(identity.userContextId); + + let item = doc.createElementNS(kNSXUL, "toolbarbutton"); + item.setAttribute("label", label); + item.setAttribute("usercontextid", identity.userContextId); + item.setAttribute("class", "subviewbutton"); + item.setAttribute("data-identity-color", identity.color); + item.setAttribute("data-identity-icon", identity.icon); + + fragment.appendChild(item); + }); + + fragment.appendChild(doc.createElementNS(kNSXUL, "menuseparator")); + + let item = doc.createElementNS(kNSXUL, "toolbarbutton"); + item.setAttribute("label", bundle.getString("userContext.aboutPage.label")); + item.setAttribute("command", "Browser:OpenAboutContainers"); + item.setAttribute("class", "subviewbutton"); + fragment.appendChild(item); + + items.appendChild(fragment); + }, + + updateVisibility(aNode) { + aNode.hidden = !Services.prefs.getBoolPref("privacy.userContext.enabled"); + }, + + observe(aSubject, aTopic, aData) { + let {instances} = CustomizableUI.getWidget("containers-panelmenu"); + for (let {node} of instances) { + if (node) { + this.updateVisibility(node); + } + } + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsISupportsWeakReference, + Ci.nsIObserver + ]), }]; let preferencesButton = { diff --git a/application/basilisk/components/moz.build b/application/basilisk/components/moz.build index 6eb3e710b2..07e79bc363 100644 --- a/application/basilisk/components/moz.build +++ b/application/basilisk/components/moz.build @@ -6,6 +6,7 @@ DIRS += [ 'about', + 'contextualidentity', 'customizableui', 'dirprovider', 'downloads', diff --git a/application/basilisk/components/preferences/containers.js b/application/basilisk/components/preferences/containers.js new file mode 100644 index 0000000000..6ca5853f7f --- /dev/null +++ b/application/basilisk/components/preferences/containers.js @@ -0,0 +1,176 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/ContextualIdentityService.jsm"); + +const containersBundle = Services.strings.createBundle("chrome://browser/locale/preferences/containers.properties"); + +const HTMLNS = "http://www.w3.org/1999/xhtml"; + +let gContainersManager = { + icons: [ + "fingerprint", + "briefcase", + "dollar", + "cart", + "circle" + ], + + colors: [ + "blue", + "turquoise", + "green", + "yellow", + "orange", + "red", + "pink", + "purple" + ], + + onLoad() { + let params = window.arguments[0] || {}; + this.init(params); + }, + + init(aParams) { + this.userContextId = aParams.userContextId || null; + this.identity = aParams.identity; + + if (aParams.windowTitle) { + document.title = aParams.windowTitle; + } + + const iconWrapper = document.getElementById("iconWrapper"); + iconWrapper.appendChild(this.createIconButtons()); + + const colorWrapper = document.getElementById("colorWrapper"); + colorWrapper.appendChild(this.createColorSwatches()); + + if (this.identity.name) { + const name = document.getElementById("name"); + name.value = this.identity.name; + this.checkForm(); + } + + this.setLabelsMinWidth(); + + // This is to prevent layout jank caused by the svgs and outlines rendering at different times + document.getElementById("containers-content").removeAttribute("hidden"); + }, + + setLabelsMinWidth() { + const labelMinWidth = containersBundle.GetStringFromName("containers.labelMinWidth"); + const labels = [ + document.getElementById("nameLabel"), + document.getElementById("iconLabel"), + document.getElementById("colorLabel") + ]; + for (let label of labels) { + label.style.minWidth = labelMinWidth; + } + }, + + uninit() { + }, + + // Check if name string as to if the form can be submitted + checkForm() { + const name = document.getElementById("name"); + let btnApplyChanges = document.getElementById("btnApplyChanges"); + if (!name.value) { + btnApplyChanges.setAttribute("disabled", true); + } else { + btnApplyChanges.removeAttribute("disabled"); + } + }, + + createIconButtons(defaultIcon) { + let radiogroup = document.createElement("radiogroup"); + radiogroup.setAttribute("id", "icon"); + radiogroup.className = "icon-buttons"; + + for (let icon of this.icons) { + let iconSwatch = document.createElement("radio"); + iconSwatch.id = "iconbutton-" + icon; + iconSwatch.name = "icon"; + iconSwatch.type = "radio"; + iconSwatch.value = icon; + + if (this.identity.icon && this.identity.icon == icon) { + iconSwatch.setAttribute("selected", true); + } + + iconSwatch.setAttribute("label", + containersBundle.GetStringFromName(`containers.${icon}.label`)); + let iconElement = document.createElement("hbox"); + iconElement.className = 'userContext-icon'; + iconElement.setAttribute("data-identity-icon", icon); + + iconSwatch.appendChild(iconElement); + radiogroup.appendChild(iconSwatch); + } + + return radiogroup; + }, + + createColorSwatches(defaultColor) { + let radiogroup = document.createElement("radiogroup"); + radiogroup.setAttribute("id", "color"); + + for (let color of this.colors) { + let colorSwatch = document.createElement("radio"); + colorSwatch.id = "colorswatch-" + color; + colorSwatch.name = "color"; + colorSwatch.type = "radio"; + colorSwatch.value = color; + + if (this.identity.color && this.identity.color == color) { + colorSwatch.setAttribute("selected", true); + } + + colorSwatch.setAttribute("label", + containersBundle.GetStringFromName(`containers.${color}.label`)); + let iconElement = document.createElement("hbox"); + iconElement.className = 'userContext-icon'; + iconElement.setAttribute("data-identity-icon", "circle"); + iconElement.setAttribute("data-identity-color", color); + + colorSwatch.appendChild(iconElement); + radiogroup.appendChild(colorSwatch); + } + return radiogroup; + }, + + onApplyChanges() { + let icon = document.getElementById("icon").value; + let color = document.getElementById("color").value; + let name = document.getElementById("name").value; + + if (this.icons.indexOf(icon) == -1) { + throw "Internal error. The icon value doesn't match."; + } + + if (this.colors.indexOf(color) == -1) { + throw "Internal error. The color value doesn't match."; + } + + if (this.userContextId) { + ContextualIdentityService.update(this.userContextId, + name, + icon, + color); + } else { + ContextualIdentityService.create(name, + icon, + color); + } + window.parent.location.reload() + }, + + onWindowKeyPress(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE) + window.close(); + } +} diff --git a/application/basilisk/components/preferences/containers.xul b/application/basilisk/components/preferences/containers.xul new file mode 100644 index 0000000000..62a775fe40 --- /dev/null +++ b/application/basilisk/components/preferences/containers.xul @@ -0,0 +1,52 @@ + + + + + + + + + + + +