PermissionDelegate.swift (8454B)
1 // This Source Code Form is subject to the terms of the Mozilla Public 2 // License, v. 2.0. If a copy of the MPL was not distributed with this 3 // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 public struct ContentPermission { 6 public enum Permission: String { 7 /// Permission for using the geolocation API. See: 8 /// https://developer.mozilla.org/en-US/docs/Web/API/Geolocation 9 case geolocation = "geolocation" 10 11 /// Permission for using the notifications API. See: 12 /// https://developer.mozilla.org/en-US/docs/Web/API/notification 13 case desktopNotification = "desktop-notification" 14 15 /// Permission for using the storage API. See: 16 /// https://developer.mozilla.org/en-US/docs/Web/API/Storage_API 17 case persistentStorage = "persistent-storage" 18 19 /// Permission for using the WebXR API. See: https://www.w3.org/TR/webxr 20 case webxr = "xr" 21 22 /// Permission for allowing autoplay of inaudible (silent) video. 23 case autoplayInaudible = "autoplay-media-inaudible" 24 25 /// Permission for allowing autoplay of audible video. 26 case autoplayAudible = "autoplay-media-audible" 27 28 /// Permission for accessing system media keys used to decode DRM media. 29 case mediaKeySystemAccess = "media-key-system-access" 30 31 /// Permission for trackers to operate on the page -- disables all 32 /// tracking protection features for a given site. 33 case tracking = "trackingprotection" 34 35 /// Permission for third party frames to access first party cookies. May 36 /// be granted heuristically in some cases. 37 case storageAccess = "storage-access" 38 } 39 40 public enum Value: Int32 { 41 /// The corresponding permission is currently set to default/prompt behavior. 42 case prompt = 3 43 /// The corresponding permission is currently set to deny. 44 case deny = 2 45 /// The corresponding permission is currently set to allow. 46 case allow = 1 47 } 48 49 /// The URI associated with this content permission. 50 public let uri: String 51 52 /// The third party origin associated with the request; currently only used for storage access 53 /// permission. 54 public let thirdPartyOrigin: String? 55 56 /// A boolean indicating whether this content permission is associated with private browsing. 57 public let privateMode: Bool 58 59 /// The type of this permission. 60 public let permission: Permission? 61 62 /// The value of the permission. 63 public let value: Value 64 65 /// The context ID associated with the permission if any. 66 public let contextId: String? 67 68 let principal: String? 69 70 static func fromDictionary(_ dict: [String: Any?]) -> ContentPermission { 71 let rawPerm = dict["perm"] as! String 72 73 var permission: Permission? = Permission(rawValue: rawPerm) 74 var thirdPartyOrigin = dict["thirdPartyOrigin"] as? String 75 76 // NOTE: This weirdness with how permissions are parsed comes from the JS code, 77 // and could be cleaned up in both Java and Swift if we change how the JS code 78 // sends permissions. 79 if rawPerm.starts(with: "3rdPartyStorage^") { 80 thirdPartyOrigin = String(rawPerm.dropFirst(16)) 81 permission = .storageAccess 82 } else if rawPerm.starts(with: "3rdPartyFrameStorage^") { 83 thirdPartyOrigin = String(rawPerm.dropFirst(21)) 84 permission = .storageAccess 85 } else if rawPerm == "trackingprotection-pb" { 86 permission = .tracking 87 } 88 89 return ContentPermission( 90 uri: dict["uri"] as! String, 91 thirdPartyOrigin: thirdPartyOrigin, 92 privateMode: dict["privateMode"] as! Bool, 93 permission: permission, 94 value: Value(rawValue: dict["value"] as! Int32)!, 95 // FIXME: Handle ContextID 96 contextId: nil, 97 principal: dict["principal"] as? String) 98 } 99 } 100 101 public struct MediaSource { 102 public enum Source { 103 case camera, screen, microphone, audiocapture, other 104 } 105 106 public enum MediaType { 107 case video, audio 108 } 109 110 /// A string giving a unique source identifier. 111 public let id: String 112 113 /// A string giving the name of the video source from the system (for example, "Camera 0, 114 /// Facing back, Orientation 90"). May be empty. 115 public let name: String? 116 117 public let source: Source 118 119 public let type: MediaType 120 121 static func fromDictionary(_ dict: [String: Any?]) -> MediaSource { 122 func parseSource(_ source: String) -> Source { 123 switch source { 124 case "camera": 125 return .camera 126 case "screen": 127 return .screen 128 case "window": 129 return .screen 130 case "browser": 131 return .screen 132 case "microphone": 133 return .microphone 134 case "audioCapture": 135 return .audiocapture 136 default: 137 return .other 138 } 139 } 140 141 func parseType(_ type: String) -> MediaType { 142 switch type { 143 case "videoinput": 144 return .video 145 case "audioinput": 146 return .audio 147 default: 148 fatalError("String: " + type + " is not a valid media type string") 149 } 150 } 151 return MediaSource( 152 id: dict["id"] as! String, 153 name: dict["name"] as? String, 154 source: parseSource(dict["source"] as! String), 155 type: parseType(dict["type"] as! String)) 156 } 157 } 158 159 public struct SelectedMediaSources { 160 public let video: MediaSource? = nil 161 public let audio: MediaSource? = nil 162 } 163 164 public protocol PermissionDelegate { 165 /// Request content permission. 166 /// 167 /// Note, that in the case of `persistentStorage`, once permission has 168 /// been granted for a site, it cannot be revoked. If the permission has 169 /// previously been granted, it is the responsibility of the consuming app 170 /// to remember the permission and prevent the prompt from being redisplayed 171 /// to the user. 172 func onContentPermissionRequest(session: GeckoSession, perm: ContentPermission) async 173 -> ContentPermission.Value 174 175 /// Request content media permissions, including request for which video 176 /// and/or audio source to use. 177 /// 178 /// Media permissions will still be requested if the associated device 179 /// permissions have been denied if there are video or audio sources in that 180 /// category that can still be accessed. It is the responsibility of 181 /// consumers to ensure that media permission requests are not displayed in 182 /// this case. 183 func onMediaPermissionRequest( 184 session: GeckoSession, uri: String, video: [MediaSource]?, audio: [MediaSource]? 185 ) async -> SelectedMediaSources? 186 } 187 188 enum PermissionEvents: String, CaseIterable { 189 // FIXME: Figure out iOS equivalent to androidPermission msg 190 // case androidPermission = "GeckoView:AndroidPermission" 191 case contentPermission = "GeckoView:ContentPermission" 192 case mediaPermission = "GeckoView:MediaPermission" 193 } 194 195 func newPermissionHandler(_ session: GeckoSession) -> GeckoSessionHandler< 196 PermissionDelegate, PermissionEvents 197 > { 198 GeckoSessionHandler(moduleName: "GeckoViewPermission", session: session) { 199 @MainActor session, delegate, event, message in 200 switch event { 201 case .contentPermission: 202 let result = await delegate?.onContentPermissionRequest( 203 session: session, perm: ContentPermission.fromDictionary(message!)) 204 return (result ?? .prompt).rawValue 205 206 case .mediaPermission: 207 let videoDicts = message!["video"] as? [[String: Any?]] 208 let audioDicts = message!["audio"] as? [[String: Any?]] 209 let result = await delegate?.onMediaPermissionRequest( 210 session: session, 211 uri: message!["uri"] as! String, 212 video: videoDicts?.map(MediaSource.fromDictionary), 213 audio: audioDicts?.map(MediaSource.fromDictionary)) 214 if result != nil { 215 return ["video": result?.video?.id, "audio": result?.video?.id] 216 } else { 217 // NOTE: This appears to match the behaviour of GeckoView-android, but is strange. 218 return false 219 } 220 } 221 } 222 }