Bug 1308309 Prompt for webextensions permissions draft
authorAndrew Swan <aswan@mozilla.com>
Tue, 03 Jan 2017 10:55:25 -0800
changeset 455424 af3d107ccdca9eb17b649e5811b5e339fc6690e8
parent 455333 cad2ea346d06ec5a3a70eda912513201dff0c21e
child 455461 2933613458285f4ef39c2e09549a026a8488d67d
push id40230
push useraswan@mozilla.com
push dateTue, 03 Jan 2017 18:58:06 +0000
bugs1308309
milestone53.0a1
Bug 1308309 Prompt for webextensions permissions MozReview-Commit-ID: 6rTGvjKcx3H
browser/base/content/popup-notifications.inc
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_extension_permissions.js
browser/base/content/test/general/browser_webext_nopermissions.xpi
browser/base/content/test/general/browser_webext_permissions.xpi
browser/base/content/test/general/file_install_extensions.html
browser/components/nsBrowserGlue.js
browser/modules/ExtensionsUI.jsm
browser/modules/moz.build
browser/themes/linux/browser.css
browser/themes/osx/browser.css
browser/themes/windows/browser.css
toolkit/components/extensions/Extension.jsm
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -66,8 +66,16 @@
         <progressmeter id="addon-progress-notification-progressmeter"/>
         <label id="addon-progress-notification-progresstext" crop="end"/>
       </popupnotificationcontent>
     </popupnotification>
 
     <popupnotification id="addon-install-confirmation-notification" hidden="true">
       <popupnotificationcontent id="addon-install-confirmation-content" orient="vertical"/>
     </popupnotification>
+
+    <popupnotification id="addon-webext-permissions-notification" hidden="true">
+      <popupnotificationcontent orient="vertical">
+        <description id="addon-webext-perm-header" class="addon-webext-perm-header"/>
+        <label id="addon-webext-perm-text" class="addon-webext-perm-text"/>
+        <html:ul id="addon-webext-perm-list" class="addon-webext-perm-list"/>
+      </popupnotificationcontent>
+    </popupnotification>
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -112,16 +112,19 @@ support-files =
   test_mcb_redirect_image.html
   test_mcb_double_redirect_image.html
   test_mcb_redirect.js
   test_mcb_redirect.sjs
   file_bug1045809_1.html
   file_bug1045809_2.html
   file_csp_block_all_mixedcontent.html
   file_csp_block_all_mixedcontent.js
+  file_install_extensions.html
+  browser_webext_permissions.xpi
+  browser_webext_nopermissions.xpi
   !/image/test/mochitest/blue.png
   !/toolkit/components/passwordmgr/test/browser/form_basic.html
   !/toolkit/components/passwordmgr/test/browser/insecure_test.html
   !/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html
   !/toolkit/content/tests/browser/common/mockTransfer.js
   !/toolkit/modules/tests/browser/metadata_*.html
   !/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi
@@ -293,16 +296,17 @@ skip-if = !datareporting
 skip-if = os == "mac" # decoder doctor isn't implemented on osx
 [browser_devedition.js]
 [browser_discovery.js]
 [browser_double_close_tab.js]
 [browser_documentnavigation.js]
 [browser_duplicateIDs.js]
 [browser_drag.js]
 skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
