diff --git a/app/client/components/BaseView.js b/app/client/components/BaseView.js index f7459c78..c089126d 100644 --- a/app/client/components/BaseView.js +++ b/app/client/components/BaseView.js @@ -373,12 +373,17 @@ BaseView.prototype._parsePasteForView = function(data, fields) { const updateColIds = updateCols.map(c => c && c.colId()); const updateColTypes = updateCols.map(c => c && c.type()); const parsers = fields.map(field => field && field.valueParser() || (x => x)); + const docIdHash = tableUtil.getDocIdHash(); const richData = data.map((col, idx) => { if (!col.length) { return col; } - const typeMatches = col[0] && col[0].colType === updateColTypes[idx]; + const typeMatches = col[0] && col[0].colType === updateColTypes[idx] && ( + // When copying references, only use the row ID (raw value) when copying within the same document + // to avoid referencing the wrong rows. + col[0].docIdHash === docIdHash || !gristTypes.isFullReferencingType(updateColTypes[idx]) + ); const parser = parsers[idx]; return col.map(v => { if (v) { diff --git a/app/client/lib/tableUtil.ts b/app/client/lib/tableUtil.ts index d1c25595..a48c7b2c 100644 --- a/app/client/lib/tableUtil.ts +++ b/app/client/lib/tableUtil.ts @@ -1,6 +1,7 @@ import type {CopySelection} from 'app/client/components/CopySelection'; import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals'; import type {KoArray} from 'app/client/lib/koArray'; +import {simpleStringHash} from 'app/client/lib/textUtils'; import type {ViewFieldRec} from 'app/client/models/DocModel'; import type {BulkUpdateRecord} from 'app/common/DocActions'; import {safeJsonParse} from 'app/common/gutil'; @@ -78,6 +79,15 @@ export function makePasteText(tableData: TableData, selection: CopySelection) { return tsvEncode(values); } +/** + * Hash of the current docId to allow checking if copying and pasting is happening in the same document, + * without leaking the actual docId which may allow others to access the document. + */ +export function getDocIdHash(): string { + const docId = (window as any).gristDocPageModel.currentDocId.get(); + return simpleStringHash(docId); +} + /** * Returns an html table of containing the cells denoted by the cross product of * the given rows and columns, styled by the given table/row/col style dictionaries. @@ -90,7 +100,8 @@ export function makePasteHtml(tableData: TableData, selection: CopySelection, in const rowStyle = selection.rowStyle || {}; // Maps rowId to style object. const colStyle = selection.colStyle || {}; // Maps colId to style object. - const elem = dom('table', {border: '1', cellspacing: '0', style: 'white-space: pre'}, + const elem = dom('table', + {border: '1', cellspacing: '0', style: 'white-space: pre', 'data-grist-doc-id-hash': getDocIdHash()}, dom('colgroup', selection.colIds.map(colId => dom('col', { style: _styleAttr(colStyle[colId]), @@ -121,6 +132,7 @@ export function makePasteHtml(tableData: TableData, selection: CopySelection, in export interface RichPasteObject { displayValue: string; + docIdHash?: string|null; colType?: string|null; // Column type of the source column. rawValue?: unknown; // Optional rawValue that should be used if colType matches destination. } @@ -134,13 +146,14 @@ export function parsePasteHtml(data: string): RichPasteObject[][] { const parser = new G.DOMParser() as DOMParser; const doc = parser.parseFromString(data, 'text/html'); const table = doc.querySelector('table'); + const docIdHash = table?.getAttribute('data-grist-doc-id-hash'); const colTypes = Array.from(table!.querySelectorAll('col'), col => col.getAttribute('data-grist-col-type')); const result = Array.from(table!.querySelectorAll('tr'), (row, rowIdx) => Array.from(row.querySelectorAll('td, th'), (cell, colIdx) => { - const o: RichPasteObject = { displayValue: cell.textContent! }; + const o: RichPasteObject = {displayValue: cell.textContent!, docIdHash}; // If there's a column type, add it to the object if (colTypes[colIdx]) { diff --git a/app/client/lib/textUtils.ts b/app/client/lib/textUtils.ts index 0dd8b5d2..28a65c75 100644 --- a/app/client/lib/textUtils.ts +++ b/app/client/lib/textUtils.ts @@ -45,3 +45,32 @@ export function findLinks(text: string): Array<{value: string, isLink: boolean}> // urls will be at odd-number indices return text.split(urlRegex).map((value, i) => ({ value, isLink : (i % 2) === 1})); } + +/** + * Based on https://stackoverflow.com/a/22429679/2482744 + * ----------------------------------------------------- + * Calculate a 32 bit FNV-1a hash + * Found here: https://gist.github.com/vaiorabbit/5657561 + * Ref.: http://isthe.com/chongo/tech/comp/fnv/ + */ +export function hashFnv32a(str: string): string { + let hval = 0x811c9dc5; + for (let i = 0; i < str.length; i++) { + hval ^= str.charCodeAt(i); + hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24); + } + // Convert to 8 digit hex string + return ("0000000" + (hval >>> 0).toString(16)).substr(-8); +} + +/** + * A poor man's hash for when proper crypto isn't worth it. + */ +export function simpleStringHash(str: string) { + let result = ''; + // Crudely convert 32 bits to 128 bits to reduce collisions + for (let i = 0; i < 4; i++) { + result += hashFnv32a(result + str); + } + return result; +}