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.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
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.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

View File

@ -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'));

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 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>
);

View File

@ -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,
};
};

View File

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

View File

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

View File

@ -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);

View File

@ -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_);

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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 = {