Index: lams_build/conf/whiteboard/readme.txt =================================================================== diff -u -r67e3d33b7babdc79472bd42680aa616f9fc5f2d8 -r6583579862c8623515c9861dd8edec09b6d1898f --- lams_build/conf/whiteboard/readme.txt (.../readme.txt) (revision 67e3d33b7babdc79472bd42680aa616f9fc5f2d8) +++ lams_build/conf/whiteboard/readme.txt (.../readme.txt) (revision 6583579862c8623515c9861dd8edec09b6d1898f) @@ -11,4 +11,6 @@ 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. \ No newline at end of file + 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.3 In server-backend.js we introduce hashing of wid + accesstoken to improve security \ No newline at end of file Index: lams_build/conf/whiteboard/scripts/server-backend.js =================================================================== diff -u --- lams_build/conf/whiteboard/scripts/server-backend.js (revision 0) +++ lams_build/conf/whiteboard/scripts/server-backend.js (revision 6583579862c8623515c9861dd8edec09b6d1898f) @@ -0,0 +1,403 @@ +const path = require("path"); + +const config = require("./config/config"); +const ReadOnlyBackendService = require("./services/ReadOnlyBackendService"); +const WhiteboardInfoBackendService = require("./services/WhiteboardInfoBackendService"); + +function startBackendServer(port) { + var fs = require("fs-extra"); + var express = require("express"); + var formidable = require("formidable"); //form upload processing + + const createDOMPurify = require("dompurify"); //Prevent xss + const { JSDOM } = require("jsdom"); + const window = new JSDOM("").window; + const DOMPurify = createDOMPurify(window); + + const { createClient } = require("webdav"); + + var s_whiteboard = require("./s_whiteboard.js"); + + var app = express(); + app.use(express.static(path.join(__dirname, "..", "dist"))); + app.use("/uploads", express.static(path.join(__dirname, "..", "public", "uploads"))); + var server = require("http").Server(app); + server.listen(port); + var io = require("socket.io")(server, { path: "/ws-api" }); + WhiteboardInfoBackendService.start(io); + + console.log("Webserver & socketserver running on port:" + port); + + const { accessToken, enableWebdav } = config.backend; + + // LAMS introduced this function to enforce security + // it mimics WhiteboardService#getWhiteboardAccessTokenHash() + const hashAccessToken = function (wid){ + if (!wid) { + return null; + } + const plainText = wid + accessToken; + let hash = 0; + for (var i = 0; i < plainText.length; i++){ + hash = Math.imul(31, hash) + plainText.charCodeAt(i) | 0; + } + return "" + hash; + } + + /** + * @api {get} /api/loadwhiteboard Get Whiteboard Data + * @apiDescription This returns all the Available Data ever drawn to this Whiteboard + * @apiName loadwhiteboard + * @apiGroup WhiteboardAPI + * + * @apiParam {Number} wid WhiteboardId you find in the Whiteboard URL + * @apiParam {Number} [at] Accesstoken (Only if activated for this server) + * + * @apiSuccess {String} body returns the data as JSON String + * @apiError {Number} 401 Unauthorized + * + * @apiExample {curl} Example usage: + * 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"]; + + // 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; + const ret = s_whiteboard.loadStoredData(widForData); + res.send(ret); + res.end(); + } else { + res.status(401); //Unauthorized + res.end(); + } + }); + + /** + * @api {get} /api/getReadOnlyWid Get the readOnlyWhiteboardId + * @apiDescription This returns the readOnlyWhiteboardId for a given WhiteboardId + * @apiName getReadOnlyWid + * @apiGroup WhiteboardAPI + * + * @apiParam {Number} wid WhiteboardId you find in the Whiteboard URL + * @apiParam {Number} [at] Accesstoken (Only if activated for this server) + * + * @apiSuccess {String} body returns the readOnlyWhiteboardId as text + * @apiError {Number} 401 Unauthorized + * + * @apiExample {curl} Example usage: + * 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 + if (accessToken === "" || hashAccessToken(wid) == at) { + res.send(ReadOnlyBackendService.getReadOnlyId(wid)); + res.end(); + } else { + res.status(401); //Unauthorized + res.end(); + } + }); + + /** + * @api {post} /api/upload Upload Images + * @apiDescription Upload Image to the server. Note that you need to add the image to the board after upload by calling "drawToWhiteboard" with addImgBG set as tool + * @apiName upload + * @apiGroup WhiteboardAPI + * + * @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 {String} imagedata The imagedata base64 encoded + * + * @apiSuccess {String} body returns "done" + * @apiError {Number} 401 Unauthorized + */ + app.post("/api/upload", function (req, res) { + //File upload + var form = new formidable.IncomingForm(); //Receive form + var formData = { + files: {}, + fields: {}, + }; + + form.on("file", function (name, file) { + formData["files"][file.name] = file; + }); + + form.on("field", function (name, value) { + formData["fields"][name] = value; + }); + + form.on("error", function (err) { + console.log("File uplaod Error!"); + }); + + form.on("end", function () { + if (accessToken === "" || hashAccessToken(formData["fields"]["wid"]) == formData["fields"]["at"]) { + progressUploadFormData(formData, function (err) { + if (err) { + if (err == "403") { + res.status(403); + } else { + res.status(500); + } + res.end(); + } else { + res.send("done"); + } + }); + } else { + res.status(401); //Unauthorized + res.end(); + } + //End file upload + }); + form.parse(req); + }); + + /** + * @api {get} /api/drawToWhiteboard Draw on the Whiteboard + * @apiDescription Function draw on whiteboard with different tools and more... + * @apiName drawToWhiteboard + * @apiGroup WhiteboardAPI + * + * @apiParam {Number} wid WhiteboardId you find in the Whiteboard URL + * @apiParam {Number} [at] Accesstoken (Only if activated for this server) + * @apiParam {String} t The tool you want to use: "line", + * "pen", + * "rect", + * "circle", + * "eraser", + * "addImgBG", + * "recSelect", + * "eraseRec", + * "addTextBox", + * "setTextboxText", + * "removeTextbox", + * "setTextboxPosition", + * "setTextboxFontSize", + * "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] + * + * @apiSuccess {String} body returns the "done" as text + * @apiError {Number} 401 Unauthorized + */ + app.get("/api/drawToWhiteboard", function (req, res) { + let query = escapeAllContentStrings(req["query"]); + const wid = query["wid"]; + const at = query["at"]; //accesstoken + if (!wid || ReadOnlyBackendService.isReadOnly(wid)) { + res.status(401); //Unauthorized + res.end(); + } + + if (accessToken === "" || hashAccessToken(wid) == at) { + const broadcastTo = (wid) => io.compress(false).to(wid).emit("drawToWhiteboard", query); + // broadcast to current whiteboard + broadcastTo(wid); + // broadcast the same query to the associated read-only whiteboard + const readOnlyId = ReadOnlyBackendService.getReadOnlyId(wid); + broadcastTo(readOnlyId); + s_whiteboard.handleEventsAndData(query); //save whiteboardchanges on the server + res.send("done"); + } else { + res.status(401); //Unauthorized + res.end(); + } + }); + + function progressUploadFormData(formData, callback) { + console.log("Progress new Form Data"); + const fields = escapeAllContentStrings(formData.fields); + const wid = fields["whiteboardId"]; + if (ReadOnlyBackendService.isReadOnly(wid)) return; + + const readOnlyWid = ReadOnlyBackendService.getReadOnlyId(wid); + + const date = fields["date"] || +new Date(); + const filename = `${readOnlyWid}_${date}.png`; + let webdavaccess = fields["webdavaccess"] || false; + try { + webdavaccess = JSON.parse(webdavaccess); + } catch (e) { + webdavaccess = false; + } + + const savingDir = path.join("./public/uploads", readOnlyWid); + fs.ensureDir(savingDir, function (err) { + if (err) { + console.log("Could not create upload folder!", err); + return; + } + let imagedata = fields["imagedata"]; + if (imagedata && imagedata != "") { + //Save from base64 data + imagedata = imagedata + .replace(/^data:image\/png;base64,/, "") + .replace(/^data:image\/jpeg;base64,/, ""); + console.log(filename, "uploaded"); + const savingPath = path.join(savingDir, filename); + fs.writeFile(savingPath, imagedata, "base64", function (err) { + if (err) { + console.log("error", err); + callback(err); + } else { + if (webdavaccess) { + //Save image to webdav + if (enableWebdav) { + saveImageToWebdav( + savingPath, + filename, + webdavaccess, + function (err) { + if (err) { + console.log("error", err); + callback(err); + } else { + callback(); + } + } + ); + } else { + callback("Webdav is not enabled on the server!"); + } + } else { + callback(); + } + } + }); + } else { + callback("no imagedata!"); + console.log("No image Data found for this upload!", filename); + } + }); + } + + function saveImageToWebdav(imagepath, filename, webdavaccess, callback) { + if (webdavaccess) { + const webdavserver = webdavaccess["webdavserver"] || ""; + const webdavpath = webdavaccess["webdavpath"] || "/"; + const webdavusername = webdavaccess["webdavusername"] || ""; + const webdavpassword = webdavaccess["webdavpassword"] || ""; + + const client = createClient(webdavserver, { + username: webdavusername, + password: webdavpassword, + }); + client + .getDirectoryContents(webdavpath) + .then((items) => { + const cloudpath = webdavpath + "" + filename; + console.log("webdav saving to:", cloudpath); + fs.createReadStream(imagepath).pipe(client.createWriteStream(cloudpath)); + callback(); + }) + .catch((error) => { + callback("403"); + console.log("Could not connect to webdav!"); + }); + } else { + callback("Error: no access data!"); + } + } + + io.on("connection", function (socket) { + let whiteboardId = null; + socket.on("disconnect", function () { + WhiteboardInfoBackendService.leave(socket.id, whiteboardId); + socket.compress(false).broadcast.to(whiteboardId).emit("refreshUserBadges", null); //Removes old user Badges + }); + + socket.on("drawToWhiteboard", function (content) { + if (!whiteboardId || ReadOnlyBackendService.isReadOnly(whiteboardId)) return; + + content = escapeAllContentStrings(content); + if (accessToken === "" || hashAccessToken(content["wid"]) == content["at"]) { + const broadcastTo = (wid) => + socket.compress(false).broadcast.to(wid).emit("drawToWhiteboard", content); + // broadcast to current whiteboard + broadcastTo(whiteboardId); + // broadcast the same content to the associated read-only whiteboard + const readOnlyId = ReadOnlyBackendService.getReadOnlyId(whiteboardId); + broadcastTo(readOnlyId); + s_whiteboard.handleEventsAndData(content); //save whiteboardchanges on the server + } else { + socket.emit("wrongAccessToken", true); + } + }); + + socket.on("joinWhiteboard", function (content) { + content = escapeAllContentStrings(content); + if (accessToken === "" || hashAccessToken(content["wid"]) == content["at"]) { + whiteboardId = content["wid"]; + + socket.emit("whiteboardConfig", { + common: config.frontend, + whiteboardSpecific: { + correspondingReadOnlyWid: ReadOnlyBackendService.getReadOnlyId( + whiteboardId + ), + isReadOnly: ReadOnlyBackendService.isReadOnly(whiteboardId), + }, + }); + + socket.join(whiteboardId); //Joins room name=wid + const screenResolution = content["windowWidthHeight"]; + WhiteboardInfoBackendService.join(socket.id, whiteboardId, screenResolution); + } else { + socket.emit("wrongAccessToken", true); + } + }); + + socket.on("updateScreenResolution", function (content) { + content = escapeAllContentStrings(content); + if (accessToken === "" || hashAccessToken(content["wid"]) == content["at"]) { + const screenResolution = content["windowWidthHeight"]; + WhiteboardInfoBackendService.setScreenResolution( + socket.id, + whiteboardId, + screenResolution + ); + } + }); + }); + + //Prevent cross site scripting (xss) + function escapeAllContentStrings(content, cnt) { + if (!cnt) cnt = 0; + + if (typeof content === "string") { + return DOMPurify.sanitize(content); + } + for (var i in content) { + if (typeof content[i] === "string") { + content[i] = DOMPurify.sanitize(content[i]); + } + if (typeof content[i] === "object" && cnt < 10) { + content[i] = escapeAllContentStrings(content[i], ++cnt); + } + } + return content; + } + + process.on("unhandledRejection", (error) => { + // Will print "unhandledRejection err is not defined" + console.log("unhandledRejection", error.message); + }); +} + +module.exports = startBackendServer; Index: lams_build/conf/whiteboard/src/js/main.js =================================================================== diff -u -r709049f023d1033d4ef6ec198b08c4e2f94e421a -r6583579862c8623515c9861dd8edec09b6d1898f --- lams_build/conf/whiteboard/src/js/main.js (.../main.js) (revision 709049f023d1033d4ef6ec198b08c4e2f94e421a) +++ lams_build/conf/whiteboard/src/js/main.js (.../main.js) (revision 6583579862c8623515c9861dd8edec09b6d1898f) @@ -37,6 +37,7 @@ const myUsername = urlParams.get("username") || "unknown" + (Math.random() + "").substring(2, 6); const accessToken = urlParams.get("accesstoken") || ""; const copyfromwid = urlParams.get("copyfromwid") || ""; +const copyaccesstoken = urlParams.get("copyaccesstoken") || ""; // Custom Html Title const title = urlParams.get("title"); @@ -176,7 +177,10 @@ // Copy from witheboard if current is empty and get parameter is given $.get(subdir + "/api/loadwhiteboard", { wid: copyfromwid, - at: accessToken, + // needed for checking hash + targetWid: whiteboardId, + // this is not the main access token, but a special one just for this operation + at: copyaccesstoken, }).done(function (originalData) { console.log(originalData); console.log(data); Index: lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/model/Whiteboard.java =================================================================== diff -u -rb5f4a44b664ee18ed97ef77d512c6480eb500826 -r6583579862c8623515c9861dd8edec09b6d1898f --- lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/model/Whiteboard.java (.../Whiteboard.java) (revision b5f4a44b664ee18ed97ef77d512c6480eb500826) +++ lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/model/Whiteboard.java (.../Whiteboard.java) (revision 6583579862c8623515c9861dd8edec09b6d1898f) @@ -134,8 +134,6 @@ Whiteboard toContent = new Whiteboard(); toContent = (Whiteboard) defaultContent.clone(); toContent.setContentId(contentId); - // copy whiteboard canvas on next open - toContent.setSourceWid(defaultContent.getContentId().toString()); return toContent; } Index: lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/service/IWhiteboardService.java =================================================================== diff -u -rb5f4a44b664ee18ed97ef77d512c6480eb500826 -r6583579862c8623515c9861dd8edec09b6d1898f --- lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/service/IWhiteboardService.java (.../IWhiteboardService.java) (revision b5f4a44b664ee18ed97ef77d512c6480eb500826) +++ lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/service/IWhiteboardService.java (.../IWhiteboardService.java) (revision 6583579862c8623515c9861dd8edec09b6d1898f) @@ -132,4 +132,6 @@ void saveOrUpdateWhiteboardConfigItem(WhiteboardConfigItem item); String getWhiteboardServerUrl() throws WhiteboardApplicationException; + + String getWhiteboardAccessTokenHash(String wid, String sourceWid); } \ No newline at end of file Index: lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/service/WhiteboardService.java =================================================================== diff -u -rb5f4a44b664ee18ed97ef77d512c6480eb500826 -r6583579862c8623515c9861dd8edec09b6d1898f --- lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/service/WhiteboardService.java (.../WhiteboardService.java) (revision b5f4a44b664ee18ed97ef77d512c6480eb500826) +++ lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/service/WhiteboardService.java (.../WhiteboardService.java) (revision 6583579862c8623515c9861dd8edec09b6d1898f) @@ -464,6 +464,22 @@ return whiteboardServerUrl; } + @Override + public String getWhiteboardAccessTokenHash(String wid, String sourceWid) { + if (StringUtils.isBlank(wid)) { + return null; + } + WhiteboardConfigItem whiteboardAccessTokenConfigItem = getConfigItem(WhiteboardConfigItem.KEY_ACCESS_TOKEN); + if (whiteboardAccessTokenConfigItem == null + || StringUtils.isBlank(whiteboardAccessTokenConfigItem.getConfigValue())) { + return null; + } + // sourceWid is present when we want to copy content from other canvas + String plainText = (StringUtils.isBlank(sourceWid) ? "" : sourceWid) + wid + + whiteboardAccessTokenConfigItem.getConfigValue(); + return String.valueOf(plainText.hashCode()); + } + public static String getWhiteboardAuthorName(UserDTO user) throws UnsupportedEncodingException { if (user == null) { return null; @@ -610,6 +626,8 @@ } Whiteboard toContent = Whiteboard.newInstance(whiteboard, toContentId); + // copy whiteboard canvas on next open + toContent.setSourceWid(whiteboard.getContentId().toString()); whiteboardDao.insert(toContent); if (toContent.isGalleryWalkEnabled() && !toContent.isGalleryWalkReadOnly()) { Index: lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/web/controller/AuthoringController.java =================================================================== diff -u -rb5f4a44b664ee18ed97ef77d512c6480eb500826 -r6583579862c8623515c9861dd8edec09b6d1898f --- lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/web/controller/AuthoringController.java (.../AuthoringController.java) (revision b5f4a44b664ee18ed97ef77d512c6480eb500826) +++ lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/web/controller/AuthoringController.java (.../AuthoringController.java) (revision 6583579862c8623515c9861dd8edec09b6d1898f) @@ -36,7 +36,6 @@ import org.lamsfoundation.lams.tool.ToolAccessMode; import org.lamsfoundation.lams.tool.whiteboard.WhiteboardConstants; import org.lamsfoundation.lams.tool.whiteboard.model.Whiteboard; -import org.lamsfoundation.lams.tool.whiteboard.model.WhiteboardConfigItem; import org.lamsfoundation.lams.tool.whiteboard.model.WhiteboardUser; import org.lamsfoundation.lams.tool.whiteboard.service.IWhiteboardService; import org.lamsfoundation.lams.tool.whiteboard.service.WhiteboardApplicationException; @@ -166,11 +165,14 @@ String whiteboardServerUrl = whiteboardService.getWhiteboardServerUrl(); request.setAttribute("whiteboardServerUrl", whiteboardServerUrl); - WhiteboardConfigItem whiteboardAccessTokenConfigItem = whiteboardService - .getConfigItem(WhiteboardConfigItem.KEY_ACCESS_TOKEN); - if (whiteboardAccessTokenConfigItem != null - && StringUtils.isNotBlank(whiteboardAccessTokenConfigItem.getConfigValue())) { - request.setAttribute("whiteboardAccessToken", whiteboardAccessTokenConfigItem.getConfigValue()); + String wid = authoringForm.getWhiteboard().getContentId().toString(); + String whiteboardAccessTokenHash = whiteboardService.getWhiteboardAccessTokenHash(wid, null); + request.setAttribute("whiteboardAccessToken", whiteboardAccessTokenHash); + + if (StringUtils.isNotBlank(authoringForm.getWhiteboard().getSourceWid())) { + String whiteboardCopyAccessTokenHash = whiteboardService.getWhiteboardAccessTokenHash(wid, + authoringForm.getWhiteboard().getSourceWid()); + request.setAttribute("whiteboardCopyAccessToken", whiteboardCopyAccessTokenHash); } return "pages/authoring/authoring"; @@ -207,17 +209,17 @@ // get back UID whiteboardPO.setUid(uid); - // if whiteboard canvas was copied from another Learning Design, - // not it becomes the source, i.e. does not copy from anything anymore - whiteboardPO.setSourceWid(null); - // if it's a teacher - change define later status if (mode.isTeacher()) { whiteboardPO.setDefineLater(false); } whiteboardPO.setUpdated(new Timestamp(new Date().getTime())); } + // if whiteboard canvas was copied from another Learning Design, + // not it becomes the source, i.e. does not copy from anything anymore + whiteboardPO.setSourceWid(null); + // *******************************Handle user******************* // try to get form system session HttpSession ss = SessionManager.getSession(); Index: lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/web/controller/LearningController.java =================================================================== diff -u -rb5f4a44b664ee18ed97ef77d512c6480eb500826 -r6583579862c8623515c9861dd8edec09b6d1898f --- lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/web/controller/LearningController.java (.../LearningController.java) (revision b5f4a44b664ee18ed97ef77d512c6480eb500826) +++ lams_tool_whiteboard/src/java/org/lamsfoundation/lams/tool/whiteboard/web/controller/LearningController.java (.../LearningController.java) (revision 6583579862c8623515c9861dd8edec09b6d1898f) @@ -35,6 +35,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.lamsfoundation.lams.notebook.model.NotebookEntry; import org.lamsfoundation.lams.notebook.service.CoreNotebookConstants; @@ -215,9 +216,19 @@ String whiteboardServerUrl = whiteboardService.getWhiteboardServerUrl(); request.setAttribute("whiteboardServerUrl", whiteboardServerUrl); - String authorName = WhiteboardService.getWhiteboardAuthorName(currentUserDto); - request.setAttribute("whiteboardAuthorName", authorName); + String whiteboardAuthorName = WhiteboardService.getWhiteboardAuthorName(currentUserDto); + request.setAttribute("whiteboardAuthorName", whiteboardAuthorName); + String wid = whiteboard.getContentId() + "-" + toolSessionId; + String whiteboardAccessTokenHash = whiteboardService.getWhiteboardAccessTokenHash(wid, null); + request.setAttribute("whiteboardAccessToken", whiteboardAccessTokenHash); + + if (StringUtils.isNotBlank(whiteboard.getSourceWid())) { + String whiteboardCopyAccessTokenHash = whiteboardService.getWhiteboardAccessTokenHash(wid, + whiteboard.getSourceWid()); + request.setAttribute("whiteboardCopyAccessToken", whiteboardCopyAccessTokenHash); + } + return "pages/learning/learning"; } Index: lams_tool_whiteboard/web/pages/authoring/basic.jsp =================================================================== diff -u -rb5f4a44b664ee18ed97ef77d512c6480eb500826 -r6583579862c8623515c9861dd8edec09b6d1898f --- lams_tool_whiteboard/web/pages/authoring/basic.jsp (.../basic.jsp) (revision b5f4a44b664ee18ed97ef77d512c6480eb500826) +++ lams_tool_whiteboard/web/pages/authoring/basic.jsp (.../basic.jsp) (revision 6583579862c8623515c9861dd8edec09b6d1898f) @@ -17,5 +17,5 @@ \ No newline at end of file Index: lams_tool_whiteboard/web/pages/learning/learning.jsp =================================================================== diff -u -rb5f4a44b664ee18ed97ef77d512c6480eb500826 -r6583579862c8623515c9861dd8edec09b6d1898f --- lams_tool_whiteboard/web/pages/learning/learning.jsp (.../learning.jsp) (revision b5f4a44b664ee18ed97ef77d512c6480eb500826) +++ lams_tool_whiteboard/web/pages/learning/learning.jsp (.../learning.jsp) (revision 6583579862c8623515c9861dd8edec09b6d1898f) @@ -216,7 +216,7 @@