aboutsummaryrefslogtreecommitdiff
path: root/src/fields/util.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/fields/util.ts')
-rw-r--r--src/fields/util.ts222
1 files changed, 119 insertions, 103 deletions
diff --git a/src/fields/util.ts b/src/fields/util.ts
index b73520999..a6499c3e3 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,12 +1,11 @@
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';
@@ -17,22 +16,43 @@ import { SchemaHeaderField } from './SchemaHeaderField';
import { ComputedField } from './ScriptField';
import { DocCast, ScriptCast, StrCast } from './Types';
+/**
+ * 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])) {
@@ -49,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) {
@@ -56,11 +77,14 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
delete curValue[FieldChanged];
}
+ 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 =
@@ -83,7 +107,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
if (value === undefined)
(target as Doc|ObjectField)[FieldChanged]?.(undefined, { $unset: { ['fields.' + prop]: '' } });
else (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) :value}});
- if (prop === 'author' || prop.toString().startsWith('acl')) updateCachedAcls(target);
+ if (prop === 'author' || prop.toString().startsWith('acl_')) updateCachedAcls(target);
} else {
DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue);
}
@@ -92,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];
@@ -128,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)
}
@@ -188,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);
}
@@ -200,12 +184,12 @@ export function SetCachedGroups(groups: string[]) {
}
function getEffectiveAcl(target: any, user?: string): symbol {
const targetAcls = target[DocAcl];
- if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin;
+ 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
@@ -214,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;
}
@@ -226,16 +210,16 @@ function getEffectiveAcl(target: any, user?: string): symbol {
/**
* Recursively distributes the access right for a user across the children of a document and its annotations.
- * @param key the key storing the access right (e.g. acl-groupname)
+ * @param key the key storing the access right (e.g. acl_groupname)
* @param acl the access right being stored (e.g. "Can Edit")
* @param target the document on which this access right is being set
* @param visited list of Doc's already distributed to.
* @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);
@@ -246,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;
@@ -282,15 +264,46 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
dataDocChanged && updateCachedAcls(dataDoc);
}
+/**
+ * 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 '@'
+ * @param prop
+ * @param propSetter
+ */
+export function SetPropSetterCb(prop: string, propSetter: ((target: any, value: any) => void) | undefined) {
+ _propSetterCB.set(prop, propSetter);
+}
+
//
// 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 {
- let prop = in_prop;
- const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : GetPropAcl(target, 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 = 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;
+ if (typeof prop === 'string' && prop.startsWith('acl_') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true;
if (typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_')) {
if (!prop.startsWith('__')) prop = prop.substring(1);
@@ -301,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) {
@@ -318,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];
@@ -325,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);
}
@@ -364,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 }
@@ -383,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(() => {
@@ -398,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
);
@@ -429,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(' + ((container as any)?.title ?? '') + ':' + prop + ')',
},
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: () => {