(core) add SELF_HYPERLINK() function for generating links to the current document

Summary:
 * Adds a `SELF_HYPERLINK()` python function, with optional keyword arguments to set a label, the page, and link parameters.
 * Adds a `UUID()` python function, since using python's uuid.uuidv4 hits a problem accessing /dev/urandom in the sandbox.  UUID makes no particular quality claims since it doesn't use an audited implementation.  A difficult to guess code is convenient for some use cases that `SELF_HYPERLINK()` enables.

The canonical URL for a document is mutable, but older versions generally forward.  So for implementation simplicity the document url is passed it on sandbox creation and remains fixed throughout the lifetime of the sandbox.  This could and should be improved in future.

The URL is passed into the sandbox as a `DOC_URL` environment variable.

The code for creating the URL is factored out of `Notifier.ts`. Since the url is a function of the organization as well as the document, some rejiggering is needed to make that information available to DocManager.

On document imports, the new document is registered in the database slightly earlier now, in order to keep the procedure for constructing the URL in different starting conditions more homogeneous.

Test Plan: updated test

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2759
This commit is contained in:
Paul Fitzpatrick
2021-03-18 18:40:02 -04:00
parent b4c34cedad
commit 0c5f7cf0a7
14 changed files with 207 additions and 39 deletions

View File

@@ -16,7 +16,7 @@ import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
import {assertAccess, Authorizer, DocAuthorizer, DummyAuthorizer,
isSingleUserMode} from 'app/server/lib/Authorizer';
import {Client} from 'app/server/lib/Client';
import {makeExceptionalDocSession, makeOptDocSession, OptDocSession} from 'app/server/lib/DocSession';
import {getDocSessionCachedDoc, makeExceptionalDocSession, makeOptDocSession, OptDocSession} from 'app/server/lib/DocSession';
import * as docUtils from 'app/server/lib/docUtils';
import {GristServer} from 'app/server/lib/GristServer';
import {IDocStorageManager} from 'app/server/lib/IDocStorageManager';
@@ -181,26 +181,26 @@ export class DocManager extends EventEmitter {
const accessId = this.makeAccessId(userId);
const docSession = makeExceptionalDocSession('nascent', {browserSettings});
const result = await this._doImportDoc(docSession,
globalUploadSet.getUploadInfo(uploadId, accessId), {
naming: workspaceId ? 'saved' : 'unsaved',
userId,
});
if (workspaceId) {
const register = async (docId: string, docTitle: string) => {
if (!workspaceId || !this._homeDbManager) { return; }
const queryResult = await this._homeDbManager.addDocument({userId}, workspaceId,
{name: result.title}, result.id);
{name: docTitle}, docId);
if (queryResult.status !== 200) {
// TODO The ready-to-add document is not yet in storageManager, but is in the filesystem. It
// should get cleaned up in case of error here.
throw new ApiError(queryResult.errMessage || 'unable to add imported document', queryResult.status);
}
}
};
return this._doImportDoc(docSession,
globalUploadSet.getUploadInfo(uploadId, accessId), {
naming: workspaceId ? 'saved' : 'unsaved',
register,
userId,
});
// The imported document is associated with the worker that did the import.
// We could break that association (see /api/docs/:docId/assign for how) if
// we start using dedicated import workers.
return result;
}
/**
@@ -369,7 +369,8 @@ export class DocManager extends EventEmitter {
public async createNewEmptyDoc(docSession: OptDocSession, basenameHint: string): Promise<ActiveDoc> {
const docName = await this._createNewDoc(basenameHint);
return mapSetOrClear(this._activeDocs, docName,
this.gristServer.create.ActiveDoc(this, docName).createDoc(docSession));
this._createActiveDoc(docSession, docName)
.then(newDoc => newDoc.createDoc(docSession)));
}
/**
@@ -389,12 +390,15 @@ export class DocManager extends EventEmitter {
}
}
if (!this._activeDocs.has(docName)) {
const newDoc = this.gristServer.create.ActiveDoc(this, docName, wantRecoveryMode);
// Propagate backupMade events from newly opened activeDocs (consolidate all to DocMan)
newDoc.on('backupMade', (bakPath: string) => {
this.emit('backupMade', bakPath);
});
return mapSetOrClear(this._activeDocs, docName, newDoc.loadDoc(docSession));
return mapSetOrClear(this._activeDocs, docName,
this._createActiveDoc(docSession, docName, wantRecoveryMode)
.then(newDoc => {
// Propagate backupMade events from newly opened activeDocs (consolidate all to DocMan)
newDoc.on('backupMade', (bakPath: string) => {
this.emit('backupMade', bakPath);
});
return newDoc.loadDoc(docSession);
}));
}
const activeDoc = await this._activeDocs.get(docName)!;
if (!activeDoc.muted) { return activeDoc; }
@@ -425,7 +429,28 @@ export class DocManager extends EventEmitter {
encBundles: EncActionBundleFromHub[]): Promise<ActiveDoc> {
const docName = await this._createNewDoc(basenameHint);
return mapSetOrClear(this._activeDocs, docName,
this.gristServer.create.ActiveDoc(this, docName).downloadSharedDoc(docId, instanceId, encBundles));
this._createActiveDoc({client: null}, docName)
.then(newDoc => newDoc.downloadSharedDoc(docId, instanceId, encBundles)));
}
private async _createActiveDoc(docSession: OptDocSession, docName: string, safeMode?: boolean) {
// Get URL for document for use with SELF_HYPERLINK().
const cachedDoc = getDocSessionCachedDoc(docSession);
let docUrl: string|undefined;
try {
if (cachedDoc) {
docUrl = await this.gristServer.getResourceUrl(cachedDoc);
} else {
docUrl = await this.gristServer.getDocUrl(docName);
}
} catch (e) {
// If there is no home url, we cannot construct links. Accept this, for the benefit
// of legacy tests.
if (!String(e).match(/need APP_HOME_URL/)) {
throw e;
}
}
return this.gristServer.create.ActiveDoc(this, docName, {docUrl, safeMode});
}
/**
@@ -435,6 +460,7 @@ export class DocManager extends EventEmitter {
private async _doImportDoc(docSession: OptDocSession, uploadInfo: UploadInfo,
options: {
naming: 'classic'|'saved'|'unsaved',
register?: (docId: string, docTitle: string) => Promise<void>,
userId?: number,
}): Promise<DocCreationInfo> {
try {
@@ -466,6 +492,7 @@ export class DocManager extends EventEmitter {
default:
throw new Error('naming mode not recognized');
}
await options.register?.(id, basename);
if (ext === '.grist') {
// If the import is a grist file, copy it to the docs directory.
// TODO: We should be skeptical of the upload file to close a possible