From 3235f58f5a5459e607749f6ddd0dbc4b0c38bf45 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Sat, 24 Apr 2021 20:23:33 +0200 Subject: [PATCH] Desktop: Resolves #4727: Add support for safe mode, which temporarily disables note rendering and plugins --- .eslintignore | 3 ++ .gitignore | 3 ++ packages/app-desktop/app.ts | 4 ++- .../app-desktop/commands/toggleSafeMode.ts | 20 +++++++++++ packages/app-desktop/gui/ErrorBoundary.tsx | 8 +++++ .../app-desktop/gui/MainScreen/MainScreen.tsx | 26 +++++++++++--- packages/app-desktop/gui/MenuBar.tsx | 1 + packages/app-desktop/gui/menuCommandNames.ts | 1 + packages/lib/markupLanguageUtils.ts | 2 ++ packages/lib/models/Setting.ts | 8 +++++ .../lib/services/plugins/PluginService.ts | 11 ++++++ packages/renderer/MarkupToHtml.ts | 36 ++++++++++++++----- .../server/src/apps/joplin/Application.ts | 3 +- 13 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 packages/app-desktop/commands/toggleSafeMode.ts diff --git a/.eslintignore b/.eslintignore index c4fe320bb..5f92fdda6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -211,6 +211,9 @@ packages/app-desktop/commands/stopExternalEditing.js.map packages/app-desktop/commands/toggleExternalEditing.d.ts packages/app-desktop/commands/toggleExternalEditing.js packages/app-desktop/commands/toggleExternalEditing.js.map +packages/app-desktop/commands/toggleSafeMode.d.ts +packages/app-desktop/commands/toggleSafeMode.js +packages/app-desktop/commands/toggleSafeMode.js.map packages/app-desktop/gui/Button/Button.d.ts packages/app-desktop/gui/Button/Button.js packages/app-desktop/gui/Button/Button.js.map diff --git a/.gitignore b/.gitignore index 9f4cb9d21..131f21838 100644 --- a/.gitignore +++ b/.gitignore @@ -198,6 +198,9 @@ packages/app-desktop/commands/stopExternalEditing.js.map packages/app-desktop/commands/toggleExternalEditing.d.ts packages/app-desktop/commands/toggleExternalEditing.js packages/app-desktop/commands/toggleExternalEditing.js.map +packages/app-desktop/commands/toggleSafeMode.d.ts +packages/app-desktop/commands/toggleSafeMode.js +packages/app-desktop/commands/toggleSafeMode.js.map packages/app-desktop/gui/Button/Button.d.ts packages/app-desktop/gui/Button/Button.js packages/app-desktop/gui/Button/Button.js.map diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index a0e3f60a3..a328f8139 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -89,10 +89,11 @@ const globalCommands = [ require('./commands/exportNotes'), require('./commands/focusElement'), require('./commands/openProfileDirectory'), + require('./commands/replaceMisspelling'), require('./commands/startExternalEditing'), require('./commands/stopExternalEditing'), require('./commands/toggleExternalEditing'), - require('./commands/replaceMisspelling'), + require('./commands/toggleSafeMode'), require('@joplin/lib/commands/historyBackward'), require('@joplin/lib/commands/historyForward'), require('@joplin/lib/commands/synchronize'), @@ -539,6 +540,7 @@ class Application extends BaseApplication { const pluginRunner = new PluginRunner(); service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store()); + service.isSafeMode = Setting.value('isSafeMode'); const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states')); diff --git a/packages/app-desktop/commands/toggleSafeMode.ts b/packages/app-desktop/commands/toggleSafeMode.ts new file mode 100644 index 000000000..91d21c9b9 --- /dev/null +++ b/packages/app-desktop/commands/toggleSafeMode.ts @@ -0,0 +1,20 @@ +import { _ } from '@joplin/lib/locale'; +import Setting from '@joplin/lib/models/Setting'; +import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; +import bridge from '../services/bridge'; + +export const declaration: CommandDeclaration = { + name: 'toggleSafeMode', + label: () => _('Toggle safe mode'), +}; + +export const runtime = (): CommandRuntime => { + return { + execute: async (_context: CommandContext, enabled: boolean = null) => { + enabled = enabled !== null ? enabled : !Setting.value('isSafeMode'); + Setting.setValue('isSafeMode', enabled); + await Setting.saveAll(); + bridge().restart(); + }, + }; +}; diff --git a/packages/app-desktop/gui/ErrorBoundary.tsx b/packages/app-desktop/gui/ErrorBoundary.tsx index 0a891a4d5..cfd6879e6 100644 --- a/packages/app-desktop/gui/ErrorBoundary.tsx +++ b/packages/app-desktop/gui/ErrorBoundary.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import versionInfo from '@joplin/lib/versionInfo'; import PluginService from '@joplin/lib/services/plugins/PluginService'; import Setting from '@joplin/lib/models/Setting'; +import bridge from '../services/bridge'; const packageInfo = require('../packageInfo.js'); const ipcRenderer = require('electron').ipcRenderer; @@ -62,6 +63,12 @@ export default class ErrorBoundary extends React.Component { render() { if (this.state.error) { + const safeMode_click = async () => { + Setting.setValue('isSafeMode', true); + await Setting.saveAll(); + bridge().restart(); + }; + try { const output = []; @@ -112,6 +119,7 @@ export default class ErrorBoundary extends React.Component {

