diff --git a/.eslintignore b/.eslintignore index c408bd934..5c94ed1d2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -133,6 +133,7 @@ packages/app-desktop/gui/ClipperConfigScreen.js packages/app-desktop/gui/ConfigScreen/ButtonBar.js packages/app-desktop/gui/ConfigScreen/ConfigScreen.js packages/app-desktop/gui/ConfigScreen/Sidebar.js +packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js diff --git a/.gitignore b/.gitignore index 2089efff3..64c2726d6 100644 --- a/.gitignore +++ b/.gitignore @@ -119,6 +119,7 @@ packages/app-desktop/gui/ClipperConfigScreen.js packages/app-desktop/gui/ConfigScreen/ButtonBar.js packages/app-desktop/gui/ConfigScreen/ConfigScreen.js packages/app-desktop/gui/ConfigScreen/Sidebar.js +packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js diff --git a/packages/app-cli/package.json b/packages/app-cli/package.json index 3978ec51a..9e150545b 100644 --- a/packages/app-cli/package.json +++ b/packages/app-cli/package.json @@ -35,7 +35,7 @@ ], "owner": "Laurent Cozic" }, - "version": "2.12.0", + "version": "2.12.1", "bin": "./main.js", "engines": { "node": ">=10.0.0" diff --git a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx index 256d83647..4c9d5155e 100644 --- a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx @@ -21,8 +21,7 @@ import getDefaultPluginsInfo from '@joplin/lib/services/plugins/defaultPlugins/d import JoplinCloudConfigScreen from '../JoplinCloudConfigScreen'; import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButton'; import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning'; -import shim from '@joplin/lib/shim'; -import StyledLink from '../style/StyledLink'; +import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink'; const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen'); const settingKeyToControl: any = { @@ -190,25 +189,14 @@ class ConfigScreenComponent extends React.Component { // saved yet). const matchesSavedTarget = settings['sync.target'] === this.props.settings['sync.target']; if (matchesSavedTarget && shouldShowMissingPasswordWarning(settings['sync.target'], settings)) { - const openMissingPasswordFAQ = () => - bridge().openExternal('https://joplinapp.org/faq#why-did-my-sync-and-encryption-passwords-disappear-after-updating-joplin'); - - const macInfoLink = ( - - {_('Help')} - - ); - - // The FAQ section related to missing passwords is specific to MacOS/ARM -- only show it - // in that case. - const showMacInfoLink = shim.isMac() && process.arch === 'arm64'; - settingComps.push(

- {_('Warning: Missing password.')}{' '}{showMacInfoLink ? macInfoLink : null} + {_('%s: Missing password.', _('Warning'))} + {' '} +

, ); } diff --git a/packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.tsx b/packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.tsx new file mode 100644 index 000000000..8c496aa3f --- /dev/null +++ b/packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; + +import shim from '@joplin/lib/shim'; +import bridge from '../../../services/bridge'; +import StyledLink from '../../style/StyledLink'; + +interface Props { + theme: any; + text: string; +} + +const openMissingPasswordFAQ = () => + bridge().openExternal('https://joplinapp.org/faq#why-did-my-sync-and-encryption-passwords-disappear-after-updating-joplin'); + +// A link to a specific part of the FAQ related to passwords being cleared when upgrading +// to a MacOS/ARM release. +const MacOSMissingPasswordHelpLink: React.FunctionComponent = props => { + const macInfoLink = ( + + {props.text} + + ); + + // The FAQ section related to missing passwords is specific to MacOS/ARM -- only show it + // in that case. + const newArchitectureReleasedRecently = Date.now() <= Date.UTC(2023, 11); // 11 = December + const showMacInfoLink = shim.isMac() && process.arch === 'arm64' && newArchitectureReleasedRecently; + + return showMacInfoLink ? macInfoLink : null; +}; + +export default MacOSMissingPasswordHelpLink; diff --git a/packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx b/packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx index 1e3c9fc3c..3963594c8 100644 --- a/packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx +++ b/packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx @@ -17,6 +17,7 @@ import Setting from '@joplin/lib/models/Setting'; import CommandService from '@joplin/lib/services/CommandService'; import { PublicPrivateKeyPair } from '@joplin/lib/services/e2ee/ppk'; import ToggleAdvancedSettingsButton from '../ConfigScreen/controls/ToggleAdvancedSettingsButton'; +import MacOSMissingPasswordHelpLink from '../ConfigScreen/controls/MissingPasswordHelpLink'; interface Props { themeId: any; @@ -252,7 +253,16 @@ const EncryptionConfigScreen = (props: Props) => { const buttonTitle = CommandService.instance().label('openMasterPasswordDialog'); const needPasswordMessage = !needMasterPassword ? null : ( -

