tor-browser

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

ContentDelegate.swift (12040B)


      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 /// Element details for onContextMenu callbacks
      6 public struct ContextElement {
      7    public enum ElementType {
      8        case none, image, video, audio
      9    }
     10 
     11    /// The base URI of the element's document.
     12    public let baseUri: String?
     13 
     14    /// The absolute link URI (href) of the element.
     15    public let linkUri: String?
     16 
     17    /// The title text of the element.
     18    public let title: String?
     19 
     20    /// The alternative text (alt) for the element.
     21    public let altText: String?
     22 
     23    /// The type of the element. One of the  flags.
     24    public let type: ElementType
     25 
     26    /// The source URI (src) of the element. Set for (nested) media elements.
     27    public let srcUri: String?
     28 
     29    /// The text content of the element
     30    public let textContent: String?
     31 }
     32 
     33 public enum SlowScriptResponse {
     34    case halt, resume
     35 }
     36 
     37 public protocol ContentDelegate {
     38    /// A page title was discovered in the content or updated after the content
     39    /// loaded.
     40    func onTitleChange(session: GeckoSession, title: String)
     41 
     42    /// A preview image was discovered in the content after the content loaded.
     43    func onPreviewImage(session: GeckoSession, previewImageUrl: String)
     44 
     45    /// A page has requested focus. Note that window.focus() in content will not
     46    /// result in this being called.
     47    func onFocusRequest(session: GeckoSession)
     48 
     49    /// A page has requested to close
     50    func onCloseRequest(session: GeckoSession)
     51 
     52    /// A page has entered or exited full screen mode.
     53    ///
     54    /// Typically the implementation would set the GeckoView to full screen when
     55    /// the page is in full screen mode.
     56    func onFullScreen(session: GeckoSession, fullScreen: Bool)
     57 
     58    /// A viewport-filt was discovered in the content or updated after the
     59    /// content.
     60    ///
     61    /// See https://drafts.csswg.org/css-round-display/#viewport-fit-descriptor
     62    func onMetaViewportFitChange(session: GeckoSession, viewportFit: String)
     63 
     64    /// Session is on a product url.
     65    func onProductUrl(session: GeckoSession)
     66 
     67    /// A user has initiated the context menu via long-press.
     68    ///
     69    /// This event is fired on links, (nested) images, and (nested) media
     70    /// elements.
     71    func onContextMenu(session: GeckoSession, screenX: Int, screenY: Int, element: ContextElement)
     72 
     73    /// This is fired when there is a response that cannot be handled by Gecko
     74    /// (e.g. a download).
     75    // FIXME: Implement onExternalResponse & WebResponse
     76    // func onExternalResponse(session: GeckoSession, response: WebResponse)
     77 
     78    /// The content process hosting this GeckoSession has crashed.
     79    ///
     80    /// The GeckoSession is now closed and unusable. You may call `open` to
     81    /// recover the session, but no state is preserved. Most applications will
     82    /// want to call `load` or `restoreState` at this point.
     83    func onCrash(session: GeckoSession)
     84 
     85    /// The content process hosting this GeckoSession has been killed.
     86    ///
     87    /// The GeckoSession is now closed and unusable. You may call `open` to
     88    /// recover the session, but no state is preserved. Most applications will
     89    /// want to call `load` or `restoreState` at this point.
     90    func onKill(session: GeckoSession)
     91 
     92    /// Notification that the first content composition has occurred.
     93    ///
     94    /// This callback is invoked for the first content composite after either a
     95    /// start or a restart of the compositor.
     96    func onFirstComposite(session: GeckoSession)
     97 
     98    /// Notification that the first content paint has occurred.
     99    ///
    100    /// This callback is invoked for the first content paint after a page has
    101    /// been loaded, or after a `onPaintStatusReset` event. The
    102    /// `onFirstComposite` will be called once the compositor has started
    103    /// rendering.
    104    ///
    105    /// However, it is possible for the compositor to start rendering before
    106    /// there is any content to render. `onFirstContentfulPaint` is called once
    107    /// some content has been rendered.  It may be nothing more than the page
    108    /// background color. It is not an indication that the whole page has been
    109    /// rendered.
    110    func onFirstContentfulPaint(session: GeckoSession)
    111 
    112    /// Notification that the paint status has been reset.
    113    ///
    114    /// This callback is invoked whenever the painted content is no longer being
    115    /// displayed.  This can occur in response to the session being paused.
    116    /// After this has fired the compositor may continue rendering, but may not
    117    /// render the page content. This callback can therefore be used in
    118    /// conjunction with `onFirstContentfulPaint` to determine when there is
    119    /// valid content being rendered.
    120    func onPaintStatusReset(session: GeckoSession)
    121 
    122    /// This is fired when the loaded document has a valid Web App Manifest
    123    /// present.
    124    ///
    125    /// The various colors (theme_color, background_color, etc.) present in the
    126    /// manifest have been transformed into #AARRGGBB format.
    127    ///
    128    /// See https://www.w3.org/TR/appmanifest/
    129    func onWebAppManifest(session: GeckoSession, manifest: Any)
    130 
    131    /// A script has exceeded its execution timeout value
    132    ///
    133    /// Returning `.halt` will halt the slow script, and `.resume` will pause
    134    /// notifications for a period of time before resuming.
    135    func onSlowScript(session: GeckoSession, scriptFileName: String) async -> SlowScriptResponse
    136 
    137    /// The app should display its dynamic toolbar, fully expanded to the height
    138    /// that was previously specified via
    139    /// `GeckoView.setDynamicToolbarMaxHeight`.
    140    func onShowDynamicToolbar(session: GeckoSession)
    141 
    142    /// This method is called when a cookie banner is detected.
    143    ///
    144    /// Note: this method is called only if the cookie banner setting is such
    145    /// that allows to handle the banner. For example, if
    146    /// `cookiebanners.service.mode=1` (Reject only), but a cookie banner can
    147    /// only be accepted on the website - the detection in that case won't be
    148    /// reported.  The exception is `MODE_DETECT_ONLY` mode, when only the
    149    /// detection event is emitted.
    150    func onCookieBannerDetected(session: GeckoSession)
    151 
    152    /// This method is called when a cookie banner was handled.
    153    func onCookieBannerHandled(session: GeckoSession)
    154 }
    155 
    156 enum ContentEvents: String, CaseIterable {
    157    case contentCrash = "GeckoView:ContentCrash"
    158    case contentKill = "GeckoView:ContentKill"
    159    case contextMenu = "GeckoView:ContextMenu"
    160    case domMetaViewportFit = "GeckoView:DOMMetaViewportFit"
    161    case pageTitleChanged = "GeckoView:PageTitleChanged"
    162    case domWindowClose = "GeckoView:DOMWindowClose"
    163    case externalResponse = "GeckoView:ExternalResponse"
    164    case focusRequest = "GeckoView:FocusRequest"
    165    case fullscreenEnter = "GeckoView:FullScreenEnter"
    166    case fullscreenExit = "GeckoView:FullScreenExit"
    167    case webAppManifest = "GeckoView:WebAppManifest"
    168    case firstContentfulPaint = "GeckoView:FirstContentfulPaint"
    169    case paintStatusReset = "GeckoView:PaintStatusReset"
    170    case previewImage = "GeckoView:PreviewImage"
    171    case cookieBannerEventDetected = "GeckoView:CookieBannerEvent:Detected"
    172    case cookieBannerEventHandled = "GeckoView:CookieBannerEvent:Handled"
    173    case savePdf = "GeckoView:SavePdf"
    174    case onProductUrl = "GeckoView:OnProductUrl"
    175 }
    176 
    177 func newContentHandler(_ session: GeckoSession) -> GeckoSessionHandler<
    178    ContentDelegate, ContentEvents
    179 > {
    180    GeckoSessionHandler(moduleName: "GeckoViewContent", session: session) {
    181        @MainActor session, delegate, event, message in
    182        switch event {
    183        case .contentCrash:
    184            session.close()
    185            delegate?.onCrash(session: session)
    186            return nil
    187        case .contentKill:
    188            session.close()
    189            delegate?.onKill(session: session)
    190            return nil
    191        case .contextMenu:
    192            func parseType(type: String) -> ContextElement.ElementType {
    193                switch type {
    194                case "HTMLImageElement": return .image
    195                case "HTMLVideoElement": return .video
    196                case "HTMLAudioElement": return .audio
    197                default: return .none
    198                }
    199            }
    200 
    201            let contextElement = ContextElement(
    202                baseUri: message!["baseUri"] as? String,
    203                linkUri: message!["linkUri"] as? String,
    204                title: message!["title"] as? String,
    205                altText: message!["alt"] as? String,
    206                type: parseType(type: message!["elementType"] as! String),
    207                srcUri: message!["elementSrc"] as? String,
    208                textContent: message!["textContent"] as? String)
    209 
    210            delegate?.onContextMenu(
    211                session: session,
    212                screenX: message!["screenX"] as! Int,
    213                screenY: message!["screenY"] as! Int,
    214                element: contextElement)
    215            return nil
    216        case .domMetaViewportFit:
    217            delegate?.onMetaViewportFitChange(
    218                session: session, viewportFit: message!["viewportfit"] as! String)
    219            return nil
    220        case .pageTitleChanged:
    221            delegate?.onTitleChange(session: session, title: message!["title"] as! String)
    222            return nil
    223        case .domWindowClose:
    224            delegate?.onCloseRequest(session: session)
    225            return nil
    226        case .externalResponse:
    227            // FIXME: implement
    228            throw HandlerError("GeckoView:ExternalResponse is unimplemented")
    229        case .focusRequest:
    230            delegate?.onFocusRequest(session: session)
    231            return nil
    232        case .fullscreenEnter:
    233            delegate?.onFullScreen(session: session, fullScreen: true)
    234            return nil
    235        case .fullscreenExit:
    236            delegate?.onFullScreen(session: session, fullScreen: false)
    237            return nil
    238        case .webAppManifest:
    239            delegate?.onWebAppManifest(session: session, manifest: message!["manifest"]!!)
    240            return nil
    241        case .firstContentfulPaint:
    242            delegate?.onFirstContentfulPaint(session: session)
    243            return nil
    244        case .paintStatusReset:
    245            delegate?.onPaintStatusReset(session: session)
    246            return nil
    247        case .previewImage:
    248            delegate?.onPreviewImage(
    249                session: session, previewImageUrl: message!["previewImageUrl"] as! String)
    250            return nil
    251        case .cookieBannerEventDetected:
    252            delegate?.onCookieBannerDetected(session: session)
    253            return nil
    254        case .cookieBannerEventHandled:
    255            delegate?.onCookieBannerHandled(session: session)
    256            return nil
    257        case .savePdf:
    258            // FIXME: implement
    259            throw HandlerError("GeckoView:SavePdf is unimplemented")
    260        case .onProductUrl:
    261            delegate?.onProductUrl(session: session)
    262            return nil
    263        }
    264    }
    265 }
    266 
    267 enum ProcessHangEvents: String, CaseIterable {
    268    case hangReport = "GeckoView:HangReport"
    269 }
    270 
    271 func newProcessHangHandler(_ session: GeckoSession) -> GeckoSessionHandler<
    272    ContentDelegate, ProcessHangEvents
    273 > {
    274    GeckoSessionHandler(moduleName: "GeckoViewProcessHangMonitor", session: session) {
    275        @MainActor session, delegate, event, message in
    276        switch event {
    277        case .hangReport:
    278            let reportId = message!["hangId"] as! Int
    279            let response = await delegate?.onSlowScript(
    280                session: session, scriptFileName: message!["scriptFileName"] as! String)
    281            switch response {
    282            case .resume:
    283                session.dispatcher.dispatch(
    284                    type: "GeckoView:HangReportWait", message: ["hangId": reportId])
    285            default:
    286                session.dispatcher.dispatch(
    287                    type: "GeckoView:HangReportStop", message: ["hangId": reportId])
    288            }
    289            return nil
    290        }
    291    }
    292 }