You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Desktop: Resolves #7934: Don't create an extra copy of default plugins (load directly from the app bundle) (#9508)
This commit is contained in:
		| @@ -1,11 +1,9 @@ | ||||
| import { installDefaultPlugins, getDefaultPluginsInstallState, setSettingsForDefaultPlugins, checkPreInstalledDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils'; | ||||
| import { afterDefaultPluginsLoaded, getDefaultPluginPathsAndSettings } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils'; | ||||
| import PluginRunner from '../../../app/services/plugins/PluginRunner'; | ||||
| import { pathExists } from 'fs-extra'; | ||||
| import { checkThrow, setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils'; | ||||
| import PluginService, { defaultPluginSetting, DefaultPluginsInfo, PluginSettings } from '@joplin/lib/services/plugins/PluginService'; | ||||
| import PluginService, { defaultPluginSetting, DefaultPluginsInfo } from '@joplin/lib/services/plugins/PluginService'; | ||||
| import Setting from '@joplin/lib/models/Setting'; | ||||
|  | ||||
| const testPluginDir = `${supportDir}/plugins`; | ||||
|  | ||||
| function newPluginService(appVersion = '1.4') { | ||||
| 	const runner = new PluginRunner(); | ||||
| @@ -27,13 +25,17 @@ function newPluginService(appVersion = '1.4') { | ||||
| describe('defaultPluginsUtils', () => { | ||||
|  | ||||
| 	const pluginsId = ['joplin.plugin.ambrt.backlinksToNote', 'org.joplinapp.plugins.ToggleSidebars']; | ||||
| 	const defaultPluginsInfo = { | ||||
| 		'joplin.plugin.ambrt.backlinksToNote': {}, | ||||
| 		'org.joplinapp.plugins.ToggleSidebars': {}, | ||||
| 	}; | ||||
|  | ||||
| 	beforeEach(async () => { | ||||
| 		await setupDatabaseAndSynchronizer(1); | ||||
| 		await switchClient(1); | ||||
| 	}); | ||||
|  | ||||
| 	it('should install default plugins with no previous default plugins installed', (async () => { | ||||
| 	it('should load default plugins when nor previously installed', (async () => { | ||||
| 		const testPluginDir = `${supportDir}/pluginRepo/plugins`; | ||||
| 		Setting.setValue('installedDefaultPlugins', []); | ||||
|  | ||||
| @@ -41,151 +43,69 @@ describe('defaultPluginsUtils', () => { | ||||
|  | ||||
| 		const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states')); | ||||
|  | ||||
| 		const newPluginsSettings = await installDefaultPlugins(service, testPluginDir, pluginsId, pluginSettings); | ||||
| 		for (const pluginId of pluginsId) { | ||||
| 			expect(pluginSettings[pluginId]).toBeFalsy(); | ||||
| 		} | ||||
|  | ||||
| 		const installedPluginPath1 = `${Setting.value('pluginDir')}/${pluginsId[0]}.jpl`; | ||||
| 		const installedPluginPath2 = `${Setting.value('pluginDir')}/${pluginsId[1]}.jpl`; | ||||
|  | ||||
| 		expect(await pathExists(installedPluginPath1)).toBe(true); | ||||
| 		expect(await pathExists(installedPluginPath2)).toBe(true); | ||||
|  | ||||
| 		expect(newPluginsSettings[pluginsId[0]]).toMatchObject(defaultPluginSetting()); | ||||
| 		expect(newPluginsSettings[pluginsId[1]]).toMatchObject(defaultPluginSetting()); | ||||
| 		const pluginPathsAndNewSettings = await getDefaultPluginPathsAndSettings(testPluginDir, defaultPluginsInfo, pluginSettings); | ||||
|  | ||||
| 		for (const pluginId of pluginsId) { | ||||
| 			expect( | ||||
| 				pluginPathsAndNewSettings.pluginSettings[pluginId], | ||||
| 			).toMatchObject(defaultPluginSetting()); | ||||
| 		} | ||||
| 	})); | ||||
|  | ||||
| 	it('should install default plugins with previous default plugins installed', (async () => { | ||||
|  | ||||
| 	it('should keep already created default plugins disabled with previous default plugins installed', (async () => { | ||||
| 		const testPluginDir = `${supportDir}/pluginRepo/plugins`; | ||||
| 		Setting.setValue('installedDefaultPlugins', ['org.joplinapp.plugins.ToggleSidebars']); | ||||
| 		Setting.setValue('plugins.states', { | ||||
| 			'org.joplinapp.plugins.ToggleSidebars': { ...defaultPluginSetting(), enabled: false }, | ||||
| 		}); | ||||
|  | ||||
| 		const service = newPluginService('2.1'); | ||||
|  | ||||
| 		const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states')); | ||||
| 		const pluginPathsAndNewSettings = await getDefaultPluginPathsAndSettings(testPluginDir, defaultPluginsInfo, pluginSettings); | ||||
|  | ||||
| 		const newPluginsSettings = await installDefaultPlugins(service, testPluginDir, pluginsId, pluginSettings); | ||||
|  | ||||
| 		const installedPluginPath1 = `${Setting.value('pluginDir')}/${pluginsId[0]}.jpl`; | ||||
| 		const installedPluginPath2 = `${Setting.value('pluginDir')}/${pluginsId[1]}.jpl`; | ||||
|  | ||||
| 		expect(await pathExists(installedPluginPath1)).toBe(true); | ||||
| 		expect(await pathExists(installedPluginPath2)).toBe(false); | ||||
|  | ||||
| 		expect(newPluginsSettings[pluginsId[0]]).toMatchObject(defaultPluginSetting()); | ||||
| 		expect(newPluginsSettings[pluginsId[1]]).toBeUndefined(); | ||||
| 		// Should still be disabled | ||||
| 		expect( | ||||
| 			pluginPathsAndNewSettings.pluginSettings['org.joplinapp.plugins.ToggleSidebars'].enabled, | ||||
| 		).toBe(false); | ||||
| 	})); | ||||
|  | ||||
| 	it('should get default plugins install state', (async () => { | ||||
| 		const testCases = [ | ||||
| 			{ | ||||
| 				'installedDefaultPlugins': [''], | ||||
| 				'loadingPlugins': [`${testPluginDir}/simple`, `${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`], | ||||
| 				'plugin1DefaultState': defaultPluginSetting(), | ||||
| 				'plugin2DefaultState': defaultPluginSetting(), | ||||
| 				'installedDefaultPlugins1': true, | ||||
| 				'installedDefaultPlugins2': true, | ||||
| 			}, | ||||
| 			{ | ||||
| 				'installedDefaultPlugins': [''], | ||||
| 				'loadingPlugins': [`${testPluginDir}/simple`], | ||||
| 				'plugin1DefaultState': defaultPluginSetting(), | ||||
| 				'plugin2DefaultState': undefined, | ||||
| 				'installedDefaultPlugins1': true, | ||||
| 				'installedDefaultPlugins2': false, | ||||
| 			}, | ||||
| 			{ | ||||
| 				'installedDefaultPlugins': ['org.joplinapp.plugins.Simple'], | ||||
| 				'loadingPlugins': [`${testPluginDir}/simple`, `${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`], | ||||
| 				'plugin1DefaultState': undefined, | ||||
| 				'plugin2DefaultState': defaultPluginSetting(), | ||||
| 				'installedDefaultPlugins1': true, | ||||
| 				'installedDefaultPlugins2': true, | ||||
| 			}, | ||||
| 			{ | ||||
| 				'installedDefaultPlugins': ['org.joplinapp.plugins.Simple'], | ||||
| 				'loadingPlugins': [`${testPluginDir}/simple`], | ||||
| 				'plugin1DefaultState': undefined, | ||||
| 				'plugin2DefaultState': undefined, | ||||
| 				'installedDefaultPlugins1': true, | ||||
| 				'installedDefaultPlugins2': false, | ||||
| 			}, | ||||
| 		]; | ||||
|  | ||||
| 		for (const testCase of testCases) { | ||||
| 			const service = newPluginService(); | ||||
| 			const pluginsId = ['org.joplinapp.plugins.Simple', 'org.joplinapp.FirstJplPlugin']; | ||||
|  | ||||
| 			Setting.setValue('installedDefaultPlugins', testCase.installedDefaultPlugins); | ||||
| 			await service.loadAndRunPlugins(testCase.loadingPlugins, {}); | ||||
|  | ||||
| 			// setting installedDefaultPlugins state | ||||
| 			const defaultInstallStates: PluginSettings = getDefaultPluginsInstallState(service, pluginsId); | ||||
|  | ||||
| 			expect(defaultInstallStates[pluginsId[0]]).toStrictEqual(testCase.plugin1DefaultState); | ||||
| 			expect(defaultInstallStates[pluginsId[1]]).toStrictEqual(testCase.plugin2DefaultState); | ||||
|  | ||||
|  | ||||
| 			const installedDefaultPlugins = Setting.value('installedDefaultPlugins'); | ||||
| 			expect(installedDefaultPlugins.includes(pluginsId[0])).toBe(testCase.installedDefaultPlugins1); | ||||
| 			expect(installedDefaultPlugins.includes(pluginsId[1])).toBe(testCase.installedDefaultPlugins2); | ||||
|  | ||||
| 		} | ||||
|  | ||||
| 	})); | ||||
|  | ||||
| 	it('should check pre-installed default plugins', (async () => { | ||||
| 		// with previous pre-installed default plugins | ||||
| 		Setting.setValue('installedDefaultPlugins', ['']); | ||||
| 		let pluginSettings, installedDefaultPlugins; | ||||
|  | ||||
| 		pluginSettings = { [pluginsId[0]]: defaultPluginSetting() }; | ||||
| 		checkPreInstalledDefaultPlugins(pluginsId, pluginSettings); | ||||
|  | ||||
| 		installedDefaultPlugins = Setting.value('installedDefaultPlugins'); | ||||
| 		expect(installedDefaultPlugins.includes(pluginsId[0])).toBe(true); | ||||
| 		expect(installedDefaultPlugins.includes(pluginsId[1])).toBe(false); | ||||
|  | ||||
|  | ||||
| 		// with no previous pre-installed default plugins | ||||
| 		Setting.setValue('installedDefaultPlugins', ['not-a-default-plugin']); | ||||
| 		pluginSettings = {}; | ||||
| 		checkPreInstalledDefaultPlugins(pluginsId, pluginSettings); | ||||
|  | ||||
| 		installedDefaultPlugins = Setting.value('installedDefaultPlugins'); | ||||
| 		expect(installedDefaultPlugins.includes(pluginsId[0])).toBe(false); | ||||
| 		expect(installedDefaultPlugins.includes(pluginsId[1])).toBe(false); | ||||
|  | ||||
| 	})); | ||||
| 	const sampleJsBundlePlugin = ` | ||||
| 	/* joplin-manifest: | ||||
| 	{ | ||||
| 		"id": "io.github.jackgruber.backup", | ||||
| 		"manifest_version": 1, | ||||
| 		"app_min_version": "1.4", | ||||
| 		"name": "JS Bundle test", | ||||
| 		"version": "1.0.0" | ||||
| 	} | ||||
| 	*/ | ||||
| 	joplin.plugins.register({ | ||||
| 		onStart: async function() { | ||||
| 			await joplin.settings.registerSettings({ | ||||
| 				path: { | ||||
| 					value: "initial-path", | ||||
| 					type: 2, | ||||
| 					section: "backupSection", | ||||
| 					public: true, | ||||
| 					label: "Backup path", | ||||
| 				  }, | ||||
| 			}) | ||||
| 		}, | ||||
| 	});`; | ||||
|  | ||||
| 	it('should set initial settings for default plugins', async () => { | ||||
| 		const service = newPluginService(); | ||||
|  | ||||
| 		const pluginScript = ` | ||||
| 		/* joplin-manifest: | ||||
| 		{ | ||||
| 			"id": "io.github.jackgruber.backup", | ||||
| 			"manifest_version": 1, | ||||
| 			"app_min_version": "1.4", | ||||
| 			"name": "JS Bundle test", | ||||
| 			"version": "1.0.0" | ||||
| 		} | ||||
| 		*/ | ||||
| 		joplin.plugins.register({ | ||||
| 			onStart: async function() { | ||||
| 				await joplin.settings.registerSettings({ | ||||
| 					path: { | ||||
| 						value: "initial-path", | ||||
| 						type: 2, | ||||
| 						section: "backupSection", | ||||
| 						public: true, | ||||
| 						label: "Backup path", | ||||
| 					  }, | ||||
| 				}) | ||||
| 			}, | ||||
| 		});`; | ||||
|  | ||||
| 		const plugin = await service.loadPluginFromJsBundle('', pluginScript); | ||||
| 		const plugin = await service.loadPluginFromJsBundle('', sampleJsBundlePlugin); | ||||
| 		plugin.builtIn = true; | ||||
| 		await service.runPlugin(plugin); | ||||
| 		const runningPlugins = { 'io.github.jackgruber.backup': plugin }; | ||||
|  | ||||
| 		const defaultPluginsInfo: DefaultPluginsInfo = { | ||||
| 			'io.github.jackgruber.backup': { | ||||
| @@ -199,46 +119,64 @@ describe('defaultPluginsUtils', () => { | ||||
|  | ||||
| 		// with pre-installed default plugin | ||||
| 		Setting.setValue('installedDefaultPlugins', ['io.github.jackgruber.backup']); | ||||
| 		setSettingsForDefaultPlugins(defaultPluginsInfo); | ||||
| 		const pluginSettings = { 'io.github.jackgruber.backup': defaultPluginSetting() }; | ||||
|  | ||||
| 		await afterDefaultPluginsLoaded( | ||||
| 			runningPlugins, | ||||
| 			defaultPluginsInfo, | ||||
| 			pluginSettings, | ||||
| 		); | ||||
| 		expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe('initial-path'); | ||||
| 		await service.destroy(); | ||||
|  | ||||
| 		// with no pre-installed default plugin | ||||
| 		Setting.setValue('installedDefaultPlugins', ['']); | ||||
| 		setSettingsForDefaultPlugins(defaultPluginsInfo); | ||||
| 		await afterDefaultPluginsLoaded( | ||||
| 			runningPlugins, | ||||
| 			defaultPluginsInfo, | ||||
| 			pluginSettings, | ||||
| 		); | ||||
| 		expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe(`${Setting.value('profileDir')}`); | ||||
| 		await service.destroy(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should not overwrite existing settings for a user-installed version of a built-in plugin', async () => { | ||||
| 		const service = newPluginService(); | ||||
|  | ||||
| 		const plugin = await service.loadPluginFromJsBundle('', sampleJsBundlePlugin); | ||||
| 		plugin.builtIn = false; | ||||
| 		await service.runPlugin(plugin); | ||||
|  | ||||
| 		const defaultPluginsInfo: DefaultPluginsInfo = { | ||||
| 			'io.github.jackgruber.backup': { | ||||
| 				settings: { | ||||
| 					'path': 'overwrite?', | ||||
| 				}, | ||||
| 			}, | ||||
| 		}; | ||||
|  | ||||
| 		// No pre-installed default plugins | ||||
| 		Setting.setValue('installedDefaultPlugins', []); | ||||
|  | ||||
| 		// The plugin is running and enabled | ||||
| 		const runningPlugins = { 'io.github.jackgruber.backup': plugin }; | ||||
| 		const pluginSettings = { 'io.github.jackgruber.backup': defaultPluginSetting() }; | ||||
|  | ||||
| 		await afterDefaultPluginsLoaded( | ||||
| 			runningPlugins, | ||||
| 			defaultPluginsInfo, | ||||
| 			pluginSettings, | ||||
| 		); | ||||
|  | ||||
| 		// Should not overwrite | ||||
| 		expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe('initial-path'); | ||||
| 	}); | ||||
|  | ||||
| 	it('should not throw error on missing setting key', async () => { | ||||
|  | ||||
| 		const service = newPluginService(); | ||||
|  | ||||
| 		const pluginScript = ` | ||||
| 		/* joplin-manifest: | ||||
| 		{ | ||||
| 			"id": "io.github.jackgruber.backup", | ||||
| 			"manifest_version": 1, | ||||
| 			"app_min_version": "1.4", | ||||
| 			"name": "JS Bundle test", | ||||
| 			"version": "1.0.0" | ||||
| 		} | ||||
| 		*/ | ||||
| 		joplin.plugins.register({ | ||||
| 			onStart: async function() { | ||||
| 				await joplin.settings.registerSettings({ | ||||
| 					path: { | ||||
| 						value: "initial-path", | ||||
| 						type: 2, | ||||
| 						section: "backupSection", | ||||
| 						public: true, | ||||
| 						label: "Backup path", | ||||
| 					  }, | ||||
| 				}) | ||||
| 			}, | ||||
| 		});`; | ||||
|  | ||||
| 		const plugin = await service.loadPluginFromJsBundle('', pluginScript); | ||||
| 		const plugin = await service.loadPluginFromJsBundle('', sampleJsBundlePlugin); | ||||
| 		plugin.builtIn = true; | ||||
| 		await service.runPlugin(plugin); | ||||
|  | ||||
| 		const defaultPluginsInfo: DefaultPluginsInfo = { | ||||
| @@ -256,7 +194,10 @@ describe('defaultPluginsUtils', () => { | ||||
| 		}; | ||||
|  | ||||
| 		Setting.setValue('installedDefaultPlugins', ['']); | ||||
| 		expect(checkThrow(() => setSettingsForDefaultPlugins(defaultPluginsInfo))).toBe(false); | ||||
| 		const pluginSettings = { 'io.github.jackgruber.backup': defaultPluginSetting() }; | ||||
| 		const runningPlugins = { 'io.github.jackgruber.backup': plugin }; | ||||
|  | ||||
| 		expect(checkThrow(() => afterDefaultPluginsLoaded(runningPlugins, defaultPluginsInfo, pluginSettings))).toBe(false); | ||||
| 		expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe(`${Setting.value('profileDir')}`); | ||||
| 		await service.destroy(); | ||||
| 	}); | ||||
|   | ||||
| @@ -65,7 +65,7 @@ import { AppState } from './app.reducer'; | ||||
| import syncDebugLog from '@joplin/lib/services/synchronizer/syncDebugLog'; | ||||
| import eventManager, { EventName } from '@joplin/lib/eventManager'; | ||||
| import path = require('path'); | ||||
| import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils'; | ||||
| import { afterDefaultPluginsLoaded, loadAndRunDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils'; | ||||
| import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher'; | ||||
| import { parseNotesParent } from '@joplin/lib/reducer'; | ||||
| import OcrService from '@joplin/lib/services/ocr/OcrService'; | ||||
| @@ -277,7 +277,6 @@ class Application extends BaseApplication { | ||||
| 		const pluginRunner = new PluginRunner(); | ||||
| 		service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store()); | ||||
| 		service.isSafeMode = Setting.value('isSafeMode'); | ||||
| 		const defaultPluginsId = Object.keys(getDefaultPluginsInfo()); | ||||
|  | ||||
| 		let pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states')); | ||||
| 		{ | ||||
| @@ -285,15 +284,11 @@ class Application extends BaseApplication { | ||||
| 			// time, however we only effectively uninstall the plugin the next | ||||
| 			// time the app is started. What plugin should be uninstalled is | ||||
| 			// stored in the settings. | ||||
| 			const newSettings = service.clearUpdateState(await service.uninstallPlugins(pluginSettings)); | ||||
| 			Setting.setValue('plugins.states', newSettings); | ||||
| 			pluginSettings = service.clearUpdateState(await service.uninstallPlugins(pluginSettings)); | ||||
| 			Setting.setValue('plugins.states', pluginSettings); | ||||
| 		} | ||||
|  | ||||
| 		checkPreInstalledDefaultPlugins(defaultPluginsId, pluginSettings); | ||||
|  | ||||
| 		try { | ||||
| 			const defaultPluginsDir = path.join(bridge().buildDir(), 'defaultPlugins'); | ||||
| 			pluginSettings = await installDefaultPlugins(service, defaultPluginsDir, defaultPluginsId, pluginSettings); | ||||
| 			if (await shim.fsDriver().exists(Setting.value('pluginDir'))) { | ||||
| 				await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings); | ||||
| 			} | ||||
| @@ -302,19 +297,31 @@ class Application extends BaseApplication { | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			const devPluginOptions = { devMode: true, builtIn: false }; | ||||
|  | ||||
| 			if (Setting.value('plugins.devPluginPaths')) { | ||||
| 				const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim()); | ||||
| 				await service.loadAndRunPlugins(paths, pluginSettings, true); | ||||
| 				await service.loadAndRunPlugins(paths, pluginSettings, devPluginOptions); | ||||
| 			} | ||||
|  | ||||
| 			// Also load dev plugins that have passed via command line arguments | ||||
| 			if (Setting.value('startupDevPlugins')) { | ||||
| 				await service.loadAndRunPlugins(Setting.value('startupDevPlugins'), pluginSettings, true); | ||||
| 				await service.loadAndRunPlugins(Setting.value('startupDevPlugins'), pluginSettings, devPluginOptions); | ||||
| 			} | ||||
| 		} catch (error) { | ||||
| 			this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error); | ||||
| 		} | ||||
|  | ||||
| 		// Load default plugins after loading other plugins -- this allows users | ||||
| 		// to override built-in plugins with development versions with the same | ||||
| 		// ID. | ||||
| 		const defaultPluginsDir = path.join(bridge().buildDir(), 'defaultPlugins'); | ||||
| 		try { | ||||
| 			pluginSettings = await loadAndRunDefaultPlugins(service, defaultPluginsDir, getDefaultPluginsInfo(), pluginSettings); | ||||
| 		} catch (error) { | ||||
| 			this.logger().error(`There was an error loading plugins from ${defaultPluginsDir}:`, error); | ||||
| 		} | ||||
|  | ||||
| 		{ | ||||
| 			// Users can potentially delete files from /plugins or even delete | ||||
| 			// the complete folder. When that happens, we still have the plugin | ||||
| @@ -322,7 +329,7 @@ class Application extends BaseApplication { | ||||
| 			// out we remove from the state any plugin that has *not* been loaded | ||||
| 			// above (meaning the file was missing). | ||||
| 			// https://github.com/laurent22/joplin/issues/5253 | ||||
| 			const oldSettings = service.unserializePluginSettings(Setting.value('plugins.states')); | ||||
| 			const oldSettings = pluginSettings; | ||||
| 			const newSettings: PluginSettings = {}; | ||||
| 			for (const pluginId of Object.keys(oldSettings)) { | ||||
| 				if (!service.pluginIds.includes(pluginId)) { | ||||
| @@ -332,6 +339,7 @@ class Application extends BaseApplication { | ||||
| 				newSettings[pluginId] = oldSettings[pluginId]; | ||||
| 			} | ||||
| 			Setting.setValue('plugins.states', newSettings); | ||||
| 			pluginSettings = newSettings; | ||||
| 		} | ||||
|  | ||||
| 		this.checkAllPluginStartedIID_ = setInterval(() => { | ||||
| @@ -346,7 +354,7 @@ class Application extends BaseApplication { | ||||
| 				// tests to wait for plugins to load. | ||||
| 				ipcRenderer.send('startup-plugins-loaded'); | ||||
|  | ||||
| 				setSettingsForDefaultPlugins(getDefaultPluginsInfo()); | ||||
| 				void afterDefaultPluginsLoaded(service.plugins, getDefaultPluginsInfo(), pluginSettings); | ||||
| 			} | ||||
| 		}, 500); | ||||
| 	} | ||||
|   | ||||
| @@ -15,9 +15,6 @@ import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; | ||||
| import * as shared from '@joplin/lib/components/shared/config/config-shared.js'; | ||||
| import ClipperConfigScreen from '../ClipperConfigScreen'; | ||||
| import restart from '../../services/restart'; | ||||
| import PluginService from '@joplin/lib/services/plugins/PluginService'; | ||||
| import { getDefaultPluginsInstallState, updateDefaultPluginsInstallState } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils'; | ||||
| import getDefaultPluginsInfo from '@joplin/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo'; | ||||
| import JoplinCloudConfigScreen from '../JoplinCloudConfigScreen'; | ||||
| import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButton'; | ||||
| import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning'; | ||||
| @@ -74,7 +71,6 @@ class ConfigScreenComponent extends React.Component<any, any> { | ||||
| 				this.switchSection(this.props.defaultSection); | ||||
| 			}); | ||||
| 		} | ||||
| 		updateDefaultPluginsInstallState(getDefaultPluginsInstallState(PluginService.instance(), Object.keys(getDefaultPluginsInfo())), this); | ||||
| 	} | ||||
|  | ||||
| 	private async handleSettingButton(key: string) { | ||||
|   | ||||
| @@ -43,6 +43,7 @@ function manifestToItem(manifest: PluginManifest): PluginItem { | ||||
| 		enabled: true, | ||||
| 		deleted: false, | ||||
| 		devMode: false, | ||||
| 		builtIn: false, | ||||
| 		hasBeenUpdated: false, | ||||
| 	}; | ||||
| } | ||||
| @@ -52,6 +53,7 @@ export interface PluginItem { | ||||
| 	enabled: boolean; | ||||
| 	deleted: boolean; | ||||
| 	devMode: boolean; | ||||
| 	builtIn: boolean; | ||||
| 	hasBeenUpdated: boolean; | ||||
| } | ||||
|  | ||||
| @@ -184,7 +186,10 @@ export default function(props: Props) { | ||||
| 	} | ||||
|  | ||||
| 	function renderDeleteButton() { | ||||
| 		// Built-in plugins can only be disabled | ||||
| 		if (item.builtIn) return null; | ||||
| 		if (!props.onDelete) return null; | ||||
|  | ||||
| 		return <Button level={ButtonLevel.Secondary} onClick={() => props.onDelete({ item })} title={_('Delete')}/>; | ||||
| 	} | ||||
|  | ||||
| @@ -221,8 +226,7 @@ export default function(props: Props) { | ||||
| 	} | ||||
|  | ||||
| 	const renderDefaultPluginLabel = () => { | ||||
| 		// Built-in plugins can only be disabled | ||||
| 		if (item.manifest._built_in) { | ||||
| 		if (item.builtIn) { | ||||
| 			return ( | ||||
| 				<BoxedLabel>{_('Built in')}</BoxedLabel> | ||||
| 			); | ||||
|   | ||||
| @@ -81,6 +81,7 @@ function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[ | ||||
| 				enabled: setting.enabled, | ||||
| 				deleted: setting.deleted, | ||||
| 				devMode: plugin.devMode, | ||||
| 				builtIn: plugin.builtIn, | ||||
| 				hasBeenUpdated: setting.hasBeenUpdated, | ||||
| 			}); | ||||
| 		} | ||||
| @@ -149,8 +150,8 @@ export default function(props: Props) { | ||||
| 		async function fetchPluginIds() { | ||||
| 			// Built-in plugins can't be updated from the main repoApi | ||||
| 			const nonDefaultPlugins = pluginItems | ||||
| 				.map(p => p.manifest) | ||||
| 				.filter(manifest => !manifest._built_in); | ||||
| 				.filter(plugin => !plugin.builtIn) | ||||
| 				.map(p => p.manifest); | ||||
|  | ||||
| 			const pluginIds = await repoApi().canBeUpdatedPlugins(nonDefaultPlugins, pluginService.appVersion); | ||||
| 			if (cancelled) return; | ||||
| @@ -288,8 +289,8 @@ export default function(props: Props) { | ||||
| 				</UserPluginsRoot> | ||||
| 			); | ||||
| 		} else { | ||||
| 			const nonDefaultPlugins = pluginItems.filter(item => !item.manifest._built_in); | ||||
| 			const defaultPlugins = pluginItems.filter(item => item.manifest._built_in); | ||||
| 			const nonDefaultPlugins = pluginItems.filter(item => !item.builtIn); | ||||
| 			const defaultPlugins = pluginItems.filter(item => item.builtIn); | ||||
| 			return ( | ||||
| 				<> | ||||
| 					<UserPluginsRoot> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
|  | ||||
| /* eslint-disable no-console */ | ||||
|  | ||||
| import { copy, exists, remove, mkdirp, readdir, mkdtemp, readFile, writeFile } from 'fs-extra'; | ||||
| import { copy, exists, remove, mkdirp, readdir, mkdtemp } from 'fs-extra'; | ||||
| import { join, resolve, basename } from 'path'; | ||||
| import { tmpdir } from 'os'; | ||||
| import { chdir, cwd } from 'process'; | ||||
| @@ -62,12 +62,6 @@ const buildDefaultPlugins = async (outputParentDir: string|null, beforeInstall: | ||||
| 			logStatus('Initializing repository.'); | ||||
| 			await execCommand('git init . -b main'); | ||||
|  | ||||
| 			logStatus('Marking manifest as built-in'); | ||||
| 			const manifestFile = './src/manifest.json'; | ||||
| 			const manifest = JSON.parse(await readFile(manifestFile, 'utf8')); | ||||
| 			manifest._built_in = true; | ||||
| 			await writeFile(manifestFile, JSON.stringify(manifest, undefined, '\t')); | ||||
|  | ||||
| 			logStatus('Creating initial commit.'); | ||||
| 			await execCommand('git add .'); | ||||
| 			await execCommand(['git', 'config', 'user.name', 'Build script']); | ||||
|   | ||||
| @@ -32,6 +32,7 @@ export default class Plugin { | ||||
| 	private dispatch_: Function; | ||||
| 	private eventEmitter_: any; | ||||
| 	private devMode_ = false; | ||||
| 	private builtIn_ = false; | ||||
| 	// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied | ||||
| 	private messageListener_: Function = null; | ||||
| 	// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied | ||||
| @@ -61,6 +62,14 @@ export default class Plugin { | ||||
| 		this.devMode_ = v; | ||||
| 	} | ||||
|  | ||||
| 	public get builtIn(): boolean { | ||||
| 		return this.builtIn_; | ||||
| 	} | ||||
|  | ||||
| 	public set builtIn(builtIn: boolean) { | ||||
| 		this.builtIn_ = builtIn; | ||||
| 	} | ||||
|  | ||||
| 	public get manifest(): PluginManifest { | ||||
| 		return this.manifest_; | ||||
| 	} | ||||
|   | ||||
| @@ -63,6 +63,11 @@ export interface PluginSettings { | ||||
| 	[pluginId: string]: PluginSetting; | ||||
| } | ||||
|  | ||||
| interface PluginLoadOptions { | ||||
| 	devMode: boolean; | ||||
| 	builtIn: boolean; | ||||
| } | ||||
|  | ||||
| function makePluginId(source: string): string { | ||||
| 	// https://www.npmjs.com/package/slug#options | ||||
| 	return uslug(source).substr(0, 32); | ||||
| @@ -340,7 +345,14 @@ export default class PluginService extends BaseService { | ||||
| 		return this.runner_.callStatsSummary(pluginId, duration); | ||||
| 	} | ||||
|  | ||||
| 	public async loadAndRunPlugins(pluginDirOrPaths: string | string[], settings: PluginSettings, devMode = false) { | ||||
| 	public async loadAndRunPlugins( | ||||
| 		pluginDirOrPaths: string | string[], settings: PluginSettings, options?: PluginLoadOptions, | ||||
| 	) { | ||||
| 		options ??= { | ||||
| 			builtIn: false, | ||||
| 			devMode: false, | ||||
| 		}; | ||||
|  | ||||
| 		let pluginPaths = []; | ||||
|  | ||||
| 		if (Array.isArray(pluginDirOrPaths)) { | ||||
| @@ -370,6 +382,10 @@ export default class PluginService extends BaseService { | ||||
| 				// such folders but to keep things sane we disallow it. | ||||
| 				if (this.plugins_[plugin.id]) throw new Error(`There is already a plugin with this ID: ${plugin.id}`); | ||||
|  | ||||
| 				// We mark the plugin as built-in even if not enabled (being built-in affects | ||||
| 				// update UI). | ||||
| 				plugin.builtIn = options.builtIn; | ||||
|  | ||||
| 				this.setPluginAt(plugin.id, plugin); | ||||
|  | ||||
| 				if (!this.pluginEnabled(settings, plugin.id)) { | ||||
| @@ -377,7 +393,7 @@ export default class PluginService extends BaseService { | ||||
| 					continue; | ||||
| 				} | ||||
|  | ||||
| 				plugin.devMode = devMode; | ||||
| 				plugin.devMode = options.devMode; | ||||
|  | ||||
| 				await this.runPlugin(plugin); | ||||
| 			} catch (error) { | ||||
|   | ||||
| @@ -1,90 +1,111 @@ | ||||
| import produce from 'immer'; | ||||
| import path = require('path'); | ||||
| import Setting from '../../../models/Setting'; | ||||
| import shim from '../../../shim'; | ||||
| import PluginService, { defaultPluginSetting, DefaultPluginsInfo, PluginSettings } from '../PluginService'; | ||||
| import PluginService, { defaultPluginSetting, DefaultPluginsInfo, Plugins, PluginSettings } from '../PluginService'; | ||||
| import Logger from '@joplin/utils/Logger'; | ||||
| import * as React from 'react'; | ||||
| const shared = require('../../../components/shared/config/config-shared.js'); | ||||
| import { join } from 'path'; | ||||
|  | ||||
| const logger = Logger.create('defaultPluginsUtils'); | ||||
|  | ||||
| export function checkPreInstalledDefaultPlugins(defaultPluginsId: string[], pluginSettings: PluginSettings) { | ||||
| 	const installedDefaultPlugins: string[] = Setting.value('installedDefaultPlugins'); | ||||
| 	for (const pluginId of defaultPluginsId) { | ||||
| 		// if pluginId is present in pluginSettings and not in installedDefaultPlugins array, | ||||
| 		// then its either pre-installed by user or just uninstalled | ||||
| 		if (pluginSettings[pluginId] && !installedDefaultPlugins.includes(pluginId)) Setting.setArrayValue('installedDefaultPlugins', pluginId); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| export async function installDefaultPlugins(service: PluginService, defaultPluginsDir: string, defaultPluginsId: string[], pluginSettings: PluginSettings): Promise<PluginSettings> { | ||||
| // Use loadAndRunDefaultPlugins | ||||
| // Exported for testing. | ||||
| export const getDefaultPluginPathsAndSettings = async ( | ||||
| 	defaultPluginsDir: string, defaultPluginsInfo: DefaultPluginsInfo, pluginSettings: PluginSettings, | ||||
| ) => { | ||||
| 	const pluginPaths: string[] = []; | ||||
|  | ||||
| 	if (!await shim.fsDriver().exists(defaultPluginsDir)) { | ||||
| 		logger.info(`Could not find default plugins' directory: ${defaultPluginsDir} - skipping installation.`); | ||||
| 		return pluginSettings; | ||||
| 		return { pluginPaths, pluginSettings }; | ||||
| 	} | ||||
|  | ||||
| 	const defaultPluginsPaths = await shim.fsDriver().readDirStats(defaultPluginsDir); | ||||
| 	if (defaultPluginsPaths.length <= 0) { | ||||
| 		logger.info(`Default plugins' directory is empty: ${defaultPluginsDir} - skipping installation.`); | ||||
| 		return pluginSettings; | ||||
| 		logger.info(`Default plugins' directory is empty: ${defaultPluginsDir} - no default plugins will be installed.`); | ||||
| 	} | ||||
|  | ||||
| 	const installedPlugins = Setting.value('installedDefaultPlugins'); | ||||
|  | ||||
| 	for (const pluginStat of defaultPluginsPaths) { | ||||
| 		const pluginId = pluginStat.path; | ||||
| 		// Each plugin should be within a folder with the same ID as the plugin | ||||
| 		const pluginFolderName = pluginStat.path; | ||||
| 		const pluginId = pluginFolderName; | ||||
|  | ||||
| 		// if pluginId is present in 'installedDefaultPlugins' array or it doesn't have default plugin ID, then we won't install it again as default plugin | ||||
| 		if (installedPlugins.includes(pluginId) || !defaultPluginsId.includes(pluginId)) { | ||||
| 			logger.debug(`Skipping default plugin ${pluginId}, ${!defaultPluginsId.includes(pluginId) ? '(Not a default)' : ''}`); | ||||
| 		if (!defaultPluginsInfo.hasOwnProperty(pluginId)) { | ||||
| 			logger.warn(`Default plugin ${pluginId} is missing in defaultPluginsInfo. Not loading.`); | ||||
| 			continue; | ||||
| 		} | ||||
| 		const defaultPluginPath: string = path.join(defaultPluginsDir, pluginId, 'plugin.jpl'); | ||||
| 		await service.installPlugin(defaultPluginPath, false); | ||||
|  | ||||
| 		pluginPaths.push(join(defaultPluginsDir, pluginFolderName, 'plugin.jpl')); | ||||
|  | ||||
| 		pluginSettings = produce(pluginSettings, (draft: PluginSettings) => { | ||||
| 			draft[pluginId] = defaultPluginSetting(); | ||||
| 			// Default plugins can be overridden but not uninstalled (as they're part of | ||||
| 			// the app bundle). When overriding and unoverriding a default plugin, the plugin's | ||||
| 			// state may be deleted. | ||||
| 			// As such, we recreate the plugin state if necessary. | ||||
| 			if (!draft[pluginId]) { | ||||
| 				draft[pluginId] = defaultPluginSetting(); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	return { pluginSettings, pluginPaths }; | ||||
| }; | ||||
|  | ||||
| export const loadAndRunDefaultPlugins = async ( | ||||
| 	service: PluginService, | ||||
| 	defaultPluginsDir: string, | ||||
| 	defaultPluginsInfo: DefaultPluginsInfo, | ||||
| 	originalPluginSettings: PluginSettings, | ||||
| ): Promise<PluginSettings> => { | ||||
| 	const { pluginPaths, pluginSettings } = await getDefaultPluginPathsAndSettings( | ||||
| 		defaultPluginsDir, defaultPluginsInfo, originalPluginSettings, | ||||
| 	) ?? { pluginPaths: [], pluginSettings: originalPluginSettings }; | ||||
|  | ||||
| 	await service.loadAndRunPlugins(pluginPaths, pluginSettings, { builtIn: true, devMode: false }); | ||||
| 	return pluginSettings; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export function setSettingsForDefaultPlugins(defaultPluginsInfo: DefaultPluginsInfo) { | ||||
| 	const installedDefaultPlugins = Setting.value('installedDefaultPlugins'); | ||||
| // Applies setting overrides and marks default plugins as installed. | ||||
| // Should be called after plugins have finished loading. | ||||
| export const afterDefaultPluginsLoaded = async ( | ||||
| 	allLoadedPlugins: Plugins, | ||||
| 	defaultPluginsInfo: DefaultPluginsInfo, | ||||
| 	pluginSettings: PluginSettings, | ||||
| ) => { | ||||
| 	const installedDefaultPlugins: string[] = Setting.value('installedDefaultPlugins'); | ||||
| 	const allDefaultPlugins = Object.keys(defaultPluginsInfo); | ||||
|  | ||||
| 	// only set initial settings if the plugin is not present in installedDefaultPlugins array | ||||
| 	for (const pluginId of Object.keys(defaultPluginsInfo)) { | ||||
| 		if (!defaultPluginsInfo[pluginId].settings) continue; | ||||
| 		for (const settingName of Object.keys(defaultPluginsInfo[pluginId].settings)) { | ||||
| 			if (!installedDefaultPlugins.includes(pluginId) && Setting.keyExists(`plugin-${pluginId}.${settingName}`)) { | ||||
| 				Setting.setValue(`plugin-${pluginId}.${settingName}`, defaultPluginsInfo[pluginId].settings[settingName]); | ||||
| 			} | ||||
| 	const isFirstLoadOfDefaultPlugin = (pluginId: string) => { | ||||
| 		// Not installed? | ||||
| 		if (!pluginSettings[pluginId]) { | ||||
| 			return false; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| export function getDefaultPluginsInstallState(service: PluginService, defaultPluginsId: string[]): PluginSettings { | ||||
| 	const settings: PluginSettings = {}; | ||||
| 	for (const pluginId of defaultPluginsId) { | ||||
| 		if (!service.pluginIds.includes(pluginId)) continue; | ||||
| 		if (!Setting.setArrayValue('installedDefaultPlugins', pluginId)) { | ||||
| 			settings[pluginId] = defaultPluginSetting(); | ||||
| 		// Not the first load | ||||
| 		if (installedDefaultPlugins.includes(pluginId)) { | ||||
| 			return false; | ||||
| 		} | ||||
| 	} | ||||
| 	return settings; | ||||
| } | ||||
|  | ||||
| export function updateDefaultPluginsInstallState(newPluginStates: PluginSettings, ConfigScreen: React.Component<any, any>) { | ||||
| 	if (Object.keys(newPluginStates).length === 0) return; | ||||
| 	const key = 'plugins.states'; | ||||
| 	const md = Setting.settingMetadata(key); | ||||
| 	let newValue = Setting.value('plugins.states'); | ||||
| 	newValue = { | ||||
| 		...newValue, ...newPluginStates, | ||||
| 		// Return true only if the plugin is built-in (and not a user-installed | ||||
| 		// copy). | ||||
| 		// | ||||
| 		// This avoids overriding existing user-set settings. | ||||
| 		return allLoadedPlugins[pluginId]?.builtIn ?? false; | ||||
| 	}; | ||||
| 	shared.updateSettingValue(ConfigScreen, key, newValue); | ||||
|  | ||||
| 	if (md.autoSave) { | ||||
| 		shared.scheduleSaveSettings(ConfigScreen); | ||||
| 	for (const pluginId of allDefaultPlugins) { | ||||
| 		// if pluginId is present in pluginSettings and not in installedDefaultPlugins array, | ||||
| 		// then it's a new default plugin and needs overrides applied. | ||||
| 		if (isFirstLoadOfDefaultPlugin(pluginId)) { | ||||
| 			// Postprocess: Apply setting overrides | ||||
| 			for (const settingName of Object.keys(defaultPluginsInfo[pluginId].settings ?? {})) { | ||||
| 				if (!installedDefaultPlugins.includes(pluginId) && Setting.keyExists(`plugin-${pluginId}.${settingName}`)) { | ||||
| 					Setting.setValue(`plugin-${pluginId}.${settingName}`, defaultPluginsInfo[pluginId].settings[settingName]); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Mark the plugin as installed so that postprocessing won't be done again. | ||||
| 			Setting.setArrayValue('installedDefaultPlugins', pluginId); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| }; | ||||
|   | ||||
| @@ -64,7 +64,6 @@ export default function manifestFromObject(o: any): PluginManifest { | ||||
| 		icons: getIcons(), | ||||
|  | ||||
| 		_recommended: getBoolean('_recommended', false, false), | ||||
| 		_built_in: getBoolean('_built_in', false, false), | ||||
| 	}; | ||||
|  | ||||
| 	validatePluginId(manifest.id); | ||||
|   | ||||
| @@ -37,5 +37,4 @@ export interface PluginManifest { | ||||
| 	_npm_package_name?: string; | ||||
| 	_obsolete?: boolean; | ||||
| 	_recommended?: boolean; | ||||
| 	_built_in?: boolean; | ||||
| } | ||||
|   | ||||
| @@ -13,16 +13,11 @@ const validateUntrustedManifest = (manifest: any, existingManifests: any) => { | ||||
| 	validatePluginId(manifest.id); | ||||
| 	validatePluginVersion(manifest.version); | ||||
|  | ||||
| 	// This prevents a plugin author from marking their own plugin as _recommended | ||||
| 	// or _built_in. | ||||
| 	// This prevents a plugin author from marking their own plugin as _recommended. | ||||
| 	if (typeof manifest._recommended !== 'undefined') { | ||||
| 		throw new Error(`Plugin ${manifest.id} cannot mark itself as recommended.`); | ||||
| 	} | ||||
|  | ||||
| 	if (typeof manifest._built_in !== 'undefined') { | ||||
| 		throw new Error(`Plugin ${manifest.id} cannot mark itself as built-in.`); | ||||
| 	} | ||||
|  | ||||
| 	checkIfPluginCanBeAdded(existingManifests, manifest); | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -52,6 +52,7 @@ These are run by the `app-desktop` package on a full `build` (e.g. on `postinsta | ||||
| ## Installing of default plugins | ||||
|  | ||||
| - All the functions related to default plugins are located in [defaultPluginsUtils.ts](https://github.com/laurent22/joplin/blob/eb7083d7888433ff6ef76ccfb7fb87ba951d513f/packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.ts) | ||||
| - On every startup, we check if there are new plugins available in build folder that have not been installed yet. After installing the new plugin, we update the `installedDefaultPlugins` array in `Setting.ts` with respective plugin ID for future reference. | ||||
| - After installing is complete, we apply the default settings for each default plugin. Default settings are located in [desktopDefaultPluginsInfo.ts](https://github.com/laurent22/joplin/blob/eb7083d7888433ff6ef76ccfb7fb87ba951d513f/packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.ts) | ||||
| - Default plugins are bundled with the app (included in the `build/` directory) and loaded from this directory. | ||||
| - To allow loading `dev` and NPM versions of the plugin, default plugins are loaded after non-default plugins. The plugin service refuses to load additional copies of already-loaded plugins. As such, non-default plugins take precedence over default plugins. | ||||
| - After loading is complete, we apply the default settings for each default plugin. Default settings are located in [desktopDefaultPluginsInfo.ts](https://github.com/laurent22/joplin/blob/eb7083d7888433ff6ef76ccfb7fb87ba951d513f/packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.ts). The `installedDefaultPlugins` setting is used to ensure that settnigs are only overridden once. | ||||
| - If the plugin is already installed by the user, then we don't apply default settings to avoid overriding user's settings. | ||||
		Reference in New Issue
	
	Block a user