mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) Check document ID when parsing pasted references
Summary: Add doc-id attribute to copied HTML columns next to column type. Only use the raw value (rather than the display value) when the parsed doc-id from pasted HTML matches the current document ID, similar to ensuring that the type matches. This only applies to references and reflists. Test Plan: Extended CopyPaste.ts Reviewers: dsagal Reviewed By: dsagal Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D3154
This commit is contained in:
		
							parent
							
								
									482ce0e4c4
								
							
						
					
					
						commit
						551ea28fc4
					
				@ -373,12 +373,17 @@ BaseView.prototype._parsePasteForView = function(data, fields) {
 | 
				
			|||||||
  const updateColIds = updateCols.map(c => c && c.colId());
 | 
					  const updateColIds = updateCols.map(c => c && c.colId());
 | 
				
			||||||
  const updateColTypes = updateCols.map(c => c && c.type());
 | 
					  const updateColTypes = updateCols.map(c => c && c.type());
 | 
				
			||||||
  const parsers = fields.map(field => field && field.valueParser() || (x => x));
 | 
					  const parsers = fields.map(field => field && field.valueParser() || (x => x));
 | 
				
			||||||
 | 
					  const docIdHash = tableUtil.getDocIdHash();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const richData = data.map((col, idx) => {
 | 
					  const richData = data.map((col, idx) => {
 | 
				
			||||||
    if (!col.length) {
 | 
					    if (!col.length) {
 | 
				
			||||||
      return col;
 | 
					      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];
 | 
					    const parser = parsers[idx];
 | 
				
			||||||
    return col.map(v => {
 | 
					    return col.map(v => {
 | 
				
			||||||
      if (v) {
 | 
					      if (v) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import type {CopySelection} from 'app/client/components/CopySelection';
 | 
					import type {CopySelection} from 'app/client/components/CopySelection';
 | 
				
			||||||
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
 | 
					import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
 | 
				
			||||||
import type {KoArray} from 'app/client/lib/koArray';
 | 
					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 {ViewFieldRec} from 'app/client/models/DocModel';
 | 
				
			||||||
import type {BulkUpdateRecord} from 'app/common/DocActions';
 | 
					import type {BulkUpdateRecord} from 'app/common/DocActions';
 | 
				
			||||||
import {safeJsonParse} from 'app/common/gutil';
 | 
					import {safeJsonParse} from 'app/common/gutil';
 | 
				
			||||||
@ -78,6 +79,15 @@ export function makePasteText(tableData: TableData, selection: CopySelection) {
 | 
				
			|||||||
  return tsvEncode(values);
 | 
					  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
 | 
					 * 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.
 | 
					 * 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 rowStyle = selection.rowStyle || {};    // Maps rowId to style object.
 | 
				
			||||||
  const colStyle = selection.colStyle || {};    // Maps colId 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('colgroup', selection.colIds.map(colId =>
 | 
				
			||||||
      dom('col', {
 | 
					      dom('col', {
 | 
				
			||||||
        style: _styleAttr(colStyle[colId]),
 | 
					        style: _styleAttr(colStyle[colId]),
 | 
				
			||||||
@ -121,6 +132,7 @@ export function makePasteHtml(tableData: TableData, selection: CopySelection, in
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export interface RichPasteObject {
 | 
					export interface RichPasteObject {
 | 
				
			||||||
  displayValue: string;
 | 
					  displayValue: string;
 | 
				
			||||||
 | 
					  docIdHash?: string|null;
 | 
				
			||||||
  colType?: string|null;  // Column type of the source column.
 | 
					  colType?: string|null;  // Column type of the source column.
 | 
				
			||||||
  rawValue?: unknown;     // Optional rawValue that should be used if colType matches destination.
 | 
					  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 parser = new G.DOMParser() as DOMParser;
 | 
				
			||||||
  const doc = parser.parseFromString(data, 'text/html');
 | 
					  const doc = parser.parseFromString(data, 'text/html');
 | 
				
			||||||
  const table = doc.querySelector('table');
 | 
					  const table = doc.querySelector('table');
 | 
				
			||||||
 | 
					  const docIdHash = table?.getAttribute('data-grist-doc-id-hash');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const colTypes = Array.from(table!.querySelectorAll('col'), col =>
 | 
					  const colTypes = Array.from(table!.querySelectorAll('col'), col =>
 | 
				
			||||||
    col.getAttribute('data-grist-col-type'));
 | 
					    col.getAttribute('data-grist-col-type'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const result = Array.from(table!.querySelectorAll('tr'), (row, rowIdx) =>
 | 
					  const result = Array.from(table!.querySelectorAll('tr'), (row, rowIdx) =>
 | 
				
			||||||
    Array.from(row.querySelectorAll('td, th'), (cell, colIdx) => {
 | 
					    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 there's a column type, add it to the object
 | 
				
			||||||
      if (colTypes[colIdx]) {
 | 
					      if (colTypes[colIdx]) {
 | 
				
			||||||
 | 
				
			|||||||
@ -45,3 +45,32 @@ export function findLinks(text: string): Array<{value: string, isLink: boolean}>
 | 
				
			|||||||
  // urls will be at odd-number indices
 | 
					  // urls will be at odd-number indices
 | 
				
			||||||
  return text.split(urlRegex).map((value, i) => ({ value, isLink : (i % 2) === 1}));
 | 
					  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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user