Skip to content

Add FXIOS-11179 [Dark Mode] Use darkreader for webviews #24327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jan 30, 2025
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ firefox-ios/Client/Assets/AllFramesAtDocumentEnd.js
firefox-ios/Client/Assets/MainFrameAtDocumentStart.js
firefox-ios/Client/Assets/MainFrameAtDocumentEnd.js
firefox-ios/Client/Assets/AutofillAllFramesAtDocumentStart.js
firefox-ios/Client/Assets/NightModeMainFrameAtDocumentEnd.js
firefox-ios/Client/Assets/WebcompatAllFramesAtDocumentStart.js
firefox-ios/Client/Assets/addressFormLayout.js
firefox-ios/Client/Assets/AddressFormManager.js
Expand Down
12 changes: 12 additions & 0 deletions BrowserKit/Sources/Shared/Extensions/WKWebViewExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ extension WKWebView {
self.evaluateJavaScript(javascript, in: nil, in: .defaultClient, completionHandler: { _ in })
}

/// This evaluates the provided JS in the specified content world
/// - Parameters:
/// - javascript: String representing javascript to be evaluated
/// - contentWorld: The content world in which to evaluate the script
public func evaluateJavascriptInCustomContentWorld(_ javascript: String, in contentWorld: WKContentWorld) {
self.evaluateJavaScript(javascript, in: nil, in: contentWorld, completionHandler: { _ in })
}

/// This calls different WebKit evaluateJavaScript functions depending on iOS version with
/// a completion that passes a tuple with optional data or an optional error
/// - If iOS14 or higher, evaluates Javascript in a .defaultClient sandboxed content world
Expand Down Expand Up @@ -73,6 +81,10 @@ extension WKUserContentController {
public func addInPageContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String) {
add(scriptMessageHandler, contentWorld: .page, name: name)
}

public func addInCustomContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String) {
add(scriptMessageHandler, contentWorld: .world(name: name), name: name)
}
}

extension WKUserScript {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ protocol WKContentScriptManager: WKScriptMessageHandler {
name: String,
forSession session: WKEngineSession)

func addContentScriptToCustomWorld(_ script: WKContentScript,
name: String,
forSession session: WKEngineSession)

func uninstall(session: WKEngineSession)
}

Expand Down Expand Up @@ -57,6 +61,23 @@ class DefaultContentScriptManager: NSObject, WKContentScriptManager {
}
}

func addContentScriptToCustomWorld(_ script: WKContentScript,
name: String,
forSession session: WKEngineSession) {
// If a script already exists on the page, skip adding this duplicate.
guard scripts[name] == nil else { return }
Comment on lines +67 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice


scripts[name] = script

// If this helper handles script messages, then get the handlers names and register them
script.scriptMessageHandlerNames().forEach { scriptMessageHandlerName in
session.webView.engineConfiguration.addInCustomContentWorld(
scriptMessageHandler: self,
name: name
)
}
}

func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
for script in scripts.values where script.scriptMessageHandlerNames().contains(message.name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ protocol WKEngineConfiguration {
func addUserScript(_ userScript: WKUserScript)
func addInDefaultContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String)
func addInPageContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String)
func addInCustomContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String)
func removeScriptMessageHandler(forName name: String)
func removeAllUserScripts()
}
Expand All @@ -35,6 +36,12 @@ struct DefaultEngineConfiguration: WKEngineConfiguration {
name: name)
}

func addInCustomContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String) {
webViewConfiguration.userContentController.add(scriptMessageHandler,
contentWorld: .world(name: name),
name: name)
}

