Index: lams_central/src/java/org/lamsfoundation/lams/web/EtherpadController.java =================================================================== diff -u --- lams_central/src/java/org/lamsfoundation/lams/web/EtherpadController.java (revision 0) +++ lams_central/src/java/org/lamsfoundation/lams/web/EtherpadController.java (revision 024f71c24280149ce16489cd0bca12fa127b17c9) @@ -0,0 +1,67 @@ +package org.lamsfoundation.lams.web; + +import java.util.Map; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.commons.lang.StringUtils; +import org.lamsfoundation.lams.etherpad.EtherpadException; +import org.lamsfoundation.lams.etherpad.service.IEtherpadService; +import org.lamsfoundation.lams.etherpad.util.EtherpadUtil; +import org.lamsfoundation.lams.usermanagement.dto.UserDTO; +import org.lamsfoundation.lams.web.session.SessionManager; +import org.lamsfoundation.lams.web.util.AttributeNames; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@RequestMapping("/etherpad") +public class EtherpadController { + + @Autowired + IEtherpadService etherpadService; + + /** + * Creates or fetches an existing Etherpad pad using API client, sets a cookie. + * + * @param groupId + * LAMS-specific group ID (just any identifier); do not confuse with Etherpad groupId, readOnlyId or + * padId + * @param content + * initial content of Etherpad; ignored if null or Etherpad already exists + * @return etherpad pad ID understood by etherpad API + */ + @RequestMapping(path = "/getPad", produces = "application/json;charset=utf-8") + @ResponseBody + private String getPad(@RequestParam String groupId, @RequestParam(required = false) String content, + HttpServletResponse response) throws EtherpadException { + String preparedContent = EtherpadUtil.preparePadContent(content); + // try to create an Etherpad pad + Map padData = etherpadService.createPad(groupId, preparedContent); + + String etherpadGroupId = (String) padData.get("groupId"); + String etherpadReadOnlyId = (String) padData.get("readOnlyId"); + //don't allow sessions that has had problems with pad initialisation + if (StringUtils.isEmpty(etherpadGroupId) || StringUtils.isEmpty(etherpadReadOnlyId)) { + throw new EtherpadException( + "Etherpad has had problems with initialization. Please seek help from your teacher."); + } + + HttpSession ss = SessionManager.getSession(); + UserDTO user = (UserDTO) ss.getAttribute(AttributeNames.USER); + + String userName = user.getFirstName() + " " + user.getLastName(); + String authorId = etherpadService.createAuthor(user.getUserID(), userName); + + // create cookie with ALL valid user session IDs so there can be multiple pads on a single page + Cookie cookie = etherpadService.createCookie(authorId, etherpadGroupId, true); + response.addCookie(cookie); + + return (String) padData.get("padId"); + } +} \ No newline at end of file Index: lams_common/src/java/org/lamsfoundation/lams/etherpad/service/EtherpadService.java =================================================================== diff -u -rb6d0779192887979b5d3d244dec3506d12a7e401 -r024f71c24280149ce16489cd0bca12fa127b17c9 --- lams_common/src/java/org/lamsfoundation/lams/etherpad/service/EtherpadService.java (.../EtherpadService.java) (revision b6d0779192887979b5d3d244dec3506d12a7e401) +++ lams_common/src/java/org/lamsfoundation/lams/etherpad/service/EtherpadService.java (.../EtherpadService.java) (revision 024f71c24280149ce16489cd0bca12fa127b17c9) @@ -3,9 +3,12 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.servlet.http.Cookie; @@ -24,7 +27,7 @@ @SuppressWarnings("unchecked") @Override - public Map createPad(String groupIdentifier) throws EtherpadException { + public Map createPad(String groupIdentifier, String content) throws EtherpadException { EPLiteClient client = getClient(); Map result = new HashMap<>(); @@ -52,6 +55,10 @@ result.put("isPadAlreadyCreated", isPadAlreadyCreated); + if (!isPadAlreadyCreated && content != null) { + client.setHTML(padId, content); + } + // gets read-only id String etherpadReadOnlyId = (String) client.getReadOnlyID(padId).get("readOnlyID"); result.put("readOnlyId", etherpadReadOnlyId); @@ -103,22 +110,44 @@ return etherpadSessionCookie; } + @Override + @SuppressWarnings("unchecked") + public Cookie createCookie(String authorId, String etherpadGroupId, boolean includeAllGroups) + throws EtherpadException { + // search for already existing user's session at Etherpad server + Map etherpadSessions = getClient().listSessionsOfAuthor(authorId); + String etherpadSessionId = getExistingSessionID(authorId, etherpadGroupId, etherpadSessions, includeAllGroups); + return createCookie(etherpadSessionId); + } + /** * Returns valid Etherpad session. Returns existing one if finds such one and creates the new one otherwise + * + * @throws EtherpadException */ @Override - @SuppressWarnings("unchecked") public String getExistingSessionID(String authorId, String etherpadGroupId, Map etherpadSessions) throws EtherpadException { - String etherpadSessionId = null; + return getExistingSessionID(authorId, etherpadGroupId, etherpadSessions, false); + } + /** + * Returns valid Etherpad session. Returns existing one if finds such one and creates the new one otherwise + */ + @Override + @SuppressWarnings("unchecked") + public String getExistingSessionID(String authorId, String etherpadGroupId, Map etherpadSessions, + boolean includeAllGroups) throws EtherpadException { // search for already existing user's session boolean isValidForMoreThan1Hour = false; + String etherpadSessionId = null; + Set otherEtherpadSessionIds = new HashSet<>(); for (String etherpadSessionIdIter : etherpadSessions.keySet()) { Map sessessionAttributes = (Map) etherpadSessions .get(etherpadSessionIdIter); String groupIdIter = (String) sessessionAttributes.get("groupID"); - if (groupIdIter.equals(etherpadGroupId)) { + boolean isTargetGroup = groupIdIter.equals(etherpadGroupId); + if (includeAllGroups || isTargetGroup) { // check session expiration date long validUntil = (Long) sessessionAttributes.get("validUntil") * 1000; @@ -127,27 +156,29 @@ //use existing session if it's valid for more than 1 hour if (isValidForMoreThan1Hour) { - etherpadSessionId = etherpadSessionIdIter; - break; - + otherEtherpadSessionIds.add(etherpadSessionIdIter); + if (isTargetGroup) { + etherpadSessionId = etherpadSessionIdIter; + } } else { // can't delete expired sessions as Etherpad throws an exception. Nonetheless it returns expired // ones when client.listSessionsOfAuthor(authorId) is requested } } - } // if session with validity of more than 1 hour doesn't exist yet - create it if (etherpadSessionId == null) { EPLiteClient client = getClient(); Map map2 = client.createSession(etherpadGroupId, authorId, 24); etherpadSessionId = (String) map2.get("sessionID"); + otherEtherpadSessionIds.add(etherpadSessionId); } - return etherpadSessionId; + return includeAllGroups ? otherEtherpadSessionIds.stream().collect(Collectors.joining(",")) : etherpadSessionId; } + @Override @SuppressWarnings("unchecked") public String createAuthor(Integer userId, String userName) throws EtherpadException { EPLiteClient client = getClient(); Index: lams_common/src/java/org/lamsfoundation/lams/etherpad/service/IEtherpadService.java =================================================================== diff -u -rb6d0779192887979b5d3d244dec3506d12a7e401 -r024f71c24280149ce16489cd0bca12fa127b17c9 --- lams_common/src/java/org/lamsfoundation/lams/etherpad/service/IEtherpadService.java (.../IEtherpadService.java) (revision b6d0779192887979b5d3d244dec3506d12a7e401) +++ lams_common/src/java/org/lamsfoundation/lams/etherpad/service/IEtherpadService.java (.../IEtherpadService.java) (revision 024f71c24280149ce16489cd0bca12fa127b17c9) @@ -15,25 +15,45 @@ String PREFIX_REGULAR_GROUP = "LAMS-group-"; /** - * Creates EPLiteClient that will make calls to Etherpad server. Throws DokumaranConfigurationException + * Creates EPLiteClient that will make calls to Etherpad server * - * @throws DokumaranConfigurationException - * if the etherpad is not configured appropriately (either server URL or API key). + * @throws EtherpadException + * if the Etherpad is not configured appropriately (either server URL or API key). */ EPLiteClient getClient() throws EtherpadException; - Map createPad(String groupIdentifier) throws EtherpadException; + /** + * Using API client creates/fetches Etherpad Pad. + * + * @param groupIdentifier + * LAMS-specific group ID (just any identifier); do not confuse with Etherpad groupId, readOnlyId or + * padId + * @param content + * initial content of Etherpad; ignored if null or Etherpad already exists + */ + Map createPad(String groupIdentifier, String content) throws EtherpadException; /** - * Constructs cookie to be stored at a clientside browser. + * Constructs cookie to be stored at a client side browser. */ Cookie createCookie(String etherpadSessionIds) throws EtherpadException; /** + * Constructs cookie to be stored at a client side browser. + */ + Cookie createCookie(String authorId, String etherpadGroupId, boolean includeAllGroup) throws EtherpadException; + + /** * Returns valid Etherpad session. Returns existing one if finds such one and creates the new one otherwise */ String getExistingSessionID(String authorId, String etherpadGroupId, Map etherpadSessions) throws EtherpadException; + /** + * Returns valid Etherpad session. Returns existing one if finds such one and creates the new one otherwise + */ + String getExistingSessionID(String authorId, String etherpadGroupId, Map etherpadSessions, + boolean includeAllGroups) throws EtherpadException; + String createAuthor(Integer userId, String userName) throws EtherpadException; } \ No newline at end of file Index: lams_common/src/java/org/lamsfoundation/lams/etherpad/util/EtherpadUtil.java =================================================================== diff -u -rb6d0779192887979b5d3d244dec3506d12a7e401 -r024f71c24280149ce16489cd0bca12fa127b17c9 --- lams_common/src/java/org/lamsfoundation/lams/etherpad/util/EtherpadUtil.java (.../EtherpadUtil.java) (revision b6d0779192887979b5d3d244dec3506d12a7e401) +++ lams_common/src/java/org/lamsfoundation/lams/etherpad/util/EtherpadUtil.java (.../EtherpadUtil.java) (revision 024f71c24280149ce16489cd0bca12fa127b17c9) @@ -4,7 +4,19 @@ public class EtherpadUtil { + /** + * Produces a standard, pretty meaningless Etherpad ID. + */ public static String getPadId(String groupId) { return groupId + "$" + IEtherpadService.DEFAULT_PAD_NAME; } + + /** + * If Etherpad intial content is not well formed, Etherpad server throws an exception. + */ + public static String preparePadContent(String rawContent) { + return rawContent == null ? null + : "" + rawContent.trim().replaceAll("[\n\r\f]", "").replaceAll(" ", "") + + ""; + } } Index: lams_tool_doku/conf/etherpad-lite/src/node/db/SecurityManager.js =================================================================== diff -u --- lams_tool_doku/conf/etherpad-lite/src/node/db/SecurityManager.js (revision 0) +++ lams_tool_doku/conf/etherpad-lite/src/node/db/SecurityManager.js (revision 024f71c24280149ce16489cd0bca12fa127b17c9) @@ -0,0 +1,261 @@ +/** + * Controls the security of pad access + */ + +/* + * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var authorManager = require("./AuthorManager"); +var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); +var padManager = require("./PadManager"); +var sessionManager = require("./SessionManager"); +var settings = require("../utils/Settings"); +var log4js = require('log4js'); +var authLogger = log4js.getLogger("auth"); + +/** + * This function controlls the access to a pad, it checks if the user can access a pad. + * @param padID the pad the user wants to access + * @param sessionCookie the session the user has (set via api) + * @param token the token of the author (randomly generated at client side, used for public pads) + * @param password the password the user has given to access this pad, can be null + * @return {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx}) + */ +exports.checkAccess = async function(padID, sessionCookie, token, password) +{ + // immutable object + let deny = Object.freeze({ accessStatus: "deny" }); + + if (!padID) { + return deny; + } + + // allow plugins to deny access + var deniedByHook = hooks.callAll("onAccessCheck", {'padID': padID, 'password': password, 'token': token, 'sessionCookie': sessionCookie}).indexOf(false) > -1; + if (deniedByHook) { + return deny; + } + + // start to get author for this token + let p_tokenAuthor = authorManager.getAuthor4Token(token); + + // start to check if pad exists + let p_padExists = padManager.doesPadExist(padID); + + if (settings.requireSession) { + // a valid session is required (api-only mode) + if (!sessionCookie) { + // without sessionCookie, access is denied + return deny; + } + } else { + // a session is not required, so we'll check if it's a public pad + if (padID.indexOf("$") === -1) { + // it's not a group pad, means we can grant access + + // assume user has access + let authorID = await p_tokenAuthor; + let statusObject = { accessStatus: "grant", authorID }; + + if (settings.editOnly) { + // user can't create pads + + let padExists = await p_padExists; + + if (!padExists) { + // pad doesn't exist - user can't have access + statusObject.accessStatus = "deny"; + } + } + + // user may create new pads - no need to check anything + // grant access, with author of token + return statusObject; + } + } + + let validSession = false; + let sessionAuthor; + let isPublic; + let isPasswordProtected; + let passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong + + // get information about all sessions contained in this cookie + if (sessionCookie) { + let groupID = padID.split("$")[0]; + let sessionIDs = sessionCookie.replace(/"/g, '').split(','); + + // was previously iterated in parallel using async.forEach + let sessionInfos = await Promise.all(sessionIDs.map(sessionID => { + return sessionManager.getSessionInfo(sessionID); + })); + + // seperated out the iteration of sessioninfos from the (parallel) fetches from the DB + for (let sessionInfo of sessionInfos) { + try { + // is it for this group? + if (sessionInfo.groupID != groupID) { + authLogger.debug("Auth failed: wrong group"); + continue; + } + + // is validUntil still ok? + let now = Math.floor(Date.now() / 1000); + if (sessionInfo.validUntil <= now) { + authLogger.debug("Auth failed: validUntil"); + continue; + } + + // fall-through - there is a valid session + validSession = true; + sessionAuthor = sessionInfo.authorID; + break; + } catch (err) { + // skip session if it doesn't exist + if (err.message == "sessionID does not exist") { + authLogger.debug("Auth failed: unknown session"); + } else { + throw err; + } + } + } + } + + let padExists = await p_padExists; + + if (padExists) { + let pad = await padManager.getPad(padID); + + // is it a public pad? + isPublic = pad.getPublicStatus(); + + // is it password protected? + isPasswordProtected = pad.isPasswordProtected(); + + // is password correct? + if (isPasswordProtected && password && pad.isCorrectPassword(password)) { + passwordStatus = "correct"; + } + } + + // - a valid session for this group is avaible AND pad exists + if (validSession && padExists) { + let authorID = sessionAuthor; + let grant = Object.freeze({ accessStatus: "grant", authorID }); + + if (!isPasswordProtected) { + // - the pad is not password protected + + // --> grant access + return grant; + } + + if (settings.sessionNoPassword) { + // - the setting to bypass password validation is set + + // --> grant access + return grant; + } + + if (isPasswordProtected && passwordStatus === "correct") { + // - the pad is password protected and password is correct + + // --> grant access + return grant; + } + + if (isPasswordProtected && passwordStatus === "wrong") { + // - the pad is password protected but wrong password given + + // --> deny access, ask for new password and tell them that the password is wrong + return { accessStatus: "wrongPassword" }; + } + + if (isPasswordProtected && passwordStatus === "notGiven") { + // - the pad is password protected but no password given + + // --> ask for password + return { accessStatus: "needPassword" }; + } + + throw new Error("Oops, something wrong happend"); + } + + if (validSession && !padExists) { + // - a valid session for this group avaible but pad doesn't exist + + // --> grant access by default + let accessStatus = "grant"; + let authorID = sessionAuthor; + + // --> deny access if user isn't allowed to create the pad + if (settings.editOnly) { + authLogger.debug("Auth failed: valid session & pad does not exist"); + accessStatus = "deny"; + } + + return { accessStatus, authorID }; + } + + if (!validSession && padExists) { + // there is no valid session avaiable AND pad exists + + let authorID = await p_tokenAuthor; + let grant = Object.freeze({ accessStatus: "grant", authorID }); + + if (isPublic && !isPasswordProtected) { + // -- it's public and not password protected + + // --> grant access, with author of token + return grant; + } + + if (isPublic && isPasswordProtected && passwordStatus === "correct") { + // - it's public and password protected and password is correct + + // --> grant access, with author of token + return grant; + } + + if (isPublic && isPasswordProtected && passwordStatus === "wrong") { + // - it's public and the pad is password protected but wrong password given + + // --> deny access, ask for new password and tell them that the password is wrong + return { accessStatus: "wrongPassword" }; + } + + if (isPublic && isPasswordProtected && passwordStatus === "notGiven") { + // - it's public and the pad is password protected but no password given + + // --> ask for password + return { accessStatus: "needPassword" }; + } + + if (!isPublic) { + // - it's not public + + authLogger.debug("Auth failed: invalid session & pad is not public"); + // --> deny access + return { accessStatus: "deny" }; + } + + throw new Error("Oops, something wrong happend"); + } + + // there is no valid session avaiable AND pad doesn't exist + authLogger.debug("Auth failed: invalid session & pad does not exist"); + return { accessStatus: "deny" }; +} Index: lams_tool_doku/src/java/org/lamsfoundation/lams/tool/dokumaran/service/DokumaranService.java =================================================================== diff -u -rb6d0779192887979b5d3d244dec3506d12a7e401 -r024f71c24280149ce16489cd0bca12fa127b17c9 --- lams_tool_doku/src/java/org/lamsfoundation/lams/tool/dokumaran/service/DokumaranService.java (.../DokumaranService.java) (revision b6d0779192887979b5d3d244dec3506d12a7e401) +++ lams_tool_doku/src/java/org/lamsfoundation/lams/tool/dokumaran/service/DokumaranService.java (.../DokumaranService.java) (revision 024f71c24280149ce16489cd0bca12fa127b17c9) @@ -40,6 +40,7 @@ import org.lamsfoundation.lams.contentrepository.client.IToolContentHandler; import org.lamsfoundation.lams.etherpad.EtherpadException; import org.lamsfoundation.lams.etherpad.service.IEtherpadService; +import org.lamsfoundation.lams.etherpad.util.EtherpadUtil; import org.lamsfoundation.lams.learningdesign.service.ExportToolContentException; import org.lamsfoundation.lams.learningdesign.service.IExportToolContentService; import org.lamsfoundation.lams.learningdesign.service.ImportToolContentException; @@ -742,6 +743,7 @@ Long toolSessionId = session.getSessionId(); Long toolContentId = dokumaran.getContentId(); String groupIdentifier = DokumaranConstants.PREFIX_REGULAR_GROUP + toolSessionId; + String etherpadHtml = null; // in case sharedPadId is present - all sessions will share the same padId if (dokumaran.isSharedPadEnabled()) { @@ -767,31 +769,21 @@ dokumaranSessionDao.saveObject(session); return; } + } else { + etherpadHtml = EtherpadUtil.preparePadContent(dokumaran.getInstructions()); } Map result; + try { - result = etherpadService.createPad(groupIdentifier); + result = etherpadService.createPad(groupIdentifier, etherpadHtml); } catch (EtherpadException e) { throw new DokumaranApplicationException("Exception while creating an etherpad pad", e); } - EPLiteClient client = (EPLiteClient) result.get("client"); String groupId = (String) result.get("groupId"); - String padId = (String) result.get("padId"); - boolean isPadAlreadyCreated = (boolean) result.get("isPadAlreadyCreated"); - - session.setEtherpadGroupId(groupId); - // set initial content - if (!dokumaran.isSharedPadEnabled() || !isPadAlreadyCreated) { - String etherpadHtml = "" - + dokumaran.getInstructions().replaceAll("[\n\r\f]", "").replaceAll(" ", "") - + ""; - client.setHTML(padId, etherpadHtml); - } - - // gets read-only id String readOnlyId = (String) result.get("readOnlyId"); + session.setEtherpadGroupId(groupId); session.setEtherpadReadOnlyId(readOnlyId); dokumaranSessionDao.saveObject(session); Index: lams_tool_doku/web/pages/learning/learning.jsp =================================================================== diff -u -r8f7e92f0c90ad0462aa647b387dec6f14cf5012e -r024f71c24280149ce16489cd0bca12fa127b17c9 --- lams_tool_doku/web/pages/learning/learning.jsp (.../learning.jsp) (revision 8f7e92f0c90ad0462aa647b387dec6f14cf5012e) +++ lams_tool_doku/web/pages/learning/learning.jsp (.../learning.jsp) (revision 024f71c24280149ce16489cd0bca12fa127b17c9) @@ -38,7 +38,7 @@ - + - + + + + +
+ +<%-- If content was provided, put it into a hidden div so new lines will not break JavaScript --%> + + + + + \ No newline at end of file Index: lams_tool_scratchie/web/WEB-INF/tlds/lams/lams.tld =================================================================== diff -u -r24ebb6c91f49a10f1e5718036b3a3c1a80c3314f -r024f71c24280149ce16489cd0bca12fa127b17c9 --- lams_tool_scratchie/web/WEB-INF/tlds/lams/lams.tld (.../lams.tld) (revision 24ebb6c91f49a10f1e5718036b3a3c1a80c3314f) +++ lams_tool_scratchie/web/WEB-INF/tlds/lams/lams.tld (.../lams.tld) (revision 024f71c24280149ce16489cd0bca12fa127b17c9) @@ -344,6 +344,10 @@ OutcomeAuthor /WEB-INF/tags/OutcomeAuthor.tag + + Etherpad + /WEB-INF/tags/Etherpad.tag + errors /WEB-INF/tags/Errors.tag Index: lams_tool_scratchie/web/pages/learning/learning.jsp =================================================================== diff -u -r1268ec552ee72a22db3a5df680d46de7db79e0c0 -r024f71c24280149ce16489cd0bca12fa127b17c9 --- lams_tool_scratchie/web/pages/learning/learning.jsp (.../learning.jsp) (revision 1268ec552ee72a22db3a5df680d46de7db79e0c0) +++ lams_tool_scratchie/web/pages/learning/learning.jsp (.../learning.jsp) (revision 024f71c24280149ce16489cd0bca12fa127b17c9) @@ -399,7 +399,16 @@
<%@ include file="questionlist.jsp"%>
+ + <%-- ETHERPAD TEST --%> + + + Initial Etherpad text + + + +