Compare commits

...
14 Commits
Author SHA1 Message Date
Laurent Cozic 7d2c1c08ed Desktop release v2.13.15 2024-01-14 12:38:12 +00:00
Laurent Cozic c412b1f0f8 Desktop: Fixes existing attachments not being shared in some cases with Joplin Server 2024-01-14 12:37:57 +00:00
Laurent Cozic 174c5f44d6 Desktop release v2.13.14 2024-01-13 18:16:36 +00:00
Laurent Cozic e0bef3c99c Desktop: Fixes #9709: Shared notebook can get corrupted if contributor changes its title 2024-01-13 18:16:22 +00:00
Laurent Cozic 15394a3f7e Plugin Generator release v2.13.3 2024-01-06 12:17:43 +00:00
Laurent Cozic c3cdeeb315 Desktop release v2.13.13 2024-01-06 12:16:04 +00:00
Henry Heino af882612e9 Desktop: Fixes #9304: Fix HTML resource links lost when editing notes in the rich text editor (Backport #9435) (#9647) 2024-01-06 12:12:39 +00:00
Henry Heino 11fffc0e4e Desktop: Fixes #9613: Fix rich text editor deletes HTML links to notes (#9624) 2024-01-04 15:22:27 +00:00
Henry Heino ea2d9ca467 Desktop: Fixes #9045: Ubuntu: Fix window sometimes doesn't appear on startup (Backport #9561) (#9612) 2024-01-04 15:21:23 +00:00
Laurent Cozic e027cdce26 Desktop release v2.13.12 2023-12-31 15:16:56 +00:00
Laurent Cozic 229b9a580e Desktop, Cli: Fix importing certain ENEX notes that include invalid tables 2023-12-31 15:16:40 +00:00
Laurent Cozic a6df9f119c Chore: Removed backup plugin sub-module 2023-12-31 15:15:32 +00:00
Laurent Cozic 6670f9ae1c Desktop release v2.13.11 2023-12-24 11:26:28 +00:00
Laurent Cozic 09506a7b40 Desktop, Cli: Fixes #9548: Import ENEX archives that contain files with invalid names 2023-12-24 11:26:07 +00:00
26 changed files with 251 additions and 46 deletions

No files matched your search

