From d87305d23c00ddd95de0d2bacf9368fa005a4b23 Mon Sep 17 00:00:00 2001 From: Agi Sferro Date: Thu, 5 Dec 2019 23:16:10 +0000 Subject: [PATCH] Bug 1599580 - Implement install/uninstall extension. r=snorp,esawin Differential Revision: https://phabricator.services.mozilla.com/D55730 --HG-- extra : moz-landing-system : lando --- .../components/geckoview/GeckoViewStartup.js | 1 + mobile/android/geckoview/api.txt | 61 ++++ .../web_extensions/borderify-missing-id.xpi | Bin 0 -> 1827 bytes .../web_extensions/borderify-unsigned.xpi | Bin 0 -> 1882 bytes .../assets/web_extensions/borderify.xpi | Bin 0 -> 9221 bytes .../geckoview/test/WebExtensionTest.kt | 137 ++++++++ .../org/mozilla/geckoview/WebExtension.java | 260 +++++++++++++-- .../geckoview/WebExtensionController.java | 295 +++++++++++++---- .../mozilla/geckoview/doc-files/CHANGELOG.md | 9 +- .../geckoview/GeckoViewWebExtension.jsm | 311 +++++++++++++++--- 10 files changed, 937 insertions(+), 137 deletions(-) create mode 100644 mobile/android/geckoview/src/androidTest/assets/web_extensions/borderify-missing-id.xpi create mode 100644 mobile/android/geckoview/src/androidTest/assets/web_extensions/borderify-unsigned.xpi create mode 100644 mobile/android/geckoview/src/androidTest/assets/web_extensions/borderify.xpi diff --git a/mobile/android/components/geckoview/GeckoViewStartup.js b/mobile/android/components/geckoview/GeckoViewStartup.js index 39e8c90ca4d6..8573b2454f55 100644 --- a/mobile/android/components/geckoview/GeckoViewStartup.js +++ b/mobile/android/components/geckoview/GeckoViewStartup.js @@ -74,6 +74,7 @@ GeckoViewStartup.prototype = { "GeckoView:PageAction:Click", "GeckoView:RegisterWebExtension", "GeckoView:UnregisterWebExtension", + "GeckoView:WebExtension:Get", "GeckoView:WebExtension:Disable", "GeckoView:WebExtension:Enable", "GeckoView:WebExtension:Install", diff --git a/mobile/android/geckoview/api.txt b/mobile/android/geckoview/api.txt index f172e7837ef7..2de788f4021f 100644 --- a/mobile/android/geckoview/api.txt +++ b/mobile/android/geckoview/api.txt @@ -1395,6 +1395,7 @@ package org.mozilla.geckoview { field public final long flags; field @NonNull public final String id; field @NonNull public final String location; + field @Nullable public final WebExtension.MetaData metaData; } @AnyThread public static class WebExtension.Action { @@ -1416,6 +1417,16 @@ package org.mozilla.geckoview { method @UiThread @Nullable default public GeckoResult onTogglePopup(@NonNull WebExtension, @NonNull WebExtension.Action); } + public static class WebExtension.BlocklistStateFlags { + ctor public BlocklistStateFlags(); + field public static final int BLOCKED = 2; + field public static final int NOT_BLOCKED = 0; + field public static final int OUTDATED = 3; + field public static final int SOFTBLOCKED = 1; + field public static final int VULNERABLE_NO_UPDATE = 5; + field public static final int VULNERABLE_UPDATE_AVAILABLE = 4; + } + public static class WebExtension.Flags { ctor protected Flags(); field public static final long ALLOW_CONTENT_MESSAGING = 1L; @@ -1427,6 +1438,22 @@ package org.mozilla.geckoview { method @AnyThread @NonNull public GeckoResult get(int); } + public static class WebExtension.InstallException extends Exception { + ctor protected InstallException(); + field public final int code; + } + + public static class WebExtension.InstallException.ErrorCodes { + ctor protected ErrorCodes(); + field public static final int ERROR_CORRUPT_FILE = -3; + field public static final int ERROR_FILE_ACCESS = -4; + field public static final int ERROR_INCORRECT_HASH = -2; + field public static final int ERROR_INCORRECT_ID = -7; + field public static final int ERROR_NETWORK_FAILURE = -1; + field public static final int ERROR_SIGNEDSTATE_REQUIRED = -5; + field public static final int ERROR_UNEXPECTED_ADDON_TYPE = -6; + } + @UiThread public static interface WebExtension.MessageDelegate { method @Nullable default public void onConnect(@NonNull WebExtension.Port); method @Nullable default public GeckoResult onMessage(@NonNull String, @NonNull Object, @NonNull WebExtension.MessageSender); @@ -1443,6 +1470,22 @@ package org.mozilla.geckoview { field @NonNull public final WebExtension webExtension; } + public class WebExtension.MetaData { + ctor protected MetaData(); + field public final int blocklistState; + field @Nullable public final String creatorName; + field @Nullable public final String creatorUrl; + field @Nullable public final String description; + field @Nullable public final String homepageUrl; + field @NonNull public final WebExtension.Icon icon; + field public final boolean isRecommended; + field @Nullable public final String name; + field @NonNull public final String[] origins; + field @NonNull public final String[] permissions; + field public final int signedState; + field @NonNull public final String version; + } + @UiThread public static class WebExtension.Port { ctor protected Port(); method public void disconnect(); @@ -1457,9 +1500,27 @@ package org.mozilla.geckoview { method default public void onPortMessage(@NonNull Object, @NonNull WebExtension.Port); } + public static class WebExtension.SignedStateFlags { + ctor public SignedStateFlags(); + field public static final int MISSING = 0; + field public static final int PRELIMINARY = 1; + field public static final int PRIVILEGED = 4; + field public static final int SIGNED = 2; + field public static final int SYSTEM = 3; + field public static final int UNKNOWN = -1; + } + public class WebExtensionController { + method @UiThread @Nullable public WebExtensionController.PromptDelegate getPromptDelegate(); method @UiThread @Nullable public WebExtensionController.TabDelegate getTabDelegate(); + method @NonNull @AnyThread public GeckoResult install(@NonNull String); + method @UiThread public void setPromptDelegate(@Nullable WebExtensionController.PromptDelegate); method @UiThread public void setTabDelegate(@Nullable WebExtensionController.TabDelegate); + method @NonNull @AnyThread public GeckoResult uninstall(@NonNull WebExtension); + } + + @UiThread public static interface WebExtensionController.PromptDelegate { + method @Nullable default public GeckoResult onInstallPrompt(@NonNull WebExtension); } public static interface WebExtensionController.TabDelegate { diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/borderify-missing-id.xpi b/mobile/android/geckoview/src/androidTest/assets/web_extensions/borderify-missing-id.xpi new file mode 100644 index 0000000000000000000000000000000000000000..19ce0d7f0f73ccbfd02baa580721ccb9983ef7e0 GIT binary patch literal 1827 zcmWIWW@h1H00DpZNIy$wVLLq_8-#fnWEhh2i&9dHGSe#cvWi1PI2o9?%9X_I_q`lj zTEWf0$nuqufq_K?s5T`(ximL5uS72?Kc!NyxTG>C703kXRj^f1GA*c3D9+ExOi?IG zO;NJu3V_@B$hF8Hs2vX2fOcjk=jRpcgKgtDeJQqj?o)KzK>9(paxj4Ovt72^;=#ng z5XHj4zz5e4GDWYrEFEsn=Xwlta)WQ@&2|v@J5{&#!|8iz)9<#Pun3x?v3AOqiIbMS z3Vxc96`NIMsE?q+Ib-JiLW)mFaBiFc7JcscR&)lY|?UjKTy zf46p!xwX@~eX)0H;wG~SKiVC~UHG%W^PT*9&H{l)uTD2jaILbKlJ2x6_jL5x?^pd| z!e<+ueH5h-RN}El=muXyKL72bSvR-(b)5~Kw_&;OpFOL;=^r)w)U`xasDI+$=Mzm% zmc3N`bV0LU?xaBR=AV;8><`Y?zgHP~Deg`G#P7@Yo!!>{C8V$9{Hr2i^@-tOo^wNd zFR^!9efp=maLJUrmz1BpnUQ|<^Zy2cwuLV?-^+^315H?>&hP@LU#lOw*!pfBrauhALJJZ9(W z#qRu~ZyaSW8s1;bY~?;X@VozG4H+xG#ZtR`EII}DT$Fzi!>tnB|B%t;`GR*v3|k%E zO^eRBN>79j@ag;)i-2H{JQJTYBe5*RKvv=O;?L8~8Rf^T@mnbGf&d(>;Gr zheBGc=7ZfYALnVzoWnT#``w#YYS&I!#PxA+#&O*s+sp+z$HU&MRDM0NUgx~>`^7Eq zc;4-H*9$xSu0QfGDG1fU-Nt@ ze*SdQqojm{2VcJwa57zDQ#f;wP37>+x^2vw(~{KB3-Bqdc=c) z>nZOhX`v^wWk&lm6aJnp@G@(Rea;pz@1WJg1$P^F))>}LzrX$U`v>>z;Mb<%*#wmEiM5T zHTikqZ2Nxp(^$JX7}?g>-<#`@fxzAGBF@uXgQx0sS?_I|GRZ8t*F=8O1NN79=c;w( zvE3_Ai(_TDd}xJn+l{*y*i(6LAI{s#;g+FtpzoUZmsfvMI{)3gF)iOn-H<14=Z}Z{ zL2LpK0-fd`6433)Y_k65*CXN3Z4om?l2P7iH|Ng;)AO-e8`J_$cQYK^6)7bg`E*n1 z-GX#Cw_kIFmbjX1JZ-LXGx*fAwl7M#hgCKd#fPf?kB*+YCbo9&^_*Ax^^28mshl&p zouqd!z?+dtju}^hD*+4+1_oewFf3^Vu~3U&R!H%SR`ep9h*_LMOk`jXV0i0T0yGg; zxB|@r6|Pv#!d0{)yKW2IEJQ&|*i6jq0dpV7$1cn;_n~DLd>)557Bf8|J2nScGoYm| Zppl@|h2eizHc+^;0^vMhG~}~_cmT7su^s>b literal 0 HcmV?d00001 diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/borderify-unsigned.xpi b/mobile/android/geckoview/src/androidTest/assets/web_extensions/borderify-unsigned.xpi new file mode 100644 index 0000000000000000000000000000000000000000..fd395d13df1a5fa2b6eb6ed7647841b7ef0ba5a9 GIT binary patch literal 1882 zcmWIWW@h1H00DpZNIy$wVLLq_8-#fnWEhh2i&9dHGSe#cvWi1PI2o9?%9X_I_q`lj zTEWf0$nuqufq_K?s5T`(ximL5uS72?Kc!NyxTG>C703kXRj^f1GA*c3D9+ExOi?IG zO;NJu3V_@B$hF8Hs2vX2fOcjk=jRpcgKgtDeJNJC?kT!$ApIa)IT%3t*)H2{@nB+L zh+<)2;DhT2nW9%*mJT;37-$xnt8;^I=goEy_&Zg%_QUCWY18kvp0Eg-qp^0%mWh*= zy$XJskQJce_Ml<*`u7E!J)+iL$VfXEbM9tpW!;~-lhszf%87T8D|k8a^VLs>pI-lZ zxPP~Hkh!(fyM3{DYT_od3P0K%$6ffd!1JB_dd>oYN3TvdO>nKUn3C?aCHHjn+3#2V zV!~$|oqZIg5LDu^M(74#Lq7lQqggk%`gNTRp0{DS@1H%Zzv&+}`_#2WRj7aB-{%ue zPL{n?{B%LHU+$zp@#deCL+lUE*1uO7c`5Eq|HSXh_MP3<{w1WZ)8C#{*=Vt&tRdV1>B$=AJE3JmWUl02&` z3>N%R^nHFKq;-<@+vCase2IsDahX=`ReTaCDS5iEo$*oIoTTCJ%?u7GW$(Du zCE3a%%FJQG(DUtyaFM;Izz>^%9Y3R8<4)>Y95=OCc#H-qHi2!FB;xo%xvX8JMg>zV+|QAzQt0zd@MQz_FR;I5yPz#-2af#<@thlMGRXV z-c5_%Bl^^TQB-TR=N+!&CE|yA9XH+iu3LKNN7t_oPUj~|yBqj6H1o*34Rg7-m(x9e zPlrNUtmcE=FCXV=%$&nG`}^ISS8CTzSj6>lZ^m)mAlu9ZI>*D_t5kkHv0mrA^83Xt z?|9zrcGn9#{jNXqFDoKFg7Q%Rj!Um^15@WiAQpzFM@VkaHL=hu$b%$G99ii#&xhjY zPbWP}N=SI{^-BRK(gGM( z|NBKx`o`<`U2cCdin)7jW_;}GnSbVYrncUEdTh?~O`dPG%x^N^lil&*-LX2hW1+L2 z@@|qAdLmn9v_CW9@7V$`v&PuxYytBQT0LBFw{d5UVg2;`+h4zbaR2au6CWPf-CtGK zH^YC{zR3;e5BAm9CHu)c2xYXe@FpPEs3lUymIOQhhWu~PTmjH{J z{5){B6`S%j7KG8Vt#5#1UbBI~p3hxZ*o^ezf3nuUKW}1T+{Bu>T8TD6_+;1wY_lYQ@*oe_7exjo}O36sqM}(8>==JefzNV z*B*`L4e_{ zV+qhiSeXno3sfd!H49gXjO@BCaI+BQGhs6^a}~^eARoIh!`z3K$MAU^;#kaNi0s%L fV1$8M_RQ?*uIjGZ()Cr9vK%xFHUz@!@lQf|{lm!G*4V@rXzIdbVJADF#5AHXLO&rl zs8lpX&r~!lGc7mBEGxsPQu$Gtjj>RfW^`;o{sWDy!iVZ@C>RKcmltI@cmy6XEJ5~H zSxm1@`M=%`_4O>s&2sNxr5Bu>lQBw0lgs3N{m z63AQzuD9StzMVgMxHDaDfTBs-W&X5(2evz-o%G)MVI_{01ULR=FruczJ-XkE)pasb z!S0jg^_Qn<{WN-({zDcS0*+1wWI<=`kzv?QHMgs;a9$~uOr;>C=BieOs&Vfp9`A?( zFpbfQ0eWAo-iAEVY_6edDbYdGQ>D{N?g9c^)jWNu$kO-?_}Quc#GHRIwgETgIQFV+ zoMt=U`vYj7`rrEdRurrBylTe|`M>Fp_Id^Iv$d5S^Y2e4J4Z9iSY7J_PLzNbqypVd z$!v+idmGFh2x{79$RqVk-9TN`qN5(kC5lQ_>Vpc>$7u2+G!$RwfxtIcgXvy;ZNJ8G z6m*X2ivUwl?Tp((WuE$i&kkOwpa#gsmyL&`3b~{rn){y&Ip$Z5-vSE=0l75I&mcG! zdi21jkJ!YKQv7chIZ}7?wN8fUzT^ZJq!vog&eMTBwq!7}hOkz^J*9p!WEJQCfE#m>yN4steNhgi(p1m{7(Ez4P>-AuB0q zvsm6I$C1#mbaG(=DD@B%bYZ-aQO|Ll;_>A?%QK3!Bo@xs%()Jp8suiLir$O@^OTU; zNcoIonP{Uq;}pRNX*i zD@&8$EM@qNP#OgFW&&G?{riu(Zy29f0Ug}uXil2~Z_i@}GHce1LN(uMF{9>~lW7vBFfm`GEyfWGsBd{%j5Scy4JD|&1T8H@s__q#EKImZatJTE(wT|w`dNE zDr%3qZ6! zt5=sch_Bx)5Qb=+aBu-O`58wjC@2g)is@uTuf#z_OZ?hTgC&XXK|xvU0u1-Fe2IX8 zh8C+S2>mQAxv@|Z;f{&~HHycq{y5|5p_FOje>U&65Lta*erRHTdhVIN(ebbwW>z(` zBEE3R_G#%?vc7If@?%D@iT~VIZ`w|^%po1u9P~1As=dS39h|w*PJ^+}E=T5^5rhGK!MSpRMgonEt(u#Z4$gW(#A$ zt@3qrJ%}f4>tLY=ecz>f?+vhl&eahlG`E5Cd33agr8huR$0%~q6XZSU830w6 z@qjS*nN34cxg%UCQH@cQ2=}W_f3|Uop$dlf3+`nAo$V$UOII~0(1{n~4n@i&Xqh|7 z5?q`v(AA$l{^aSrl4CpAt#^}G+b)~FAGE4#NjT70B3X{1PWD%JP{vt0?2FG`hBORW z-WOezOrgV(JhodV1A1&N_6t=rbaLErP%9koQlGNy5^A7^fDQ=Pi<@}MU)9l zjIOuPJpb}g#et&wPA2FjFSuEX@oB43jBY|jdUdu1?Jn~Ab}uGzFW+-y<_-m&33uq` z69&S{bhrizaLfI4WvoE1F_(b~tu#Qlso|yzfSGCDV-JVtvq0Jg!i-I0vDwK+m3Tt@ z_t0H8rY}0Z?zjH06aBvr9p?Wtbap_qsgEw)ac(c^W~TU5pMfakv1|dxF6mmst5J+H z!3#y(k>j^UfO2d z+Q#(9JUot1?0&bUo28+}gYayecd*Me2-w*uB3anw2$1lrHh$#taNW{F`=LbQ8LSWh zYFOylOv=ns_AY4JaDTT(ks%QWMB&d>imkV+l`H`XjFF_B8}GAyT|V{Wv&YFN8U%aE zmx0sjKHc&=qR5Gk+ajZ_!`b=PE^Jrv@gm@qJ;iS=HJ?Zt6l zv+r*H*N4q41wSPl@X|3`V(KFD`+GCGCwrKIn|g`XMNssSbEUB!?TgK6g}kk^jyzB3 z`5;_C#fN2+43_gvgB?Gb3NH%dOZmmm%S-Ci-)@tJGx4W6$PW2h-i0K#*gUX(ShGEe zShSZ{Y;c!u`(AT=r}nkQA9i5Og#uW9Sr}lu0ntEbEVH#308GZSDnU@u0E#G>VI*S_ zDoRPKVuusy-V~MpQvM=C)QX0pi-SrLSLQgss>~q;+tH}$c+H^%McvBGkfXL|;T&&? z;9+A|A+!>VBc+cqScs2~GhO8<(rCh@F$rFi;hb*~BTPhOwKXgp*L} zexhqHW0D~9O;`1KtRGIlgm+COJEN~|xUr0XJUd|Yx1&nx9ZSGJl2Ytu>mwvkbrZ~& zOK!-(9#}j;KUZ~Yr+jccdLFW?7dq%Y=&ZnX*nOn>yhM5rWztdudQMY11e?~a@6QQr z=@!lDX&HT82pqJ(+5BafI$0YoPFutaTE8A}d^!+=Uw5VZDNC#g9_@X|)&fy7=Dlwo z(|LKBEpIa*`1(#eT6|J80gLBv> z4tTSOHCGh5h2?J6^zd)re#SI$swl%CDdy7XTi#96H@ij=7I^ZN)Uq;J#!_Jy3s*%C@Q~0@omw$JU=;bw9mrqdIO*9K7kh ze~*WYq#vr9k#CW;Plu+Qo?uANA1T0IZ?lw6WQ0=%o z&-2V_+h%jAO9Y-1x@W)V_@ZD586i+QLm!f(vh9)2gpyHf(YrX{Wz85CfYWVB10H|o8Zb%kQeqR!za69nn7Idu~ z)QK>2=!kbrVqCA=E(ho*IQr5`Ti4S}0G`qD*q>~u zhHHJh*M)cIN?FDvAQTb0G&w&?e~G7Ueae&Yq);bW)h;D=8Vlok18z^GZ4tmy;LI~` zkGTi{76&Ax@%u=*tBJKYNnmGAfhnb&6XKXNEreKql?h zMMxww=#X)(qT+W(_p~+LvN7&Byy)lQ6L@!`3e-b4bEvH%*O-}-zB)Drw`Xk;c5FY`cwC(97yW6{YE0X17`9ZDs(zwsvS7(= zd=+6;RWV+$0*A4%G2KtU?#(3OG>UP;ZrPkm2{bUSjj;Ci12U8_nWcgB*rz-qOX|Kf zh73?FF)@m)R2H&oMfHX>y2Xf9Wm-5sjx_Mt5m5wj57r8&`KJOjfpG}!lHZ8w z!~g>pZ>Tstk0lykRBt|PHMb1yANe=2J(-hUr#fD7 zDzv=ek{a#F{M_f5OB+>W_U2~GN_&lK=nFqkLL`AKdFz=z_fHJKLA7D%c>IpxE!`Fc0jBRJ78_Kopf2> zZ&OGa%1DlEbw24+%0FeBs6K+GimgqD@nwA0m)v6;&5=7!f1=&kTA0>CO-C#Ub43bb zYzK&%xgiin#IkSDsS~m00jw_dp-H?QM=P z^%m79TN>d!)gNSp3O>@uTmt-Cp^8jyh*P8Z>OCZ0vcV7BC7sMNJHyTqJ{oFBg7Sz)NLRkrUs!F*B0uxO=SA+~Vn!U?>oMgb$dS-g`zNkd&EG7Lz zbatrB#;(T8`5}&74US~j*#wr^=MhFD+I`#T`i(S;#F!Qv%%tlj{K_!;b7y&Bt7i*{ zaAq0%q+Q*DH5Z9LO`iY4H_v#+`>n5zAk}O9;=j9o*nhfy|904!K&EjMZ4t%7M6jpn z1(hqiZ0^MaxZd8?T}UD1#OvPf5iIZNPcOMlPqaVTA*XufbH_f~Mi5VHL9r5?7>2%A z8ANGlkpA?jc)JUorm-OKF7@Hhv`I1sJ znwd|_!;Q)o+df90ye?QPl$3!Ud3Y!LrIMC>jFXe4)za`-ZGbUlGNC0QEpIv|E%7_A z0_@8M-mBBDWE-}hXnZ_xt&Z^JP$wsL@XMvO0M}@M)XF+_a%z5j9&LcweR8P>^e}EW zNNh^HnF`O8 z!f{e1Mlk&QME5&4ak6<2RFSS0GHxX$P_ZE~7583nN6*W+CnquT&w;vEw;iq&0*#3p zg{kTInPv8^~zZmo}^lw-*EM3ffs zlG;c3-wXX+0Hz_LWf@H|84&K#>f9b zeIRRBprxfDlbtEjsQj2511+QMG^!jU-RSUGjS9=u`&FcC8}=!O0eK}xrim+s;URfB zMy4v2{siMB4zUw-k2Q_f!MNMl`zA(1J+S|Qu6-)xDt@nz81>rze;2|3 zRl?TJa4OMB4ePF@H<^Os(g;9K3Gh+IPLe@D1Snv@| zMpnR7RukU~!j5Mmm$m~M8Rj8Ek<;H)K07+x95>xmxE;^koh0Afjk;v;T#W9t0+ilF zU{8fDkTlRBWf~;&2fIX3vqD2dNMneT-EjqX8dhhXq9aEQU(L@o8 zZ`@XEMWGeespCj)ycDL)&C~P1R1uI}a`;X%>60cf4@m*>-tSg2gAvz6;_IMoYDG_P zkic-jpZF-8^qPFD5ASN{Le#@vo{3uMeJz=Saw3t^Fq?3y0K-2Saz~Ew6D-;heH+kWDC^DAsV+G!!OBsav z;zzWXR*R1DCP8KkYYkhg&oIr4qXnJ02Vt8nIrAmeR0*9uPAud+S{t)voTG)yd|Y3* z2qsrA>-nQgYh{DU!_pQR{qB0oXvf(7rziE!?{kQo^JU)$1@3$qKc_S$wKlyOKD3!T zq0bCg&(*w@$gO8;rVdDm;LiR=)?X+^q=fVfFhc<9qOx4_dK`+DqIT>v_z^jsVz;>#f&2Cy+9G?q+ zId}6zhJXE?oF17;JM;&~$7FaXu!V;9d~Icb)c0wpJi_A`=mfGh-i2=1NR#ni#U_^l zOdQtDAIk^Iqxflx_2&4Bgd|ly=d<68irV4>(7*EW&~(uY>o65(@S^AHYF`Asa_ zs07G~+7I%LjNxcF25B`Jr8~8L4gjdY`=;kAZmU6dUwv?5hmKSXeKw_KPR=ni>K9^P zZ$F#3quCqu-4xdng&JPLg@eEKCbEL;AA#>NvAY|X2hDhc@6Dq*n^(ob=+>URV=90;OC0K1|E=#~p?Iq5hl@*TT(uW_I zdA3d8OZFr9&16f=SdhAmXTEU0lhco?zIuvY^R z57uO^TLG6=pLT~xq|dtRb`DHJ`H2-NHH8)cB^I+>yf~$Y#9D6-xK?7H#VG}4tcf&^ z3*?&g@ic{H8|gxZ+#~1ePxpBEX5S!irw_|^uxMH|q;2qW|1|CFU~ZYKi^!TL&+y2T zF7sRYa_vF6(naDD#49;$?&2|OEJ|e|lf4cHWZFdMN z|9bZ*n%ldYYzSE5Cr z97J?aZ@!=s2n~5*K0b40^3%=6f}`ALDgkKem-gppwGTV!fv6>CQgHj^aII*QB(u)P zjqixYAH*G3OL`h+k6Gv+KC5)NizU>3(qPYNWc)Pd?7E|OMX|o97_Y=KUAP2gg40q# zb~;ZijHI+s~_vmyY{+F zZN>T3V^viDv1kATzevPk`1Ib26v4Ehlrw&?t@~nyZ6Uc>a-4{CoKc>#A+lj#<_xZ+{ewZs}bSca=&`it8Q0Rp5)^GXxCK* z(Bk*#PpCQvN3ir1r=h-I-SoGJY_(!SK!yKe%|Pd#X+-(}Xxvzgy4RFe+x&t@Md)*KO?OyUCc) zZv!nb_bcaz3yP7UOrM^`XfDQ^ttE~FQ|V2rkrE@9c7p_tT#NJbk7pkJmL{ep_%^=I ze`T#IcHls@LruA|9`$P*j2xR&v~HN;>CBoj(b!@rYAbhjrZq<6q+Gd>KQ3dK9-R|m zHj2Nhq<-o&lM7VZKSLF(U~h-^ho|_)55UFN@`ZfciQWZia8=Wc$$5x+sg+2dupqDWl#}LPf9R z+q$)W?;tH;6+!Ikzo4@5F6&o`wP9qm6}2@{DzK$QuXCQ0VVZZ^%hus)UsJ)-sq{@T z#Yvcz6BF+kWsYiJP)c0XZ7O33FZIW(uJyCq+I(k$z0Npgpan zmz2u7X{lz4Ecc8fO69qxn^87F{Y!B8m?`@Y7(zlHybqma_DP@<1hA)5e+bu|VA%S! z(A)??jy12lObToHG9jMiX zS;w9Bm6}c!PnagsdyJfOjUDUh*K2KfPnC9bap~LbYe_VT(rsW?q#SZ9ccg=-nhM>{ zE;ADh>x{J~QPJ3ecY%13U7NC>7#qecxfH1{$lthrnZF7L& z!#kLA?*GA16jzT{O3^t3{T@Tk@#vF!Pu51^)T~&wz(lc!bv7YbKEs8qyQaY6@Ptu_ z@p07*87#@aAlK^Qmnt{8lr6P#NjzrdF%*}U2ibkKXlD%k))5>`Z^UIqzj@C4+Li<)C*QC-Y(R9`4 zT3tb8F)`D%$}1+=9PBE%&sAE+7-l$rC_*fphvp9k1z>E{j9czY8PRD1NStfzr0 zQHI2AL5zK+ zvc>^nRPn4+wtBvzM%USScDj5&%DH;%jaGY2#%1}q_=aWBiqXUObI&Z)i2-N4`9LJa z-avmxv7fX%^J(ULt#hvatxKf(o;bo$Z{3Ob>H`vpH)oq2GhAD;O6A)j%N(=nS@T`B zRBYKI)MY#e^UxTq@;!kj`q?^KP5h(3coUdPtAF&w;cwcA!Yw^(h0hyq@iwc?<{w!d zySe=8^8PVLTEHpm;JO>L?I_vuUYVj2RM&b`V#aCsGBqHEj=W^o!;#ys<^F>6Z(LfD z`LY-NS|tJhnzd4vgM`9{_^WK=&k~E@WgCB|KZ`j2$@yn-!tWA{zsTzK=YIktzY8+{ z$@*s^`FHigUu5+PpZ{LV@K4@9Q>(wB^k4M!3cLTs`?tjNKLz}m;QO6{{fnq!|3Kz{ z%g6px#-EX*->JaAXdUjKGF}sd|78C&Q1CmP^cO8*{K5Vo8-h&#DdA6){&y=7zjptJ n@WMX@{5dE7{s`;;lYm!fuPg`qsv86Z;_Jct8uO?l``!CLjcb@t literal 0 HcmV?d00001 diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt index 45d1925a70bc..6adaf9adb015 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt @@ -12,8 +12,11 @@ import org.hamcrest.core.StringEndsWith.endsWith import org.hamcrest.core.IsEqual.equalTo import org.json.JSONObject import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Assert.fail +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mozilla.geckoview.* @@ -37,6 +40,16 @@ class WebExtensionTest : BaseSessionTest() { val MESSAGING_CONTENT: String = "resource://android/assets/web_extensions/messaging-content/" } + @Before + fun setup() { + sessionRule.addExternalDelegateUntilTestEnd( + WebExtensionController.PromptDelegate::class, + sessionRule.runtime.webExtensionController::setPromptDelegate, + { sessionRule.runtime.webExtensionController.promptDelegate = null }, + object : WebExtensionController.PromptDelegate {} + ) + } + @Test fun registerWebExtension() { mainSession.loadUri("example.com") @@ -73,6 +86,130 @@ class WebExtensionTest : BaseSessionTest() { colorAfter as String, equalTo("")) } + @Test + fun installWebExtension() { + mainSession.loadUri("example.com") + sessionRule.waitForPageStop() + + // First let's check that the color of the border is empty before loading + // the WebExtension + val colorBefore = mainSession.evaluateJS("document.body.style.borderColor") + assertThat("The border color should be empty when loading without extensions.", + colorBefore as String, equalTo("")) + + sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { + @AssertCalled + override fun onInstallPrompt(extension: WebExtension): GeckoResult { + assertEquals(extension.metaData!!.description, + "Adds a red border to all webpages matching example.com.") + assertEquals(extension.metaData!!.name, "Borderify") + assertEquals(extension.metaData!!.version, "1.0") + // TODO: Bug 1601067 + // assertEquals(extension.isBuiltIn, false) + // TODO: Bug 1599585 + // assertEquals(extension.isEnabled, false) + assertEquals(extension.metaData!!.signedState, + WebExtension.SignedStateFlags.SIGNED) + assertEquals(extension.metaData!!.blocklistState, + WebExtension.BlocklistStateFlags.NOT_BLOCKED) + + return GeckoResult.fromValue(AllowOrDeny.ALLOW) + } + }) + + val borderify = sessionRule.waitForResult( + sessionRule.runtime.webExtensionController.install( + "resource://android/assets/web_extensions/borderify.xpi")) + + mainSession.reload() + sessionRule.waitForPageStop() + + // Check that the WebExtension was applied by checking the border color + val color = mainSession.evaluateJS("document.body.style.borderColor") + assertThat("Content script should have been applied", + color as String, equalTo("red")) + + // Unregister WebExtension and check again + sessionRule.waitForResult(sessionRule.runtime.webExtensionController.uninstall(borderify)) + + mainSession.reload() + sessionRule.waitForPageStop() + + // Check that the WebExtension was not applied after being unregistered + val colorAfter = mainSession.evaluateJS("document.body.style.borderColor") + assertThat("Content script should have been applied", + colorAfter as String, equalTo("")) + } + + private fun testInstallError(name: String, expectedError: Int) { + sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { + @AssertCalled(count = 0) + override fun onInstallPrompt(extension: WebExtension): GeckoResult { + return GeckoResult.fromValue(AllowOrDeny.ALLOW) + } + }) + + sessionRule.waitForResult( + sessionRule.runtime.webExtensionController.install( + "resource://android/assets/web_extensions/$name") + .accept({ + // We should not be able to install unsigned extensions + assertTrue(false) + }, { exception -> + val installException = exception as WebExtension.InstallException + assertEquals(installException.code, expectedError) + })) + } + + @Test + fun installUnsignedExtensionSignatureNotRequired() { + sessionRule.setPrefsUntilTestEnd(mapOf( + "xpinstall.signatures.required" to false + )) + + sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { + override fun onInstallPrompt(extension: WebExtension): GeckoResult { + return GeckoResult.fromValue(AllowOrDeny.ALLOW) + } + }) + + val borderify = sessionRule.waitForResult( + sessionRule.runtime.webExtensionController.install( + "resource://android/assets/web_extensions/borderify-unsigned.xpi") + .then { extension -> + assertEquals(extension!!.metaData!!.signedState, + WebExtension.SignedStateFlags.MISSING) + assertEquals(extension.metaData!!.blocklistState, + WebExtension.BlocklistStateFlags.NOT_BLOCKED) + assertEquals(extension.metaData!!.name, "Borderify") + GeckoResult.fromValue(extension) + }) + + sessionRule.waitForResult( + sessionRule.runtime.webExtensionController.uninstall(borderify)) + } + + @Test + fun installUnsignedExtensionSignatureRequired() { + sessionRule.setPrefsUntilTestEnd(mapOf( + "xpinstall.signatures.required" to true + )) + testInstallError("borderify-unsigned.xpi", + WebExtension.InstallException.ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED) + } + + @Test + fun installExtensionFileNotFound() { + testInstallError("file-not-found.xpi", + WebExtension.InstallException.ErrorCodes.ERROR_NETWORK_FAILURE) + } + + @Test + fun installExtensionMissingId() { + testInstallError("borderify-missing-id.xpi", + WebExtension.InstallException.ErrorCodes.ERROR_CORRUPT_FILE) + } + // This test // - Registers a web extension // - Listens for messages and waits for a message diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java index 32c6552f47cd..78e8e9c59edc 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java @@ -54,8 +54,9 @@ public class WebExtension { */ public final @WebExtensionFlags long flags; - // TODO: make public - final MetaData metaData; + /** Provides information about this {@link WebExtension}. */ + // TODO: move to @NonNull when we remove registerWebExtension + public final @Nullable MetaData metaData; // TODO: make public final boolean isBuiltIn; @@ -63,6 +64,26 @@ public class WebExtension { // TODO: make public final boolean isEnabled; + /** Called whenever a delegate is set or unset on this {@link WebExtension} instance. + /* package */ interface DelegateObserver { + void onMessageDelegate(final String nativeApp, final MessageDelegate delegate); + void onActionDelegate(final ActionDelegate delegate); + } + + private WeakReference mDelegateObserver = new WeakReference<>(null); + + /* package */ void setDelegateObserver(final DelegateObserver observer) { + mDelegateObserver = new WeakReference<>(observer); + + if (observer != null) { + // Notify observers of already attached delegates + for (final Map.Entry entry : messageDelegates.entrySet()) { + observer.onMessageDelegate(entry.getKey(), entry.getValue()); + } + observer.onActionDelegate(actionDelegate); + } + } + /** * Delegates that handle messaging between this WebExtension and the app. */ @@ -219,6 +240,10 @@ public class WebExtension { @UiThread public void setMessageDelegate(final @Nullable MessageDelegate messageDelegate, final @NonNull String nativeApp) { + final DelegateObserver observer = mDelegateObserver.get(); + if (observer != null) { + observer.onMessageDelegate(nativeApp, messageDelegate); + } if (messageDelegate == null) { messageDelegates.remove(nativeApp); return; @@ -1022,6 +1047,58 @@ public class WebExtension { } } + /** Extension thrown when an error occurs during extension installation. */ + public static class InstallException extends Exception { + public static class ErrorCodes { + /** The download failed due to network problems. */ + public static final int ERROR_NETWORK_FAILURE = -1; + /** The downloaded file did not match the provided hash. */ + public static final int ERROR_INCORRECT_HASH = -2; + /** The downloaded file seems to be corrupted in some way. */ + public static final int ERROR_CORRUPT_FILE = -3; + /** An error occurred trying to write to the filesystem. */ + public static final int ERROR_FILE_ACCESS = -4; + /** The extension must be signed and isn't. */ + public static final int ERROR_SIGNEDSTATE_REQUIRED = -5; + /** The downloaded extension had a different type than expected. */ + public static final int ERROR_UNEXPECTED_ADDON_TYPE = -6; + /** The extension did not have the expected ID. */ + public static final int ERROR_INCORRECT_ID = -7; + + /** For testing. */ + protected ErrorCodes() {} + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + ErrorCodes.ERROR_NETWORK_FAILURE, + ErrorCodes.ERROR_INCORRECT_HASH, + ErrorCodes.ERROR_CORRUPT_FILE, + ErrorCodes.ERROR_FILE_ACCESS, + ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED, + ErrorCodes.ERROR_UNEXPECTED_ADDON_TYPE, + ErrorCodes.ERROR_INCORRECT_ID + }) + /* package */ @interface Codes {} + + /** One of {@link ErrorCodes} that provides more information about this exception. */ + public final @Codes int code; + + /** For testing */ + protected InstallException() { + this.code = ErrorCodes.ERROR_NETWORK_FAILURE; + } + + @Override + public String toString() { + return "InstallException: " + code; + } + + /* package */ InstallException(final @Codes int code) { + this.code = code; + } + } + /** * Set the Action delegate for this WebExtension. * @@ -1035,6 +1112,11 @@ public class WebExtension { */ @AnyThread public void setActionDelegate(final @Nullable ActionDelegate delegate) { + final DelegateObserver observer = mDelegateObserver.get(); + if (observer != null) { + observer.onActionDelegate(delegate); + } + actionDelegate = delegate; final GeckoBundle bundle = new GeckoBundle(1); @@ -1044,15 +1126,27 @@ public class WebExtension { "GeckoView:ActionDelegate:Attached", bundle); } - // TODO: make public - // Keep in sync with AddonManager.jsm - static class SignedStateFlags { - final static int UNKNOWN = -1; - final static int MISSING = 0; - final static int PRELIMINARY = 1; - final static int SIGNED = 2; - final static int SYSTEM = 3; - final static int PRIVILEGED = 4; + /** Describes the signed status for a {@link WebExtension}. + * + * See + * Add-on signing in Firefox. + * + */ + public static class SignedStateFlags { + // Keep in sync with AddonManager.jsm + /** This extension may be signed but by a certificate that doesn't + * chain to our our trusted certificate. */ + public final static int UNKNOWN = -1; + /** This extension is unsigned. */ + public final static int MISSING = 0; + /** This extension has been preliminarily reviewed. */ + public final static int PRELIMINARY = 1; + /** This extension has been fully reviewed. */ + public final static int SIGNED = 2; + /** This extension is a system add-on. */ + public final static int SYSTEM = 3; + /** This extension is signed with a "Mozilla Extensions" certificate. */ + public final static int PRIVILEGED = 4; /* package */ final static int LAST = PRIVILEGED; } @@ -1062,15 +1156,27 @@ public class WebExtension { SignedStateFlags.SIGNED, SignedStateFlags.SYSTEM, SignedStateFlags.PRIVILEGED}) @interface SignedState {} - // TODO: make public - // Keep in sync with nsIBlocklistService.idl - static class BlocklistStateFlags { - final static int NOT_BLOCKED = 0; - final static int SOFTBLOCKED = 1; - final static int BLOCKED = 2; - final static int OUTDATED = 3; - final static int VULNERABLE_UPDATE_AVAILABLE = 4; - final static int VULNERABLE_NO_UPDATE = 5; + /** Describes the blocklist state for a {@link WebExtension}. + * See + * Add-ons that cause stability or security issues are put on a blocklist + * . + */ + public static class BlocklistStateFlags { + // Keep in sync with nsIBlocklistService.idl + /** This extension does not appear in the blocklist. */ + public final static int NOT_BLOCKED = 0; + /** This extension is in the blocklist but the problem is not severe + * enough to warant forcibly blocking. */ + public final static int SOFTBLOCKED = 1; + /** This extension should be blocked and never used. */ + public final static int BLOCKED = 2; + /** This extension is considered outdated, and there is a known update + * available. */ + public final static int OUTDATED = 3; + /** This extension is vulnerable and there is an update. */ + public final static int VULNERABLE_UPDATE_AVAILABLE = 4; + /** This extension is vulnerable and there is no update. */ + public final static int VULNERABLE_NO_UPDATE = 5; } @Retention(RetentionPolicy.SOURCE) @@ -1080,22 +1186,106 @@ public class WebExtension { BlocklistStateFlags.VULNERABLE_NO_UPDATE}) @interface BlocklistState {} - // TODO: make public - class MetaData { - final Icon icon; - final String[] permissions; - final String[] origins; - final String name; - final String description; - final String version; - final String creatorName; - final String creatorUrl; - final String homepageUrl; - final String optionsPageUrl; + /** Provides information about a {@link WebExtension}. */ + public class MetaData { + /** Main {@link Icon} branding for this {@link WebExtension}. + * Can be used when displaying prompts. */ + public final @NonNull Icon icon; + /** API permissions requested or granted to this extension. + * + * Permission identifiers match entries in the manifest, see + * + * API permissions + * . + */ + public final @NonNull String[] permissions; + /** Host permissions requested or granted to this extension. + * + * See + * Host permissions + * . + */ + public final @NonNull String[] origins; + /** Branding name for this extension. + * + * See + * manifest.json/name + * + */ + public final @Nullable String name; + /** Branding description for this extension. This string will be + * localized using the current GeckoView language setting. + * + * See + * manifest.json/description + * + */ + public final @Nullable String description; + /** Version string for this extension. + * + * See + * manifest.json/version + * + */ + public final @NonNull String version; + /** Creator name as provided in the manifest. + * + * See + * manifest.json/developer + * + */ + public final @Nullable String creatorName; + /** Creator url as provided in the manifest. + * + * See + * manifest.json/developer + * + */ + public final @Nullable String creatorUrl; + /** Homepage url as provided in the manifest. + * + * See + * manifest.json/homepage_url + * + */ + public final @Nullable String homepageUrl; + /** Options page as provided in the manifest. + * + * See + * manifest.json/options_ui + * + */ + // TODO: Bug 1598792 + final @Nullable String optionsPageUrl; + /** Whether the options page should be open in a Tab or not. + * + * See + * manifest.json/options_ui#Syntax + * + */ + // TODO: Bug 1598792 final boolean openOptionsPageInTab; - final boolean isRecommended; - final @BlocklistState int blocklistState; - final @SignedState int signedState; + /** Whether or not this is a recommended extension. + * + * See + * Recommended Extensions program + * + */ + public final boolean isRecommended; + /** Blocklist status for this extension. + * + * See + * Add-ons that cause stability or security issues are put on a blocklist + * . + */ + public final @BlocklistState int blocklistState; + /** Signed status for this extension. + * + * See + * Add-on signing in Firefox. + * . + */ + public final @SignedState int signedState; /** Override for testing. */ protected MetaData() { diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java index 53fd84e4b87d..307d3ec84c4a 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java @@ -1,5 +1,6 @@ package org.mozilla.geckoview; +import android.support.annotation.AnyThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; @@ -22,8 +23,6 @@ public class WebExtensionController { private GeckoRuntime mRuntime; - private boolean mHandlerRegistered = false; - private TabDelegate mTabDelegate; private PromptDelegate mPromptDelegate; @@ -33,12 +32,18 @@ public class WebExtensionController { public GeckoResult get(final String id) { final WebExtension extension = mData.get(id); if (extension == null) { - if (BuildConfig.DEBUG) { - // TODO: Bug 1582185 Some gecko tests install WebExtensions that we - // don't know about and cause this to trigger. - // throw new RuntimeException("Could not find extension: " + extensionId); - } - Log.e(LOGTAG, "Could not find extension: " + id); + final WebExtensionResult result = new WebExtensionResult("extension"); + + final GeckoBundle bundle = new GeckoBundle(1); + bundle.putString("extensionId", id); + + EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Get", + bundle, result); + + return result.then(ext -> { + mData.put(ext.id, ext); + return GeckoResult.fromValue(ext); + }); } return GeckoResult.fromValue(extension); @@ -48,7 +53,6 @@ public class WebExtensionController { mData.remove(id); } - // TODO: remove once registerWebExtension is removed public void put(final String id, final WebExtension extension) { mData.put(id, extension); } @@ -62,19 +66,54 @@ public class WebExtensionController { // Avoids exposing listeners to the API private class Internals implements BundleEventListener, - WebExtension.Port.DisconnectDelegate { + WebExtension.Port.DisconnectDelegate, + WebExtension.DelegateObserver { + private boolean mMessageListenersAttached = false; + private boolean mActionListenersAttached = false; + @Override + // BundleEventListener public void handleMessage(final String event, final GeckoBundle message, final EventCallback callback) { WebExtensionController.this.handleMessage(event, message, callback, null); } @Override + // WebExtension.Port.DisconnectDelegate public void onDisconnectFromApp(final WebExtension.Port port) { // If the port has been disconnected from the app side, we don't need to notify anyone and // we just need to remove it from our list of ports. mPorts.remove(port.id); } + + @Override + // WebExtension.DelegateObserver + public void onMessageDelegate(final String nativeApp, + final WebExtension.MessageDelegate delegate) { + if (delegate != null && !mMessageListenersAttached) { + EventDispatcher.getInstance().registerUiThreadListener( + this, + "GeckoView:WebExtension:Message", + "GeckoView:WebExtension:PortMessage", + "GeckoView:WebExtension:Connect", + "GeckoView:WebExtension:Disconnect"); + mMessageListenersAttached = true; + } + } + + @Override + // WebExtension.DelegateObserver + public void onActionDelegate(final WebExtension.ActionDelegate delegate) { + if (delegate != null && !mActionListenersAttached) { + EventDispatcher.getInstance().registerUiThreadListener( + this, + "GeckoView:BrowserAction:Update", + "GeckoView:BrowserAction:OpenPopup", + "GeckoView:PageAction:Update", + "GeckoView:PageAction:OpenPopup"); + mActionListenersAttached = true; + } + } } public interface TabDelegate { @@ -139,31 +178,64 @@ public class WebExtensionController { mTabDelegate = delegate; } - // TODO: make public - interface PromptDelegate { - default GeckoResult onInstallPrompt(WebExtension extension) { + /** + * This delegate will be called whenever an extension is about to be installed or it needs + * new permissions, e.g during an update or because it called permissions.request + */ + @UiThread + public interface PromptDelegate { + /** + * Called whenever a new extension is being installed. This is intended as an + * opportunity for the app to prompt the user for the permissions required by + * this extension. + * + * @param extension The {@link WebExtension} that is about to be installed. + * You can use {@link WebExtension#metaData} to gather information + * about this extension when building the user prompt dialog. + * @return A {@link GeckoResult} that completes to either {@link AllowOrDeny#ALLOW ALLOW} + * if this extension should be installed or {@link AllowOrDeny#DENY DENY} if + * this extension should not be installed. A null value will be interpreted as + * {@link AllowOrDeny#DENY DENY}. + */ + @Nullable + default GeckoResult onInstallPrompt(final @NonNull WebExtension extension) { return null; } + + /* + TODO: Bug 1599581 default GeckoResult onUpdatePrompt( WebExtension currentlyInstalled, WebExtension updatedExtension, String[] newPermissions) { return null; } + TODO: Bug 1601420 default GeckoResult onOptionalPrompt( WebExtension extension, String[] optionalPermissions) { return null; - } + } */ } - // TODO: make public - PromptDelegate getPromptDelegate() { + /** + * @return the current {@link PromptDelegate} instance. + * @see PromptDelegate + */ + @UiThread + @Nullable + public PromptDelegate getPromptDelegate() { return mPromptDelegate; } - // TODO: make public - void setPromptDelegate(final PromptDelegate delegate) { + /** Set the {@link PromptDelegate} for this instance. This delegate will be used + * to be notified whenever an extension is being installed or needs new permissions. + * + * @param delegate the delegate instance. + * @see PromptDelegate + */ + @UiThread + public void setPromptDelegate(final @Nullable PromptDelegate delegate) { if (delegate == null && mPromptDelegate != null) { EventDispatcher.getInstance().unregisterUiThreadListener( mInternals, @@ -183,7 +255,8 @@ public class WebExtensionController { mPromptDelegate = delegate; } - private static class WebExtensionResult extends CallbackResult { + private static class WebExtensionResult extends GeckoResult + implements EventCallback { private final String mFieldName; public WebExtensionResult(final String fieldName) { @@ -195,11 +268,59 @@ public class WebExtensionController { final GeckoBundle bundle = (GeckoBundle) response; complete(new WebExtension(bundle.getBundle(mFieldName))); } + + @Override + public void sendError(final Object response) { + if (response instanceof GeckoBundle + && ((GeckoBundle) response).containsKey("installError")) { + final GeckoBundle bundle = (GeckoBundle) response; + final int errorCode = bundle.getInt("installError"); + completeExceptionally(new WebExtension.InstallException(errorCode)); + } else { + completeExceptionally(new Exception(response.toString())); + } + } } - // TODO: make public - GeckoResult install(final String uri) { - final CallbackResult result = new WebExtensionResult("extension"); + /** + * Install an extension. + * + * An installed extension will persist and will be available even when restarting the + * {@link GeckoRuntime}. + * + * Installed extensions through this method need to be signed by Mozilla, see + * + * Distributing your add-on + * . + * + * When calling this method, the GeckoView library will download the extension, validate + * its manifest and signature, and give you an opportunity to verify its permissions through + * {@link PromptDelegate#installPrompt}, you can use this method to prompt the user if + * appropriate. + * + * @param uri URI to the extension's .xpi package. This can be a remote + * https: URI or a local file: or resource: + * URI. Note: the app needs the appropriate permissions for local URIs. + * + * @return A {@link GeckoResult} that will complete when the installation process finishes. + * For successful installations, the GeckoResult will return the {@link WebExtension} + * object that you can use to set delegates and retrieve information about the + * WebExtension using {@link WebExtension#metaData}. + * + * If an error occurs during the installation process, the GeckoResult will complete + * exceptionally with a + * {@link WebExtension.InstallException InstallException} that will contain + * the relevant error code in + * {@link WebExtension.InstallException#code InstallException#code}. + * + * @see PromptDelegate#installPrompt + * @see WebExtension.InstallException.ErrorCodes + * @see WebExtension#metaData + */ + @NonNull + @AnyThread + public GeckoResult install(final @NonNull String uri) { + final WebExtensionResult result = new WebExtensionResult("extension"); final GeckoBundle bundle = new GeckoBundle(1); bundle.putString("locationUri", uri); @@ -207,12 +328,15 @@ public class WebExtensionController { EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Install", bundle, result); - return result; + return result.then(extension -> { + registerWebExtension(extension); + return GeckoResult.fromValue(extension); + }); } - // TODO: make public + // TODO: Bug 1601067 make public GeckoResult installBuiltIn(final String uri) { - final CallbackResult result = new WebExtensionResult("extension"); + final WebExtensionResult result = new WebExtensionResult("extension"); final GeckoBundle bundle = new GeckoBundle(1); bundle.putString("locationUri", uri); @@ -220,11 +344,25 @@ public class WebExtensionController { EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:InstallBuiltIn", bundle, result); - return result; + return result.then(extension -> { + registerWebExtension(extension); + return GeckoResult.fromValue(extension); + }); } - // TODO: make public - GeckoResult uninstall(final WebExtension extension) { + /** + * Uninstall an extension. + * + * Uninstalling an extension will remove it from the current {@link GeckoRuntime} instance, + * delete all its data and trigger a request to close all extension pages currently open. + * + * @param extension The {@link WebExtension} to be uninstalled. + * + * @return A {@link GeckoResult} that will complete when the uninstall process is completed. + */ + @NonNull + @AnyThread + public GeckoResult uninstall(final @NonNull WebExtension extension) { final CallbackResult result = new CallbackResult() { @Override public void sendSuccess(final Object response) { @@ -232,6 +370,8 @@ public class WebExtensionController { } }; + unregisterWebExtension(extension); + final GeckoBundle bundle = new GeckoBundle(1); bundle.putString("webExtensionId", extension.id); @@ -241,9 +381,9 @@ public class WebExtensionController { return result; } - // TODO: make public + // TODO: Bug 1599585 make public GeckoResult enable(final WebExtension extension) { - final CallbackResult result = new WebExtensionResult("extension"); + final WebExtensionResult result = new WebExtensionResult("extension"); final GeckoBundle bundle = new GeckoBundle(1); bundle.putString("webExtensionId", extension.id); @@ -251,12 +391,15 @@ public class WebExtensionController { EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Enable", bundle, result); - return result; + return result.then(newExtension -> { + registerWebExtension(newExtension); + return GeckoResult.fromValue(newExtension); + }); } - // TODO: make public + // TODO: Bug 1599585 make public GeckoResult disable(final WebExtension extension) { - final CallbackResult result = new WebExtensionResult("extension"); + final WebExtensionResult result = new WebExtensionResult("extension"); final GeckoBundle bundle = new GeckoBundle(1); bundle.putString("webExtensionId", extension.id); @@ -264,10 +407,13 @@ public class WebExtensionController { EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Disable", bundle, result); - return result; + return result.then(newExtension -> { + registerWebExtension(newExtension); + return GeckoResult.fromValue(newExtension); + }); } - // TODO: make public + // TODO: Bug 1600742 make public GeckoResult> listInstalled() { final CallbackResult> result = new CallbackResult>() { @Override @@ -276,7 +422,9 @@ public class WebExtensionController { .getBundleArray("extensions"); final List list = new ArrayList<>(bundles.length); for (GeckoBundle bundle : bundles) { - list.add(new WebExtension(bundle)); + final WebExtension extension = new WebExtension(bundle); + registerWebExtension(extension); + list.add(extension); } complete(list); @@ -289,9 +437,9 @@ public class WebExtensionController { return result; } - // TODO: make public + // TODO: Bug 1599581 make public GeckoResult update(final WebExtension extension) { - final CallbackResult result = new WebExtensionResult("extension"); + final WebExtensionResult result = new WebExtensionResult("extension"); final GeckoBundle bundle = new GeckoBundle(1); bundle.putString("webExtensionId", extension.id); @@ -299,7 +447,10 @@ public class WebExtensionController { EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Update", bundle, result); - return result; + return result.then(newExtension -> { + registerWebExtension(newExtension); + return GeckoResult.fromValue(newExtension); + }); } /* package */ WebExtensionController(final GeckoRuntime runtime) { @@ -307,23 +458,7 @@ public class WebExtensionController { } /* package */ void registerWebExtension(final WebExtension webExtension) { - if (!mHandlerRegistered) { - EventDispatcher.getInstance().registerUiThreadListener( - mInternals, - "GeckoView:WebExtension:Message", - "GeckoView:WebExtension:PortMessage", - "GeckoView:WebExtension:Connect", - "GeckoView:WebExtension:Disconnect", - - // {Browser,Page}Actions - "GeckoView:BrowserAction:Update", - "GeckoView:BrowserAction:OpenPopup", - "GeckoView:PageAction:Update", - "GeckoView:PageAction:OpenPopup" - ); - mHandlerRegistered = true; - } - + webExtension.setDelegateObserver(mInternals); mExtensions.put(webExtension.id, webExtension); } @@ -350,6 +485,9 @@ public class WebExtensionController { } else if ("GeckoView:PageAction:OpenPopup".equals(event)) { openPopup(message, session, WebExtension.Action.TYPE_PAGE_ACTION); return; + } else if ("GeckoView:WebExtension:InstallPrompt".equals(event)) { + installPrompt(message, callback); + return; } final String nativeApp = message.getString("nativeApp"); @@ -381,6 +519,42 @@ public class WebExtensionController { }); } + private void installPrompt(final GeckoBundle message, final EventCallback callback) { + final GeckoBundle extensionBundle = message.getBundle("extension"); + if (extensionBundle == null || !extensionBundle.containsKey("webExtensionId") + || !extensionBundle.containsKey("locationURI")) { + if (BuildConfig.DEBUG) { + throw new RuntimeException("Missing webExtensionId or locationURI"); + } + + Log.e(LOGTAG, "Missing webExtensionId or locationURI"); + return; + } + + final WebExtension extension = new WebExtension(extensionBundle); + + if (mPromptDelegate == null) { + Log.e(LOGTAG, "Tried to install extension " + extension.id + + " but no delegate is registered"); + return; + } + + final GeckoResult promptResponse = mPromptDelegate.onInstallPrompt(extension); + if (promptResponse == null) { + return; + } + + promptResponse.accept(allowOrDeny -> { + GeckoBundle response = new GeckoBundle(1); + if (AllowOrDeny.ALLOW.equals(allowOrDeny)) { + response.putBoolean("allow", true); + } else { + response.putBoolean("allow", false); + } + callback.sendSuccess(response); + }); + } + private void newTab(final GeckoBundle message, final EventCallback callback) { if (mTabDelegate == null) { callback.sendSuccess(null); @@ -411,8 +585,12 @@ public class WebExtensionController { return; } - mExtensions.get(message.getString("extensionId")).then(extension -> - mTabDelegate.onCloseTab(extension, session) + mExtensions.get(message.getString("extensionId")).then( + extension -> mTabDelegate.onCloseTab(extension, session), + // On uninstall, we close all extension pages, in that case + // the extension object may be gone already so we can't + // send it to the delegate + exception -> mTabDelegate.onCloseTab(null, session) ).accept(value -> { if (value == AllowOrDeny.ALLOW) { callback.sendSuccess(null); @@ -425,6 +603,7 @@ public class WebExtensionController { /* package */ void unregisterWebExtension(final WebExtension webExtension) { mExtensions.remove(webExtension.id); + webExtension.setDelegateObserver(null); // Some ports may still be open so we need to go through the list and close all of the // ports tied to this web extension diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md index ff44f4beabb3..edebcd0ba7f4 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md @@ -13,6 +13,13 @@ exclude: true ⚠️ breaking change +## v73 +- Added [`WebExtensionController.install`][73.1] and [`uninstall`][73.2] to + manage installed extensions + +[73.1]: {{javadoc_uri}}/WebExtensionController.html#install-java.lang.String- +[73.2]: {{javadoc_uri}}/WebExtensionController.html#uninstall-org.mozilla.geckoview.WebExtension- + ## v72 - Added [`GeckoSession.NavigationDelegate.LoadRequest#hasUserGesture`][72.1]. This indicates if a load was requested while a user gesture was active (e.g., a tap). @@ -476,4 +483,4 @@ exclude: true [65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String- [65.25]: {{javadoc_uri}}/GeckoResult.html -[api-version]: 4c9f04038d8478206efac05b518920819faeacea +[api-version]: 5856cdf682140fafdd09d74dbc004bf0b6bb7398 diff --git a/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm b/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm index e39a6f35abbc..2fcff5007024 100644 --- a/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm +++ b/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm @@ -19,12 +19,20 @@ const { GeckoViewUtils } = ChromeUtils.import( const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.jsm", EventDispatcher: "resource://gre/modules/Messaging.jsm", Extension: "resource://gre/modules/Extension.jsm", ExtensionChild: "resource://gre/modules/ExtensionChild.jsm", GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.jsm", }); +XPCOMUtils.defineLazyServiceGetter( + this, + "mimeService", + "@mozilla.org/mime;1", + "nsIMIMEService" +); + const { debug, warn } = GeckoViewUtils.initLogging("Console"); // eslint-disable-line no-unused-vars /** Provides common logic between page and browser actions */ @@ -231,6 +239,133 @@ class GeckoViewConnection { } } +function exportExtension(aAddon, aPermissions, aSourceURI) { + const { origins, permissions } = aPermissions; + const { + creator, + description, + homepageURL, + signedState, + name, + icons, + version, + optionsURL, + optionsBrowserStyle, + isRecommended, + blocklistState, + isActive, + isBuiltin, + id, + } = aAddon; + let creatorName = null; + let creatorURL = null; + if (creator) { + const { name, url } = creator; + creatorName = name; + creatorURL = url; + } + const openOptionsPageInTab = + optionsBrowserStyle === AddonManager.OPTIONS_TYPE_TAB; + return { + webExtensionId: id, + locationURI: aSourceURI != null ? aSourceURI.spec : "", + isEnabled: isActive, + isBuiltIn: isBuiltin, + metaData: { + permissions, + origins, + description, + version, + creatorName, + creatorURL, + homepageURL, + name, + optionsPageURL: optionsURL, + openOptionsPageInTab, + isRecommended, + blocklistState, + signedState, + icons, + }, + }; +} + +class ExtensionInstallListener { + constructor(aResolve) { + this.resolve = aResolve; + } + + onDownloadCancelled(aInstall) { + const { error: installError } = aInstall; + this.resolve({ installError }); + } + + onDownloadFailed(aInstall) { + const { error: installError } = aInstall; + this.resolve({ installError }); + } + + onDownloadEnded() { + // Nothing to do + } + + onInstallCancelled(aInstall) { + const { error: installError } = aInstall; + this.resolve({ installError }); + } + + onInstallFailed(aInstall) { + const { error: installError } = aInstall; + this.resolve({ installError }); + } + + onInstallEnded(aInstall, aAddon) { + const extension = exportExtension( + aAddon, + aAddon.userPermissions, + aInstall.sourceURI + ); + this.resolve({ extension }); + } +} + +class ExtensionPromptObserver { + constructor() { + Services.obs.addObserver(this, "webextension-permission-prompt"); + } + + async permissionPrompt(aInstall, aAddon, aInfo) { + const { sourceURI } = aInstall; + const { permissions } = aInfo; + const extension = exportExtension(aAddon, permissions, sourceURI); + const response = await EventDispatcher.instance.sendRequestForResult({ + type: "GeckoView:WebExtension:InstallPrompt", + extension, + }); + + if (response.allow) { + aInfo.resolve(); + } else { + aInfo.reject(); + } + } + + observe(aSubject, aTopic, aData) { + debug`observe ${aTopic}`; + + switch (aTopic) { + case "webextension-permission-prompt": { + const { info } = aSubject.wrappedJSObject; + const { addon, install } = info; + this.permissionPrompt(install, addon, info); + break; + } + } + } +} + +new ExtensionPromptObserver(); + var GeckoViewWebExtension = { async registerWebExtension(aId, aUri, allowContentMessaging, aCallback) { const params = { @@ -268,44 +403,106 @@ var GeckoViewWebExtension = { }, async extensionById(aId) { - const scope = this.extensionScopes.get(aId); + let scope = this.extensionScopes.get(aId); if (!scope) { - return null; + // Check if this is an installed extension we haven't seen yet + const addon = await AddonManager.getAddonByID(aId); + if (!addon) { + debug`Could not find extension with id=${aId}`; + return null; + } + scope = { + allowContentMessaging: false, + extension: addon, + }; } return scope.extension; }, + async installWebExtension(aUri) { + const install = await AddonManager.getInstallForURL(aUri.spec); + const promise = new Promise(resolve => { + install.addListener(new ExtensionInstallListener(resolve)); + }); + + const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + const mimeType = mimeService.getTypeFromURI(aUri); + AddonManager.installAddonFromWebpage( + mimeType, + null, + systemPrincipal, + install + ); + + return promise; + }, + + async uninstallWebExtension(aId) { + const extension = await this.extensionById(aId); + if (!extension) { + throw new Error(`Could not find an extension with id='${aId}'.`); + } + + return extension.uninstall(); + }, + + async browserActionClick(aId) { + const extension = await this.extensionById(aId); + if (!extension) { + return; + } + + const browserAction = this.browserActions.get(extension); + if (!browserAction) { + return; + } + + browserAction.click(); + }, + + async pageActionClick(aId) { + const extension = await this.extensionById(aId); + if (!extension) { + return; + } + + const pageAction = this.pageActions.get(extension); + if (!pageAction) { + return; + } + + pageAction.click(); + }, + + async actionDelegateAttached(aId) { + const extension = await this.extensionById(aId); + if (!extension) { + return; + } + + const browserAction = this.browserActions.get(extension); + if (browserAction) { + // Send information about this action to the delegate + browserAction.updateOnChange(null); + } + + const pageAction = this.pageActions.get(extension); + if (pageAction) { + pageAction.updateOnChange(null); + } + }, + async onEvent(aEvent, aData, aCallback) { debug`onEvent ${aEvent} ${aData}`; switch (aEvent) { case "GeckoView:BrowserAction:Click": { - const extension = await this.extensionById(aData.extensionId); - if (!extension) { - return; - } - - const browserAction = this.browserActions.get(extension); - if (!browserAction) { - return; - } - - browserAction.click(); + this.browserActionClick(aData.extensionId); break; } case "GeckoView:PageAction:Click": { - const extension = await this.extensionById(aData.extensionId); - if (!extension) { - return; - } - - const pageAction = this.pageActions.get(extension); - if (!pageAction) { - return; - } - - pageAction.click(); + this.pageActionClick(aData.extensionId); break; } case "GeckoView:RegisterWebExtension": { @@ -353,21 +550,7 @@ var GeckoViewWebExtension = { } case "GeckoView:ActionDelegate:Attached": { - const extension = await this.extensionById(aData.extensionId); - if (!extension) { - return; - } - - const browserAction = this.browserActions.get(extension); - if (browserAction) { - // Send information about this action to the delegate - browserAction.updateOnChange(null); - } - - const pageAction = this.pageActions.get(extension); - if (pageAction) { - pageAction.updateOnChange(null); - } + this.actionDelegateAttached(aData.extensionId); break; } @@ -383,9 +566,44 @@ var GeckoViewWebExtension = { break; } + case "GeckoView:WebExtension:Get": { + const extension = await this.extensionById(aData.extensionId); + if (!extension) { + aCallback.onError( + `Could not find extension with id: ${aData.extensionId}` + ); + return; + } + + aCallback.onSuccess({ + extension: exportExtension( + extension, + extension.userPermissions, + /* aSourceURI */ null + ), + }); + break; + } + case "GeckoView:WebExtension:Install": { - // TODO - aCallback.onError(`Not implemented`); + const uri = Services.io.newURI(aData.locationUri); + if (uri == null) { + aCallback.onError(`Could not parse uri: ${uri}`); + return; + } + + try { + const result = await this.installWebExtension(uri); + if (result.extension) { + aCallback.onSuccess(result); + } else { + aCallback.onError(result); + } + } catch (ex) { + debug`Install exception error ${ex}`; + aCallback.onError(`Unexpected error: ${ex}`); + } + break; } @@ -396,8 +614,15 @@ var GeckoViewWebExtension = { } case "GeckoView:WebExtension:Uninstall": { - // TODO - aCallback.onError(`Not implemented`); + try { + await this.uninstallWebExtension(aData.webExtensionId); + aCallback.onSuccess(); + } catch (ex) { + debug`Failed uninstall ${ex}`; + aCallback.onError( + `This extension cannot be uninstalled. Error: ${ex}.` + ); + } break; }