From dd6e15fb5c2001c3712f0569e6ce57315dc1e436 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Fri, 30 Apr 2021 11:04:10 -0400 Subject: [PATCH] Fix injecting multiple css/js files (fix #458) (#1162) * Add ability to inject multiple css/js files * API doc: Move misplaced macOS shortcuts doc (PR #1158) When I added this documentation originally, I guess I placed it in the wrong location. * README: use quotes in example, to divert users from shell globbing pitfalls Follow-up of https://github.com/nativefier/nativefier/issues/1159#issuecomment-827184112 * Support opening URLs passed as arg to Nativefied application (fix #405) (PR #1154) Co-authored-by: Ronan Jouchet * macOS: Fix crash when using --tray (fix #527), and invisible icon (fix #942, fix #668) (#1156) This fixes: 1. A startup crash on macOS when using the `--tray` option; see #527. ![image](https://user-images.githubusercontent.com/22625791/115987741-99544600-a5b6-11eb-866a-dadb5640eecb.png) 2. Invisible tray icon on macOS; see #942 and #668. ![image](https://user-images.githubusercontent.com/22625791/115988276-24364000-a5b9-11eb-80c3-561a8a646754.png) Co-authored-by: Ronan Jouchet * API.md / --widevine: document signing apps to make some sites like HBO Max & Udemy work (fix #1147) * Prompt to confirm when page is attempting to prevent unload (#1163) Should alleviate part of the issue in #1151 * Add an option to upgrade an existing app (fix #1131) (PR #1138) This adds a `--upgrade` option to upgrade-in-place an old app, re-using its options it can. Should help fix #1131 Co-authored-by: Ronan Jouchet * Bump to Electron 12.0.5 with Chrome 89.0.4389.128 * Add newly discovered Google internal login page (#1167) * Fix Widevine by properly listening to widevine-... events, and update docs (fix #1153) (PR #1164) As documented in #1153, for Widevine support to work properly, we need to listen for the Widevine ready event, and as well for certain sites the app must be signed. This PR adds the events, and as well adds better documentation on the signing limitation. This may also help resolve #1147 * Improve suffix creation + tests * API: clarif in existing doc by the way * Typo Co-authored-by: Ronan Jouchet Co-authored-by: Ben Curtis Co-authored-by: Fabian Wolf <22625791+fabiwlf@users.noreply.github.com> --- app/src/helpers/helpers.ts | 22 ++++++++++++++----- app/src/preload.ts | 20 ++++++++++++++--- docs/api.md | 2 +- src/build/prepareElectronApp.ts | 39 ++++++++++++--------------------- src/helpers/helpers.test.ts | 24 +++++++++++++++++++- src/helpers/helpers.ts | 8 +++++++ 6 files changed, 80 insertions(+), 35 deletions(-) diff --git a/app/src/helpers/helpers.ts b/app/src/helpers/helpers.ts index 60952ae..43cd55f 100644 --- a/app/src/helpers/helpers.ts +++ b/app/src/helpers/helpers.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { BrowserWindow } from 'electron'; -const INJECT_CSS_PATH = path.join(__dirname, '..', 'inject/inject.css'); +export const INJECT_DIR = path.join(__dirname, '..', 'inject'); export function isOSX(): boolean { return os.platform() === 'darwin'; @@ -80,17 +80,29 @@ export function linkIsInternal( export function shouldInjectCss(): boolean { try { - fs.accessSync(INJECT_CSS_PATH); - return true; + return fs.existsSync(INJECT_DIR); } catch (e) { return false; } } export function getCssToInject(): string { - return fs.readFileSync(INJECT_CSS_PATH).toString(); + let cssToInject = ''; + const cssFiles = fs + .readdirSync(INJECT_DIR, { withFileTypes: true }) + .filter( + (injectFile) => injectFile.isFile() && injectFile.name.endsWith('.css'), + ) + .map((cssFileStat) => + path.resolve(path.join(INJECT_DIR, cssFileStat.name)), + ); + for (const cssFile of cssFiles) { + console.log(`Injecting CSS file: ${cssFile}`); + const cssFileData = fs.readFileSync(cssFile); + cssToInject += `/* ${cssFile} */\n\n ${cssFileData}\n\n`; + } + return cssToInject; } - /** * Helper to print debug messages from the main process in the browser window */ diff --git a/app/src/preload.ts b/app/src/preload.ts index f1832aa..6fdaa04 100644 --- a/app/src/preload.ts +++ b/app/src/preload.ts @@ -12,7 +12,8 @@ import * as path from 'path'; import { ipcRenderer } from 'electron'; -const INJECT_JS_PATH = path.join(__dirname, '..', 'inject/inject.js'); +export const INJECT_DIR = path.join(__dirname, '..', 'inject'); + /** * Patches window.Notification to: * - set a callback on a new Notification @@ -38,12 +39,25 @@ function setNotificationCallback(createCallback, clickCallback) { } function injectScripts() { - const needToInject = fs.existsSync(INJECT_JS_PATH); + const needToInject = fs.existsSync(INJECT_DIR); if (!needToInject) { return; } // Dynamically require scripts - require(INJECT_JS_PATH); + try { + const jsFiles = fs + .readdirSync(INJECT_DIR, { withFileTypes: true }) + .filter( + (injectFile) => injectFile.isFile() && injectFile.name.endsWith('.js'), + ) + .map((jsFileStat) => path.join('..', 'inject', jsFileStat.name)); + for (const jsFile of jsFiles) { + console.log(`Injecting JS file: ${jsFile}`); + require(jsFile); + } + } catch (error) { + console.warn('Error encoutered injecting JS files', error); + } } function notifyNotificationCreate(title, opt) { diff --git a/docs/api.md b/docs/api.md index 53eebbb..0198747 100644 --- a/docs/api.md +++ b/docs/api.md @@ -514,7 +514,7 @@ Forces the maximum disk space to be used by the disk cache. Value is given in by --inject ``` -Allows you to inject a javascript or css file. This command can be run multiple times to inject the files. +Allows you to inject javascript or css files. This command can be repeated multiple times to inject multiple files. _Note:_ The javascript file is loaded _after_ `DOMContentLoaded`, so you can assume the DOM is complete & available. diff --git a/src/build/prepareElectronApp.ts b/src/build/prepareElectronApp.ts index 84cf90e..99a8783 100644 --- a/src/build/prepareElectronApp.ts +++ b/src/build/prepareElectronApp.ts @@ -1,11 +1,10 @@ import * as fs from 'fs'; -import * as crypto from 'crypto'; import * as path from 'path'; import { promisify } from 'util'; import * as log from 'loglevel'; -import { copyFileOrDir } from '../helpers/helpers'; +import { copyFileOrDir, generateRandomSuffix } from '../helpers/helpers'; import { AppOptions } from '../options/model'; const writeFileAsync = promisify(fs.writeFile); @@ -103,6 +102,8 @@ async function maybeCopyScripts(srcs: string[], dest: string): Promise { return; } + const supportedInjectionExtensions = ['.css', '.js']; + log.debug(`Copying ${srcs.length} files to inject in app.`); for (const src of srcs) { if (!fs.existsSync(src)) { @@ -111,26 +112,22 @@ async function maybeCopyScripts(srcs: string[], dest: string): Promise { ); } - let destFileName: string; - if (path.extname(src) === '.js') { - destFileName = 'inject.js'; - } else if (path.extname(src) === '.css') { - destFileName = 'inject.css'; - } else { - return; + if (supportedInjectionExtensions.indexOf(path.extname(src)) < 0) { + console.log(`Skipping unsupported injection file: ${src}`); + continue; } + const postFixHash = generateRandomSuffix(); + const destFileName = `inject-${postFixHash}.${path.extname(src)}`; const destPath = path.join(dest, 'inject', destFileName); log.debug(`Copying injection file "${src}" to "${destPath}"`); await copyFileOrDir(src, destPath); } } -function normalizeAppName(appName: string, url: string): string { - // use a simple 3 byte random string to prevent collision - const hash = crypto.createHash('md5'); - hash.update(url); - const postFixHash = hash.digest('hex').substring(0, 6); +function normalizeAppName(appName: string): string { + // use a simple random string to prevent collisions + const postFixHash = generateRandomSuffix(); const normalized = appName .toLowerCase() .replace(/[,:.]/g, '') @@ -138,14 +135,10 @@ function normalizeAppName(appName: string, url: string): string { return `${normalized}-nativefier-${postFixHash}`; } -function changeAppPackageJsonName( - appPath: string, - name: string, - url: string, -): void { +function changeAppPackageJsonName(appPath: string, name: string): void { const packageJsonPath = path.join(appPath, '/package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString()); - const normalizedAppName = normalizeAppName(name, url); + const normalizedAppName = normalizeAppName(name); packageJson.name = normalizedAppName; log.debug(`Updating ${packageJsonPath} 'name' field to ${normalizedAppName}`); @@ -189,9 +182,5 @@ export async function prepareElectronApp( } catch (err) { log.error('Error copying injection files.', err); } - changeAppPackageJsonName( - dest, - options.packager.name, - options.packager.targetUrl, - ); + changeAppPackageJsonName(dest, options.packager.name); } diff --git a/src/helpers/helpers.test.ts b/src/helpers/helpers.test.ts index fd4492e..f1d22ff 100644 --- a/src/helpers/helpers.test.ts +++ b/src/helpers/helpers.test.ts @@ -1,4 +1,4 @@ -import { isArgFormatInvalid } from './helpers'; +import { isArgFormatInvalid, generateRandomSuffix } from './helpers'; describe('isArgFormatInvalid', () => { test('is false for correct short args', () => { @@ -34,3 +34,25 @@ describe('isArgFormatInvalid', () => { expect(isArgFormatInvalid('--test-run-with-many-dashes')).toBe(false); }); }); + +describe('generateRandomSuffix', () => { + test('is not empty', () => { + expect(generateRandomSuffix()).not.toBe(''); + }); + + test('is not null', () => { + expect(generateRandomSuffix()).not.toBeNull(); + }); + + test('is not undefined', () => { + expect(generateRandomSuffix()).toBeDefined(); + }); + + test('is different per call', () => { + expect(generateRandomSuffix()).not.toBe(generateRandomSuffix()); + }); + + test('respects the length param', () => { + expect(generateRandomSuffix(10).length).toBe(10); + }); +}); diff --git a/src/helpers/helpers.ts b/src/helpers/helpers.ts index 04eef50..72d80e8 100644 --- a/src/helpers/helpers.ts +++ b/src/helpers/helpers.ts @@ -1,4 +1,5 @@ import { spawnSync } from 'child_process'; +import * as crypto from 'crypto'; import * as os from 'os'; import * as path from 'path'; @@ -163,3 +164,10 @@ export function isArgFormatInvalid(arg: string): boolean { !['--x', '--y'].includes(arg) // exception for long args --{x,y} ); } + +export function generateRandomSuffix(length = 6): string { + const hash = crypto.createHash('md5'); + // Add a random salt to help avoid collisions + hash.update(crypto.randomBytes(256)); + return hash.digest('hex').substring(0, length); +}