243 lines
8.6 KiB
TypeScript
243 lines
8.6 KiB
TypeScript
import {
|
|
ComponentAction,
|
|
ComponentFeatureDescription,
|
|
ComponentPermission,
|
|
FindNativeFeature,
|
|
} from '@standardnotes/features'
|
|
import { ComponentInterface, ComponentMutator, PermissionDialog, UIFeature } from '@standardnotes/models'
|
|
import {
|
|
AlertService,
|
|
ItemManagerInterface,
|
|
MutatorClientInterface,
|
|
SyncServiceInterface,
|
|
} from '@standardnotes/services'
|
|
import { AllowedBatchContentTypes, AllowedBatchStreaming } from '../Types'
|
|
import { Copy, filterFromArray, removeFromArray, uniqueArray } from '@standardnotes/utils'
|
|
import { permissionsStringForPermissions } from '../permissionsStringForPermissions'
|
|
|
|
export class RunWithPermissionsUseCase {
|
|
private permissionDialogs: PermissionDialog[] = []
|
|
private pendingErrorAlerts: Set<string> = new Set()
|
|
|
|
constructor(
|
|
private permissionDialogUIHandler: (dialog: PermissionDialog) => void,
|
|
private alerts: AlertService,
|
|
private mutator: MutatorClientInterface,
|
|
private sync: SyncServiceInterface,
|
|
private items: ItemManagerInterface,
|
|
) {}
|
|
|
|
deinit() {
|
|
this.permissionDialogs = []
|
|
;(this.permissionDialogUIHandler as unknown) = undefined
|
|
;(this.alerts as unknown) = undefined
|
|
;(this.mutator as unknown) = undefined
|
|
;(this.sync as unknown) = undefined
|
|
;(this.items as unknown) = undefined
|
|
}
|
|
|
|
public execute(
|
|
componentIdentifier: string,
|
|
requiredPermissions: ComponentPermission[],
|
|
runFunction: () => void,
|
|
): void {
|
|
const uiFeature = this.findUIFeature(componentIdentifier)
|
|
|
|
if (!uiFeature) {
|
|
if (!this.pendingErrorAlerts.has(componentIdentifier)) {
|
|
this.pendingErrorAlerts.add(componentIdentifier)
|
|
void this.alerts
|
|
.alert(
|
|
`Unable to find component with ID ${componentIdentifier}. Please restart the app and try again.`,
|
|
'An unexpected error occurred',
|
|
)
|
|
.then(() => {
|
|
this.pendingErrorAlerts.delete(componentIdentifier)
|
|
})
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if (uiFeature.isNativeFeature) {
|
|
runFunction()
|
|
return
|
|
}
|
|
|
|
if (!this.areRequestedPermissionsValid(uiFeature, requiredPermissions)) {
|
|
console.error('Component is requesting invalid permissions', componentIdentifier, requiredPermissions)
|
|
return
|
|
}
|
|
|
|
const acquiredPermissions = uiFeature.acquiredPermissions
|
|
|
|
/* Make copy as not to mutate input values */
|
|
requiredPermissions = Copy<ComponentPermission[]>(requiredPermissions)
|
|
for (const required of requiredPermissions.slice()) {
|
|
/* Remove anything we already have */
|
|
const respectiveAcquired = acquiredPermissions.find((candidate) => candidate.name === required.name)
|
|
if (!respectiveAcquired) {
|
|
continue
|
|
}
|
|
/* We now match on name, lets substract from required.content_types anything we have in acquired. */
|
|
const requiredContentTypes = required.content_types
|
|
if (!requiredContentTypes) {
|
|
/* If this permission does not require any content types (i.e stream-context-item)
|
|
then we can remove this from required since we match by name (respectiveAcquired.name === required.name) */
|
|
filterFromArray(requiredPermissions, required)
|
|
continue
|
|
}
|
|
for (const acquiredContentType of respectiveAcquired.content_types as string[]) {
|
|
removeFromArray(requiredContentTypes, acquiredContentType)
|
|
}
|
|
if (requiredContentTypes.length === 0) {
|
|
/* We've removed all acquired and end up with zero, means we already have all these permissions */
|
|
filterFromArray(requiredPermissions, required)
|
|
}
|
|
}
|
|
if (requiredPermissions.length > 0) {
|
|
this.promptForPermissionsWithDeferredRendering(
|
|
uiFeature.asComponent,
|
|
requiredPermissions,
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
async (approved) => {
|
|
if (approved) {
|
|
runFunction()
|
|
}
|
|
},
|
|
)
|
|
} else {
|
|
runFunction()
|
|
}
|
|
}
|
|
|
|
setPermissionDialogUIHandler(handler: (dialog: PermissionDialog) => void): void {
|
|
this.permissionDialogUIHandler = handler
|
|
}
|
|
|
|
areRequestedPermissionsValid(
|
|
uiFeature: UIFeature<ComponentFeatureDescription>,
|
|
permissions: ComponentPermission[],
|
|
): boolean {
|
|
for (const permission of permissions) {
|
|
if (permission.name === ComponentAction.StreamItems) {
|
|
if (!AllowedBatchStreaming.includes(uiFeature.featureIdentifier)) {
|
|
return false
|
|
}
|
|
const hasNonAllowedBatchPermission = permission.content_types?.some(
|
|
(type) => !AllowedBatchContentTypes.includes(type),
|
|
)
|
|
if (hasNonAllowedBatchPermission) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
private promptForPermissionsWithDeferredRendering(
|
|
component: ComponentInterface,
|
|
permissions: ComponentPermission[],
|
|
callback: (approved: boolean) => Promise<void>,
|
|
): void {
|
|
setTimeout(() => {
|
|
this.promptForPermissions(component, permissions, callback)
|
|
})
|
|
}
|
|
|
|
private promptForPermissions(
|
|
component: ComponentInterface,
|
|
permissions: ComponentPermission[],
|
|
callback: (approved: boolean) => Promise<void>,
|
|
): void {
|
|
const params: PermissionDialog = {
|
|
component: component,
|
|
permissions: permissions,
|
|
permissionsString: permissionsStringForPermissions(permissions, component),
|
|
actionBlock: callback,
|
|
callback: async (approved: boolean) => {
|
|
const latestComponent = this.items.findItem<ComponentInterface>(component.uuid)
|
|
|
|
if (!latestComponent) {
|
|
return
|
|
}
|
|
|
|
if (approved) {
|
|
const componentPermissions = Copy(latestComponent.permissions) as ComponentPermission[]
|
|
for (const permission of permissions) {
|
|
const matchingPermission = componentPermissions.find((candidate) => candidate.name === permission.name)
|
|
if (!matchingPermission) {
|
|
componentPermissions.push(permission)
|
|
} else {
|
|
/* Permission already exists, but content_types may have been expanded */
|
|
const contentTypes = matchingPermission.content_types || []
|
|
matchingPermission.content_types = uniqueArray(contentTypes.concat(permission.content_types as string[]))
|
|
}
|
|
}
|
|
|
|
await this.mutator.changeItem(component, (m) => {
|
|
const mutator = m as ComponentMutator
|
|
mutator.permissions = componentPermissions
|
|
})
|
|
|
|
void this.sync.sync()
|
|
}
|
|
|
|
this.permissionDialogs = this.permissionDialogs.filter((pendingDialog) => {
|
|
/* Remove self */
|
|
if (pendingDialog === params) {
|
|
pendingDialog.actionBlock && pendingDialog.actionBlock(approved)
|
|
return false
|
|
}
|
|
const containsObjectSubset = (source: ComponentPermission[], target: ComponentPermission[]) => {
|
|
return !target.some((val) => !source.find((candidate) => JSON.stringify(candidate) === JSON.stringify(val)))
|
|
}
|
|
if (pendingDialog.component === component) {
|
|
/* remove pending dialogs that are encapsulated by already approved permissions, and run its function */
|
|
if (
|
|
pendingDialog.permissions === permissions ||
|
|
containsObjectSubset(permissions, pendingDialog.permissions)
|
|
) {
|
|
/* If approved, run the action block. Otherwise, if canceled, cancel any
|
|
pending ones as well, since the user was explicit in their intentions */
|
|
if (approved) {
|
|
pendingDialog.actionBlock && pendingDialog.actionBlock(approved)
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
if (this.permissionDialogs.length > 0) {
|
|
this.permissionDialogUIHandler(this.permissionDialogs[0])
|
|
}
|
|
},
|
|
}
|
|
/**
|
|
* Since these calls are asyncronous, multiple dialogs may be requested at the same time.
|
|
* We only want to present one and trigger all callbacks based on one modal result
|
|
*/
|
|
const existingDialog = this.permissionDialogs.find((dialog) => dialog.component === component)
|
|
this.permissionDialogs.push(params)
|
|
if (!existingDialog) {
|
|
this.permissionDialogUIHandler(params)
|
|
}
|
|
}
|
|
|
|
private findUIFeature(identifier: string): UIFeature<ComponentFeatureDescription> | undefined {
|
|
const nativeFeature = FindNativeFeature<ComponentFeatureDescription>(identifier)
|
|
if (nativeFeature) {
|
|
return new UIFeature(nativeFeature)
|
|
}
|
|
|
|
const componentItem = this.items.findItem<ComponentInterface>(identifier)
|
|
if (componentItem) {
|
|
return new UIFeature<ComponentFeatureDescription>(componentItem)
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
}
|