File: ContentDelegate.swift

package info (click to toggle)
firefox 149.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,767,760 kB
  • sloc: cpp: 7,416,064; javascript: 6,752,859; ansic: 3,774,850; python: 1,250,473; xml: 641,578; asm: 439,191; java: 186,617; sh: 56,634; makefile: 18,856; objc: 13,092; perl: 12,763; pascal: 5,960; yacc: 4,583; cs: 3,846; lex: 1,720; ruby: 1,002; php: 436; lisp: 258; awk: 105; sql: 66; sed: 53; csh: 10; exp: 6
file content (292 lines) | stat: -rw-r--r-- 12,040 bytes parent folder | download | duplicates (3)
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
// 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/.

/// Element details for onContextMenu callbacks
public struct ContextElement {
    public enum ElementType {
        case none, image, video, audio
    }

    /// The base URI of the element's document.
    public let baseUri: String?

    /// The absolute link URI (href) of the element.
    public let linkUri: String?

    /// The title text of the element.
    public let title: String?

    /// The alternative text (alt) for the element.
    public let altText: String?

    /// The type of the element. One of the  flags.
    public let type: ElementType

    /// The source URI (src) of the element. Set for (nested) media elements.
    public let srcUri: String?

    /// The text content of the element
    public let textContent: String?
}

public enum SlowScriptResponse {
    case halt, resume
}

public protocol ContentDelegate {
    /// A page title was discovered in the content or updated after the content
    /// loaded.
    func onTitleChange(session: GeckoSession, title: String)

    /// A preview image was discovered in the content after the content loaded.
    func onPreviewImage(session: GeckoSession, previewImageUrl: String)

    /// A page has requested focus. Note that window.focus() in content will not
    /// result in this being called.
    func onFocusRequest(session: GeckoSession)

    /// A page has requested to close
    func onCloseRequest(session: GeckoSession)

    /// A page has entered or exited full screen mode.
    ///
    /// Typically the implementation would set the GeckoView to full screen when
    /// the page is in full screen mode.
    func onFullScreen(session: GeckoSession, fullScreen: Bool)

    /// A viewport-filt was discovered in the content or updated after the
    /// content.
    ///
    /// See https://drafts.csswg.org/css-round-display/#viewport-fit-descriptor
    func onMetaViewportFitChange(session: GeckoSession, viewportFit: String)

    /// Session is on a product url.
    func onProductUrl(session: GeckoSession)

    /// A user has initiated the context menu via long-press.
    ///
    /// This event is fired on links, (nested) images, and (nested) media
    /// elements.
    func onContextMenu(session: GeckoSession, screenX: Int, screenY: Int, element: ContextElement)

    /// This is fired when there is a response that cannot be handled by Gecko
    /// (e.g. a download).
    // FIXME: Implement onExternalResponse & WebResponse
    // func onExternalResponse(session: GeckoSession, response: WebResponse)

    /// The content process hosting this GeckoSession has crashed.
    ///
    /// The GeckoSession is now closed and unusable. You may call `open` to
    /// recover the session, but no state is preserved. Most applications will
    /// want to call `load` or `restoreState` at this point.
    func onCrash(session: GeckoSession)

    /// The content process hosting this GeckoSession has been killed.
    ///
    /// The GeckoSession is now closed and unusable. You may call `open` to
    /// recover the session, but no state is preserved. Most applications will
    /// want to call `load` or `restoreState` at this point.
    func onKill(session: GeckoSession)

    /// Notification that the first content composition has occurred.
    ///
    /// This callback is invoked for the first content composite after either a
    /// start or a restart of the compositor.
    func onFirstComposite(session: GeckoSession)

    /// Notification that the first content paint has occurred.
    ///
    /// This callback is invoked for the first content paint after a page has
    /// been loaded, or after a `onPaintStatusReset` event. The
    /// `onFirstComposite` will be called once the compositor has started
    /// rendering.
    ///
    /// However, it is possible for the compositor to start rendering before
    /// there is any content to render. `onFirstContentfulPaint` is called once
    /// some content has been rendered.  It may be nothing more than the page
    /// background color. It is not an indication that the whole page has been
    /// rendered.
    func onFirstContentfulPaint(session: GeckoSession)

    /// Notification that the paint status has been reset.
    ///
    /// This callback is invoked whenever the painted content is no longer being
    /// displayed.  This can occur in response to the session being paused.
    /// After this has fired the compositor may continue rendering, but may not
    /// render the page content. This callback can therefore be used in
    /// conjunction with `onFirstContentfulPaint` to determine when there is
    /// valid content being rendered.
    func onPaintStatusReset(session: GeckoSession)

    /// This is fired when the loaded document has a valid Web App Manifest
    /// present.
    ///
    /// The various colors (theme_color, background_color, etc.) present in the
    /// manifest have been transformed into #AARRGGBB format.
    ///
    /// See https://www.w3.org/TR/appmanifest/
    func onWebAppManifest(session: GeckoSession, manifest: Any)

    /// A script has exceeded its execution timeout value
    ///
    /// Returning `.halt` will halt the slow script, and `.resume` will pause
    /// notifications for a period of time before resuming.
    func onSlowScript(session: GeckoSession, scriptFileName: String) async -> SlowScriptResponse

    /// The app should display its dynamic toolbar, fully expanded to the height
    /// that was previously specified via
    /// `GeckoView.setDynamicToolbarMaxHeight`.
    func onShowDynamicToolbar(session: GeckoSession)

