diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index 5078b2ab..be9c1028 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -15,6 +15,7 @@ import { docSessionFromRequest, makeExceptionalDocSession, OptDocSession } from import { DocWorker } from "app/server/lib/DocWorker"; import { IDocWorkerMap } from "app/server/lib/DocWorkerMap"; import { expressWrap } from 'app/server/lib/expressWrap'; +import { filterDocumentInPlace } from "app/server/lib/filterUtils"; import { GristServer } from 'app/server/lib/GristServer'; import { HashUtil } from 'app/server/lib/HashUtil'; import { makeForkIds } from "app/server/lib/idUtils"; @@ -218,7 +219,8 @@ export class DocWorkerApi { const docId = stringParam(req.params.docId); const srcDocId = stringParam(req.body.srcDocId); if (srcDocId !== req.specialPermit?.otherDocId) { throw new Error('access denied'); } - await this._docManager.storageManager.prepareFork(srcDocId, docId); + const fname = await this._docManager.storageManager.prepareFork(srcDocId, docId); + await filterDocumentInPlace(docSessionFromRequest(req), fname); res.json({srcDocId, docId}); })); diff --git a/app/server/lib/DocStorageManager.ts b/app/server/lib/DocStorageManager.ts index 1795da05..68875864 100644 --- a/app/server/lib/DocStorageManager.ts +++ b/app/server/lib/DocStorageManager.ts @@ -90,8 +90,10 @@ export class DocStorageManager implements IDocStorageManager { // nothing to do } - public async prepareFork(srcDocName: string, destDocName: string): Promise { - // nothing to do + public async prepareFork(srcDocName: string, destDocName: string): Promise { + // This is implemented only to support old tests. + await fse.copy(this.getPath(srcDocName), this.getPath(destDocName)); + return this.getPath(destDocName); } /** diff --git a/app/server/lib/DocWorker.ts b/app/server/lib/DocWorker.ts index 5172127c..39ed5994 100644 --- a/app/server/lib/DocWorker.ts +++ b/app/server/lib/DocWorker.ts @@ -7,7 +7,8 @@ import {ActionHistoryImpl} from 'app/server/lib/ActionHistoryImpl'; import {assertAccess, getOrSetDocAuth, getUserId, RequestWithLogin} from 'app/server/lib/Authorizer'; import {Client} from 'app/server/lib/Client'; import * as Comm from 'app/server/lib/Comm'; -import {DocSession} from 'app/server/lib/DocSession'; +import {DocSession, docSessionFromRequest} from 'app/server/lib/DocSession'; +import {filterDocumentInPlace} from 'app/server/lib/filterUtils'; import {IDocStorageManager} from 'app/server/lib/IDocStorageManager'; import * as log from 'app/server/lib/log'; import {integerParam, optStringParam, stringParam} from 'app/server/lib/requestUtils'; @@ -75,6 +76,7 @@ export class DocWorker { // If template flag is on, remove data and history from the download. await removeData(tmpPath); } + await filterDocumentInPlace(docSessionFromRequest(mreq), tmpPath); // NOTE: We may want to reconsider the mimeType used for Grist files. return res.type('application/x-sqlite3') .download(tmpPath, (optStringParam(req.query.title) || docTitle || 'document') + ".grist", async (err: any) => { diff --git a/app/server/lib/HostedStorageManager.ts b/app/server/lib/HostedStorageManager.ts index 982851b0..3c6e6222 100644 --- a/app/server/lib/HostedStorageManager.ts +++ b/app/server/lib/HostedStorageManager.ts @@ -248,8 +248,9 @@ export class HostedStorageManager implements IDocStorageManager { * Initialize one document from another, associating the result with the current * worker. */ - public async prepareFork(srcDocName: string, destDocName: string): Promise { + public async prepareFork(srcDocName: string, destDocName: string): Promise { await this.prepareLocalDoc(destDocName, srcDocName); + return this.getPath(destDocName); } // Gets a copy of the document, eg. for downloading. Returns full file path. diff --git a/app/server/lib/IDocStorageManager.ts b/app/server/lib/IDocStorageManager.ts index 87f22674..8d7f44eb 100644 --- a/app/server/lib/IDocStorageManager.ts +++ b/app/server/lib/IDocStorageManager.ts @@ -12,7 +12,7 @@ export interface IDocStorageManager { // AsyncCreate[docName]. prepareLocalDoc(docName: string): Promise; prepareToCreateDoc(docName: string): Promise; - prepareFork(srcDocName: string, destDocName: string): Promise; + prepareFork(srcDocName: string, destDocName: string): Promise; // Returns filename. listDocs(): Promise; deleteDoc(docName: string, deletePermanently?: boolean): Promise; diff --git a/app/server/lib/filterUtils.ts b/app/server/lib/filterUtils.ts new file mode 100644 index 00000000..1ae00ae5 --- /dev/null +++ b/app/server/lib/filterUtils.ts @@ -0,0 +1,28 @@ +import { OpenMode, SQLiteDB } from 'app/server/lib/SQLiteDB'; +import { OptDocSession } from "app/server/lib/DocSession"; + +/** + * Filter a Grist document when it is copied or downloaded. Changes made: + * - Any FullCopies special rules are removed. + * In the future, the changes could be made conditional on the user. This would + * allow us for example to permit downloads of documents with row-level filters + * in place. + */ +export async function filterDocumentInPlace(docSession: OptDocSession, filename: string) { + // We ignore docSession for now, since no changes are user-dependent yet. + // The change we need to make is simple, so we open the doc as a SQLite DB. + // Note: the change is not entered in document history. + const db = await SQLiteDB.openDBRaw(filename, OpenMode.OPEN_EXISTING); + // Fetch ids of any special resources mentioning FullCopies (ideally there would be + // at most one). + const resourceIds = (await db.all("SELECT id FROM _grist_ACLResources " + + "WHERE tableId='*SPECIAL' AND colIds='FullCopies'")) + .map(row => row.id as number); + if (resourceIds.length > 0) { + // Remove any related rules. + await db.run(`DELETE FROM _grist_ACLRules WHERE resource IN (${resourceIds})`); + // Remove the resources. + await db.run(`DELETE FROM _grist_ACLResources WHERE id IN (${resourceIds})`); + } + await db.close(); +}