1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-24 08:12:24 +02:00

Clipper: Improve support for future versions of Chrome (upgrade to manifest version 3) (#10109)

This commit is contained in:
Henry Heino 2024-03-14 11:38:20 -07:00 committed by GitHub
parent 78b8839ae3
commit e72cce0d07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 127 additions and 190 deletions

View File

@ -1229,6 +1229,7 @@ packages/tools/packageJsonLint.js
packages/tools/postPreReleasesToForum.js
packages/tools/release-android.js
packages/tools/release-cli.js
packages/tools/release-clipper.js
packages/tools/release-electron.js
packages/tools/release-ios.js
packages/tools/release-plugin-repo-cli.js

1
.gitignore vendored
View File

@ -1209,6 +1209,7 @@ packages/tools/packageJsonLint.js
packages/tools/postPreReleasesToForum.js
packages/tools/release-android.js
packages/tools/release-cli.js
packages/tools/release-clipper.js
packages/tools/release-electron.js
packages/tools/release-ios.js
packages/tools/release-plugin-repo-cli.js

View File

@ -11,13 +11,9 @@
if (typeof browser !== 'undefined') {
// eslint-disable-next-line no-undef
browser_ = browser;
// eslint-disable-next-line no-undef
browserSupportsPromises_ = true;
} else if (typeof chrome !== 'undefined') {
// eslint-disable-next-line no-undef
browser_ = chrome;
// eslint-disable-next-line no-undef
browserSupportsPromises_ = false;
}
function escapeHtml(s) {
@ -461,6 +457,7 @@
tags: command.tags,
windowInnerWidth: window.innerWidth,
windowInnerHeight: window.innerHeight,
devicePixelRatio: window.devicePixelRatio,
};
browser_.runtime.sendMessage({

View File

@ -1,10 +1,12 @@
{
"manifest_version": 2,
"manifest_version": 3,
"name": "Joplin Web Clipper [DEV]",
"version": "3.0.0",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": "script-src 'self'; object-src 'self'",
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
},
"icons": {
"32": "icons/32.png",
"48": "icons/48.png",
@ -13,18 +15,21 @@
"permissions": [
"activeTab",
"tabs",
"http://*/",
"https://*/",
"<all_urls>",
"scripting",
"storage"
],
"browser_action": {
"host_permissions": [
"http://*/",
"https://*/",
"<all_urls>"
],
"action": {
"default_icon": "icons/32.png",
"default_title": "Joplin Web Clipper",
"default_popup": "popup/build/index.html"
},
"commands": {
"_execute_browser_action": {
"_execute_action": {
"suggested_key": {
"default": "Alt+Shift+J"
}
@ -49,12 +54,12 @@
}
},
"background": {
"scripts": [
"background.js"
],
"persistent": false
"scripts": ["service_worker.mjs"],
"service_worker": "service_worker.mjs",
"type": "module"
},
"applications": {
"browser_specific_settings": {
"gecko": {
"id": "{8419486a-54e9-11e8-9401-ac9e17909436}"
}

View File

@ -115,6 +115,13 @@ class AppComponent extends Component {
this.clipScreenshot_click = async () => {
try {
// Firefox requires the <all_urls> host permission to take a
// screenshot of the current page, however, this may change
// in the future. Note that Firefox also forces this permission
// to be optional.
// See https://discourse.mozilla.org/t/browser-tabs-capturevisibletab-not-working-in-firefox-for-mv3/122965/3
await bridge().browser().permissions.request({ origins: ['<all_urls>'] });
const baseUrl = await bridge().clipperServerBaseUrl();
await bridge().sendCommandToActiveTab({
@ -179,12 +186,14 @@ class AppComponent extends Component {
}
async loadContentScripts() {
await bridge().tabsExecuteScript({ file: '/content_scripts/setUpEnvironment.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/JSDOMParser.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability-readerable.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/clipperUtils.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/index.js' });
await bridge().tabsExecuteScript([
'/content_scripts/setUpEnvironment.js',
'/content_scripts/JSDOMParser.js',
'/content_scripts/Readability.js',
'/content_scripts/Readability-readerable.js',
'/content_scripts/clipperUtils.js',
'/content_scripts/index.js',
]);
}
async componentDidMount() {

View File

@ -1,5 +1,7 @@
/* eslint-disable no-console */
import getActiveTabs from '../../util/getActiveTabs.mjs';
import joplinEnv from '../../util/joplinEnv.mjs';
const { randomClipperPort } = require('./randomClipperPort');
function msleep(ms) {
@ -17,13 +19,12 @@ class Bridge {
this.token_ = null;
}
async init(browser, browserSupportsPromises, store) {
async init(browser, store) {
console.info('Popup: Init bridge');
this.browser_ = browser;
this.dispatch_ = store.dispatch;
this.store_ = store;
this.browserSupportsPromises_ = browserSupportsPromises;
this.clipperServerPort_ = null;
this.clipperServerPortStatus_ = 'searching';
@ -74,12 +75,7 @@ class Bridge {
}
};
this.browser_.runtime.onMessage.addListener(this.browser_notify);
const backgroundPage = await this.backgroundPage(this.browser_);
// Not sure why the getBackgroundPage() sometimes returns null, so
// in that case default to "prod" environment, which means the live
// extension won't be affected by this bug.
this.env_ = backgroundPage ? backgroundPage.joplinEnv() : 'prod';
this.env_ = joplinEnv();
console.info('Popup: Env:', this.env());
@ -197,17 +193,6 @@ class Bridge {
}
}
async backgroundPage(browser) {
const bgp = browser.extension.getBackgroundPage();
if (bgp) return bgp;
return new Promise((resolve) => {
browser.runtime.getBackgroundPage((bgp) => {
resolve(bgp);
});
});
}
env() {
return this.env_;
}
@ -305,50 +290,26 @@ class Bridge {
return `http://127.0.0.1:${port}`;
}
async tabsExecuteScript(options) {
if (this.browserSupportsPromises_) return this.browser().tabs.executeScript(options);
return new Promise((resolve, reject) => {
this.browser().tabs.executeScript(options, () => {
const e = this.browser().runtime.lastError;
if (e) {
const msg = [`tabsExecuteScript: Cannot load ${JSON.stringify(options)}`];
if (e.message) msg.push(e.message);
reject(new Error(msg.join(': ')));
}
resolve();
});
async tabsExecuteScript(files) {
const activeTabs = await getActiveTabs(this.browser());
await this.browser().scripting.executeScript({
target: {
tabId: activeTabs[0].id,
},
files,
});
}
async tabsQuery(options) {
if (this.browserSupportsPromises_) return this.browser().tabs.query(options);
return new Promise((resolve) => {
this.browser().tabs.query(options, (tabs) => {
resolve(tabs);
});
});
return this.browser().tabs.query(options);
}
async tabsSendMessage(tabId, command) {
if (this.browserSupportsPromises_) return this.browser().tabs.sendMessage(tabId, command);
return new Promise((resolve) => {
this.browser().tabs.sendMessage(tabId, command, (result) => {
resolve(result);
});
});
return this.browser().tabs.sendMessage(tabId, command);
}
async tabsCreate(options) {
if (this.browserSupportsPromises_) return this.browser().tabs.create(options);
return new Promise((resolve) => {
this.browser().tabs.create(options, () => {
resolve();
});
});
return this.browser().tabs.create(options);
}
async folderTree() {
@ -356,29 +317,15 @@ class Bridge {
}
async storageSet(keys) {
if (this.browserSupportsPromises_) return this.browser().storage.local.set(keys);
return new Promise((resolve) => {
this.browser().storage.local.set(keys, () => {
resolve();
});
});
return this.browser().storage.local.set(keys);
}
async storageGet(keys, defaultValue = null) {
if (this.browserSupportsPromises_) {
try {
const r = await this.browser().storage.local.get(keys);
return r;
} catch (error) {
return defaultValue;
}
} else {
return new Promise((resolve) => {
this.browser().storage.local.get(keys, (result) => {
resolve(result);
});
});
try {
const r = await this.browser().storage.local.get(keys);
return r;
} catch (error) {
return defaultValue;
}
}

View File

@ -112,7 +112,7 @@ async function main() {
console.info('Popup: Init bridge and restore state...');
await bridge().init(window.browser ? window.browser : window.chrome, !!window.browser, store);
await bridge().init(window.browser ? window.browser : window.chrome, store);
console.info('Popup: Creating React app...');

View File

@ -1,25 +1,13 @@
import joplinEnv from './util/joplinEnv.mjs';
import getActiveTabs from './util/getActiveTabs.mjs';
let browser_ = null;
if (typeof browser !== 'undefined') {
browser_ = browser;
browserSupportsPromises_ = true;
} else if (typeof chrome !== 'undefined') {
browser_ = chrome;
browserSupportsPromises_ = false;
}
let env_ = null;
// Make this function global so that it can be accessed
// from the popup too.
// https://stackoverflow.com/questions/6323184/communication-between-background-page-and-popup-page-in-a-chrome-extension
window.joplinEnv = function() {
if (env_) return env_;
const manifest = browser_.runtime.getManifest();
env_ = manifest.name.indexOf('[DEV]') >= 0 ? 'dev' : 'prod';
return env_;
};
async function browserCaptureVisibleTabs(windowId) {
const options = {
format: 'jpeg',
@ -31,53 +19,19 @@ async function browserCaptureVisibleTabs(windowId) {
// https://discourse.joplinapp.org/t/clip-screenshot-image-quality/12302/4
quality: 92,
};
if (browserSupportsPromises_) return browser_.tabs.captureVisibleTab(windowId, options);
return new Promise((resolve) => {
browser_.tabs.captureVisibleTab(windowId, options, (image) => {
resolve(image);
});
});
}
async function browserGetZoom(tabId) {
if (browserSupportsPromises_) return browser_.tabs.getZoom(tabId);
return new Promise((resolve) => {
browser_.tabs.getZoom(tabId, (zoom) => {
resolve(zoom);
});
});
return browser_.tabs.captureVisibleTab(windowId, options);
}
browser_.runtime.onInstalled.addListener(() => {
if (window.joplinEnv() === 'dev') {
browser_.browserAction.setIcon({
if (joplinEnv() === 'dev') {
browser_.action.setIcon({
path: 'icons/32-dev.png',
});
}
});
async function getImageSize(dataUrl) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = function() {
resolve({ width: image.width, height: image.height });
};
image.onerror = function(event) {
reject(event);
};
image.src = dataUrl;
});
}
browser_.runtime.onMessage.addListener(async (command) => {
if (command.name === 'screenshotArea') {
const browserZoom = await browserGetZoom();
// The dimensions of the image returned by Firefox are the regular ones,
// while the one returned by Chrome depend on the screen pixel ratio. So
// it would return a 600*400 image if the window dimensions are 300x200
@ -90,15 +44,18 @@ browser_.runtime.onMessage.addListener(async (command) => {
//
// The crop rectangle is always in real pixels, so we need to multiply
// it by the ratio we've calculated.
//
// 8/3/2024: With manifest v3, we don't have access to DOM APIs in Chrome.
// As a result, we can't easily calculate the size of the captured image.
// We instead base the crop region exclusively on window.devicePixelRatio,
// which seems to work in modern Firefox and Chrome.
const imageDataUrl = await browserCaptureVisibleTabs(null);
const imageSize = await getImageSize(imageDataUrl);
const imagePixelRatio = imageSize.width / command.content.windowInnerWidth;
const content = { ...command.content };
content.image_data_url = imageDataUrl;
if ('url' in content) content.source_url = content.url;
const ratio = browserZoom * imagePixelRatio;
const ratio = content.devicePixelRatio;
const newArea = { ...command.content.crop_rect };
newArea.x *= ratio;
newArea.y *= ratio;
@ -117,19 +74,8 @@ browser_.runtime.onMessage.addListener(async (command) => {
}
});
async function getActiveTabs() {
const options = { active: true, currentWindow: true };
if (browserSupportsPromises_) return browser_.tabs.query(options);
return new Promise((resolve) => {
browser_.tabs.query(options, (tabs) => {
resolve(tabs);
});
});
}
async function sendClipMessage(clipType) {
const tabs = await getActiveTabs();
const tabs = await getActiveTabs(browser_);
if (!tabs || !tabs.length) {
console.error('No active tabs');
return;

View File

@ -0,0 +1,7 @@
const getActiveTabs = async (browser) => {
const options = { active: true, currentWindow: true };
return await browser.tabs.query(options);
}
export default getActiveTabs;

View File

@ -0,0 +1,3 @@
// AUTOGENERATED by release-clipper
export default () => 'dev';

View File

@ -1,26 +1,27 @@
const fs = require('fs-extra');
const { execCommand, rootDir } = require('./tool-utils.js');
import * as fs from 'fs-extra';
import { execCommand, rootDir } from './tool-utils';
const md5File = require('md5-file');
const glob = require('glob');
import * as glob from 'glob';
const clipperDir = `${rootDir}/packages/app-clipper`;
const tmpSourceDirName = 'Clipper-source';
async function copyDir(baseSourceDir, sourcePath, baseDestDir) {
async function copyDir(baseSourceDir: string, sourcePath: string, baseDestDir: string) {
await fs.mkdirp(`${baseDestDir}/${sourcePath}`);
await fs.copy(`${baseSourceDir}/${sourcePath}`, `${baseDestDir}/${sourcePath}`);
}
async function copyToDist(distDir) {
async function copyToDist(distDir: string) {
await copyDir(clipperDir, 'popup/build', distDir);
await copyDir(clipperDir, 'content_scripts', distDir);
await copyDir(clipperDir, 'icons', distDir);
await fs.copy(`${clipperDir}/background.js`, `${distDir}/background.js`);
await copyDir(clipperDir, 'util', distDir);
await fs.copy(`${clipperDir}/service_worker.mjs`, `${distDir}/service_worker.mjs`);
await fs.copy(`${clipperDir}/manifest.json`, `${distDir}/manifest.json`);
await fs.remove(`${distDir}/popup/build/manifest.json`);
}
async function updateManifestVersionNumber(manifestPath) {
async function updateManifestVersionNumber(manifestPath: string) {
const manifestText = await fs.readFile(manifestPath, 'utf-8');
const manifest = JSON.parse(manifestText);
const v = manifest.version.split('.');
@ -47,11 +48,11 @@ async function createSourceZip() {
return filePath;
}
async function compareFiles(path1, path2) {
async function compareFiles(path1: string, path2: string) {
return await md5File(path1) === await md5File(path2);
}
async function compareDir(dir1, dir2) {
async function compareDir(dir1: string, dir2: string) {
console.info(`Comparing directories ${dir1} to ${dir2}`);
const globOptions = {
@ -61,7 +62,7 @@ async function compareDir(dir1, dir2) {
],
};
const filterFiles = (f) => {
const filterFiles = (f: string) => {
const stat = fs.statSync(f);
return !stat.isDirectory();
};
@ -69,11 +70,11 @@ async function compareDir(dir1, dir2) {
const files1 = glob.sync(`${dir1}/**/*`, globOptions).filter(filterFiles).map(f => f.substr(dir1.length + 1));
const files2 = glob.sync(`${dir2}/**/*`, globOptions).filter(filterFiles).map(f => f.substr(dir2.length + 1));
const missingFiles1 = [];
const missingFiles2 = [];
const canBeMissing1 = [];
const canBeMissing2 = ['manifest.json'];
const differentFiles = [];
const missingFiles1: string[] = [];
const missingFiles2: string[] = [];
const canBeMissing1: string[] = [];
const canBeMissing2: string[] = ['manifest.json'];
const differentFiles: string[] = [];
for (const f of files1) {
if (!files2.includes(f)) {
if (canBeMissing2.includes(f)) continue;
@ -104,7 +105,7 @@ async function compareDir(dir1, dir2) {
return false;
}
async function checkSourceZip(sourceZip, compiledZip) {
async function checkSourceZip(sourceZip: string, compiledZip: string) {
const tmpDir = `${require('os').tmpdir()}/${Date.now()}`;
console.info(`Checking source ZIP in ${tmpDir}`);
@ -119,6 +120,7 @@ async function checkSourceZip(sourceZip, compiledZip) {
console.info(await execCommand(`unzip "${sourceZip}"`));
process.chdir(`${sourceDir}/Clipper-source/popup`);
console.info(await execCommand('npm install'));
console.info(await execCommand('npm run build'));
process.chdir(compiledDir);
console.info(await execCommand(`cp "${compiledZip}" .`));
@ -132,6 +134,11 @@ async function checkSourceZip(sourceZip, compiledZip) {
}
}
async function setReleaseMode(isReleaseMode: boolean) {
const joplinEnvPath = `${clipperDir}/util/joplinEnv.mjs`;
await fs.writeFile(joplinEnvPath, `// AUTOGENERATED by release-clipper\n\nexport default () => '${isReleaseMode ? 'prod' : 'dev'}';`);
}
async function main() {
console.info(await execCommand('git pull'));
@ -139,24 +146,34 @@ async function main() {
console.info('Building extension...');
process.chdir(`${clipperDir}/popup`);
// SKIP_PREFLIGHT_CHECK avoids the error "There might be a problem with the project dependency tree." due to eslint 5.12.0 being
// installed by CRA and 6.1.0 by us. It doesn't affect anything though, and the behaviour of the preflight
// check is buggy so we can ignore it.
await setReleaseMode(true);
console.info(await execCommand(`rm -rf ${clipperDir}/popup/build`));
console.info(await execCommand('npm run build'));
const dists = {
type PlatformDistOptions = {
removeManifestKeys(manifest: Record<string, any>): Record<string, any>;
outputPath?: string;
};
const dists: Record<string, PlatformDistOptions> = {
chrome: {
removeManifestKeys: (manifest) => {
manifest = { ...manifest };
delete manifest.applications;
delete manifest.browser_specific_settings;
manifest.background = { ...manifest.background };
delete manifest.background.scripts;
delete manifest.background.persistent;
return manifest;
},
},
firefox: {
removeManifestKeys: (manifest) => {
manifest = { ...manifest };
delete manifest.background.persistent;
manifest.background = { ...manifest.background };
delete manifest.background.service_worker;
return manifest;
},
},
@ -186,12 +203,16 @@ async function main() {
const sourceZip = await createSourceZip();
await checkSourceZip(sourceZip, dists.firefox.outputPath);
await setReleaseMode(false);
process.chdir(clipperDir);
console.info(await execCommand('git add -A'));
console.info(await execCommand(`git commit -m "Clipper release v${newVersion}"`));
console.info(await execCommand(`git tag clipper-${newVersion}`));
console.info(await execCommand('git push'));
console.info(await execCommand('git push --tags'));
if (!process.argv.includes('--no-publish')) {
console.info(await execCommand('git add -A'));
console.info(await execCommand(`git commit -m "Clipper release v${newVersion}"`));
console.info(await execCommand(`git tag clipper-${newVersion}`));
console.info(await execCommand('git push'));
console.info(await execCommand('git push --tags'));
}
}
main().catch((error) => {