From f9b1a32ae7f6318735ad1d074524d8e3def9ceed Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Sat, 12 Oct 2024 12:08:37 +0100 Subject: [PATCH 01/17] Tools: Update script to test plugins --- packages/app-desktop/testPluginDemo.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/app-desktop/testPluginDemo.sh b/packages/app-desktop/testPluginDemo.sh index 2037f36a6..e2e3ab95b 100755 --- a/packages/app-desktop/testPluginDemo.sh +++ b/packages/app-desktop/testPluginDemo.sh @@ -16,9 +16,13 @@ if [[ $NEED_COMPILING == 1 ]]; then echo "Copying from: $PLUGIN_PATH" echo "To: $TEMP_PLUGIN_PATH" - rsync -a --delete "$PLUGIN_PATH/" "$TEMP_PLUGIN_PATH/" + rsync -a --exclude "cache/" --exclude "node_modules" --delete "$PLUGIN_PATH/" "$TEMP_PLUGIN_PATH/" - NODE_OPTIONS=--openssl-legacy-provider npm install --prefix="$TEMP_PLUGIN_PATH" && yarn start --dev-plugins "$TEMP_PLUGIN_PATH" + cd "$TEMP_PLUGIN_PATH/" + NODE_OPTIONS=--openssl-legacy-provider npm install + + cd "$SCRIPT_DIR" + yarn start --dev-plugins "$TEMP_PLUGIN_PATH" else yarn start --dev-plugins "$PLUGIN_PATH" fi From 591c458a4f1364be11489af66849c7463e383cf7 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Tue, 15 Oct 2024 08:37:15 -0700 Subject: [PATCH 02/17] Desktop: Security: Improve KaTeX error handling (#11207) --- packages/app-cli/tests/MdToHtml.ts | 8 ++++++++ packages/renderer/MdToHtml/rules/katex.ts | 14 ++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/app-cli/tests/MdToHtml.ts b/packages/app-cli/tests/MdToHtml.ts index 1c2615bf6..19419a329 100644 --- a/packages/app-cli/tests/MdToHtml.ts +++ b/packages/app-cli/tests/MdToHtml.ts @@ -352,4 +352,12 @@ describe('MdToHtml', () => { expect(html).toContain('Inline'); expect(html).toContain('Block'); }); + + it('should sanitize KaTeX errors', async () => { + const markdown = '$\\a$'; + const renderResult = await newTestMdToHtml().render(markdown, null, { bodyOnly: true }); + + // Should not contain the HTML in unsanitized form + expect(renderResult.html).not.toContain(''); + }); }); diff --git a/packages/renderer/MdToHtml/rules/katex.ts b/packages/renderer/MdToHtml/rules/katex.ts index a3c6b1823..98dc6bb14 100644 --- a/packages/renderer/MdToHtml/rules/katex.ts +++ b/packages/renderer/MdToHtml/rules/katex.ts @@ -310,12 +310,6 @@ function renderToStringWithCache(latex: string, katexOptions: any) { } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied -function renderKatexError(latex: string, error: any): string { - console.error('Katex error for:', latex, error); - return `
${error.message}
`; -} - export default { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied plugin: function(markdownIt: any, options: RuleOptions) { @@ -329,6 +323,10 @@ export default { katexOptions.macros = options.context.userData.__katex.macros; katexOptions.trust = true; + const renderKatexError = (error: Error): string => { + return `
${markdownIt.utils.escapeHtml(error.message)}
`; + }; + // set KaTeX as the renderer for markdown-it-simplemath const katexInline = function(latex: string) { katexOptions.displayMode = false; @@ -336,7 +334,7 @@ export default { try { outputHtml = renderToStringWithCache(latex, katexOptions); } catch (error) { - outputHtml = renderKatexError(latex, error); + outputHtml = renderKatexError(error); } return `${markdownIt.utils.escapeHtml(latex)}${outputHtml}`; }; @@ -353,7 +351,7 @@ export default { try { outputHtml = renderToStringWithCache(latex, katexOptions); } catch (error) { - outputHtml = renderKatexError(latex, error); + outputHtml = renderKatexError(error); } return `
${markdownIt.utils.escapeHtml(latex)}
${outputHtml}
`; From 9d8cd1d707f212a092e4111561ccffe5bf4da815 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Tue, 15 Oct 2024 08:38:33 -0700 Subject: [PATCH 03/17] Desktop: Security: Open more target="_blank" links in a browser (#11212) --- packages/app-desktop/ElectronAppWrapper.ts | 39 +++++++-- .../integration-tests/main.spec.ts | 87 ++++++++++--------- 2 files changed, 76 insertions(+), 50 deletions(-) diff --git a/packages/app-desktop/ElectronAppWrapper.ts b/packages/app-desktop/ElectronAppWrapper.ts index ba271a401..961f4b02a 100644 --- a/packages/app-desktop/ElectronAppWrapper.ts +++ b/packages/app-desktop/ElectronAppWrapper.ts @@ -5,7 +5,7 @@ import type ShimType from '@joplin/lib/shim'; const shim: typeof ShimType = require('@joplin/lib/shim').default; import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils'; -import { BrowserWindow, Tray, screen } from 'electron'; +import { BrowserWindow, Tray, WebContents, screen } from 'electron'; import bridge from './bridge'; const url = require('url'); const path = require('path'); @@ -232,14 +232,35 @@ export default class ElectronAppWrapper { }, 3000); } - // will-frame-navigate is fired by clicking on a link within the BrowserWindow. - this.win_.webContents.on('will-frame-navigate', event => { - // If the link changes the URL of the browser window, - if (event.isMainFrame) { - event.preventDefault(); - void bridge().openExternal(event.url); - } - }); + const addWindowEventHandlers = (webContents: WebContents) => { + // will-frame-navigate is fired by clicking on a link within the BrowserWindow. + webContents.on('will-frame-navigate', event => { + // If the link changes the URL of the browser window, + if (event.isMainFrame) { + event.preventDefault(); + void bridge().openExternal(event.url); + } + }); + + // Override calls to window.open and links with target="_blank": Open most in a browser instead + // of Electron: + webContents.setWindowOpenHandler((event) => { + if (event.url === 'about:blank') { + // Script-controlled pages: Used for opening notes in new windows + return { + action: 'allow', + }; + } else if (event.url.match(/^https?:\/\//)) { + void bridge().openExternal(event.url); + } + return { action: 'deny' }; + }); + + webContents.on('did-create-window', (event) => { + addWindowEventHandlers(event.webContents); + }); + }; + addWindowEventHandlers(this.win_.webContents); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied this.win_.on('close', (event: any) => { diff --git a/packages/app-desktop/integration-tests/main.spec.ts b/packages/app-desktop/integration-tests/main.spec.ts index 736059b25..4b6f1690b 100644 --- a/packages/app-desktop/integration-tests/main.spec.ts +++ b/packages/app-desktop/integration-tests/main.spec.ts @@ -136,50 +136,55 @@ test.describe('main', () => { expect(fullSize[0] / resizedSize[0]).toBeCloseTo(fullSize[1] / resizedSize[1]); }); - test('clicking on an external link should try to launch a browser', async ({ electronApp, mainWindow }) => { - const mainScreen = new MainScreen(mainWindow); - await mainScreen.waitFor(); + for (const target of ['', '_blank']) { + test(`clicking on an external link with target=${JSON.stringify(target)} should try to launch a browser`, async ({ electronApp, mainWindow }) => { + const mainScreen = new MainScreen(mainWindow); + await mainScreen.waitFor(); - // Mock openExternal - const nextExternalUrlPromise = electronApp.evaluate(({ shell }) => { - return new Promise(resolve => { - const openExternal = async (url: string) => { - resolve(url); - }; - shell.openExternal = openExternal; + // Mock openExternal + const nextExternalUrlPromise = electronApp.evaluate(({ shell }) => { + return new Promise(resolve => { + const openExternal = async (url: string) => { + resolve(url); + }; + shell.openExternal = openExternal; + }); }); + + // Create a test link + const testLinkTitle = 'This is a test link!'; + const linkHref = 'https://joplinapp.org/'; + + await mainWindow.evaluate(({ testLinkTitle, linkHref, target }) => { + const testLink = document.createElement('a'); + testLink.textContent = testLinkTitle; + testLink.onclick = () => { + // We need to navigate by setting location.href -- clicking on a link + // directly within the main window (i.e. not in a PDF viewer) doesn't + // navigate. + location.href = linkHref; + }; + testLink.href = '#'; + + // Display on top of everything + testLink.style.zIndex = '99999'; + testLink.style.position = 'fixed'; + testLink.style.top = '0'; + testLink.style.left = '0'; + if (target) { + testLink.target = target; + } + + document.body.appendChild(testLink); + }, { testLinkTitle, linkHref, target }); + + const testLink = mainWindow.getByText(testLinkTitle); + await expect(testLink).toBeVisible(); + await testLink.click({ noWaitAfter: true }); + + expect(await nextExternalUrlPromise).toBe(linkHref); }); - - // Create a test link - const testLinkTitle = 'This is a test link!'; - const linkHref = 'https://joplinapp.org/'; - - await mainWindow.evaluate(({ testLinkTitle, linkHref }) => { - const testLink = document.createElement('a'); - testLink.textContent = testLinkTitle; - testLink.onclick = () => { - // We need to navigate by setting location.href -- clicking on a link - // directly within the main window (i.e. not in a PDF viewer) doesn't - // navigate. - location.href = linkHref; - }; - testLink.href = '#'; - - // Display on top of everything - testLink.style.zIndex = '99999'; - testLink.style.position = 'fixed'; - testLink.style.top = '0'; - testLink.style.left = '0'; - - document.body.appendChild(testLink); - }, { testLinkTitle, linkHref }); - - const testLink = mainWindow.getByText(testLinkTitle); - await expect(testLink).toBeVisible(); - await testLink.click({ noWaitAfter: true }); - - expect(await nextExternalUrlPromise).toBe(linkHref); - }); + } test('should start in safe mode if profile-dir/force-safe-mode-on-next-start exists', async ({ profileDirectory }) => { await writeFile(join(profileDirectory, 'force-safe-mode-on-next-start'), 'true', 'utf8'); From efb58c5f40368522e96812d5a24c5ad1d5f5053c Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:49:29 -0700 Subject: [PATCH 04/17] Desktop: Fix error screen shown on opening settings when an incompatible plugin is installed (#11223) --- .../app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx b/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx index cf728e6ca..e71d02f0b 100644 --- a/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx +++ b/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx @@ -234,7 +234,7 @@ export default function(props: Props) { return ( - {PluginService.instance().describeIncompatibility(props.manifest)} + {PluginService.instance().describeIncompatibility(item.manifest)} ); From 2c40cec6392d79814d626f7edaec093103194088 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:49:50 -0700 Subject: [PATCH 05/17] Chore: Desktop: Fix incorrect log tag (#11215) --- .../lib/services/keychain/KeychainServiceDriver.electron.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/services/keychain/KeychainServiceDriver.electron.ts b/packages/lib/services/keychain/KeychainServiceDriver.electron.ts index ce5db9648..c6de9337d 100644 --- a/packages/lib/services/keychain/KeychainServiceDriver.electron.ts +++ b/packages/lib/services/keychain/KeychainServiceDriver.electron.ts @@ -4,7 +4,7 @@ import Logger from '@joplin/utils/Logger'; import KvStore from '../KvStore'; import Setting from '../../models/Setting'; -const logger = Logger.create('KeychainServiceDriver.node'); +const logger = Logger.create('KeychainServiceDriver.electron'); const canUseSafeStorage = () => { return !!shim.electronBridge?.()?.safeStorage?.isEncryptionAvailable(); From 41b251d67a8ba861e7652cf24197c3e51c9d7447 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:58:03 -0700 Subject: [PATCH 06/17] Linux: Move keychain support behind an off-by-default feature flag (#11227) --- packages/lib/models/settings/builtInMetadata.ts | 14 ++++++++++++++ packages/lib/services/SettingUtils.ts | 7 +++++++ packages/lib/versionInfo.ts | 16 +++++++++++++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts index e822d3689..0482f4fb4 100644 --- a/packages/lib/models/settings/builtInMetadata.ts +++ b/packages/lib/models/settings/builtInMetadata.ts @@ -1576,6 +1576,20 @@ const builtInMetadata = (Setting: typeof SettingType) => { isGlobal: true, }, + 'featureFlag.linuxKeychain': { + value: false, + type: SettingItemType.Bool, + public: true, + storage: SettingStorage.File, + appTypes: [AppType.Desktop], + label: () => 'Enable keychain support', + description: () => 'This is an experimental setting to enable keychain support on Linux', + show: () => shim.isLinux(), + section: 'general', + isGlobal: true, + advanced: true, + }, + // 'featureFlag.syncAccurateTimestamps': { // value: false, diff --git a/packages/lib/services/SettingUtils.ts b/packages/lib/services/SettingUtils.ts index 131b555d2..4686a27a8 100644 --- a/packages/lib/services/SettingUtils.ts +++ b/packages/lib/services/SettingUtils.ts @@ -34,6 +34,13 @@ export async function loadKeychainServiceAndSettings(keychainServiceDrivers: Key Setting.setKeychainService(KeychainService.instance()); await Setting.load(); + // Using Linux with the keychain has been observed to cause all secure settings to be lost + // on Fedora 40 + GNOME. (This may have been related to running multiple Joplin instances). + // For now, make saving to the keychain opt-in until more feedback is received. + if (shim.isLinux() && !Setting.value('featureFlag.linuxKeychain')) { + KeychainService.instance().readOnly = true; + } + // This is part of the migration to the new sync target info. It needs to be // set as early as possible since it's used to tell if E2EE is enabled, it // contains the master keys, etc. Once it has been set, it becomes a noop diff --git a/packages/lib/versionInfo.ts b/packages/lib/versionInfo.ts index dd9d061bc..037e34085 100644 --- a/packages/lib/versionInfo.ts +++ b/packages/lib/versionInfo.ts @@ -1,9 +1,13 @@ +import Logger from '@joplin/utils/Logger'; import { _ } from './locale'; import Setting from './models/Setting'; import { reg } from './registry'; +import KeychainService from './services/keychain/KeychainService'; import { Plugins } from './services/plugins/PluginService'; import shim from './shim'; +const logger = Logger.create('versionInfo'); + export interface PackageInfo { name: string; version: string; @@ -70,15 +74,21 @@ export default function versionInfo(packageInfo: PackageInfo, plugins: Plugins) copyrightText.replace('YYYY', `${now.getFullYear()}`), ]; + let keychainSupported = false; + try { + // To allow old keys to be read, certain apps allow read-only keychain access: + keychainSupported = Setting.value('keychain.supported') >= 1 && !KeychainService.instance().readOnly; + } catch (error) { + logger.error('Failed to determine if keychain is supported', error); + } + const body = [ _('%s %s (%s, %s)', p.name, p.version, Setting.value('env'), shim.platformName()), '', _('Client ID: %s', Setting.value('clientId')), _('Sync Version: %s', Setting.value('syncVersion')), _('Profile Version: %s', reg.db().version()), - // The portable app temporarily supports read-only keychain access (but disallows - // write). - _('Keychain Supported: %s', (Setting.value('keychain.supported') >= 1 && !shim.isPortable()) ? _('Yes') : _('No')), + _('Keychain Supported: %s', keychainSupported ? _('Yes') : _('No')), ]; if (gitInfo) { From 1ad150c1bf7fa7356d98c4e2b5f9f7eca9a9b665 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Thu, 17 Oct 2024 23:05:46 +0100 Subject: [PATCH 07/17] Desktop release v3.1.19 --- packages/app-desktop/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-desktop/package.json b/packages/app-desktop/package.json index e35dd30e4..df6974b07 100644 --- a/packages/app-desktop/package.json +++ b/packages/app-desktop/package.json @@ -1,6 +1,6 @@ { "name": "@joplin/app-desktop", - "version": "3.1.18", + "version": "3.1.19", "description": "Joplin for Desktop", "main": "main.js", "private": true, From a2069df3e08ff1119bc6b2f9c8472b34bf6107c1 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Thu, 17 Oct 2024 23:14:16 +0100 Subject: [PATCH 08/17] Android 3.1.6 --- packages/app-mobile/android/app/build.gradle | 4 ++-- readme/about/changelog/android.md | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/app-mobile/android/app/build.gradle b/packages/app-mobile/android/app/build.gradle index f736977da..daadc3900 100644 --- a/packages/app-mobile/android/app/build.gradle +++ b/packages/app-mobile/android/app/build.gradle @@ -79,8 +79,8 @@ android { applicationId "net.cozic.joplin" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 2097753 - versionName "3.1.5" + versionCode 2097754 + versionName "3.1.6" ndk { abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64" } diff --git a/readme/about/changelog/android.md b/readme/about/changelog/android.md index 4f141cb3e..b0c010cc3 100644 --- a/readme/about/changelog/android.md +++ b/readme/about/changelog/android.md @@ -1,5 +1,15 @@ # Joplin Android Changelog +## [android-v3.1.6](https://github.com/laurent22/joplin/releases/tag/android-v3.1.6) (Pre-release) - 2024-10-17T22:13:06Z + +- Improved: Downgrade CodeMirror packages to fix various Android regressions (#11170 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Improved: Plugins: Name webview root attribute so that it can be styled (75b8caf) +- Improved: Updated packages @react-native/babel-preset (v0.74.85), @react-native/metro-config (v0.74.85), glob (v10.4.5), katex (v0.16.11), react-native-document-picker (v9.3.0), react-native-safe-area-context (v4.10.7), stream (v0.0.3) +- Fixed: Fix automatic resource download mode (#11144) (#11134 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Fix incorrect list switching behavior (#11137) (#11135 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Fix new note/edit buttons only work if pressed quickly (#11185) (#11183 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Fix regression: Search screen not hidden when cached for search result navigation (#11131) (#11130 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) + ## [android-v3.1.5](https://github.com/laurent22/joplin/releases/tag/android-v3.1.5) (Pre-release) - 2024-10-11T22:11:20Z - Improved: Downgrade CodeMirror packages to fix various Android regressions (#11170 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) From 0b3f6a268e737dd68704b97a10e0b67eecdc38cf Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Thu, 17 Oct 2024 23:17:57 +0100 Subject: [PATCH 09/17] iOS 13.1.6 --- .../ios/Joplin.xcodeproj/project.pbxproj | 16 +++++----- readme/about/changelog/ios.md | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/app-mobile/ios/Joplin.xcodeproj/project.pbxproj b/packages/app-mobile/ios/Joplin.xcodeproj/project.pbxproj index 5c6a2bde0..9c66cbe42 100644 --- a/packages/app-mobile/ios/Joplin.xcodeproj/project.pbxproj +++ b/packages/app-mobile/ios/Joplin.xcodeproj/project.pbxproj @@ -505,13 +505,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = A9BXAFS6CT; ENABLE_BITCODE = NO; INFOPLIST_FILE = Joplin/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 13.1.5; + MARKETING_VERSION = 13.1.6; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -536,12 +536,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = A9BXAFS6CT; INFOPLIST_FILE = Joplin/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 13.1.5; + MARKETING_VERSION = 13.1.6; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -726,14 +726,14 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = A9BXAFS6CT; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 13.1.5; + MARKETING_VERSION = 13.1.6; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( @@ -764,14 +764,14 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = A9BXAFS6CT; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 13.1.5; + MARKETING_VERSION = 13.1.6; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( "$(inherited)", diff --git a/readme/about/changelog/ios.md b/readme/about/changelog/ios.md index b901d3c5b..02fe677bd 100644 --- a/readme/about/changelog/ios.md +++ b/readme/about/changelog/ios.md @@ -1,5 +1,37 @@ # Joplin iOS Changelog +## [ios-v13.1.6](https://github.com/laurent22/joplin/releases/tag/ios-v13.1.6) - 2024-10-17T22:16:20Z + +- Improved: Added feature flag to disable sync lock support (#10925) (#10407) +- Improved: Automatically detect and use operating system theme by default (5beb80b) +- Improved: Downgrade CodeMirror packages to fix various Android regressions (#11170 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Improved: Make feature flags advanced settings by default (700ffa2) +- Improved: Make pressing "back" navigate to the previous note after following a link (#11086) (#11082 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Improved: Plugins: Name webview root attribute so that it can be styled (75b8caf) +- Improved: Scroll dropdown to selected value when first opened (#11091 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Improved: Show loading indicator while loading search results (#11104 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Improved: Support permanent note deletion on mobile (#10786) (#10763 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Improved: Updated packages @bam.tech/react-native-image-resizer (v3.0.10), @js-draw/material-icons (v1.20.3), @react-native-clipboard/clipboard (v1.14.1), @react-native-community/datetimepicker (v8.0.1), @react-native/babel-preset (v0.74.85), @react-native/metro-config (v0.74.85), @rollup/plugin-commonjs (v25.0.8), @rollup/plugin-replace (v5.0.7), async-mutex (v0.5.0), dayjs (v1.11.11), glob (v10.4.5), js-draw (v1.20.3), jsdom (v24.1.0), katex (v0.16.11), markdown-it-ins (v4), markdown-it-sup (v2), react, react-native-device-info (v10.14.0), react-native-document-picker (v9.3.0), react-native-localize (v3.1.0), react-native-safe-area-context (v4.10.7), react-native-share (v10.2.1), react-native-webview (v13.8.7), react-native-zip-archive (v6.1.2), sass (v1.77.6), sharp (v0.33.4), stream (v0.0.3), tesseract.js (v5.1.0), turndown (v7.2.0) +- Improved: Upgrade CodeMirror packages (#11034 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Improved: Use fade animation for edit link dialog (#11090 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Accessibility: Fix sidebar broken in right-to-left mode, improve screen reader accessibility (#11056) (#11028 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Decrypt master keys only as needed (#10990) (#10856 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Delete revisions on the sync target when deleted locally (#11035) (#11017 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Drawing: Fix clicking "cancel" after starting a new drawing in editing mode creates an empty resource (#10986 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Fix "Enable auto-updates" enabled by default and visible on unsupported platforms (#10897) (#10896 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Fix BMP image rendering in the Markdown viewer (#10915) (#10914 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Fix automatic resource download mode (#11144) (#11134 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Fix incorrect list switching behavior (#11137) (#11135 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Fix new note/edit buttons only work if pressed quickly (#11185) (#11183 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Fix regression: Search screen not hidden when cached for search result navigation (#11131) (#11130 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Fix toolbar overflow menu is invisible (#10871) (#10867 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Fix unable to change incorrect decryption password if the same as the master password (#11026 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Fixed italic support in Fountain documents (5fdd088) +- Fixed: Improve performance when there are many selected items (#11067) (#11065 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Markdown editor: Fix toggling bulleted lists when items start with asterisks (#10902) (#10891 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: Move accessibility focus to the first note action menu item on open (#11031) (#10253 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) +- Fixed: WebDAV synchronisation not working because of URL encoding differences (#11076) (#10608 by [@pedr](https://github.com/pedr)) + ## [ios-v13.1.5](https://github.com/laurent22/joplin/releases/tag/ios-v13.1.5) - 2024-10-11T22:29:29Z - Improved: Added feature flag to disable sync lock support (#10925) (#10407) From 81993628abdce57e3af3c083b07e7186b7cc3e12 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Tue, 22 Oct 2024 11:51:50 +0100 Subject: [PATCH 10/17] Desktop release v3.1.20 --- packages/app-desktop/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-desktop/package.json b/packages/app-desktop/package.json index df6974b07..cadb1781a 100644 --- a/packages/app-desktop/package.json +++ b/packages/app-desktop/package.json @@ -1,6 +1,6 @@ { "name": "@joplin/app-desktop", - "version": "3.1.19", + "version": "3.1.20", "description": "Joplin for Desktop", "main": "main.js", "private": true, From f1e5ab82554d663fb1a873e43280db295c6997cb Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Sat, 26 Oct 2024 13:08:51 -0700 Subject: [PATCH 11/17] Desktop: Re-enable media with local file URLs in the note viewer (#11244) --- packages/app-desktop/app.ts | 6 ++ packages/app-desktop/gui/NoteTextViewer.tsx | 13 ++-- .../app-desktop/gui/note-viewer/index.html | 18 +++++ .../handleCustomProtocols.test.ts | 52 ++++++++++++--- .../customProtocols/handleCustomProtocols.ts | 66 +++++++++++++++++-- .../lib/models/settings/builtInMetadata.ts | 12 ++++ packages/tools/cspell/dictionary4.txt | 1 + readme/dev/spec/note_viewer_isolation.md | 9 ++- 8 files changed, 156 insertions(+), 21 deletions(-) diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 311095269..0a26f8aef 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -127,6 +127,12 @@ class Application extends BaseApplication { bridge().setLocale(Setting.value('locale')); } + if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'renderer.fileUrls' || action.type === 'SETTING_UPDATE_ALL') { + bridge().electronApp().getCustomProtocolHandler().setMediaAccessEnabled( + Setting.value('renderer.fileUrls'), + ); + } + if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'showTrayIcon' || action.type === 'SETTING_UPDATE_ALL') { this.updateTray(); } diff --git a/packages/app-desktop/gui/NoteTextViewer.tsx b/packages/app-desktop/gui/NoteTextViewer.tsx index debb4451e..acc789227 100644 --- a/packages/app-desktop/gui/NoteTextViewer.tsx +++ b/packages/app-desktop/gui/NoteTextViewer.tsx @@ -29,6 +29,7 @@ export default class NoteTextViewerComponent extends React.Component private webviewRef_: React.RefObject; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied private webviewListeners_: any = null; + private removePluginAssetsCallback_: RemovePluginAssetsCallback|null = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -110,7 +111,7 @@ export default class NoteTextViewerComponent extends React.Component window.addEventListener('message', this.webview_message); } - public destroyWebview() { + private destroyWebview() { const wv = this.webviewRef_.current; if (!wv || !this.initialized_) return; @@ -194,14 +195,13 @@ export default class NoteTextViewerComponent extends React.Component } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public setHtml(html: string, options: SetHtmlOptions) { + const protocolHandler = bridge().electronApp().getCustomProtocolHandler(); + // Grant & remove asset access. if (options.pluginAssets) { this.removePluginAssetsCallback_?.(); - const protocolHandler = bridge().electronApp().getCustomProtocolHandler(); - const pluginAssetPaths: string[] = options.pluginAssets.map((asset) => asset.path); const assetAccesses = pluginAssetPaths.map( path => protocolHandler.allowReadAccessToFile(path), @@ -216,7 +216,10 @@ export default class NoteTextViewerComponent extends React.Component }; } - this.send('setHtml', html, options); + this.send('setHtml', html, { + ...options, + mediaAccessKey: protocolHandler.getMediaAccessKey(), + }); } // ---------------------------------------------------------------- diff --git a/packages/app-desktop/gui/note-viewer/index.html b/packages/app-desktop/gui/note-viewer/index.html index 55531e1fd..6f016f7df 100644 --- a/packages/app-desktop/gui/note-viewer/index.html +++ b/packages/app-desktop/gui/note-viewer/index.html @@ -377,6 +377,20 @@ contentElement.scrollTop = scrollTop; } + const rewriteFileUrls = (accessKey) => { + if (!accessKey) return; + + // To allow accessing local files from the viewer's non-file URL, file:// URLs are re-written + // to joplin-content:// URLs: + const mediaElements = document.querySelectorAll('video[src], audio[src], source[src], img[src]'); + for (const element of mediaElements) { + if (element.src?.startsWith('file:')) { + const newUrl = element.src.replace(/^file:\/\//, 'joplin-content://file-media/'); + element.src = `${newUrl}?access-key=${accessKey}`; + } + } + }; + ipc.setHtml = (event) => { const html = event.html; @@ -388,6 +402,10 @@ contentElement.innerHTML = html; + if (html.includes('file://')) { + rewriteFileUrls(event.options.mediaAccessKey); + } + scrollmap.create(event.options.markupLineCount); if (typeof event.options.percent !== 'number') { restorePercentScroll(); // First, a quick treatment is applied. diff --git a/packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.ts b/packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.ts index 48d34ee36..a5a488df4 100644 --- a/packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.ts +++ b/packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.ts @@ -42,21 +42,29 @@ const setUpProtocolHandler = () => { return { protocolHandler, onRequestListener }; }; +interface ExpectBlockedOptions { + host?: string; +} + // Although none of the paths in this test suite point to real files, file paths must be in // a certain format on Windows to avoid invalid path exceptions. const toPlatformPath = (path: string) => process.platform === 'win32' ? `C:/${path}` : path; -const expectPathToBeBlocked = async (onRequestListener: ProtocolHandler, filePath: string) => { - const url = `joplin-content://note-viewer/${toPlatformPath(filePath)}`; - - await expect( - async () => await onRequestListener(new Request(url)), - ).rejects.toThrowError('Read access not granted for URL'); +const toAccessUrl = (path: string, { host = 'note-viewer' }: ExpectBlockedOptions = {}) => { + return `joplin-content://${host}/${toPlatformPath(path)}`; }; -const expectPathToBeUnblocked = async (onRequestListener: ProtocolHandler, filePath: string) => { +const expectPathToBeBlocked = async (onRequestListener: ProtocolHandler, filePath: string, options?: ExpectBlockedOptions) => { + const url = toAccessUrl(filePath, options); + await expect( + async () => await onRequestListener(new Request(url)), + ).rejects.toThrow(/Read access not granted for URL|Invalid or missing media access key|Media access denied/); +}; + +const expectPathToBeUnblocked = async (onRequestListener: ProtocolHandler, filePath: string, options?: ExpectBlockedOptions) => { + const url = toAccessUrl(filePath, options); const handleRequestResult = await onRequestListener( - new Request(`joplin-content://note-viewer/${toPlatformPath(filePath)}`), + new Request(url), ); expect(handleRequestResult.body).toBeTruthy(); }; @@ -107,6 +115,34 @@ describe('handleCustomProtocols', () => { await expectPathToBeBlocked(onRequestListener, '/test/path/a.txt'); }); + test('should only allow access to file-media/ URLs when given the correct access key', async () => { + const { protocolHandler, onRequestListener } = setUpProtocolHandler(); + const expectBlocked = (path: string) => { + return expectPathToBeBlocked(onRequestListener, path, { host: 'file-media' }); + }; + const expectUnblocked = (path: string) => { + return expectPathToBeUnblocked(onRequestListener, path, { host: 'file-media' }); + }; + + fetchMock.mockImplementation(async (_url: string) => { + return new Response('', { headers: { 'Content-Type': 'image/jpeg' } }); + }); + + + const testPath = join(supportDir, 'photo.jpg'); + await expectBlocked(testPath); + await expectBlocked(`${testPath}?access-key=wrongKey`); + await expectBlocked(`${testPath}?access-key=false`); + + protocolHandler.setMediaAccessEnabled(true); + const key = protocolHandler.getMediaAccessKey(); + await expectUnblocked(`${testPath}?access-key=${key}`); + await expectBlocked(`${testPath}?access-key=null`); + protocolHandler.setMediaAccessEnabled(false); + + await expectBlocked(`${testPath}?access-key=${key}`); + }); + test('should allow requesting part of a file', async () => { const { protocolHandler, onRequestListener } = setUpProtocolHandler(); diff --git a/packages/app-desktop/utils/customProtocols/handleCustomProtocols.ts b/packages/app-desktop/utils/customProtocols/handleCustomProtocols.ts index 9cd80182f..354be211c 100644 --- a/packages/app-desktop/utils/customProtocols/handleCustomProtocols.ts +++ b/packages/app-desktop/utils/customProtocols/handleCustomProtocols.ts @@ -7,10 +7,20 @@ import { LoggerWrapper } from '@joplin/utils/Logger'; import * as fs from 'fs-extra'; import { createReadStream } from 'fs'; import { fromFilename } from '@joplin/lib/mime-utils'; +import { createSecureRandom } from '@joplin/lib/uuid'; + +export interface AccessController { + remove(): void; +} export interface CustomProtocolHandler { + // note-viewer/ URLs allowReadAccessToDirectory(path: string): void; - allowReadAccessToFile(path: string): { remove(): void }; + allowReadAccessToFile(path: string): AccessController; + + // file-media/ URLs + setMediaAccessEnabled(enabled: boolean): void; + getMediaAccessKey(): string; } @@ -130,8 +140,11 @@ const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler => debug: () => {}, }; + // Allow-listed files/directories for joplin-content://note-viewer/ const readableDirectories: string[] = []; const readableFiles = new Map(); + // Access for joplin-content://file-media/ + let mediaAccessKey: string|false = false; // See also the protocol.handle example: https://www.electronjs.org/docs/latest/api/protocol#protocolhandlescheme-handler protocol.handle(contentProtocolName, async request => { @@ -147,10 +160,9 @@ const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler => pathname = resolve(appBundleDirectory, pathname); - const allowedHosts = ['note-viewer']; - let canRead = false; - if (allowedHosts.includes(host)) { + let mediaOnly = true; + if (host === 'note-viewer') { if (readableFiles.has(pathname)) { canRead = true; } else { @@ -161,6 +173,20 @@ const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler => } } } + + mediaOnly = false; + } else if (host === 'file-media') { + if (!mediaAccessKey) { + throw new Error('Media access denied. This must be enabled with .setMediaAccessEnabled'); + } + + canRead = true; + mediaOnly = true; + + const accessKey = url.searchParams.get('access-key'); + if (accessKey !== mediaAccessKey) { + throw new Error(`Invalid or missing media access key (was ${accessKey}). An allow-listed ?access-key= parameter must be provided.`); + } } else { throw new Error(`Invalid URL ${request.url}`); } @@ -173,12 +199,26 @@ const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler => logger.debug('protocol handler: Fetch file URL', asFileUrl); const rangeHeader = request.headers.get('Range'); + let response; if (!rangeHeader) { - const response = await net.fetch(asFileUrl); - return response; + response = await net.fetch(asFileUrl); } else { - return handleRangeRequest(request, pathname); + response = await handleRangeRequest(request, pathname); } + + if (mediaOnly) { + // Tells the browser to avoid MIME confusion attacks. See + // https://blog.mozilla.org/security/2016/08/26/mitigating-mime-confusion-attacks-in-firefox/ + response.headers.set('X-Content-Type-Options', 'nosniff'); + + // This is an extra check to prevent loading text/html and arbitrary non-media content from the URL. + const contentType = response.headers.get('Content-Type'); + if (!contentType || !contentType.match(/^(image|video|audio)\//)) { + throw new Error(`Attempted to access non-media file from ${request.url}, which is media-only. Content type was ${contentType}.`); + } + } + + return response; }); const appBundleDirectory = dirname(dirname(__dirname)); @@ -210,6 +250,18 @@ const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler => }, }; }, + setMediaAccessEnabled: (enabled: boolean) => { + if (enabled) { + mediaAccessKey ||= createSecureRandom(); + } else { + mediaAccessKey = false; + } + }, + // Allows access to all local media files, provided a matching ?access-key= is added + // to the request URL. + getMediaAccessKey: () => { + return mediaAccessKey || null; + }, }; }; diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts index 0482f4fb4..9f35bf956 100644 --- a/packages/lib/models/settings/builtInMetadata.ts +++ b/packages/lib/models/settings/builtInMetadata.ts @@ -926,6 +926,18 @@ const builtInMetadata = (Setting: typeof SettingType) => { 'markdown.plugin.insert': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ++insert++ syntax')}${wysiwygYes}` }, 'markdown.plugin.multitable': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable multimarkdown table extension')}${wysiwygNo}` }, + // For now, applies only to the Markdown viewer + 'renderer.fileUrls': { + storage: SettingStorage.File, + isGlobal: true, + value: false, + type: SettingItemType.Bool, + section: 'markdownPlugins', + public: true, + appTypes: [AppType.Desktop], + label: () => `${_('Enable file:// URLs for images and videos')}${wysiwygYes}`, + }, + // Tray icon (called AppIndicator) doesn't work in Ubuntu // http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html // Might be fixed in Electron 18.x but no non-beta release yet. So for now diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index e67f10bb0..10849b8ab 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -132,3 +132,4 @@ Famegear rcompare tabindex Backblaze +nosniff diff --git a/readme/dev/spec/note_viewer_isolation.md b/readme/dev/spec/note_viewer_isolation.md index 742858418..ab86d9138 100644 --- a/readme/dev/spec/note_viewer_isolation.md +++ b/readme/dev/spec/note_viewer_isolation.md @@ -31,7 +31,7 @@ Here's an example: - Joplin checks to make sure the `joplin-content://` protocol has access to `/home/user/.config/joplin-desktop/path/here.css`. If it does, it fetches and returns the file. -## `joplin-content://` only has access to specific directories +## `joplin-content://note-viewer/` only has access to specific directories When `handleCustomProtocols` creates a handler for the `joplin-content://` protocol, it returns an object that allows certain directories to be marked as readable. @@ -41,6 +41,13 @@ By default, the list of readable directories includes: - The resource directory - The profile directory +## `joplin-content://file-media/` can only load specific file types + +To allow images and videos with `file://` URLs, Joplin maps `file://` URIs to `joplin-content://file-media/`. The `file-media/` host has the following restrictions: +- Only files with certain extensions/content-types can be loaded. + - For example, `text/html` is disallowed but `image/png` is allowed. +- A valid `?access-key=<...>` parameter must be provided with the request. + - A new access key is created for each render and old access keys are revoked. ## Why not the [`sandbox`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox) property? From f7f4a50d35ac84e3600e97ae2c5c553c39ed7abf Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:09:23 -0700 Subject: [PATCH 12/17] Desktop: Custom CSS: Add cm-listItem class to lines with list items, don't add region start/end markers for items that are always single-line (#11291) --- .../CodeMirror/markdown/decoratorExtension.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/editor/CodeMirror/markdown/decoratorExtension.ts b/packages/editor/CodeMirror/markdown/decoratorExtension.ts index a9627f79a..7f46ed2ad 100644 --- a/packages/editor/CodeMirror/markdown/decoratorExtension.ts +++ b/packages/editor/CodeMirror/markdown/decoratorExtension.ts @@ -84,6 +84,18 @@ const tableDelimiterDecoration = Decoration.line({ attributes: { class: 'cm-tableDelimiter' }, }); +const orderedListDecoration = Decoration.line({ + attributes: { class: 'cm-orderedList' }, +}); + +const unorderedListDecoration = Decoration.line({ + attributes: { class: 'cm-unorderedList' }, +}); + +const listItemDecoration = Decoration.line({ + attributes: { class: 'cm-listItem' }, +}); + const horizontalRuleDecoration = Decoration.mark({ attributes: { class: 'cm-hr' }, }); @@ -97,6 +109,10 @@ const nodeNameToLineDecoration: Record = { 'CodeBlock': codeBlockDecoration, 'BlockMath': mathBlockDecoration, 'Blockquote': blockQuoteDecoration, + 'OrderedList': orderedListDecoration, + 'BulletList': unorderedListDecoration, + + 'ListItem': listItemDecoration, 'SetextHeading1': header1LineDecoration, 'ATXHeading1': header1LineDecoration, @@ -122,6 +138,14 @@ const nodeNameToMarkDecoration: Record = { 'TaskMarker': taskMarkerDecoration, }; +const multilineNodes = { + 'FencedCode': true, + 'CodeBlock': true, + 'BlockMath': true, + 'Blockquote': true, + 'OrderedList': true, + 'BulletList': true, +}; type DecorationDescription = { pos: number; length: number; decoration: Decoration }; @@ -179,8 +203,8 @@ const computeDecorations = (view: EditorView) => { addDecorationToRange(viewFrom, viewTo, decoration); } - // Only block decorations will have differing first and last lines - if (blockDecorated) { + // Only certain block decorations will have differing first and last lines + if (blockDecorated && multilineNodes.hasOwnProperty(node.name)) { // Allow different styles for the first, last lines in a block. if (viewFrom === node.from) { addDecorationToLines(viewFrom, viewFrom, regionStartDecoration); From 297446588299eeb78e95324b711207c24fca91f2 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:09:37 -0700 Subject: [PATCH 13/17] Desktop: Resolves #11279: Remove left/right edge margin around editor content when disabled in settings (#11290) --- .../NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.tsx | 2 ++ packages/editor/CodeMirror/theme.ts | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.tsx index 14cf9e219..19a88ad44 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.tsx @@ -378,6 +378,8 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef { // be at least this specific. const selectionBackgroundSelector = '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground'; + // Matches the editor only when there are no gutters (e.g. line numbers) added by + // plugins + const editorNoGuttersSelector = '&:not(:has(> .cm-scroller > .cm-gutters))'; + const baseHeadingStyle = { fontWeight: 'bold', fontFamily: theme.fontFamily, @@ -180,6 +184,12 @@ const createTheme = (theme: EditorTheme): Extension[] => { marginRight: 'auto', } : undefined, + // Allows editor content to be left-aligned with the toolbar on desktop. + // See https://github.com/laurent22/joplin/issues/11279 + [`${editorNoGuttersSelector} .cm-line`]: theme.isDesktop ? { + paddingLeft: 0, + } : undefined, + // Override the default URL style when the URL is within a link '& .tok-url.tok-link, & .tok-link.tok-meta, & .tok-link.tok-string': { opacity: 0.6, From 612d72d7658a87dde1838770c8c129db8f594c47 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:09:59 -0700 Subject: [PATCH 14/17] Desktop: Fixes #11274: Fix content dropped into the Markdown editor is missing a cursor preview or dropped at the wrong location (#11289) --- .../CodeMirror/v6/useEditorCommands.ts | 23 +++++++++----- .../app-desktop/gui/NoteEditor/utils/types.ts | 16 ++++++++++ .../gui/NoteEditor/utils/useDropHandler.ts | 30 +++++++++++++------ packages/editor/CodeMirror/createEditor.ts | 3 ++ packages/editor/types.ts | 1 + 5 files changed, 57 insertions(+), 16 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts index ab2e7a323..e6983409d 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts @@ -1,10 +1,10 @@ import { RefObject, useMemo } from 'react'; -import { CommandValue } from '../../../utils/types'; +import { CommandValue, DropCommandValue } from '../../../utils/types'; import { commandAttachFileToBody } from '../../../utils/resourceHandling'; import { _ } from '@joplin/lib/locale'; import dialogs from '../../../../dialogs'; -import { EditorCommandType } from '@joplin/editor/types'; +import { EditorCommandType, UserEventSource } from '@joplin/editor/types'; import Logger from '@joplin/utils/Logger'; import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl'; import { MarkupLanguage } from '@joplin/renderer'; @@ -38,13 +38,22 @@ const useEditorCommands = (props: Props) => { }; return { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - dropItems: async (cmd: any) => { + dropItems: async (cmd: DropCommandValue) => { + let pos = cmd.pos && editorRef.current.editor.posAtCoords({ x: cmd.pos.clientX, y: cmd.pos.clientY }); if (cmd.type === 'notes') { - editorRef.current.insertText(cmd.markdownTags.join('\n')); + const text = cmd.markdownTags.join('\n'); + if ((pos ?? null) !== null) { + editorRef.current.select(pos, pos); + } + + editorRef.current.insertText(text, UserEventSource.Drop); } else if (cmd.type === 'files') { - const pos = props.selectionRange.from; - const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, { createFileURL: !!cmd.createFileURL, position: pos, markupLanguage: props.contentMarkupLanguage }); + pos ??= props.selectionRange.from; + const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, { + createFileURL: !!cmd.createFileURL, + position: pos, + markupLanguage: props.contentMarkupLanguage, + }); editorRef.current.updateBody(newBody); } else { logger.warn('CodeMirror: unsupported drop item: ', cmd); diff --git a/packages/app-desktop/gui/NoteEditor/utils/types.ts b/packages/app-desktop/gui/NoteEditor/utils/types.ts index 1f261661a..7d17bf460 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/types.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/types.ts @@ -252,3 +252,19 @@ export interface CommandValue { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied value?: any; // For TinyMCE only } + +type DropCommandBase = { + pos: { + clientX: number; + clientY: number; + }|undefined; +}; + +export type DropCommandValue = ({ + type: 'notes'; + markdownTags: string[]; +}|{ + type: 'files'; + paths: string[]; + createFileURL: boolean; +}) & DropCommandBase; diff --git a/packages/app-desktop/gui/NoteEditor/utils/useDropHandler.ts b/packages/app-desktop/gui/NoteEditor/utils/useDropHandler.ts index cbf415416..2b292d9d3 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useDropHandler.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useDropHandler.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react'; import Note from '@joplin/lib/models/Note'; import { DragEvent as ReactDragEvent } from 'react'; +import { DropCommandValue } from './types'; interface HookDependencies { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -19,6 +20,11 @@ export default function useDropHandler(dependencies: HookDependencies): DropHand const dt = event.dataTransfer; const createFileURL = event.altKey; + const eventPosition = { + clientX: event.clientX, + clientY: event.clientY, + }; + if (dt.types.indexOf('text/x-jop-note-ids') >= 0) { const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids')); @@ -29,12 +35,15 @@ export default function useDropHandler(dependencies: HookDependencies): DropHand noteMarkdownTags.push(Note.markdownTag(note)); } + const props: DropCommandValue = { + type: 'notes', + pos: eventPosition, + markdownTags: noteMarkdownTags, + }; + editorRef.current.execCommand({ name: 'dropItems', - value: { - type: 'notes', - markdownTags: noteMarkdownTags, - }, + value: props, }); }; void dropNotes(); @@ -51,13 +60,16 @@ export default function useDropHandler(dependencies: HookDependencies): DropHand paths.push(file.path); } + const props: DropCommandValue = { + type: 'files', + pos: eventPosition, + paths: paths, + createFileURL: createFileURL, + }; + editorRef.current.execCommand({ name: 'dropItems', - value: { - type: 'files', - paths: paths, - createFileURL: createFileURL, - }, + value: props, }); return true; } diff --git a/packages/editor/CodeMirror/createEditor.ts b/packages/editor/CodeMirror/createEditor.ts index cd03786a2..0fb27e8ff 100644 --- a/packages/editor/CodeMirror/createEditor.ts +++ b/packages/editor/CodeMirror/createEditor.ts @@ -6,6 +6,7 @@ import { classHighlighter } from '@lezer/highlight'; import { EditorView, drawSelection, highlightSpecialChars, ViewUpdate, Command, rectangularSelection, + dropCursor, } from '@codemirror/view'; import { history, undoDepth, redoDepth, standardKeymap } from '@codemirror/commands'; @@ -253,6 +254,8 @@ const createEditor = ( // Apply styles to entire lines (block-display decorations) decoratorExtension, + dropCursor(), + biDirectionalTextExtension, // Adds additional CSS classes to tokens (the default CSS classes are diff --git a/packages/editor/types.ts b/packages/editor/types.ts index 2c468b08b..b25bbd0a3 100644 --- a/packages/editor/types.ts +++ b/packages/editor/types.ts @@ -90,6 +90,7 @@ export interface ContentScriptData { // Intended to correspond with https://codemirror.net/docs/ref/#state.Transaction%5EuserEvent export enum UserEventSource { Paste = 'input.paste', + Drop = 'input.drop', } export interface EditorControl { From ddd18551eba98fa68433109b4fa6f419a897f48a Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Thu, 31 Oct 2024 01:18:13 -0700 Subject: [PATCH 15/17] Mobile: Fixes #11197: Fix search result note hidden after powering on device (#11297) --- packages/app-mobile/components/screens/Notes.tsx | 2 ++ packages/app-mobile/root.tsx | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/packages/app-mobile/components/screens/Notes.tsx b/packages/app-mobile/components/screens/Notes.tsx index 6a7e740a7..a3550c160 100644 --- a/packages/app-mobile/components/screens/Notes.tsx +++ b/packages/app-mobile/components/screens/Notes.tsx @@ -167,6 +167,8 @@ class NotesScreenComponent extends BaseScreenComponent { }); if (source === props.notesSource) return; + // For now, search refresh is handled by the search screen. + if (props.notesParentType === 'Search') return; let notes: NoteEntity[] = []; if (props.notesParentType === 'Folder') { diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index 8b651f2ee..84b04110d 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -308,6 +308,10 @@ const appReducer = (state = appDefaultState, action: any) => { newState.selectedNoteHash = ''; + if (action.routeName === 'Search') { + newState.notesParentType = 'Search'; + } + if ('noteId' in action) { newState.selectedNoteIds = action.noteId ? [action.noteId] : []; } @@ -344,6 +348,8 @@ const appReducer = (state = appDefaultState, action: any) => { newState.route = action; newState.historyCanGoBack = !!navHistory.length; + + logger.debug('Navigated to route:', newState.route?.routeName, 'with notesParentType:', newState.notesParentType); } break; From a5c14c8d1098f5f556e9ccde1830e491107f0a5e Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Mon, 4 Nov 2024 20:21:49 +0000 Subject: [PATCH 16/17] Desktop release v3.1.21 --- packages/app-desktop/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-desktop/package.json b/packages/app-desktop/package.json index cadb1781a..c1682d2ba 100644 --- a/packages/app-desktop/package.json +++ b/packages/app-desktop/package.json @@ -1,6 +1,6 @@ { "name": "@joplin/app-desktop", - "version": "3.1.20", + "version": "3.1.21", "description": "Joplin for Desktop", "main": "main.js", "private": true, From 6875fd271c411961c703258dde1b8d1d227dd324 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Mon, 4 Nov 2024 20:28:06 +0000 Subject: [PATCH 17/17] Android 3.1.7 --- packages/app-mobile/android/app/build.gradle | 4 ++-- readme/about/changelog/android.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/app-mobile/android/app/build.gradle b/packages/app-mobile/android/app/build.gradle index daadc3900..187dc3435 100644 --- a/packages/app-mobile/android/app/build.gradle +++ b/packages/app-mobile/android/app/build.gradle @@ -79,8 +79,8 @@ android { applicationId "net.cozic.joplin" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 2097754 - versionName "3.1.6" + versionCode 2097755 + versionName "3.1.7" ndk { abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64" } diff --git a/readme/about/changelog/android.md b/readme/about/changelog/android.md index b0c010cc3..e98ead245 100644 --- a/readme/about/changelog/android.md +++ b/readme/about/changelog/android.md @@ -1,5 +1,9 @@ # Joplin Android Changelog +## [android-v3.1.7](https://github.com/laurent22/joplin/releases/tag/android-v3.1.7) (Pre-release) - 2024-11-04T20:27:52Z + +- Fixed: Fix search result note hidden after powering on device (#11297) (#11197 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator)) + ## [android-v3.1.6](https://github.com/laurent22/joplin/releases/tag/android-v3.1.6) (Pre-release) - 2024-10-17T22:13:06Z - Improved: Downgrade CodeMirror packages to fix various Android regressions (#11170 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))