+1
View File
@@ -385,6 +385,7 @@ packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/createStartupArgs.js
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
+1
View File
@@ -367,6 +367,7 @@ packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/createStartupArgs.js
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export4.dtd">
<en-export export-date="20230920T212153Z" application="Evernote" version="10.60.4">
<note>
<title>ABOUT</title>
<created>20200221T010525Z</created>
<updated>20230720T000138Z</updated>
<tag>recovery</tag>
<tag>San Luis Obispo</tag>
<tag>homelessness in SLO</tag>
<tag>Sunny Acres</tag>
<note-attributes>
<author>Michael B. Goldstein</author>
<source>web.clip</source>
<source-url>https://www.sunnyacres.community/about.html</source-url>
</note-attributes>
<content>
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
<en-note></en-note> ]]>
</content>
<resource>
<data encoding="base64">
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 truncated
</data>
<mime>image/png</mime>
<width>16</width>
<height>16</height>
<resource-attributes>
<file-name>08.06.2014 16:58:55</file-name>
<source-url>en-cache://tokenKey%3D%22AuthToken%3AUser%3A33065190%22+542a52b5-e793-4e26-9839-c13552e5a75a+2fb6ad590ae59702499393cc35850cdf+https://www.evernote.com/shard/s254/res/6da89c4b-e2d2-41a5-afe7-df2c15839e6f</source-url>
</resource-attributes>
</resource>
</note>
</en-export>
@@ -1,4 +1,5 @@
<table>
<div></div> <!-- INVALID! -->
<tr>
<td>one</td>
<td>two</td>
@@ -63,8 +63,6 @@ export default function(props: Props) {
const folder: FolderEntity = {
title: folderTitle,
icon: Folder.serializeIcon(folderIcon),
is_shared: 0,
share_id: '',
};
if (!isNew) folder.id = props.folderId;
@@ -856,7 +856,18 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const resourcesEqual = resourceInfosEqual(lastOnChangeEventInfo.current.resourceInfos, props.resourceInfos);
if (lastOnChangeEventInfo.current.content !== props.content || !resourcesEqual) {
const result = await props.markupToHtml(props.contentMarkupLanguage, props.content, markupRenderOptions({ resourceInfos: props.resourceInfos }));
const result = await props.markupToHtml(
props.contentMarkupLanguage,
props.content,
markupRenderOptions({
resourceInfos: props.resourceInfos,
// Allow file:// URLs that point to the resource directory.
// This prevents HTML-style resource URLs (e.g. <a href="file://path/to/resource/.../"></a>)
// from being discarded.
allowedFilePrefixes: [props.resourceDirectory],
}),
);
if (cancelled) return;
editor.setContent(awfulBrHack(result.html));
@@ -439,6 +439,7 @@ function NoteEditor(props: NoteEditorProps) {
contentMarkupLanguage: formNote.markup_language,
contentOriginalCss: formNote.originalCss,
resourceInfos: resourceInfos,
resourceDirectory: Setting.value('resourceDir'),
htmlToMarkdown: htmlToMarkdown,
markupToHtml: markupToHtml,
allAssets: allAssets,
@@ -82,6 +82,7 @@ export interface NoteBodyEditorProps {
visiblePanes: string[];
keyboardMode: string;
resourceInfos: ResourceInfos;
resourceDirectory: string;
locale: string;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onDrop: Function;
@@ -26,6 +26,7 @@ export interface MarkupToHtmlOptions {
noteId?: string;
vendorDir?: string;
platformName?: string;
allowedFilePrefixes?: string[];
}
export default function useMarkupToHtml(deps: HookDependencies) {
@@ -7,6 +7,7 @@ import { writeFile } from 'fs-extra';
import { join } from 'path';
import createStartupArgs from './util/createStartupArgs';
import firstNonDevToolsWindow from './util/firstNonDevToolsWindow';
import setFilePickerResponse from './util/setFilePickerResponse';
test.describe('main', () => {
@@ -20,17 +21,7 @@ test.describe('main', () => {
test('should be able to create and edit a new note', async ({ mainWindow }) => {
const mainScreen = new MainScreen(mainWindow);
await mainScreen.newNoteButton.click();
const editor = mainScreen.noteEditor;
await editor.waitFor();
// Wait for the title input to have the correct placeholder
await mainWindow.locator('input[placeholder^="Creating new note"]').waitFor();
// Fill the title
await editor.noteTitleInput.click();
await editor.noteTitleInput.fill('Test note');
const editor = await mainScreen.createNewNote('Test note');
// Note list should contain the new note
await expect(mainScreen.noteListContainer.getByText('Test note')).toBeVisible();
@@ -49,6 +40,50 @@ test.describe('main', () => {
await expect(viewerFrame.locator('h1')).toHaveText('Test note!');
});
test('HTML links should be preserved when editing a note in the WYSIWYG editor', async ({ electronApp, mainWindow }) => {
const mainScreen = new MainScreen(mainWindow);
await mainScreen.createNewNote('Testing!');
const editor = mainScreen.noteEditor;
// Set the note's content
await editor.codeMirrorEditor.click();
// Attach this file to the note (create a resource ID)
await setFilePickerResponse(electronApp, [__filename]);
await editor.attachFileButton.click();
// Wait to render
const viewerFrame = editor.getNoteViewerIframe();
await viewerFrame.locator('a[data-from-md]').waitFor();
// Should have an attached resource
const codeMirrorContent = await editor.codeMirrorEditor.innerText();
const resourceUrlExpression = /\[.*\]\(:\/(\w+)\)/;
expect(codeMirrorContent).toMatch(resourceUrlExpression);
const resourceId = codeMirrorContent.match(resourceUrlExpression)[1];
// Create a new note with just an HTML link
await mainScreen.createNewNote('Another test');
await editor.codeMirrorEditor.click();
await mainWindow.keyboard.type(`<a href=":/${resourceId}">HTML Link</a>`);
// Switch to the RTE
await editor.toggleEditorsButton.click();
await editor.richTextEditor.waitFor();
// Edit the note to cause the original content to update
await editor.getTinyMCEFrameLocator().locator('a').click();
await mainWindow.keyboard.type('Test...');
await editor.toggleEditorsButton.click();
await editor.codeMirrorEditor.waitFor();
// Note should still contain the resource ID and note title
const finalCodeMirrorContent = await editor.codeMirrorEditor.innerText();
expect(finalCodeMirrorContent).toContain(`:/${resourceId}`);
});
test('should be possible to remove sort order buttons in settings', async ({ electronApp, mainWindow }) => {
const mainScreen = new MainScreen(mainWindow);
await mainScreen.waitFor();
@@ -6,7 +6,7 @@ export default class MainScreen {
public readonly noteListContainer: Locator;
public readonly noteEditor: NoteEditorScreen;
public constructor(page: Page) {
public constructor(private page: Page) {
this.newNoteButton = page.locator('.new-note-button');
this.noteListContainer = page.locator('.rli-noteList');
this.noteEditor = new NoteEditorScreen(page);
@@ -17,4 +17,20 @@ export default class MainScreen {
await this.noteEditor.waitFor();
await this.noteListContainer.waitFor();
}
// Follows the steps a user would use to create a new note.
public async createNewNote(title: string) {
await this.waitFor();
await this.newNoteButton.click();
await this.noteEditor.waitFor();
// Wait for the title input to have the correct placeholder
await this.page.locator('input[placeholder^="Creating new note"]').waitFor();
// Fill the title
await this.noteEditor.noteTitleInput.click();
await this.noteEditor.noteTitleInput.fill(title);
return this.noteEditor;
}
}
@@ -3,13 +3,19 @@ import { Locator, Page } from '@playwright/test';
export default class NoteEditorPage {
public readonly codeMirrorEditor: Locator;
public readonly richTextEditor: Locator;
public readonly noteTitleInput: Locator;
public readonly attachFileButton: Locator;
public readonly toggleEditorsButton: Locator;
private readonly containerLocator: Locator;
public constructor(private readonly page: Page) {
this.containerLocator = page.locator('.rli-editor');
this.codeMirrorEditor = this.containerLocator.locator('.codeMirrorEditor');
this.richTextEditor = this.containerLocator.locator('iframe[title="Rich Text Area"]');
this.noteTitleInput = this.containerLocator.locator('.title-input');
this.attachFileButton = this.containerLocator.locator('[title^="Attach file"]');
this.toggleEditorsButton = this.containerLocator.locator('[title^="Toggle editors"]');
}
public getNoteViewerIframe() {
@@ -19,8 +25,15 @@ export default class NoteEditorPage {
return this.page.frame({ url: /.*note-viewer[/\\]index.html.*/ });
}
public getTinyMCEFrameLocator() {
// We use frameLocator(':scope') to convert the richTextEditor Locator into
// a FrameLocator. (:scope selects the locator itself).
// https://playwright.dev/docs/api/class-framelocator
return this.richTextEditor.frameLocator(':scope');
}
public async waitFor() {
await this.codeMirrorEditor.waitFor();
await this.noteTitleInput.waitFor();
await this.toggleEditorsButton.waitFor();
}
}
@@ -0,0 +1,13 @@
import { ElectronApplication } from '@playwright/test';
const setFilePickerResponse = (electronApp: ElectronApplication, response: string[]) => {
return electronApp.evaluate(async ({ dialog }, response) => {
dialog.showOpenDialog = async () => ({
canceled: false,
filePaths: response,
});
dialog.showOpenDialogSync = () => response;
}, response);
};
export default setFilePickerResponse;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.13.10",
"version": "2.13.15",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "generator-joplin",
"version": "2.13.2",
"version": "2.13.3",
"description": "Scaffolds out a new Joplin plugin",
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
"author": {
@@ -34,4 +34,4 @@
"repository": "https://github.com/laurent22/generator-joplin",
"license": "AGPL-3.0-or-later",
"private": true
}
}
+29 -13
View File
@@ -142,20 +142,28 @@ describe('import-enex-md-gen', () => {
expect(all[0].mime).toBe('application/zip');
});
it('should keep importing notes when one of them is corrupted', async () => {
const filePath = `${enexSampleBaseDir}/ImportTestCorrupt.enex`;
const errors: any[] = [];
await importEnex('', filePath, {
onError: (error: any) => errors.push(error),
});
const notes = await Note.all();
expect(notes.length).toBe(2);
// Disabled for now because the ENEX parser has become so error-tolerant
// that it's no longer possible to generate a note that would generate a
// failure.
// Check that an error was recorded and that it includes the title
// of the note, so that it can be found back by the user
expect(errors.length).toBe(1);
expect(errors[0].message.includes('Note 2')).toBe(true);
});
// it('should keep importing notes when one of them is corrupted', async () => {
// const filePath = `${enexSampleBaseDir}/ImportTestCorrupt.enex`;
// const errors: any[] = [];
// const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(jest.fn());
// await importEnex('', filePath, {
// onError: (error: any) => errors.push(error),
// });
// consoleSpy.mockRestore();
// const notes:NoteEntity[] = await Note.all();
// expect(notes.length).toBe(2);
// expect(notes.find(n => n.title === 'Note 1')).toBeTruthy();
// expect(notes.find(n => n.title === 'Note 3')).toBeTruthy();
// // Check that an error was recorded and that it includes the title
// // of the note, so that it can be found back by the user
// expect(errors.length).toBe(1);
// expect(errors[0].message.includes('Note 2')).toBe(true);
// });
it('should throw an error and stop if the outer XML is invalid', async () => {
await expectThrow(async () => importEnexFile('invalid_html.enex'));
@@ -204,4 +212,12 @@ describe('import-enex-md-gen', () => {
expect(resource.title).toBe('app_images/resizable/961b875f-24ac-402f-9b76-37e2d4f03a6c/house_500.jpg.png');
});
it('should sanitize resource filenames with colons', async () => {
await importEnexFile('resource_filename_with_colons.enex');
const resource: ResourceEntity = (await Resource.all())[0];
expect(resource.filename).toBe('08.06.2014165855');
expect(resource.file_extension).toBe('2014165855');
expect(resource.title).toBe('08.06.2014 16:58:55');
});
});
+9 -4
View File
@@ -1239,6 +1239,14 @@ function drawTable(table: Section) {
continue;
}
if (typeof tr === 'string') {
// A <TABLE> tag should only have <TR> tags as direct children.
// However certain Evernote notes can contain other random tags
// such as empty DIVs. In that case we just skip the content.
// See test "table_with_invalid_content.html".
continue;
}
const isHeader = tr.isHeader;
const line = [];
const headerLine = [];
@@ -1247,10 +1255,7 @@ function drawTable(table: Section) {
const td = tr.lines[tdIndex];
if (typeof td === 'string') {
// A <TR> tag should only have <TD> tags as direct children.
// However certain Evernote notes can contain other random tags
// such as empty DIVs. In that case we just skip the content.
// See test "table_with_invalid_content.html".
// Same comment as above the <TR> tags.
continue;
}
+2 -2
View File
@@ -9,7 +9,7 @@ import shim from './shim';
import { NoteEntity, ResourceEntity } from './services/database/types';
import { enexXmlToMd } from './import-enex-md-gen';
import { MarkupToHtml } from '@joplin/renderer';
import { fileExtension, friendlySafeFilename } from './path-utils';
import { fileExtension, friendlySafeFilename, safeFileExtension } from './path-utils';
const moment = require('moment');
const { wrapError } = require('./errorUtils');
const { enexXmlToHtml } = require('./import-enex-html-gen.js');
@@ -208,7 +208,7 @@ async function saveNoteResources(note: ExtractedNote) {
delete (toSave as any).dataFilePath;
delete (toSave as any).dataEncoding;
delete (toSave as any).hasData;
toSave.file_extension = resource.filename ? fileExtension(resource.filename) : '';
toSave.file_extension = resource.filename ? safeFileExtension(fileExtension(resource.filename)) : '';
// ENEX resource filenames can contain slashes, which may confuse other
// parts of the app, which expect this `filename` field to be safe.
@@ -311,11 +311,14 @@ describe('models/Folder.sharing', () => {
expect(resource.share_id).toBe('');
}
const previousBlobUpdatedTime = (await Resource.load(resourceId)).blob_updated_time;
await msleep(1);
await Folder.updateAllShareIds(resourceService);
{
const resource: ResourceEntity = await Resource.load(resourceId);
expect(resource.share_id).toBe(note1.share_id);
expect(resource.blob_updated_time).toBeGreaterThan(previousBlobUpdatedTime);
}
await Note.save({ id: note1.id, parent_id: folder2.id });
+16 -2
View File
@@ -416,6 +416,7 @@ export default class Folder extends BaseItem {
share_id: string;
is_shared: number;
resource_is_shared: number;
resource_share_id: string;
}
for (let i = 0; i < 5; i++) {
@@ -425,7 +426,12 @@ export default class Folder extends BaseItem {
// same time we also process the is_shared property.
const rows = (await this.db().selectAll(`
SELECT r.id, n.share_id, n.is_shared, r.is_shared as resource_is_shared
SELECT
r.id,
n.share_id,
n.is_shared,
r.is_shared as resource_is_shared,
r.share_id as resource_share_id
FROM note_resources nr
LEFT JOIN resources r ON nr.resource_id = r.id
LEFT JOIN notes n ON nr.note_id = n.id
@@ -512,7 +518,15 @@ export default class Folder extends BaseItem {
updated_time: now,
};
if (row.is_shared !== row.resource_is_shared) {
// When a resource becomes published or shared, we set
// `blob_updated_time` to ensure that the resource content
// is uploaded too during the next sync operation.
//
// This is necessary because Joplin Server needs to
// associate `share_id` or `is_shared` with the resource
// content for sharing to work. Otherwise the share
// recipient will only get the resource metadata.
if (row.is_shared !== row.resource_is_shared || row.share_id !== row.resource_share_id) {
resource.blob_updated_time = now;
}
+3
View File
@@ -39,6 +39,9 @@ export function isHidden(path: string) {
return b[0] === '.';
}
// Note that this function only sanitizes a file extension - it does NOT extract
// the file extension from a filename. So the way you'd normally call this is
// `safeFileExtension(fileExtension(filename))`
export function safeFileExtension(e: string, maxLength: number = null) {
// In theory the file extension can have any length but in practice Joplin
// expects a fixed length, so we limit it to 20 which should cover most cases.
+18 -2
View File
@@ -58,10 +58,26 @@ const shim = {
},
isGNOME: () => {
if ((!shim.isLinux() && !shim.isFreeBSD()) || !process) {
return false;
}
const currentDesktop = process.env['XDG_CURRENT_DESKTOP'] ?? '';
// XDG_CURRENT_DESKTOP may be something like "ubuntu:GNOME" and not just "GNOME".
// Thus, we use .includes and not ===.
return (shim.isLinux() || shim.isFreeBSD())
&& process && (process.env['XDG_CURRENT_DESKTOP'] ?? '').includes('GNOME');
if (currentDesktop.includes('GNOME')) {
return true;
}
// On Ubuntu, "XDG_CURRENT_DESKTOP=ubuntu:GNOME" is replaced with "Unity" and
// ORIGINAL_XDG_CURRENT_DESKTOP stores the original desktop.
const originalCurrentDesktop = process.env['ORIGINAL_XDG_CURRENT_DESKTOP'] ?? '';
if (originalCurrentDesktop.includes('GNOME')) {
return true;
}
return false;
},
isFreeBSD: () => {
+11 -2
View File
@@ -38,6 +38,12 @@ interface RenderOptions {
postMessageSyntax: string;
enableLongPress: boolean;
itemIdToUrl?: ItemIdToUrlHandler;
allowedFilePrefixes?: string[];
// For compatibility with MdToHtml options:
plugins?: {
link_open?: { linkRenderingType?: number };
};
}
// https://github.com/es-shims/String.prototype.trimStart/blob/main/implementation.js
@@ -95,11 +101,13 @@ export default class HtmlToHtml {
...options,
};
const cacheKey = md5(escape(markup));
const cacheKey = md5(escape(JSON.stringify({ markup, options })));
let html = this.cache_.value(cacheKey);
if (!html) {
html = htmlUtils.sanitizeHtml(markup);
html = htmlUtils.sanitizeHtml(markup, {
allowedFilePrefixes: options.allowedFilePrefixes,
});
html = htmlUtils.processImageTags(html, (data: any) => {
if (!data.src) return null;
@@ -128,6 +136,7 @@ export default class HtmlToHtml {
ResourceModel: this.ResourceModel_,
postMessageSyntax: options.postMessageSyntax,
enableLongPress: options.enableLongPress,
...options.plugins?.link_open,
});
if (!r.html) return null;
+4
View File
@@ -192,6 +192,10 @@ export interface RuleOptions {
vendorDir?: string;
itemIdToUrl?: ItemIdToUrlHandler;
// Passed to the HTML sanitizer: Allows file:// URLs with
// paths with the included prefixes.
allowedFilePrefixes?: string[];
platformName?: string;
}
@@ -34,7 +34,13 @@ export default {
// So the sanitizeHtml function must handle this kind of non-valid HTML.
if (!sanitizedContent) {
sanitizedContent = htmlUtils.sanitizeHtml(token.content, { addNoMdConvClass: true });
sanitizedContent = htmlUtils.sanitizeHtml(
token.content,
{
addNoMdConvClass: true,
allowedFilePrefixes: ruleOptions.allowedFilePrefixes,
},
);
}
token.content = sanitizedContent;
+3 -1
View File
@@ -189,10 +189,12 @@ class HtmlUtils {
// If true, adds a "jop-noMdConv" class to all the tags.
// It can be used afterwards to restore HTML tags in Markdown.
addNoMdConvClass: false,
allowedFilePrefixes: [],
...options,
};
// If options.allowedFilePrefixes is `undefined`, default to [].
options.allowedFilePrefixes ??= [];
const output: string[] = [];
const tagStack: string[] = [];