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 shim from '@joplin/lib/shim';
|
||||||
import { _, setLocale } from '@joplin/lib/locale';
|
import { _, setLocale } from '@joplin/lib/locale';
|
||||||
import { BrowserWindow, nativeTheme, nativeImage, dialog, shell, MessageBoxSyncOptions } from 'electron';
|
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 { fileUriToPath } from '@joplin/utils/url';
|
||||||
import { urlDecode } from '@joplin/lib/string-utils';
|
import { urlDecode } from '@joplin/lib/string-utils';
|
||||||
import * as Sentry from '@sentry/electron/main';
|
import * as Sentry from '@sentry/electron/main';
|
||||||
@ -88,11 +88,6 @@ export class Bridge {
|
|||||||
return this.rootProfileDir_;
|
return this.rootProfileDir_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private logWarning(...message: string[]) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('bridge:', ...message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public electronApp() {
|
public electronApp() {
|
||||||
return this.electronWrapper_;
|
return this.electronWrapper_;
|
||||||
}
|
}
|
||||||
@ -330,13 +325,10 @@ export class Bridge {
|
|||||||
fullPath = fileUriToPath(urlDecode(fullPath), shim.platformName());
|
fullPath = fileUriToPath(urlDecode(fullPath), shim.platformName());
|
||||||
}
|
}
|
||||||
fullPath = normalize(fullPath);
|
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.
|
// Note: pathExists is intended to mitigate a security issue related to network drives
|
||||||
const isUntrustedUncPath = isUncPath(fullPath);
|
// on Windows.
|
||||||
if (isUntrustedUncPath) {
|
if (await pathExists(fullPath)) {
|
||||||
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)) {
|
|
||||||
return shell.openPath(fullPath);
|
return shell.openPath(fullPath);
|
||||||
} else {
|
} else {
|
||||||
return 'Path does not exist.';
|
return 'Path does not exist.';
|
||||||
|
@ -4,6 +4,23 @@ import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
|
|||||||
import HtmlToMd from '@joplin/lib/HtmlToMd';
|
import HtmlToMd from '@joplin/lib/HtmlToMd';
|
||||||
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types';
|
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', () => {
|
describe('resourceHandling', () => {
|
||||||
it('should sanitize pasted HTML', async () => {
|
it('should sanitize pasted HTML', async () => {
|
||||||
Setting.setConstant('resourceDir', '/home/.config/joplin/resources');
|
Setting.setConstant('resourceDir', '/home/.config/joplin/resources');
|
||||||
@ -27,18 +44,7 @@ describe('resourceHandling', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should clean up pasted HTML', async () => {
|
it('should clean up pasted HTML', async () => {
|
||||||
const markupToHtml: MarkupToHtmlHandler = async (markupLanguage, markup, options) => {
|
const { markupToHtml, htmlToMd } = createTestMarkupConverters();
|
||||||
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 testCases = [
|
const testCases = [
|
||||||
['<p style="background-color: red">Hello</p><p style="display: hidden;">World</p>', '<p>Hello</p>\n<p>World</p>\n'],
|
['<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;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHandler | null, mdToHtml: MarkupToHtmlHandler | null) {
|
|
||||||
|
const processImagesInPastedHtml = async (html: string) => {
|
||||||
const allImageUrls: string[] = [];
|
const allImageUrls: string[] = [];
|
||||||
const mappedResources: Record<string, 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) => {
|
htmlUtils.replaceImageUrls(html, (src: string) => {
|
||||||
allImageUrls.push(src);
|
allImageUrls.push(src);
|
||||||
});
|
});
|
||||||
@ -200,6 +194,19 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa
|
|||||||
|
|
||||||
await Promise.all(downloadImages);
|
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
|
// 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
|
// 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
|
// 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;
|
html = (await mdToHtml(MarkupLanguage.Markdown, md, markupRenderOptions({ bodyOnly: true }))).html;
|
||||||
}
|
}
|
||||||
|
|
||||||
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(
|
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(html, {
|
||||||
htmlUtils.replaceImageUrls(html, (src: string) => {
|
allowedFilePrefixes: [Setting.value('resourceDir')],
|
||||||
return mappedResources[src];
|
}));
|
||||||
}), {
|
|
||||||
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);
|
messenger.setLocalInterface(control);
|
||||||
return control;
|
return control;
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { closestSupportedLocale, parsePluralForm, setLocale, _n } from './locale';
|
import { closestSupportedLocale, parsePluralForm, setLocale, _n, toIso639 } from './locale';
|
||||||
|
|
||||||
describe('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'],
|
['cos', 'co'],
|
||||||
['cre', 'cr'],
|
['cre', 'cr'],
|
||||||
['dan', 'da'],
|
['dan', 'da'],
|
||||||
|
['deu', 'de'],
|
||||||
['div', 'dv'],
|
['div', 'dv'],
|
||||||
['dzo', 'dz'],
|
['dzo', 'dz'],
|
||||||
['eng', 'en'],
|
['eng', 'en'],
|
||||||
|
@ -755,21 +755,24 @@ function shimInit(options: ShimInitOptions = null) {
|
|||||||
shim.pdfExtractEmbeddedText = async (pdfPath: string): Promise<string[]> => {
|
shim.pdfExtractEmbeddedText = async (pdfPath: string): Promise<string[]> => {
|
||||||
const loadingTask = pdfJs.getDocument(pdfPath);
|
const loadingTask = pdfJs.getDocument(pdfPath);
|
||||||
const doc = await loadingTask.promise;
|
const doc = await loadingTask.promise;
|
||||||
|
|
||||||
const textByPage = [];
|
const textByPage = [];
|
||||||
|
|
||||||
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
|
try {
|
||||||
const page = await doc.getPage(pageNum);
|
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
|
||||||
const textContent = await page.getTextContent();
|
const page = await doc.getPage(pageNum);
|
||||||
|
const textContent = await page.getTextContent();
|
||||||
|
|
||||||
const strings = textContent.items.map(item => {
|
const strings = textContent.items.map(item => {
|
||||||
const text = (item as TextItem).str ?? '';
|
const text = (item as TextItem).str ?? '';
|
||||||
return text;
|
return text;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
// Some PDFs contain unsupported characters that can lead to hard-to-debug issues.
|
// Some PDFs contain unsupported characters that can lead to hard-to-debug issues.
|
||||||
// We remove them here.
|
// We remove them here.
|
||||||
textByPage.push(replaceUnsupportedCharacters(strings));
|
textByPage.push(replaceUnsupportedCharacters(strings));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await doc.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
return textByPage;
|
return textByPage;
|
||||||
@ -807,23 +810,27 @@ function shimInit(options: ShimInitOptions = null) {
|
|||||||
const loadingTask = pdfJs.getDocument(pdfPath);
|
const loadingTask = pdfJs.getDocument(pdfPath);
|
||||||
const doc = await loadingTask.promise;
|
const doc = await loadingTask.promise;
|
||||||
|
|
||||||
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
|
try {
|
||||||
const page = await doc.getPage(pageNum);
|
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
|
||||||
const viewport = page.getViewport({ scale: 2 });
|
const page = await doc.getPage(pageNum);
|
||||||
const canvas = createCanvas();
|
const viewport = page.getViewport({ scale: 2 });
|
||||||
const ctx = canvas.getContext('2d');
|
const canvas = createCanvas();
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
|
|
||||||
const renderTask = page.render({ canvasContext: ctx, viewport: viewport });
|
const renderTask = page.render({ canvasContext: ctx, viewport: viewport });
|
||||||
await renderTask.promise;
|
await renderTask.promise;
|
||||||
|
|
||||||
const buffer = await canvasToBuffer(canvas);
|
const buffer = await canvasToBuffer(canvas);
|
||||||
const filePath = `${outputDirectoryPath}/${filePrefix}_${pageNum.toString().padStart(4, '0')}.jpg`;
|
const filePath = `${outputDirectoryPath}/${filePrefix}_${pageNum.toString().padStart(4, '0')}.jpg`;
|
||||||
output.push(filePath);
|
output.push(filePath);
|
||||||
await writeFile(filePath, buffer, 'binary');
|
await writeFile(filePath, buffer, 'binary');
|
||||||
if (!(await shim.fsDriver().exists(filePath))) throw new Error(`Could not write to file: ${filePath}`);
|
if (!(await shim.fsDriver().exists(filePath))) throw new Error(`Could not write to file: ${filePath}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await doc.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
|
Loading…
Reference in New Issue
Block a user