summaryrefslogtreecommitdiffstats
path: root/mobile/android/actors/GeckoViewPermissionChild.sys.mjs
blob: bbe9457cc5eeb0c21ba2320c78acf349aac00a8a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/* 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/. */

import { GeckoViewActorChild } from "resource://gre/modules/GeckoViewActorChild.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
});

const PERM_ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION";

const MAPPED_TO_EXTENSION_PERMISSIONS = [
  "persistent-storage",
  // TODO(Bug 1336194): support geolocation manifest permission
  // (see https://bugzilla.mozilla.org/show_bug.cgi?id=1336194#c17)l
];

export class GeckoViewPermissionChild extends GeckoViewActorChild {
  getMediaPermission(aPermission) {
    return this.eventDispatcher.sendRequestForResult({
      type: "GeckoView:MediaPermission",
      ...aPermission,
    });
  }

  addCameraPermission() {
    return this.sendQuery("AddCameraPermission");
  }

  getAppPermissions(aPermissions) {
    return this.sendQuery("GetAppPermissions", aPermissions);
  }

  mediaRecordingStatusChanged(aDevices) {
    return this.eventDispatcher.sendRequest({
      type: "GeckoView:MediaRecordingStatusChanged",
      devices: aDevices,
    });
  }

  // Some WebAPI permissions can be requested and granted to extensions through the
  // the extension manifest.json, which the user have been already prompted for
  // (e.g. at install time for the one listed in the manifest.json permissions property,
  // or at runtime through the optional_permissions property and the permissions.request
  // WebExtensions API method).
  //
  // WebAPI permission that are expected to be mapped to extensions permissions are listed
  // in the MAPPED_TO_EXTENSION_PERMISSIONS array.
  //
  // @param {nsIContentPermissionType} perm
  //        The WebAPI permission being requested
  // @param {nsIContentPermissionRequest} aRequest
  //        The nsIContentPermissionRequest as received by the promptPermission method.
  //
  // @returns {null | { allow: boolean, permission: Object }
  //          Returns null if the request was not handled and should continue with the
  //          regular permission prompting flow, otherwise it returns an object to
  //          allow or disallow the permission request right away.
  checkIfGrantedByExtensionPermissions(perm, aRequest) {
    if (!aRequest.principal.addonPolicy) {
      // Not an extension, continue with the regular permission prompting flow.
      return null;
    }

    // Return earlier and continue with the regular permission prompting flow if the
    // the permission is no one that can be requested from the extension manifest file.
    if (!MAPPED_TO_EXTENSION_PERMISSIONS.includes(perm.type)) {
      return null;
    }

    // Disallow if the extension is not active anymore.
    if (!aRequest.principal.addonPolicy.active) {
      return { allow: false };
    }

    // Check if the permission have been already granted to the extension, if it is allow it right away.
    const isGranted =
      Services.perms.testPermissionFromPrincipal(
        aRequest.principal,
        perm.type
      ) === Services.perms.ALLOW_ACTION;
    if (isGranted) {
      return {
        allow: true,
        permission: { [perm.type]: Services.perms.ALLOW_ACTION },
      };
    }

    // continue with the regular permission prompting flow otherwise.
    return null;
  }

  async promptPermission(aRequest) {
    // Only allow exactly one permission request here.
    const types = aRequest.types.QueryInterface(Ci.nsIArray);
    if (types.length !== 1) {
      return { allow: false };
    }

    const perm = types.queryElementAt(0, Ci.nsIContentPermissionType);

    // Check if the request principal is an extension principal and if the permission requested
    // should be already granted based on the extension permissions (or disallowed right away
    // because the extension is not enabled anymore.
    const extensionResult = this.checkIfGrantedByExtensionPermissions(
      perm,
      aRequest
    );
    if (extensionResult) {
      return extensionResult;
    }

    if (
      perm.type === "desktop-notification" &&
      !aRequest.hasValidTransientUserGestureActivation &&
      Services.prefs.getBoolPref(
        "dom.webnotifications.requireuserinteraction",
        true
      )
    ) {
      // We need user interaction and don't have it.
      return { allow: false };
    }

    const principal =
      perm.type === "storage-access"
        ? aRequest.principal
        : aRequest.topLevelPrincipal;

    let allowOrDeny;
    try {
      allowOrDeny = await this.eventDispatcher.sendRequestForResult({
        type: "GeckoView:ContentPermission",
        uri: principal.URI.displaySpec,
        thirdPartyOrigin: aRequest.principal.origin,
        principal: lazy.E10SUtils.serializePrincipal(principal),
        perm: perm.type,
        value: perm.capability,
        contextId: principal.originAttributes.geckoViewSessionContextId ?? null,
        privateMode: principal.privateBrowsingId != 0,
      });

      if (allowOrDeny === Services.perms.ALLOW_ACTION) {
        // Ask for app permission after asking for content permission.
        if (perm.type === "geolocation") {
          const granted = await this.getAppPermissions([
            PERM_ACCESS_FINE_LOCATION,
          ]);
          allowOrDeny = granted
            ? Services.perms.ALLOW_ACTION
            : Services.perms.DENY_ACTION;
        }
      }
    } catch (error) {
      console.error("Permission error:", error);
      allowOrDeny = Services.perms.DENY_ACTION;
    }

    // Manually release the target request here to facilitate garbage collection.
    aRequest = undefined;

    const allow = allowOrDeny === Services.perms.ALLOW_ACTION;

    // The storage access code adds itself to the perm manager; no need for us to do it.
    if (perm.type === "storage-access") {
      if (allow) {
        return { allow, permission: { "storage-access": "allow" } };
      }
      return { allow };
    }

    Services.perms.addFromPrincipal(
      principal,
      perm.type,
      allowOrDeny,
      Services.perms.EXPIRE_NEVER
    );

    return { allow };
  }
}

const { debug, warn } = GeckoViewPermissionChild.initLogging(
  "GeckoViewPermissionChild"
);