+[browser_extension_permissions.js]
 [browser_favicon_change.js]
 [browser_favicon_change_not_in_document.js]
 [browser_findbarClose.js]
 [browser_focusonkeydown.js]
 [browser_fullscreen-window-open.js]
 tags = fullscreen
 skip-if = os == "linux" # Linux: Intermittent failures - bug 941575.
 [browser_fxaccounts.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_extension_permissions.js
@@ -0,0 +1,111 @@
+"use strict";
+
+const BASE = getRootDirectory(gTestPath)
+  .replace("chrome://mochitests/content/", "https://example.com/");
+
+const PAGE = `${BASE}/file_install_extensions.html`;
+const PERMS_XPI = `${BASE}/browser_webext_permissions.xpi`;
+const NO_PERMS_XPI = `${BASE}/browser_webext_nopermissions.xpi`;
+const ID = "permissions@test.mozilla.org";
+
+const DEFAULT_EXTENSION_ICON = "chrome://browser/content/extension.svg";
+
+function promisePopupNotificationShown(name) {
+  return new Promise(resolve => {
+    PopupNotifications.panel.addEventListener("popupshown", () => {
+      let notification = PopupNotifications.getNotification(name);
+      ok(notification, `${name} notification shown`);
+      ok(PopupNotifications.isPanelOpen, "notification panel open");
+
+      resolve(PopupNotifications.panel.firstChild);
+    }, {once: true});
+  });
+}
+
+function promiseGetAddonByID(id) {
+  return new Promise(resolve => {
+    AddonManager.getAddonByID(id, resolve);
+  });
+}
+
+function checkNotification(panel, url) {
+  let icon = panel.getAttribute("icon");
+
+  let uls = panel.firstChild.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "ul");
+  is(uls.length, 1, "Found the permissions list");
+  let ul = uls[0];
+
+  let headers = panel.firstChild.getElementsByClassName("addon-webext-perm-text");
+  is(headers.length, 1, "Found the header");
+  let header = headers[0];
+
+  if (url == PERMS_XPI) {
+    // The icon should come from the extension, don't bother with the precise
+    // path, just make sure we've got a jar url pointing to the right path
+    // inside the jar.
+    ok(icon.startsWith("jar:file://"), "Icon is a jar url");
+    ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar");
+
+    is(header.getAttribute("hidden"), "", "Permission list header is visible");
+    is(ul.childElementCount, 4, "Permissions list has 4 entries");
+    // Real checking of the contents here is deferred until bug 1316996 lands
+  } else if (url == NO_PERMS_XPI) {
+    // This extension has no icon, it should have the default
+    is(icon, DEFAULT_EXTENSION_ICON, "Icon is the default extension icon");
+
+    is(header.getAttribute("hidden"), "true", "Permission list header is hidden");
+    is(ul.childElementCount, 0, "Permissions list has 0 entries");
+  }
+}
+
+const INSTALL_FUNCTIONS = [
+  function installMozAM(url) {
+    return ContentTask.spawn(gBrowser.selectedBrowser, url, function*(cUrl) {
+      return content.wrappedJSObject.installMozAM(cUrl);
+    });
+  },
+];
+
+add_task(function* () {
+  yield SpecialPowers.pushPrefEnv({set: [
+    ["extensions.webapi.testing", true],
+    ["extensions.install.requireBuiltInCerts", false],
+
+    // XXX remove this when prompts are enabled by default
+    ["extensions.webextPermissionPrompts", true],
+  ]});
+
+  function* runOnce(installFn, url, cancel) {
+    let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+    let installPromise = installFn(url);
+
+    let panel = yield promisePopupNotificationShown("addon-webext-permissions");
+    checkNotification(panel, url);
+
+    if (cancel) {
+      panel.secondaryButton.click();
+    } else {
+      panel.button.click();
+    }
+
+    let result = yield installPromise;
+    let addon = yield promiseGetAddonByID(ID);
+    if (cancel) {
+      is(result, "onInstallCancelled", "Installation was cancelled");
+      is(addon, null, "Extension is not installed");
+    } else {
+      is(result, "onInstallEnded", "Installation completed");
+      isnot(addon, null, "Extension is installed");
+      addon.uninstall();
+    }
+
+    yield BrowserTestUtils.removeTab(tab);
+  }
+
+  for (let installFn of INSTALL_FUNCTIONS) {
+    yield runOnce(installFn, NO_PERMS_XPI, true);
+    yield runOnce(installFn, PERMS_XPI, true);
+    yield runOnce(installFn, PERMS_XPI, false);
+  }
+});
new file mode 100644
index 0000000000000000000000000000000000000000..ab97d96a113a51cb187d01a6ed3e805b15684b0e
GIT binary patch
literal 4273
zc${@tcQl;q*Bv#Y3lY6{MkmBb^lo&5Xo)gJ@1mFJEqX6O5Ot!LsKFS55WR-cjV_|j
z&%NKe>wY)u-u<rg$9~uIoVCw$_CH%w6$6tD007_s64IDea5vF!f8hfFFNgtvKmMxA
z>q-Na!3um1PH$}O?Jam+-7FT<ZN<LdPz~4!{REQ2MSoVF(5q5droTyIGX9jzYHhhJ
zL>kIb%%dCzC71DhC#m4cG^icM)Mih+1JcOdY!|B;@u=QyzY0X19~)OM-?Y?FGw^42
zmCHvx`J(=YpOq!I%N?76-&VTITo@=tP)htNNG4t<2ry>`UU5fX<PMVhhD%)Vc3TsN
z0zEiX9htu~0XPlx3AQB)IRrjSk|MBhCXXyc%OVvE;D9`T?(T96=H;DIBFJf@9$B25
zxE~>31c;L52F8r#>x-C3BCx!2a?_E87~@@oXuDw^QdBc-z7n%@Mc)8B`~bLX50a1l
zenLL@LE}`?l&7ayWkm7rsC|-{*S&gthO=q%^6ttIx@S5*N-GPXF9DaRSc4lOIQb^7
zbKSS1b$~EKTv~c+QfkYv0w1dfL&+^C8i?*IIzMUw^tFZnwfn;m?Mq>SW#M$6kA-Aa
zZmkkCyDz=)T6fh=Gg<Ro!Wk;|{mWoTS>ao>Zh9Lz=T|O{KliJ_baeMJC3Pi{lAyq6
zlu|*5Hvu>1xWp*{3ryPU(qLi+bIn0*Ih}y1#QQH3Oy?N}T%R+<)So8;>qg`B3amzI
zw4Rh_SquJ}JoBV(a*;i20=Wv8hg4t$2U94&b~xPLl=w){a3nK(MCa3HUkaPS5&0gJ
z4lJ%jrM|(_a!CJC*b9Dlcuo~pYW1`g3pmA8DvPIf?(&EfaJ-c5!MWV6vU#2|SY>A>
z8g!U@%~5DGMTkgThu*VQ%ceTh#vrtjSZ{R|dyvEWNWJpq)+)Ir+r1u|{sumc@A-(4
zeV+<3+Ow5xdvuSYn6LyzcZlusMxRrF`}k+2_~6TVy{uYYFfi~uOqeoi&cEZpL4`bN
z%@d{;87_#^(5OIHzbX{t!%r^EXxHH{KdOA@zNwz?YZCn!S0El-&+U=VTWvum^%-qD
zWhsx{=wLMdq|w8gn&mQ2WQqo-64>anbTda=Qyadb#0e(X!PU<-aMFtD*=)$2Xft=I
zLkL#LZ;P8id09o*$J7%(?KO8aDp6t2(qLfMK@LD0n>ji*bA6NCz3Unxog*iK-3ECS
z-23cqJ~ao_PCXB+E|j2Qa{3X<J6;sp?MpE%du>_dczgriWJE~nfhVRIYx;e@`64R%
zdnx%Wsi%vR9MJ@k0SN|sdAf#?87ok2D{XeO=oeH`$b+--`sW7Xp{ZQW8}=k?TbT3q
z@!#4P_?-t!54;Cnh>l<E%BtXy;iIFfqzWU5dffz-D0!f6#zmKN-(qHzo7|Q{O)P$G
zv9{Hp&3~ZH?+LX*IIjv#GV2<AKb3ncV-gv#2)(L%y+NUL9gQQf=CYxkgu4G|ITrlo
z%!|>m0-_VvI+lDzf8~<lP^E@sUWKg3K~@RsgWKryeVXIiTo{uNq>|;m)5MaZGem4*
zNyDySj`1^Zlx{puZjsb`oGF;zQ#kP&IU6h+V^vz=^T*tao*I4)GRkAFy@Gi|tNoPz
zt)baK#+JP4Srj?72O_@JRL9uQfB#FA0Q4c}4W)>L7WJbczpjCL`?m|Ubz_<(xIm|3
z|4G{kT<Xt7j#vJ8om_?>yHzpSrmAn3Eu9pP6Z>mWh(Z5X)kJOj7YN1r5&4Z-hpPnp
zFVoNDGUH^>PF_2F7KuSm+Kf|r(z7~r;KIiDhBO=OloRVPq}`F{>Ht#`nY=|C%3&bg
z07vH;LkgVZ2sM(JbQ8b@R$UQ%wXZA8XEbcBL^-<VHB7=3X51_9<uw<JtAQo4xcv+!
zyX|M#KAMj0mr72yy)bSBvoqQ-4?Z;O*XagL&%RxlE7<N<?9gv}IqMNu<Giyj(Xg+c
zxRZE#dyF-oC0$@trd%@EZ76|9vQo0O=DUM82_k>!g?io5%+}UDLXMWx8V;{CGZM1w
zj1@VXDo?J9EFx@Cd(my^C92_}JPW}5MJN@dj$#o{xsQ2qlR8Fg4%+HeaiI&^W$!aw
zbP*fHa8$ikaC>dAKy(SGt2F?t80o7h>?C$XXm8{nqdp8>vxhuRDp7=sC#a1dPD_ee
zt6o|X2P~}R!CZSb*&=jv7tLLPuYFDSCe<@R)j!VltTb{wPO3(xFNQyCk&8ro5FE*W
z`y4l`87SQL`bLVAL`F4*^vV2lbnUsqLxpE`k4WZ69eR%ry853ljfq&A8m+H4S_qHQ
z9JlzhFPhn8R-DbyhnK%_;N&~5{t>;AlGuUt(q}TW48jZTi+M_!;}$tEM};MxW;f;U
z8U!!)Ay8VB*JFNw!Q&p^vN!tURJlS1#-aQUrxE?fQbXMKQSH|asL;;?DmmTVpI7b*
zeAqE<4Setr*vn%VgNAqLV>&Id3S(`As(shDR^~czO_Zc0snb$kVuJT~h?AA#C*^d>
zjyc=YuXN{e3L}g^@kn^WL50g7Q?Dd#=)X=I!TnPsWid6Q1}|eYt6S<0lk@U;<|Ca0
zg_PWPp$X5jT_?5u7&+n@gKB)QCz2;(?z=bj3(PM{pF44x0yzYMJ{_i|$w70sYdvSr
z5Pn%?Rp<i_2~D}t3T(=&GS%6Ja0EvSWo}0jDk)7x!UqybS57zk5zpUKpQD3Ga?W?8
z!0L<YSTYnPdqq$(SY~|r@FBB@PwH1B1s^%p;YGHKZ|2J-IyR3GAOF~@97IP~v8`(H
z)<jrxQ}`#Z`y1S6MhTh4HwK-4>6GENj!cRdWn(TNBRt^T17eN^P2OaHJ^;tuFlc{8
z%N5$i0pOsxrZpGSjlE~aKO;@qi#8<-^e)>5P>H2d(5OjE-+z0{A;ODI!Hxfe5)#Fr
z{%#+f-rD2V+YGk*bR-A(wID0;3_qW6MMsvnq~U}u4=ee(*|(&Si0g%x#?`2Y2}i#w
zsZp<3h`Y?CsK5^L-_b~$zIXFy5LW~hm6lRtzr8%SlbbavvR^b|S==*0VK>qv54N5C
z$FiHu#soXMkpd0rY!CWx#Vn4iGpG5BGg1Y9w$&l^iAF`l18&M0%NnCSq6If?P#Khy
z3-T~8(N8L63by&WgacfqJR7lmx^=ah9a5dB*;=`wFIsvzL2NY@aKs*2GAI-dB|Ll3
z_`&B0pW+Iy!+8Vwj*t~<DcQOCP{X1r1_tH+Vv^qVM2mOd344?JvpC{jMuC`F;Yk9o
z^E$ppzL}`HtoWv{2FUnvMbCl?tsN-(GsBM)#s+@UJWlHhMVOrxJ?pQ<VsYgACRqe5
z+JbG%bA^1@OtrM5&!1MA)UMS)5DX`2uWOSXV5#uds!$tntrBlGwa3l_VXeooKILU^
zgcv+a_J?85$A7BrG~%~o_{4veLfD}=db;Wf&no)x3o_h}z4*0e<r=Qyd=$!Mv3{R$
zWVXDuWX(*eAGm>l3C@ATi$abD*=s81GobIz@`Gs(Fbgu={I}CGJJpqjZ4gWBl3WCx
zY)TFA@X8-*OZ;#)t0*_j(_-|6wK<J`J-SB<19O!->h-yLNXWagVc1%Ws>5YFnrwiX
zw`=m{5rkNancz~{;<XJ|$ZEs;^Ek!pIMdP$y`_#hs*4;?IS}L~IgHUuJ;vw6&NrrJ
zLSV0?YCrpxDUEGAz@>eM40JC%5sCM)GX1%dVI5|=fhG4+i&w|Y;gf5EEP%*U3QK%K
zHafua32I#JHVYOI=e)fExT=xUJUt+#?YbC}3AVnTZ^k%8ed-yzx0m7XYUU`guYFYv
zhG)9GYNNEKjlEhndK3#lV;r%DR=(9kVjiE|*O%)V%?ZIs!CFpM!%U)bvQ#C(Z`Q0y
zwHx_}M|)60&@3oaHb3h)UMH>zyj+m8G>qbuQ1p!<vxHKfv8=5R9Xf?Vu3-_o93SI&
z?CM&*OYIW2Te4$MNhBQ^`l2HgOazYNalv~SX-Y%J)|CmD*vi546x*EMfufT}#4jL0
z<F%Y=es63EiXa1}k{mW-<0idGt7+qLwYotk$@PemfRk_8EHo>Fd3bOy6{Ez1st|6i
z-2m1`ugA@yqx?Ijmq~-_EfC6(xpt{IrYRU_T3UtH+Kp*8^vFfzJ6$TYxwxT3f^3V6
zSl?|m*p-N0W>8DGx|I`BH#9TV!iv;J&KsMs<G+z*F%!jF)PUFeR_v!m7}jfXE(=R~
zSw2&05SzJ(_6xM19>O0bx-58KJKFbjahJ_Px7R@L%wZAvNQC5Kl53mD>fD4m$FTQ_
z1c)zixKA4DCNH{8@^lY(O5lgS&KB_|K|aK<PY8}xZ>Z=xa4Pn`n=LC5ER_OU5&|YK
z7I~V(9>|k;SApaE<K;O`auA3Kilq&YDU5f-si`?^IGshr+T^4@f~^^xr}|~a7u=>A
zlkjQo<HgVzCL@M75BMtmdL_=7^L9y;%WKRYBSN$q(1-1wCt*6}it<)R@xE|OCU$%`
zJk6O)`-Z3dkVx|`DdGN^l;ZW7RrtO*V29ta@w+0pD+dclTWc#fcV0U;Cr5|)&xAev
z_+jUux9gPhT{BUSrS<Ki<!@8uf{fM|m9x;WMX&ZH@*iUb#kVhrrSB%|q^-DwoD@>$
z(KKQC2f+rF_Z|Bev&FMnp!jsELl{0QUKYy&-pVb#oroqZjmw#OUhP<Y{8ZX(bBq@=
zpdJb<I<xDX)>9c|57$%@p{;b8rk)=CUxERD3I_Z?Jh&?_tCv74Z+D=DtGlhW#VdC=
zKFxnbL5|>Jp3pm%4){F^{xzwC^@;lMkeVux_ZxvKkZWXkv`(A<yU@z&vFfmTgEs7~
z%8u2;%}tjU%MYPsPd17-A6cbYBFK$7f3;?CVfu<uxuQ|F68mst)znsU1u9M;1Pw-=
z1JnI5=$9kQPTh@-c@tHdxrP9*I_)*inKUM%$uv4Smic42CV~<Z-(S0u8uRi3{wW$y
z{94%Vj``db>A&SyH*35RwNX_bPN2%<KSUpU2uykms%hK}bN0tn7=o2#JUkQ358`eb
z{BgBG+F$YUaAj$F(cumbEg_OL1OlnABX%D(TiIq~S(Rd4v9yt=Um!5C3_jeGXw95)
zbwRt6hK5dt@!#S4F9e|h{v&Gt*#Yq1ru!RQ1ixAE@2mg5{{O?B62LscznkHo;(xcp
p-{SEI|J@wFcldobe|P92{MSuss$yaPaRBS?le&8&W1>IU{{S6!uaW=&
new file mode 100644
index 0000000000000000000000000000000000000000..de43b24888b2ed82f526c6af1455ef8288d0ed52
GIT binary patch
literal 16581
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
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/file_install_extensions.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+  <meta charset="utf-8">
+</head>
+<body>
+<script type="text/javascript">
+function installMozAM(url) {
+  return navigator.mozAddonManager.createInstall({url}).then(install => new Promise(resolve => {
+    const EVENTS = [
+      "onDownloadCancelled",
+      "onDownloadFailed",
+      "onInstallCancelled",
+      "onInstallEnded",
+      "onInstallFailed",
+    ];
+    for (let event of EVENTS) {
+      install.addEventListener(event, () => { resolve(event); });
+    }
+    install.install();
+  }));
+}
+</script>
+</body>
+</html>
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -52,16 +52,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
   ["BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"],
   ["BrowserUsageTelemetry", "resource:///modules/BrowserUsageTelemetry.jsm"],
   ["CaptivePortalWatcher", "resource:///modules/CaptivePortalWatcher.jsm"],
   ["ContentClick", "resource:///modules/ContentClick.jsm"],
   ["ContentPrefServiceParent", "resource://gre/modules/ContentPrefServiceParent.jsm"],
   ["ContentSearch", "resource:///modules/ContentSearch.jsm"],
   ["DateTimePickerHelper", "resource://gre/modules/DateTimePickerHelper.jsm"],
   ["DirectoryLinksProvider", "resource:///modules/DirectoryLinksProvider.jsm"],
+  ["ExtensionsUI", "resource:///modules/ExtensionsUI.jsm"],
   ["Feeds", "resource:///modules/Feeds.jsm"],
   ["FileUtils", "resource://gre/modules/FileUtils.jsm"],
   ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"],
   ["Integration", "resource://gre/modules/Integration.jsm"],
   ["LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"],
   ["LoginHelper", "resource://gre/modules/LoginHelper.jsm"],
   ["LoginManagerParent", "resource://gre/modules/LoginManagerParent.jsm"],
   ["NetUtil", "resource://gre/modules/NetUtil.jsm"],
@@ -1064,16 +1065,18 @@ BrowserGlue.prototype = {
         if (!(addon.permissions & AddonManager.PERM_CAN_ENABLE)) {
           continue;
         }
 
         win.openUILinkIn("about:newaddon?id=" + addon.id, "tab");
       }
     });
 
+    ExtensionsUI.init();
+
     let signingRequired;
     if (AppConstants.MOZ_REQUIRE_SIGNING) {
       signingRequired = true;
     } else {
       signingRequired = Services.prefs.getBoolPref("xpinstall.signatures.required");
     }
 
     if (signingRequired) {
new file mode 100644
--- /dev/null
+++ b/browser/modules/ExtensionsUI.jsm
@@ -0,0 +1,155 @@
+/* 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";
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+this.EXPORTED_SYMBOLS = ["ExtensionsUI"];
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const DEFAULT_EXENSION_ICON = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+this.ExtensionsUI = {
+  init() {
+    Services.obs.addObserver(this, "webextension-permission-prompt", false);
+  },
+
+  observe(subject, topic, data) {
+    if (topic == "webextension-permission-prompt") {
+      let {target, info} = subject.wrappedJSObject;
+      this.showPermissionsPrompt(target, info).then(answer => {
+        Services.obs.notifyObservers(subject, "webextension-permission-response",
+                                     JSON.stringify(answer));
+      });
+    }
+  },
+
+  showPermissionsPrompt(target, info) {
+    let perms = info.addon.userPermissions;
+    if (!perms) {
+      return Promise.resolve();
+    }
+
+    let win = target.ownerGlobal;
+
+    let name = info.addon.name;
+    if (name.length > 50) {
+      name = name.slice(0, 49) + "…";
+    }
+
+    // The strings below are placeholders, they will switch over to the
+    // bundle.get*String() calls as part of bug 1316996.
+
+    // let bundle = win.gNavigatorBundle;
+    // let header = bundle.getFormattedString("webextPerms.header", [name])
+    // let listHeader = bundle.getString("webextPerms.listHeader");
+    let header = "Add ADDON?".replace("ADDON", name);
+    let listHeader = "It can:";
+
+    let formatPermission = perm => {
+      try {
+        // return bundle.getString(`webextPerms.description.${perm}`);
+        return `localized description of permission ${perm}`;
+      } catch (err) {
+        // return bundle.getFormattedString("webextPerms.description.unknown",
+        //                                  [perm]);
+        return `localized description of unknown permission ${perm}`;
+      }
+    };
+
+    let formatHostPermission = perm => {
+      if (perm == "<all_urls>") {
+        // return bundle.getString("webextPerms.hostDescription.allUrls");
+        return "localized description of <all_urls> host permission";
+      }
+      let match = /^[htps*]+:\/\/([^/]+)\//.exec(perm);
+      if (!match) {
+        throw new Error("Unparseable host permission");
+      }
+      if (match[1].startsWith("*.")) {
+        let domain = match[1].slice(2);
+        // return bundle.getFormattedString("webextPerms.hostDescription.wildcard", [domain]);
+        return `localized description of wildcard host permission for ${domain}`;
+      }
+
+      //  return bundle.getFormattedString("webextPerms.hostDescription.oneSite", [match[1]]);
+      return `localized description of single host permission for ${match[1]}`;
+    };
+
+    let msgs = [
+      ...perms.permissions.map(formatPermission),
+      ...perms.hosts.map(formatHostPermission),
+    ];
+
+    // let acceptText = bundle.getString("webextPerms.accept.label");
+    // let acceptKey = bundle.getString("webextPerms.accept.accessKey");
+    // let cancelText = bundle.getString("webextPerms.cancel.label");
+    // let cancelKey = bundle.getString("webextPerms.cancel.accessKey");
+    let acceptText = "Add extension";
+    let acceptKey = "A";
+    let cancelText = "Cancel";
+    let cancelKey = "C";
+
+    let rendered = false;
+    let popupOptions = {
+      hideClose: true,
+      popupIconURL: info.icon,
+      persistent: true,
+
+      eventCallback(topic) {
+        if (topic == "showing") {
+          // This check can be removed when bug 1325223 is resolved.
+          if (rendered) {
+            return false;
+          }
+
+          let doc = this.browser.ownerDocument;
+          doc.getElementById("addon-webext-perm-header").textContent = header;
+
+          let list = doc.getElementById("addon-webext-perm-list");
+          while (list.firstChild) {
+            list.firstChild.remove();
+          }
+
+          let listHeaderEl = doc.getElementById("addon-webext-perm-text");
+          listHeaderEl.value = listHeader;
+          listHeaderEl.hidden = (msgs.length == 0);
+
+          for (let msg of msgs) {
+            let item = doc.createElementNS(HTML_NS, "li");
+            item.textContent = msg;
+            list.appendChild(item);
+          }
+          rendered = true;
+        } else if (topic == "dismissed") {
+          rendered = false;
+        } else if (topic == "swapping") {
+          rendered = false;
+          return true;
+        }
+        return false;
+      },
+    };
+
+    return new Promise(resolve => {
+      win.PopupNotifications.show(target, "addon-webext-permissions", "",
+                                  "addons-notification-icon",
+                                  {
+                                    label: acceptText,
+                                    accessKey: acceptKey,
+                                    callback: () => resolve(true),
+                                  },
+                                  [
+                                    {
+                                      label: cancelText,
+                                      accessKey: cancelKey,
+                                      callback: () => resolve(false),
+                                    },
+                                  ], popupOptions);
+    });
+  },
+};
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -21,16 +21,17 @@ EXTRA_JS_MODULES += [
     'ContentClick.jsm',
     'ContentCrashHandlers.jsm',
     'ContentLinkHandler.jsm',
     'ContentObservers.jsm',
     'ContentSearch.jsm',
     'ContentWebRTC.jsm',
     'DirectoryLinksProvider.jsm',
     'E10SUtils.jsm',
+    'ExtensionsUI.jsm',
     'Feeds.jsm',
     'FormSubmitObserver.jsm',
     'FormValidationHandler.jsm',
     'HiddenFrame.jsm',
     'LaterRun.jsm',
     'NetworkPrioritizer.jsm',
     'offlineAppCache.jsm',
     'PermissionUI.jsm',
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -828,16 +828,21 @@ menuitem.bookmark-item {
   width: 28em;
   max-width: 28em;
 }
 
 .addon-install-confirmation-name {
   font-weight: bold;
 }
 
+.addon-webext-perm-header {
+  font-weight: bold;
+  font-size: 1.3em;
+}
+
 /* Notification icon box */
 
 .notification-anchor-icon:-moz-focusring {
   outline: 1px dotted -moz-DialogText;
 }
 
 /* Translation infobar */
 
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -3086,16 +3086,21 @@ menulist.translate-infobar-element > .me
   width: 28em;
   max-width: 28em;
 }
 
 .addon-install-confirmation-name {
   font-weight: bold;
 }
 
+.addon-webext-perm-header {
+  font-weight: bold;
+  font-size: 1.3em;
+}
+
 /* Status panel */
 
 .statuspanel-label {
   margin: 0;
   padding: 2px 4px;
   background: linear-gradient(#fff, #ddd);
   border: 1px none #ccc;
   border-top-style: solid;
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2131,16 +2131,21 @@ toolbarbutton.bookmark-item[dragover="tr
   width: 28em;
   max-width: 28em;
 }
 
 .addon-install-confirmation-name {
   font-weight: bold;
 }
 
+.addon-webext-perm-header {
+  font-weight: bold;
+  font-size: 1.3em;
+}
+
 /* Notification icon box */
 
 .notification-anchor-icon:-moz-focusring {
   outline: 1px dotted -moz-DialogText;
 }
 
 /* Translation infobar */
 
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -372,16 +372,22 @@ this.ExtensionData = class {
   // manifest.  The current implementation just returns the contents
   // of the permissions attribute, if we add things like url_overrides,
   // they should also be added here.
   userPermissions() {
     let result = {
       hosts: this.whiteListedHosts.pat,
       apis: [...this.apiNames],
     };
+
+    if (Array.isArray(this.manifest.content_scripts)) {
+      for (let entry of this.manifest.content_scripts) {
+        result.hosts.push(...entry.matches);
+      }
+    }
     const EXP_PATTERN = /^experiments\.\w+/;
     result.permissions = [...this.permissions]
       .filter(p => !result.hosts.includes(p) && !EXP_PATTERN.test(p));
     return result;
   }
 
   // Reads the extension's |manifest.json| file, and stores its
   // parsed contents in |this.manifest|.
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -42,16 +42,17 @@ const PREF_EM_HOTFIX_URL              = 
 const PREF_EM_CERT_CHECKATTRIBUTES    = "extensions.hotfix.cert.checkAttributes";
 const PREF_EM_HOTFIX_CERTS            = "extensions.hotfix.certs.";
 const PREF_MATCH_OS_LOCALE            = "intl.locale.matchOS";
 const PREF_SELECTED_LOCALE            = "general.useragent.locale";
 const UNKNOWN_XPCOM_ABI               = "unknownABI";
 
 const PREF_MIN_WEBEXT_PLATFORM_VERSION = "extensions.webExtensionsMinPlatformVersion";
 const PREF_WEBAPI_TESTING             = "extensions.webapi.testing";
+const PREF_WEBEXT_PREF_PROMPTS        = "extensions.webextPermissionPrompts";
 
 const UPDATE_REQUEST_VERSION          = 2;
 const CATEGORY_UPDATE_PARAMS          = "extension-update-params";
 
 const XMLURI_BLOCKLIST                = "http://www.mozilla.org/2006/addons-blocklist";
 
 const KEY_PROFILEDIR                  = "ProfD";
 const KEY_APPDIR                      = "XCurProcD";
@@ -73,16 +74,18 @@ const WEBAPI_TEST_INSTALL_HOSTS = [
 ];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/AsyncShutdown.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
                                   "resource://gre/modules/addons/AddonRepository.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
@@ -2833,16 +2836,40 @@ var AddonManagerInternal = {
       try {
         checkInstallUrl(options.url);
       } catch (err) {
         return Promise.reject({message: err.message});
       }
 
       return AddonManagerInternal.getInstallForURL(options.url, "application/x-xpinstall",
                                                    options.hash).then(install => {
+        if (Preferences.get(PREF_WEBEXT_PREF_PROMPTS, false)) {
+          install._permHandler = info => new Promise((resolve, reject) => {
+            const observer = {
+              observe(subject, topic, data) {
+                if (topic == "webextension-permission-response" &&
+                    subject.wrappedJSObject.info.addon == info.addon) {
+                  let answer = JSON.parse(data);
+                  Services.obs.removeObserver(this, "webextension-permission-response");
+                  if (answer) {
+                    resolve();
+                  } else {
+                    reject();
+                  }
+                }
+              }
+            };
+
+            Services.obs.addObserver(observer, "webextension-permission-response", false);
+
+            let subject = {wrappedJSObject: {target, info}};
+            Services.obs.notifyObservers(subject, "webextension-permission-prompt", null);
+          });
+        }
+
         let id = this.nextInstall++;
         let listener = this.makeListener(id, target.messageManager);
         install.addListener(listener);
 
         this.installs.set(id, {install, target, listener});
 
         let result = {id};
         this.copyProps(install, result);
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -82,16 +82,22 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "amIAddonPathService");
 
 XPCOMUtils.defineLazyGetter(this, "CertUtils", function() {
   let certUtils = {};
   Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
   return certUtils;
 });
 
+XPCOMUtils.defineLazyGetter(this, "IconDetails", () => {
+  const {ExtensionUtils} = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {});
+  return ExtensionUtils.IconDetails;
+});
+
+
 Cu.importGlobalProperties(["URL"]);
 
 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
                                        "initWithPath");
 
 const PREF_DB_SCHEMA                  = "extensions.databaseSchema";
 const PREF_INSTALL_CACHE              = "extensions.installCache";
 const PREF_XPI_STATE                  = "extensions.xpiState";
@@ -5691,29 +5697,42 @@ class AddonInstall {
       this.addon.compatibilityOverrides = repoAddon ?
         repoAddon.compatibilityOverrides :
         null;
       this.addon.appDisabled = !isUsableAddon(this.addon);
       return undefined;
     }).bind(this));
   }
 
+  getIcon(desiredSize = 64) {
+    if (!this.addon.icons || !this.file) {
+      return null;
+    }
+
+    let {icon} = IconDetails.getPreferredIcon(this.addon.icons, null, desiredSize);
+    if (icon.startsWith("chrome://")) {
+      return icon;
+    }
+    return buildJarURI(this.file, icon).spec;
+  }
+
   /**
    * This method should be called when the XPI is ready to be installed,
    * i.e., when a download finishes or when a local file has been verified.
    * It should only be called from install() when the install is in
    * STATE_DOWNLOADED (which actually means that the file is available
    * and has been verified).
    */
   checkPermissions() {
     Task.spawn((function*() {
       if (this.permHandler) {
         let info = {
-          existinAddon: this.existingAddon,
-          addon: this.addon,
+          existingAddon: this.existingAddon ? this.existingAddon.wrapper : null,
+          addon: this.addon.wrapper,
+          icon: this.getIcon(),
         };
 
         try {
           yield this.permHandler(info);
         } catch (err) {
           logger.info(`Install of ${this.addon.id} cancelled since user declined permissions`);
           this.state = AddonManager.STATE_CANCELLED;
           XPIProvider.removeActiveInstall(this);
@@ -7653,16 +7672,20 @@ AddonWrapper.prototype = {
     let addon = addonFor(this);
     let hotfixID = Preferences.get(PREF_EM_HOTFIX_ID, undefined);
     if (hotfixID && hotfixID == addon.id) {
       return false;
     }
     return (addon._installLocation.name == KEY_APP_PROFILE);
   },
 
+  get userPermissions() {
+    return addonFor(this).userPermissions;
+  },
+
   isCompatibleWith(aAppVersion, aPlatformVersion) {
     return addonFor(this).isCompatibleWith(aAppVersion, aPlatformVersion);
   },
 
   uninstall(alwaysAllowUndo) {
     let addon = addonFor(this);
     XPIProvider.uninstallAddon(addon, alwaysAllowUndo);
   },