Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 5 additions & 14 deletions .github/workflows/eslint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,18 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v7
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
with:
fetch-depth: 2
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}

# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}

- name: Install ESLint
run: |
npm install @microsoft/eslint-formatter-sarif@3.1.0
npm ci --ignore-scripts
- name: Install dependencies
run: npm ci --ignore-scripts

- name: Run ESLint
run: SARIF_ESLINT_IGNORE_SUPPRESSED=true npm run test_eslint_ci
run: npm run test_eslint_ci
continue-on-error: true

- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: eslint-results.sarif
wait-for-processing: true
wait-for-processing: true
3 changes: 2 additions & 1 deletion conf/tsconfig.content_scripts.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"target": "ES2022",
"target": "ES2025",
"lib": ["ES2025", "DOM", "DOM.Iterable", "DOM.AsyncIterable", "WebWorker.ImportScripts", "ScriptHost", "ESNext.TypedArrays"],
"module": "es2022",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
Expand Down
2 changes: 1 addition & 1 deletion conf/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["es6", "dom"],
"lib": ["es2022", "dom", "es2024.promise", "es2024.string", "es2025.regexp", "esnext.typedarrays"],
"alwaysStrict": true,
"noImplicitAny": true,
"noFallthroughCasesInSwitch": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export namespace GmailRes {

export class GmailParser {
public static findHeader = (apiGmailMsgObj: GmailRes.GmailMsg | GmailRes.GmailMsg$payload, headerName: string) => {
const node: GmailRes.GmailMsg$payload = apiGmailMsgObj.hasOwnProperty('payload')
const node: GmailRes.GmailMsg$payload = Object.hasOwn(apiGmailMsgObj, 'payload')
? (apiGmailMsgObj as GmailRes.GmailMsg).payload! // eslint-disable-line @typescript-eslint/no-non-null-assertion
: (apiGmailMsgObj as GmailRes.GmailMsg$payload);
if (typeof node.headers !== 'undefined') {
Expand Down Expand Up @@ -177,7 +177,7 @@ export class GmailParser {
}
}
}
if ('body' in msgOrPayloadOrPart && msgOrPayloadOrPart.body?.hasOwnProperty('attachmentId')) {
if ('body' in msgOrPayloadOrPart && msgOrPayloadOrPart.body && Object.hasOwn(msgOrPayloadOrPart.body, 'attachmentId')) {
const payload = msgOrPayloadOrPart as GmailRes.GmailMsg$payload$part;
const treatAs = Attachment.treatAsForPgpEncryptedAttachments(payload.mimeType, pgpEncryptedIndex);
const inline = (GmailParser.findHeader(payload, 'content-disposition') || '').toLowerCase().startsWith('inline');
Expand Down
19 changes: 9 additions & 10 deletions extension/js/common/api/email-provider/gmail/gmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,17 +220,16 @@ export class Gmail extends EmailProviderApi implements EmailProviderInterface {
reject(new Error('Chunk response could not be parsed'));
return;
}
for (let i = 0; parsedJsonDataField && i < 50; i++) {
try {
resolve(Buf.fromBase64UrlStr(parsedJsonDataField));
return;
} catch {
// the chunk of data may have been cut at an inconvenient index
// shave off up to 50 trailing characters until it can be decoded
parsedJsonDataField = parsedJsonDataField.slice(0, -1);
}
const base64Remainder = parsedJsonDataField.length % 4;
if (base64Remainder === 1 || (base64Remainder === 3 && parsedJsonDataField.endsWith('='))) {
parsedJsonDataField = parsedJsonDataField.slice(0, -1);
}
try {
resolve(Buf.fromBase64UrlStr(parsedJsonDataField));
return;
} catch {
reject(new Error('Chunk response could not be decoded'));
}
reject(new Error('Chunk response could not be decoded'));
}
};
GoogleOAuth.googleApiAuthHeader(this.acctEmail)
Expand Down
2 changes: 1 addition & 1 deletion extension/js/common/api/shared/api-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export class ApiErr {
if (e instanceof AjaxErr && e.resDetails === internalType) {
return true;
}
if ((e as StandardError).hasOwnProperty('internal') && !!(e as StandardError).message && (e as StandardError).internal === internalType) {
if (Object.hasOwn(e, 'internal') && !!(e as StandardError).message && (e as StandardError).internal === internalType) {
return true;
}
if ((e as StandardErrRes).error && typeof (e as StandardErrRes).error === 'object' && (e as StandardErrRes).error.internal === internalType) {
Expand Down
3 changes: 1 addition & 2 deletions extension/js/common/api/shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,7 @@ export class Api {
}

public static randomFortyHexChars(): string {
const bytes = Array.from(secureRandomBytes(20));
return bytes.map(b => ('0' + (b & 0xff).toString(16)).slice(-2)).join('');
return secureRandomBytes(20).toHex();
}

public static isRecipientHeaderNameType(value: string): value is 'to' | 'cc' | 'bcc' {
Expand Down
7 changes: 3 additions & 4 deletions extension/js/common/browser/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@ export class CommonHandlers {

public static sendRequestAndHandleAsyncResult = async <T>(send: (requestUid: string) => void): Promise<T> => {
const requestUid = Str.sloppyRandom(10);
const p = new Promise((resolve: (value: T) => void) => {
CommonHandlers.respondMap.set(requestUid, resolve);
});
const { promise, resolve } = Promise.withResolvers<T>();
CommonHandlers.respondMap.set(requestUid, resolve);
send(requestUid);
return await p;
return await promise;
};

// for specific types
Expand Down
73 changes: 10 additions & 63 deletions extension/js/common/core/buf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@

'use strict';

import { base64decode, base64encode } from '../platform/util.js';

export class Buf extends Uint8Array {
public static concat = (arrays: Uint8Array[]): Buf => {
const result = new Uint8Array(arrays.reduce((totalLen, arr) => totalLen + arr.length, 0));
const result = new Buf(arrays.reduce((totalLen, arr) => totalLen + arr.length, 0));
let offset = 0;
for (const array of arrays) {
result.set(array, offset);
offset += array.length;
}
return Buf.fromUint8(result);
return result;
};

public static with = (input: Uint8Array | Buf | string): Buf => {
Expand All @@ -39,57 +37,13 @@ export class Buf extends Uint8Array {
};

public static fromUtfStr = (utfStr: string): Buf => {
// adapted from https://github.com/feross/buffer/blob/master/index.js see https://github.com/feross/buffer/blob/master/LICENSE (MIT as of Jan 2018)
let codePoint;
const length = utfStr.length;
let leadSurrogate: number | undefined;
const bytes: number[] = [];
for (let i = 0; i < length; ++i) {
codePoint = utfStr.charCodeAt(i);
if (codePoint > 0xd7ff && codePoint < 0xe000) {
// is surrogate component
if (!leadSurrogate) {
// last char was a lead
if (codePoint > 0xdbff) {
// no lead yet
bytes.push(0xef, 0xbf, 0xbd); // unexpected trail
continue;
} else if (i + 1 === length) {
bytes.push(0xef, 0xbf, 0xbd);
continue;
}
leadSurrogate = codePoint; // valid lead
continue;
}
if (codePoint < 0xdc00) {
// 2 leads in a row
bytes.push(0xef, 0xbf, 0xbd);
leadSurrogate = codePoint;
continue;
}
codePoint = (((leadSurrogate - 0xd800) << 10) | (codePoint - 0xdc00)) + 0x10000; // valid surrogate pair
} else if (leadSurrogate) {
bytes.push(0xef, 0xbf, 0xbd);
}
leadSurrogate = undefined;
// encode utf8
if (codePoint < 0x80) {
bytes.push(codePoint);
} else if (codePoint < 0x800) {
bytes.push((codePoint >> 0x6) | 0xc0, (codePoint & 0x3f) | 0x80);
} else if (codePoint < 0x10000) {
bytes.push((codePoint >> 0xc) | 0xe0, ((codePoint >> 0x6) & 0x3f) | 0x80, (codePoint & 0x3f) | 0x80);
} else if (codePoint < 0x110000) {
bytes.push((codePoint >> 0x12) | 0xf0, ((codePoint >> 0xc) & 0x3f) | 0x80, ((codePoint >> 0x6) & 0x3f) | 0x80, (codePoint & 0x3f) | 0x80);
} else {
throw new Error('Invalid code point');
}
}
return new Buf(bytes);
const bytes = new TextEncoder().encode(utfStr);
return new Buf(bytes.buffer, bytes.byteOffset, bytes.byteLength);
};

public static fromBase64Str = (b64str: string): Buf => {
return Buf.fromRawBytesStr(base64decode(b64str));
const bytes = Uint8Array.fromBase64(b64str, { lastChunkHandling: 'loose' });
return new Buf(bytes.buffer, bytes.byteOffset, bytes.byteLength);
};

public static fromBase64UrlStr = (b64UrlStr: string): Buf => {
Expand Down Expand Up @@ -186,22 +140,15 @@ export class Buf extends Uint8Array {
};

public toHexStr = (uppercaseFlag = true): string => {
const chars: string[] = [];
for (const v of this.values()) {
let char = ('00' + v.toString(16)).slice(-2);
if (uppercaseFlag) {
char = char.toUpperCase();
}
chars.push(char);
}
return chars.join('');
const hex = this.toHex();
return uppercaseFlag ? hex.toUpperCase() : hex;
};

public toBase64Str = (): string => {
return base64encode(this.toRawBytesStr());
return this.toBase64();
};

public toBase64UrlStr = (): string => {
return this.toBase64Str().replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
return this.toBase64({ alphabet: 'base64url', omitPadding: true });
};
}
41 changes: 10 additions & 31 deletions extension/js/common/core/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

'use strict';

import { base64decode, base64encode } from '../platform/util.js';
import { Xss } from '../platform/xss.js';
import { Buf } from './buf.js';
import { MOCK_PORT } from './const.js';
Expand Down Expand Up @@ -254,7 +253,7 @@ export class Str {
};

public static regexEscape = (toBeUsedInRegex: string) => {
return toBeUsedInRegex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return RegExp.escape(toBeUsedInRegex);
};

public static escapeTextAsRenderableHtml = (text: string) => {
Expand Down Expand Up @@ -328,29 +327,21 @@ export class Str {
};

private static base64urlUtfEncode = (str: string) => {
// https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
if (typeof str === 'undefined') {
return str;
}
return base64encode(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode(parseInt(String(p1), 16))))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
if (!str.isWellFormed()) {
throw new URIError('URI malformed');
}
return new TextEncoder().encode(str).toBase64({ alphabet: 'base64url', omitPadding: true });
};

private static base64urlUtfDecode = (str: string) => {
// https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
if (typeof str === 'undefined') {
return str;
}

return decodeURIComponent(
Array.prototype.map
.call(base64decode(str.replace(/-/g, '+').replace(/_/g, '/')), (c: string) => {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join('')
);
const bytes = Uint8Array.fromBase64(str.replace(/-/g, '+').replace(/_/g, '/'), { lastChunkHandling: 'loose' });
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
};
}

Expand All @@ -368,15 +359,7 @@ export class DateUtility {

export class Value {
public static arr = {
unique: <T>(array: T[]): T[] => {
const unique: T[] = [];
for (const v of array) {
if (!unique.includes(v)) {
unique.push(v);
}
}
return unique;
},
unique: <T>(array: T[]): T[] => [...new Set(array)],
withoutKey: <T>(array: T[], i: number) => array.splice(0, i).concat(array.splice(i + 1, array.length)),
withoutVal: <T>(array: T[], withoutVal: T) => {
const result: T[] = [];
Expand Down Expand Up @@ -560,12 +543,8 @@ export const stringTuple = <T extends string[]>(...data: T): T => {
};

export const checkValidURL = (url: string): boolean => {
try {
const parsedUrl = new URL(url);
return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:';
} catch {
return false;
}
const parsedUrl = URL.parse(url);
return parsedUrl?.protocol === 'http:' || parsedUrl?.protocol === 'https:';
};

/**
Expand Down
14 changes: 6 additions & 8 deletions extension/js/common/core/crypto/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,9 @@ export class KeyUtil {

// Get the base64 after the '='
const checksumLine = dataCandidates[checksumIndex].slice(1);
let providedBytes: string;
let providedBytes: Uint8Array;
try {
providedBytes = atob(checksumLine);
providedBytes = Uint8Array.fromBase64(checksumLine, { lastChunkHandling: 'loose' });
} catch {
continue; // Not valid base64, skip
}
Expand All @@ -538,14 +538,14 @@ export class KeyUtil {
if (providedBytes.length !== 3) {
continue;
}
const providedCRC = (providedBytes.charCodeAt(0) << 16) | (providedBytes.charCodeAt(1) << 8) | providedBytes.charCodeAt(2);
const providedCRC = (providedBytes[0] << 16) | (providedBytes[1] << 8) | providedBytes[2];

// Decode all lines before the checksum line
const dataLines = dataCandidates.slice(0, checksumIndex);
const decodedChunks: string[] = [];
const decodedChunks: Uint8Array[] = [];
for (const line of dataLines) {
try {
decodedChunks.push(atob(line));
decodedChunks.push(Uint8Array.fromBase64(line, { lastChunkHandling: 'loose' }));
} catch {
// skip lines that aren't valid base64
}
Expand All @@ -556,9 +556,7 @@ export class KeyUtil {
}

// Join all decoded base64 data and calculate its CRC
const rawData = decodedChunks.join('');
// eslint-disable-next-line @typescript-eslint/no-misused-spread
const dataBytes = new Uint8Array([...rawData].map(c => c.charCodeAt(0)));
const dataBytes = Buf.concat(decodedChunks);
if (KeyUtil.crc24(dataBytes) !== providedCRC) {
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion extension/js/common/core/crypto/pgp/msg-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export class MsgUtil {

for (const term of disallowTerms) {
// Escape term for regex
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escapedTerm = Str.regexEscape(term);
// Use regex to ensure the term appears as a separate token
// (^|\W) ensures the term is at start or preceded by non-word char
// (\W|$) ensures the term is followed by non-word char or end
Expand Down
4 changes: 2 additions & 2 deletions extension/js/common/core/crypto/pgp/pgp-armor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ export class PgpArmor {

// Build regex patterns from headers, escaping special regex characters
const patterns = pgpHeaders.map(header => {
const escapedBegin = header.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escapedBegin = Str.regexEscape(header.begin);
// header.end can be string or RegExp, handle both cases
const escapedEnd = typeof header.end === 'string' ? header.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : header.end.source; // If it's already a RegExp, use its source
const escapedEnd = typeof header.end === 'string' ? Str.regexEscape(header.end) : header.end.source; // If it's already a RegExp, use its source
return `(${escapedBegin}[\\s\\S]*?${escapedEnd})`;
});

Expand Down
Loading
Loading