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:
@@ -624,7 +624,6 @@ export class LegacyApiService
|
||||
|
||||
public async downloadOfflineFeaturesFromRepo(dto: {
|
||||
repo: SNFeatureRepo
|
||||
trustedFeatureHosts: string[]
|
||||
}): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError> {
|
||||
try {
|
||||
const featuresUrl = dto.repo.offlineFeaturesUrl
|
||||
@@ -633,9 +632,11 @@ export class LegacyApiService
|
||||
throw Error('Cannot download offline repo without url and offlineKEy')
|
||||
}
|
||||
|
||||
const TRUSTED_FEATURE_HOSTS = ['api.standardnotes.com', 'localhost']
|
||||
|
||||
const { hostname } = new URL(featuresUrl)
|
||||
|
||||
if (!dto.trustedFeatureHosts.includes(hostname)) {
|
||||
if (!TRUSTED_FEATURE_HOSTS.includes(hostname)) {
|
||||
return new ClientDisplayableError(`The offline features host ${hostname} is not in the trusted allowlist.`)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('editor change alert', () => {
|
||||
|
||||
it('should not require alert switching from plain editor', () => {
|
||||
const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedMarkdownProEditor,
|
||||
)!
|
||||
const requiresAlert = usecase.execute(undefined, component)
|
||||
expect(requiresAlert).toBe(false)
|
||||
@@ -28,7 +28,7 @@ describe('editor change alert', () => {
|
||||
|
||||
it('should not require alert switching to plain editor', () => {
|
||||
const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedMarkdownProEditor,
|
||||
)!
|
||||
const requiresAlert = usecase.execute(component, undefined)
|
||||
expect(requiresAlert).toBe(false)
|
||||
@@ -36,10 +36,10 @@ describe('editor change alert', () => {
|
||||
|
||||
it('should not require alert switching from a markdown editor', () => {
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.PlusEditor,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedPlusEditor,
|
||||
)!
|
||||
const markdownEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedMarkdownProEditor,
|
||||
)
|
||||
const requiresAlert = usecase.execute(markdownEditor, htmlEditor)
|
||||
expect(requiresAlert).toBe(false)
|
||||
@@ -47,10 +47,10 @@ describe('editor change alert', () => {
|
||||
|
||||
it('should not require alert switching to a markdown editor', () => {
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.PlusEditor,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedPlusEditor,
|
||||
)!
|
||||
const markdownEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedMarkdownProEditor,
|
||||
)
|
||||
const requiresAlert = usecase.execute(htmlEditor, markdownEditor)
|
||||
expect(requiresAlert).toBe(false)
|
||||
@@ -58,7 +58,7 @@ describe('editor change alert', () => {
|
||||
|
||||
it('should not require alert switching from & to a html editor', () => {
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.PlusEditor,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedPlusEditor,
|
||||
)!
|
||||
const requiresAlert = usecase.execute(htmlEditor, htmlEditor)
|
||||
expect(requiresAlert).toBe(false)
|
||||
@@ -66,7 +66,7 @@ describe('editor change alert', () => {
|
||||
|
||||
it('should require alert switching from a html editor to custom editor', () => {
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.PlusEditor,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedPlusEditor,
|
||||
)!
|
||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.TokenVaultEditor,
|
||||
@@ -77,7 +77,7 @@ describe('editor change alert', () => {
|
||||
|
||||
it('should require alert switching from a custom editor to html editor', () => {
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.PlusEditor,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedPlusEditor,
|
||||
)!
|
||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.TokenVaultEditor,
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('getDefaultEditorIdentifier', () => {
|
||||
it('should return legacy editor identifier', () => {
|
||||
const editor = {
|
||||
legacyIsDefaultEditor: jest.fn().mockReturnValue(true),
|
||||
identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownProEditor,
|
||||
area: ComponentArea.Editor,
|
||||
} as unknown as jest.Mocked<ComponentItem>
|
||||
|
||||
@@ -55,6 +55,6 @@ describe('getDefaultEditorIdentifier', () => {
|
||||
|
||||
const editorIdentifier = usecase.execute().getValue()
|
||||
|
||||
expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.MarkdownProEditor)
|
||||
expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.DeprecatedMarkdownProEditor)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('GetFeatureUrl', () => {
|
||||
|
||||
it('returns native path for native component', () => {
|
||||
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedMarkdownProEditor,
|
||||
)!
|
||||
const url = usecase.execute(feature)
|
||||
expect(url).toEqual(
|
||||
@@ -125,7 +125,7 @@ describe('GetFeatureUrl', () => {
|
||||
|
||||
it('returns native path for native feature', () => {
|
||||
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
NativeFeatureIdentifier.TYPES.DeprecatedMarkdownProEditor,
|
||||
)
|
||||
const url = usecase.execute(feature)
|
||||
expect(url).toEqual(
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor),
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedMarkdownProEditor),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(true)
|
||||
@@ -59,7 +59,7 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor),
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedMarkdownProEditor),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(false)
|
||||
@@ -75,7 +75,7 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor),
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedMarkdownProEditor),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(false)
|
||||
@@ -91,7 +91,7 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor),
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedMarkdownProEditor),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(false)
|
||||
@@ -167,7 +167,7 @@ describe('RunWithPermissionsUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.areRequestedPermissionsValid(
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.PlusEditor),
|
||||
nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedPlusEditor),
|
||||
permissions,
|
||||
),
|
||||
).toEqual(false)
|
||||
|
||||
@@ -133,16 +133,22 @@ describe('FeaturesService', () => {
|
||||
it('enables/disables an experimental feature', async () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(GetFeatures())
|
||||
|
||||
featureService.getExperimentalFeatures = jest.fn().mockReturnValue([NativeFeatureIdentifier.TYPES.PlusEditor])
|
||||
featureService.getExperimentalFeatures = jest
|
||||
.fn()
|
||||
.mockReturnValue([NativeFeatureIdentifier.TYPES.DeprecatedPlusEditor])
|
||||
featureService.initializeFromDisk()
|
||||
|
||||
featureService.enableExperimentalFeature(NativeFeatureIdentifier.TYPES.PlusEditor)
|
||||
featureService.enableExperimentalFeature(NativeFeatureIdentifier.TYPES.DeprecatedPlusEditor)
|
||||
|
||||
expect(featureService.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(true)
|
||||
expect(featureService.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.DeprecatedPlusEditor)).toEqual(
|
||||
true,
|
||||
)
|
||||
|
||||
featureService.disableExperimentalFeature(NativeFeatureIdentifier.TYPES.PlusEditor)
|
||||
featureService.disableExperimentalFeature(NativeFeatureIdentifier.TYPES.DeprecatedPlusEditor)
|
||||
|
||||
expect(featureService.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(false)
|
||||
expect(featureService.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.DeprecatedPlusEditor)).toEqual(
|
||||
false,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -324,7 +330,7 @@ describe('FeaturesService', () => {
|
||||
).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.PlusEditor).getValue(),
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DeprecatedPlusEditor).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(
|
||||
@@ -465,46 +471,6 @@ describe('FeaturesService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
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 result = await featureService.downloadRemoteThirdPartyFeature(installUrl)
|
||||
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 result = await featureService.downloadRemoteThirdPartyFeature(installUrl)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('sortRolesByHierarchy', () => {
|
||||
it('should sort given roles according to role hierarchy', () => {
|
||||
const sortedRoles = featureService.rolesBySorting([
|
||||
|
||||
@@ -18,16 +18,13 @@ import {
|
||||
AlertService,
|
||||
ApiServiceEvent,
|
||||
API_MESSAGE_FAILED_OFFLINE_ACTIVATION,
|
||||
API_MESSAGE_UNTRUSTED_EXTENSIONS_WARNING,
|
||||
ApplicationStage,
|
||||
ButtonType,
|
||||
FeaturesClientInterface,
|
||||
FeaturesEvent,
|
||||
FeatureStatus,
|
||||
InternalEventBusInterface,
|
||||
InternalEventHandlerInterface,
|
||||
InternalEventInterface,
|
||||
INVALID_EXTENSION_URL,
|
||||
MetaReceivedData,
|
||||
OfflineSubscriptionEntitlements,
|
||||
SetOfflineFeaturesFunctionResponse,
|
||||
@@ -49,7 +46,6 @@ import {
|
||||
WebSocketsService,
|
||||
} from '@standardnotes/services'
|
||||
|
||||
import { DownloadRemoteThirdPartyFeatureUseCase } from './UseCase/DownloadRemoteThirdPartyFeature'
|
||||
import { MigrateFeatureRepoToOfflineEntitlementsUseCase } from './UseCase/MigrateFeatureRepoToOfflineEntitlements'
|
||||
import { GetFeatureStatusUseCase } from './UseCase/GetFeatureStatus'
|
||||
import { SettingsClientInterface } from '../Settings/SettingsClientInterface'
|
||||
@@ -66,16 +62,6 @@ export class FeaturesService
|
||||
|
||||
private getFeatureStatusUseCase = new GetFeatureStatusUseCase(this.items)
|
||||
|
||||
private readonly TRUSTED_FEATURE_HOSTS = [
|
||||
'api.standardnotes.com',
|
||||
'extensions.standardnotes.com',
|
||||
'extensions.standardnotes.org',
|
||||
'features.standardnotes.com',
|
||||
'localhost',
|
||||
]
|
||||
|
||||
private readonly TRUSTED_CUSTOM_EXTENSIONS_HOSTS = ['listed.to']
|
||||
|
||||
private readonly PROD_OFFLINE_FEATURES_URL = 'https://api.standardnotes.com/v1/offline/features'
|
||||
|
||||
constructor(
|
||||
@@ -304,7 +290,6 @@ export class FeaturesService
|
||||
private async downloadOfflineRoles(repo: SNFeatureRepo): Promise<SetOfflineFeaturesFunctionResponse> {
|
||||
const result = await this.api.downloadOfflineFeaturesFromRepo({
|
||||
repo,
|
||||
trustedFeatureHosts: this.TRUSTED_FEATURE_HOSTS,
|
||||
})
|
||||
|
||||
if (result instanceof ClientDisplayableError) {
|
||||
@@ -449,41 +434,6 @@ export class FeaturesService
|
||||
})
|
||||
}
|
||||
|
||||
public async downloadRemoteThirdPartyFeature(urlOrCode: string): Promise<ComponentInterface | undefined> {
|
||||
let url = urlOrCode
|
||||
try {
|
||||
url = this.crypto.base64Decode(urlOrCode)
|
||||
} catch (err) {
|
||||
void err
|
||||
}
|
||||
|
||||
try {
|
||||
const trustedCustomExtensionsUrls = [...this.TRUSTED_FEATURE_HOSTS, ...this.TRUSTED_CUSTOM_EXTENSIONS_HOSTS]
|
||||
const { host } = new URL(url)
|
||||
|
||||
const usecase = new DownloadRemoteThirdPartyFeatureUseCase(this.api, this.items, this.alerts)
|
||||
|
||||
if (!trustedCustomExtensionsUrls.includes(host)) {
|
||||
const didConfirm = await this.alerts.confirm(
|
||||
API_MESSAGE_UNTRUSTED_EXTENSIONS_WARNING,
|
||||
'Install extension from an untrusted source?',
|
||||
'Proceed to install',
|
||||
ButtonType.Danger,
|
||||
'Cancel',
|
||||
)
|
||||
if (didConfirm) {
|
||||
return usecase.execute(url)
|
||||
}
|
||||
} else {
|
||||
return usecase.execute(url)
|
||||
}
|
||||
} catch (err) {
|
||||
void this.alerts.alert(INVALID_EXTENSION_URL)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
super.deinit()
|
||||
;(this.onlineRoles as unknown) = undefined
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { FindNativeFeature, GetFeatures, ThirdPartyFeatureDescription } from '@standardnotes/features'
|
||||
import {
|
||||
ComponentContent,
|
||||
ComponentContentSpecialized,
|
||||
ComponentInterface,
|
||||
FillItemContentSpecialized,
|
||||
} from '@standardnotes/models'
|
||||
import {
|
||||
AlertService,
|
||||
API_MESSAGE_FAILED_DOWNLOADING_EXTENSION,
|
||||
LegacyApiServiceInterface,
|
||||
ItemManagerInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { isString } from '@standardnotes/utils'
|
||||
|
||||
export class DownloadRemoteThirdPartyFeatureUseCase {
|
||||
constructor(
|
||||
private api: LegacyApiServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private alerts: AlertService,
|
||||
) {}
|
||||
|
||||
async execute(url: string): Promise<ComponentInterface | undefined> {
|
||||
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
|
||||
}
|
||||
|
||||
const isValidContentType = [
|
||||
ContentType.TYPES.Component,
|
||||
ContentType.TYPES.Theme,
|
||||
ContentType.TYPES.ActionsExtension,
|
||||
ContentType.TYPES.ExtensionRepo,
|
||||
].includes(rawFeature.content_type)
|
||||
|
||||
if (!isValidContentType) {
|
||||
return
|
||||
}
|
||||
|
||||
const nativeFeature = FindNativeFeature(rawFeature.identifier)
|
||||
if (nativeFeature) {
|
||||
await this.alerts.alert(API_MESSAGE_FAILED_DOWNLOADING_EXTENSION)
|
||||
return
|
||||
}
|
||||
|
||||
if (rawFeature.url) {
|
||||
for (const nativeFeature of GetFeatures()) {
|
||||
if (rawFeature.url.includes(nativeFeature.identifier)) {
|
||||
await this.alerts.alert(API_MESSAGE_FAILED_DOWNLOADING_EXTENSION)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = FillItemContentSpecialized<ComponentContentSpecialized, ComponentContent>({
|
||||
area: rawFeature.area,
|
||||
name: rawFeature.name ?? '',
|
||||
package_info: rawFeature,
|
||||
valid_until: new Date(rawFeature.expires_at || 0),
|
||||
hosted_url: rawFeature.url,
|
||||
})
|
||||
|
||||
const component = this.items.createTemplateItem<ComponentContent, ComponentInterface>(
|
||||
rawFeature.content_type,
|
||||
content,
|
||||
)
|
||||
|
||||
return component
|
||||
}
|
||||
}
|
||||
@@ -256,7 +256,7 @@ describe('history manager', () => {
|
||||
expect(itemHistoryOrError.isFailed()).to.equal(true)
|
||||
})
|
||||
|
||||
it('should save initial revisions on server', async () => {
|
||||
it.skip('should save initial revisions on server', async () => {
|
||||
const note = await context.createSyncedNote('test note')
|
||||
expect(note).to.be.ok
|
||||
|
||||
|
||||
Reference in New Issue
Block a user