feat: Markdown, Rich text, Code, and Checklist note types have been moved to the new Plugins preferences pane. Previous notes created using these types will not experience any disruption. To create new notes using these types, you can reinstall them from the Plugins preferences screen. It is recommended to use the Super note type in place of these replaced note types. (#2630)
This commit is contained in:
10
packages/ui-services/src/Plugins/PluginListing.ts
Normal file
10
packages/ui-services/src/Plugins/PluginListing.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ThirdPartyFeatureDescription } from '@standardnotes/features'
|
||||
|
||||
export type PluginListing = ThirdPartyFeatureDescription & {
|
||||
publisher: string
|
||||
base64Hash: string
|
||||
binaryHash: string
|
||||
showInGallery: boolean
|
||||
}
|
||||
|
||||
export type PluginsList = PluginListing[]
|
||||
95
packages/ui-services/src/Plugins/PluginsService.spec.ts
Normal file
95
packages/ui-services/src/Plugins/PluginsService.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { ItemInterface } from '@standardnotes/models'
|
||||
import { PluginsService } from './PluginsService'
|
||||
import {
|
||||
AlertService,
|
||||
ItemManagerInterface,
|
||||
LegacyApiServiceInterface,
|
||||
MutatorClientInterface,
|
||||
SyncServiceInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { ThirdPartyFeatureDescription } from '@standardnotes/features'
|
||||
|
||||
describe('Plugins Service', () => {
|
||||
let itemManager: ItemManagerInterface
|
||||
let apiService: LegacyApiServiceInterface
|
||||
let pluginsService: PluginsService
|
||||
let crypto: PureCryptoInterface
|
||||
let mutator: MutatorClientInterface
|
||||
let syncService: SyncServiceInterface
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = {} as jest.Mocked<LegacyApiServiceInterface>
|
||||
apiService.addEventObserver = jest.fn()
|
||||
itemManager = {} as jest.Mocked<ItemManagerInterface>
|
||||
|
||||
crypto = {} as jest.Mocked<PureCryptoInterface>
|
||||
crypto.base64Decode = jest.fn()
|
||||
|
||||
itemManager.createTemplateItem = jest.fn().mockReturnValue({})
|
||||
itemManager.addObserver = jest.fn()
|
||||
|
||||
let alertService: AlertService
|
||||
alertService = {} as jest.Mocked<AlertService>
|
||||
alertService.confirm = jest.fn().mockReturnValue(true)
|
||||
alertService.alert = jest.fn()
|
||||
|
||||
mutator = {} as jest.Mocked<MutatorClientInterface>
|
||||
mutator.createItem = jest.fn()
|
||||
mutator.changeComponent = jest.fn().mockReturnValue({} as jest.Mocked<ItemInterface>)
|
||||
mutator.setItemsToBeDeleted = jest.fn()
|
||||
mutator.changeItem = jest.fn()
|
||||
mutator.changeFeatureRepo = jest.fn()
|
||||
|
||||
syncService = {} as jest.Mocked<SyncServiceInterface>
|
||||
syncService.sync = jest.fn()
|
||||
|
||||
pluginsService = new PluginsService(itemManager, mutator, syncService, apiService, alertService, crypto)
|
||||
})
|
||||
|
||||
describe('downloadRemoteThirdPartyFeature', () => {
|
||||
it('should not allow if identifier matches native identifier', async () => {
|
||||
apiService.downloadFeatureUrl = jest.fn().mockReturnValue({
|
||||
data: {
|
||||
identifier: 'org.standardnotes.bold-editor',
|
||||
name: 'Bold Editor',
|
||||
content_type: 'SN|Component',
|
||||
area: 'editor-editor',
|
||||
version: '1.0.0',
|
||||
url: 'http://localhost:8005/',
|
||||
},
|
||||
})
|
||||
|
||||
const installUrl = 'http://example.com'
|
||||
crypto.base64Decode = jest.fn().mockReturnValue(installUrl)
|
||||
|
||||
const plugin = await pluginsService.getPluginDetailsFromUrl('some-url')
|
||||
expect(plugin).toBeDefined()
|
||||
|
||||
const result = await pluginsService.installExternalPlugin(plugin as ThirdPartyFeatureDescription)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not allow if url matches native url', async () => {
|
||||
apiService.downloadFeatureUrl = jest.fn().mockReturnValue({
|
||||
data: {
|
||||
identifier: 'org.foo.bar',
|
||||
name: 'Bold Editor',
|
||||
content_type: 'SN|Component',
|
||||
area: 'editor-editor',
|
||||
version: '1.0.0',
|
||||
url: 'http://localhost:8005/org.standardnotes.bold-editor/index.html',
|
||||
},
|
||||
})
|
||||
|
||||
const installUrl = 'http://example.com'
|
||||
crypto.base64Decode = jest.fn().mockReturnValue(installUrl)
|
||||
|
||||
const plugin = await pluginsService.getPluginDetailsFromUrl('some-url')
|
||||
expect(plugin).toBeDefined()
|
||||
|
||||
const result = await pluginsService.installExternalPlugin(plugin as ThirdPartyFeatureDescription)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
180
packages/ui-services/src/Plugins/PluginsService.ts
Normal file
180
packages/ui-services/src/Plugins/PluginsService.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import {
|
||||
ComponentContent,
|
||||
ComponentContentSpecialized,
|
||||
ComponentInterface,
|
||||
FillItemContentSpecialized,
|
||||
} from '@standardnotes/models'
|
||||
import { PluginListing, PluginsList } from './PluginListing'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { FindNativeFeature, GetFeatures, ThirdPartyFeatureDescription } from '@standardnotes/features'
|
||||
import {
|
||||
API_MESSAGE_FAILED_DOWNLOADING_EXTENSION,
|
||||
AlertService,
|
||||
ItemManagerInterface,
|
||||
LegacyApiServiceInterface,
|
||||
MutatorClientInterface,
|
||||
SyncServiceInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { PluginsServiceInterface } from './PluginsServiceInterface'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { isString } from '@standardnotes/utils'
|
||||
|
||||
const PluginsUrl = 'https://raw.githubusercontent.com/standardnotes/plugins/main/cdn/dist/packages.json'
|
||||
|
||||
type DownloadedPackages = {
|
||||
[key: string]: PluginListing
|
||||
}
|
||||
|
||||
export class PluginsService implements PluginsServiceInterface {
|
||||
private originalPlugins?: PluginsList
|
||||
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private api: LegacyApiServiceInterface,
|
||||
private alerts: AlertService,
|
||||
private crypto: PureCryptoInterface,
|
||||
) {}
|
||||
|
||||
private async performDownloadPlugins(): Promise<PluginsList> {
|
||||
const response = await fetch(PluginsUrl)
|
||||
const changelog = await response.text()
|
||||
const parsedData = JSON.parse(changelog) as DownloadedPackages
|
||||
|
||||
return Object.values(parsedData)
|
||||
}
|
||||
|
||||
public async getInstallablePlugins(): Promise<PluginsList> {
|
||||
if (this.originalPlugins) {
|
||||
return this.filterInstallablePlugins(this.originalPlugins)
|
||||
}
|
||||
|
||||
this.originalPlugins = await this.performDownloadPlugins()
|
||||
|
||||
return this.filterInstallablePlugins(this.originalPlugins)
|
||||
}
|
||||
|
||||
private filterInstallablePlugins(plugins: PluginsList): PluginsList {
|
||||
const filtered = plugins.filter((plugin) => {
|
||||
if (!plugin.showInGallery) {
|
||||
return false
|
||||
}
|
||||
|
||||
const nativeFeature = FindNativeFeature(plugin.identifier)
|
||||
if (nativeFeature && !nativeFeature.deprecated) {
|
||||
return false
|
||||
}
|
||||
|
||||
const existingInstalled = this.items.getDisplayableComponents().find((component) => {
|
||||
return component.identifier === plugin.identifier
|
||||
})
|
||||
|
||||
return !existingInstalled
|
||||
})
|
||||
|
||||
return filtered.sort((a, b) => {
|
||||
if (a.name === b.name) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1
|
||||
})
|
||||
}
|
||||
|
||||
public async installPlugin(
|
||||
plugin: PluginListing | ThirdPartyFeatureDescription,
|
||||
): Promise<ComponentInterface | undefined> {
|
||||
const isValidContentType = [
|
||||
ContentType.TYPES.Component,
|
||||
ContentType.TYPES.Theme,
|
||||
ContentType.TYPES.ActionsExtension,
|
||||
ContentType.TYPES.ExtensionRepo,
|
||||
].includes(plugin.content_type)
|
||||
|
||||
if (!isValidContentType) {
|
||||
return
|
||||
}
|
||||
|
||||
const nativeFeature = FindNativeFeature(plugin.identifier)
|
||||
if (nativeFeature && !nativeFeature.deprecated) {
|
||||
void this.alerts.alert('Unable to install plugin due to a conflict with a native feature.')
|
||||
return
|
||||
}
|
||||
|
||||
if (plugin.url) {
|
||||
for (const nativeFeature of GetFeatures()) {
|
||||
if (plugin.url.includes(nativeFeature.identifier) && !nativeFeature.deprecated) {
|
||||
void this.alerts.alert('Unable to install plugin due to a conflict with a native feature.')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = FillItemContentSpecialized<ComponentContentSpecialized, ComponentContent>({
|
||||
area: plugin.area,
|
||||
name: plugin.name ?? '',
|
||||
package_info: plugin,
|
||||
valid_until: new Date(plugin.expires_at || 0),
|
||||
hosted_url: plugin.url,
|
||||
})
|
||||
|
||||
const component = this.items.createTemplateItem<ComponentContent, ComponentInterface>(plugin.content_type, content)
|
||||
|
||||
await this.mutator.insertItem(component)
|
||||
void this.sync.sync()
|
||||
|
||||
return component
|
||||
}
|
||||
|
||||
public async getPluginDetailsFromUrl(urlOrCode: string): Promise<ThirdPartyFeatureDescription | undefined> {
|
||||
let url = urlOrCode
|
||||
try {
|
||||
url = this.crypto.base64Decode(urlOrCode)
|
||||
} catch (err) {
|
||||
void err
|
||||
}
|
||||
|
||||
const response = await this.api.downloadFeatureUrl(url)
|
||||
if (response.data?.error) {
|
||||
await this.alerts.alert(API_MESSAGE_FAILED_DOWNLOADING_EXTENSION)
|
||||
return undefined
|
||||
}
|
||||
|
||||
let rawFeature = response.data as ThirdPartyFeatureDescription
|
||||
|
||||
if (isString(rawFeature)) {
|
||||
try {
|
||||
rawFeature = JSON.parse(rawFeature)
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
if (!rawFeature.content_type) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return rawFeature
|
||||
}
|
||||
|
||||
public async installExternalPlugin(
|
||||
plugin: PluginListing | ThirdPartyFeatureDescription,
|
||||
): Promise<ComponentInterface | undefined> {
|
||||
const nativeFeature = FindNativeFeature(plugin.identifier)
|
||||
if (nativeFeature) {
|
||||
await this.alerts.alert('Unable to install external plugin due to a conflict with a native feature.')
|
||||
return
|
||||
}
|
||||
|
||||
if (plugin.url) {
|
||||
for (const nativeFeature of GetFeatures()) {
|
||||
if (plugin.url.includes(nativeFeature.identifier)) {
|
||||
await this.alerts.alert('Unable to install external plugin due to a conflict with a native feature.')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.installPlugin(plugin)
|
||||
}
|
||||
}
|
||||
10
packages/ui-services/src/Plugins/PluginsServiceInterface.ts
Normal file
10
packages/ui-services/src/Plugins/PluginsServiceInterface.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ComponentInterface } from '@standardnotes/models'
|
||||
import { PluginListing, PluginsList } from './PluginListing'
|
||||
import { ThirdPartyFeatureDescription } from '@standardnotes/features'
|
||||
|
||||
export interface PluginsServiceInterface {
|
||||
getInstallablePlugins(): Promise<PluginsList>
|
||||
installPlugin(plugin: PluginListing): Promise<ComponentInterface | undefined>
|
||||
getPluginDetailsFromUrl(urlOrCode: string): Promise<ThirdPartyFeatureDescription | undefined>
|
||||
installExternalPlugin(plugin: PluginListing | ThirdPartyFeatureDescription): Promise<ComponentInterface | undefined>
|
||||
}
|
||||
@@ -28,6 +28,10 @@ export * from './Route/RouteServiceEvent'
|
||||
export * from './Security/AutolockService'
|
||||
export * from './Storage/LocalStorage'
|
||||
|
||||
export * from './Plugins/PluginListing'
|
||||
export * from './Plugins/PluginsService'
|
||||
export * from './Plugins/PluginsServiceInterface'
|
||||
|
||||
export * from './UseCase/IsGlobalSpellcheckEnabled'
|
||||
export * from './UseCase/IsNativeMobileWeb'
|
||||
export * from './UseCase/IsMobileDevice'
|
||||
|
||||
Reference in New Issue
Block a user