diff options
-rw-r--r-- | package-lock.json | 5 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | src/Utils.ts | 2 | ||||
-rw-r--r-- | src/client/util/LinkManager.ts | 6 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 4 | ||||
-rw-r--r-- | src/client/views/InkControlPtHandles.tsx | 16 | ||||
-rw-r--r-- | src/client/views/InkStrokeProperties.ts | 72 | ||||
-rw-r--r-- | src/client/views/InkTangentHandles.tsx | 6 | ||||
-rw-r--r-- | src/client/views/InkingStroke.tsx | 9 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 5 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 4 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.tsx | 77 | ||||
-rw-r--r-- | src/server/server_Initialization.ts | 115 |
14 files changed, 206 insertions, 122 deletions
diff --git a/package-lock.json b/package-lock.json index c1dd8506f..3aff3a549 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9264,6 +9264,11 @@ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", "optional": true }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=" + }, "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", diff --git a/package.json b/package.json index 5d10c0d54..99c818062 100644 --- a/package.json +++ b/package.json @@ -194,6 +194,7 @@ "libxmljs": "^0.19.7", "lodash": "^4.17.15", "material-ui": "^0.20.2", + "memorystream": "^0.3.1", "mobile-detect": "^1.4.4", "mobx": "^5.15.7", "mobx-react": "^5.4.4", diff --git a/src/Utils.ts b/src/Utils.ts index 53182cc9c..bfb29fe8d 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -599,7 +599,7 @@ export function getWordAtPoint(elem: any, x: number, y: number): string | undefi range.selectNodeContents(elem); var currentPos = 0; const endPos = range.endOffset; - while (currentPos + 1 < endPos) { + while (currentPos + 1 <= endPos) { range.setStart(elem, currentPos); range.setEnd(elem, currentPos + 1); const rangeRect = range.getBoundingClientRect(); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 90a8f2737..62b13e2c6 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -234,8 +234,10 @@ export class LinkManager { setTimeout(LightboxView.Next); finished?.(); } else { - const containerDoc = Cast(target.annotationOn, Doc, null) || target; - const targetContext = Cast(containerDoc?.context, Doc, null); + const containerAnnoDoc = Cast(target.annotationOn, Doc, null); + const containerDoc = containerAnnoDoc || target; + const containerDocContext = Cast(containerDoc?.context, Doc, null); + const targetContext = LightboxView.LightboxDoc ? containerAnnoDoc || containerDocContext : containerDocContext; const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined; DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "lightbox"), finished), targetNavContext, linkDoc, undefined, sourceDoc, finished); } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 17a81149c..5b44a0552 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -195,11 +195,13 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P @action onRotateDown = (e: React.PointerEvent): void => { this._rotateUndo = UndoManager.StartBatch("rotatedown"); + const pt = { x: (this.Bounds.x + this.Bounds.r) / 2, y: (this.Bounds.y + this.Bounds.b) / 2 }; setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { const movement = { X: delta[0], Y: e.clientY - down[1] }; const angle = Math.max(1, Math.abs(movement.Y / 10)); - InkStrokeProperties.Instance?.rotateInk(2 * movement.X / angle * (Math.PI / 180)); + const selectedInk = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK); + InkStrokeProperties.Instance?.rotateInk(selectedInk, 2 * movement.X / angle * (Math.PI / 180), pt); return false; }, () => { diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index 0644488b3..73de4a3e0 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -3,18 +3,20 @@ import { action, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../fields/Doc"; import { ControlPoint, InkData, PointData } from "../../fields/InkField"; +import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { Cast } from "../../fields/Types"; import { setupMoveUpEvents } from "../../Utils"; import { Transform } from "../util/Transform"; import { UndoManager } from "../util/UndoManager"; import { Colors } from "./global/globalEnums"; -import { InkStrokeProperties } from "./InkStrokeProperties"; -import { List } from "../../fields/List"; import { InkingStroke } from "./InkingStroke"; +import { InkStrokeProperties } from "./InkStrokeProperties"; +import { DocumentView } from "./nodes/DocumentView"; export interface InkControlProps { inkDoc: Doc; + inkView: DocumentView; inkCtrlPoints: InkData; screenCtrlPoints: InkData; screenSpaceLineWidth: number; @@ -51,12 +53,12 @@ export class InkControlPtHandles extends React.Component<InkControlProps> { setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt"); - InkStrokeProperties.Instance?.moveControlPtHandle(delta[0] * screenScale, delta[1] * screenScale, controlIndex); + InkStrokeProperties.Instance?.moveControlPtHandle(this.props.inkView, delta[0] * screenScale, delta[1] * screenScale, controlIndex); return false; }), action(() => { if (this.controlUndo) { - InkStrokeProperties.Instance?.snapControl(this.props.inkDoc, controlIndex); + InkStrokeProperties.Instance?.snapControl(this.props.inkView, controlIndex); } this.controlUndo?.end(); this.controlUndo = undefined; @@ -71,11 +73,11 @@ export class InkControlPtHandles extends React.Component<InkControlProps> { } else { if (brokenIndices?.includes(equivIndex)) { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth"); - InkStrokeProperties.Instance?.snapHandleTangent(equivIndex, handleIndexA, handleIndexB); + InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB); } if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth"); - InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB); + InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB); } } this.controlUndo?.end(); @@ -98,7 +100,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> { @action onDelete = (e: KeyboardEvent) => { if (["-", "Backspace", "Delete"].includes(e.key)) { - InkStrokeProperties.Instance?.deletePoints(); + InkStrokeProperties.Instance?.deletePoints(this.props.inkView); e.stopPropagation(); } } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index ee30caa3d..33e25bbbb 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -1,16 +1,15 @@ import { Bezier } from "bezier-js"; -import { action, computed, observable, reaction } from "mobx"; -import { Doc } from "../../fields/Doc"; -import { Document } from "../../fields/documentSchemas"; +import { action, observable, reaction } from "mobx"; +import { Doc, Opt } from "../../fields/Doc"; import { InkData, InkField, InkTool, PointData } from "../../fields/InkField"; import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { Cast, NumCast } from "../../fields/Types"; import { DocumentType } from "../documents/DocumentTypes"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; -import { SelectionManager } from "../util/SelectionManager"; import { undoBatch } from "../util/UndoManager"; import { InkingStroke } from "./InkingStroke"; +import { DocumentView } from "./nodes/DocumentView"; export class InkStrokeProperties { static Instance: InkStrokeProperties | undefined; @@ -25,21 +24,16 @@ export class InkStrokeProperties { reaction(() => CurrentUserUtils.SelectedTool, tool => (tool !== InkTool.None) && (this._controlButton = false)); } - @computed get selectedInk() { - const inks = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK); - return inks.length ? inks : undefined; - } - /** * Helper function that enables other functions to be applied to a particular ink instance. * @param func The inputted function. * @param requireCurrPoint Indicates whether the current selected point is needed. */ - applyFunction = (func: (doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { X: number, Y: number }[] | undefined, requireCurrPoint: boolean = false) => { + applyFunction = (strokes: Opt<DocumentView | DocumentView[]>, func: (view: DocumentView, ink: InkData, ptsXscale: number, ptsYscale: number, inkStrokeWidth: number) => { X: number, Y: number }[] | undefined, requireCurrPoint: boolean = false) => { var appliedFunc = false; - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1 && (!requireCurrPoint || this._currentPoint !== -1)) { - const doc = Document(inkView.rootDoc); + (strokes instanceof DocumentView ? [strokes] : strokes)?.forEach(action(inkView => { + if (!requireCurrPoint || this._currentPoint !== -1) { + const doc = inkView.rootDoc; if (doc.type === DocumentType.INK && doc.width && doc.height) { const ink = Cast(doc.data, InkField)?.inkData; if (ink) { @@ -47,7 +41,7 @@ export class InkStrokeProperties { const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); const ptsXscale = ((NumCast(doc._width) - NumCast(doc.strokeWidth)) / ((oldXrange.max - oldXrange.min) || 1)) || 1; const ptsYscale = ((NumCast(doc._height) - NumCast(doc.strokeWidth)) / ((oldYrange.max - oldYrange.min) || 1)) || 1; - const newPoints = func(doc, ink, ptsXscale, ptsYscale); + const newPoints = func(inkView, ink, ptsXscale, ptsYscale, NumCast(doc.strokeWidth)); if (newPoints) { const newXrange = (xs => ({ min: Math.min(...xs), max: Math.max(...xs) }))(newPoints.map(p => p.X)); const newYrange = (ys => ({ min: Math.min(...ys), max: Math.max(...ys) }))(newPoints.map(p => p.Y)); @@ -73,8 +67,9 @@ export class InkStrokeProperties { */ @undoBatch @action - addPoints = (t: number, i: number, controls: { X: number, Y: number }[]) => { - this.applyFunction((doc: Doc, ink: InkData) => { + addPoints = (inkView: DocumentView, t: number, i: number, controls: { X: number, Y: number }[]) => { + this.applyFunction(inkView, (view: DocumentView, ink: InkData) => { + const doc = view.rootDoc; const array = [controls[i], controls[i + 1], controls[i + 2], controls[i + 3]]; const newsegs = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).split(t); const splicepts = [...newsegs.left.points, ...newsegs.right.points]; @@ -143,9 +138,10 @@ export class InkStrokeProperties { */ @undoBatch @action - deletePoints = () => this.applyFunction((doc: Doc, ink: InkData) => { + deletePoints = (inkView: DocumentView) => this.applyFunction(inkView, (view: DocumentView, ink: InkData) => { + const doc = view.rootDoc; const newPoints: { X: number, Y: number }[] = []; - const toRemove = Math.floor(((this._currentPoint + 2) / 4)); + const toRemove = Math.floor((this._currentPoint + 2) / 4); const last = this._currentPoint === ink.length - 1; for (let i = 0; i < ink.length; i++) { if (Math.floor((i + 2) / 4) !== toRemove && (toRemove !== 0 || i > 3)) { @@ -164,17 +160,22 @@ export class InkStrokeProperties { */ @undoBatch @action - rotateInk = (angle: number) => { - this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { - const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); - const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); - const centerPoint = { X: (oldXrange.min + oldXrange.max) / 2, Y: (oldYrange.min + oldYrange.max) / 2 }; + rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: { x: number, y: number }) => { + this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => { + const oldXrangeMin = Math.min(...ink.map(p => p.X)); + const oldYrangeMin = Math.min(...ink.map(p => p.Y)); + const docViewCenterPt = view.screenToLocalTransform().transformPoint(scrpt.x, scrpt.y); + const inkCenterPt = { + X: (docViewCenterPt[0] - inkStrokeWidth / 2) / xScale + oldXrangeMin, + Y: (docViewCenterPt[1] - inkStrokeWidth / 2) / yScale + oldYrangeMin + }; const newPoints = ink.map(i => { - const pt = { X: i.X - centerPoint.X, Y: i.Y - centerPoint.Y }; + const pt = { X: i.X - inkCenterPt.X, Y: i.Y - inkCenterPt.Y }; const newX = Math.cos(angle) * pt.X - Math.sin(angle) * pt.Y * yScale / xScale; const newY = Math.sin(angle) * pt.X * xScale / yScale + Math.cos(angle) * pt.Y; - return { X: newX + centerPoint.X, Y: newY + centerPoint.Y }; + return { X: newX + inkCenterPt.X, Y: newY + inkCenterPt.Y }; }); + const doc = view.rootDoc; doc.rotation = NumCast(doc.rotation) + angle; return newPoints; }); @@ -185,8 +186,8 @@ export class InkStrokeProperties { */ @undoBatch @action - moveControlPtHandle = (deltaX: number, deltaY: number, controlIndex: number) => - this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { + moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number) => + this.applyFunction(inkView, (view: DocumentView, ink: InkData, xScale: number, yScale: number) => { const order = controlIndex % 4; const closed = InkingStroke.IsClosed(ink); @@ -235,7 +236,8 @@ export class InkStrokeProperties { /** * Handles the movement/scaling of a control point. */ - snapControl = (inkDoc: Doc, controlIndex: number) => { + snapControl = (inkView: DocumentView, controlIndex: number) => { + const inkDoc = inkView.rootDoc; const ink = Cast(inkDoc.data, InkField)?.inkData; if (ink) { const closed = InkingStroke.IsClosed(ink); @@ -257,8 +259,8 @@ export class InkStrokeProperties { const near = Math.sqrt((nearestPt.X - refPt.X) * (nearestPt.X - refPt.X) * ptsXscale * ptsXscale + (nearestPt.Y - refPt.Y) * (nearestPt.Y - refPt.Y) * ptsYscale * ptsYscale); - if (near / (this.selectedInk?.lastElement().props.ScreenToLocalTransform().Scale || 1) < 10) { - return this.moveControlPtHandle((nearestPt.X - ink[controlIndex].X) * ptsXscale, (nearestPt.Y - ink[controlIndex].Y) * ptsYscale, controlIndex); + if (near / (inkView.props.ScreenToLocalTransform().Scale || 1) < 10) { + return this.moveControlPtHandle(inkView, (nearestPt.X - ink[controlIndex].X) * ptsXscale, (nearestPt.Y - ink[controlIndex].Y) * ptsYscale, controlIndex); } } return false; @@ -269,8 +271,9 @@ export class InkStrokeProperties { * @param handleIndexA The handle point that retains its current position. * @param handleIndexB The handle point that is rotated to be 180 degrees from its opposite. */ - snapHandleTangent = (controlIndex: number, handleIndexA: number, handleIndexB: number) => { - this.applyFunction((doc: Doc, ink: InkData) => { + snapHandleTangent = (inkView: DocumentView, controlIndex: number, handleIndexA: number, handleIndexB: number) => { + this.applyFunction(inkView, (view: DocumentView, ink: InkData) => { + const doc = view.rootDoc; const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"), []); const ind = brokenIndices.findIndex(value => value === controlIndex); if (ind !== -1) { @@ -330,8 +333,9 @@ export class InkStrokeProperties { */ @undoBatch @action - moveTangentHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => - this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { + moveTangentHandle = (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => + this.applyFunction(inkView, (view: DocumentView, ink: InkData, xScale: number, yScale: number) => { + const doc = view.rootDoc; const closed = InkingStroke.IsClosed(ink); const oldHandlePoint = ink[handleIndex]; const oppositeHandlePoint = ink[oppositeHandleIndex]; diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx index df5bebf31..f88a20448 100644 --- a/src/client/views/InkTangentHandles.tsx +++ b/src/client/views/InkTangentHandles.tsx @@ -10,11 +10,13 @@ import { emptyFunction, setupMoveUpEvents } from "../../Utils"; import { Transform } from "../util/Transform"; import { UndoManager } from "../util/UndoManager"; import { Colors } from "./global/globalEnums"; -import { InkStrokeProperties } from "./InkStrokeProperties"; import { InkingStroke } from "./InkingStroke"; +import { InkStrokeProperties } from "./InkStrokeProperties"; +import { DocumentView } from "./nodes/DocumentView"; export interface InkHandlesProps { inkDoc: Doc; + inkView: DocumentView; screenCtrlPoints: InkData; screenSpaceLineWidth: number; ScreenToLocalTransform: () => Transform; @@ -37,7 +39,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> { setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { if (!controlUndo) controlUndo = UndoManager.StartBatch("DocDecs move tangent"); if (e.altKey) this.onBreakTangent(controlIndex); - InkStrokeProperties.Instance?.moveTangentHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex); + InkStrokeProperties.Instance?.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex); return false; }, () => { controlUndo?.end(); diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 752db1413..d312331d0 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,17 +1,17 @@ import React = require("react"); -import { Bezier } from "bezier-js"; import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../fields/Doc"; import { documentSchema } from "../../fields/documentSchemas"; import { InkData, InkField, InkTool } from "../../fields/InkField"; import { makeInterface } from "../../fields/Schema"; -import { Cast, NumCast, StrCast, BoolCast } from "../../fields/Types"; +import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { InteractionUtils } from "../util/InteractionUtils"; +import { SnappingManager } from "../util/SnappingManager"; import { ContextMenu } from "./ContextMenu"; import { ViewBoxBaseComponent } from "./DocComponent"; import { Colors } from "./global/globalEnums"; @@ -20,7 +20,6 @@ import "./InkStroke.scss"; import { InkStrokeProperties } from "./InkStrokeProperties"; import { InkTangentHandles } from "./InkTangentHandles"; import { FieldView, FieldViewProps } from "./nodes/FieldView"; -import { SnappingManager } from "../util/SnappingManager"; import Color = require("color"); type InkDocument = makeInterface<[typeof documentSchema]>; @@ -83,7 +82,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1); this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView } else if (this._properties?._controlButton) { - this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice()); + this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice()); } }), this._properties?._controlButton, this._properties?._controlButton ); @@ -166,6 +165,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume StrCast(inkDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(inkDoc.strokeBezier), "none", startMarker, endMarker, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)} <InkControlPtHandles + inkView={this.props.docViewPath().lastElement()} inkDoc={inkDoc} inkCtrlPoints={inkData} screenCtrlPoints={screenHdlPts} @@ -173,6 +173,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} ScreenToLocalTransform={this.props.ScreenToLocalTransform} /> <InkTangentHandles + inkView={this.props.docViewPath().lastElement()} inkDoc={inkDoc} screenCtrlPoints={screenHdlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index e662f3ddf..5dffc65fc 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -378,7 +378,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: // } } if (uriList) { - console.log("Web URI = ", uriList); // const existingWebDoc = await Hypothesis.findWebDoc(uriList); // if (existingWebDoc) { // const alias = Doc.MakeAlias(existingWebDoc); @@ -390,7 +389,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: // addDocument(alias); // } else { - console.log("Adding ..."); const newDoc = Docs.Create.WebDocument(uriList.split("#annotations:")[0], {// clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig) ...options, title: uriList.split("#annotations:")[0], @@ -399,8 +397,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: _nativeWidth: 850, useCors: true }); - console.log(" ... " + newDoc.title); - console.log(" ... " + addDocument(newDoc) + " " + newDoc.title); + addDocument(newDoc); } return; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 7dcd63b80..febccbfcc 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -75,7 +75,7 @@ export type collectionFreeformViewProps = { scaleField?: string; noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) engineProps?: any; - dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are trnasparent or not. + dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are transparent or not. // However, this screws up interactions since only the top layer gets events. so we render the freeformview a 3rd time with all documents in order to get interaction events (eg., marquee) but we don't actually want to display the documents. }; @@ -1472,7 +1472,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> - <div ref={this._marqueeRef} style={{ display: this.props.dontRenderDocuments ? "none" : undefined }}> + <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0 : undefined }}> {this.layoutDoc._backgroundGridShow ? this.backgroundGrid : (null)} <CollectionFreeFormViewPannableContents isAnnotationOverlay={this.isAnnotationOverlay} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 1ccf38de2..949e0e168 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -813,7 +813,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); contentScaling = () => this.ContentScale; onClickFunc = () => this.onClickHandler; - setHeight = (height: number) => this.layoutDoc._height = height; + setHeight = (height: number) => { + if (this.props.renderDepth !== -1) { + this.layoutDoc._height = height; + } + } setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view); isContentActive = (outsideReaction?: boolean) => { return CurrentUserUtils.SelectedTool !== InkTool.None || diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 37d716993..9956cc36b 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -58,8 +58,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _sidebarRef = React.createRef<SidebarAnnos>(); private _searchRef = React.createRef<HTMLInputElement>(); private _searchString = ""; + @observable private _webUrl = ""; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't wan the src parameter to also change as that would cause an unnecessary re-render. + @observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe. so thisforces the ifrmae to be rebuilt when allowScripts is toggled @observable private _searching: boolean = false; - @observable _showSidebar = false; + @observable private _showSidebar = false; @observable private _scrollTimer: any; @observable private _overlayAnnoInfo: Opt<Doc>; @observable private _marqueeing: number[] | undefined; @@ -79,6 +81,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps super(props); Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850); Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850); + runInAction(() => this._webUrl = this._url); // setting the weburl will change the src parameter of the embedded iframe and force a navigation to it. } @action @@ -239,7 +242,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale]; - if (word) { + if (word || (e.target as any || "").className.includes("rangeslider") || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) { setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. } else { this._iframeClick = this._iframe ?? undefined; @@ -269,9 +272,24 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action iframeLoaded = (e: any) => { const iframe = this._iframe; - this._iframe?.contentDocument?.addEventListener("pointerup", this.iframeUp); + let requrlraw = decodeURIComponent(iframe?.contentWindow?.location.href.replace(Utils.prepend("") + "/corsProxy/", "") ?? this._url.toString()); + if (requrlraw !== this._url.toString()) { + if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) { + const matches = requrlraw.match(/[^a-zA-z]q=[^&]*/g); + const newsearch = matches?.lastElement()!; + if (matches) { + requrlraw = requrlraw.substring(0, requrlraw.indexOf(newsearch)); + for (let i = 1; i < Array.from(matches)?.length; i++) { + requrlraw = requrlraw.replace(matches[i], ""); + } + } + requrlraw = requrlraw.replace(/q=[^&]*/, newsearch.substring(1)).replace("search&", "search?").replace("?gbv=1", ""); + } + this.submitURL(requrlraw, undefined, true); + } if (iframe?.contentDocument) { - iframe?.contentDocument.addEventListener("pointerdown", this.iframeDown); + iframe.contentDocument.addEventListener("pointerup", this.iframeUp); + iframe.contentDocument.addEventListener("pointerdown", this.iframeDown); this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument.body.scrollHeight); setTimeout(action(() => this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0)), 5000); const initialScroll = this._initialScroll; @@ -281,13 +299,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._initialScroll = undefined; } iframe.setAttribute("enable-annotation", "true"); - iframe.contentDocument.addEventListener("click", undoBatch(action(e => { + iframe.contentDocument.addEventListener("click", undoBatch(action((e: MouseEvent) => { let href = ""; - for (let ele = e.target; ele; ele = ele.parentElement) { + for (let ele = e.target as any; ele; ele = ele.parentElement) { href = (typeof (ele.href) === "string" ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href; } - if (href && this.webField?.origin) { - this.submitURL(href.replace(Utils.prepend(""), this.webField?.origin)); + const origin = this.webField?.origin; + if (href && origin) { + e.stopPropagation(); + setTimeout(() => this.submitURL(href.replace(Utils.prepend(""), origin))); if (this._outerRef.current) { this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop); this._outerRef.current.scrollLeft = 0; @@ -338,8 +358,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), []); const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); if (future.length) { + const curUrl = this._url; this.dataDoc[this.fieldKey + "-history"] = new List<string>([...history, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!)); + if (this._webUrl === this._url) { + this._webUrl = curUrl; + setTimeout(action(() => this._webUrl = this._url)); + } else { + this._webUrl = this._url; + } return true; } return false; @@ -350,10 +377,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string")); const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); if (history.length) { + const curUrl = this._url; if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]); else this.dataDoc[this.fieldKey + "-future"] = new List<string>([...future, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); - console.log(this._urlHash); + if (this._webUrl === this._url) { + this._webUrl = curUrl; + setTimeout(action(() => this._webUrl = this._url)); + } else { + this._webUrl = this._url; + } return true; } return false; @@ -362,9 +395,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps static urlHash = (s: string) => { return Math.abs(s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0)); } - @action - submitURL = (newUrl?: string, preview?: boolean) => { + submitURL = (newUrl?: string, preview?: boolean, dontUpdateIframe?: boolean) => { if (!newUrl) return; if (!newUrl.startsWith("http")) newUrl = "http://" + newUrl; try { @@ -376,7 +408,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.layoutDoc._scrollTop = 0; future && (future.length = 0); } - if (!preview) this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl)); + if (!preview) { + this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl)); + !dontUpdateIframe && (this._webUrl = this._url); + } } catch (e) { console.log("WebBox URL error:" + this._url); } @@ -430,6 +465,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!cm.findByDescription("Options...")) { !Doc.UserDoc().noviceMode && funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" }); funcs.push({ + description: (this.layoutDoc.allowScripts ? "Prevent" : "Allow") + " Scripts", event: () => { + this.layoutDoc.allowScripts = !this.layoutDoc.allowScripts; + if (this._iframe) { + runInAction(() => this._hackHide = true); + setTimeout(action(() => this._hackHide = false)); + } + }, icon: "snowflake" + }); + funcs.push({ description: (!this.layoutDoc.forceReflow ? "Force" : "Prevent") + " Reflow", event: () => { const nw = !this.layoutDoc.forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.scaling?.() || 1); this.layoutDoc.forceReflow = !nw; @@ -468,18 +512,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } @computed get urlContent() { + if (this._hackHide) return (null); const field = this.dataDoc[this.props.fieldKey]; let view; if (field instanceof HtmlField) { view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { - const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._url) : this._url; + const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._webUrl) : this._webUrl; view = <iframe className="webBox-iframe" enable-annotation={"true"} style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={url} onLoad={this.iframeLoaded} // the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page // sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />; - sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin"} />; + sandbox={`${this.layoutDoc.allowScripts ? "allow-scripts" : ""} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`} />; } else { view = <iframe className="webBox-iframe" enable-annotation={"true"} style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly @@ -615,12 +660,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps moveDocument={this.moveDocument} addDocument={this.sidebarAddDocument} styleProvider={this.childStyleProvider} - pointerEvents={CurrentUserUtils.SelectedTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />; + childPointerEvents={this.props.isContentActive() ? "all" : undefined} + pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />; return ( <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.pointerEvents() }} > <div className={`webBox-container`} style={{ pointerEvents }} onContextMenu={this.specificContextMenu}> - <base target="_blank" /> <div className={"webBox-outerContent"} ref={this._outerRef} style={{ width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale * (this._previewWidth ? scale : 1)}px)`, diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index 00a801e03..de93b64c3 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -13,6 +13,7 @@ import * as request from 'request'; import * as webpack from 'webpack'; import * as wdm from 'webpack-dev-middleware'; import * as whm from 'webpack-hot-middleware'; +import * as zlib from 'zlib'; import { publicDirectory } from '.'; import { logPort } from './ActionUtilities'; import { SSL } from './apis/google/CredentialsLoader'; @@ -36,39 +37,30 @@ export let resolvedPorts: { server: number, socket: number } = { server: 1050, s export let resolvedServerUrl: string; export default async function InitializeServer(routeSetter: RouteSetter) { + const isRelease = determineEnvironment(); const app = buildWithMiddleware(express()); - // Root route of express app - app.get("/", (req, res) => res.redirect("/home")); - app.use(express.static(publicDirectory, { - setHeaders: res => res.setHeader("Access-Control-Allow-Origin", "*") - })); - app.use("/images", express.static(publicDirectory)); + // route table managed by express. routes are tested sequentially against each of these map rules. when a match is found, the handler is called to process the request + app.get(new RegExp(/^\/+$/), (req, res) => res.redirect(req.user ? "/home" : "/login")); // target urls that consist of one or more '/'s with nothing in between + app.use(express.static(publicDirectory, { setHeaders: res => res.setHeader("Access-Control-Allow-Origin", "*") })); //all urls that start with dash's public directory: /files/ (e.g., /files/images, /files/audio, etc) app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) })); - app.use(wdm(compiler, { publicPath: config.output.publicPath })); app.use(whm(compiler)); - - registerAuthenticationRoutes(app); - registerCorsProxy(app); - - const isRelease = determineEnvironment(); - + registerAuthenticationRoutes(app); // this adds routes to authenticate a user (login, etc) + registerCorsProxy(app); // this adds a /corsProxy/ route to allow clients to get to urls that would otherwise be blocked by cors policies isRelease && !SSL.Loaded && SSL.exit(); - - routeSetter(new RouteManager(app, isRelease)); - registerRelativePath(app); + routeSetter(new RouteManager(app, isRelease)); // this sets up all the regular supervised routes (things like /home, download/upload api's, pdf, search, session, etc) + registerEmbeddedBrowseRelativePathHandler(app); // this allows renered web pages which internally have relative paths to find their content let server: HttpServer | HttpsServer; - const { serverPort, serverName } = process.env; - isRelease && serverPort && (resolvedPorts.server = Number(serverPort)); + isRelease && process.env.serverPort && (resolvedPorts.server = Number(process.env.serverPort)); await new Promise<void>(resolve => server = isRelease ? createServer(SSL.Credentials, app).listen(resolvedPorts.server, resolve) : app.listen(resolvedPorts.server, resolve) ); logPort("server", resolvedPorts.server); - resolvedServerUrl = `${isRelease && serverName ? `https://${serverName}.com` : "http://localhost"}:${resolvedPorts.server}`; + resolvedServerUrl = `${isRelease && process.env.serverName ? `https://${process.env.serverName}.com` : "http://localhost"}:${resolvedPorts.server}`; // initialize the web socket (bidirectional communication: if a user changes // a field on one client, that change must be broadcast to all other clients) @@ -141,54 +133,81 @@ function registerAuthenticationRoutes(server: express.Express) { } function registerCorsProxy(server: express.Express) { - const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; server.use("/corsProxy", async (req, res) => { - const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : ""; - const requrlraw = decodeURIComponent(req.url.substring(1)); + let requrlraw = decodeURIComponent(req.url.substring(1)); + const qsplit = requrlraw.split("?q="); + const newqsplit = requrlraw.split("&q="); + if (qsplit.length > 1 && newqsplit.length > 1) { + const lastq = newqsplit[newqsplit.length - 1]; + requrlraw = qsplit[0] + "?q=" + lastq.split("&")[0] + "&" + qsplit[1].split("&")[1]; + } const requrl = requrlraw.startsWith("/") ? referer + requrlraw : requrlraw; - // cors weirdness here... - // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative, + // cors weirdness here... + // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative, // then we redirect again to the cors referer and just add the relative path. if (!requrl.startsWith("http") && req.originalUrl.startsWith("/corsProxy") && referer?.includes("corsProxy")) { res.redirect(referer + (referer.endsWith("/") ? "" : "/") + requrl); } else { - try { - await new Promise<void>((resolve, reject) => { - request(requrl).on("response", resolve).on("error", reject); - }); - } catch { - console.log(`Malformed CORS url: ${requrl}`); - return res.send(); - } - req.pipe(request(requrl)).on("response", res => { - const headers = Object.keys(res.headers); - headers.forEach(headerName => { - const header = res.headers[headerName]; - if (Array.isArray(header)) { - res.headers[headerName] = header.filter(h => !headerCharRegex.test(h)); - } else if (header) { - if (headerCharRegex.test(header as any)) { - delete res.headers[headerName]; - } - } - }); - }).on("error", () => console.log(`Malformed CORS url: ${requrl}`)).pipe(res); + proxyServe(req, requrl, res); } }); } -function registerRelativePath(server: express.Express) { +function proxyServe(req: any, requrl: string, response: any) { + const htmlBodyMemoryStream = new (require('memorystream'))(); + req.headers.cookie = ""; + req.pipe(request(requrl)) + .on("error", (e: any) => console.log(`Malformed CORS url: ${requrl}`, e)) + .on("end", () => { + var rewrittenHtmlBody: any = undefined; + req.pipe(request(requrl)) + .on("response", (res: any) => { + const headers = Object.keys(res.headers); + const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; + headers.forEach(headerName => { + const header = res.headers[headerName]; + if (Array.isArray(header)) { + res.headers[headerName] = header.filter(h => !headerCharRegex.test(h)); + } else if (headerCharRegex.test(header || "")) { + delete res.headers[headerName]; + } + if (headerName === "content-encoding" && header.includes("gzip")) { + try { + const replacer = (match: any, href: string, offset: any, string: any) => { + return `href="${resolvedServerUrl + "/corsProxy/http" + href}"`; + }; + const zipToStringDecoder = new (require('string_decoder').StringDecoder)('utf8'); + const htmlText = zipToStringDecoder.write(zlib.gunzipSync(htmlBodyMemoryStream.read()).toString('utf8') + .replace('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>') + .replace(/href="http([^"]*)"/g, replacer) + .replace(/target="_blank"/g, "")); + rewrittenHtmlBody = zlib.gzipSync(htmlText); + } catch (e) { console.log(e); } + } + }); + }) + .on('data', (e: any) => { + rewrittenHtmlBody && response.send(rewrittenHtmlBody); + rewrittenHtmlBody = undefined; + }) + .pipe(response); + }) + .pipe(htmlBodyMemoryStream); +} + +function registerEmbeddedBrowseRelativePathHandler(server: express.Express) { server.use("*", (req, res) => { const relativeUrl = req.originalUrl; - if (!res.headersSent && req.headers.referer?.includes("corsProxy")) { // a request for something by a proxied referrer means it must be a relative reference. So construct a proxied absolute reference here. + if (!req.user) res.redirect("/home"); // When no user is logged in, we interpret a relative URL as being a reference to something they don't have access to and redirect to /home + else if (!res.headersSent && req.headers.referer?.includes("corsProxy")) { // a request for something by a proxied referrer means it must be a relative reference. So construct a proxied absolute reference here. const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:<port>/corsProxy/https://en.wikipedia.org/wiki/Engelbart) const dashServerUrl = proxiedRefererUrl.match(/.*corsProxy\//)![0]; // the dash server url (e.g.: http://localhost:<port>/corsProxy/ ) const actualReferUrl = proxiedRefererUrl.replace(dashServerUrl, ""); // the url of the referer without the proxy (e.g., : http:s//en.wikipedia.org/wiki/Engelbart) const absoluteTargetBaseUrl = actualReferUrl.match(/http[s]?:\/\/[^\/]*/)![0]; // the base of the original url (e.g., https://en.wikipedia.org) const redirectedProxiedUrl = dashServerUrl + encodeURIComponent(absoluteTargetBaseUrl + relativeUrl); // the new proxied full url (e..g, http://localhost:<port>/corsProxy/https://en.wikipedia.org/<somethingelse>) res.redirect(redirectedProxiedUrl); - } else if (relativeUrl.startsWith("/search")) { // detect search query and use default search engine + } else if (relativeUrl.startsWith("/search") && !req.headers.referer?.includes("corsProxy")) { // detect search query and use default search engine res.redirect(req.headers.referer + "corsProxy/" + encodeURIComponent("http://www.google.com" + relativeUrl)); } else { res.end(); |