You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Desktop: Resolves #4727: Add support for safe mode, which temporarily disables note rendering and plugins
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -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 | ||||
|   | ||||
| @@ -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')); | ||||
|  | ||||
|   | ||||
							
								
								
									
										20
									
								
								packages/app-desktop/commands/toggleSafeMode.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								packages/app-desktop/commands/toggleSafeMode.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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(); | ||||
| 		}, | ||||
| 	}; | ||||
| }; | ||||
| @@ -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<Props, State> { | ||||
|  | ||||
| 	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<Props, State> { | ||||
| 					<div style={{ overflow: 'auto', fontFamily: 'sans-serif', padding: '5px 20px' }}> | ||||
| 						<h1>Error</h1> | ||||
| 						<p>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.</p> | ||||
| 						<p>To continue you may close the app. Alternatively, if the error persists you may try to <a href="#" onClick={safeMode_click}>restart in safe mode</a>, which will temporarily disable all plugins.</p> | ||||
| 						{output} | ||||
| 					</div> | ||||
| 				); | ||||
|   | ||||
| @@ -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<Props, State> { | ||||
| 		// 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<Props, State> { | ||||
| 			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 = ( | ||||
| 				<span> | ||||
| 					{_('Safe mode is currently active. Note rendering and all plugins are temporarily disabled.')}{' '} | ||||
| 					<a href="#" onClick={() => onDisableSafeModeAndRestart()}> | ||||
| 						{_('Disable safe mode and restart')} | ||||
| 					</a> | ||||
| 				</span> | ||||
| 			); | ||||
| 		} else if (this.props.shouldUpgradeSyncTarget) { | ||||
| 			msg = ( | ||||
| 				<span> | ||||
| 					{_('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<Props, State> { | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	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, | ||||
| 	}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -737,6 +737,7 @@ function useMenu(props: Props) { | ||||
| 						}, | ||||
| 					}, | ||||
|  | ||||
| 					menuItemDic.toggleSafeMode, | ||||
| 					menuItemDic.openProfileDirectory, | ||||
| 					menuItemDic.copyDevCommand, | ||||
|  | ||||
|   | ||||
| @@ -43,5 +43,6 @@ export default function() { | ||||
| 		'editor.sortSelectedLines', | ||||
| 		'editor.swapLineUp', | ||||
| 		'editor.swapLineDown', | ||||
| 		'toggleSafeMode', | ||||
| 	]; | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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_); | ||||
|   | ||||
| @@ -74,6 +74,7 @@ export default class PluginService extends BaseService { | ||||
| 	private plugins_: Plugins = {}; | ||||
| 	private runner_: BasePluginRunner = null; | ||||
| 	private startedPlugins_: Record<string, boolean> = {}; | ||||
| 	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 { | ||||
|   | ||||
| @@ -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<RenderResult> { | ||||
| 	public async render(markupLanguage: MarkupLanguage, markup: string, theme: any, options: any): Promise<RenderResult> { | ||||
| 		if (this.options_.isSafeMode) { | ||||
| 			return { | ||||
| 				html: `<pre>${markup}</pre>`, | ||||
| 				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); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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<FileViewerResponse> { | ||||
| 		const markupToHtml = new MarkupToHtml({ | ||||
| 			ResourceModel: Resource, | ||||
| 			ResourceModel: Resource as OptionsResourceModel, | ||||
| 		}); | ||||
|  | ||||
| 		const renderOptions: any = { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user