1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Merge branch 'release-2.14' into dev

This commit is contained in:
Laurent Cozic 2024-03-18 10:17:39 +00:00
commit fd4d7ead43
7 changed files with 105 additions and 66 deletions

View File

@ -2,7 +2,7 @@ import ElectronAppWrapper from './ElectronAppWrapper';
import shim from '@joplin/lib/shim';
import { _, setLocale } from '@joplin/lib/locale';
import { BrowserWindow, nativeTheme, nativeImage, dialog, shell, MessageBoxSyncOptions } from 'electron';
import { dirname, isUncPath, toSystemSlashes } from '@joplin/lib/path-utils';
import { dirname, toSystemSlashes } from '@joplin/lib/path-utils';
import { fileUriToPath } from '@joplin/utils/url';
import { urlDecode } from '@joplin/lib/string-utils';
import * as Sentry from '@sentry/electron/main';
@ -88,11 +88,6 @@ export class Bridge {
return this.rootProfileDir_;
}
private logWarning(...message: string[]) {
// eslint-disable-next-line no-console
console.warn('bridge:', ...message);
}
public electronApp() {
return this.electronWrapper_;
}
@ -330,13 +325,10 @@ export class Bridge {
fullPath = fileUriToPath(urlDecode(fullPath), shim.platformName());
}
fullPath = normalize(fullPath);
// On Windows, \\example.com\... links can map to network drives. Opening files on these
// drives can lead to arbitrary remote code execution.
const isUntrustedUncPath = isUncPath(fullPath);
if (isUntrustedUncPath) {
this.logWarning(`Not opening external file link: ${fullPath} -- it starts with two \\s, so could be to a network drive.`);
return 'Refusing to open file on a network drive.';
} else if (await pathExists(fullPath)) {
// Note: pathExists is intended to mitigate a security issue related to network drives
// on Windows.
if (await pathExists(fullPath)) {
return shell.openPath(fullPath);
} else {
return 'Path does not exist.';

View File

@ -4,6 +4,23 @@ import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
import HtmlToMd from '@joplin/lib/HtmlToMd';
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types';
const createTestMarkupConverters = () => {
const markupToHtml: MarkupToHtmlHandler = async (markupLanguage, markup, options) => {
const conv = markupLanguageUtils.newMarkupToHtml({}, {
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
customCss: '',
});
return conv.render(markupLanguage, markup, {}, options);
};
const htmlToMd: HtmlToMarkdownHandler = async (_markupLanguage, html, _originalCss) => {
const conv = new HtmlToMd();
return conv.parse(html);
};
return { markupToHtml, htmlToMd };
};
describe('resourceHandling', () => {
it('should sanitize pasted HTML', async () => {
Setting.setConstant('resourceDir', '/home/.config/joplin/resources');
@ -27,18 +44,7 @@ describe('resourceHandling', () => {
});
it('should clean up pasted HTML', async () => {
const markupToHtml: MarkupToHtmlHandler = async (markupLanguage, markup, options) => {
const conv = markupLanguageUtils.newMarkupToHtml({}, {
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
customCss: '',
});
return conv.render(markupLanguage, markup, {}, options);
};
const htmlToMd: HtmlToMarkdownHandler = async (_markupLanguage, html, _originalCss) => {
const conv = new HtmlToMd();
return conv.parse(html);
};
const { markupToHtml, htmlToMd } = createTestMarkupConverters();
const testCases = [
['<p style="background-color: red">Hello</p><p style="display: hidden;">World</p>', '<p>Hello</p>\n<p>World</p>\n'],
@ -50,4 +56,11 @@ describe('resourceHandling', () => {
}
});
it('should preserve images pasted from the resource directory', async () => {
const { markupToHtml, htmlToMd } = createTestMarkupConverters();
// All images in the resource directory should be preserved.
const html = `<img src="file://${encodeURI(Setting.value('resourceDir'))}/resource.png" alt="test"/>`;
expect(await processPastedHtml(html, htmlToMd, markupToHtml)).toBe(html);
});
});

View File

@ -138,17 +138,11 @@ export async function getResourcesFromPasteEvent(event: any) {
return output;
}
export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHandler | null, mdToHtml: MarkupToHtmlHandler | null) {
const processImagesInPastedHtml = async (html: string) => {
const allImageUrls: string[] = [];
const mappedResources: Record<string, string> = {};
// When copying text from eg. GitHub, the HTML might contain non-breaking
// spaces instead of regular spaces. If these non-breaking spaces are
// inserted into the TinyMCE editor (using insertContent), they will be
// dropped. So here we convert them to regular spaces.
// https://stackoverflow.com/a/31790544/561309
html = html.replace(/[\u202F\u00A0]/g, ' ');
htmlUtils.replaceImageUrls(html, (src: string) => {
allImageUrls.push(src);
});
@ -200,6 +194,19 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa
await Promise.all(downloadImages);
return htmlUtils.replaceImageUrls(html, (src: string) => mappedResources[src]);
};
export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHandler | null, mdToHtml: MarkupToHtmlHandler | null) {
// When copying text from eg. GitHub, the HTML might contain non-breaking
// spaces instead of regular spaces. If these non-breaking spaces are
// inserted into the TinyMCE editor (using insertContent), they will be
// dropped. So here we convert them to regular spaces.
// https://stackoverflow.com/a/31790544/561309
html = html.replace(/[\u202F\u00A0]/g, ' ');
html = await processImagesInPastedHtml(html);
// TinyMCE can accept any type of HTML, including HTML that may not be preserved once saved as
// Markdown. For example the content may have a dark background which would be supported by
// TinyMCE, but lost once the note is saved. So here we convert the HTML to Markdown then back
@ -209,11 +216,7 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa
html = (await mdToHtml(MarkupLanguage.Markdown, md, markupRenderOptions({ bodyOnly: true }))).html;
}
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(
htmlUtils.replaceImageUrls(html, (src: string) => {
return mappedResources[src];
}), {
allowedFilePrefixes: [Setting.value('resourceDir')],
},
));
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(html, {
allowedFilePrefixes: [Setting.value('resourceDir')],
}));
}

View File

@ -34,6 +34,19 @@ export const initCodeMirror = (
},
});
// Works around https://github.com/laurent22/joplin/issues/10047 by handling
// the text/uri-list MIME type when pasting, rather than sending the paste event
// to CodeMirror.
//
// TODO: Remove this workaround when the issue has been fixed upstream.
control.on('paste', (_editor, event: ClipboardEvent) => {
const clipboardData = event.clipboardData;
if (clipboardData.types.length === 1 && clipboardData.types[0] === 'text/uri-list') {
event.preventDefault();
control.insertText(clipboardData.getData('text/uri-list'));
}
});
messenger.setLocalInterface(control);
return control;
};

