Index: lams_build/conf/whiteboard/config.default.yml =================================================================== diff -u -r1cb71fb7cfd13ce1de802d0a069ebe849466a7fc -r1616c084c977d21c5a92f57fe18c89e16172776a --- lams_build/conf/whiteboard/config.default.yml (.../config.default.yml) (revision 1cb71fb7cfd13ce1de802d0a069ebe849466a7fc) +++ lams_build/conf/whiteboard/config.default.yml (.../config.default.yml) (revision 1616c084c977d21c5a92f57fe18c89e16172776a) @@ -31,15 +31,13 @@ # Image download format, can be "png", "jpeg" (or "webp" -> only working on chrome) -- string imageDownloadFormat: "png" - # LAMS addition - URL prefix for uploaded images, without the trailing slash, for example - # imageURL: "http://localhost:9003" # if it is blank, images will use relative path, for example "/uploads/.png" imageURL: "" # draw the background grid to images on download ? (If True, even PNGs are also not transparent anymore) -- boolean drawBackgroundGrid: false - # Background Image; Can be "bg_grid.png" or "bg_dots.png" -- string + # Background Image; Can be "bg_grid.png", "bg_dots.png" or "bg_white.png" (Place your background at assets/images if you want your own background) -- string backgroundGridImage: "bg_grid.png" # Frontend performance tweaks Index: lams_build/conf/whiteboard/config/webpack.base.js =================================================================== diff -u --- lams_build/conf/whiteboard/config/webpack.base.js (revision 0) +++ lams_build/conf/whiteboard/config/webpack.base.js (revision 1616c084c977d21c5a92f57fe18c89e16172776a) @@ -0,0 +1,64 @@ +const webpack = require("webpack"); +const { CleanWebpackPlugin } = require("clean-webpack-plugin"); +const CopyPlugin = require("copy-webpack-plugin"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const path = require("path"); + +const config = { + entry: { + main: ["./src/js/index.js"], + "pdf.worker": "pdfjs-dist/build/pdf.worker.entry", + }, + output: { + path: path.join(__dirname, "..", "dist"), + filename: "[name]-[fullhash].js", + }, + resolve: { + extensions: ["*", ".json", ".js"], + }, + module: { + rules: [ + { + test: /\.(js)$/, + exclude: /node_modules/, + loader: "babel-loader", + options: { + compact: true, + }, + }, + { + test: /\.css$/, + use: ["style-loader", "css-loader"], + }, + { + test: /\.(png|jpe?g|gif|otf|pdf)$/i, + use: [ + { + loader: "file-loader", + }, + ], + }, + ], + }, + plugins: [ + new CleanWebpackPlugin(), + new webpack.ProvidePlugin({ + $: "jquery", + jQuery: "jquery", + "window.jQuery": "jquery", + "window.$": "jquery", + }), + new CopyPlugin({ patterns: [{ from: "assets", to: "" }] }), + new HtmlWebpackPlugin({ + template: "src/index.html", + minify: false, + inject: true, + }), + ], + // LAMS modification + devServer: { + port: 9003 + } +}; + +module.exports = config; Index: lams_build/conf/whiteboard/readme.txt =================================================================== diff -u -rae5f13de8792e1d26a5c9771d7897f868f5bdeaf -r1616c084c977d21c5a92f57fe18c89e16172776a --- lams_build/conf/whiteboard/readme.txt (.../readme.txt) (revision ae5f13de8792e1d26a5c9771d7897f868f5bdeaf) +++ lams_build/conf/whiteboard/readme.txt (.../readme.txt) (revision 1616c084c977d21c5a92f57fe18c89e16172776a) @@ -2,7 +2,7 @@ 1. Get code from https://github.com/cracker0dks/whiteboard -Current version is 1.6 with all changes up to date 2021-05-21 +Current version is 1.7 with all changes up to date 2023-02-10 It requires Node.js version 16 as version 14 has a problem with image loading https://stackoverflow.com/questions/63610932/express-static-network-requests-stuck-on-pending @@ -13,7 +13,9 @@ 3.1 In main.js we allow cloning content from another Whiteboard canvas even if target canvas is not empty. There is also some processing of source data - all images are put in background, so target canvas' drawings go on top. - 3.2 In server.js we set up default port to 9003 instead of 8080, so it does not collide with WildFly development mode. + 3.2 In server.js and webpack.base.js we set up default port to 9003 instead of 8080, + so it does not collide with WildFly development mode. + Whiteboard in development mode seems to always run on 8081 port, though. 3.3 In server-backend.js we introduce hashing of wid + accesstoken to improve security. Also an API method is added to upload Whiteboard canvas content after tool content import @@ -24,4 +26,4 @@ 3.5 In index.html we hide some buttons In index.html and index.js we hide Whiteboard contents until everything loads, otherwise the UI looks messed up at first. - 3.6 In main.css we make buttons on iPad smaller. Otherwise they obstruct pretty much whole canvas. \ No newline at end of file + 3.6 In main.css we make buttons on iPad smaller. Otherwise they obstruct pretty much whole canvas. \ No newline at end of file Index: lams_build/conf/whiteboard/scripts/s_whiteboard.js =================================================================== diff -u -r63061fed615977a3a7e768fa11cf4e7f0db9dade -r1616c084c977d21c5a92f57fe18c89e16172776a --- lams_build/conf/whiteboard/scripts/s_whiteboard.js (.../s_whiteboard.js) (revision 63061fed615977a3a7e768fa11cf4e7f0db9dade) +++ lams_build/conf/whiteboard/scripts/s_whiteboard.js (.../s_whiteboard.js) (revision 1616c084c977d21c5a92f57fe18c89e16172776a) @@ -1,19 +1,31 @@ //This file is only for saving the whiteboard. const fs = require("fs"); const config = require("./config/config"); +const { getSafeFilePath } = require("./utils"); +const FILE_DATABASE_FOLDER = "savedBoards"; var savedBoards = {}; var savedUndos = {}; var saveDelay = {}; if (config.backend.enableFileDatabase) { // make sure that folder with saved boards exists - fs.mkdirSync("savedBoards", { + fs.mkdirSync(FILE_DATABASE_FOLDER, { // this option also mutes an error if path exists recursive: true }); } +/** + * Get the file path for a whiteboard. + * @param {string} wid Whiteboard id to get the path for + * @returns {string} File path to the whiteboard + * @throws {Error} if wid contains potentially unsafe directory characters + */ +function fileDatabasePath(wid) { + return getSafeFilePath(FILE_DATABASE_FOLDER, wid + ".json"); +} + module.exports = { handleEventsAndData: function (content) { var tool = content["t"]; //Tool witch is used @@ -24,7 +36,7 @@ delete savedBoards[wid]; delete savedUndos[wid]; // delete the corresponding file too - fs.unlink("savedBoards/" + wid + ".json", function (err) { + fs.unlink(fileDatabasePath(wid), function (err) { if (err) { return console.log(err); } @@ -59,7 +71,6 @@ if (!savedUndos[wid]) { savedUndos[wid] = []; } - console.log("undo wid: " + wid, savedUndos[wid]); let savedBoard = this.loadStoredData(wid); for (var i = savedUndos[wid].length - 1; i >= 0; i--) { if (savedUndos[wid][i]["username"] == username) { @@ -121,7 +132,7 @@ saveDelay[wid] = false; if (savedBoards[wid]) { fs.writeFile( - "savedBoards/" + wid + ".json", + fileDatabasePath(wid), JSON.stringify(savedBoards[wid]), (err) => { if (err) { @@ -145,7 +156,7 @@ // try to load from DB if (config.backend.enableFileDatabase) { //read saved board from file - var filePath = "savedBoards/" + wid + ".json"; + var filePath = fileDatabasePath(wid); if (fs.existsSync(filePath)) { var data = fs.readFileSync(filePath); if (data) { @@ -162,23 +173,23 @@ return; } savedBoards[targetWid] = sourceData.slice(); - - // LAMS: after load prefix author so his steps can not be undone + + // LAMS: after load prefix author so his steps can not be undone for (var i = 0; i < savedBoards[targetWid].length; i++) { let username = savedBoards[targetWid][i]["username"]; if (username && !username.startsWith('authored-')) { savedBoards[targetWid][i]["username"] = 'authored-' + username; } - } + } this.saveToDB(targetWid); }, - saveData: function(wid, data, processEmbeddedImages) { + saveData: function (wid, data, processEmbeddedImages) { const existingData = this.loadStoredData(wid); if (existingData.length > 0 || !data) { return; } - let savedBoard = JSON.parse(data); + let savedBoard = JSON.parse(data); // importing LAMS content which has base64 images embedded if (processEmbeddedImages) { Index: lams_build/conf/whiteboard/scripts/server-backend.js =================================================================== diff -u -r1cb71fb7cfd13ce1de802d0a069ebe849466a7fc -r1616c084c977d21c5a92f57fe18c89e16172776a --- lams_build/conf/whiteboard/scripts/server-backend.js (.../server-backend.js) (revision 1cb71fb7cfd13ce1de802d0a069ebe849466a7fc) +++ lams_build/conf/whiteboard/scripts/server-backend.js (.../server-backend.js) (revision 1616c084c977d21c5a92f57fe18c89e16172776a) @@ -3,6 +3,7 @@ const config = require("./config/config"); const ReadOnlyBackendService = require("./services/ReadOnlyBackendService"); const WhiteboardInfoBackendService = require("./services/WhiteboardInfoBackendService"); +const { getSafeFilePath } = require("./utils"); function startBackendServer(port) { var fs = require("fs-extra"); @@ -25,14 +26,27 @@ var io = require("socket.io")(server, { path: "/ws-api" }); WhiteboardInfoBackendService.start(io); - console.log("Webserver & socketserver running on port:" + port); + console.log("socketserver running on port:" + port); const { accessToken, enableWebdav } = config.backend; //Expose static folders app.use(express.static(path.join(__dirname, "..", "dist"))); app.use("/uploads", express.static(path.join(__dirname, "..", "public", "uploads"))); + /** + * @api {get} /api/health Health Check + * @apiDescription This returns nothing but a status code of 200 + * @apiName health + * @apiGroup WhiteboardAPI + * + * @apiSuccess {Number} 200 OK + */ + app.get("/api/health", function (req, res) { + res.status(200); //OK + res.end(); + }); + // LAMS introduced this function to enforce security // it mimics WhiteboardService#getWhiteboardAccessTokenHash() const hashAccessToken = function (wid){ @@ -46,7 +60,7 @@ } return "" + hash; } - + /** * @api {get} /api/loadwhiteboard Get Whiteboard Data * @apiDescription This returns all the Available Data ever drawn to this Whiteboard @@ -63,19 +77,20 @@ * curl -i http://[rootUrl]/api/loadwhiteboard?wid=[MyWhiteboardId] */ app.get("/api/loadwhiteboard", function (req, res) { - const wid = req["query"]["wid"]; - const at = req["query"]["at"]; //accesstoken - const targetWid = req["query"]["targetWid"]; - const embedImages = req["query"]["embedImages"]; - + let query = escapeAllContentStrings(req["query"]); + const wid = query["wid"]; + const at = query["at"]; //accesstoken + const targetWid = query["targetWid"]; + const embedImages = query["embedImages"]; + // if targetWid is present, hash generation is based on combined wids if (accessToken === "" || hashAccessToken(wid + (targetWid || "")) == at) { const widForData = ReadOnlyBackendService.isReadOnly(wid) ? ReadOnlyBackendService.getIdFromReadOnlyId(wid) : wid; let ret = s_whiteboard.loadStoredData(widForData); - - if (embedImages) { + + if (embedImages) { // exporting LAMS content: save image data directly in JSON ret = ret.slice(); ret.forEach(function(entry){ @@ -87,7 +102,7 @@ entry.imageData = contents; }); } - + res.send(ret); res.end(); } else { @@ -155,8 +170,9 @@ * curl -i http://[rootUrl]/api/getReadOnlyWid?wid=[MyWhiteboardId] */ app.get("/api/getReadOnlyWid", function (req, res) { - const wid = req["query"]["wid"]; - const at = req["query"]["at"]; //accesstoken + let query = escapeAllContentStrings(req["query"]); + const wid = query["wid"]; + const at = query["at"]; //accesstoken if (accessToken === "" || hashAccessToken(wid) == at) { res.send(ReadOnlyBackendService.getReadOnlyId(wid)); res.end(); @@ -174,8 +190,8 @@ * * @apiParam {Number} wid WhiteboardId you find in the Whiteboard URL * @apiParam {Number} [at] Accesstoken (Only if activated for this server) - * @apiParam {Number} current timestamp - * @apiParam {Boolean} webdavaccess set true to upload to webdav (Optional; Only if activated for this server) + * @apiParam {Number} [date] current timestamp (This is for the filename on the server; Don't set it if not sure) + * @apiParam {Boolean} [webdavaccess] set true to upload to webdav (Optional; Only if activated for this server) * @apiParam {String} imagedata The imagedata base64 encoded * * @apiSuccess {String} body returns "done" @@ -245,16 +261,16 @@ * "removeTextbox", * "setTextboxPosition", * "setTextboxFontSize", - * "setTextboxFontColor", + * "setTextboxFontColor" * @apiParam {String} [username] The username performing this action. Only relevant for the undo/redo function * @apiParam {Number} [draw] Only has a function if t is set to "addImgBG". Set 1 to draw on canvas; 0 to draw into background * @apiParam {String} [url] Only has a function if t is set to "addImgBG", then it has to be set to: [rootUrl]/uploads/[ReadOnlyWid]/[ReadOnlyWid]_[date].png * @apiParam {String} [c] Color: Only used if color is needed (pen, rect, circle, addTextBox ... ) * @apiParam {String} [th] Thickness: Only used if Thickness is needed (pen, rect ... ) * @apiParam {Number[]} d has different function on every tool you use: - * pen: [width, height, left, top, rotation] + * fx. pen or addImgBG: [width, height, left, top, rotation] * - * @apiSuccess {String} body returns the "done" as text + * @apiSuccess {String} body returns "done" as text * @apiError {Number} 401 Unauthorized */ app.get("/api/drawToWhiteboard", function (req, res) { @@ -284,7 +300,7 @@ function progressUploadFormData(formData, callback) { console.log("Progress new Form Data"); const fields = escapeAllContentStrings(formData.fields); - const wid = fields["whiteboardId"]; + const wid = fields["wid"]; if (ReadOnlyBackendService.isReadOnly(wid)) return; const readOnlyWid = ReadOnlyBackendService.getReadOnlyId(wid); @@ -298,7 +314,7 @@ webdavaccess = false; } - const savingDir = path.join("./public/uploads", readOnlyWid); + const savingDir = getSafeFilePath("public/uploads", readOnlyWid); fs.ensureDir(savingDir, function (err) { if (err) { console.log("Could not create upload folder!", err); @@ -311,7 +327,7 @@ .replace(/^data:image\/png;base64,/, "") .replace(/^data:image\/jpeg;base64,/, ""); console.log(filename, "uploaded"); - const savingPath = path.join(savingDir, filename); + const savingPath = getSafeFilePath(savingDir, filename); fs.writeFile(savingPath, imagedata, "base64", function (err) { if (err) { console.log("error", err); @@ -387,6 +403,8 @@ if (!whiteboardId || ReadOnlyBackendService.isReadOnly(whiteboardId)) return; content = escapeAllContentStrings(content); + content = purifyEncodedStrings(content); + if (accessToken === "" || hashAccessToken(content["wid"]) == content["at"]) { const broadcastTo = (wid) => socket.compress(false).broadcast.to(wid).emit("drawToWhiteboard", content); @@ -454,6 +472,46 @@ return content; } + //Sanitize strings known to be encoded and decoded + function purifyEncodedStrings(content) { + if (content.hasOwnProperty("t") && content["t"] === "setTextboxText") { + return purifyTextboxTextInContent(content); + } + return content; + } + + function purifyTextboxTextInContent(content) { + const raw = content["d"][1]; + const decoded = base64decode(raw); + const purified = DOMPurify.sanitize(decoded, { + ALLOWED_TAGS: ["div", "br"], + ALLOWED_ATTR: [], + ALLOW_DATA_ATTR: false, + }); + + if (purified !== decoded) { + console.warn("setTextboxText payload needed be DOMpurified"); + console.warn("raw: " + removeControlCharactersForLogs(raw)); + console.warn("decoded: " + removeControlCharactersForLogs(decoded)); + console.warn("purified: " + removeControlCharactersForLogs(purified)); + } + + content["d"][1] = base64encode(purified); + return content; + } + + function base64encode(s) { + return Buffer.from(s, "utf8").toString("base64"); + } + + function base64decode(s) { + return Buffer.from(s, "base64").toString("utf8"); + } + + function removeControlCharactersForLogs(s) { + return s.replace(/[\u0000-\u001F\u007F-\u009F]/g, ""); + } + process.on("unhandledRejection", (error) => { // Will print "unhandledRejection err is not defined" console.log("unhandledRejection", error.message); Index: lams_build/conf/whiteboard/src/css/main.css =================================================================== diff -u -rc03b7b669adda44ad8dc8f14ad4e40c0be688c7b -r1616c084c977d21c5a92f57fe18c89e16172776a --- lams_build/conf/whiteboard/src/css/main.css (.../main.css) (revision c03b7b669adda44ad8dc8f14ad4e40c0be688c7b) +++ lams_build/conf/whiteboard/src/css/main.css (.../main.css) (revision 1616c084c977d21c5a92f57fe18c89e16172776a) @@ -73,7 +73,7 @@ .btn-group button .activeToolIcon + img { top: 7px !important; - } + } } /* @@ -240,3 +240,29 @@ padding: 5px; margin: 5px; } + +.picker_wrapper .picker_palette { + width: 100%; + order: 1; + display: flex; + margin-top: 0; + margin-bottom: 0; + flex-wrap: wrap; +} + +.picker_wrapper .picker_splotch { + /*flex:1;*/ + width: 17px; + height: 19px; + margin: 4px 4px; + box-shadow: 0 0 0 1px silver; + border: 2px solid transparent; +} + +.picker_wrapper .picker_splotch:hover { + border: 2px solid black; +} + +.picker_wrapper .picker_splotch.picker_splotch_active { + border: 2px dotted yellow; +} Index: lams_build/conf/whiteboard/src/index.html =================================================================== diff -u -r1cb71fb7cfd13ce1de802d0a069ebe849466a7fc -r1616c084c977d21c5a92f57fe18c89e16172776a --- lams_build/conf/whiteboard/src/index.html (.../index.html) (revision 1cb71fb7cfd13ce1de802d0a069ebe849466a7fc) +++ lams_build/conf/whiteboard/src/index.html (.../index.html) (revision 1616c084c977d21c5a92f57fe18c89e16172776a) @@ -51,6 +51,15 @@ + + + +
+ +
+ + `; + + let colorPicker = null; + function intColorPicker(initColor) { + if (colorPicker) { + colorPicker.destroy(); + } + colorPicker = new Picker({ + parent: $("#whiteboardColorpicker")[0], + color: initColor || "#000000", + onChange: function (color) { + whiteboard.setDrawColor(color.rgbaString); + }, + onDone: function (color) { + let palette = JSON.parse(localStorage.getItem("savedColors")); + if (!palette.includes(color.rgbaString)) { + palette.push(color.rgbaString); + localStorage.setItem("savedColors", JSON.stringify(palette)); + } + intColorPicker(color.rgbaString); + }, + onOpen: colorPickerOnOpen, + template: colorPickerTemplate, + }); + } + intColorPicker(); + + let bgColorPicker = null; + function intBgColorPicker(initColor) { + if (bgColorPicker) { + bgColorPicker.destroy(); + } + bgColorPicker = new Picker({ + parent: $("#textboxBackgroundColorPicker")[0], + color: initColor || "#f5f587", + bgcolor: initColor || "#f5f587", + onChange: function (bgcolor) { + whiteboard.setTextBackgroundColor(bgcolor.rgbaString); + }, + onDone: function (bgcolor) { + let palette = JSON.parse(localStorage.getItem("savedColors")); + if (!palette.includes(color.rgbaString)) { + palette.push(color.rgbaString); + localStorage.setItem("savedColors", JSON.stringify(palette)); + } + intBgColorPicker(color.rgbaString); + }, + onOpen: colorPickerOnOpen, + template: colorPickerTemplate, + }); + } + intBgColorPicker(); + // on startup select mouse shortcutFunctions.setTool_mouse(); // fix bug cursor not showing up @@ -828,15 +963,16 @@ url: document.URL.substr(0, document.URL.lastIndexOf("/")) + "/api/upload", data: { imagedata: base64data, - whiteboardId: whiteboardId, + wid: whiteboardId, date: date, at: accessToken, }, success: function (msg) { const { correspondingReadOnlyWid } = ConfigService; const filename = `${correspondingReadOnlyWid}_${date}.png`; + const rootUrl = document.URL.substr(0, document.URL.lastIndexOf("/")); whiteboard.addImgToCanvasByUrl( - `/uploads/${correspondingReadOnlyWid}/${filename}` + `${rootUrl}/uploads/${correspondingReadOnlyWid}/${filename}` ); //Add image to canvas console.log("Image uploaded!"); }, @@ -853,7 +989,7 @@ url: document.URL.substr(0, document.URL.lastIndexOf("/")) + "/api/upload", data: { imagedata: base64data, - whiteboardId: whiteboardId, + wid: whiteboardId, date: date, at: accessToken, webdavaccess: JSON.stringify(webdavaccess), @@ -913,7 +1049,7 @@ // handle pasting from clipboard window.addEventListener("paste", function (e) { - if ($(".basicalert").length > 0) { + if ($(".basicalert").length > 0 || !!e.origin) { return; } if (e.clipboardData) { Index: lams_build/conf/whiteboard/src/js/whiteboard.js =================================================================== diff -u -r63061fed615977a3a7e768fa11cf4e7f0db9dade -r1616c084c977d21c5a92f57fe18c89e16172776a --- lams_build/conf/whiteboard/src/js/whiteboard.js (.../whiteboard.js) (revision 63061fed615977a3a7e768fa11cf4e7f0db9dade) +++ lams_build/conf/whiteboard/src/js/whiteboard.js (.../whiteboard.js) (revision 1616c084c977d21c5a92f57fe18c89e16172776a) @@ -5,6 +5,7 @@ import ThrottlingService from "./services/ThrottlingService"; import ConfigService from "./services/ConfigService"; import html2canvas from "html2canvas"; +import DOMPurify from "dompurify"; const RAD_TO_DEG = 180.0 / Math.PI; const DEG_TO_RAD = Math.PI / 180.0; @@ -25,6 +26,7 @@ * @type Point */ startCoords: new Point(0, 0), + viewCoords: { x: 0, y: 0 }, drawFlag: false, oldGCO: null, mouseover: false, @@ -150,6 +152,8 @@ currentPos.x, currentPos.y, ]; + } else if (_this.tool === "hand") { + _this.startCoords = currentPos; } else if (_this.tool === "eraser") { _this.drawEraserLine( currentPos.x, @@ -160,7 +164,12 @@ ); _this.sendFunction({ t: _this.tool, - d: [currentPos.x, currentPos.y, currentPos.x, currentPos.y], + d: [ + currentPos.x - _this.viewCoords.x, + currentPos.y - _this.viewCoords.y, + currentPos.x - _this.viewCoords.x, + currentPos.y - _this.viewCoords.y, + ], th: _this.thickness, }); } else if (_this.tool === "line") { @@ -222,7 +231,29 @@ }); _this.mouseOverlay.on("mousemove touchmove", function (e) { + //Move hole canvas e.preventDefault(); + + if (_this.tool == "hand" && _this.drawFlag) { + let currentPos = Point.fromEvent(e); + let xDif = _this.startCoords.x - currentPos.x; + let yDif = _this.startCoords.y - currentPos.y; + + _this.viewCoords.x -= xDif; + _this.viewCoords.y -= yDif; + + _this.startCoords.x = currentPos.x; + _this.startCoords.y = currentPos.y; + + const dbCp = JSON.parse(JSON.stringify(_this.drawBuffer)); // Copy the buffer + _this.canvas.width = $(window).width(); + _this.canvas.height = $(window).height(); // Set new canvas height + _this.drawBuffer = []; + _this.textContainer.empty(); + _this.imgContainer.empty(); + _this.loadData(dbCp); // draw old content in + } + if (ReadOnlyService.readOnlyActive) return; _this.triggerMouseMove(e); }); @@ -237,7 +268,6 @@ } if (ReadOnlyService.readOnlyActive) return; _this.drawFlag = false; - _this.drawId++; _this.ctx.globalCompositeOperation = _this.oldGCO; let currentPos = Point.fromEvent(e); @@ -264,15 +294,18 @@ ); _this.sendFunction({ t: _this.tool, - d: [currentPos.x, currentPos.y, _this.startCoords.x, _this.startCoords.y], + d: [ + currentPos.x - _this.viewCoords.x, + currentPos.y - _this.viewCoords.y, + _this.startCoords.x - _this.viewCoords.x, + _this.startCoords.y - _this.viewCoords.y, + ], c: _this.drawcolor, th: _this.thickness, }); _this.svgContainer.find("line").remove(); } else if (_this.tool === "pen") { - _this.drawId--; _this.pushPointSmoothPen(currentPos.x, currentPos.y); - _this.drawId++; } else if (_this.tool === "rect") { if (_this.pressedKeys.shift) { if ( @@ -301,7 +334,12 @@ ); _this.sendFunction({ t: _this.tool, - d: [_this.startCoords.x, _this.startCoords.y, currentPos.x, currentPos.y], + d: [ + _this.startCoords.x - _this.viewCoords.x, + _this.startCoords.y - _this.viewCoords.y, + currentPos.x - _this.viewCoords.x, + currentPos.y - _this.viewCoords.y, + ], c: _this.drawcolor, th: _this.thickness, }); @@ -317,7 +355,11 @@ ); _this.sendFunction({ t: _this.tool, - d: [_this.startCoords.x, _this.startCoords.y, r], + d: [ + _this.startCoords.x - _this.viewCoords.x, + _this.startCoords.y - _this.viewCoords.y, + r, + ], c: _this.drawcolor, th: _this.thickness, }); @@ -397,15 +439,24 @@ _this.drawId++; _this.sendFunction({ t: _this.tool, - d: [left, top, leftT, topT, width, height], + d: [ + left - _this.viewCoords.x, //left from + top - _this.viewCoords.y, + leftT - _this.viewCoords.x, //Left too + topT - _this.viewCoords.y, + width, + height, + ], }); + _this.dragCanvasRectContent(left, top, leftT, topT, width, height); imgDiv.remove(); dragOutOverlay.remove(); }); imgDiv.draggable(); _this.svgContainer.find("rect").remove(); } + _this.drawId++; }; _this.mouseOverlay.on("mouseout", function (e) { @@ -430,8 +481,8 @@ _this.drawcolor, _this.textboxBackgroundColor, fontsize, - currentPos.x, - currentPos.y, + currentPos.x - _this.viewCoords.x, + currentPos.y - _this.viewCoords.y, txId, isStickyNote, ], @@ -440,8 +491,8 @@ _this.drawcolor, _this.textboxBackgroundColor, fontsize, - currentPos.x, - currentPos.y, + currentPos.x - _this.viewCoords.x, + currentPos.y - _this.viewCoords.y, txId, isStickyNote, true @@ -495,7 +546,12 @@ ); _this.sendFunction({ t: _this.tool, - d: [currentPos.x, currentPos.y, _this.prevPos.x, _this.prevPos.y], + d: [ + currentPos.x - _this.viewCoords.x, + currentPos.y - _this.viewCoords.y, + _this.prevPos.x - _this.viewCoords.x, + _this.prevPos.y - _this.viewCoords.y, + ], th: _this.thickness, }); } @@ -557,6 +613,8 @@ }); ThrottlingService.throttle(currentPos, () => { + currentPos.x -= _this.viewCoords.x; + currentPos.y -= _this.viewCoords.y; _this.lastPointerPosition = currentPos; _this.sendFunction({ t: "cursor", @@ -622,7 +680,11 @@ var left = Math.round(p.left * 100) / 100; var top = Math.round(p.top * 100) / 100; _this.drawId++; - _this.sendFunction({ t: "eraseRec", d: [left, top, width, height] }); + _this.sendFunction({ + t: "eraseRec", + d: [left - _this.viewCoords.x, top - _this.viewCoords.y, width, height], + }); + _this.eraseRec(left, top, width, height); }); _this.mouseOverlay.find(".xCanvasBtn").click(); //Remove all current drops @@ -653,45 +715,71 @@ _this.penSmoothLastCoords.push(X, Y); if (_this.penSmoothLastCoords.length >= 8) { _this.drawPenSmoothLine(_this.penSmoothLastCoords, _this.drawcolor, _this.thickness); + let sendArray = []; + for (let i = 0; i < _this.penSmoothLastCoords.length; i++) { + sendArray.push(_this.penSmoothLastCoords[i]); + if (i % 2 == 0) { + sendArray[i] -= this.viewCoords.x; + } else { + sendArray[i] -= this.viewCoords.y; + } + } _this.sendFunction({ t: _this.tool, - d: _this.penSmoothLastCoords, + d: sendArray, c: _this.drawcolor, th: _this.thickness, }); } }, - dragCanvasRectContent: function (xf, yf, xt, yt, width, height) { + dragCanvasRectContent: function (xf, yf, xt, yt, width, height, remote) { + var _this = this; + let xOffset = remote ? _this.viewCoords.x : 0; + let yOffset = remote ? _this.viewCoords.y : 0; var tempCanvas = document.createElement("canvas"); tempCanvas.width = width; tempCanvas.height = height; var tempCanvasContext = tempCanvas.getContext("2d"); - tempCanvasContext.drawImage(this.canvas, xf, yf, width, height, 0, 0, width, height); - this.eraseRec(xf, yf, width, height); - this.ctx.drawImage(tempCanvas, xt, yt); + tempCanvasContext.drawImage( + this.canvas, + xf + xOffset, + yf + yOffset, + width, + height, + 0, + 0, + width, + height + ); + this.eraseRec(xf + xOffset, yf + yOffset, width, height); + this.ctx.drawImage(tempCanvas, xt + xOffset, yt + yOffset); }, - eraseRec: function (fromX, fromY, width, height) { + eraseRec: function (fromX, fromY, width, height, remote) { var _this = this; + let xOffset = remote ? _this.viewCoords.x : 0; + let yOffset = remote ? _this.viewCoords.y : 0; _this.ctx.beginPath(); - _this.ctx.rect(fromX, fromY, width, height); + _this.ctx.rect(fromX + xOffset, fromY + yOffset, width, height); _this.ctx.fillStyle = "rgba(0,0,0,1)"; _this.ctx.globalCompositeOperation = "destination-out"; _this.ctx.fill(); _this.ctx.closePath(); _this.ctx.globalCompositeOperation = _this.oldGCO; }, - drawPenLine: function (fromX, fromY, toX, toY, color, thickness) { + drawPenLine: function (fromX, fromY, toX, toY, color, thickness, remote) { var _this = this; _this.ctx.beginPath(); - _this.ctx.moveTo(fromX, fromY); - _this.ctx.lineTo(toX, toY); + let xOffset = remote ? _this.viewCoords.x : 0; + let yOffset = remote ? _this.viewCoords.y : 0; + _this.ctx.moveTo(fromX + xOffset, fromY + yOffset); + _this.ctx.lineTo(toX + xOffset, toY + yOffset); _this.ctx.strokeStyle = color; _this.ctx.lineWidth = thickness; _this.ctx.lineCap = _this.lineCap; _this.ctx.stroke(); _this.ctx.closePath(); }, - drawPenSmoothLine: function (coords, color, thickness) { + drawPenSmoothLine: function (coords, color, thickness, remote) { var _this = this; var xm1 = coords[0]; var ym1 = coords[1]; @@ -704,25 +792,29 @@ var length = Math.sqrt(Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2)); var steps = Math.ceil(length / 5); _this.ctx.beginPath(); - _this.ctx.moveTo(x0, y0); + let xOffset = remote ? _this.viewCoords.x : 0; + let yOffset = remote ? _this.viewCoords.y : 0; + _this.ctx.moveTo(x0 + xOffset, y0 + yOffset); if (steps == 0) { - _this.ctx.lineTo(x0, y0); + _this.ctx.lineTo(x0 + xOffset, y0 + yOffset); } for (var i = 0; i < steps; i++) { var point = lanczosInterpolate(xm1, ym1, x0, y0, x1, y1, x2, y2, (i + 1) / steps); - _this.ctx.lineTo(point[0], point[1]); + _this.ctx.lineTo(point[0] + xOffset, point[1] + yOffset); } _this.ctx.strokeStyle = color; _this.ctx.lineWidth = thickness; _this.ctx.lineCap = _this.lineCap; _this.ctx.stroke(); _this.ctx.closePath(); }, - drawEraserLine: function (fromX, fromY, toX, toY, thickness) { + drawEraserLine: function (fromX, fromY, toX, toY, thickness, remote) { var _this = this; + let xOffset = remote ? _this.viewCoords.x : 0; + let yOffset = remote ? _this.viewCoords.y : 0; _this.ctx.beginPath(); - _this.ctx.moveTo(fromX, fromY); - _this.ctx.lineTo(toX, toY); + _this.ctx.moveTo(fromX + xOffset, fromY + yOffset); + _this.ctx.lineTo(toX + xOffset, toY + yOffset); _this.ctx.strokeStyle = "rgba(0,0,0,1)"; _this.ctx.lineWidth = thickness * 2; _this.ctx.lineCap = _this.lineCap; @@ -731,22 +823,26 @@ _this.ctx.closePath(); _this.ctx.globalCompositeOperation = _this.oldGCO; }, - drawRec: function (fromX, fromY, toX, toY, color, thickness) { + drawRec: function (fromX, fromY, toX, toY, color, thickness, remote) { var _this = this; - toX = toX - fromX; - toY = toY - fromY; + let xOffset = remote ? _this.viewCoords.x : 0; + let yOffset = remote ? _this.viewCoords.y : 0; + toX = toX - fromX - xOffset; + toY = toY - fromY - yOffset; _this.ctx.beginPath(); - _this.ctx.rect(fromX, fromY, toX, toY); + _this.ctx.rect(fromX + xOffset, fromY + yOffset, toX + xOffset, toY + yOffset); _this.ctx.strokeStyle = color; _this.ctx.lineWidth = thickness; _this.ctx.lineCap = _this.lineCap; _this.ctx.stroke(); _this.ctx.closePath(); }, - drawCircle: function (fromX, fromY, radius, color, thickness) { + drawCircle: function (fromX, fromY, radius, color, thickness, remote) { var _this = this; + let xOffset = remote ? _this.viewCoords.x : 0; + let yOffset = remote ? _this.viewCoords.y : 0; _this.ctx.beginPath(); - _this.ctx.arc(fromX, fromY, radius, 0, 2 * Math.PI, false); + _this.ctx.arc(fromX + xOffset, fromY + yOffset, radius, 0, 2 * Math.PI, false); _this.ctx.lineWidth = thickness; _this.ctx.strokeStyle = color; _this.ctx.stroke(); @@ -774,6 +870,14 @@ _this.setTextboxFontSize(_this.latestActiveTextBoxId, thickness); } }, + imgWithSrc(url) { + return $( + DOMPurify.sanitize('', { + ALLOWED_TAGS: ["img"], + ALLOWED_ATTR: ["src"], // kill any attributes malicious url introduced + }) + ); + }, addImgToCanvasByUrl: function (url) { var _this = this; var oldTool = _this.tool; @@ -784,14 +888,14 @@ finalURL = imageURL + url; } + var img = this.imgWithSrc(finalURL).css({ width: "100%", height: "100%" }); + finalURL = img.attr("src"); + _this.setTool("mouse"); //Set to mouse tool while dropping to prevent errors _this.imgDragActive = true; _this.mouseOverlay.css({ cursor: "default" }); var imgDiv = $( '
' + - '' + '
' + ' ' + ' ' + @@ -801,6 +905,7 @@ '
' + "
" ); + imgDiv.prepend(img); imgDiv .find(".xCanvasBtn") .off("click") @@ -860,8 +965,8 @@ ui.position.top += recoupTop; }, stop: function (event, ui) { - left = ui.position.left; - top = ui.position.top; + left = ui.position.left - _this.viewCoords.x; + top = ui.position.top - _this.viewCoords.y; }, }); imgDiv.resizable(); @@ -884,20 +989,17 @@ dom.i2svg(); }, drawImgToBackground(url, width, height, left, top, rotationAngle) { + var _this = this; + const px = (v) => Number(v).toString() + "px"; this.imgContainer.append( - '' + this.imgWithSrc(url).css({ + width: px(width), + height: px(height), + top: px(top + _this.viewCoords.y), + left: px(left + _this.viewCoords.x), + position: "absolute", + transform: "rotate(" + Number(rotationAngle) + "rad)", + }) ); }, addTextBox( @@ -908,14 +1010,18 @@ top, txId, isStickyNote, - newLocalBox + newLocalBox, + remote ) { var _this = this; - console.log(isStickyNote); var cssclass = "textBox"; if (isStickyNote) { cssclass += " stickyNote"; } + + left = left + _this.viewCoords.x; + top = top + _this.viewCoords.y; + let editable = _this.tool == "text" || _this.tool === "stickynote" ? "true" : "false"; var textBox = $( '
' + - '
' + - '
x
' + + '; min-width:50px; min-height:100%;">
' + + '
🗑
' + '
' + "
" ); @@ -975,14 +1083,22 @@ var textBoxPosition = textBox.position(); _this.sendFunction({ t: "setTextboxPosition", - d: [txId, textBoxPosition.top, textBoxPosition.left], + d: [ + txId, + textBoxPosition.top - _this.viewCoords.y, + textBoxPosition.left - _this.viewCoords.x, + ], }); }, drag: function () { var textBoxPosition = textBox.position(); _this.sendFunction({ t: "setTextboxPosition", - d: [txId, textBoxPosition.top, textBoxPosition.left], + d: [ + txId, + textBoxPosition.top - _this.viewCoords.y, + textBoxPosition.left - _this.viewCoords.x, + ], }); }, }); @@ -1021,7 +1137,10 @@ $("#" + txId).remove(); }, setTextboxPosition(txId, top, left) { - $("#" + txId).css({ top: top + "px", left: left + "px" }); + $("#" + txId).css({ + top: top + this.viewCoords.y + "px", + left: left + this.viewCoords.x + "px", + }); }, setTextboxFontSize(txId, fontSize) { $("#" + txId) @@ -1039,15 +1158,30 @@ .css({ "background-color": textboxBackgroundColor }); }, drawImgToCanvas(url, width, height, left, top, rotationAngle, doneCallback) { + top = Number(top); // probably not as important here + left = Number(left); // as it is when generating html + width = Number(width); + height = Number(height); + rotationAngle = Number(rotationAngle); + var _this = this; var img = document.createElement("img"); img.onload = function () { rotationAngle = rotationAngle ? rotationAngle : 0; if (rotationAngle === 0) { - _this.ctx.drawImage(img, left, top, width, height); + _this.ctx.drawImage( + img, + left + _this.viewCoords.x, + top + _this.viewCoords.y, + width, + height + ); } else { _this.ctx.save(); - _this.ctx.translate(left + width / 2, top + height / 2); + _this.ctx.translate( + left + _this.viewCoords.x + width / 2, + top + _this.viewCoords.y + height / 2 + ); _this.ctx.rotate(rotationAngle); _this.ctx.drawImage(img, -(width / 2), -(height / 2), width, height); _this.ctx.restore(); @@ -1056,10 +1190,11 @@ doneCallback(); } }; - img.src = url; + + img.src = this.imgWithSrc(url).attr("src"); // or here - but consistent }, undoWhiteboard: function (username) { - //Not call this directly because you will get out of sync whit others... + //Not call this directly because you will get out of sync whith others... var _this = this; if (!username) { username = _this.settings.username; @@ -1089,7 +1224,7 @@ }); }, redoWhiteboard: function (username) { - //Not call this directly because you will get out of sync whit others... + //Not call this directly because you will get out of sync whith others... var _this = this; if (!username) { username = _this.settings.username; @@ -1130,9 +1265,11 @@ if (this.tool === "text" || this.tool === "stickynote") { $(".textBox").addClass("active"); this.textContainer.appendTo($(whiteboardContainer)); //Bring textContainer to the front + $(".textContent").attr("contenteditable", "true"); } else { $(".textBox").removeClass("active"); this.mouseOverlay.appendTo($(whiteboardContainer)); + $(".textContent").attr("contenteditable", "false"); } this.refreshCursorAppearance(); this.mouseOverlay.find(".xCanvasBtn").click(); @@ -1142,7 +1279,7 @@ var _this = this; _this.drawcolor = color; $("#whiteboardColorpicker").css({ background: color }); - if ((_this.tool == "text" || this.tool === "stickynote") && _this.latestActiveTextBoxId) { + if ((_this.tool == "text" || _this.tool === "stickynote") && _this.latestActiveTextBoxId) { _this.sendFunction({ t: "setTextboxFontColor", d: [_this.latestActiveTextBoxId, color], @@ -1196,20 +1333,28 @@ if (tool === "line" || tool === "pen") { if (data.length == 4) { //Only used for old json imports - _this.drawPenLine(data[0], data[1], data[2], data[3], color, thickness); + _this.drawPenLine(data[0], data[1], data[2], data[3], color, thickness, true); } else { - _this.drawPenSmoothLine(data, color, thickness); + _this.drawPenSmoothLine(data, color, thickness, true); } } else if (tool === "rect") { - _this.drawRec(data[0], data[1], data[2], data[3], color, thickness); + _this.drawRec(data[0], data[1], data[2], data[3], color, thickness, true); } else if (tool === "circle") { - _this.drawCircle(data[0], data[1], data[2], color, thickness); + _this.drawCircle(data[0], data[1], data[2], color, thickness, true); } else if (tool === "eraser") { - _this.drawEraserLine(data[0], data[1], data[2], data[3], thickness); + _this.drawEraserLine(data[0], data[1], data[2], data[3], thickness, true); } else if (tool === "eraseRec") { - _this.eraseRec(data[0], data[1], data[2], data[3]); + _this.eraseRec(data[0], data[1], data[2], data[3], true); } else if (tool === "recSelect") { - _this.dragCanvasRectContent(data[0], data[1], data[2], data[3], data[4], data[5]); + _this.dragCanvasRectContent( + data[0], + data[1], + data[2], + data[3], + data[4], + data[5], + true + ); } else if (tool === "addImgBG") { if (content["draw"] == "1") { _this.drawImgToCanvas( @@ -1232,7 +1377,16 @@ ); } } else if (tool === "addTextBox") { - _this.addTextBox(data[0], data[1], data[2], data[3], data[4], data[5], data[6]); + _this.addTextBox( + data[0], + data[1], + data[2], + data[3], + data[4], + data[5], + data[6], + true + ); } else if (tool === "setTextboxText") { _this.setTextboxText(data[0], data[1]); } else if (tool === "removeTextbox") { @@ -1255,15 +1409,16 @@ } else if (tool === "cursor" && _this.settings) { if (content["event"] === "move") { if (_this.cursorContainer.find("." + content["username"]).length >= 1) { - _this.cursorContainer - .find("." + content["username"]) - .css({ left: data[0] + "px", top: data[1] - 15 + "px" }); + _this.cursorContainer.find("." + content["username"]).css({ + left: data[0] + _this.viewCoords.x + "px", + top: data[1] + _this.viewCoords.y - 15 + "px", + }); } else { _this.cursorContainer.append( '
' + @@ -1432,7 +1587,6 @@ }, loadJsonData(content, doneCallback) { var _this = this; - _this.loadDataInSteps(content, false, function (stepData, index) { _this.sendFunction(stepData); if (index >= content.length - 1) { @@ -1520,4 +1674,31 @@ return [cm1 * xm1 + c0 * x0 + c1 * x1 + c2 * x2, cm1 * ym1 + c0 * y0 + c1 * y1 + c2 * y2]; } +function testImage(url, callback, timeout) { + timeout = timeout || 5000; + var timedOut = false, + timer; + var img = new Image(); + img.onerror = img.onabort = function () { + if (!timedOut) { + clearTimeout(timer); + callback(false); + } + }; + img.onload = function () { + if (!timedOut) { + clearTimeout(timer); + callback(true); + } + }; + img.src = url; + timer = setTimeout(function () { + timedOut = true; + // reset .src to invalid URL so it stops previous + // loading, but doesn't trigger new load + img.src = "//!!!!/test.jpg"; + callback(false); + }, timeout); +} + export default whiteboard;