Error

Joplin encountered a fatal error and could not continue. To report the error, please copy the *entire content* of this page and post it on Joplin forum or GitHub.

+

To continue you may close the app. Alternatively, if the error persists you may try to restart in safe mode, which will temporarily disable all plugins.

{output}
); diff --git a/packages/app-desktop/gui/MainScreen/MainScreen.tsx b/packages/app-desktop/gui/MainScreen/MainScreen.tsx index 9dad2c4a7..438e9219f 100644 --- a/packages/app-desktop/gui/MainScreen/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen/MainScreen.tsx @@ -63,6 +63,7 @@ interface Props { settingEditorCodeView: boolean; pluginsLegacy: any; startupPluginsLoaded: boolean; + isSafeMode: boolean; } interface State { @@ -237,7 +238,7 @@ class MainScreenComponent extends React.Component { // For example, it cannot be closed right away if a note is being saved. // If a note is being saved, we wait till it is saved and then call // "appCloseReply" again. - ipcRenderer.on('appClose', () => { + ipcRenderer.on('appClose', async () => { if (this.waitForNotesSavedIID_) shim.clearInterval(this.waitForNotesSavedIID_); this.waitForNotesSavedIID_ = null; @@ -489,8 +490,24 @@ class MainScreenComponent extends React.Component { bridge().restart(); }; + const onDisableSafeModeAndRestart = async () => { + Setting.setValue('isSafeMode', false); + await Setting.saveAll(); + bridge().restart(); + }; + let msg = null; - if (this.props.shouldUpgradeSyncTarget) { + + if (this.props.isSafeMode) { + msg = ( + + {_('Safe mode is currently active. Note rendering and all plugins are temporarily disabled.')}{' '} + onDisableSafeModeAndRestart()}> + {_('Disable safe mode and restart')} + + + ); + } else if (this.props.shouldUpgradeSyncTarget) { msg = ( {_('The sync target needs to be upgraded before Joplin can sync. The operation may take a few minutes to complete and the app needs to be restarted. To proceed please click on the link.')}{' '} @@ -553,9 +570,9 @@ class MainScreenComponent extends React.Component { ); } - messageBoxVisible(props: any = null) { + messageBoxVisible(props: Props = null) { if (!props) props = this.props; - return props.hasDisabledSyncItems || props.showMissingMasterKeyMessage || props.showNeedUpgradingMasterKeyMessage || props.showShouldReencryptMessage || props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget; + return props.hasDisabledSyncItems || props.showMissingMasterKeyMessage || props.showNeedUpgradingMasterKeyMessage || props.showShouldReencryptMessage || props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget || props.isSafeMode; } registerCommands() { @@ -777,6 +794,7 @@ const mapStateToProps = (state: AppState) => { layoutMoveMode: state.layoutMoveMode, mainLayout: state.mainLayout, startupPluginsLoaded: state.startupPluginsLoaded, + isSafeMode: state.settings.isSafeMode, }; }; diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 0ff00e94c..6340891dc 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -737,6 +737,7 @@ function useMenu(props: Props) { }, }, + menuItemDic.toggleSafeMode, menuItemDic.openProfileDirectory, menuItemDic.copyDevCommand, diff --git a/packages/app-desktop/gui/menuCommandNames.ts b/packages/app-desktop/gui/menuCommandNames.ts index ae6f4efc4..68a57b74c 100644 --- a/packages/app-desktop/gui/menuCommandNames.ts +++ b/packages/app-desktop/gui/menuCommandNames.ts @@ -43,5 +43,6 @@ export default function() { 'editor.sortSelectedLines', 'editor.swapLineUp', 'editor.swapLineDown', + 'toggleSafeMode', ]; } diff --git a/packages/lib/markupLanguageUtils.ts b/packages/lib/markupLanguageUtils.ts index 0f22be45d..aa0426c10 100644 --- a/packages/lib/markupLanguageUtils.ts +++ b/packages/lib/markupLanguageUtils.ts @@ -7,6 +7,7 @@ import htmlUtils from './htmlUtils'; import Resource from './models/Resource'; export class MarkupLanguageUtils { + private lib_(language: MarkupLanguage) { if (language === MarkupLanguage.Html) return htmlUtils; if (language === MarkupLanguage.Markdown) return markdownUtils; @@ -31,6 +32,7 @@ export class MarkupLanguageUtils { pluginOptions: pluginOptions, tempDir: Setting.value('tempDir'), fsDriver: shim.fsDriver(), + isSafeMode: Setting.value('isSafeMode'), }, options); return new MarkupToHtml(options); diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index 796006748..acb630f70 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -1133,6 +1133,14 @@ class Setting extends BaseModel { 'Restart app to see changes.'), storage: SettingStorage.File, }, + + isSafeMode: { + value: false, + type: SettingItemType.Bool, + public: false, + appTypes: ['desktop'], + storage: SettingStorage.Database, + }, }; this.metadata_ = Object.assign(this.metadata_, this.customMetadata_); diff --git a/packages/lib/services/plugins/PluginService.ts b/packages/lib/services/plugins/PluginService.ts index 240ba8954..ce2bf2fcb 100644 --- a/packages/lib/services/plugins/PluginService.ts +++ b/packages/lib/services/plugins/PluginService.ts @@ -74,6 +74,7 @@ export default class PluginService extends BaseService { private plugins_: Plugins = {}; private runner_: BasePluginRunner = null; private startedPlugins_: Record = {}; + private isSafeMode_: boolean = false; public initialize(appVersion: string, platformImplementation: any, runner: BasePluginRunner, store: any) { this.appVersion_ = appVersion; @@ -86,6 +87,14 @@ export default class PluginService extends BaseService { return this.plugins_; } + public get isSafeMode(): boolean { + return this.isSafeMode_; + } + + public set isSafeMode(v: boolean) { + this.isSafeMode_ = v; + } + private setPluginAt(pluginId: string, plugin: Plugin) { this.plugins_ = { ...this.plugins_, @@ -346,6 +355,8 @@ export default class PluginService extends BaseService { } public async runPlugin(plugin: Plugin) { + if (this.isSafeMode) throw new Error(`Plugin was not started due to safe mode: ${plugin.manifest.id}`); + if (!this.isCompatible(plugin.manifest.app_min_version)) { throw new Error(`Plugin "${plugin.id}" was disabled because it requires Joplin version ${plugin.manifest.app_min_version} and current version is ${this.appVersion_}.`); } else { diff --git a/packages/renderer/MarkupToHtml.ts b/packages/renderer/MarkupToHtml.ts index b2b294af4..794956ec8 100644 --- a/packages/renderer/MarkupToHtml.ts +++ b/packages/renderer/MarkupToHtml.ts @@ -27,24 +27,35 @@ export interface RenderResult { cssStrings: string[]; } +export interface OptionsResourceModel { + isResourceUrl: (url: string)=> boolean; +} + +export interface Options { + isSafeMode?: boolean; + ResourceModel: OptionsResourceModel; +} + export default class MarkupToHtml { static MARKUP_LANGUAGE_MARKDOWN: number = MarkupLanguage.Markdown; static MARKUP_LANGUAGE_HTML: number = MarkupLanguage.Html; private renderers_: any = {}; - private options_: any; + private options_: Options; private rawMarkdownIt_: any; - constructor(options: any) { - this.options_ = Object.assign({}, { + public constructor(options: Options) { + this.options_ = { ResourceModel: { isResourceUrl: () => false, }, - }, options); + isSafeMode: false, + ...options, + }; } - renderer(markupLanguage: MarkupLanguage) { + private renderer(markupLanguage: MarkupLanguage) { if (this.renderers_[markupLanguage]) return this.renderers_[markupLanguage]; let RendererClass = null; @@ -61,7 +72,7 @@ export default class MarkupToHtml { return this.renderers_[markupLanguage]; } - stripMarkup(markupLanguage: MarkupLanguage, markup: string, options: any = null) { + public stripMarkup(markupLanguage: MarkupLanguage, markup: string, options: any = null) { if (!markup) return ''; options = Object.assign({}, { @@ -89,16 +100,23 @@ export default class MarkupToHtml { return output; } - clearCache(markupLanguage: MarkupLanguage) { + public clearCache(markupLanguage: MarkupLanguage) { const r = this.renderer(markupLanguage); if (r.clearCache) r.clearCache(); } - async render(markupLanguage: MarkupLanguage, markup: string, theme: any, options: any): Promise { + public async render(markupLanguage: MarkupLanguage, markup: string, theme: any, options: any): Promise { + if (this.options_.isSafeMode) { + return { + html: `
${markup}
`, + cssStrings: [], + pluginAssets: [], + }; + } return this.renderer(markupLanguage).render(markup, theme, options); } - async allAssets(markupLanguage: MarkupLanguage, theme: any) { + public async allAssets(markupLanguage: MarkupLanguage, theme: any) { return this.renderer(markupLanguage).allAssets(theme); } } diff --git a/packages/server/src/apps/joplin/Application.ts b/packages/server/src/apps/joplin/Application.ts index f9cdb5516..180d83239 100644 --- a/packages/server/src/apps/joplin/Application.ts +++ b/packages/server/src/apps/joplin/Application.ts @@ -12,6 +12,7 @@ import FileModel from '../../models/FileModel'; import { ErrorNotFound } from '../../utils/errors'; import BaseApplication from '../../services/BaseApplication'; import { formatDateTime } from '../../utils/time'; +import { OptionsResourceModel } from '@joplin/renderer/MarkupToHtml'; const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js'); const { themeStyle } = require('@joplin/lib/theme'); @@ -163,7 +164,7 @@ export default class Application extends BaseApplication { private async renderNote(share: Share, note: NoteEntity, resourceInfos: ResourceInfos, linkedItemInfos: LinkedItemInfos): Promise { const markupToHtml = new MarkupToHtml({ - ResourceModel: Resource, + ResourceModel: Resource as OptionsResourceModel, }); const renderOptions: any = {