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:
commit
fd4d7ead43
@ -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.';
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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')],
|
||||
}));
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -213,6 +213,7 @@ const iso639Map_ = [
|
||||
['cos', 'co'],
|
||||
['cre', 'cr'],
|
||||
['dan', 'da'],
|
||||
['deu', 'de'],
|
||||
['div', 'dv'],
|
||||
['dzo', 'dz'],
|
||||
['eng', 'en'],
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user