1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-02 12:47:41 +02:00

Desktop: Resolves #4727: Add support for safe mode, which temporarily disables note rendering and plugins

This commit is contained in:
Laurent Cozic 2021-04-24 20:23:33 +02:00
parent 920f54f5d3
commit 3235f58f5a
13 changed files with 111 additions and 15 deletions

View File

@ -211,6 +211,9 @@ packages/app-desktop/commands/stopExternalEditing.js.map
packages/app-desktop/commands/toggleExternalEditing.d.ts packages/app-desktop/commands/toggleExternalEditing.d.ts
packages/app-desktop/commands/toggleExternalEditing.js packages/app-desktop/commands/toggleExternalEditing.js
packages/app-desktop/commands/toggleExternalEditing.js.map 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.d.ts
packages/app-desktop/gui/Button/Button.js packages/app-desktop/gui/Button/Button.js
packages/app-desktop/gui/Button/Button.js.map packages/app-desktop/gui/Button/Button.js.map

3
.gitignore vendored
View File

@ -198,6 +198,9 @@ packages/app-desktop/commands/stopExternalEditing.js.map
packages/app-desktop/commands/toggleExternalEditing.d.ts packages/app-desktop/commands/toggleExternalEditing.d.ts
packages/app-desktop/commands/toggleExternalEditing.js packages/app-desktop/commands/toggleExternalEditing.js
packages/app-desktop/commands/toggleExternalEditing.js.map 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.d.ts
packages/app-desktop/gui/Button/Button.js packages/app-desktop/gui/Button/Button.js
packages/app-desktop/gui/Button/Button.js.map packages/app-desktop/gui/Button/Button.js.map

View File

@ -89,10 +89,11 @@ const globalCommands = [
require('./commands/exportNotes'), require('./commands/exportNotes'),
require('./commands/focusElement'), require('./commands/focusElement'),
require('./commands/openProfileDirectory'), require('./commands/openProfileDirectory'),
require('./commands/replaceMisspelling'),
require('./commands/startExternalEditing'), require('./commands/startExternalEditing'),
require('./commands/stopExternalEditing'), require('./commands/stopExternalEditing'),
require('./commands/toggleExternalEditing'), require('./commands/toggleExternalEditing'),
require('./commands/replaceMisspelling'), require('./commands/toggleSafeMode'),
require('@joplin/lib/commands/historyBackward'), require('@joplin/lib/commands/historyBackward'),
require('@joplin/lib/commands/historyForward'), require('@joplin/lib/commands/historyForward'),
require('@joplin/lib/commands/synchronize'), require('@joplin/lib/commands/synchronize'),
@ -539,6 +540,7 @@ class Application extends BaseApplication {
const pluginRunner = new PluginRunner(); const pluginRunner = new PluginRunner();
service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store()); service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store());
service.isSafeMode = Setting.value('isSafeMode');
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states')); const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));