    /// This method is called when a cookie banner is detected.
    ///
    /// Note: this method is called only if the cookie banner setting is such
    /// that allows to handle the banner. For example, if
    /// `cookiebanners.service.mode=1` (Reject only), but a cookie banner can
    /// only be accepted on the website - the detection in that case won't be
    /// reported.  The exception is `MODE_DETECT_ONLY` mode, when only the
    /// detection event is emitted.
    func onCookieBannerDetected(session: GeckoSession)

    /// This method is called when a cookie banner was handled.
    func onCookieBannerHandled(session: GeckoSession)
}

enum ContentEvents: String, CaseIterable {
    case contentCrash = "GeckoView:ContentCrash"
    case contentKill = "GeckoView:ContentKill"
    case contextMenu = "GeckoView:ContextMenu"
    case domMetaViewportFit = "GeckoView:DOMMetaViewportFit"
    case pageTitleChanged = "GeckoView:PageTitleChanged"
    case domWindowClose = "GeckoView:DOMWindowClose"
    case externalResponse = "GeckoView:ExternalResponse"
    case focusRequest = "GeckoView:FocusRequest"
    case fullscreenEnter = "GeckoView:FullScreenEnter"
    case fullscreenExit = "GeckoView:FullScreenExit"
    case webAppManifest = "GeckoView:WebAppManifest"
    case firstContentfulPaint = "GeckoView:FirstContentfulPaint"
    case paintStatusReset = "GeckoView:PaintStatusReset"
    case previewImage = "GeckoView:PreviewImage"
    case cookieBannerEventDetected = "GeckoView:CookieBannerEvent:Detected"
    case cookieBannerEventHandled = "GeckoView:CookieBannerEvent:Handled"
    case savePdf = "GeckoView:SavePdf"
    case onProductUrl = "GeckoView:OnProductUrl"
}

func newContentHandler(_ session: GeckoSession) -> GeckoSessionHandler<
    ContentDelegate, ContentEvents
> {
    GeckoSessionHandler(moduleName: "GeckoViewContent", session: session) {
        @MainActor session, delegate, event, message in
        switch event {
        case .contentCrash:
            session.close()
            delegate?.onCrash(session: session)
            return nil
        case .contentKill:
            session.close()
            delegate?.onKill(session: session)
            return nil
        case .contextMenu:
            func parseType(type: String) -> ContextElement.ElementType {
                switch type {
                case "HTMLImageElement": return .image
                case "HTMLVideoElement": return .video
                case "HTMLAudioElement": return .audio
                default: return .none
                }
            }

            let contextElement = ContextElement(
                baseUri: message!["baseUri"] as? String,
                linkUri: message!["linkUri"] as? String,
                title: message!["title"] as? String,
                altText: message!["alt"] as? String,
                type: parseType(type: message!["elementType"] as! String),
                srcUri: message!["elementSrc"] as? String,
                textContent: message!["textContent"] as? String)

            delegate?.onContextMenu(
                session: session,
                screenX: message!["screenX"] as! Int,
                screenY: message!["screenY"] as! Int,
                element: contextElement)
            return nil
        case .domMetaViewportFit:
            delegate?.onMetaViewportFitChange(
                session: session, viewportFit: message!["viewportfit"] as! String)
            return nil
        case .pageTitleChanged:
            delegate?.onTitleChange(session: session, title: message!["title"] as! String)
            return nil
        case .domWindowClose:
            delegate?.onCloseRequest(session: session)
            return nil
        case .externalResponse:
            // FIXME: implement
            throw HandlerError("GeckoView:ExternalResponse is unimplemented")
        case .focusRequest:
            delegate?.onFocusRequest(session: session)
            return nil
        case .fullscreenEnter:
            delegate?.onFullScreen(session: session, fullScreen: true)
            return nil
        case .fullscreenExit:
            delegate?.onFullScreen(session: session, fullScreen: false)
            return nil
        case .webAppManifest:
            delegate?.onWebAppManifest(session: session, manifest: message!["manifest"]!!)
            return nil
        case .firstContentfulPaint:
            delegate?.onFirstContentfulPaint(session: session)
            return nil
        case .paintStatusReset:
            delegate?.onPaintStatusReset(session: session)
            return nil
        case .previewImage:
            delegate?.onPreviewImage(
                session: session, previewImageUrl: message!["previewImageUrl"] as! String)
            return nil
        case .cookieBannerEventDetected:
            delegate?.onCookieBannerDetected(session: session)
            return nil
        case .cookieBannerEventHandled:
            delegate?.onCookieBannerHandled(session: session)
            return nil
        case .savePdf:
            // FIXME: implement
            throw HandlerError("GeckoView:SavePdf is unimplemented")
        case .onProductUrl:
            delegate?.onProductUrl(session: session)
            return nil
        }
    }
}

enum ProcessHangEvents: String, CaseIterable {
    case hangReport = "GeckoView:HangReport"
}

func newProcessHangHandler(_ session: GeckoSession) -> GeckoSessionHandler<
    ContentDelegate, ProcessHangEvents
> {
    GeckoSessionHandler(moduleName: "GeckoViewProcessHangMonitor", session: session) {
        @MainActor session, delegate, event, message in
        switch event {
        case .hangReport:
            let reportId = message!["hangId"] as! Int
            let response = await delegate?.onSlowScript(
                session: session, scriptFileName: message!["scriptFileName"] as! String)
            switch response {
            case .resume:
                session.dispatcher.dispatch(
                    type: "GeckoView:HangReportWait", message: ["hangId": reportId])
            default:
                session.dispatcher.dispatch(
                    type: "GeckoView:HangReportStop", message: ["hangId": reportId])
            }
            return nil
        }
    }
}