View File

@ -1,4 +1,4 @@
import { closestSupportedLocale, parsePluralForm, setLocale, _n } from './locale';
import { closestSupportedLocale, parsePluralForm, setLocale, _n, toIso639 } from './locale';
describe('locale', () => {
@ -91,4 +91,14 @@ describe('locale', () => {
}
});
test.each([
['en_GB', 'eng'],
['en', 'eng'],
['de', 'deu'],
['fr_FR', 'fra'],
])('should convert to ISO-639', (input, expected) => {
const actual = toIso639(input);
expect(actual).toBe(expected);
});
});

View File

@ -213,6 +213,7 @@ const iso639Map_ = [
['cos', 'co'],
['cre', 'cr'],
['dan', 'da'],
['deu', 'de'],
['div', 'dv'],
['dzo', 'dz'],
['eng', 'en'],

View File

@ -755,21 +755,24 @@ function shimInit(options: ShimInitOptions = null) {
shim.pdfExtractEmbeddedText = async (pdfPath: string): Promise<string[]> => {
const loadingTask = pdfJs.getDocument(pdfPath);
const doc = await loadingTask.promise;
const textByPage = [];
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
const page = await doc.getPage(pageNum);
const textContent = await page.getTextContent();
try {
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
const page = await doc.getPage(pageNum);
const textContent = await page.getTextContent();
const strings = textContent.items.map(item => {
const text = (item as TextItem).str ?? '';
return text;
}).join('\n');
const strings = textContent.items.map(item => {
const text = (item as TextItem).str ?? '';
return text;
}).join('\n');
// Some PDFs contain unsupported characters that can lead to hard-to-debug issues.
// We remove them here.
textByPage.push(replaceUnsupportedCharacters(strings));
// Some PDFs contain unsupported characters that can lead to hard-to-debug issues.
// We remove them here.
textByPage.push(replaceUnsupportedCharacters(strings));
}
} finally {
await doc.destroy();
}
return textByPage;
@ -807,23 +810,27 @@ function shimInit(options: ShimInitOptions = null) {
const loadingTask = pdfJs.getDocument(pdfPath);
const doc = await loadingTask.promise;
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
const page = await doc.getPage(pageNum);
const viewport = page.getViewport({ scale: 2 });
const canvas = createCanvas();
const ctx = canvas.getContext('2d');
try {
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
const page = await doc.getPage(pageNum);
const viewport = page.getViewport({ scale: 2 });
const canvas = createCanvas();
const ctx = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderTask = page.render({ canvasContext: ctx, viewport: viewport });
await renderTask.promise;
const renderTask = page.render({ canvasContext: ctx, viewport: viewport });
await renderTask.promise;
const buffer = await canvasToBuffer(canvas);
const filePath = `${outputDirectoryPath}/${filePrefix}_${pageNum.toString().padStart(4, '0')}.jpg`;
output.push(filePath);
await writeFile(filePath, buffer, 'binary');
if (!(await shim.fsDriver().exists(filePath))) throw new Error(`Could not write to file: ${filePath}`);
const buffer = await canvasToBuffer(canvas);
const filePath = `${outputDirectoryPath}/${filePrefix}_${pageNum.toString().padStart(4, '0')}.jpg`;
output.push(filePath);
await writeFile(filePath, buffer, 'binary');
if (!(await shim.fsDriver().exists(filePath))) throw new Error(`Could not write to file: ${filePath}`);
}
} finally {
await doc.destroy();
}
return output;