tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }