Source code
Revision control
Copy as Markdown
Other Tools
// 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
public struct ContentPermission {
public enum Permission: String {
/// Permission for using the geolocation API. See:
case geolocation = "geolocation"
/// Permission for using the notifications API. See:
case desktopNotification = "desktop-notification"
/// Permission for using the storage API. See:
case persistentStorage = "persistent-storage"
case webxr = "xr"
/// Permission for allowing autoplay of inaudible (silent) video.
case autoplayInaudible = "autoplay-media-inaudible"
/// Permission for allowing autoplay of audible video.
case autoplayAudible = "autoplay-media-audible"
/// Permission for accessing system media keys used to decode DRM media.
case mediaKeySystemAccess = "media-key-system-access"
/// Permission for trackers to operate on the page -- disables all
/// tracking protection features for a given site.
case tracking = "trackingprotection"
/// Permission for third party frames to access first party cookies. May
/// be granted heuristically in some cases.
case storageAccess = "storage-access"
}
public enum Value: Int32 {
/// The corresponding permission is currently set to default/prompt behavior.
case prompt = 3
/// The corresponding permission is currently set to deny.
case deny = 2
/// The corresponding permission is currently set to allow.
case allow = 1
}
/// The URI associated with this content permission.
public let uri: String
/// The third party origin associated with the request; currently only used for storage access
/// permission.
public let thirdPartyOrigin: String?
/// A boolean indicating whether this content permission is associated with private browsing.
public let privateMode: Bool
/// The type of this permission.
public let permission: Permission?
/// The value of the permission.
public let value: Value
/// The context ID associated with the permission if any.
public let contextId: String?
let principal: String?
static func fromDictionary(_ dict: [String: Any?]) -> ContentPermission {
let rawPerm = dict["perm"] as! String
var permission: Permission? = Permission(rawValue: rawPerm)
var thirdPartyOrigin = dict["thirdPartyOrigin"] as? String
// NOTE: This weirdness with how permissions are parsed comes from the JS code,
// and could be cleaned up in both Java and Swift if we change how the JS code
// sends permissions.
if rawPerm.starts(with: "3rdPartyStorage^") {
thirdPartyOrigin = String(rawPerm.dropFirst(16))
permission = .storageAccess
} else if rawPerm.starts(with: "3rdPartyFrameStorage^") {
thirdPartyOrigin = String(rawPerm.dropFirst(21))
permission = .storageAccess
} else if rawPerm == "trackingprotection-pb" {
permission = .tracking
}
return ContentPermission(
uri: dict["uri"] as! String,
thirdPartyOrigin: thirdPartyOrigin,
privateMode: dict["privateMode"] as! Bool,
permission: permission,
value: Value(rawValue: dict["value"] as! Int32)!,
// FIXME: Handle ContextID
contextId: nil,
principal: dict["principal"] as? String)
}
}
public struct MediaSource {
public enum Source {
case camera, screen, microphone, audiocapture, other
}
public enum MediaType {
case video, audio
}
/// A string giving a unique source identifier.
public let id: String
/// A string giving the name of the video source from the system (for example, "Camera 0,
/// Facing back, Orientation 90"). May be empty.
public let name: String?
public let source: Source
public let type: MediaType
static func fromDictionary(_ dict: [String: Any?]) -> MediaSource {
func parseSource(_ source: String) -> Source {
switch source {
case "camera":
return .camera
case "screen":
return .screen
case "window":
return .screen
case "browser":
return .screen
case "microphone":
return .microphone
case "audioCapture":
return .audiocapture
default:
return .other
}
}
func parseType(_ type: String) -> MediaType {
switch type {
case "videoinput":
return .video
case "audioinput":
return .audio
default:
fatalError("String: " + type + " is not a valid media type string")
}
}
return MediaSource(
id: dict["id"] as! String,
name: dict["name"] as? String,
source: parseSource(dict["source"] as! String),
type: parseType(dict["type"] as! String))
}
}
public struct SelectedMediaSources {
public let video: MediaSource? = nil
public let audio: MediaSource? = nil
}
public protocol PermissionDelegate {
/// Request content permission.
///
/// Note, that in the case of `persistentStorage`, once permission has
/// been granted for a site, it cannot be revoked. If the permission has
/// previously been granted, it is the responsibility of the consuming app
/// to remember the permission and prevent the prompt from being redisplayed
/// to the user.
func onContentPermissionRequest(session: GeckoSession, perm: ContentPermission) async
-> ContentPermission.Value
/// Request content media permissions, including request for which video
/// and/or audio source to use.
///
/// Media permissions will still be requested if the associated device
/// permissions have been denied if there are video or audio sources in that
/// category that can still be accessed. It is the responsibility of
/// consumers to ensure that media permission requests are not displayed in
/// this case.
func onMediaPermissionRequest(
session: GeckoSession, uri: String, video: [MediaSource]?, audio: [MediaSource]?
) async -> SelectedMediaSources?
}
enum PermissionEvents: String, CaseIterable {
// FIXME: Figure out iOS equivalent to androidPermission msg
// case androidPermission = "GeckoView:AndroidPermission"
case contentPermission = "GeckoView:ContentPermission"
case mediaPermission = "GeckoView:MediaPermission"
}
func newPermissionHandler(_ session: GeckoSession) -> GeckoSessionHandler<
PermissionDelegate, PermissionEvents
> {
GeckoSessionHandler(moduleName: "GeckoViewPermission", session: session) {
@MainActor session, delegate, event, message in
switch event {
case .contentPermission:
let result = await delegate?.onContentPermissionRequest(
session: session, perm: ContentPermission.fromDictionary(message!))
return (result ?? .prompt).rawValue
case .mediaPermission:
let videoDicts = message!["video"] as? [[String: Any?]]
let audioDicts = message!["audio"] as? [[String: Any?]]
let result = await delegate?.onMediaPermissionRequest(
session: session,
uri: message!["uri"] as! String,
video: videoDicts?.map(MediaSource.fromDictionary),
audio: audioDicts?.map(MediaSource.fromDictionary))
if result != nil {
return ["video": result?.video?.id, "audio": result?.video?.id]
} else {
// NOTE: This appears to match the behaviour of GeckoView-android, but is strange.
return false
}
}
}
}