View 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();
},
};
};

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import versionInfo from '@joplin/lib/versionInfo'; import versionInfo from '@joplin/lib/versionInfo';
import PluginService from '@joplin/lib/services/plugins/PluginService'; import PluginService from '@joplin/lib/services/plugins/PluginService';
import Setting from '@joplin/lib/models/Setting'; import Setting from '@joplin/lib/models/Setting';
import bridge from '../services/bridge';
const packageInfo = require('../packageInfo.js'); const packageInfo = require('../packageInfo.js');
const ipcRenderer = require('electron').ipcRenderer; const ipcRenderer = require('electron').ipcRenderer;
@ -62,6 +63,12 @@ export default class ErrorBoundary extends React.Component<Props, State> {
render() { render() {
if (this.state.error) { if (this.state.error) {
const safeMode_click = async () => {
Setting.setValue('isSafeMode', true);
await Setting.saveAll();
bridge().restart();
};
try { try {
const output = []; 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' }}> <div style={{ overflow: 'auto', fontFamily: 'sans-serif', padding: '5px 20px' }}>
<h1>Error</h1> <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>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} {output}
</div> </div>
); );

View File

@ -63,6 +63,7 @@ interface Props {
settingEditorCodeView: boolean; settingEditorCodeView: boolean;
pluginsLegacy: any; pluginsLegacy: any;
startupPluginsLoaded: boolean; startupPluginsLoaded: boolean;
isSafeMode: boolean;
} }
interface State { 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. // 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 // If a note is being saved, we wait till it is saved and then call
// "appCloseReply" again. // "appCloseReply" again.
ipcRenderer.on('appClose', () => { ipcRenderer.on('appClose', async () => {
if (this.waitForNotesSavedIID_) shim.clearInterval(this.waitForNotesSavedIID_); if (this.waitForNotesSavedIID_) shim.clearInterval(this.waitForNotesSavedIID_);
this.waitForNotesSavedIID_ = null; this.waitForNotesSavedIID_ = null;
@ -489,8 +490,24 @@ class MainScreenComponent extends React.Component<Props, State> {
bridge().restart(); bridge().restart();
}; };
const onDisableSafeModeAndRestart = async () => {
Setting.setValue('isSafeMode', false);
await Setting.saveAll();
bridge().restart();
};
let msg = null; 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 = ( msg = (
<span> <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.')}{' '} {_('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; 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() { registerCommands() {
@ -777,6 +794,7 @@ const mapStateToProps = (state: AppState) => {
layoutMoveMode: state.layoutMoveMode, layoutMoveMode: state.layoutMoveMode,
mainLayout: state.mainLayout, mainLayout: state.mainLayout,
startupPluginsLoaded: state.startupPluginsLoaded, startupPluginsLoaded: state.startupPluginsLoaded,
isSafeMode: state.settings.isSafeMode,
}; };
}; };

View File

@ -737,6 +737,7 @@ function useMenu(props: Props) {
}, },
}, },
menuItemDic.toggleSafeMode,
menuItemDic.openProfileDirectory, menuItemDic.openProfileDirectory,
menuItemDic.copyDevCommand, menuItemDic.copyDevCommand,

View File

@ -43,5 +43,6 @@ export default function() {
'editor.sortSelectedLines', 'editor.sortSelectedLines',
'editor.swapLineUp', 'editor.swapLineUp',
'editor.swapLineDown', 'editor.swapLineDown',
'toggleSafeMode',
]; ];
} }

View File

@ -7,6 +7,7 @@ import htmlUtils from './htmlUtils';
import Resource from './models/Resource'; import Resource from './models/Resource';
export class MarkupLanguageUtils { export class MarkupLanguageUtils {
private lib_(language: MarkupLanguage) { private lib_(language: MarkupLanguage) {
if (language === MarkupLanguage.Html) return htmlUtils; if (language === MarkupLanguage.Html) return htmlUtils;
if (language === MarkupLanguage.Markdown) return markdownUtils; if (language === MarkupLanguage.Markdown) return markdownUtils;
@ -31,6 +32,7 @@ export class MarkupLanguageUtils {
pluginOptions: pluginOptions, pluginOptions: pluginOptions,
tempDir: Setting.value('tempDir'), tempDir: Setting.value('tempDir'),
fsDriver: shim.fsDriver(), fsDriver: shim.fsDriver(),
isSafeMode: Setting.value('isSafeMode'),
}, options); }, options);
return new MarkupToHtml(options); return new MarkupToHtml(options);

View File

@ -1133,6 +1133,14 @@ class Setting extends BaseModel {
'Restart app to see changes.'), 'Restart app to see changes.'),
storage: SettingStorage.File, storage: SettingStorage.File,
}, },
isSafeMode: {
value: false,
type: SettingItemType.Bool,
public: false,
appTypes: ['desktop'],
storage: SettingStorage.Database,
},
}; };
this.metadata_ = Object.assign(this.metadata_, this.customMetadata_); this.metadata_ = Object.assign(this.metadata_, this.customMetadata_);

View File

