Bug 1508364 - New UI for Private Browsing with Search r=andreio
☠☠ backed out by 32541f2f0575 ☠ ☠
authorRicky Rosario <rickyrosario@gmail.com>
Mon, 21 Jan 2019 17:05:08 +0000
changeset 514733 13f379946829814f44e8e7b192ac4dd4e93d6959
parent 514732 2307fb1cfa80a5d4cf05c404917344510a982194
child 514736 f851b150476a2259413390d4510f27ed62a1d1c2
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersandreio
bugs1508364
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1508364 - New UI for Private Browsing with Search r=andreio MozReview-Commit-ID: 4WSGpL5Gvde Differential Revision: https://phabricator.services.mozilla.com/D16854
browser/app/profile/firefox.js
browser/components/about/AboutPrivateBrowsingHandler.jsm
browser/components/nsBrowserGlue.js
browser/components/privatebrowsing/content/aboutPrivateBrowsing.css
browser/components/privatebrowsing/content/aboutPrivateBrowsing.js
browser/components/privatebrowsing/content/aboutPrivateBrowsing.xhtml
browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about.js
browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css
toolkit/components/remotepagemanager/MessagePort.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -968,16 +968,19 @@ pref("browser.security.newcerterrorpage.
 pref("browser.security.newcerterrorpage.mitm.enabled", false);
 #endif
 
 pref("security.certerrors.recordEventTelemetry", true);
 
 // Whether to start the private browsing mode at application startup
 pref("browser.privatebrowsing.autostart", false);
 
+// Whether to show the new private browsing UI with in-content search box.
+pref("browser.privatebrowsing.searchUI", true);
+
 // Whether the bookmark panel should be shown when bookmarking a page.
 pref("browser.bookmarks.editDialog.showForNewBookmarks", true);
 
 // Don't try to alter this pref, it'll be reset the next time you use the
 // bookmarking dialog
 pref("browser.bookmarks.editDialog.firstEditField", "namePicker");
 
 pref("dom.ipc.plugins.flash.disable-protected-mode", false);
--- a/browser/components/about/AboutPrivateBrowsingHandler.jsm
+++ b/browser/components/about/AboutPrivateBrowsingHandler.jsm
@@ -2,21 +2,23 @@
  * 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 EXPORTED_SYMBOLS = ["AboutPrivateBrowsingHandler"];
 
 ChromeUtils.import("resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 var AboutPrivateBrowsingHandler = {
   _topics: [
     "DontShowIntroPanelAgain",
     "OpenPrivateWindow",
+    "SearchHandoff",
   ],
 
   init() {
     this.pageListener = new RemotePages("about:privatebrowsing");
     for (let topic of this._topics) {
       this.pageListener.addMessageListener(topic, this.receiveMessage.bind(this));
     }
   },
@@ -35,11 +37,72 @@ var AboutPrivateBrowsingHandler = {
         win.OpenBrowserWindow({private: true});
         break;
       }
       case "DontShowIntroPanelAgain": {
         let win = aMessage.target.browser.ownerGlobal;
         win.ContentBlocking.dontShowIntroPanelAgain();
         break;
       }
+      case "SearchHandoff": {
+        let searchAlias = "";
+        let searchAliases = Services.search.defaultEngine.wrappedJSObject.__internalAliases;
+        if (searchAliases && searchAliases.length > 0) {
+          searchAlias = `${searchAliases[0]} `;
+        }
+        let urlBar = aMessage.target.browser.ownerGlobal.gURLBar;
+        let isFirstChange = true;
+
+        if (!aMessage.data || !aMessage.data.text) {
+          urlBar.hiddenFocus();
+        } else {
+          // Pass the provided text to the awesomebar. Prepend the @engine shortcut.
+          urlBar.search(`${searchAlias}${aMessage.data.text}`);
+          isFirstChange = false;
+        }
+
+        let checkFirstChange = () => {
+          // Check if this is the first change since we hidden focused. If it is,
+          // remove hidden focus styles, prepend the search alias and hide the
+          // in-content search.
+          if (isFirstChange) {
+            isFirstChange = false;
+            urlBar.removeHiddenFocus();
+            urlBar.search(searchAlias);
+            aMessage.target.sendAsyncMessage("HideSearch");
+            urlBar.removeEventListener("compositionstart", checkFirstChange);
+            urlBar.removeEventListener("paste", checkFirstChange);
+          }
+        };
+
+        let onKeydown = ev => {
+          // Check if the keydown will cause a value change.
+          if (ev.key.length === 1 && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {
+            checkFirstChange();
+          }
+          // If the Esc button is pressed, we are done. Show in-content search and cleanup.
+          if (ev.key === "Escape") {
+            onDone();
+          }
+        };
+
+        let onDone = () => {
+          // We are done. Show in-content search again and cleanup.
+          aMessage.target.sendAsyncMessage("ShowSearch");
+          urlBar.removeHiddenFocus();
+
+          urlBar.removeEventListener("keydown", onKeydown);
+          urlBar.removeEventListener("mousedown", onDone);
+          urlBar.removeEventListener("blur", onDone);
+          urlBar.removeEventListener("compositionstart", checkFirstChange);
+          urlBar.removeEventListener("paste", checkFirstChange);
+        };
+
+        urlBar.addEventListener("keydown", onKeydown);
+        urlBar.addEventListener("mousedown", onDone);
+        urlBar.addEventListener("blur", onDone);
+        urlBar.addEventListener("compositionstart", checkFirstChange);
+        urlBar.addEventListener("paste", checkFirstChange);
+        break;
+      }
     }
   },
 };
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -87,17 +87,17 @@ let ACTORS = {
       },
     },
   },
 
   ContentSearch: {
     child: {
       module: "resource:///actors/ContentSearchChild.jsm",
       group: "browsers",
-      matches: ["about:home", "about:newtab", "about:welcome",
+      matches: ["about:home", "about:newtab", "about:welcome", "about:privatebrowsing",
                 "chrome://mochitests/content/*"],
       events: {
         "ContentSearchClient": {capture: true, wantUntrusted: true},
       },
       messages: [
         "ContentSearch",
       ],
     },
--- a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.css
+++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.css
@@ -1,6 +1,8 @@
 html.private .showNormal,
 html.normal .showPrivate,
+html.search-ui .dontShowSearch,
+html.no-search-ui .showSearch,
 body[tpEnabled] .showTpDisabled,
 body:not([tpEnabled]) .showTpEnabled {
   display: none !important;
 }
--- a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js
+++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js
@@ -1,15 +1,16 @@
 /* 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/. */
 
 /* eslint-env mozilla/frame-script */
 
 const TP_PB_ENABLED_PREF = "privacy.trackingprotection.pbmode.enabled";
+const PB_SEARCH_UI_ENABLED_PREF = "browser.privatebrowsing.searchUI";
 
 document.addEventListener("DOMContentLoaded", function() {
   if (!RPMIsWindowPrivate()) {
     document.documentElement.classList.remove("private");
     document.documentElement.classList.add("normal");
     document.getElementById("startPrivateBrowsing").addEventListener("click", function() {
       RPMSendAsyncMessage("OpenPrivateWindow");
     });
@@ -29,9 +30,78 @@ document.addEventListener("DOMContentLoa
   document.getElementById("learnMore").setAttribute("href",
     RPMGetFormatURLPref("app.support.baseURL") + "private-browsing");
 
   let tpEnabled = RPMGetBoolPref(TP_PB_ENABLED_PREF);
   if (!tpEnabled) {
     document.getElementById("tpSubHeader").remove();
     document.getElementById("tpSection").remove();
   }
+
+  let searchUIEnabled = RPMGetBoolPref(PB_SEARCH_UI_ENABLED_PREF);
+  if (searchUIEnabled) {
+    setupSearchUI();
+  }
 });
+
+function setupSearchUI() {
+  // Show the new search UI and hide the old one.
+  document.documentElement.classList.remove("no-search-ui");
+  document.documentElement.classList.add("search-ui");
+
+  // Setup the private browsing myths link.
+  document.getElementById("private-browsing-myths").setAttribute("href",
+    RPMGetFormatURLPref("app.support.baseURL") + "private-browsing-myths");
+
+  // Setup the search hand-off box.
+  let btn = document.getElementById("search-handoff-button");
+  let editable = document.getElementById("fake-editable");
+  let HIDE_SEARCH_TOPIC = "HideSearch";
+  let SHOW_SEARCH_TOPIC = "ShowSearch";
+  let SEARCH_HANDOFF_TOPIC = "SearchHandoff";
+
+  function showSearch() {
+    btn.classList.remove("focused");
+    btn.classList.remove("hidden");
+    RPMRemoveMessageListener(SHOW_SEARCH_TOPIC, showSearch);
+  }
+
+  function hideSearch() {
+    btn.classList.add("hidden");
+  }
+
+  function handoffSearch(text) {
+    RPMSendAsyncMessage(SEARCH_HANDOFF_TOPIC, {text});
+    RPMAddMessageListener(SHOW_SEARCH_TOPIC, showSearch);
+    if (text) {
+      hideSearch();
+    } else {
+      btn.classList.add("focused");
+      RPMAddMessageListener(HIDE_SEARCH_TOPIC, hideSearch);
+    }
+  }
+  btn.addEventListener("focus", function() {
+    handoffSearch();
+  });
+  btn.addEventListener("click", function() {
+    handoffSearch();
+  });
+
+  // Hand-off any text that gets dropped or pasted
+  editable.addEventListener("drop", function(ev) {
+    ev.preventDefault();
+    let text = ev.dataTransfer.getData("text");
+    if (text) {
+      handoffSearch(text);
+    }
+  });
+  editable.addEventListener("paste", function(ev) {
+    ev.preventDefault();
+    handoffSearch(ev.clipboardData.getData("Text"));
+  });
+
+  // Load contentSearchUI so it sets the search engine icon for us.
+  // TODO: FIXME. We should eventually refector contentSearchUI to do only what
+  // we need and have it do the common search handoff work for
+  // about:newtab and about:privatebrowsing.
+  let input = document.getElementById("dummy-input");
+  new window.ContentSearchUIController(input, input.parentNode, "aboutprivatebrowsing", "aboutprivatebrowsing");
+}
--- a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.xhtml
+++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.xhtml
@@ -12,31 +12,32 @@
   <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
   %brandDTD;
   <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
   %browserDTD;
   <!ENTITY % aboutPrivateBrowsingDTD SYSTEM "chrome://browser/locale/aboutPrivateBrowsing.dtd">
   %aboutPrivateBrowsingDTD;
 ]>
 
-<html xmlns="http://www.w3.org/1999/xhtml" class="private">
+<html xmlns="http://www.w3.org/1999/xhtml" class="private no-search-ui">
   <head>
-    <meta http-equiv="Content-Security-Policy" content="default-src chrome:"/>
+    <meta http-equiv="Content-Security-Policy" content="default-src chrome: blob:"/>
     <link rel="icon" type="image/png" href="chrome://browser/skin/privatebrowsing/favicon.svg"/>
     <link rel="stylesheet" href="chrome://browser/content/aboutPrivateBrowsing.css" type="text/css" media="all"/>
     <link rel="stylesheet" href="chrome://browser/skin/privatebrowsing/aboutPrivateBrowsing.css" type="text/css" media="all"/>
     <script type="application/javascript" src="chrome://browser/content/aboutPrivateBrowsing.js"></script>
+    <script type="application/javascript" src="chrome://browser/content/contentSearchUI.js"></script>
   </head>
 
   <body dir="&locale.dir;">
     <p class="showNormal">&aboutPrivateBrowsing.notPrivate;</p>
     <button id="startPrivateBrowsing"
             class="showNormal"
             accesskey="&privatebrowsingpage.openPrivateWindow.accesskey;">&privatebrowsingpage.openPrivateWindow.label;</button>
-    <div class="showPrivate container">
+    <div class="showPrivate dontShowSearch container">
       <h1 class="title">
         <span id="title">&privateBrowsing.title;</span>
       </h1>
       <section class="section-main">
         <p>&aboutPrivateBrowsing.info.notsaved.before;<strong>&aboutPrivateBrowsing.info.notsaved.emphasize;</strong>&aboutPrivateBrowsing.info.notsaved.after;</p>
         <ul class="list-row">
           <li>&aboutPrivateBrowsing.info.visited;</li>
           <li>&aboutPrivateBrowsing.info.cookies;</li>
@@ -62,10 +63,32 @@
           <a id="startTour" class="button">&trackingProtection.startTour1;</a>
         </p>
       </section>
 
       <section class="section-main">
         <p class="about-info">&aboutPrivateBrowsing.learnMore3.before;<a id="learnMore" target="_blank">&aboutPrivateBrowsing.learnMore3.title;</a>&aboutPrivateBrowsing.learnMore3.after;</p>
       </section>
     </div>
+    <div class="showPrivate showSearch container">
+      <div class="logo-and-wordmark">
+        <div class="logo" />
+        <div class="wordmark" />
+      </div>
+      <div class="search-inner-wrapper">
+        <button id="search-handoff-button" class="search-handoff-button" title="&aboutPrivateBrowsing.search.placeholder;" tabindex="-1">
+          <div class="fake-textbox">&aboutPrivateBrowsing.search.placeholder;</div>
+          <div id="fake-editable" class="fake-editable" tabindex="-1" aria-hidden="true" contenteditable="" />
+          <div class="fake-caret" />
+        </button>
+        <input id="dummy-input" class="dummy-input" type="search" />
+      </div>
+      <div class="info">
+        <h1>&aboutPrivateBrowsing.info.title;</h1>
+        <p>
+          &aboutPrivateBrowsing.info.description;
+          <br/>
+          <a id="private-browsing-myths">&aboutPrivateBrowsing.info.myths;</a>
+        </p>
+      </div>
+    </div>
   </body>
 </html>
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about.js
@@ -67,8 +67,100 @@ add_task(async function test_links() {
   await testLinkOpensUrl({ win, tab,
     elementId: "startTour",
     expectedUrl: "https://example.com/tour?variation=1",
   });
 
   await BrowserTestUtils.closeWindow(win);
 });
 
+/**
+ * Tests the private-browsing-myths link in "about:privatebrowsing".
+ */
+add_task(async function test_myths_link() {
+  Services.prefs.setCharPref("app.support.baseURL", "https://example.com/");
+  registerCleanupFunction(function() {
+    Services.prefs.clearUserPref("app.support.baseURL");
+  });
+
+  let { win, tab } = await openAboutPrivateBrowsing();
+
+  await testLinkOpensUrl({ win, tab,
+    elementId: "private-browsing-myths",
+    expectedUrl: "https://example.com/private-browsing-myths",
+  });
+
+  await BrowserTestUtils.closeWindow(win);
+});
+
+function urlBarHasHiddenFocus(win) {
+  return win.gURLBar.hasAttribute("focused") && win.gURLBar.classList.contains("hidden-focus");
+}
+
+function urlBarHasNormalFocus(win) {
+  return win.gURLBar.hasAttribute("focused") && !win.gURLBar.classList.contains("hidden-focus");
+}
+
+/**
+ * Tests the search hand-off on character keydown in "about:privatebrowsing".
+ */
+add_task(async function test_search_handoff_on_keydown() {
+  let { win, tab } = await openAboutPrivateBrowsing();
+
+  await ContentTask.spawn(tab, null, async function() {
+    let btn = content.document.getElementById("search-handoff-button");
+    btn.click();
+    ok(btn.classList.contains("focused"), "in-content search has focus styles");
+  });
+  ok(urlBarHasHiddenFocus(win), "url bar has hidden focused");
+  await new Promise(r => EventUtils.synthesizeKey("f", {}, win, r));
+  await ContentTask.spawn(tab, null, async function() {
+    ok(content.document.getElementById("search-handoff-button").classList.contains("hidden"),
+      "in-content search is hidden");
+  });
+  ok(urlBarHasNormalFocus(win), "url bar has normal focused");
+  is(win.gURLBar.value, "@google f", "url bar has search text");
+
+  // Hitting ESC should reshow the in-content search
+  await new Promise(r => EventUtils.synthesizeKey("KEY_Escape", {}, win, r));
+  await ContentTask.spawn(tab, null, async function() {
+    ok(!content.document.getElementById("search-handoff-button").classList.contains("hidden"),
+      "in-content search is not");
+  });
+
+  await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests the search hand-off on composition start in "about:privatebrowsing".
+ */
+add_task(async function test_search_handoff_on_composition_start() {
+  let { win, tab } = await openAboutPrivateBrowsing();
+
+  await ContentTask.spawn(tab, null, async function() {
+    content.document.getElementById("search-handoff-button").click();
+  });
+  ok(urlBarHasHiddenFocus(win), "url bar has hidden focused");
+  await new Promise(r => EventUtils.synthesizeComposition({type: "compositionstart"}, win, r));
+  ok(urlBarHasNormalFocus(win), "url bar has normal focused");
+
+  await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+* Tests the search hand-off on paste in "about:privatebrowsing".
+*/
+add_task(async function test_search_handoff_on_paste() {
+  let { win, tab } = await openAboutPrivateBrowsing();
+
+  await ContentTask.spawn(tab, null, async function() {
+    content.document.getElementById("search-handoff-button").click();
+  });
+  ok(urlBarHasHiddenFocus(win), "url bar has hidden focused");
+  var helper = SpecialPowers.Cc["@mozilla.org/widget/clipboardhelper;1"]
+     .getService(SpecialPowers.Ci.nsIClipboardHelper);
+  helper.copyString("words");
+  await new Promise(r => EventUtils.synthesizeKey("v", {accelKey: true}, win, r));
+  ok(urlBarHasNormalFocus(win), "url bar has normal focused");
+  is(win.gURLBar.value, "@google words", "url bar has search text");
+
+  await BrowserTestUtils.closeWindow(win);
+});
--- a/browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css
+++ b/browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css
@@ -98,8 +98,153 @@ a.button {
   text-decoration: none;
   display: inline-block;
 }
 
 a.button:hover:active {
   color: inherit;
   background-color: #6000a1;
 }
+
+.logo-and-wordmark {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+  margin-bottom: 50px;
+}
+
+.search-ui .container {
+  max-width: 768px;
+}
+
+.logo {
+  background: url("chrome://branding/content/icon128.png") no-repeat center center;
+  background-size: 97px;
+  display: inline-block;
+  height: 97px;
+  width: 97px;
+}
+
+.wordmark {
+  background: url("resource://activity-stream/data/content/assets/firefox-wordmark.svg") no-repeat center center;
+  background-size: 175px;
+  -moz-context-properties: fill;
+  display: inline-block;
+  fill: #fff;
+  height: 97px;
+  margin-inline-start: 15px;
+  width: 175px;
+}
+
+.search-inner-wrapper {
+  display: flex;
+  height: 48px;
+  margin-bottom: 64px;
+  padding: 0 22px;
+}
+
+.search-handoff-button {
+  background: #fff var(--newtab-search-icon) 12px center no-repeat;
+  background-size: 24px;
+  border: solid 1px rgba(249, 249, 250, 0.2);
+  border-radius: 3px;
+  box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.15);
+  cursor: text;
+  font-size: 15px;
+  margin: 0;
+  padding: 0;
+  padding-inline-end: 48px;
+  padding-inline-start: 46px;
+  position: relative;
+  opacity: 1;
+  transition: opacity 500ms;
+  width: 100%;
+}
+
+.search-handoff-button.focused {
+  border: solid 1px #0060df;
+  box-shadow: 0 0 0 1px #0060df, 0 0 0 4px rgba(0, 96, 223, 0.3);
+}
+
+.search-handoff-button.hidden {
+  opacity: 0;
+  visibility: hidden;
+}
+
+.search-handoff-button:dir(rtl) {
+  background-position-x: right 12px;
+}
+
+.search-inner-wrapper .search-handoff-button:hover {
+  background-color: #fff;
+}
+
+.search-handoff-button.focused .fake-caret {
+  display: block;
+}
+
+.fake-editable:focus {
+  outline: none;
+  caret-color: transparent;
+}
+
+.fake-editable {
+  color: transparent;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+
+.fake-textbox {
+  color: rgb(12, 12, 13);
+  opacity: 0.54;
+  text-align: start;
+}
+
+@keyframes caret-animation {
+  to {
+    visibility: hidden;
+  }
+}
+
+.fake-caret {
+  animation: caret-animation 1.3s steps(5, start) infinite;
+  background: rgb(12, 12, 13);
+  display: none;
+  inset-inline-start: 47px;
+  height: 17px;
+  position: absolute;
+  top: 16px;
+  width: 1px;
+}
+
+.dummy-input {
+  display: none;
+}
+
+.info {
+  background-color: rgba(0, 0, 0, 0.2);
+  background-image: url("chrome://browser/skin/privatebrowsing/private-browsing.svg");
+  background-position: left 32px top 20px;
+  background-repeat: no-repeat;
+  background-size: 32px;
+  border-radius: 6px;
+  letter-spacing: -0.2px;
+  padding: 20px;
+  padding-inline-start: 76px;
+}
+
+.info:dir(rtl) {
+  background-position: right 32px top 20px;
+}
+
+.info h1 {
+  font-size: 18px;
+  font-weight: bold;
+  line-height: 28px;
+}
+
+.info p {
+  font-size: 15px;
+  line-height: 25px;
+}
--- a/toolkit/components/remotepagemanager/MessagePort.jsm
+++ b/toolkit/components/remotepagemanager/MessagePort.jsm
@@ -20,17 +20,18 @@ ChromeUtils.defineModuleGetter(this, "Pr
  * whitelisted within AsyncPrefs.jsm.
  */
 let RPMAccessManager = {
   accessMap: {
     "about:privatebrowsing": {
       // "sendAsyncMessage": handled within AboutPrivateBrowsingHandler.jsm
       // "setBoolPref": handled within AsyncPrefs.jsm and uses the pref
       //                ["privacy.trackingprotection.pbmode.enabled"],
-      "getBoolPref": ["privacy.trackingprotection.pbmode.enabled"],
+      "getBoolPref": ["privacy.trackingprotection.pbmode.enabled",
+                      "browser.privatebrowsing.searchUI"],
       "getFormatURLPref": ["privacy.trackingprotection.introURL",
                            "app.support.baseURL"],
       "isWindowPrivate": ["yes"],
     },
   },
 
   checkAllowAccess(aPrincipal, aFeature, aValue) {
     // if there is no content principal; deny access