Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Added JS event listener for tab web-state.
Browse files Browse the repository at this point in the history
  • Loading branch information
Brandon-T committed Mar 30, 2022
1 parent b39ec78 commit 0de8e2e
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 1 deletion.
8 changes: 8 additions & 0 deletions Client.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,8 @@
CA225DF72795D1A80024C104 /* MainFrameAtDocumentEndSandboxed.js in Resources */ = {isa = PBXBuildFile; fileRef = CA225DF52795D1A80024C104 /* MainFrameAtDocumentEndSandboxed.js */; };
CA225DF92795D7690024C104 /* WKContentWorld+Sandbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA225DF82795D7690024C104 /* WKContentWorld+Sandbox.swift */; };
CA29F2F3273DAEA100C391C3 /* PlaylistOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA29F2F2273DAEA100C391C3 /* PlaylistOnboardingView.swift */; };
CA2CA1B127F4B50300B25646 /* ReadyState.js in Resources */ = {isa = PBXBuildFile; fileRef = CA2CA1B027F4B50300B25646 /* ReadyState.js */; };
CA2CA1CF27F4B93400B25646 /* ReadyStateScriptHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2CA1CE27F4B93400B25646 /* ReadyStateScriptHelper.swift */; };
CA2EE0A527349E970089C75F /* disconnect-entitylist.json in Resources */ = {isa = PBXBuildFile; fileRef = CA2EE09927349E970089C75F /* disconnect-entitylist.json */; };
CA2EE0A727349F760089C75F /* OnboardingAdblockDisconnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2EE0A627349F760089C75F /* OnboardingAdblockDisconnect.swift */; };
CA439A5925E6F29D00FE9150 /* VideoPlayerInfoBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA439A5825E6F29D00FE9150 /* VideoPlayerInfoBar.swift */; };
Expand Down Expand Up @@ -3014,6 +3016,8 @@
CA225DF52795D1A80024C104 /* MainFrameAtDocumentEndSandboxed.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = MainFrameAtDocumentEndSandboxed.js; sourceTree = "<group>"; };
CA225DF82795D7690024C104 /* WKContentWorld+Sandbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKContentWorld+Sandbox.swift"; sourceTree = "<group>"; };
CA29F2F2273DAEA100C391C3 /* PlaylistOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistOnboardingView.swift; sourceTree = "<group>"; };
CA2CA1B027F4B50300B25646 /* ReadyState.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = ReadyState.js; sourceTree = "<group>"; };
CA2CA1CE27F4B93400B25646 /* ReadyStateScriptHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadyStateScriptHelper.swift; sourceTree = "<group>"; };
CA2EE09927349E970089C75F /* disconnect-entitylist.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "disconnect-entitylist.json"; sourceTree = "<group>"; };
CA2EE0A627349F760089C75F /* OnboardingAdblockDisconnect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAdblockDisconnect.swift; sourceTree = "<group>"; };
CA439A5825E6F29D00FE9150 /* VideoPlayerInfoBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerInfoBar.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4833,6 +4837,7 @@
D31F95E81AC226CB005C9F3B /* ScreenshotHelper.swift */,
F35B8D2C1D6383E9008E3D61 /* SessionRestoreHelper.swift */,
5E4845C122DE3DF800372022 /* WindowRenderHelperScript.swift */,
CA2CA1CE27F4B93400B25646 /* ReadyStateScriptHelper.swift */,
);
path = Helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -6064,6 +6069,7 @@
5EC2C0E425C0B321005EA984 /* PlaylistDetector.js */,
5EC0017D260129AC005DDE4A /* PlaylistSwizzler.js */,
CA55048F269DED8F00C19917 /* MediaBackgrounding.js */,
CA2CA1B027F4B50300B25646 /* ReadyState.js */,
);
path = UserScripts;
sourceTree = "<group>";
Expand Down Expand Up @@ -7732,6 +7738,7 @@
CA225DF22795CECE0024C104 /* MainFrame.js in Resources */,
4481F23626CBD27B00658EAC /* GlobalSignRootCA_E46.cer in Resources */,
4422D55221BFFB7E00BF1855 /* make_unicode_casefold.py in Resources */,
CA2CA1B127F4B50300B25646 /* ReadyState.js in Resources */,
5E4E078324A0E4D700B01720 /* YoutubeAdblock.js in Resources */,
445ABC962731DDFF0089710D /* NewYorkMedium-Bold.otf in Resources */,
E4B7B7791A793CF20022C5E0 /* FiraSans-Light.ttf in Resources */,
Expand Down Expand Up @@ -8440,6 +8447,7 @@
27FD2CAB2146C31C00A5A779 /* RequestDesktopSiteActivity.swift in Sources */,
3BCE6D3C1CEB9E4D0080928C /* ThirdPartySearchAlerts.swift in Sources */,
4422D55A21BFFB7F00BF1855 /* regexp.cc in Sources */,
CA2CA1CF27F4B93400B25646 /* ReadyStateScriptHelper.swift in Sources */,
0A7B5D722269E7AD00AADF22 /* BookmarkEditMode.swift in Sources */,
4422D4DC21BFFB7600BF1855 /* block_builder.cc in Sources */,
27201EFE24539B5500C19DD1 /* NewTabPageBackground.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Client/Frontend/Browser/BrowserViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2046,6 +2046,7 @@ extension BrowserViewController: TabDelegate {

tab.addContentScript(RewardsReporting(rewards: rewards, tab: tab), name: RewardsReporting.name(), contentWorld: .page)
tab.addContentScript(AdsMediaReporting(rewards: rewards, tab: tab), name: AdsMediaReporting.name(), contentWorld: .defaultClient)
tab.addContentScript(ReadyStateScriptHelper(tab: tab), name: ReadyStateScriptHelper.name(), contentWorld: .page)
}

func tab(_ tab: Tab, willDeleteWebView webView: WKWebView) {
Expand Down
82 changes: 82 additions & 0 deletions Client/Frontend/Browser/Helpers/ReadyStateScriptHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2022 The Brave Authors. All rights reserved.
// 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/.

import Foundation
import Shared
import WebKit

private let log = Logger.browserLogger

struct ReadyState: Codable {
let securityToken: String
let state: State

enum State: String, Codable {
// Page State
case loading
case interactive
case complete
case loaded

// History State
case pushstate
case replacestate
case popstate
}

public static func from(message: WKScriptMessage) -> ReadyState? {
if !JSONSerialization.isValidJSONObject(message.body) {
return nil
}

do {
let data = try JSONSerialization.data(withJSONObject: message.body, options: [])
return try JSONDecoder().decode(ReadyState.self, from: data)
} catch {
log.error("Error Decoding ReadyState: \(error)")
}

return nil
}

private enum CodingKeys: String, CodingKey {
case securityToken = "securitytoken"
case state
}
}

class ReadyStateScriptHelper: TabContentScript {
private weak var tab: Tab?
private var debounceTimer: Timer?

required init(tab: Tab) {
self.tab = tab
}

class func name() -> String {
return "ReadyStateScriptHelper"
}

func scriptMessageHandlerName() -> String? {
return "ReadyState_\(UserScriptManager.messageHandlerTokenString)"
}

func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage, replyHandler: (Any?, String?) -> Void) {

defer { replyHandler(nil, nil) }

guard let readyState = ReadyState.from(message: message) else {
log.error("Invalid Ready State")
return
}

guard readyState.securityToken == UserScriptManager.securityTokenString else {
log.error("Invalid or Missing security token")
return
}

tab?.onPageReadyStateChanged?(readyState.state)
}
}
2 changes: 2 additions & 0 deletions Client/Frontend/Browser/Tab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ class Tab: NSObject {
var screenshotUUID: UUID? {
didSet { TabMO.saveScreenshotUUID(screenshotUUID, tabId: id) }
}

var onPageReadyStateChanged: ((ReadyState.State) -> Void)?

// If this tab has been opened from another, its parent will point to the tab from which it was opened
var parent: Tab?
Expand Down
22 changes: 21 additions & 1 deletion Client/Frontend/Browser/TabManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,27 @@ class TabManager: NSObject {
if flushToDisk && !zombie && !isPrivate {
saveTab(tab, saveOrder: true)
}


// When the state of the page changes, we debounce a call to save the screenshots and tab information
// This fixes pages that have dynamic URL via changing history
// as well as regular pages that load DOM normally.
var debounce_timer: Timer?
tab.onPageReadyStateChanged = { [weak self] state in
guard let self = self else { return }

debounce_timer?.invalidate()
debounce_timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
debounce_timer?.invalidate()

if state == .complete || state == .loaded || state == .pushstate || state == .popstate {
// Saving Tab Private Mode - not supported yet.
if !tab.isPrivate {
self.preserveScreenshots()
self.saveTab(tab)
}
}
}
}
}

func indexOfWebView(_ webView: WKWebView) -> UInt? {
Expand Down
29 changes: 29 additions & 0 deletions Client/Frontend/Browser/UserScriptManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,31 @@ class UserScriptManager {
forMainFrameOnly: true,
in: .defaultClient)
}()

private let ReadyStateScript: WKUserScript? = {
guard let path = Bundle.main.path(forResource: "ReadyState", ofType: "js"), let source = try? String(contentsOfFile: path) else {
log.error("Failed to load ReadyState.js")
return nil
}

var alteredSource = source
let token = UserScriptManager.securityTokenString

let replacements = [
"$<security_token>": token,
"$<handler>": "ReadyState_\(messageHandlerTokenString)",
]

replacements.forEach({
alteredSource = alteredSource.replacingOccurrences(of: $0.key, with: $0.value, options: .literal)
})

return WKUserScript.create(
source: alteredSource,
injectionTime: .atDocumentStart,
forMainFrameOnly: true,
in: .page)
}()

private func reloadUserScripts() {
tab?.webView?.configuration.userContentController.do {
Expand Down Expand Up @@ -403,6 +428,10 @@ class UserScriptManager {
if isNightModeEnabled, let script = NightModeScript {
$0.addUserScript(script)
}

if let script = ReadyStateScript {
$0.addUserScript(script)
}

if let domainUserScript = domainUserScript, let script = domainUserScript.script {
$0.addUserScript(script)
Expand Down
77 changes: 77 additions & 0 deletions Client/Frontend/UserContent/UserScripts/ReadyState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2021 The Brave Authors. All rights reserved.
// 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/.

(function() {
// Listen for document ready state
document.addEventListener('readystatechange', (event) => {
window.webkit.messageHandlers.$<handler>.postMessage({
"securitytoken": "$<security_token>",
"state": document.readyState
});
});

// Listen for document load state
window.addEventListener('load', (event) => {
window.webkit.messageHandlers.$<handler>.postMessage({
"securitytoken": "$<security_token>",
"state": "loaded"
});
});

// Listen for history popped
window.addEventListener('popstate', (event) => {
if (event.state) {
// Run on the browser's next run-loop
setTimeout(() => {
window.webkit.messageHandlers.$<handler>.postMessage({
"securitytoken": "$<security_token>",
"state": "popstate"
});
}, 0);
}
});

// Listen for history pushed
const pushState = History.prototype.pushState;
History.prototype.pushState = function(state, unused, url) {
pushState.call(this, state, unused, url);

if (state) {
// Run on the browser's next run-loop
setTimeout(() => {
window.webkit.messageHandlers.$<handler>.postMessage({
"securitytoken": "$<security_token>",
"state": "pushstate"
});
}, 0);
}
};

// Listen for history replaced
const replaceState = History.prototype.replaceState;
History.prototype.replaceState = function(state, unused, url) {
replaceState.call(this, state, unused, url);

if (state) {
// Run on the browser's next run-loop
setTimeout(() => {
window.webkit.messageHandlers.$<handler>.postMessage({
"securitytoken": "$<security_token>",
"state": "replacestate"
});
}, 0);
}
};

// Hide the pushState trampoline
History.prototype.pushState.toString = function() {
return "function () { [native code] }";
};

// Hide the replaceState trampoline
History.prototype.replaceState.toString = function() {
return "function () { [native code] }";
};
})();

0 comments on commit 0de8e2e

Please sign in to comment.