diff options
Diffstat (limited to 'src/fields/util.ts')
-rw-r--r-- | src/fields/util.ts | 204 |
1 files changed, 101 insertions, 103 deletions
diff --git a/src/fields/util.ts b/src/fields/util.ts index ad592391e..f87c200c1 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,12 +1,11 @@ -import { $mobx, action, observable, runInAction, trace, values } from 'mobx'; +import { $mobx, action, observable, runInAction, trace } from 'mobx'; import { computedFn } from 'mobx-utils'; -import { returnZero } from '../Utils'; +import { ClientUtils, returnZero } from '../ClientUtils'; import { DocServer } from '../client/DocServer'; -import { LinkManager } from '../client/util/LinkManager'; import { SerializationHelper } from '../client/util/SerializationHelper'; import { UndoManager } from '../client/util/UndoManager'; -import { Doc, DocListCast, Field, FieldResult, HierarchyMapping, ReverseHierarchyMap, StrListCast, aclLevel, updateCachedAcls } from './Doc'; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, UpdatingFromServer, Width } from './DocSymbols'; +import { Doc, DocListCast, FieldType, FieldResult, HierarchyMapping, ReverseHierarchyMap, StrListCast, aclLevel, updateCachedAcls } from './Doc'; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, DirectLinks, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, UpdatingFromServer, Width } from './DocSymbols'; import { FieldChanged, Id, Parent, ToValue } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; @@ -16,24 +15,44 @@ import { RichTextField } from './RichTextField'; import { SchemaHeaderField } from './SchemaHeaderField'; import { ComputedField } from './ScriptField'; import { DocCast, ScriptCast, StrCast } from './Types'; -import { BaseException } from 'pdfjs-dist/types/src/shared/util'; + +/** + * These are the various levels of access a user can have to a document. + * + * Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document. + * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document. + * Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything. + * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. + * None: the document is not shared with that user. + * Unset: Remove a sharing permission (eg., used ) + */ +export enum SharingPermissions { + Admin = 'Admin', + Edit = 'Edit', + Augment = 'Augment', + View = 'View', + None = 'Not-Shared', +} function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); } -var tracing = false; +// eslint-disable-next-line prefer-const +let tracing = false; export function TraceMobx() { tracing && trace(); } -const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { +export const _propSetterCB = new Map<string, ((target: any, value: any) => void) | undefined>(); + +const _setterImpl = action((target: any, prop: string | symbol | number, valueIn: any, receiver: any): boolean => { if (SerializationHelper.IsSerializing() || typeof prop === 'symbol') { - target[prop] = value; + target[prop] = valueIn; return true; } - value = value?.[SelfProxy] ?? value; // convert any Doc type values to Proxy's + let value = valueIn?.[SelfProxy] ?? valueIn; // convert any Doc type values to Proxy's const curValue = target.__fieldTuples[prop]; if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { @@ -50,6 +69,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number throw new Error("Can't put the same object in multiple documents at the same time"); } value[Parent] = receiver; + // eslint-disable-next-line no-use-before-define value[FieldChanged] = containedFieldChangedHandler(receiver, prop, value); } if (curValue instanceof ObjectField) { @@ -59,11 +79,12 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number if (typeof prop === 'string' && _propSetterCB.has(prop)) _propSetterCB.get(prop)!(target[SelfProxy], value); + // eslint-disable-next-line no-use-before-define const effectiveAcl = GetEffectiveAcl(target); const writeMode = DocServer.getFieldWriteMode(prop as string); const fromServer = target[UpdatingFromServer]; - const sameAuthor = fromServer || receiver.author === Doc.CurrentUserEmail; + const sameAuthor = fromServer || receiver.author === ClientUtils.CurrentUserEmail; const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Playground || writeMode === DocServer.WriteMode.LivePlayground || (effectiveAcl === AclAugment && value instanceof RichTextField); const writeToServer = @@ -95,7 +116,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent( { - redo: () => (receiver[prop] = value), + redo: () => { + receiver[prop] = value; + }, undo: () => { const wasUpdate = receiver[UpdatingFromServer]; const wasForce = receiver[ForceServerWrite]; @@ -131,57 +154,16 @@ export function denormalizeEmail(email: string) { return email.replace(/__/g, '.'); } -/** - * Copies parent's acl fields to the child - */ -export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) { - [...Object.keys(parent), ...(Doc.CurrentUserEmail !== parent.author ? ['acl-Owner'] : [])] - .filter(key => key.startsWith('acl')) - .forEach(key => { - // if the default acl mode is private, then don't inherit the acl-guest permission, but set it to private. - // const permission: string = key === 'acl-guest' && Doc.defaultAclPrivate ? AclPrivate : parent[key]; - const parAcl = ReverseHierarchyMap.get(StrCast(key === 'acl-Owner' ? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Edit) : parent[key]))?.acl; - if (parAcl) { - const sharePermission = HierarchyMapping.get(parAcl)?.name; - sharePermission && distributeAcls(key === 'acl-Owner' ? `acl-${normalizeEmail(StrCast(parent.author))}` : key, sharePermission, child, undefined, false, layoutOnly); - } - }); -} - -/** - * These are the various levels of access a user can have to a document. - * - * Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document. - * - * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document. - * - * Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything. - * - * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. - * - * None: the document is not shared with that user. - * - * Unset: Remove a sharing permission (eg., used ) - */ -export enum SharingPermissions { - Admin = 'Admin', - Edit = 'Edit', - Augment = 'Augment', - View = 'View', - None = 'Not-Shared', -} - // return acl from cache or cache the acl and return. -const getEffectiveAclCache = computedFn(function (target: any, user?: string) { - return getEffectiveAcl(target, user); -}, true); +// eslint-disable-next-line no-use-before-define +const getEffectiveAclCache = computedFn((target: any, user?: string) => getEffectiveAcl(target, user), true); /** * Calculates the effective access right to a document for the current user. */ export function GetEffectiveAcl(target: any, user?: string): symbol { if (!target) return AclPrivate; - if (target[UpdatingFromServer] || Doc.CurrentUserEmail === 'guest') return AclAdmin; + if (target[UpdatingFromServer] || ClientUtils.CurrentUserEmail === 'guest') return AclAdmin; return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) } @@ -191,10 +173,9 @@ export function GetPropAcl(target: any, prop: string | symbol | number) { return GetEffectiveAcl(target); } -let cachedGroups = observable([] as string[]); -const getCachedGroupByNameCache = computedFn(function (name: string) { - return cachedGroups.includes(name); -}, true); +const cachedGroups = observable([] as string[]); +const getCachedGroupByNameCache = computedFn((name: string) => cachedGroups.includes(name), true); + export function GetCachedGroupByName(name: string) { return getCachedGroupByNameCache(name); } @@ -205,10 +186,10 @@ function getEffectiveAcl(target: any, user?: string): symbol { const targetAcls = target[DocAcl]; if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin; - const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group + const userChecked = user || ClientUtils.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group if (targetAcls && Object.keys(targetAcls).length) { let effectiveAcl = AclPrivate; - for (const [key, value] of Object.entries(targetAcls)) { + Object.entries(targetAcls).forEach(([key, value]) => { // there are issues with storing fields with . in the name, so they are replaced with _ during creation // as a result we need to restore them again during this comparison. const entity = denormalizeEmail(key.substring(4)); // an individual or a group @@ -217,7 +198,7 @@ function getEffectiveAcl(target: any, user?: string): symbol { effectiveAcl = value as symbol; } } - } + }); return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)!.level < aclLevel.editable ? AclEdit : effectiveAcl; } @@ -236,9 +217,9 @@ function getEffectiveAcl(target: any, user?: string): symbol { * @param allowUpgrade whether permissions can be made less restrictive * @param layoutOnly just sets the layout doc's ACL (unless the data doc has no entry for the ACL, in which case it will be set as well) */ -export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited?: Doc[], allowUpgrade?: boolean, layoutOnly = false) { - const selfKey = `acl-${normalizeEmail(Doc.CurrentUserEmail)}`; - if (!visited) visited = [] as Doc[]; +// eslint-disable-next-line default-param-last +export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited: Doc[] = [], allowUpgrade?: boolean, layoutOnly = false) { + const selfKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail)}`; if (!target || visited.includes(target) || key === selfKey) return; visited.push(target); @@ -249,23 +230,21 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc if (!layoutOnly && dataDoc && (allowUpgrade !== false || !dataDoc[key] || curVal > aclVal)) { // propagate ACLs to links, children, and annotations - LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, visited, allowUpgrade ? true : false)); + dataDoc[DirectLinks].forEach(link => distributeAcls(key, acl, link, visited, !!allowUpgrade)); DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => { - distributeAcls(key, acl, d, visited, allowUpgrade ? true : false); - d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade ? true : false); + distributeAcls(key, acl, d, visited, !!allowUpgrade); + d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, !!allowUpgrade); }); DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '_annotations']).forEach(d => { - distributeAcls(key, acl, d, visited, allowUpgrade ? true : false); - d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade ? true : false); + distributeAcls(key, acl, d, visited, !!allowUpgrade); + d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, !!allowUpgrade); }); Object.keys(target) // share expanded layout templates (eg, for presElementBox'es ) .filter(lkey => lkey.includes('layout[') && DocCast(target[lkey])) - .map(lkey => { - distributeAcls(key, acl, DocCast(target[lkey]), visited, allowUpgrade ? true : false); - }); + .forEach(lkey => distributeAcls(key, acl, DocCast(target[lkey]), visited, !!allowUpgrade)); if (GetEffectiveAcl(dataDoc) === AclAdmin) { dataDoc[key] = acl; @@ -285,7 +264,23 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc dataDocChanged && updateCachedAcls(dataDoc); } -export var _propSetterCB = new Map<string, ((target: any, value: any) => void) | undefined>(); +/** + * Copies parent's acl fields to the child + */ +export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) { + [...Object.keys(parent), ...(ClientUtils.CurrentUserEmail !== parent.author ? ['acl-Owner'] : [])] + .filter(key => key.startsWith('acl')) + .forEach(key => { + // if the default acl mode is private, then don't inherit the acl-guest permission, but set it to private. + // const permission: string = key === 'acl-guest' && Doc.defaultAclPrivate ? AclPrivate : parent[key]; + const parAcl = ReverseHierarchyMap.get(StrCast(key === 'acl-Owner' ? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Edit) : parent[key]))?.acl; + if (parAcl) { + const sharePermission = HierarchyMapping.get(parAcl)?.name; + sharePermission && distributeAcls(key === 'acl-Owner' ? `acl-${normalizeEmail(StrCast(parent.author))}` : key, sharePermission, child, undefined, false, layoutOnly); + } + }); +} + /** * sets a callback function to be called whenever a value is assigned to the specified field key. * For example, this is used to "publish" documents with titles that start with '@' @@ -299,13 +294,13 @@ export function SetPropSetterCb(prop: string, setter: ((target: any, value: any) // // target should be either a Doc or ListImpl. receiver should be a Proxy<Doc> Or List. // -export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { - if (!in_prop) { +export function setter(target: any, inProp: string | symbol | number, value: any, receiver: any): boolean { + if (!inProp) { console.log('WARNING: trying to set an empty property. This should be fixed. '); return false; } - let prop = in_prop; - const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop); + let prop = inProp; + const effectiveAcl = inProp === 'constructor' || typeof inProp === 'symbol' ? AclAdmin : GetPropAcl(target, prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true; @@ -319,12 +314,24 @@ export function setter(target: any, in_prop: string | symbol | number, value: an } if (target.__fieldTuples[prop] instanceof ComputedField) { if (target.__fieldTuples[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) { - return ScriptCast(target.__fieldTuples[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false; + return !!ScriptCast(target.__fieldTuples[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success; } } return _setter(target, prop, value, receiver); } +function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any { + const field = target.__fieldTuples[prop]; + const value = field?.[ToValue]?.(proxy); // converts ComputedFields to values, or unpacks ProxyFields into Proxys + if (value) return value.value; + if (field === undefined && !ignoreProto && prop !== 'proto') { + const proto = getFieldImpl(target, 'proto', proxy, true); // TODO tfs: instead of proxy we could use target[SelfProxy]... I don't which semantics we want or if it really matters + if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) { + return getFieldImpl(proto, prop, proxy, ignoreProto); + } + } + return field; +} export function getter(target: any, prop: string | symbol, proxy: any): any { // prettier-ignore switch (prop) { @@ -336,6 +343,7 @@ export function getter(target: any, prop: string | symbol, proxy: any): any { case $mobx: return target.__fieldTuples[prop]; case DocLayout: return target.__LAYOUT__; case Height: case Width: if (GetEffectiveAcl(target) === AclPrivate) return returnZero; + // eslint-disable-next-line no-fallthrough default : if (typeof prop === 'symbol') return target[prop]; if (prop.startsWith('isMobX')) return target[prop]; @@ -343,23 +351,11 @@ export function getter(target: any, prop: string | symbol, proxy: any): any { if (GetEffectiveAcl(target) === AclPrivate && prop !== 'author') return undefined; } - const layout_prop = prop.startsWith('_') ? prop.substring(1) : undefined; - if (layout_prop && target.__LAYOUT__) return target.__LAYOUT__[layout_prop]; - return getFieldImpl(target, layout_prop ?? prop, proxy); + const layoutProp = prop.startsWith('_') ? prop.substring(1) : undefined; + if (layoutProp && target.__LAYOUT__) return target.__LAYOUT__[layoutProp]; + return getFieldImpl(target, layoutProp ?? prop, proxy); } -function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any { - const field = target.__fieldTuples[prop]; - const value = field?.[ToValue]?.(proxy); // converts ComputedFields to values, or unpacks ProxyFields into Proxys - if (value) return value.value; - if (field === undefined && !ignoreProto && prop !== 'proto') { - const proto = getFieldImpl(target, 'proto', proxy, true); //TODO tfs: instead of proxy we could use target[SelfProxy]... I don't which semantics we want or if it really matters - if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) { - return getFieldImpl(proto, prop, proxy, ignoreProto); - } - } - return field; -} export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any { return getFieldImpl(target, prop, target[SelfProxy], ignoreProto); } @@ -382,10 +378,10 @@ export function deleteProperty(target: any, prop: string | number | symbol) { // were replaced. Based on this specification, an Undo event is setup that will save enough information about the ObjectField to be // able to undo and redo the partial change. // -export function containedFieldChangedHandler(container: List<Field> | Doc, prop: string | number, liveContainedField: ObjectField) { +export function containedFieldChangedHandler(container: List<FieldType> | Doc, prop: string | number, liveContainedField: ObjectField) { let lastValue: FieldResult = liveContainedField instanceof ObjectField ? ObjectField.MakeCopy(liveContainedField) : liveContainedField; - return (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; items: Field[] | undefined; length: number | undefined; hint?: any }, dummyServerOp?: any) => { - const serializeItems = () => ({ __type: 'list', fields: diff?.items?.map((item: Field) => SerializationHelper.Serialize(item)) }); + return (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; items: FieldType[] | undefined; length: number | undefined; hint?: any }, dummyServerOp?: any) => { + const serializeItems = () => ({ __type: 'list', fields: diff?.items?.map((item: FieldType) => SerializationHelper.Serialize(item)) }); // prettier-ignore const serverOp = diff?.op === '$addToSet' ? { $addToSet: { ['fields.' + prop]: serializeItems() }, length: diff.length } @@ -401,8 +397,8 @@ export function containedFieldChangedHandler(container: List<Field> | Doc, prop: UndoManager.AddEvent( { redo: () => { - //console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo - (container as any)[prop as any]?.push(...(diff.items || [])?.map((item: any) => item.value ?? item)); + // console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo + (container as any)[prop as any]?.push(...((diff.items || [])?.map((item: any) => item.value ?? item) ?? [])); lastValue = ObjectField.MakeCopy((container as any)[prop as any]); }, undo: action(() => { @@ -416,7 +412,7 @@ export function containedFieldChangedHandler(container: List<Field> | Doc, prop: }); lastValue = ObjectField.MakeCopy((container as any)[prop as any]); }), - prop: 'add ' + diff.items?.length + ' items to list', + prop: 'add ' + (diff.items?.length ?? 0) + ' items to list', }, diff?.items ); @@ -447,12 +443,14 @@ export function containedFieldChangedHandler(container: List<Field> | Doc, prop: }); lastValue = ObjectField.MakeCopy((container as any)[prop as any]); }, - prop: 'remove ' + diff.items?.length + ' items from list', + prop: 'remove ' + (diff.items?.length ?? 0) + ' items from list', }, diff?.items ); } else { - const setFieldVal = (val: Field | undefined) => (container instanceof Doc ? (container[prop as string] = val) : (container[prop as number] = val as Field)); + const setFieldVal = (val: FieldType | undefined) => { + container instanceof Doc ? (container[prop as string] = val) : (container[prop as number] = val as FieldType); + }; UndoManager.AddEvent( { redo: () => { |