[iOS] Opt-in to prettier for iOS typescript/markdown (#36998)

This commit is contained in:
Kyle Hickinson
2026-06-04 20:53:52 -04:00
committed by GitHub
parent d12b8b1d55
commit 7726cd762a
23 changed files with 824 additions and 660 deletions
+2 -1
View File
@@ -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 })
}
+19 -7
View File
@@ -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,
})
+31 -27
View File
@@ -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)
+207 -188
View File
@@ -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)
+68 -46
View File
@@ -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.$
+16 -11
View File
@@ -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,
})
}