@ -74,6 +74,7 @@ export default class PluginService extends BaseService {
private plugins_: Plugins = {}; private plugins_: Plugins = {};
private runner_: BasePluginRunner = null; private runner_: BasePluginRunner = null;
private startedPlugins_: Record<string, boolean> = {}; private startedPlugins_: Record<string, boolean> = {};
private isSafeMode_: boolean = false;
public initialize(appVersion: string, platformImplementation: any, runner: BasePluginRunner, store: any) { public initialize(appVersion: string, platformImplementation: any, runner: BasePluginRunner, store: any) {
this.appVersion_ = appVersion; this.appVersion_ = appVersion;
@ -86,6 +87,14 @@ export default class PluginService extends BaseService {
return this.plugins_; return this.plugins_;
} }
public get isSafeMode(): boolean {
return this.isSafeMode_;
}
public set isSafeMode(v: boolean) {
this.isSafeMode_ = v;
}
private setPluginAt(pluginId: string, plugin: Plugin) { private setPluginAt(pluginId: string, plugin: Plugin) {
this.plugins_ = { this.plugins_ = {
...this.plugins_, ...this.plugins_,
@ -346,6 +355,8 @@ export default class PluginService extends BaseService {
} }
public async runPlugin(plugin: Plugin) { 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)) { 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_}.`); 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 { } else {

View File

@ -27,24 +27,35 @@ export interface RenderResult {
cssStrings: string[]; cssStrings: string[];
} }
export interface OptionsResourceModel {
isResourceUrl: (url: string)=> boolean;
}
export interface Options {
isSafeMode?: boolean;
ResourceModel: OptionsResourceModel;
}
export default class MarkupToHtml { export default class MarkupToHtml {
static MARKUP_LANGUAGE_MARKDOWN: number = MarkupLanguage.Markdown; static MARKUP_LANGUAGE_MARKDOWN: number = MarkupLanguage.Markdown;
static MARKUP_LANGUAGE_HTML: number = MarkupLanguage.Html; static MARKUP_LANGUAGE_HTML: number = MarkupLanguage.Html;
private renderers_: any = {}; private renderers_: any = {};
private options_: any; private options_: Options;
private rawMarkdownIt_: any; private rawMarkdownIt_: any;
constructor(options: any) { public constructor(options: Options) {
this.options_ = Object.assign({}, { this.options_ = {
ResourceModel: { ResourceModel: {
isResourceUrl: () => false, isResourceUrl: () => false,
}, },
}, options); isSafeMode: false,
...options,
};
} }
renderer(markupLanguage: MarkupLanguage) { private renderer(markupLanguage: MarkupLanguage) {
if (this.renderers_[markupLanguage]) return this.renderers_[markupLanguage]; if (this.renderers_[markupLanguage]) return this.renderers_[markupLanguage];
let RendererClass = null; let RendererClass = null;
@ -61,7 +72,7 @@ export default class MarkupToHtml {
return this.renderers_[markupLanguage]; return this.renderers_[markupLanguage];
} }
stripMarkup(markupLanguage: MarkupLanguage, markup: string, options: any = null) { public stripMarkup(markupLanguage: MarkupLanguage, markup: string, options: any = null) {
if (!markup) return ''; if (!markup) return '';
options = Object.assign({}, { options = Object.assign({}, {
@ -89,16 +100,23 @@ export default class MarkupToHtml {
return output; return output;
} }
clearCache(markupLanguage: MarkupLanguage) { public clearCache(markupLanguage: MarkupLanguage) {
const r = this.renderer(markupLanguage); const r = this.renderer(markupLanguage);
if (r.clearCache) r.clearCache(); 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); 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); return this.renderer(markupLanguage).allAssets(theme);
} }
} }

View File

@ -12,6 +12,7 @@ import FileModel from '../../models/FileModel';
import { ErrorNotFound } from '../../utils/errors'; import { ErrorNotFound } from '../../utils/errors';
import BaseApplication from '../../services/BaseApplication'; import BaseApplication from '../../services/BaseApplication';
import { formatDateTime } from '../../utils/time'; import { formatDateTime } from '../../utils/time';
import { OptionsResourceModel } from '@joplin/renderer/MarkupToHtml';
const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js'); const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js');
const { themeStyle } = require('@joplin/lib/theme'); 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> { private async renderNote(share: Share, note: NoteEntity, resourceInfos: ResourceInfos, linkedItemInfos: LinkedItemInfos): Promise<FileViewerResponse> {
const markupToHtml = new MarkupToHtml({ const markupToHtml = new MarkupToHtml({
ResourceModel: Resource, ResourceModel: Resource as OptionsResourceModel,
}); });
const renderOptions: any = { const renderOptions: any = {