Skip to content

Commit 8160f7c

Browse files
authoredMar 12, 2025
Add FXIOS-7894 #17639 [WebEngine] Migrate web server utilities (#24946)
* Create and setup webserver * fix imports * Write tests * Swiftlint * Integrate in SampleBrowser * Swiftlint * Add logger * Add import * Fix tests again * Add import to fix Focus * import common * Resolve comments + fix SampleBrowser build * Fix import * Fix import
1 parent 3d949a9 commit 8160f7c

37 files changed

+534
-86
lines changed
 

‎BrowserKit/Sources/Shared/AppConstants.swift ‎BrowserKit/Sources/Common/Constants/AppConstants.swift

-44
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,8 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at http://mozilla.org/MPL/2.0/
44

5-
import Common
65
import UIKit
76

8-
public enum AppName: String, CustomStringConvertible {
9-
case shortName = "Firefox"
10-
11-
public var description: String {
12-
return self.rawValue
13-
}
14-
}
15-
16-
public enum PocketAppName: String, CustomStringConvertible {
17-
case shortName = "Pocket"
18-
19-
public var description: String {
20-
return self.rawValue
21-
}
22-
}
23-
24-
public enum FakespotName: String, CustomStringConvertible {
25-
case shortName = "Fakespot"
26-
27-
public var description: String {
28-
return self.rawValue
29-
}
30-
}
31-
32-
public enum MozillaName: String, CustomStringConvertible {
33-
case shortName = "Mozilla"
34-
35-
public var description: String {
36-
return self.rawValue
37-
}
38-
}
39-
40-
public enum KVOConstants: String {
41-
case loading
42-
case estimatedProgress
43-
case URL
44-
case title
45-
case canGoBack
46-
case canGoForward
47-
case contentSize
48-
case hasOnlySecureContent
49-
}
50-
517
public class AppConstants {
528
// Any type of tests (UI and Unit)
539
public static let isRunningTest = NSClassFromString("XCTestCase") != nil
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
import Foundation
6+
7+
public protocol DispatchSourceTimerFactory {
8+
func createDispatchSource() -> DispatchSourceInterface
9+
}
10+
11+
public struct DefaultDispatchSourceTimerFactory: DispatchSourceTimerFactory {
12+
public func createDispatchSource() -> DispatchSourceInterface {
13+
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
14+
return DefaultDispatchSourceTimer(timer: timer)
15+
}
16+
17+
public init() {}
18+
}
19+
20+
public protocol DispatchSourceInterface {
21+
func schedule(deadline: DispatchTime, repeating interval: DispatchTimeInterval, leeway: DispatchTimeInterval)
22+
23+
func setEventHandler(completion: @escaping () -> Void)
24+
25+
func resume()
26+
27+
func cancel()
28+
}
29+
30+
public extension DispatchSourceInterface {
31+
func schedule(deadline: DispatchTime,
32+
repeating interval: DispatchTimeInterval = .never,
33+
leeway: DispatchTimeInterval = .nanoseconds(0)) {
34+
schedule(deadline: deadline, repeating: interval, leeway: leeway)
35+
}
36+
}
37+
38+
public struct DefaultDispatchSourceTimer: DispatchSourceInterface {
39+
let timer: DispatchSourceTimer
40+
41+
public func schedule(deadline: DispatchTime,
42+
repeating interval: DispatchTimeInterval = .never,
43+
leeway: DispatchTimeInterval = .nanoseconds(0)) {
44+
timer.schedule(deadline: deadline, repeating: interval, leeway: leeway)
45+
}
46+
47+
public func setEventHandler(completion: @escaping () -> Void) {
48+
timer.setEventHandler(handler: completion)
49+
}
50+
51+
public func resume() {
52+
timer.resume()
53+
}
54+
55+
public func cancel() {
56+
timer.cancel()
57+
}
58+
}
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
import Common
6+
import UIKit
7+
8+
public enum AppName: String, CustomStringConvertible {
9+
case shortName = "Firefox"
10+
11+
public var description: String {
12+
return self.rawValue
13+
}
14+
}
15+
16+
public enum PocketAppName: String, CustomStringConvertible {
17+
case shortName = "Pocket"
18+
19+
public var description: String {
20+
return self.rawValue
21+
}
22+
}
23+
24+
public enum FakespotName: String, CustomStringConvertible {
25+
case shortName = "Fakespot"
26+
27+
public var description: String {
28+
return self.rawValue
29+
}
30+
}
31+
32+
public enum MozillaName: String, CustomStringConvertible {
33+
case shortName = "Mozilla"
34+
35+
public var description: String {
36+
return self.rawValue
37+
}
38+
}
39+
40+
public enum KVOConstants: String {
41+
case loading
42+
case estimatedProgress
43+
case URL
44+
case title
45+
case canGoBack
46+
case canGoForward
47+
case contentSize
48+
case hasOnlySecureContent
49+
}

‎BrowserKit/Sources/WebEngine/Engine.swift

+6
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,10 @@ public protocol Engine {
1515
/// - Parameter dependencies: Pass in the required session dependencies on creation
1616
/// - Returns: The created `EngineSession`
1717
func createSession(dependencies: EngineSessionDependencies) throws -> EngineSession
18+
19+
/// Warm the `Engine` whenever we move the application to foreground
20+
func warmEngine()
21+
22+
/// Idle the `Engine` whenever we move the application to background
23+
func idleEngine()
1824
}

‎BrowserKit/Sources/WebEngine/WKWebview/WKEngine.swift

+29-1
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at http://mozilla.org/MPL/2.0/
44

5+
import Common
56
import Foundation
67

78
public class WKEngine: Engine {
9+
private let sourceTimerFactory: DispatchSourceTimerFactory
10+
private var shutdownWebServerTimer: DispatchSourceInterface?
811
private let userScriptManager: WKUserScriptManager
12+
private let webServerUtil: WKWebServerUtil
913

1014
public static func factory() -> WKEngine {
1115
return WKEngine()
1216
}
1317

14-
init(userScriptManager: WKUserScriptManager = DefaultUserScriptManager()) {
18+
init(userScriptManager: WKUserScriptManager = DefaultUserScriptManager(),
19+
webServerUtil: WKWebServerUtil = DefaultWKWebServerUtil(),
20+
sourceTimerFactory: DispatchSourceTimerFactory = DefaultDispatchSourceTimerFactory()) {
1521
self.userScriptManager = userScriptManager
22+
self.webServerUtil = webServerUtil
23+
self.sourceTimerFactory = sourceTimerFactory
1624

1725
InternalUtil().setUpInternalHandlers()
1826
}
@@ -31,4 +39,24 @@ public class WKEngine: Engine {
3139

3240
return session
3341
}
42+
43+
public func warmEngine() {
44+
shutdownWebServerTimer?.cancel()
45+
shutdownWebServerTimer = nil
46+
47+
webServerUtil.setUpWebServer()
48+
}
49+
50+
public func idleEngine() {
51+
let timer = sourceTimerFactory.createDispatchSource()
52+
// 2 seconds is ample for a localhost request to be completed by GCDWebServer.
53+
// <500ms is expected on newer devices.
54+
timer.schedule(deadline: .now() + 2.0, repeating: .never)
55+
timer.setEventHandler {
56+
self.webServerUtil.stopWebServer()
57+
self.shutdownWebServerTimer = nil
58+
}
59+
timer.resume()
60+
shutdownWebServerTimer = timer
61+
}
3462
}

‎BrowserKit/Sources/WebEngine/WKWebview/WebServer/WKEngineWebServer.swift

+78-21
Original file line numberDiff line numberDiff line change
@@ -7,52 +7,60 @@ import Foundation
77
import GCDWebServers
88

99
protocol WKEngineWebServerProtocol {
10-
var server: GCDWebServer { get }
10+
var isRunning: Bool { get }
11+
1112
@discardableResult
1213
func start() throws -> Bool
14+
func stop()
1315

16+
func addTestHandler()
1417
func baseReaderModeURL() -> String
1518
}
1619

1720
class WKEngineWebServer: WKEngineWebServerProtocol {
18-
private var logger: Logger
19-
2021
static let shared: WKEngineWebServerProtocol = WKEngineWebServer()
2122

22-
let server = GCDWebServer()
23-
24-
var base: String {
23+
private let logger: Logger
24+
private let server = GCDWebServer()
25+
private var base: String {
2526
return "http://localhost:\(server.port)"
2627
}
2728

2829
/// The private credentials for accessing resources on this Web server.
29-
let credentials: URLCredential
30+
private let credentials: URLCredential
3031

3132
/// A random, transient token used for authenticating requests.
3233
/// Other apps are able to make requests to our local Web server,
3334
/// so this prevents them from accessing any resources.
3435
private let sessionToken = UUID().uuidString
3536

37+
var isRunning: Bool {
38+
server.isRunning
39+
}
40+
3641
init(logger: Logger = DefaultLogger.shared) {
3742
credentials = URLCredential(user: sessionToken, password: "", persistence: .forSession)
3843
self.logger = logger
3944
}
4045

4146
@discardableResult
4247
func start() throws -> Bool {
43-
// TODO: FXIOS-7894 #17639 Move dependencies for WebServer
44-
// if !server.isRunning {
45-
// try server.start(options: [
46-
// GCDWebServerOption_Port: WKEngineInfo.webserverPort,
47-
// GCDWebServerOption_BindToLocalhost: true,
48-
// GCDWebServerOption_AutomaticallySuspendInBackground: false, // done by the app in AppDelegate
49-
// GCDWebServerOption_AuthenticationMethod: GCDWebServerAuthenticationMethod_Basic,
50-
// GCDWebServerOption_AuthenticationAccounts: [sessionToken: ""]
51-
// ])
52-
// }
48+
if !server.isRunning {
49+
try server.start(options: [
50+
GCDWebServerOption_Port: WKEngineInfo.webserverPort,
51+
GCDWebServerOption_BindToLocalhost: true,
52+
GCDWebServerOption_AutomaticallySuspendInBackground: false, // done by the app in AppDelegate
53+
GCDWebServerOption_AuthenticationMethod: GCDWebServerAuthenticationMethod_Basic,
54+
GCDWebServerOption_AuthenticationAccounts: [sessionToken: ""]
55+
])
56+
}
5357
return server.isRunning
5458
}
5559

60+
func stop() {
61+
server.stop()
62+
}
63+
5664
/// Convenience method to register a dynamic handler. Will be mounted at $base/$module/$resource
5765
func registerHandlerForMethod(
5866
_ method: String,
@@ -62,10 +70,9 @@ class WKEngineWebServer: WKEngineWebServerProtocol {
6270
) {
6371
// Prevent serving content if the requested host isn't a safelisted local host.
6472
let wrappedHandler = {(request: GCDWebServerRequest?) -> GCDWebServerResponse? in
65-
// TODO: FXIOS-7894 #17639 Move dependencies for WebServer
66-
// guard let request = request,
67-
// InternalURL.isValid(url: request.url)
68-
// else { return GCDWebServerResponse(statusCode: 403) }
73+
guard let request = request,
74+
WKInternalURL.isValid(url: request.url)
75+
else { return GCDWebServerResponse(statusCode: 403) }
6976

7077
return handler(request)
7178
}
@@ -108,6 +115,56 @@ class WKEngineWebServer: WKEngineWebServerProtocol {
108115
return URLForResource("page", module: "reader-mode")
109116
}
110117

118+
func addTestHandler() {
119+
// Add tracking protection check page
120+
server.addHandler(forMethod: "GET",
121+
path: "/test-fixture/find-in-page-test.html",
122+
request: GCDWebServerRequest.self) { (_: GCDWebServerRequest?) in
123+
let node = """
124+
<span> And the beast shall come forth surrounded by a roiling cloud of vengeance. \
125+
The house of the unbelievers shall be razed and they shall be scorched to the earth. \
126+
Their tags shall blink until the end of days. from The Book of Mozilla, 12:10 And the \
127+
beast shall be made legion. Its numbers shall be increased a thousand thousand fold. The \
128+
din of a million keyboards like unto a great storm shall cover the earth, and the followers \
129+
of Mammon shall tremble. from The Book of Mozilla, 3:31 (Red Letter Edition) </span>
130+
"""
131+
132+
let repeatCount = 1000
133+
let textNodes = [String](repeating: node, count: repeatCount).reduce("", +)
134+
return GCDWebServerDataResponse(html: "<html><body>\(textNodes)</body></html>")
135+
}
136+
137+
let htmlFixtures = ["test-indexeddb-private",
138+
"test-window-opener",
139+
"test-password",
140+
"test-password-submit",
141+
"test-password-2",
142+
"test-password-submit-2",
143+
"empty-login-form",
144+
"empty-login-form-submit",
145+
"test-example",
146+
"test-example-link",
147+
"test-mozilla-book",
148+
"test-mozilla-org",
149+
"test-popup-blocker",
150+
"test-user-agent"]
151+
htmlFixtures.forEach {
152+
addHTMLFixture(name: $0, server: server)
153+
}
154+
}
155+
156+
// Make sure to add files to '/test-fixtures' directory in the source tree
157+
private func addHTMLFixture(name: String, server: GCDWebServer) {
158+
if let filePath = Bundle.main.path(forResource: "test-fixtures/\(name)", ofType: "html") {
159+
let fileHtml = try? String(contentsOfFile: filePath, encoding: .utf8)
160+
server.addHandler(forMethod: "GET",
161+
path: "/test-fixture/\(name).html",
162+
request: GCDWebServerRequest.self) { (request: GCDWebServerRequest?) in
163+
return GCDWebServerDataResponse(html: fileHtml!)
164+
}
165+
}
166+
}
167+
111168
/// Return a full url, as a string, for a resource in a module.
112169
/// No check is done to find out if the resource actually exist.
113170
private func URLForResource(_ resource: String, module: String) -> String {

0 commit comments

Comments
 (0)
Failed to load comments.