{_('Your password is needed to decrypt some of your data.')}
{_('Please click on "%s" to proceed, or set the passwords in the "%s" list below.', buttonTitle, _('Encryption keys'))}

+

+ {_('Your password is needed to decrypt some of your data.')} +
+ {_('Please click on "%s" to proceed, or set the passwords in the "%s" list below.', buttonTitle, _('Encryption keys'))} +
+ +

); return ( diff --git a/packages/app-desktop/gui/MainScreen/commands/openItem.ts b/packages/app-desktop/gui/MainScreen/commands/openItem.ts index d4fd7f2b8..1ac94a2be 100644 --- a/packages/app-desktop/gui/MainScreen/commands/openItem.ts +++ b/packages/app-desktop/gui/MainScreen/commands/openItem.ts @@ -3,7 +3,8 @@ import shim from '@joplin/lib/shim'; import { _ } from '@joplin/lib/locale'; import bridge from '../../../services/bridge'; import { openItemById } from '../../NoteEditor/utils/contextMenu'; -const { parseResourceUrl, urlProtocol, fileUriToPath } = require('@joplin/lib/urlUtils'); +const { parseResourceUrl, urlProtocol } = require('@joplin/lib/urlUtils'); +import { fileUriToPath } from '@joplin/utils/url'; const { urlDecode } = require('@joplin/lib/string-utils'); export const declaration: CommandDeclaration = { diff --git a/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.ts b/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.ts index e993bcade..7df618d26 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.ts @@ -1,10 +1,15 @@ +import Setting from '@joplin/lib/models/Setting'; import { processPastedHtml } from './resourceHandling'; describe('resourceHandling', () => { it('should sanitize pasted HTML', async () => { + Setting.setConstant('resourceDir', '/home/.config/joplin/resources'); + const testCases = [ ['Test: ', 'Test: '], ['test', 'test'], + ['test', 'test'], + ['evil.pdf', 'evil.pdf'], ['evil()', ''], ['', ''], [ diff --git a/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts b/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts index ea3c4418f..1f1e4b8bf 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts @@ -8,7 +8,7 @@ import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; import htmlUtils from '@joplin/lib/htmlUtils'; import rendererHtmlUtils, { extractHtmlBody } from '@joplin/renderer/htmlUtils'; import Logger from '@joplin/utils/Logger'; -const { fileUriToPath } = require('@joplin/lib/urlUtils'); +import { fileUriToPath } from '@joplin/utils/url'; const joplinRendererUtils = require('@joplin/renderer').utils; const { clipboard } = require('electron'); const mimeUtils = require('@joplin/lib/mime-utils.js').mime; @@ -179,6 +179,8 @@ export async function processPastedHtml(html: string) { return extractHtmlBody(rendererHtmlUtils.sanitizeHtml( htmlUtils.replaceImageUrls(html, (src: string) => { return mappedResources[src]; - }), + }), { + allowedFilePrefixes: [Setting.value('resourceDir')], + }, )); } diff --git a/packages/app-desktop/package.json b/packages/app-desktop/package.json index 7e569b6dd..347f4ebf0 100644 --- a/packages/app-desktop/package.json +++ b/packages/app-desktop/package.json @@ -1,6 +1,6 @@ { "name": "@joplin/app-desktop", - "version": "2.12.11", + "version": "2.12.14", "description": "Joplin for Desktop", "main": "main.js", "private": true, diff --git a/packages/fork-htmlparser2/package.json b/packages/fork-htmlparser2/package.json index bc421a1cf..56e6e3b53 100644 --- a/packages/fork-htmlparser2/package.json +++ b/packages/fork-htmlparser2/package.json @@ -1,7 +1,7 @@ { "name": "@joplin/fork-htmlparser2", "description": "Fast & forgiving HTML/XML/RSS parser", - "version": "4.1.45", + "version": "4.1.46", "author": "Felix Boehm ", "publishConfig": { "access": "public" diff --git a/packages/fork-sax/package.json b/packages/fork-sax/package.json index a30e74dc1..dc52d3261 100644 --- a/packages/fork-sax/package.json +++ b/packages/fork-sax/package.json @@ -2,7 +2,7 @@ "name": "@joplin/fork-sax", "description": "An evented streaming XML parser in JavaScript", "author": "Isaac Z. Schlueter (http://blog.izs.me/)", - "version": "1.2.49", + "version": "1.2.50", "main": "lib/sax.js", "publishConfig": { "access": "public" diff --git a/packages/fork-uslug/package.json b/packages/fork-uslug/package.json index 8a0302af2..e39fc8f37 100644 --- a/packages/fork-uslug/package.json +++ b/packages/fork-uslug/package.json @@ -1,6 +1,6 @@ { "name": "@joplin/fork-uslug", - "version": "1.0.10", + "version": "1.0.11", "description": "A permissive slug generator that works with unicode.", "author": "Jeremy Selier ", "publishConfig": { diff --git a/packages/generate-plugin-doc/package.json b/packages/generate-plugin-doc/package.json index 89f96ba8a..56817d95e 100644 --- a/packages/generate-plugin-doc/package.json +++ b/packages/generate-plugin-doc/package.json @@ -1,6 +1,7 @@ { "name": "generate-plugin-doc", "packageManager": "yarn@3.6.0", + "private": true, "scripts": { "buildPluginDoc_": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme '../../Assets/PluginDocTheme/' --readme '../../Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../../../joplin-website/docs/api/references/plugin_api ../lib/services/plugins/api/" }, diff --git a/packages/htmlpack/package.json b/packages/htmlpack/package.json index 84db6dc6a..79fbd9d17 100644 --- a/packages/htmlpack/package.json +++ b/packages/htmlpack/package.json @@ -1,6 +1,6 @@ { "name": "@joplin/htmlpack", - "version": "2.12.0", + "version": "2.12.1", "description": "Pack an HTML file and all its linked resources into a single HTML file", "main": "dist/index.js", "types": "src/index.ts", @@ -14,7 +14,7 @@ "author": "Laurent Cozic", "license": "MIT", "dependencies": { - "@joplin/fork-htmlparser2": "^4.1.45", + "@joplin/fork-htmlparser2": "^4.1.46", "css": "3.0.0", "datauri": "4.1.0", "fs-extra": "11.1.1", diff --git a/packages/lib/package.json b/packages/lib/package.json index f019534cc..bb3b3870d 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -1,6 +1,6 @@ { "name": "@joplin/lib", - "version": "2.12.0", + "version": "2.12.1", "description": "Joplin Core library", "author": "Laurent Cozic", "homepage": "", @@ -31,14 +31,14 @@ "dependencies": { "@aws-sdk/client-s3": "3.296.0", "@aws-sdk/s3-request-presigner": "3.296.0", - "@joplin/fork-htmlparser2": "^4.1.44", - "@joplin/fork-sax": "^1.2.48", - "@joplin/fork-uslug": "^1.0.9", - "@joplin/htmlpack": "~2.12", - "@joplin/renderer": "~2.12", - "@joplin/turndown": "^4.0.66", - "@joplin/turndown-plugin-gfm": "^1.0.48", - "@joplin/utils": "~2.12", + "@joplin/fork-htmlparser2": "^4.1.46", + "@joplin/fork-sax": "^1.2.50", + "@joplin/fork-uslug": "^1.0.11", + "@joplin/htmlpack": "^2.12.1", + "@joplin/renderer": "^2.12.1", + "@joplin/turndown": "^4.0.68", + "@joplin/turndown-plugin-gfm": "^1.0.50", + "@joplin/utils": "^2.12.1", "@types/nanoid": "3.0.0", "async-mutex": "0.4.0", "base-64": "1.0.0", diff --git a/packages/lib/services/rest/routes/notes.ts b/packages/lib/services/rest/routes/notes.ts index 828265ca1..8b90b0670 100644 --- a/packages/lib/services/rest/routes/notes.ts +++ b/packages/lib/services/rest/routes/notes.ts @@ -24,9 +24,9 @@ import * as ArrayUtils from '../../../ArrayUtils'; import Logger from '@joplin/utils/Logger'; const { mimeTypeFromHeaders } = require('../../../net-utils'); const { fileExtension, safeFileExtension, safeFilename, filename } = require('../../../path-utils'); -const { fileUriToPath } = require('../../../urlUtils'); const { MarkupToHtml } = require('@joplin/renderer'); const { ErrorNotFound } = require('../utils/errors'); +import { fileUriToPath } from '@joplin/utils/url'; const logger = Logger.create('routes/notes'); diff --git a/packages/lib/urlUtils.js b/packages/lib/urlUtils.js index 88aa34112..d509716ae 100644 --- a/packages/lib/urlUtils.js +++ b/packages/lib/urlUtils.js @@ -105,101 +105,4 @@ urlUtils.objectToQueryString = function(query) { return queryString; }; -// This is a modified version of the file-uri-to-path package: -// -// - It removes the dependency to the "path" package, which wouldn't work with -// React Native. -// -// - It always returns paths with forward slashes "/". This is normally handled -// properly everywhere. -// -// - Adds the "platform" parameter to optionall return paths with "\" for win32 -function fileUriToPath_(uri, platform) { - const sep = '/'; - - if ( - typeof uri !== 'string' || - uri.length <= 7 || - uri.substring(0, 7) !== 'file://' - ) { - throw new TypeError( - 'must pass in a file:// URI to convert to a file path', - ); - } - - const rest = decodeURI(uri.substring(7)); - const firstSlash = rest.indexOf('/'); - let host = rest.substring(0, firstSlash); - let path = rest.substring(firstSlash + 1); - - // 2. Scheme Definition - // As a special case, can be the string "localhost" or the empty - // string; this is interpreted as "the machine from which the URL is - // being interpreted". - if (host === 'localhost') { - host = ''; - } - - if (host) { - host = sep + sep + host; - } - - // 3.2 Drives, drive letters, mount points, file system root - // Drive letters are mapped into the top of a file URI in various ways, - // depending on the implementation; some applications substitute - // vertical bar ("|") for the colon after the drive letter, yielding - // "file:///c|/tmp/test.txt". In some cases, the colon is left - // unchanged, as in "file:///c:/tmp/test.txt". In other cases, the - // colon is simply omitted, as in "file:///c/tmp/test.txt". - path = path.replace(/^(.+)\|/, '$1:'); - - // for Windows, we need to invert the path separators from what a URI uses - // if (sep === '\\') { - // path = path.replace(/\//g, '\\'); - // } - - if (/^.+:/.test(path)) { - // has Windows drive at beginning of path - } else { - // unix path… - path = sep + path; - } - - if (platform === 'win32') { - return (host + path).replace(/\//g, '\\'); - } else { - return host + path; - } -} - -urlUtils.fileUriToPath = (path, platform = 'linux') => { - const output = fileUriToPath_(path, platform); - - // The file-uri-to-path module converts Windows path such as - // - // file://c:/autoexec.bat => \\c:\autoexec.bat - // - // Probably because a file:// that starts with only two slashes is not - // quite valid. If we use three slashes, it works: - // - // file:///c:/autoexec.bat => c:\autoexec.bat - // - // However there are various places in the app where we can find - // paths with only two slashes because paths are often constructed - // as `file://${resourcePath}` - which works in all OSes except - // Windows. - // - // So here we introduce a special case - if we detect that we have - // an invalid Windows path that starts with \\x:, we just remove - // the first two backslashes. - // - // https://github.com/laurent22/joplin/issues/5693 - - if (output.match(/^\/\/[a-zA-Z]:/)) { - return output.substr(2); - } - - return output; -}; - module.exports = urlUtils; diff --git a/packages/lib/urlUtils.test.js b/packages/lib/urlUtils.test.js index f6704c90b..c23936830 100644 --- a/packages/lib/urlUtils.test.js +++ b/packages/lib/urlUtils.test.js @@ -71,30 +71,4 @@ describe('urlUtils', () => { } })); - it('should convert a file URI to a file path', (async () => { - // Tests imported from https://github.com/TooTallNate/file-uri-to-path/tree/master/test - const testCases = { - 'file://host/path': '//host/path', - 'file://localhost/etc/fstab': '/etc/fstab', - 'file:///etc/fstab': '/etc/fstab', - 'file:///c:/WINDOWS/clock.avi': 'c:/WINDOWS/clock.avi', - 'file://localhost/c|/WINDOWS/clock.avi': 'c:/WINDOWS/clock.avi', - 'file:///c|/WINDOWS/clock.avi': 'c:/WINDOWS/clock.avi', - 'file://localhost/c:/WINDOWS/clock.avi': 'c:/WINDOWS/clock.avi', - 'file://hostname/path/to/the%20file.txt': '//hostname/path/to/the file.txt', - 'file:///c:/path/to/the%20file.txt': 'c:/path/to/the file.txt', - 'file:///C:/Documents%20and%20Settings/davris/FileSchemeURIs.doc': 'C:/Documents and Settings/davris/FileSchemeURIs.doc', - 'file:///C:/caf%C3%A9/%C3%A5r/d%C3%BCnn/%E7%89%9B%E9%93%83/Ph%E1%BB%9F/%F0%9F%98%B5.exe': 'C:/café/år/dünn/牛铃/Phở/😵.exe', - }; - - for (const [input, expected] of Object.entries(testCases)) { - const actual = urlUtils.fileUriToPath(input); - expect(actual).toBe(expected); - } - - expect(urlUtils.fileUriToPath('file://c:/not/quite/right')).toBe('c:/not/quite/right'); - expect(urlUtils.fileUriToPath('file:///d:/better')).toBe('d:/better'); - expect(urlUtils.fileUriToPath('file:///c:/AUTOEXEC.BAT', 'win32')).toBe('c:\\AUTOEXEC.BAT'); - })); - }); diff --git a/packages/plugin-repo-cli/package.json b/packages/plugin-repo-cli/package.json index e5d96b44f..0083a24a1 100644 --- a/packages/plugin-repo-cli/package.json +++ b/packages/plugin-repo-cli/package.json @@ -1,6 +1,6 @@ { "name": "@joplin/plugin-repo-cli", - "version": "2.12.0", + "version": "2.12.1", "description": "", "main": "index.js", "bin": "./dist/index.js", @@ -18,9 +18,9 @@ "author": "", "license": "AGPL-3.0-or-later", "dependencies": { - "@joplin/lib": "~2.12", - "@joplin/tools": "~2.12", - "@joplin/utils": "~2.12", + "@joplin/lib": "^2.12.1", + "@joplin/tools": "^2.12.1", + "@joplin/utils": "^2.12.1", "fs-extra": "11.1.1", "gh-release-assets": "2.0.1", "node-fetch": "2.6.7", diff --git a/packages/react-native-saf-x/package.json b/packages/react-native-saf-x/package.json index 912880052..e1f849866 100644 --- a/packages/react-native-saf-x/package.json +++ b/packages/react-native-saf-x/package.json @@ -1,6 +1,6 @@ { "name": "@joplin/react-native-saf-x", - "version": "2.12.0", + "version": "2.12.1", "description": "a module to help work with scoped storages on android easily", "main": "src/index", "react-native": "src/index", diff --git a/packages/renderer/htmlUtils.ts b/packages/renderer/htmlUtils.ts index 8b2b0eb75..f263b839c 100644 --- a/packages/renderer/htmlUtils.ts +++ b/packages/renderer/htmlUtils.ts @@ -1,5 +1,6 @@ const Entities = require('html-entities').AllHtmlEntities; const htmlentities = new Entities().encode; +import { fileUriToPath } from '@joplin/utils/url'; const htmlparser2 = require('@joplin/fork-htmlparser2'); // [\s\S] instead of . for multiline matching @@ -31,7 +32,8 @@ const selfClosingElements = [ ]; interface SanitizeHtmlOptions { - addNoMdConvClass: boolean; + addNoMdConvClass?: boolean; + allowedFilePrefixes?: string[]; } export const attributesHtml = (attr: Record) => { @@ -157,20 +159,36 @@ class HtmlUtils { .replace(/ { + + it('should convert a file URI to a file path', (async () => { + // Tests imported from https://github.com/TooTallNate/file-uri-to-path/tree/master/test + const testCases = { + 'file://host/path': '//host/path', + 'file://localhost/etc/fstab': '/etc/fstab', + 'file:///etc/fstab': '/etc/fstab', + 'file:///c:/WINDOWS/clock.avi': 'c:/WINDOWS/clock.avi', + 'file://localhost/c|/WINDOWS/clock.avi': 'c:/WINDOWS/clock.avi', + 'file:///c|/WINDOWS/clock.avi': 'c:/WINDOWS/clock.avi', + 'file://localhost/c:/WINDOWS/clock.avi': 'c:/WINDOWS/clock.avi', + 'file://hostname/path/to/the%20file.txt': '//hostname/path/to/the file.txt', + 'file:///c:/path/to/the%20file.txt': 'c:/path/to/the file.txt', + 'file:///C:/Documents%20and%20Settings/davris/FileSchemeURIs.doc': 'C:/Documents and Settings/davris/FileSchemeURIs.doc', + 'file:///C:/caf%C3%A9/%C3%A5r/d%C3%BCnn/%E7%89%9B%E9%93%83/Ph%E1%BB%9F/%F0%9F%98%B5.exe': 'C:/café/år/dünn/牛铃/Phở/😵.exe', + }; + + for (const [input, expected] of Object.entries(testCases)) { + const actual = fileUriToPath(input); + expect(actual).toBe(expected); + } + + expect(fileUriToPath('file://c:/not/quite/right')).toBe('c:/not/quite/right'); + expect(fileUriToPath('file:///d:/better')).toBe('d:/better'); + expect(fileUriToPath('file:///c:/AUTOEXEC.BAT', 'win32')).toBe('c:\\AUTOEXEC.BAT'); + })); + +}); diff --git a/packages/utils/url.ts b/packages/utils/url.ts new file mode 100644 index 000000000..54153cb1b --- /dev/null +++ b/packages/utils/url.ts @@ -0,0 +1,98 @@ +/* eslint-disable import/prefer-default-export */ + +// This is a modified version of the file-uri-to-path package: +// +// - It removes the dependency to the "path" package, which wouldn't work with +// React Native. +// +// - It always returns paths with forward slashes "/". This is normally handled +// properly everywhere. +// +// - Adds the "platform" parameter to optionall return paths with "\" for win32 +function fileUriToPath_(uri: string, platform: string) { + const sep = '/'; + + if ( + typeof uri !== 'string' || + uri.length <= 7 || + uri.substring(0, 7) !== 'file://' + ) { + throw new TypeError( + 'must pass in a file:// URI to convert to a file path', + ); + } + + const rest = decodeURI(uri.substring(7)); + const firstSlash = rest.indexOf('/'); + let host = rest.substring(0, firstSlash); + let path = rest.substring(firstSlash + 1); + + // 2. Scheme Definition + // As a special case, can be the string "localhost" or the empty + // string; this is interpreted as "the machine from which the URL is + // being interpreted". + if (host === 'localhost') { + host = ''; + } + + if (host) { + host = sep + sep + host; + } + + // 3.2 Drives, drive letters, mount points, file system root + // Drive letters are mapped into the top of a file URI in various ways, + // depending on the implementation; some applications substitute + // vertical bar ("|") for the colon after the drive letter, yielding + // "file:///c|/tmp/test.txt". In some cases, the colon is left + // unchanged, as in "file:///c:/tmp/test.txt". In other cases, the + // colon is simply omitted, as in "file:///c/tmp/test.txt". + path = path.replace(/^(.+)\|/, '$1:'); + + // for Windows, we need to invert the path separators from what a URI uses + // if (sep === '\\') { + // path = path.replace(/\//g, '\\'); + // } + + if (/^.+:/.test(path)) { + // has Windows drive at beginning of path + } else { + // unix path… + path = sep + path; + } + + if (platform === 'win32') { + return (host + path).replace(/\//g, '\\'); + } else { + return host + path; + } +} + +export const fileUriToPath = (path: string, platform = 'linux') => { + const output = fileUriToPath_(path, platform); + + // The file-uri-to-path module converts Windows path such as + // + // file://c:/autoexec.bat => \\c:\autoexec.bat + // + // Probably because a file:// that starts with only two slashes is not + // quite valid. If we use three slashes, it works: + // + // file:///c:/autoexec.bat => c:\autoexec.bat + // + // However there are various places in the app where we can find + // paths with only two slashes because paths are often constructed + // as `file://${resourcePath}` - which works in all OSes except + // Windows. + // + // So here we introduce a special case - if we detect that we have + // an invalid Windows path that starts with \\x:, we just remove + // the first two backslashes. + // + // https://github.com/laurent22/joplin/issues/5693 + + if (output.match(/^\/\/[a-zA-Z]:/)) { + return output.substr(2); + } + + return output; +}; diff --git a/readme/changelog_cli.md b/readme/changelog_cli.md index 8fc82d0c0..11beae5d4 100644 --- a/readme/changelog_cli.md +++ b/readme/changelog_cli.md @@ -1,5 +1,14 @@ # Joplin terminal app changelog +## [cli-v2.12.1](https://github.com/laurent22/joplin/releases/tag/cli-v2.12.1) - 2023-08-23T12:53:19Z + +- New: Add support for share permissions (#8491) +- Improved: Allow importing Evernote task lists (#8440 by Rob Moffat) +- Improved: Rotating log files (#8376) (#5521 by [@hubert](https://github.com/hubert)) +- Improved: Updated packages @rmp135/sql-ts (v1.18.0), buildTools, clean-html (v2), dayjs (v1.11.9), domhandler (v5), gettext-parser (v7.0.1), glob (v10.3.3), highlight.js (v11.8.0), jsdom (v22.1.0), sass (v1.63.6), sharp (v0.32.3), standard (v17.1.0), word-wrap (v1.2.4) +- Improved: WebDAV: Show a more descriptive error message when the password is empty (#8477) (#8466 by Henry Heino) +- Security: Prevent XSS when passing specially encoded string to a link (57b4198) + ## [cli-v2.11.1](https://github.com/laurent22/joplin/releases/tag/cli-v2.11.1) - 2023-06-27T09:28:01Z - Improved: Updated packages aws, buildTools, domutils (v3.1.0), fs-extra (v11.1.1), jsdom (v21.1.2), markdown-it-multimd-table (v4.2.2), nanoid (v3.3.6), node-persist (v3.1.3), open (v8.4.2), reselect (v4.1.8), sass (v1.62.1), sharp (v0.32.1), sqlite3 (v5.1.6), tar (v6.1.15), turndown (v7.1.2), yargs (v17.7.2) diff --git a/yarn.lock b/yarn.lock index 8e544f1de..c9f65497a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4626,7 +4626,7 @@ __metadata: languageName: unknown linkType: soft -"@joplin/fork-htmlparser2@^4.1.44, @joplin/fork-htmlparser2@^4.1.45, @joplin/fork-htmlparser2@workspace:packages/fork-htmlparser2": +"@joplin/fork-htmlparser2@^4.1.46, @joplin/fork-htmlparser2@workspace:packages/fork-htmlparser2": version: 0.0.0-use.local resolution: "@joplin/fork-htmlparser2@workspace:packages/fork-htmlparser2" dependencies: @@ -4647,7 +4647,7 @@ __metadata: languageName: unknown linkType: soft -"@joplin/fork-sax@^1.2.48, @joplin/fork-sax@workspace:packages/fork-sax": +"@joplin/fork-sax@^1.2.50, @joplin/fork-sax@workspace:packages/fork-sax": version: 0.0.0-use.local resolution: "@joplin/fork-sax@workspace:packages/fork-sax" dependencies: @@ -4656,7 +4656,7 @@ __metadata: languageName: unknown linkType: soft -"@joplin/fork-uslug@^1.0.9, @joplin/fork-uslug@workspace:packages/fork-uslug": +"@joplin/fork-uslug@^1.0.11, @joplin/fork-uslug@workspace:packages/fork-uslug": version: 0.0.0-use.local resolution: "@joplin/fork-uslug@workspace:packages/fork-uslug" dependencies: @@ -4666,11 +4666,11 @@ __metadata: languageName: unknown linkType: soft -"@joplin/htmlpack@workspace:packages/htmlpack, @joplin/htmlpack@~2.12": +"@joplin/htmlpack@^2.12.1, @joplin/htmlpack@workspace:packages/htmlpack": version: 0.0.0-use.local resolution: "@joplin/htmlpack@workspace:packages/htmlpack" dependencies: - "@joplin/fork-htmlparser2": ^4.1.45 + "@joplin/fork-htmlparser2": ^4.1.46 "@types/fs-extra": 11.0.1 css: 3.0.0 datauri: 4.1.0 @@ -4679,20 +4679,20 @@ __metadata: languageName: unknown linkType: soft -"@joplin/lib@workspace:packages/lib, @joplin/lib@~2.12": +"@joplin/lib@^2.12.1, @joplin/lib@workspace:packages/lib, @joplin/lib@~2.12": version: 0.0.0-use.local resolution: "@joplin/lib@workspace:packages/lib" dependencies: "@aws-sdk/client-s3": 3.296.0 "@aws-sdk/s3-request-presigner": 3.296.0 - "@joplin/fork-htmlparser2": ^4.1.44 - "@joplin/fork-sax": ^1.2.48 - "@joplin/fork-uslug": ^1.0.9 - "@joplin/htmlpack": ~2.12 - "@joplin/renderer": ~2.12 - "@joplin/turndown": ^4.0.66 - "@joplin/turndown-plugin-gfm": ^1.0.48 - "@joplin/utils": ~2.12 + "@joplin/fork-htmlparser2": ^4.1.46 + "@joplin/fork-sax": ^1.2.50 + "@joplin/fork-uslug": ^1.0.11 + "@joplin/htmlpack": ^2.12.1 + "@joplin/renderer": ^2.12.1 + "@joplin/turndown": ^4.0.68 + "@joplin/turndown-plugin-gfm": ^1.0.50 + "@joplin/utils": ^2.12.1 "@types/fs-extra": 11.0.1 "@types/jest": 29.5.3 "@types/js-yaml": 4.0.5 @@ -4796,9 +4796,9 @@ __metadata: version: 0.0.0-use.local resolution: "@joplin/plugin-repo-cli@workspace:packages/plugin-repo-cli" dependencies: - "@joplin/lib": ~2.12 - "@joplin/tools": ~2.12 - "@joplin/utils": ~2.12 + "@joplin/lib": ^2.12.1 + "@joplin/tools": ^2.12.1 + "@joplin/utils": ^2.12.1 "@types/fs-extra": 11.0.1 "@types/jest": 29.5.3 "@types/node": 18.16.18 @@ -4847,12 +4847,13 @@ __metadata: languageName: unknown linkType: soft -"@joplin/renderer@workspace:packages/renderer, @joplin/renderer@~2.12": +"@joplin/renderer@^2.12.1, @joplin/renderer@workspace:packages/renderer, @joplin/renderer@~2.12": version: 0.0.0-use.local resolution: "@joplin/renderer@workspace:packages/renderer" dependencies: - "@joplin/fork-htmlparser2": ^4.1.44 - "@joplin/fork-uslug": ^1.0.9 + "@joplin/fork-htmlparser2": ^4.1.46 + "@joplin/fork-uslug": ^1.0.11 + "@joplin/utils": ~2.12 "@types/jest": 29.5.3 "@types/node": 18.16.18 font-awesome-filetypes: 2.1.0 @@ -4945,14 +4946,14 @@ __metadata: languageName: unknown linkType: soft -"@joplin/tools@workspace:packages/tools, @joplin/tools@~2.12": +"@joplin/tools@^2.12.1, @joplin/tools@workspace:packages/tools, @joplin/tools@~2.12": version: 0.0.0-use.local resolution: "@joplin/tools@workspace:packages/tools" dependencies: - "@joplin/fork-htmlparser2": ^4.1.44 - "@joplin/lib": ~2.12 - "@joplin/renderer": ~2.12 - "@joplin/utils": ~2.12 + "@joplin/fork-htmlparser2": ^4.1.46 + "@joplin/lib": ^2.12.1 + "@joplin/renderer": ^2.12.1 + "@joplin/utils": ^2.12.1 "@rmp135/sql-ts": 1.18.0 "@types/fs-extra": 11.0.1 "@types/jest": 29.5.3 @@ -4988,7 +4989,7 @@ __metadata: languageName: unknown linkType: soft -"@joplin/turndown-plugin-gfm@^1.0.48, @joplin/turndown-plugin-gfm@workspace:packages/turndown-plugin-gfm": +"@joplin/turndown-plugin-gfm@^1.0.50, @joplin/turndown-plugin-gfm@workspace:packages/turndown-plugin-gfm": version: 0.0.0-use.local resolution: "@joplin/turndown-plugin-gfm@workspace:packages/turndown-plugin-gfm" dependencies: @@ -5000,7 +5001,7 @@ __metadata: languageName: unknown linkType: soft -"@joplin/turndown@^4.0.66, @joplin/turndown@workspace:packages/turndown": +"@joplin/turndown@^4.0.68, @joplin/turndown@workspace:packages/turndown": version: 0.0.0-use.local resolution: "@joplin/turndown@workspace:packages/turndown" dependencies: @@ -5017,7 +5018,7 @@ __metadata: languageName: unknown linkType: soft -"@joplin/utils@workspace:packages/utils, @joplin/utils@~2.12": +"@joplin/utils@^2.12.1, @joplin/utils@workspace:packages/utils, @joplin/utils@~2.12": version: 0.0.0-use.local resolution: "@joplin/utils@workspace:packages/utils" dependencies: