[iOS] Opt-in to prettier for iOS typescript/markdown (#36998)
This commit is contained in:
+2
-1
@@ -11,7 +11,7 @@
|
||||
/chromium_src/components/
|
||||
/chromium_src/third_party/
|
||||
/chromium_src/ui/
|
||||
/ios
|
||||
/ios/brave-ios/
|
||||
/resources
|
||||
/test
|
||||
/.storybook/
|
||||
@@ -21,6 +21,7 @@
|
||||
/third_party/
|
||||
/third_party/boringtun/vendor/
|
||||
/third_party/wasm/vendor/
|
||||
/ios/third_party/
|
||||
|
||||
# Ignore github .yml files:
|
||||
.github/**/*.yml
|
||||
|
||||
@@ -3,74 +3,100 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {CrWebApi, gCrWeb} from
|
||||
'//ios/web/public/js_messaging/resources/gcrweb.js';
|
||||
import {
|
||||
CrWebApi,
|
||||
gCrWeb,
|
||||
} from '//ios/web/public/js_messaging/resources/gcrweb.js'
|
||||
|
||||
const kRolesToSkip: string[] = [
|
||||
'audio', 'banner', 'button', 'complementary',
|
||||
'contentinfo', 'footer', 'img', 'label', 'navigation',
|
||||
'textbox', 'combobox', 'listbox', 'checkbox', 'radiobutton',
|
||||
'slider', 'spinbutton', 'searchbox',
|
||||
];
|
||||
'audio',
|
||||
'banner',
|
||||
'button',
|
||||
'complementary',
|
||||
'contentinfo',
|
||||
'footer',
|
||||
'img',
|
||||
'label',
|
||||
'navigation',
|
||||
'textbox',
|
||||
'combobox',
|
||||
'listbox',
|
||||
'checkbox',
|
||||
'radiobutton',
|
||||
'slider',
|
||||
'spinbutton',
|
||||
'searchbox',
|
||||
]
|
||||
|
||||
const kTagsToSkip: string[] = [
|
||||
'AUDIO', 'HEADER', 'BUTTON', 'ASIDE',
|
||||
'FOOTER', 'IMG', 'PICTURE', 'LABEL', 'NAV',
|
||||
'INPUT', 'SEARCH', 'STYLE',
|
||||
];
|
||||
'AUDIO',
|
||||
'HEADER',
|
||||
'BUTTON',
|
||||
'ASIDE',
|
||||
'FOOTER',
|
||||
'IMG',
|
||||
'PICTURE',
|
||||
'LABEL',
|
||||
'NAV',
|
||||
'INPUT',
|
||||
'SEARCH',
|
||||
'STYLE',
|
||||
]
|
||||
|
||||
// Walk the node tree to find <main> and <article> tags and return them.
|
||||
function getRootNodes(): Node[] {
|
||||
const result: Node[] = [];
|
||||
const queue: Node[] = [document.documentElement];
|
||||
const result: Node[] = []
|
||||
const queue: Node[] = [document.documentElement]
|
||||
while (queue.length !== 0) {
|
||||
const node = queue.pop()!;
|
||||
const el = node as HTMLElement;
|
||||
if ((el.role === 'main' || node.nodeName === 'MAIN') ||
|
||||
(el.role === 'article' || node.nodeName === 'ARTICLE')) {
|
||||
result.push(node);
|
||||
continue;
|
||||
const node = queue.pop()!
|
||||
const el = node as HTMLElement
|
||||
if (
|
||||
el.role === 'main'
|
||||
|| node.nodeName === 'MAIN'
|
||||
|| el.role === 'article'
|
||||
|| node.nodeName === 'ARTICLE'
|
||||
) {
|
||||
result.push(node)
|
||||
continue
|
||||
}
|
||||
for (const child of node.childNodes) {
|
||||
queue.push(child);
|
||||
queue.push(child)
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
// Recursively collect text from root, skipping unwanted roles and tags.
|
||||
function collectText(root: Node, out: string[]): void {
|
||||
const queue: Node[] = [root];
|
||||
const queue: Node[] = [root]
|
||||
while (queue.length !== 0) {
|
||||
const node = queue.pop()!;
|
||||
const el = node as HTMLElement;
|
||||
const node = queue.pop()!
|
||||
const el = node as HTMLElement
|
||||
if (el.role && kRolesToSkip.includes(el.role)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (kTagsToSkip.includes(node.nodeName)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
out.push((node as Text).wholeText);
|
||||
out.push((node as Text).wholeText)
|
||||
}
|
||||
for (const child of node.childNodes) {
|
||||
queue.push(child);
|
||||
queue.push(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMainArticle(): string {
|
||||
const rootNodes = getRootNodes();
|
||||
const textParts: string[] = [];
|
||||
const rootNodes = getRootNodes()
|
||||
const textParts: string[] = []
|
||||
for (const node of rootNodes) {
|
||||
collectText(node, textParts);
|
||||
collectText(node, textParts)
|
||||
}
|
||||
const text = textParts.join(' ');
|
||||
return text.length !== 0 ?
|
||||
text :
|
||||
(document.body ? document.body.innerText : '');
|
||||
const text = textParts.join(' ')
|
||||
return text.length !== 0 ? text : document.body ? document.body.innerText : ''
|
||||
}
|
||||
|
||||
const aiChatDistillerApi = new CrWebApi('aiChatDistiller');
|
||||
aiChatDistillerApi.addFunction('getMainArticle', getMainArticle);
|
||||
gCrWeb.registerApi(aiChatDistillerApi);
|
||||
const aiChatDistillerApi = new CrWebApi('aiChatDistiller')
|
||||
aiChatDistillerApi.addFunction('getMainArticle', getMainArticle)
|
||||
gCrWeb.registerApi(aiChatDistillerApi)
|
||||
|
||||
@@ -3,35 +3,43 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {sendWebKitMessage} from '//ios/web/public/js_messaging/resources/utils.js';
|
||||
import { sendWebKitMessage } from '//ios/web/public/js_messaging/resources/utils.js'
|
||||
|
||||
function sendMessage(playing: boolean) {
|
||||
sendWebKitMessage('AdsMediaReportingMessageHandler', {'isPlaying': playing});
|
||||
sendWebKitMessage('AdsMediaReportingMessageHandler', { 'isPlaying': playing })
|
||||
}
|
||||
|
||||
function isPlayingVideoWithAudio(video: HTMLVideoElement): boolean {
|
||||
return !video.paused && !video.muted;
|
||||
return !video.paused && !video.muted
|
||||
}
|
||||
|
||||
function hookVideoElement(video: HTMLVideoElement) {
|
||||
video.addEventListener('pause', () => sendMessage(false), false);
|
||||
video.addEventListener('playing', () => sendMessage(isPlayingVideoWithAudio(video)), false);
|
||||
video.addEventListener('volumechange', () => sendMessage(isPlayingVideoWithAudio(video)), false);
|
||||
video.addEventListener('pause', () => sendMessage(false), false)
|
||||
video.addEventListener(
|
||||
'playing',
|
||||
() => sendMessage(isPlayingVideoWithAudio(video)),
|
||||
false,
|
||||
)
|
||||
video.addEventListener(
|
||||
'volumechange',
|
||||
() => sendMessage(isPlayingVideoWithAudio(video)),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
document.querySelectorAll('video').forEach(hookVideoElement);
|
||||
document.querySelectorAll('video').forEach(hookVideoElement)
|
||||
|
||||
const observer = new MutationObserver(function(mutations: MutationRecord[]) {
|
||||
mutations.forEach(function(mutation: MutationRecord) {
|
||||
mutation.addedNodes.forEach(function(node: Node) {
|
||||
const observer = new MutationObserver(function (mutations: MutationRecord[]) {
|
||||
mutations.forEach(function (mutation: MutationRecord) {
|
||||
mutation.addedNodes.forEach(function (node: Node) {
|
||||
if (node instanceof HTMLVideoElement) {
|
||||
hookVideoElement(node);
|
||||
hookVideoElement(node)
|
||||
} else if (node instanceof HTMLElement) {
|
||||
// Some sites inject a container element that already has video
|
||||
// descendants, so the video itself is never a direct added node.
|
||||
node.querySelectorAll('video').forEach(hookVideoElement);
|
||||
node.querySelectorAll('video').forEach(hookVideoElement)
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
observer.observe(document, {subtree: true, childList: true});
|
||||
})
|
||||
})
|
||||
})
|
||||
observer.observe(document, { subtree: true, childList: true })
|
||||
|
||||
@@ -3,21 +3,24 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {CrWebApi, gCrWeb} from '//ios/web/public/js_messaging/resources/gcrweb.js';
|
||||
import {
|
||||
CrWebApi,
|
||||
gCrWeb,
|
||||
} from '//ios/web/public/js_messaging/resources/gcrweb.js'
|
||||
|
||||
interface Creative {
|
||||
creativeInstanceId?: string;
|
||||
placementId?: string;
|
||||
creativeSetId?: string;
|
||||
campaignId?: string;
|
||||
advertiserId?: string;
|
||||
landingPage?: string;
|
||||
headlineText?: string;
|
||||
description?: string;
|
||||
rewardsValue?: string;
|
||||
conversionUrlPatternValue?: string;
|
||||
conversionAdvertiserPublicKeyValue?: string;
|
||||
conversionObservationWindowValue?: string;
|
||||
creativeInstanceId?: string
|
||||
placementId?: string
|
||||
creativeSetId?: string
|
||||
campaignId?: string
|
||||
advertiserId?: string
|
||||
landingPage?: string
|
||||
headlineText?: string
|
||||
description?: string
|
||||
rewardsValue?: string
|
||||
conversionUrlPatternValue?: string
|
||||
conversionAdvertiserPublicKeyValue?: string
|
||||
conversionObservationWindowValue?: string
|
||||
}
|
||||
|
||||
const creativeFieldNamesMapping: Record<string, keyof Creative> = {
|
||||
@@ -35,37 +38,38 @@ const creativeFieldNamesMapping: Record<string, keyof Creative> = {
|
||||
'conversionAdvertiserPublicKeyValue',
|
||||
'data-conversion-observation-window-value':
|
||||
'conversionObservationWindowValue',
|
||||
};
|
||||
}
|
||||
|
||||
function getCreatives(): string {
|
||||
const creatives: Creative[] = [];
|
||||
const scripts =
|
||||
document.querySelectorAll('script[type="application/ld+json"]');
|
||||
const creatives: Creative[] = []
|
||||
const scripts = document.querySelectorAll(
|
||||
'script[type="application/ld+json"]',
|
||||
)
|
||||
try {
|
||||
const jsonLdList =
|
||||
Array.from(scripts).map(script => JSON.parse(script.textContent || ''));
|
||||
const jsonLdList = Array.from(scripts).map((script) =>
|
||||
JSON.parse(script.textContent || ''),
|
||||
)
|
||||
|
||||
jsonLdList.forEach(jsonLd => {
|
||||
jsonLdList.forEach((jsonLd) => {
|
||||
if (jsonLd['@type'] === 'Product' && jsonLd.creatives) {
|
||||
jsonLd.creatives.forEach((creative: Record<string, string>) => {
|
||||
if (creative['@type'] === 'SearchResultAd') {
|
||||
const mapped: Creative = {};
|
||||
const mapped: Creative = {}
|
||||
for (const key in creative) {
|
||||
const mappedKey = creativeFieldNamesMapping[key];
|
||||
const mappedKey = creativeFieldNamesMapping[key]
|
||||
if (mappedKey) {
|
||||
mapped[mappedKey] = creative[key];
|
||||
mapped[mappedKey] = creative[key]
|
||||
}
|
||||
}
|
||||
creatives.push(mapped);
|
||||
creatives.push(mapped)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
}
|
||||
return JSON.stringify(creatives);
|
||||
})
|
||||
} catch {}
|
||||
return JSON.stringify(creatives)
|
||||
}
|
||||
|
||||
const braveSearchAdResultsApi = new CrWebApi('braveSearchAdResults');
|
||||
braveSearchAdResultsApi.addFunction('getCreatives', getCreatives);
|
||||
gCrWeb.registerApi(braveSearchAdResultsApi);
|
||||
const braveSearchAdResultsApi = new CrWebApi('braveSearchAdResults')
|
||||
braveSearchAdResultsApi.addFunction('getCreatives', getCreatives)
|
||||
gCrWeb.registerApi(braveSearchAdResultsApi)
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {sendWebKitMessageWithReply} from
|
||||
'//ios/web/public/js_messaging/resources/utils.js';
|
||||
import { sendWebKitMessageWithReply } from '//ios/web/public/js_messaging/resources/utils.js'
|
||||
|
||||
const allowedOrigins = [
|
||||
'https://safesearch.brave.com',
|
||||
@@ -14,7 +13,7 @@ const allowedOrigins = [
|
||||
'https://search.brave.com',
|
||||
'https://search.brave.software',
|
||||
'https://search.bravesoftware.com',
|
||||
];
|
||||
]
|
||||
|
||||
if (allowedOrigins.includes(window.location.origin)) {
|
||||
Object.defineProperty(window, 'brave', {
|
||||
@@ -24,13 +23,17 @@ if (allowedOrigins.includes(window.location.origin)) {
|
||||
value: {
|
||||
getCanSetDefaultSearchProvider() {
|
||||
return sendWebKitMessageWithReply(
|
||||
'BraveSearchMakeDefaultMessageHandler', {method_id: 1});
|
||||
'BraveSearchMakeDefaultMessageHandler',
|
||||
{ method_id: 1 },
|
||||
)
|
||||
},
|
||||
|
||||
setIsDefaultSearchProvider() {
|
||||
return sendWebKitMessageWithReply(
|
||||
'BraveSearchMakeDefaultMessageHandler', {method_id: 2});
|
||||
}
|
||||
}
|
||||
});
|
||||
'BraveSearchMakeDefaultMessageHandler',
|
||||
{ method_id: 2 },
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,28 +3,28 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {sendWebKitMessage} from
|
||||
'//ios/web/public/js_messaging/resources/utils.js';
|
||||
import { sendWebKitMessage } from '//ios/web/public/js_messaging/resources/utils.js'
|
||||
|
||||
const allowedOrigins = [
|
||||
'https://talk.brave.com',
|
||||
'https://talk.bravesoftware.com',
|
||||
'https://talk.brave.software',
|
||||
];
|
||||
]
|
||||
|
||||
if (allowedOrigins.includes(window.location.origin)) {
|
||||
const observer = new MutationObserver((mutations: MutationRecord[]) => {
|
||||
for (const mutation of mutations) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node instanceof HTMLIFrameElement) {
|
||||
sendWebKitMessage(
|
||||
'BraveTalkLauncherMessageHandler', {url: node.src});
|
||||
observer.disconnect();
|
||||
return;
|
||||
sendWebKitMessage('BraveTalkLauncherMessageHandler', {
|
||||
url: node.src,
|
||||
})
|
||||
observer.disconnect()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
observer.observe(document, {childList: true, subtree: true});
|
||||
observer.observe(document, { childList: true, subtree: true })
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
# Favicon Documentation
|
||||
|
||||
This file documents the list of changes made to the Chromium iOS Favicon implementation as it isn't compatible with Brave-iOS.
|
||||
|
||||
This file documents the list of changes made to the Chromium iOS Favicon
|
||||
implementation as it isn't compatible with Brave-iOS.
|
||||
|
||||
# Changes
|
||||
|
||||
[BraveIOSWebFaviconDriver.h](https://github.com/brave/brave-core/blob/master/ios/browser/favicon/brave_ios_web_favicon_driver.h): Modification of `WebFaviconDriver` to get rid of `web::WebState` and instead use `ProfileIOS` as iOS cannot use `web::WebState` since we do not use `CRWWebView` in our Swift code. This means we got rid of all the `WebStateObserver` and `UserData` as well, and instead store the class with the `ProfileIOS`. This is done via the `CreateForBrowserState` and `FromBrowserState` functions.
|
||||
[BraveIOSWebFaviconDriver.h](https://github.com/brave/brave-core/blob/master/ios/browser/favicon/brave_ios_web_favicon_driver.h):
|
||||
Modification of `WebFaviconDriver` to get rid of `web::WebState` and instead use
|
||||
`ProfileIOS` as iOS cannot use `web::WebState` since we do not use `CRWWebView`
|
||||
in our Swift code. This means we got rid of all the `WebStateObserver` and
|
||||
`UserData` as well, and instead store the class with the `ProfileIOS`. This is
|
||||
done via the `CreateForBrowserState` and `FromBrowserState` functions.
|
||||
|
||||
[BraveIOSWebFaviconDriver.mm](https://github.com/brave/brave-core/blob/master/ios/browser/favicon/brave_ios_web_favicon_driver.mm)
|
||||
|
||||
The below code was added in order to setup a navigation stack for the Swift iOS WebView when navigation has just begun.
|
||||
The below code was added in order to setup a navigation stack for the Swift iOS
|
||||
WebView when navigation has just begun.
|
||||
|
||||
```c++
|
||||
void BraveIOSWebFaviconDriver::DidStartNavigation(
|
||||
ProfileIOS* profile,
|
||||
@@ -22,17 +29,22 @@ void BraveIOSWebFaviconDriver::DidStartNavigation(
|
||||
}
|
||||
```
|
||||
|
||||
When navigation is complete, we call the below function to begin fetching the `Favicon` for the `URL` that was navigated to:
|
||||
When navigation is complete, we call the below function to begin fetching the
|
||||
`Favicon` for the `URL` that was navigated to:
|
||||
|
||||
```c++
|
||||
void BraveIOSWebFaviconDriver::DidFinishNavigation(
|
||||
ProfileIOS* profile,
|
||||
const GURL& page_url) {
|
||||
web::NavigationItemImpl* item = !items.empty() ? items.back().get() : nullptr;
|
||||
DCHECK(item);
|
||||
|
||||
|
||||
// Fetch the fav-icon
|
||||
FetchFavicon(item->GetURL(), /*IsSameDocument*/ **false**);
|
||||
}
|
||||
```
|
||||
|
||||
This all emulates the `web::WebState::DidStartNavigation` and `web::WebState::DidFinishNavigation` functions in a much simpler way, that would be compatible with Brave-iOS until the day we switch over to using Chromium's `CRWWebView`.
|
||||
This all emulates the `web::WebState::DidStartNavigation` and
|
||||
`web::WebState::DidFinishNavigation` functions in a much simpler way, that would
|
||||
be compatible with Brave-iOS until the day we switch over to using Chromium's
|
||||
`CRWWebView`.
|
||||
|
||||
@@ -7,5 +7,5 @@ Object.defineProperty(navigator, 'globalPrivacyControl', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: (window as any).gCrWebPlaceholderGPCEnabled
|
||||
value: (window as any).gCrWebPlaceholderGPCEnabled,
|
||||
})
|
||||
|
||||
@@ -3,55 +3,59 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {sendWebKitMessageWithReply} from '//ios/web/public/js_messaging/resources/utils.js';
|
||||
import { sendWebKitMessageWithReply } from '//ios/web/public/js_messaging/resources/utils.js'
|
||||
|
||||
let timesToCheck = 20;
|
||||
let intervalId = 0;
|
||||
let timesToCheck = 20
|
||||
let intervalId = 0
|
||||
|
||||
function checkForAmp(): void {
|
||||
const htmlElm = document.documentElement;
|
||||
const headElm = document.head;
|
||||
const htmlElm = document.documentElement
|
||||
const headElm = document.head
|
||||
|
||||
if (!headElm && !htmlElm) {
|
||||
if (--timesToCheck === 0) {
|
||||
window.clearInterval(intervalId);
|
||||
window.clearInterval(intervalId)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!htmlElm.hasAttribute('amp') && !htmlElm.hasAttribute('⚡')) {
|
||||
window.clearInterval(intervalId);
|
||||
return;
|
||||
window.clearInterval(intervalId)
|
||||
return
|
||||
}
|
||||
|
||||
const canonicalLinkElm =
|
||||
document.querySelector('head > link[rel="canonical"][href^="http"]');
|
||||
const canonicalLinkElm = document.querySelector(
|
||||
'head > link[rel="canonical"][href^="http"]',
|
||||
)
|
||||
if (!canonicalLinkElm) {
|
||||
if (--timesToCheck === 0) {
|
||||
window.clearInterval(intervalId);
|
||||
window.clearInterval(intervalId)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const destUrl = new URL(canonicalLinkElm.getAttribute('href')!);
|
||||
window.clearInterval(intervalId);
|
||||
const destUrl = new URL(canonicalLinkElm.getAttribute('href')!)
|
||||
window.clearInterval(intervalId)
|
||||
|
||||
if (window.location.href === destUrl.href ||
|
||||
!(destUrl.protocol === 'http:' || destUrl.protocol === 'https:')) {
|
||||
return;
|
||||
if (
|
||||
window.location.href === destUrl.href
|
||||
|| !(destUrl.protocol === 'http:' || destUrl.protocol === 'https:')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
sendWebKitMessageWithReply('DeAmpMessageHandler', {destURL: destUrl.href})
|
||||
.then((shouldRedirect: boolean) => {
|
||||
if (shouldRedirect) {
|
||||
window.location.replace(destUrl.href);
|
||||
}
|
||||
});
|
||||
sendWebKitMessageWithReply('DeAmpMessageHandler', {
|
||||
destURL: destUrl.href,
|
||||
}).then((shouldRedirect: boolean) => {
|
||||
if (shouldRedirect) {
|
||||
window.location.replace(destUrl.href)
|
||||
}
|
||||
})
|
||||
} catch (_) {
|
||||
window.clearInterval(intervalId);
|
||||
window.clearInterval(intervalId)
|
||||
}
|
||||
}
|
||||
|
||||
intervalId = window.setInterval(checkForAmp, 250);
|
||||
checkForAmp();
|
||||
intervalId = window.setInterval(checkForAmp, 250)
|
||||
checkForAmp()
|
||||
|
||||
@@ -3,37 +3,39 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {CrWebApi, gCrWeb}
|
||||
from '//ios/web/public/js_messaging/resources/gcrweb.js';
|
||||
import {sendWebKitMessage}
|
||||
from '//ios/web/public/js_messaging/resources/utils.js';
|
||||
import {
|
||||
CrWebApi,
|
||||
gCrWeb,
|
||||
} from '//ios/web/public/js_messaging/resources/gcrweb.js'
|
||||
import { sendWebKitMessage } from '//ios/web/public/js_messaging/resources/utils.js'
|
||||
|
||||
function download(url: string): void {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.onreadystatechange = function() {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.responseType = 'arraybuffer'
|
||||
xhr.onreadystatechange = function () {
|
||||
if (this.readyState !== XMLHttpRequest.DONE) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (this.status === 200) {
|
||||
const byteArray = new Uint8Array(this.response as ArrayBuffer);
|
||||
const binaryString =
|
||||
Array.from(byteArray).map(b => String.fromCharCode(b)).join('');
|
||||
const byteArray = new Uint8Array(this.response as ArrayBuffer)
|
||||
const binaryString = Array.from(byteArray)
|
||||
.map((b) => String.fromCharCode(b))
|
||||
.join('')
|
||||
sendWebKitMessage('DocumentFetchMessageHandler', {
|
||||
statusCode: this.status,
|
||||
base64Data: window.btoa(binaryString),
|
||||
});
|
||||
})
|
||||
} else {
|
||||
sendWebKitMessage('DocumentFetchMessageHandler', {
|
||||
statusCode: this.status,
|
||||
base64Data: '',
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
xhr.open('GET', url, true);
|
||||
xhr.send(null);
|
||||
}
|
||||
xhr.open('GET', url, true)
|
||||
xhr.send(null)
|
||||
}
|
||||
|
||||
const documentFetchApi = new CrWebApi('documentFetch');
|
||||
documentFetchApi.addFunction('download', download);
|
||||
gCrWeb.registerApi(documentFetchApi);
|
||||
const documentFetchApi = new CrWebApi('documentFetch')
|
||||
documentFetchApi.addFunction('download', download)
|
||||
gCrWeb.registerApi(documentFetchApi)
|
||||
|
||||
@@ -3,32 +3,37 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {CrWebApi, gCrWeb} from '//ios/web/public/js_messaging/resources/gcrweb.js';
|
||||
import {
|
||||
CrWebApi,
|
||||
gCrWeb,
|
||||
} from '//ios/web/public/js_messaging/resources/gcrweb.js'
|
||||
|
||||
function pasteIntoActiveElement(contents: string) {
|
||||
let element: Element | null = document.activeElement;
|
||||
let element: Element | null = document.activeElement
|
||||
while (element?.tagName === 'IFRAME') {
|
||||
// If the element is an iframe, recurse into it to find the active element
|
||||
element = (element as HTMLIFrameElement).contentDocument?.activeElement ?? null;
|
||||
element =
|
||||
(element as HTMLIFrameElement).contentDocument?.activeElement ?? null
|
||||
}
|
||||
if (element === null) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (!(element.tagName === 'INPUT' || element.tagName === 'TEXTAREA')) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const inputElement = element as HTMLInputElement | HTMLTextAreaElement;
|
||||
const start = inputElement.selectionStart ?? 0;
|
||||
const inputElement = element as HTMLInputElement | HTMLTextAreaElement
|
||||
const start = inputElement.selectionStart ?? 0
|
||||
// Paste into expected position, replacing contents if any are selected
|
||||
inputElement.value =
|
||||
inputElement.value.slice(0, start) + contents +
|
||||
inputElement.value.slice(inputElement.selectionEnd ?? start);
|
||||
inputElement.value.slice(0, start)
|
||||
+ contents
|
||||
+ inputElement.value.slice(inputElement.selectionEnd ?? start)
|
||||
// Reset caret position to expected position
|
||||
const newSelection = start + contents.length;
|
||||
inputElement.selectionStart = newSelection;
|
||||
inputElement.selectionEnd = newSelection;
|
||||
const newSelection = start + contents.length
|
||||
inputElement.selectionStart = newSelection
|
||||
inputElement.selectionEnd = newSelection
|
||||
}
|
||||
|
||||
const forcePasteApi = new CrWebApi('forcePaste');
|
||||
forcePasteApi.addFunction('pasteIntoActiveElement', pasteIntoActiveElement);
|
||||
gCrWeb.registerApi(forcePasteApi);
|
||||
const forcePasteApi = new CrWebApi('forcePaste')
|
||||
forcePasteApi.addFunction('pasteIntoActiveElement', pasteIntoActiveElement)
|
||||
gCrWeb.registerApi(forcePasteApi)
|
||||
|
||||
@@ -6,79 +6,86 @@
|
||||
import {
|
||||
sendWebKitMessage,
|
||||
sendWebKitMessageWithReply,
|
||||
} from '//ios/web/public/js_messaging/resources/utils.js';
|
||||
} from '//ios/web/public/js_messaging/resources/utils.js'
|
||||
|
||||
const HANDLER_NAME = 'LoginsMessageHandler';
|
||||
const HANDLER_NAME = 'LoginsMessageHandler'
|
||||
|
||||
const KEYCODE_ARROW_DOWN = 40;
|
||||
const KEYCODE_ARROW_DOWN = 40
|
||||
|
||||
let gEnabled = true;
|
||||
let gStoreWhenAutocompleteOff = true;
|
||||
let gAutofillForms = true;
|
||||
let gEnabled = true
|
||||
let gStoreWhenAutocompleteOff = true
|
||||
let gAutofillForms = true
|
||||
|
||||
interface LoginData {
|
||||
hostname: string;
|
||||
formSubmitURL: string;
|
||||
httpRealm: string;
|
||||
username: string;
|
||||
password: string;
|
||||
usernameField: string;
|
||||
passwordField: string;
|
||||
hostname: string
|
||||
formSubmitURL: string
|
||||
httpRealm: string
|
||||
username: string
|
||||
password: string
|
||||
usernameField: string
|
||||
passwordField: string
|
||||
}
|
||||
|
||||
function isUsernameFieldType(element: Element): boolean {
|
||||
if (!(element instanceof HTMLInputElement)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
const fieldType = element.hasAttribute('type') ?
|
||||
element.getAttribute('type')!.toLowerCase() :
|
||||
element.type;
|
||||
return fieldType === 'text' || fieldType === 'email' ||
|
||||
fieldType === 'url' || fieldType === 'tel' ||
|
||||
fieldType === 'number';
|
||||
const fieldType = element.hasAttribute('type')
|
||||
? element.getAttribute('type')!.toLowerCase()
|
||||
: element.type
|
||||
return (
|
||||
fieldType === 'text'
|
||||
|| fieldType === 'email'
|
||||
|| fieldType === 'url'
|
||||
|| fieldType === 'tel'
|
||||
|| fieldType === 'number'
|
||||
)
|
||||
}
|
||||
|
||||
function isAutocompleteDisabled(element: Element | null): boolean {
|
||||
if (element && element.hasAttribute('autocomplete') &&
|
||||
element.getAttribute('autocomplete')!.toLowerCase() === 'off') {
|
||||
return true;
|
||||
if (
|
||||
element
|
||||
&& element.hasAttribute('autocomplete')
|
||||
&& element.getAttribute('autocomplete')!.toLowerCase() === 'off'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
interface PasswordFieldEntry {
|
||||
index: number;
|
||||
element: HTMLInputElement;
|
||||
index: number
|
||||
element: HTMLInputElement
|
||||
}
|
||||
|
||||
function getPasswordFields(
|
||||
form: HTMLFormElement,
|
||||
skipEmpty: boolean): PasswordFieldEntry[] | null {
|
||||
const pwFields: PasswordFieldEntry[] = [];
|
||||
form: HTMLFormElement,
|
||||
skipEmpty: boolean,
|
||||
): PasswordFieldEntry[] | null {
|
||||
const pwFields: PasswordFieldEntry[] = []
|
||||
for (let i = 0; i < form.elements.length; i++) {
|
||||
const element = form.elements[i];
|
||||
if (!(element instanceof HTMLInputElement) ||
|
||||
element.type !== 'password') {
|
||||
continue;
|
||||
const element = form.elements[i]
|
||||
if (!(element instanceof HTMLInputElement) || element.type !== 'password') {
|
||||
continue
|
||||
}
|
||||
if (skipEmpty && !element.value) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
pwFields.push({index: i, element});
|
||||
pwFields.push({ index: i, element })
|
||||
}
|
||||
if (pwFields.length === 0 || pwFields.length > 3) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return pwFields;
|
||||
return pwFields
|
||||
}
|
||||
|
||||
function getFormFields(
|
||||
form: HTMLFormElement,
|
||||
isSubmission: boolean): [HTMLInputElement | null, HTMLInputElement | null,
|
||||
HTMLInputElement | null] {
|
||||
const pwFields = getPasswordFields(form, isSubmission);
|
||||
form: HTMLFormElement,
|
||||
isSubmission: boolean,
|
||||
): [HTMLInputElement | null, HTMLInputElement | null, HTMLInputElement | null] {
|
||||
const pwFields = getPasswordFields(form, isSubmission)
|
||||
if (!pwFields) {
|
||||
return [null, null, null];
|
||||
return [null, null, null]
|
||||
}
|
||||
|
||||
// getPasswordFields guarantees at least one entry; destructure for safe
|
||||
@@ -87,248 +94,260 @@ function getFormFields(
|
||||
PasswordFieldEntry,
|
||||
PasswordFieldEntry | undefined,
|
||||
PasswordFieldEntry | undefined,
|
||||
];
|
||||
]
|
||||
|
||||
let usernameField: HTMLInputElement | null = null;
|
||||
let usernameField: HTMLInputElement | null = null
|
||||
for (let i = pw0.index - 1; i >= 0; i--) {
|
||||
const element = form.elements[i];
|
||||
const element = form.elements[i]
|
||||
if (isUsernameFieldType(element as Element)) {
|
||||
usernameField = element as HTMLInputElement;
|
||||
break;
|
||||
usernameField = element as HTMLInputElement
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSubmission || pwFields.length === 1) {
|
||||
return [usernameField, pw0.element, null];
|
||||
return [usernameField, pw0.element, null]
|
||||
}
|
||||
|
||||
const val0 = pw0.element.value;
|
||||
const val1 = pw1!.element.value;
|
||||
const val2 = pw2 ? pw2.element.value : null;
|
||||
const val0 = pw0.element.value
|
||||
const val1 = pw1!.element.value
|
||||
const val2 = pw2 ? pw2.element.value : null
|
||||
|
||||
let oldPasswordField: HTMLInputElement | null;
|
||||
let newPasswordField: HTMLInputElement;
|
||||
let oldPasswordField: HTMLInputElement | null
|
||||
let newPasswordField: HTMLInputElement
|
||||
|
||||
if (pwFields.length === 3) {
|
||||
if (val0 === val1 && val1 === val2) {
|
||||
newPasswordField = pw0.element;
|
||||
oldPasswordField = null;
|
||||
newPasswordField = pw0.element
|
||||
oldPasswordField = null
|
||||
} else if (val0 === val1) {
|
||||
newPasswordField = pw0.element;
|
||||
oldPasswordField = pw2!.element;
|
||||
newPasswordField = pw0.element
|
||||
oldPasswordField = pw2!.element
|
||||
} else if (val1 === val2) {
|
||||
oldPasswordField = pw0.element;
|
||||
newPasswordField = pw2!.element;
|
||||
oldPasswordField = pw0.element
|
||||
newPasswordField = pw2!.element
|
||||
} else if (val0 === val2) {
|
||||
newPasswordField = pw0.element;
|
||||
oldPasswordField = pw1!.element;
|
||||
newPasswordField = pw0.element
|
||||
oldPasswordField = pw1!.element
|
||||
} else {
|
||||
return [null, null, null];
|
||||
return [null, null, null]
|
||||
}
|
||||
} else {
|
||||
if (val0 === val1) {
|
||||
newPasswordField = pw0.element;
|
||||
oldPasswordField = null;
|
||||
newPasswordField = pw0.element
|
||||
oldPasswordField = null
|
||||
} else {
|
||||
oldPasswordField = pw0.element;
|
||||
newPasswordField = pw1!.element;
|
||||
oldPasswordField = pw0.element
|
||||
newPasswordField = pw1!.element
|
||||
}
|
||||
}
|
||||
|
||||
return [usernameField, newPasswordField, oldPasswordField];
|
||||
return [usernameField, newPasswordField, oldPasswordField]
|
||||
}
|
||||
|
||||
function dispatchKeyboardEvent(
|
||||
element: HTMLElement, eventName: string, keyCode: number): void {
|
||||
element: HTMLElement,
|
||||
eventName: string,
|
||||
keyCode: number,
|
||||
): void {
|
||||
element.dispatchEvent(
|
||||
new KeyboardEvent(eventName, {bubbles: true, cancelable: true, keyCode}));
|
||||
new KeyboardEvent(eventName, { bubbles: true, cancelable: true, keyCode }),
|
||||
)
|
||||
}
|
||||
|
||||
function fillForm(
|
||||
form: HTMLFormElement,
|
||||
autofillForm: boolean,
|
||||
ignoreAutocomplete: boolean,
|
||||
clobberPassword: boolean,
|
||||
userTriggered: boolean,
|
||||
foundLogins: LoginData[]): [boolean, LoginData[]] {
|
||||
const [usernameField, passwordField] = getFormFields(form, false);
|
||||
form: HTMLFormElement,
|
||||
autofillForm: boolean,
|
||||
ignoreAutocomplete: boolean,
|
||||
clobberPassword: boolean,
|
||||
userTriggered: boolean,
|
||||
foundLogins: LoginData[],
|
||||
): [boolean, LoginData[]] {
|
||||
const [usernameField, passwordField] = getFormFields(form, false)
|
||||
|
||||
if (!passwordField) {
|
||||
return [false, foundLogins];
|
||||
return [false, foundLogins]
|
||||
}
|
||||
if (passwordField.disabled || passwordField.readOnly) {
|
||||
return [false, foundLogins];
|
||||
return [false, foundLogins]
|
||||
}
|
||||
|
||||
let maxUsernameLen = Number.MAX_VALUE;
|
||||
let maxPasswordLen = Number.MAX_VALUE;
|
||||
let maxUsernameLen = Number.MAX_VALUE
|
||||
let maxPasswordLen = Number.MAX_VALUE
|
||||
if (usernameField && usernameField.maxLength >= 0) {
|
||||
maxUsernameLen = usernameField.maxLength;
|
||||
maxUsernameLen = usernameField.maxLength
|
||||
}
|
||||
if (passwordField.maxLength >= 0) {
|
||||
maxPasswordLen = passwordField.maxLength;
|
||||
maxPasswordLen = passwordField.maxLength
|
||||
}
|
||||
|
||||
const logins = foundLogins.filter(
|
||||
l => l.username.length <= maxUsernameLen &&
|
||||
l.password.length <= maxPasswordLen);
|
||||
(l) =>
|
||||
l.username.length <= maxUsernameLen
|
||||
&& l.password.length <= maxPasswordLen,
|
||||
)
|
||||
|
||||
if (logins.length === 0) {
|
||||
return [false, foundLogins];
|
||||
return [false, foundLogins]
|
||||
}
|
||||
|
||||
// Don't clobber an existing password.
|
||||
if (passwordField.value && !clobberPassword) {
|
||||
return [false, foundLogins];
|
||||
return [false, foundLogins]
|
||||
}
|
||||
|
||||
let selectedLogin: LoginData | null = null;
|
||||
let selectedLogin: LoginData | null = null
|
||||
|
||||
if (usernameField &&
|
||||
(usernameField.value || usernameField.disabled ||
|
||||
usernameField.readOnly)) {
|
||||
const username = usernameField.value.toLowerCase();
|
||||
const matchingLogins =
|
||||
logins.filter(l => l.username.toLowerCase() === username);
|
||||
if (
|
||||
usernameField
|
||||
&& (usernameField.value || usernameField.disabled || usernameField.readOnly)
|
||||
) {
|
||||
const username = usernameField.value.toLowerCase()
|
||||
const matchingLogins = logins.filter(
|
||||
(l) => l.username.toLowerCase() === username,
|
||||
)
|
||||
if (matchingLogins.length) {
|
||||
for (const l of matchingLogins) {
|
||||
if (l.username === usernameField.value) {
|
||||
selectedLogin = l;
|
||||
break;
|
||||
selectedLogin = l
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!selectedLogin) {
|
||||
selectedLogin = matchingLogins[0] ?? null;
|
||||
selectedLogin = matchingLogins[0] ?? null
|
||||
}
|
||||
}
|
||||
} else if (logins.length === 1) {
|
||||
selectedLogin = logins[0] ?? null;
|
||||
selectedLogin = logins[0] ?? null
|
||||
} else {
|
||||
const matchingLogins = usernameField ?
|
||||
logins.filter(l => l.username) :
|
||||
logins.filter(l => !l.username);
|
||||
selectedLogin = matchingLogins[0] ?? null;
|
||||
const matchingLogins = usernameField
|
||||
? logins.filter((l) => l.username)
|
||||
: logins.filter((l) => !l.username)
|
||||
selectedLogin = matchingLogins[0] ?? null
|
||||
}
|
||||
|
||||
let isFormDisabled = false;
|
||||
if (!ignoreAutocomplete &&
|
||||
(isAutocompleteDisabled(form) ||
|
||||
isAutocompleteDisabled(usernameField) ||
|
||||
isAutocompleteDisabled(passwordField))) {
|
||||
isFormDisabled = true;
|
||||
let isFormDisabled = false
|
||||
if (
|
||||
!ignoreAutocomplete
|
||||
&& (isAutocompleteDisabled(form)
|
||||
|| isAutocompleteDisabled(usernameField)
|
||||
|| isAutocompleteDisabled(passwordField))
|
||||
) {
|
||||
isFormDisabled = true
|
||||
}
|
||||
|
||||
if (selectedLogin && autofillForm && !isFormDisabled) {
|
||||
if (usernameField) {
|
||||
const disabledOrReadOnly =
|
||||
usernameField.disabled || usernameField.readOnly;
|
||||
const userNameDiffers =
|
||||
selectedLogin.username !== usernameField.value;
|
||||
const userEnteredDifferentCase = userTriggered && userNameDiffers &&
|
||||
usernameField.value.toLowerCase() ===
|
||||
selectedLogin.username.toLowerCase();
|
||||
usernameField.disabled || usernameField.readOnly
|
||||
const userNameDiffers = selectedLogin.username !== usernameField.value
|
||||
const userEnteredDifferentCase =
|
||||
userTriggered
|
||||
&& userNameDiffers
|
||||
&& usernameField.value.toLowerCase()
|
||||
=== selectedLogin.username.toLowerCase()
|
||||
|
||||
if (!disabledOrReadOnly && !userEnteredDifferentCase &&
|
||||
userNameDiffers) {
|
||||
usernameField.value = selectedLogin.username;
|
||||
if (!disabledOrReadOnly && !userEnteredDifferentCase && userNameDiffers) {
|
||||
usernameField.value = selectedLogin.username
|
||||
if (document.activeElement !== usernameField) {
|
||||
usernameField.dispatchEvent(new Event('change'));
|
||||
usernameField.dispatchEvent(new Event('change'))
|
||||
}
|
||||
dispatchKeyboardEvent(usernameField, 'keydown', KEYCODE_ARROW_DOWN);
|
||||
dispatchKeyboardEvent(usernameField, 'keyup', KEYCODE_ARROW_DOWN);
|
||||
dispatchKeyboardEvent(usernameField, 'keydown', KEYCODE_ARROW_DOWN)
|
||||
dispatchKeyboardEvent(usernameField, 'keyup', KEYCODE_ARROW_DOWN)
|
||||
}
|
||||
}
|
||||
if (passwordField.value !== selectedLogin.password) {
|
||||
passwordField.value = selectedLogin.password;
|
||||
passwordField.value = selectedLogin.password
|
||||
if (document.activeElement !== passwordField) {
|
||||
passwordField.dispatchEvent(new Event('change'));
|
||||
passwordField.dispatchEvent(new Event('change'))
|
||||
}
|
||||
dispatchKeyboardEvent(passwordField, 'keydown', KEYCODE_ARROW_DOWN);
|
||||
dispatchKeyboardEvent(passwordField, 'keyup', KEYCODE_ARROW_DOWN);
|
||||
dispatchKeyboardEvent(passwordField, 'keydown', KEYCODE_ARROW_DOWN)
|
||||
dispatchKeyboardEvent(passwordField, 'keyup', KEYCODE_ARROW_DOWN)
|
||||
}
|
||||
return [true, foundLogins];
|
||||
return [true, foundLogins]
|
||||
}
|
||||
|
||||
return [false, foundLogins];
|
||||
return [false, foundLogins]
|
||||
}
|
||||
|
||||
async function asyncFindLogins(
|
||||
form: HTMLFormElement): Promise<LoginData[]> {
|
||||
const fields = getFormFields(form, false);
|
||||
async function asyncFindLogins(form: HTMLFormElement): Promise<LoginData[]> {
|
||||
const fields = getFormFields(form, false)
|
||||
if (!fields[0] || !fields[1]) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
|
||||
fields[0].addEventListener('blur', onBlur);
|
||||
fields[0].addEventListener('blur', onBlur)
|
||||
|
||||
const actionOrigin = form.action || form.baseURI;
|
||||
const actionOrigin = form.action || form.baseURI
|
||||
if (!actionOrigin) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const logins = await sendWebKitMessageWithReply(HANDLER_NAME, {
|
||||
type: 'request',
|
||||
actionOrigin,
|
||||
});
|
||||
return Array.isArray(logins) ? (logins as LoginData[]) : [];
|
||||
})
|
||||
return Array.isArray(logins) ? (logins as LoginData[]) : []
|
||||
} catch (_) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function onBlur(event: Event): Promise<void> {
|
||||
if (!gEnabled) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const acInputField = event.target as HTMLInputElement;
|
||||
const acInputField = event.target as HTMLInputElement
|
||||
if (!(acInputField.ownerDocument instanceof HTMLDocument)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (!isUsernameFieldType(acInputField)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const acForm = acInputField.form;
|
||||
const acForm = acInputField.form
|
||||
if (!acForm) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (!acInputField.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const [usernameField, passwordField] = getFormFields(acForm, false);
|
||||
const [usernameField, passwordField] = getFormFields(acForm, false)
|
||||
if (usernameField === acInputField && passwordField) {
|
||||
const logins = await asyncFindLogins(acForm);
|
||||
fillForm(acForm, true, true, true, true, logins);
|
||||
const logins = await asyncFindLogins(acForm)
|
||||
fillForm(acForm, true, true, true, true, logins)
|
||||
}
|
||||
}
|
||||
|
||||
function onFormSubmit(form: HTMLFormElement): void {
|
||||
if (!gEnabled) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const hostname = document.documentURI;
|
||||
const hostname = document.documentURI
|
||||
if (!hostname) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const formSubmitURL = form.action || form.baseURI;
|
||||
const fields = getFormFields(form, true);
|
||||
const [usernameField, newPasswordField, oldPasswordField] = fields;
|
||||
const formSubmitURL = form.action || form.baseURI
|
||||
const fields = getFormFields(form, true)
|
||||
const [usernameField, newPasswordField, oldPasswordField] = fields
|
||||
|
||||
if (!newPasswordField) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if ((isAutocompleteDisabled(form) ||
|
||||
isAutocompleteDisabled(usernameField) ||
|
||||
isAutocompleteDisabled(newPasswordField) ||
|
||||
isAutocompleteDisabled(oldPasswordField)) &&
|
||||
!gStoreWhenAutocompleteOff) {
|
||||
return;
|
||||
if (
|
||||
(isAutocompleteDisabled(form)
|
||||
|| isAutocompleteDisabled(usernameField)
|
||||
|| isAutocompleteDisabled(newPasswordField)
|
||||
|| isAutocompleteDisabled(oldPasswordField))
|
||||
&& !gStoreWhenAutocompleteOff
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
sendWebKitMessage(HANDLER_NAME, {
|
||||
@@ -341,14 +360,14 @@ function onFormSubmit(form: HTMLFormElement): void {
|
||||
passwordField: newPasswordField.name,
|
||||
formSubmitURL,
|
||||
}),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
async function findLogins(form: HTMLFormElement): Promise<void> {
|
||||
try {
|
||||
const logins = await asyncFindLogins(form);
|
||||
const logins = await asyncFindLogins(form)
|
||||
if (logins.length > 0) {
|
||||
fillForm(form, gAutofillForms, false, false, false, logins);
|
||||
fillForm(form, gAutofillForms, false, false, false, logins)
|
||||
}
|
||||
} catch (_) {
|
||||
// Eat errors to avoid leaking them to the page
|
||||
@@ -357,23 +376,23 @@ async function findLogins(form: HTMLFormElement): Promise<void> {
|
||||
|
||||
function findForms(nodes: NodeList): void {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
const node = nodes[i]
|
||||
if (!node) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if ((node as Element).nodeName === 'FORM') {
|
||||
findLogins(node as HTMLFormElement);
|
||||
findLogins(node as HTMLFormElement)
|
||||
} else if (node.hasChildNodes()) {
|
||||
findForms(node.childNodes);
|
||||
findForms(node.childNodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(mutations => {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
findForms(mutation.addedNodes);
|
||||
findForms(mutation.addedNodes)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
observer.observe(document.body, {
|
||||
@@ -381,60 +400,60 @@ window.addEventListener('load', () => {
|
||||
childList: true,
|
||||
characterData: false,
|
||||
subtree: true,
|
||||
});
|
||||
})
|
||||
for (let i = 0; i < document.forms.length; i++) {
|
||||
const form = document.forms[i];
|
||||
const form = document.forms[i]
|
||||
if (form) {
|
||||
findLogins(form);
|
||||
findLogins(form)
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
window.addEventListener('submit', event => {
|
||||
window.addEventListener('submit', (event) => {
|
||||
try {
|
||||
for (let i = 0; i < document.forms.length; i++) {
|
||||
const form = document.forms[i];
|
||||
const form = document.forms[i]
|
||||
if (form) {
|
||||
findLogins(form);
|
||||
findLogins(form)
|
||||
}
|
||||
}
|
||||
onFormSubmit(event.target as HTMLFormElement);
|
||||
onFormSubmit(event.target as HTMLFormElement)
|
||||
} catch (_) {
|
||||
// Eat errors to avoid leaking them to the page
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
window.addEventListener('pagehide', event => {
|
||||
window.addEventListener('pagehide', (event) => {
|
||||
if ((event as PageTransitionEvent).persisted) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const isSubmittedForm = (form: HTMLFormElement): boolean => {
|
||||
const fields = getFormFields(form, false);
|
||||
const fields = getFormFields(form, false)
|
||||
if (!fields[0] || !fields[1]) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
const actionOrigin = form.action || form.baseURI;
|
||||
const actionOrigin = form.action || form.baseURI
|
||||
if (!actionOrigin) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
if (field && (!field.value || field.value.length === 0)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
for (const form of document.forms) {
|
||||
if (isSubmittedForm(form)) {
|
||||
try {
|
||||
onFormSubmit(form);
|
||||
onFormSubmit(form)
|
||||
} catch (_) {
|
||||
// Eat errors to avoid leaking them to the page
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -5,101 +5,122 @@
|
||||
|
||||
function enable(): void {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(
|
||||
Document.prototype, 'visibilityState'
|
||||
)!;
|
||||
const visibilityStateGet = descriptor.get!;
|
||||
Document.prototype,
|
||||
'visibilityState',
|
||||
)!
|
||||
const visibilityStateGet = descriptor.get!
|
||||
|
||||
Object.defineProperty(Document.prototype, 'visibilityState', {
|
||||
enumerable: descriptor.enumerable,
|
||||
configurable: descriptor.configurable,
|
||||
get() {
|
||||
const result = visibilityStateGet.call(this);
|
||||
const result = visibilityStateGet.call(this)
|
||||
if (result !== 'visible') {
|
||||
return 'visible';
|
||||
return 'visible'
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
return result
|
||||
},
|
||||
})
|
||||
|
||||
const pauseControl = HTMLVideoElement.prototype.pause;
|
||||
HTMLVideoElement.prototype.pause = function(): void {
|
||||
(this as any).userHitPause = true;
|
||||
pauseControl.call(this);
|
||||
};
|
||||
const pauseControl = HTMLVideoElement.prototype.pause
|
||||
HTMLVideoElement.prototype.pause = function (): void {
|
||||
;(this as any).userHitPause = true
|
||||
pauseControl.call(this)
|
||||
}
|
||||
|
||||
const playControl = HTMLVideoElement.prototype.play;
|
||||
HTMLVideoElement.prototype.play = function(): Promise<void> {
|
||||
(this as any).userHitPause = false;
|
||||
return playControl.call(this);
|
||||
};
|
||||
const playControl = HTMLVideoElement.prototype.play
|
||||
HTMLVideoElement.prototype.play = function (): Promise<void> {
|
||||
;(this as any).userHitPause = false
|
||||
return playControl.call(this)
|
||||
}
|
||||
|
||||
function addListeners(element: HTMLVideoElement): void {
|
||||
if (!(element as any).pauseListener) {
|
||||
(element as any).pauseListener = true;
|
||||
(element as any).visibilityState = visibilityStateGet.call(document);
|
||||
;(element as any).pauseListener = true
|
||||
;(element as any).visibilityState = visibilityStateGet.call(document)
|
||||
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
(element as any).visibilityState = visibilityStateGet.call(document);
|
||||
}, false);
|
||||
document.addEventListener(
|
||||
'visibilitychange',
|
||||
function () {
|
||||
;(element as any).visibilityState = visibilityStateGet.call(document)
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
||||
element.addEventListener('pause', function() {
|
||||
if (!(element as any).userHitPause &&
|
||||
visibilityStateGet.call(document) === 'visible') {
|
||||
const onVisibilityChanged = () => {
|
||||
document.removeEventListener(
|
||||
'visibilitychange', onVisibilityChanged
|
||||
);
|
||||
if (visibilityStateGet.call(document) !== 'visible' &&
|
||||
!element.ended) {
|
||||
playControl.call(element);
|
||||
element.addEventListener(
|
||||
'pause',
|
||||
function () {
|
||||
if (
|
||||
!(element as any).userHitPause
|
||||
&& visibilityStateGet.call(document) === 'visible'
|
||||
) {
|
||||
const onVisibilityChanged = () => {
|
||||
document.removeEventListener(
|
||||
'visibilitychange',
|
||||
onVisibilityChanged,
|
||||
)
|
||||
if (
|
||||
visibilityStateGet.call(document) !== 'visible'
|
||||
&& !element.ended
|
||||
) {
|
||||
playControl.call(element)
|
||||
}
|
||||
}
|
||||
document.addEventListener('visibilitychange', onVisibilityChanged)
|
||||
setTimeout(function () {
|
||||
document.removeEventListener(
|
||||
'visibilitychange',
|
||||
onVisibilityChanged,
|
||||
)
|
||||
}, 2000)
|
||||
} else {
|
||||
if (
|
||||
!(element as any).userHitPause
|
||||
&& (element as any).visibilityState === 'visible'
|
||||
&& !element.ended
|
||||
) {
|
||||
playControl.call(element)
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', onVisibilityChanged);
|
||||
setTimeout(function() {
|
||||
document.removeEventListener(
|
||||
'visibilitychange', onVisibilityChanged
|
||||
);
|
||||
}, 2000);
|
||||
} else {
|
||||
if (!(element as any).userHitPause &&
|
||||
(element as any).visibilityState === 'visible' &&
|
||||
!element.ended) {
|
||||
playControl.call(element);
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
if (!(element as any).presentationModeListener) {
|
||||
(element as any).presentationModeListener = true;
|
||||
element.addEventListener('webkitpresentationmodechanged', function(e) {
|
||||
e.stopPropagation();
|
||||
}, true);
|
||||
;(element as any).presentationModeListener = true
|
||||
element.addEventListener(
|
||||
'webkitpresentationmodechanged',
|
||||
function (e) {
|
||||
e.stopPropagation()
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const queue: MutationRecord[] = [];
|
||||
const queue: MutationRecord[] = []
|
||||
function onMutation(): void {
|
||||
for (const mutation of queue) {
|
||||
mutation.addedNodes.forEach(function(node: Node) {
|
||||
mutation.addedNodes.forEach(function (node: Node) {
|
||||
if (node instanceof HTMLVideoElement) {
|
||||
addListeners(node);
|
||||
addListeners(node)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
queue.length = 0;
|
||||
queue.length = 0
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(function(mutations: MutationRecord[]) {
|
||||
const observer = new MutationObserver(function (mutations: MutationRecord[]) {
|
||||
if (!queue.length) {
|
||||
requestAnimationFrame(onMutation);
|
||||
requestAnimationFrame(onMutation)
|
||||
}
|
||||
queue.push(...mutations);
|
||||
});
|
||||
queue.push(...mutations)
|
||||
})
|
||||
|
||||
document.querySelectorAll('video').forEach(
|
||||
(v) => addListeners(v as HTMLVideoElement)
|
||||
);
|
||||
document
|
||||
.querySelectorAll('video')
|
||||
.forEach((v) => addListeners(v as HTMLVideoElement))
|
||||
|
||||
observer.observe(document, {
|
||||
childList: true,
|
||||
@@ -107,10 +128,10 @@ function enable(): void {
|
||||
characterData: false,
|
||||
subtree: true,
|
||||
attributeOldValue: false,
|
||||
characterDataOldValue: false
|
||||
});
|
||||
characterDataOldValue: false,
|
||||
})
|
||||
}
|
||||
|
||||
if ((window as any).gCrWebPlaceholderMediaBackgroundingEnabled) {
|
||||
enable();
|
||||
enable()
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ Object.defineProperty(navigator, 'brave', {
|
||||
configurable: true,
|
||||
writable: false,
|
||||
value: Object.freeze({
|
||||
isBrave: (): Promise<boolean> => Promise.resolve(true)
|
||||
})
|
||||
});
|
||||
isBrave: (): Promise<boolean> => Promise.resolve(true),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -3,28 +3,35 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {CrWebApi, gCrWeb} from '//ios/web/public/js_messaging/resources/gcrweb.js';
|
||||
import {
|
||||
CrWebApi,
|
||||
gCrWeb,
|
||||
} from '//ios/web/public/js_messaging/resources/gcrweb.js'
|
||||
|
||||
function getMetadata(): string {
|
||||
const searchEl = document.querySelector(
|
||||
'link[type="application/opensearchdescription+xml"]');
|
||||
const search = searchEl ? {
|
||||
title: (searchEl as HTMLLinkElement).title,
|
||||
href: (searchEl as HTMLLinkElement).href,
|
||||
} : null;
|
||||
'link[type="application/opensearchdescription+xml"]',
|
||||
)
|
||||
const search = searchEl
|
||||
? {
|
||||
title: (searchEl as HTMLLinkElement).title,
|
||||
href: (searchEl as HTMLLinkElement).href,
|
||||
}
|
||||
: null
|
||||
|
||||
const feedNodes = document.querySelectorAll(
|
||||
'link[type="application/rss+xml"], ' +
|
||||
'link[type="application/atom+xml"], ' +
|
||||
'link[rel="alternate"][type="application/json"]');
|
||||
const feeds = Array.from(feedNodes).map(link => ({
|
||||
'link[type="application/rss+xml"], '
|
||||
+ 'link[type="application/atom+xml"], '
|
||||
+ 'link[rel="alternate"][type="application/json"]',
|
||||
)
|
||||
const feeds = Array.from(feedNodes).map((link) => ({
|
||||
href: (link as HTMLLinkElement).href,
|
||||
title: (link as HTMLLinkElement).title,
|
||||
}));
|
||||
}))
|
||||
|
||||
return JSON.stringify({search, feeds});
|
||||
return JSON.stringify({ search, feeds })
|
||||
}
|
||||
|
||||
const pageMetadataApi = new CrWebApi('pageMetadata');
|
||||
pageMetadataApi.addFunction('getMetadata', getMetadata);
|
||||
gCrWeb.registerApi(pageMetadataApi);
|
||||
const pageMetadataApi = new CrWebApi('pageMetadata')
|
||||
pageMetadataApi.addFunction('getMetadata', getMetadata)
|
||||
gCrWeb.registerApi(pageMetadataApi)
|
||||
|
||||
@@ -3,226 +3,240 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {CrWebApi, gCrWeb} from
|
||||
'//ios/web/public/js_messaging/resources/gcrweb.js';
|
||||
import {
|
||||
CrWebApi,
|
||||
gCrWeb,
|
||||
} from '//ios/web/public/js_messaging/resources/gcrweb.js'
|
||||
|
||||
// Readability.js and Readability-readerable.js are injected as separate
|
||||
// FeatureScripts before this one and expose their APIs as globals.
|
||||
declare class Readability<T = string> {
|
||||
constructor(
|
||||
document: Document,
|
||||
options?: {
|
||||
debug?: boolean; maxElemsToParse?: number; nbTopCandidates?: number;
|
||||
charThreshold?: number;
|
||||
classesToPreserve?: string[];
|
||||
keepClasses?: boolean;
|
||||
serializer?: (node: Node) => T;
|
||||
disableJSONLD?: boolean;
|
||||
allowedVideoRegex?: RegExp;
|
||||
});
|
||||
parse(): null|{
|
||||
title: string | null | undefined;
|
||||
content: T | null | undefined;
|
||||
textContent: string | null | undefined;
|
||||
length: number | null | undefined;
|
||||
excerpt: string | null | undefined;
|
||||
byline: string | null | undefined;
|
||||
dir: string | null | undefined;
|
||||
siteName: string | null | undefined;
|
||||
lang: string | null | undefined;
|
||||
publishedTime: string | null | undefined;
|
||||
};
|
||||
document: Document,
|
||||
options?: {
|
||||
debug?: boolean
|
||||
maxElemsToParse?: number
|
||||
nbTopCandidates?: number
|
||||
charThreshold?: number
|
||||
classesToPreserve?: string[]
|
||||
keepClasses?: boolean
|
||||
serializer?: (node: Node) => T
|
||||
disableJSONLD?: boolean
|
||||
allowedVideoRegex?: RegExp
|
||||
},
|
||||
)
|
||||
parse(): null | {
|
||||
title: string | null | undefined
|
||||
content: T | null | undefined
|
||||
textContent: string | null | undefined
|
||||
length: number | null | undefined
|
||||
excerpt: string | null | undefined
|
||||
byline: string | null | undefined
|
||||
dir: string | null | undefined
|
||||
siteName: string | null | undefined
|
||||
lang: string | null | undefined
|
||||
publishedTime: string | null | undefined
|
||||
}
|
||||
}
|
||||
|
||||
declare function isProbablyReaderable(
|
||||
document: Document,
|
||||
options?: {
|
||||
minContentLength?: number;
|
||||
minScore?: number;
|
||||
visibilityChecker?: (node: Node) => boolean;
|
||||
}): boolean;
|
||||
document: Document,
|
||||
options?: {
|
||||
minContentLength?: number
|
||||
minScore?: number
|
||||
visibilityChecker?: (node: Node) => boolean
|
||||
},
|
||||
): boolean
|
||||
|
||||
type ReadabilityParseResult = ReturnType<Readability['parse']>;
|
||||
type ReadabilityParseResult = ReturnType<Readability['parse']>
|
||||
|
||||
interface ExtendedParseResult extends NonNullable<ReadabilityParseResult> {
|
||||
cspMetaTags?: string[];
|
||||
documentLanguage?: string;
|
||||
cspMetaTags?: string[]
|
||||
documentLanguage?: string
|
||||
}
|
||||
|
||||
const kReaderModeURL = /^internal:\/\/local\/reader-mode/;
|
||||
const kReaderModeURL = /^internal:\/\/local\/reader-mode/
|
||||
|
||||
const kBlockImagesSelector =
|
||||
'.content p > img:only-child, ' +
|
||||
'.content p > a:only-child > img:only-child, ' +
|
||||
'.content .wp-caption img, ' +
|
||||
'.content figure img';
|
||||
'.content p > img:only-child, '
|
||||
+ '.content p > a:only-child > img:only-child, '
|
||||
+ '.content .wp-caption img, '
|
||||
+ '.content figure img'
|
||||
|
||||
let readabilityResult: ExtendedParseResult|null = null;
|
||||
let readabilityResult: ExtendedParseResult | null = null
|
||||
|
||||
interface ReaderModeStyle {
|
||||
theme?: string;
|
||||
fontSize?: number;
|
||||
fontType?: string;
|
||||
theme?: string
|
||||
fontSize?: number
|
||||
fontType?: string
|
||||
}
|
||||
|
||||
let currentStyle: ReaderModeStyle|null = null;
|
||||
let currentStyle: ReaderModeStyle | null = null
|
||||
|
||||
function checkReadability(): string|null {
|
||||
function checkReadability(): string | null {
|
||||
if (!isProbablyReaderable(document)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Short circuit if Readability already ran (back/forward cache hit).
|
||||
if (readabilityResult && readabilityResult['content']) {
|
||||
return JSON.stringify(readabilityResult);
|
||||
return JSON.stringify(readabilityResult)
|
||||
}
|
||||
|
||||
// Serialize then re-parse to avoid cloneNode crashes (bug 1128774).
|
||||
const docStr = new XMLSerializer().serializeToString(document);
|
||||
const docStr = new XMLSerializer().serializeToString(document)
|
||||
|
||||
// Skip documents with <frameset> to avoid WKWebView crashes (bug 1489543).
|
||||
if (docStr.includes('<frameset ')) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const doc = new DOMParser().parseFromString(docStr, 'text/html');
|
||||
const result = new Readability(doc, {}).parse();
|
||||
const doc = new DOMParser().parseFromString(docStr, 'text/html')
|
||||
const result = new Readability(doc, {}).parse()
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
readabilityResult = result as ExtendedParseResult;
|
||||
readabilityResult = result as ExtendedParseResult
|
||||
|
||||
// Extract only the CSP policy value; the native side constructs the
|
||||
// meta tag to prevent raw HTML injection via a crafted tag.
|
||||
const cspMetaTags = document.querySelectorAll(
|
||||
'meta[http-equiv="Content-Security-Policy"]');
|
||||
'meta[http-equiv="Content-Security-Policy"]',
|
||||
)
|
||||
if (cspMetaTags.length > 0) {
|
||||
readabilityResult.cspMetaTags =
|
||||
Array.from(cspMetaTags)
|
||||
.map((e) => e.getAttribute('content') ?? '')
|
||||
.filter(Boolean);
|
||||
readabilityResult.cspMetaTags = Array.from(cspMetaTags)
|
||||
.map((e) => e.getAttribute('content') ?? '')
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
const documentLanguage =
|
||||
document.documentElement.lang ||
|
||||
document.querySelector('meta[http-equiv="Content-Language"]')
|
||||
?.getAttribute('content') ||
|
||||
null;
|
||||
document.documentElement.lang
|
||||
|| document
|
||||
.querySelector('meta[http-equiv="Content-Language"]')
|
||||
?.getAttribute('content')
|
||||
|| null
|
||||
if (documentLanguage) {
|
||||
readabilityResult.documentLanguage = documentLanguage;
|
||||
readabilityResult.documentLanguage = documentLanguage
|
||||
}
|
||||
|
||||
return JSON.stringify(readabilityResult);
|
||||
return JSON.stringify(readabilityResult)
|
||||
}
|
||||
|
||||
function setStyle(style: ReaderModeStyle): void {
|
||||
if (currentStyle?.theme) {
|
||||
document.body.classList.remove(currentStyle.theme);
|
||||
document.body.classList.remove(currentStyle.theme)
|
||||
}
|
||||
if (style?.theme) {
|
||||
document.body.classList.add(style.theme);
|
||||
document.body.classList.add(style.theme)
|
||||
}
|
||||
|
||||
if (currentStyle?.fontSize !== undefined) {
|
||||
document.body.classList.remove('font-size' + currentStyle.fontSize);
|
||||
document.body.classList.remove('font-size' + currentStyle.fontSize)
|
||||
}
|
||||
if (style?.fontSize !== undefined) {
|
||||
document.body.classList.add('font-size' + style.fontSize);
|
||||
document.body.classList.add('font-size' + style.fontSize)
|
||||
}
|
||||
|
||||
if (currentStyle?.fontType) {
|
||||
document.body.classList.remove(currentStyle.fontType);
|
||||
document.body.classList.remove(currentStyle.fontType)
|
||||
}
|
||||
if (style?.fontType) {
|
||||
document.body.classList.add(style.fontType);
|
||||
document.body.classList.add(style.fontType)
|
||||
}
|
||||
|
||||
currentStyle = style;
|
||||
currentStyle = style
|
||||
}
|
||||
|
||||
function updateImageMargins(): void {
|
||||
const contentElement = document.getElementById('reader-content');
|
||||
const contentElement = document.getElementById('reader-content')
|
||||
if (!contentElement) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const windowWidth = window.innerWidth;
|
||||
const contentWidth = contentElement.offsetWidth;
|
||||
const maxWidthStyle = windowWidth + 'px !important';
|
||||
const windowWidth = window.innerWidth
|
||||
const contentWidth = contentElement.offsetWidth
|
||||
const maxWidthStyle = windowWidth + 'px !important'
|
||||
|
||||
type ImgWithOriginalWidth =
|
||||
HTMLImageElement&{_originalWidth?: number};
|
||||
type ImgWithOriginalWidth = HTMLImageElement & { _originalWidth?: number }
|
||||
|
||||
const setImageMargins = (img: ImgWithOriginalWidth): void => {
|
||||
if (!img._originalWidth) {
|
||||
img._originalWidth = img.offsetWidth;
|
||||
img._originalWidth = img.offsetWidth
|
||||
}
|
||||
|
||||
let imgWidth = img._originalWidth;
|
||||
let imgWidth = img._originalWidth
|
||||
if (imgWidth < contentWidth && imgWidth > windowWidth * 0.55) {
|
||||
imgWidth = windowWidth;
|
||||
imgWidth = windowWidth
|
||||
}
|
||||
|
||||
const sideMargin =
|
||||
Math.max(
|
||||
(contentWidth - windowWidth) / 2,
|
||||
(contentWidth - imgWidth) / 2);
|
||||
const sideMargin = Math.max(
|
||||
(contentWidth - windowWidth) / 2,
|
||||
(contentWidth - imgWidth) / 2,
|
||||
)
|
||||
|
||||
img.style.cssText =
|
||||
'max-width: ' + maxWidthStyle + ';' +
|
||||
'width: ' + imgWidth + 'px !important;' +
|
||||
'margin-left: ' + sideMargin + 'px !important;' +
|
||||
'margin-right: ' + sideMargin + 'px !important;';
|
||||
};
|
||||
'max-width: '
|
||||
+ maxWidthStyle
|
||||
+ ';'
|
||||
+ 'width: '
|
||||
+ imgWidth
|
||||
+ 'px !important;'
|
||||
+ 'margin-left: '
|
||||
+ sideMargin
|
||||
+ 'px !important;'
|
||||
+ 'margin-right: '
|
||||
+ sideMargin
|
||||
+ 'px !important;'
|
||||
}
|
||||
|
||||
const imgs =
|
||||
document.querySelectorAll<ImgWithOriginalWidth>(kBlockImagesSelector);
|
||||
document.querySelectorAll<ImgWithOriginalWidth>(kBlockImagesSelector)
|
||||
for (let i = imgs.length - 1; i >= 0; i--) {
|
||||
const img = imgs[i];
|
||||
const img = imgs[i]
|
||||
if (!img) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (img.width > 0) {
|
||||
setImageMargins(img);
|
||||
setImageMargins(img)
|
||||
} else {
|
||||
img.onload = () => setImageMargins(img);
|
||||
img.onload = () => setImageMargins(img)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showContent(): void {
|
||||
const messageElement = document.getElementById('reader-message');
|
||||
const messageElement = document.getElementById('reader-message')
|
||||
if (messageElement) {
|
||||
messageElement.style.display = 'none';
|
||||
messageElement.style.display = 'none'
|
||||
}
|
||||
const headerElement = document.getElementById('reader-header');
|
||||
const headerElement = document.getElementById('reader-header')
|
||||
if (headerElement) {
|
||||
headerElement.style.display = 'block';
|
||||
headerElement.style.display = 'block'
|
||||
}
|
||||
const contentElement = document.getElementById('reader-content');
|
||||
const contentElement = document.getElementById('reader-content')
|
||||
if (contentElement) {
|
||||
contentElement.style.display = 'block';
|
||||
contentElement.style.display = 'block'
|
||||
}
|
||||
}
|
||||
|
||||
function configureReader(): void {
|
||||
const styleAttr = document.body.getAttribute('data-readerStyle');
|
||||
const styleAttr = document.body.getAttribute('data-readerStyle')
|
||||
if (styleAttr) {
|
||||
setStyle(JSON.parse(styleAttr) as ReaderModeStyle);
|
||||
setStyle(JSON.parse(styleAttr) as ReaderModeStyle)
|
||||
}
|
||||
showContent();
|
||||
updateImageMargins();
|
||||
showContent()
|
||||
updateImageMargins()
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
if (document.location.href.match(kReaderModeURL)) {
|
||||
configureReader();
|
||||
configureReader()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const readerModeApi = new CrWebApi('readerMode');
|
||||
readerModeApi.addFunction('checkReadability', checkReadability);
|
||||
readerModeApi.addFunction('setStyle', setStyle);
|
||||
gCrWeb.registerApi(readerModeApi);
|
||||
const readerModeApi = new CrWebApi('readerMode')
|
||||
readerModeApi.addFunction('checkReadability', checkReadability)
|
||||
readerModeApi.addFunction('setStyle', setStyle)
|
||||
gCrWeb.registerApi(readerModeApi)
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {CrWebApi, gCrWeb} from
|
||||
'//ios/web/public/js_messaging/resources/gcrweb.js';
|
||||
import {resetQuality} from
|
||||
'//brave/ios/browser/youtube/resources/yt_video_quality_utils.js';
|
||||
import {
|
||||
CrWebApi,
|
||||
gCrWeb,
|
||||
} from '//ios/web/public/js_messaging/resources/gcrweb.js'
|
||||
import { resetQuality } from '//brave/ios/browser/youtube/resources/yt_video_quality_utils.js'
|
||||
|
||||
const youtubeQualityApi = new CrWebApi('youtubeQuality');
|
||||
youtubeQualityApi.addFunction('resetQuality', resetQuality);
|
||||
gCrWeb.registerApi(youtubeQualityApi);
|
||||
const youtubeQualityApi = new CrWebApi('youtubeQuality')
|
||||
youtubeQualityApi.addFunction('resetQuality', resetQuality)
|
||||
gCrWeb.registerApi(youtubeQualityApi)
|
||||
|
||||
@@ -3,28 +3,27 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {functionAsListener} from '//ios/web/public/js_messaging/resources/utils.js';
|
||||
import {requestVideoQualityPreference} from
|
||||
'//brave/ios/browser/youtube/resources/yt_video_quality_utils.js';
|
||||
import { functionAsListener } from '//ios/web/public/js_messaging/resources/utils.js'
|
||||
import { requestVideoQualityPreference } from '//brave/ios/browser/youtube/resources/yt_video_quality_utils.js'
|
||||
|
||||
const allowedOrigins = [
|
||||
'https://youtube.com',
|
||||
'https://www.youtube.com',
|
||||
'https://m.youtube.com',
|
||||
];
|
||||
]
|
||||
|
||||
if (allowedOrigins.includes(window.location.origin)) {
|
||||
const listener = functionAsListener(requestVideoQualityPreference);
|
||||
const listener = functionAsListener(requestVideoQualityPreference)
|
||||
|
||||
// Apply quality when the video element finishes loading data (covers the
|
||||
// initial load as well as mid-session source changes).
|
||||
document.addEventListener('loadeddata', listener, {capture: true});
|
||||
document.addEventListener('loadeddata', listener, { capture: true })
|
||||
|
||||
// YouTube uses History API (pushState) for in-app navigation. The page
|
||||
// dispatches 'yt-navigate-finish' on document when each navigation
|
||||
// settles.
|
||||
document.addEventListener('yt-navigate-finish', listener);
|
||||
document.addEventListener('yt-navigate-finish', listener)
|
||||
|
||||
// Attempt to apply the highest quality immediately
|
||||
requestVideoQualityPreference();
|
||||
requestVideoQualityPreference()
|
||||
}
|
||||
|
||||
@@ -3,39 +3,44 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {sendWebKitMessageWithReply} from
|
||||
'//ios/web/public/js_messaging/resources/utils.js';
|
||||
import { sendWebKitMessageWithReply } from '//ios/web/public/js_messaging/resources/utils.js'
|
||||
|
||||
function findPlayer(): any {
|
||||
return document.getElementById('movie_player') ||
|
||||
document.querySelector('.html5-video-player');
|
||||
return (
|
||||
document.getElementById('movie_player')
|
||||
|| document.querySelector('.html5-video-player')
|
||||
)
|
||||
}
|
||||
|
||||
export function applyHighestQuality(): void {
|
||||
const player = findPlayer();
|
||||
if (!player || typeof player.getAvailableQualityLevels === 'undefined' ||
|
||||
!player.setPlaybackQualityRange) {
|
||||
return;
|
||||
const player = findPlayer()
|
||||
if (
|
||||
!player
|
||||
|| typeof player.getAvailableQualityLevels === 'undefined'
|
||||
|| !player.setPlaybackQualityRange
|
||||
) {
|
||||
return
|
||||
}
|
||||
const qualities: string[] = player.getAvailableQualityLevels();
|
||||
const qualities: string[] = player.getAvailableQualityLevels()
|
||||
if (qualities && qualities.length > 0) {
|
||||
player.setPlaybackQualityRange(qualities[0], qualities[0]);
|
||||
player.setPlaybackQualityRange(qualities[0], qualities[0])
|
||||
}
|
||||
}
|
||||
|
||||
export function resetQuality(): void {
|
||||
const player = findPlayer();
|
||||
const player = findPlayer()
|
||||
if (!player || !player.setPlaybackQualityRange) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
player.setPlaybackQualityRange('auto', 'auto');
|
||||
player.setPlaybackQualityRange('auto', 'auto')
|
||||
}
|
||||
|
||||
export function requestVideoQualityPreference() {
|
||||
sendWebKitMessageWithReply('YouTubeQualityMessageHandler', {})
|
||||
.then((shouldApplyHighestQuality: boolean) => {
|
||||
sendWebKitMessageWithReply('YouTubeQualityMessageHandler', {}).then(
|
||||
(shouldApplyHighestQuality: boolean) => {
|
||||
if (shouldApplyHighestQuality) {
|
||||
applyHighestQuality();
|
||||
applyHighestQuality()
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,18 +3,21 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {CrWebApi, gCrWeb} from '//ios/web/public/js_messaging/resources/gcrweb.js';
|
||||
import {sendTokenizedWebKitMessage} from "//brave/ios/web/js_messaging/resources/utils.js";
|
||||
import {sendWebKitMessage} from "//ios/web/public/js_messaging/resources/utils.js";
|
||||
import {
|
||||
CrWebApi,
|
||||
gCrWeb,
|
||||
} from '//ios/web/public/js_messaging/resources/gcrweb.js'
|
||||
import { sendTokenizedWebKitMessage } from '//brave/ios/web/js_messaging/resources/utils.js'
|
||||
import { sendWebKitMessage } from '//ios/web/public/js_messaging/resources/utils.js'
|
||||
|
||||
function send() {
|
||||
sendTokenizedWebKitMessage('ScriptHandlerName', {'key': 'value'});
|
||||
sendTokenizedWebKitMessage('ScriptHandlerName', { 'key': 'value' })
|
||||
}
|
||||
function sendInvalid() {
|
||||
sendWebKitMessage('ScriptHandlerName', {'key': 'value'});
|
||||
sendWebKitMessage('ScriptHandlerName', { 'key': 'value' })
|
||||
}
|
||||
|
||||
const testApi = new CrWebApi('message_handler_token_tests');
|
||||
testApi.addFunction('sendTokenizedWebKitMessage', send);
|
||||
testApi.addFunction('sendWebKitMessage', sendInvalid);
|
||||
gCrWeb.registerApi(testApi);
|
||||
const testApi = new CrWebApi('message_handler_token_tests')
|
||||
testApi.addFunction('sendTokenizedWebKitMessage', send)
|
||||
testApi.addFunction('sendWebKitMessage', sendInvalid)
|
||||
gCrWeb.registerApi(testApi)
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import {messageHandlerName} from "//brave/ios/web/js_messaging/resources/utils.js";
|
||||
import {CrWebApi, gCrWeb} from '//ios/web/public/js_messaging/resources/gcrweb.js';
|
||||
import {sendWebKitMessage} from "//ios/web/public/js_messaging/resources/utils.js";
|
||||
import { messageHandlerName } from '//brave/ios/web/js_messaging/resources/utils.js'
|
||||
import {
|
||||
CrWebApi,
|
||||
gCrWeb,
|
||||
} from '//ios/web/public/js_messaging/resources/gcrweb.js'
|
||||
import { sendWebKitMessage } from '//ios/web/public/js_messaging/resources/utils.js'
|
||||
|
||||
function send() {
|
||||
sendWebKitMessage(messageHandlerName, {});
|
||||
sendWebKitMessage(messageHandlerName, {})
|
||||
}
|
||||
|
||||
const testApi = new CrWebApi('randomized_message_handler_tests');
|
||||
testApi.addFunction('sendWebKitMessage', send);
|
||||
gCrWeb.registerApi(testApi);
|
||||
const testApi = new CrWebApi('randomized_message_handler_tests')
|
||||
testApi.addFunction('sendWebKitMessage', send)
|
||||
gCrWeb.registerApi(testApi)
|
||||
|
||||
@@ -5,105 +5,126 @@
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
webkit: any;
|
||||
webkit: any
|
||||
}
|
||||
}
|
||||
|
||||
class SafeBuiltins {
|
||||
readonly $Object: typeof Object = this.secureCopy(Object);
|
||||
readonly $Function: typeof Function = this.secureCopy(Function);
|
||||
readonly $Array: typeof Array = this.secureCopy(Array);
|
||||
readonly $ = function(value: any): any { return value; }
|
||||
readonly $Object: typeof Object = this.secureCopy(Object)
|
||||
readonly $Function: typeof Function = this.secureCopy(Function)
|
||||
readonly $Array: typeof Array = this.secureCopy(Array)
|
||||
readonly $ = function (value: any): any {
|
||||
return value
|
||||
}
|
||||
|
||||
// Sends a message to a script message handler in the browser
|
||||
readonly sendWebKitMessage: (handlerName: string, message: object|string) => void;
|
||||
readonly sendWebKitMessage: (
|
||||
handlerName: string,
|
||||
message: object | string,
|
||||
) => void
|
||||
|
||||
// Sends a message to a script message handler in the browser and returns a
|
||||
// Promise that resolves when the browser replies
|
||||
readonly sendWebKitMessageWithReply: (handlerName: string, message: object|string) => Promise<any>;
|
||||
readonly sendWebKitMessageWithReply: (
|
||||
handlerName: string,
|
||||
message: object | string,
|
||||
) => Promise<any>
|
||||
|
||||
// Send a message expecting synchronously by using window.prompt and returns
|
||||
// the reply from the browser.
|
||||
readonly sendWebKitMessageSynchronously: (handlerName: string, message: object|string) => any|null;
|
||||
readonly sendWebKitMessageSynchronously: (
|
||||
handlerName: string,
|
||||
message: object | string,
|
||||
) => any | null
|
||||
|
||||
constructor() {
|
||||
// Setup private refs to capture in safe builtin functions
|
||||
const webkitMessageHandlers = window.webkit.messageHandlers;
|
||||
const windowPrompt = window.prompt.bind(window);
|
||||
const jsonStringify = JSON.stringify.bind(JSON);
|
||||
const jsonParse = JSON.parse.bind(JSON);
|
||||
const webkitMessageHandlers = window.webkit.messageHandlers
|
||||
const windowPrompt = window.prompt.bind(window)
|
||||
const jsonStringify = JSON.stringify.bind(JSON)
|
||||
const jsonParse = JSON.parse.bind(JSON)
|
||||
|
||||
this.sendWebKitMessage = (handlerName, message) => {
|
||||
webkitMessageHandlers[handlerName].postMessage(message);
|
||||
webkitMessageHandlers[handlerName].postMessage(message)
|
||||
}
|
||||
|
||||
this.sendWebKitMessageWithReply = (handlerName, message) => {
|
||||
return webkitMessageHandlers[handlerName].postMessage(message);
|
||||
return webkitMessageHandlers[handlerName].postMessage(message)
|
||||
}
|
||||
|
||||
this.sendWebKitMessageSynchronously = (handlerName, message) => {
|
||||
const response = windowPrompt(jsonStringify({
|
||||
handler: handlerName,
|
||||
message: message
|
||||
}));
|
||||
const response = windowPrompt(
|
||||
jsonStringify({
|
||||
handler: handlerName,
|
||||
message: message,
|
||||
}),
|
||||
)
|
||||
if (!response) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return jsonParse(response)
|
||||
} catch {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Freeze all the safe builtins and any function we export
|
||||
for (const value of [this.$Object, this.$Function, this.$Array, this.$,
|
||||
this.sendWebKitMessage, this.sendWebKitMessageWithReply]) {
|
||||
this.deepFreeze(value);
|
||||
for (const value of [
|
||||
this.$Object,
|
||||
this.$Function,
|
||||
this.$Array,
|
||||
this.$,
|
||||
this.sendWebKitMessage,
|
||||
this.sendWebKitMessageWithReply,
|
||||
]) {
|
||||
this.deepFreeze(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Freeze an object and its prototype
|
||||
private deepFreeze(value: any) {
|
||||
if (!value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this.$Object.freeze(value);
|
||||
const prototype = (value as any).prototype;
|
||||
this.$Object.freeze(value)
|
||||
const prototype = (value as any).prototype
|
||||
if (prototype) {
|
||||
this.$Object.freeze(prototype);
|
||||
this.$Object.freeze(prototype)
|
||||
}
|
||||
}
|
||||
|
||||
// Copies an object's signature to an object with no prototype to prevent
|
||||
// prototype polution attacks
|
||||
private secureCopy(value: any): any {
|
||||
let prototypeProperties = Object.create(null, value.prototype ?
|
||||
Object.getOwnPropertyDescriptors(value.prototype) : {});
|
||||
delete prototypeProperties['prototype'];
|
||||
let prototypeProperties = Object.create(
|
||||
null,
|
||||
value.prototype ? Object.getOwnPropertyDescriptors(value.prototype) : {},
|
||||
)
|
||||
delete prototypeProperties['prototype']
|
||||
|
||||
let properties = Object.assign(
|
||||
Object.create(null),
|
||||
Object.getOwnPropertyDescriptors(value),
|
||||
value.prototype ? Object.getOwnPropertyDescriptors(value.prototype) :
|
||||
{}
|
||||
);
|
||||
value.prototype ? Object.getOwnPropertyDescriptors(value.prototype) : {},
|
||||
)
|
||||
|
||||
// Do not copy the prototype.
|
||||
delete properties['prototype'];
|
||||
delete properties['prototype']
|
||||
|
||||
return new Proxy(Object.create(null, properties), {
|
||||
get(target, property, _receiver) {
|
||||
if (property == 'prototype') {
|
||||
return prototypeProperties;
|
||||
return prototypeProperties
|
||||
}
|
||||
return target[property];
|
||||
}
|
||||
});
|
||||
return target[property]
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type SafeBuiltinsType = Window&(typeof globalThis)&{readonly __gSafeBuiltins: SafeBuiltins};
|
||||
type SafeBuiltinsType = Window
|
||||
& typeof globalThis & { readonly __gSafeBuiltins: SafeBuiltins }
|
||||
|
||||
// Initializes window's `__gSafeBuiltins` property.
|
||||
if (!(window as SafeBuiltinsType).__gSafeBuiltins) {
|
||||
@@ -111,14 +132,15 @@ if (!(window as SafeBuiltinsType).__gSafeBuiltins) {
|
||||
value: Object.freeze(new SafeBuiltins()),
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: false
|
||||
});
|
||||
enumerable: false,
|
||||
})
|
||||
}
|
||||
|
||||
export const gSafeBuiltins: SafeBuiltins = (window as SafeBuiltinsType).__gSafeBuiltins;
|
||||
export const gSafeBuiltins: SafeBuiltins = (window as SafeBuiltinsType)
|
||||
.__gSafeBuiltins
|
||||
|
||||
// Export some shortcuts to items in SafeBuiltins
|
||||
export const $Object = gSafeBuiltins.$Object;
|
||||
export const $Function = gSafeBuiltins.$Function;
|
||||
export const $Array = gSafeBuiltins.$Array;
|
||||
export const $ = gSafeBuiltins.$;
|
||||
export const $Object = gSafeBuiltins.$Object
|
||||
export const $Function = gSafeBuiltins.$Function
|
||||
export const $Array = gSafeBuiltins.$Array
|
||||
export const $ = gSafeBuiltins.$
|
||||
|
||||
@@ -3,33 +3,36 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import { gSafeBuiltins }
|
||||
from "//brave/ios/web/js_messaging/resources/safe_builtins.js";
|
||||
import { gSafeBuiltins } from '//brave/ios/web/js_messaging/resources/safe_builtins.js'
|
||||
|
||||
// The message handler name to use with sendWebKitMessage when your JavaScript
|
||||
// feature supports randomized message handler names.
|
||||
export const messageHandlerName: string = 'gCrWebPlaceholderMessageHandlerName';
|
||||
export const messageHandlerName: string = 'gCrWebPlaceholderMessageHandlerName'
|
||||
|
||||
// A token to be used for validating communication with the browser
|
||||
const messageHandlerToken: string = 'gCrWebPlaceholderMessageHandlerToken';
|
||||
const messageHandlerToken: string = 'gCrWebPlaceholderMessageHandlerToken'
|
||||
|
||||
// Posts `message` to the webkit message handler specified by `handlerName` and
|
||||
// embeds a token for the browser to validate
|
||||
export function sendTokenizedWebKitMessage(
|
||||
handlerName: string, message: object|string) {
|
||||
handlerName: string,
|
||||
message: object | string,
|
||||
) {
|
||||
gSafeBuiltins.sendWebKitMessage(handlerName, {
|
||||
token: messageHandlerToken,
|
||||
message: message
|
||||
});
|
||||
message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// Posts `message` to the webkit message handler specified by `handlerName` and
|
||||
// embeds a token for the browser to validate and waits for a reply
|
||||
export function sendTokenizedWebKitMessageWithReply(
|
||||
handlerName: string, message: object|string): Promise<any> {
|
||||
handlerName: string,
|
||||
message: object | string,
|
||||
): Promise<any> {
|
||||
return gSafeBuiltins.sendWebKitMessageWithReply(handlerName, {
|
||||
token: messageHandlerToken,
|
||||
message: message
|
||||
message: message,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,9 +40,11 @@ export function sendTokenizedWebKitMessageWithReply(
|
||||
// validation and waits for the reply synchronously. If the message isnt valid
|
||||
// JSON or the response cannot be parsed as JSON then this returns null
|
||||
export function sendTokenizedWebKitMessageSynchronously(
|
||||
handlerName: string, message: object|string): any|null {
|
||||
handlerName: string,
|
||||
message: object | string,
|
||||
): any | null {
|
||||
return gSafeBuiltins.sendWebKitMessageSynchronously(handlerName, {
|
||||
token: messageHandlerToken,
|
||||
message: message
|
||||
message: message,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user