func removeScriptMessageHandler(forName name: String) {
webViewConfiguration.userContentController.removeScriptMessageHandler(forName: name)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ class MockWKContentScriptManager: NSObject, WKContentScriptManager {
var scripts = [String: WKContentScript]()
var addContentScriptCalled = 0
var addContentScriptToPageCalled = 0
var addContentScriptToCustomWorldCalled = 0
var uninstallCalled = 0
var userContentControllerCalled = 0

var savedContentScriptNames = [String]()
var savedContentScriptPageNames = [String]()
var savedContentScriptCustomWorldNames = [String]()

func addContentScript(_ script: WKContentScript,
name: String,
Expand All @@ -32,6 +34,14 @@ class MockWKContentScriptManager: NSObject, WKContentScriptManager {
addContentScriptToPageCalled += 1
}

func addContentScriptToCustomWorld(_ script: WKContentScript,
name: String,
forSession session: WKEngineSession) {
scripts[name] = script
savedContentScriptCustomWorldNames.append(name)
addContentScriptToCustomWorldCalled += 1
}

func uninstall(session: WKEngineSession) {
uninstallCalled += 1
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class MockWKEngineConfiguration: WKEngineConfiguration {
var addUserScriptCalled = 0
var addInDefaultContentWorldCalled = 0
var addInPageContentWorldCalled = 0
var addInCustomContentWorldCalled = 0
var removeScriptMessageHandlerCalled = 0
var removeAllUserScriptsCalled = 0

Expand All @@ -28,6 +29,11 @@ class MockWKEngineConfiguration: WKEngineConfiguration {
addInPageContentWorldCalled += 1
}

func addInCustomContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String) {
scriptNameAdded = name
addInCustomContentWorldCalled += 1
}

func removeScriptMessageHandler(forName name: String) {
removeScriptMessageHandlerCalled += 1
}
Expand Down
4 changes: 4 additions & 0 deletions firefox-ios/Client.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,7 @@
BA1C68BC2B7ED153000D9397 /* MockWebKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1C68BB2B7ED153000D9397 /* MockWebKit.swift */; };
BA7A14842C2CCEB3008DF1D9 /* EditAddressWebViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7A14832C2CCEB3008DF1D9 /* EditAddressWebViewManager.swift */; };
BA8E197F2BF2FB1900590B5F /* AddressFormManager.js in Resources */ = {isa = PBXBuildFile; fileRef = BA8E197E2BF2FB1900590B5F /* AddressFormManager.js */; };
BA9AB4942D49A55900DD85E0 /* NightModeMainFrameAtDocumentEnd.js in Resources */ = {isa = PBXBuildFile; fileRef = BA9AB4932D49A55700DD85E0 /* NightModeMainFrameAtDocumentEnd.js */; };
BC003F5E2B59F44600929ECB /* BrowserViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC003F5D2B59F44500929ECB /* BrowserViewControllerTests.swift */; };
BCFF93EE2AAA9F6E005B5B71 /* RustFirefoxSuggest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFF93ED2AAA9C47005B5B71 /* RustFirefoxSuggest.swift */; };
BCFF93F02AABA55A005B5B71 /* BackgroundFirefoxSuggestIngestUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFF93EF2AAB97A8005B5B71 /* BackgroundFirefoxSuggestIngestUtility.swift */; };
Expand Down Expand Up @@ -8486,6 +8487,7 @@
BA7A14832C2CCEB3008DF1D9 /* EditAddressWebViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAddressWebViewManager.swift; sourceTree = "<group>"; };
BA8E197E2BF2FB1900590B5F /* AddressFormManager.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = AddressFormManager.js; sourceTree = "<group>"; };
BA904A3B89BC820A7A802D55 /* es-CL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-CL"; path = "es-CL.lproj/Storage.strings"; sourceTree = "<group>"; };
BA9AB4932D49A55700DD85E0 /* NightModeMainFrameAtDocumentEnd.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = NightModeMainFrameAtDocumentEnd.js; path = Client/Assets/NightModeMainFrameAtDocumentEnd.js; sourceTree = "<group>"; };
BAA64356B54F1CD258764620 /* su */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = su; path = su.lproj/FindInPage.strings; sourceTree = "<group>"; };
BAF14FEC94CB9DA4E08BA60C /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Storage.strings; sourceTree = "<group>"; };
BAF74394B38CDAE36910B788 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Shared.strings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -14353,6 +14355,7 @@
F84B21B51A090F8100AAB793 = {
isa = PBXGroup;
children = (
BA9AB4932D49A55700DD85E0 /* NightModeMainFrameAtDocumentEnd.js */,
2FA435FC1ABB83B4008031D1 /* Account */,
D4F0C2642B2C90FB008ECEE8 /* BrowserKit */,
F84B21C01A090F8100AAB793 /* Client */,
Expand Down Expand Up @@ -15677,6 +15680,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BA9AB4942D49A55900DD85E0 /* NightModeMainFrameAtDocumentEnd.js in Resources */,
BA8E197F2BF2FB1900590B5F /* AddressFormManager.js in Resources */,
8A1A935B2B757C7C0069C190 /* wave.json in Resources */,
D4AFAB0E2AFA8F9A000BFEAA /* SyncIntegrationTests in Resources */,
Expand Down
34 changes: 34 additions & 0 deletions firefox-ios/Client/Assets/About/Licenses.html
Original file line number Diff line number Diff line change
Expand Up @@ -747,5 +747,39 @@ <h4 id="exhibit-b---incompatible-with-secondary-licenses-notice">Exhibit B - “
</div>
<center><p>-</p></center>

<div>
<input id="ac-28" name="accordion-1" type="checkbox" />
<label for="ac-28"><h2>darkreader</h2></label>
<article>
<p class="link"><a href="https://github.com/darkreader/darkreader">https://github.com/darkreader/darkreader</a></p>
<div class="text">
<p>MIT License</p>

<p>Copyright (c) 2024 Dark Reader Ltd.</p>

<p>All rights reserved.</p>

<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>

<p>The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.</p>

<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.</p>
</div>
</article>
</div>
<center><p>-</p></center>

</html>

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ enum NimbusFeatureFlagID: String, CaseIterable {
case contextualHintForToolbar
case creditCardAutofillStatus
case cleanupHistoryReenabled
case darkReader
case fakespotBackInStock
case fakespotFeature
case fakespotProductAds
Expand Down Expand Up @@ -112,6 +113,7 @@ struct NimbusFlaggableFeature: HasNimbusSearchBar {
.cleanupHistoryReenabled,
.creditCardAutofillStatus,
.closeRemoteTabs,
.darkReader,
.fakespotBackInStock,
.fakespotFeature,
.fakespotProductAds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3567,7 +3567,7 @@ extension BrowserViewController: LegacyTabDelegate {
tab.addContentScriptToPage(printHelper, name: PrintHelper.name())

let nightModeHelper = NightModeHelper()
tab.addContentScript(nightModeHelper, name: NightModeHelper.name())
tab.addContentScriptToCustomWorld(nightModeHelper, name: NightModeHelper.name())

// XXX: Bug 1390200 - Disable NSUserActivity/CoreSpotlight temporarily
// let spotlightHelper = SpotlightHelper(tab: tab)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,20 @@ class NightModeHelper: TabContentScript, FeatureFlaggable {
return ["NightMode"]
}

static func jsCallbackBuilder(_ enabled: Bool) -> String {
let isDarkReader = LegacyFeatureFlagsManager.shared.isFeatureEnabled(.darkReader, checking: .buildOnly)
return "window.__firefox__.NightMode.setEnabled(\(enabled), \(isDarkReader))"
}

func userContentController(
_ userContentController: WKUserContentController,
didReceiveScriptMessage message: WKScriptMessage
) {
guard let webView = message.frameInfo.webView else { return }
let jsCallback = "window.__firefox__.NightMode.setEnabled(\(NightModeHelper.isActivated()))"
webView.evaluateJavascriptInDefaultContentWorld(jsCallback)
webView.evaluateJavascriptInCustomContentWorld(
NightModeHelper.jsCallbackBuilder(NightModeHelper.isActivated()),
in: .world(name: NightModeHelper.name())
)
}

static func toggle(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ class UserScriptManager: FeatureFlaggable {
source: "window.__firefox__.NoImageMode.setEnabled(true)",
injectionTime: .atDocumentStart,
forMainFrameOnly: true)
private let nightModeUserScript = WKUserScript.createInDefaultContentWorld(
source: "window.__firefox__.NightMode.setEnabled(true)",
private let nightModeUserScript = WKUserScript(
source: NightModeHelper.jsCallbackBuilder(true),
injectionTime: .atDocumentEnd,
forMainFrameOnly: true)
forMainFrameOnly: true,
in: .world(name: NightModeHelper.name()))
private let printHelperUserScript = WKUserScript.createInPageContentWorld(
source: "window.print = function () { window.webkit.messageHandlers.printHandler.postMessage({}) }",
injectionTime: .atDocumentEnd,
Expand All @@ -39,8 +40,7 @@ class UserScriptManager: FeatureFlaggable {
let mainframeString = mainFrameOnly ? "MainFrame" : "AllFrames"
let injectionString = injectionTime == .atDocumentStart ? "Start" : "End"
let name = mainframeString + "AtDocument" + injectionString
if let path = Bundle.main.path(forResource: name, ofType: "js"),
let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String {
if let source = UserScriptManager.getScriptSource(name) {
let wrappedSource = "(function() { const APP_ID_TOKEN = '\(UserScriptManager.appIdToken)'; \(source) })()"
let userScript = WKUserScript.createInDefaultContentWorld(
source: wrappedSource,
Expand All @@ -52,11 +52,7 @@ class UserScriptManager: FeatureFlaggable {

// Autofill scripts
let autofillName = "Autofill\(name)"
if let autofillScriptCompatPath = Bundle.main.path(
forResource: autofillName, ofType: "js"),
let source = try? NSString(
contentsOfFile: autofillScriptCompatPath,
encoding: String.Encoding.utf8.rawValue) as String {
if let source = UserScriptManager.getScriptSource(autofillName) {
let wrappedSource = "(function() { const APP_ID_TOKEN = '\(UserScriptManager.appIdToken)'; \(source) })()"
let userScript = WKUserScript.createInDefaultContentWorld(
source: wrappedSource,
Expand All @@ -65,15 +61,20 @@ class UserScriptManager: FeatureFlaggable {
compiledUserScripts[autofillName] = userScript
}

// NightMode scripts
let nightModeName = "NightMode\(name)"
if let source = UserScriptManager.getScriptSource(nightModeName) {
let wrappedSource = "(function() { const APP_ID_TOKEN = '\(UserScriptManager.appIdToken)'; \(source) })()"
let userScript = WKUserScript(
source: wrappedSource,
injectionTime: injectionTime,
forMainFrameOnly: mainFrameOnly,
in: .world(name: NightModeHelper.name()))
compiledUserScripts[nightModeName] = userScript
}

let webcompatName = "Webcompat\(name)"
if let webCompatPath = Bundle.main.path(
forResource: webcompatName,
ofType: "js"
),
let source = try? NSString(
contentsOfFile: webCompatPath,
encoding: String.Encoding.utf8.rawValue
) as String {
if let source = UserScriptManager.getScriptSource(webcompatName) {
let wrappedSource = "(function() { const APP_ID_TOKEN = '\(UserScriptManager.appIdToken)'; \(source) })()"
let userScript = WKUserScript.createInPageContentWorld(
source: wrappedSource,
Expand All @@ -87,6 +88,13 @@ class UserScriptManager: FeatureFlaggable {
self.compiledUserScripts = compiledUserScripts
}

private static func getScriptSource(_ scriptName: String) -> String? {
guard let path = Bundle.main.path(forResource: scriptName, ofType: "js") else {
return nil
}
return try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String
}

public func injectUserScriptsIntoWebView(_ webView: WKWebView?, nightMode: Bool, noImageMode: Bool) {
// Start off by ensuring that any previously-added user scripts are
// removed to prevent the same script from being injected twice.
Expand All @@ -110,6 +118,11 @@ class UserScriptManager: FeatureFlaggable {
webView?.configuration.userContentController.addUserScript(autofillScript)
}

let nightModeName = "NightMode\(name)"
if let nightModeScript = compiledUserScripts[nightModeName] {
webView?.configuration.userContentController.addUserScript(nightModeScript)
}

let webcompatName = "Webcompat\(name)"
if let webcompatUserScript = compiledUserScripts[webcompatName] {
webView?.configuration.userContentController.addUserScript(webcompatUserScript)
Expand Down
Loading