diff --git a/docker/demo/Dockerfile b/docker/demo/Dockerfile index 7ff44b19..c3139d3f 100644 --- a/docker/demo/Dockerfile +++ b/docker/demo/Dockerfile @@ -6,7 +6,7 @@ ARG SEBSERVER_VERSION WORKDIR /demo RUN if [ "x${GIT_TAG}" = "x" ] ; \ then git clone --depth 1 https://github.com/SafeExamBrowser/seb-server.git ; \ - else git clone -b "v$GIT_TAG" --depth 1 https://github.com/SafeExamBrowser/seb-server.git ; fi + else git clone -b "$GIT_TAG" --depth 1 https://github.com/SafeExamBrowser/seb-server.git ; fi FROM maven:3.5-jdk-8-alpine diff --git a/docker/demo/docker-compose.yml b/docker/demo/docker-compose.yml index 94b7ccd1..ed659588 100644 --- a/docker/demo/docker-compose.yml +++ b/docker/demo/docker-compose.yml @@ -5,6 +5,8 @@ services: container_name: seb-server-mariadb environment: MYSQL_ROOT_PASSWORD: somePW + volumes: + - seb-server-mariadb-data:/var/lib/mysql ports: - 3306:3306 networks: @@ -33,4 +35,7 @@ services: - "mariadb" networks: - ralph: \ No newline at end of file + ralph: + +volumes: + seb-server-mariadb-data: \ No newline at end of file diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 2e59b043..0af7a354 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -27,6 +27,7 @@ public final class API { public static final String INSTITUTION_VAR_PATH_SEGMENT = "/{" + PARAM_INSTITUTION_ID + "}"; public static final String MODEL_ID_VAR_PATH_SEGMENT = "/{" + PARAM_MODEL_ID + "}"; + public static final String PARENT_MODEL_ID_VAR_PATH_SEGMENT = "/{" + PARAM_PARENT_MODEL_ID + "}"; public static final String OAUTH_ENDPOINT = "/oauth"; public static final String OAUTH_TOKEN_ENDPOINT = OAUTH_ENDPOINT + "/token"; // TODO to config properties? @@ -100,6 +101,7 @@ public final class API { public static final String QUIZ_DISCOVERY_ENDPOINT = "/quiz"; public static final String EXAM_ADMINISTRATION_ENDPOINT = "/exam"; + public static final String EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT = "/downloadConfig"; public static final String EXAM_INDICATOR_ENDPOINT = "/indicator"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java b/src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java index 4841093e..5dcdc60e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java @@ -29,7 +29,7 @@ import org.slf4j.LoggerFactory; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; -import ch.ethz.seb.sebserver.gui.service.remote.DownloadService; +import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java index 2d8a4cf2..be16b30e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java @@ -17,9 +17,12 @@ import java.util.function.Supplier; import org.apache.commons.lang3.BooleanUtils; import org.apache.tomcat.util.buf.StringUtils; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.rap.rwt.client.service.UrlLauncher; import org.eclipse.swt.widgets.Composite; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -50,6 +53,8 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; +import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService; +import ch.ethz.seb.sebserver.gui.service.remote.download.SebExamConfigDownload; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteExamConfigMapping; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteIndicator; @@ -73,9 +78,6 @@ public class ExamForm implements TemplateComposer { private static final Logger log = LoggerFactory.getLogger(ExamForm.class); - private final PageService pageService; - private final ResourceService resourceService; - private static final LocTextKey CONFIG_EMPTY_LIST_MESSAGE = new LocTextKey("sebserver.exam.configuration.list.empty"); private static final LocTextKey INDICATOR_EMPTY_LIST_MESSAGE = @@ -123,12 +125,21 @@ public class ExamForm implements TemplateComposer { private final static LocTextKey INDICATOR_EMPTY_SELECTION_TEXT_KEY = new LocTextKey("sebserver.exam.indicator.list.pleaseSelect"); + private final PageService pageService; + private final ResourceService resourceService; + private final DownloadService downloadService; + private final String downloadFileName; + protected ExamForm( final PageService pageService, - final ResourceService resourceService) { + final ResourceService resourceService, + final DownloadService downloadService, + @Value("${sebserver.gui.seb.exam.config.download.filename}") final String downloadFileName) { this.pageService = pageService; this.resourceService = resourceService; + this.downloadService = downloadService; + this.downloadFileName = downloadFileName; } @Override @@ -305,19 +316,7 @@ public class ExamForm implements TemplateComposer { this.resourceService::localizedExamConfigStatusName)) .withDefaultActionIf( () -> editable, - t -> actionBuilder - .newAction(ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP) - .withSelectionSupplier(() -> { - final ExamConfigurationMap selectedROWData = t.getSelectedROWData(); - final HashSet result = new HashSet<>(); - if (selectedROWData != null) { - result.add(new EntityKey( - selectedROWData.configurationNodeId, - EntityType.CONFIGURATION_NODE)); - } - return result; - }) - .create()) + this::viewExamConfigPageAction) .compose(pageContext.copyOf(content)); @@ -356,6 +355,15 @@ public class ExamForm implements TemplateComposer { CONFIG_EMPTY_SELECTION_TEXT_KEY) .publishIf(() -> modifyGrant && configurationTable.hasAnyContent() && editable) + .newAction(ActionDefinition.EXAM_CONFIGURATION_EXPORT) + .withParentEntityKey(entityKey) + .withSelect( + getConfigSelection(configurationTable), + this::downloadExamConfigAction, + CONFIG_EMPTY_SELECTION_TEXT_KEY) + .noEventPropagation() + .publishIf(() -> userGrantCheck.r() && configurationTable.hasAnyContent()) + .newAction(ActionDefinition.EXAM_CONFIGURATION_GET_CONFIG_KEY) .withSelect( getConfigSelection(configurationTable), @@ -423,6 +431,41 @@ public class ExamForm implements TemplateComposer { } } + private PageAction viewExamConfigPageAction(final EntityTable table) { + + final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(table.getPageContext() + .clearEntityKeys() + .removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA)); + + return actionBuilder + .newAction(ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP) + .withSelectionSupplier(() -> { + final ExamConfigurationMap selectedROWData = table.getSelectedROWData(); + final HashSet result = new HashSet<>(); + if (selectedROWData != null) { + result.add(new EntityKey( + selectedROWData.configurationNodeId, + EntityType.CONFIGURATION_NODE)); + } + return result; + }) + .create(); + } + + private PageAction downloadExamConfigAction(final PageAction action) { + final UrlLauncher urlLauncher = RWT.getClient().getService(UrlLauncher.class); + final EntityKey selection = action.getSingleSelection(); + if (selection != null) { + final String downloadURL = this.downloadService.createDownloadURL( + selection.modelId, + action.pageContext().getParentEntityKey().modelId, + SebExamConfigDownload.class, + this.downloadFileName); + urlLauncher.openURL(downloadURL); + } + return action; + } + private Supplier> getConfigMappingSelection( final EntityTable configurationTable) { return () -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientConfigForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientConfigForm.java index 82e1bbdf..d22f805a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientConfigForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientConfigForm.java @@ -32,8 +32,8 @@ import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.page.impl.PageUtils; -import ch.ethz.seb.sebserver.gui.service.remote.DownloadService; -import ch.ethz.seb.sebserver.gui.service.remote.SebClientConfigDownload; +import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService; +import ch.ethz.seb.sebserver.gui.service.remote.download.SebClientConfigDownload; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.clientconfig.ActivateClientConfig; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.clientconfig.DeactivateClientConfig; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java index 819d2e7c..0839ff53 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java @@ -39,8 +39,8 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; -import ch.ethz.seb.sebserver.gui.service.remote.DownloadService; -import ch.ethz.seb.sebserver.gui.service.remote.SebExamConfigDownload; +import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService; +import ch.ethz.seb.sebserver.gui.service.remote.download.SebExamConfigPlaintextDownload; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ExportConfigKey; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetExamConfigNode; @@ -188,7 +188,7 @@ public class SebExamConfigPropForm implements TemplateComposer { .withExec(action -> { final String downloadURL = this.downloadService.createDownloadURL( entityKey.modelId, - SebExamConfigDownload.class, + SebExamConfigPlaintextDownload.class, this.downloadFileName); urlLauncher.openURL(downloadURL); return action; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index c549f4c3..056425d7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -247,8 +247,13 @@ public enum ActionDefinition { ImageIcon.DELETE, PageStateDefinition.EXAM_VIEW, ActionCategory.EXAM_CONFIG_MAPPING_LIST), + EXAM_CONFIGURATION_EXPORT( + new LocTextKey("sebserver.exam.configuration.action.export-config"), + ImageIcon.EXPORT, + PageStateDefinition.EXAM_VIEW, + ActionCategory.EXAM_CONFIG_MAPPING_LIST), EXAM_CONFIGURATION_GET_CONFIG_KEY( - new LocTextKey("sebserver.examconfig.action.get-config-key"), + new LocTextKey("sebserver.exam.configuration.action.get-config-key"), ImageIcon.SECURE, ActionCategory.EXAM_CONFIG_MAPPING_LIST), EXAM_CONFIGURATION_SAVE( diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/AbstractDownloadServiceHandler.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/AbstractDownloadServiceHandler.java similarity index 59% rename from src/main/java/ch/ethz/seb/sebserver/gui/service/remote/AbstractDownloadServiceHandler.java rename to src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/AbstractDownloadServiceHandler.java index ce884b46..c4c14a8f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/AbstractDownloadServiceHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/AbstractDownloadServiceHandler.java @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.gui.service.remote; +package ch.ethz.seb.sebserver.gui.service.remote.download; import java.io.OutputStream; @@ -39,44 +39,34 @@ public abstract class AbstractDownloadServiceHandler implements DownloadServiceH log.debug("download requested... trying to get needed parameter from request"); - final String configId = request.getParameter(API.PARAM_MODEL_ID); - if (StringUtils.isBlank(configId)) { + final String modelId = request.getParameter(API.PARAM_MODEL_ID); + if (StringUtils.isBlank(modelId)) { log.error( "Mandatory modelId parameter not found within HttpServletRequest. Download request is ignored"); return; } - log.debug( - "Found modelId: {} for {} download. Trying to request webservice...", - configId, - downloadFileName); + if (log.isDebugEnabled()) { + log.debug( + "Found modelId: {} for {} download. Trying to request webservice...", + modelId, + downloadFileName); + } + + final String parentModelId = request.getParameter(API.PARAM_PARENT_MODEL_ID); + if (log.isDebugEnabled()) { + log.debug( + "Found parentModelId: {} for {} download. Trying to request webservice...", + modelId, + downloadFileName); + } final String header = "attachment; filename=\"" + Utils.preventResponseSplittingAttack(downloadFileName) + "\""; response.setHeader(HttpHeaders.CONTENT_DISPOSITION, header); response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); - webserviceCall(configId, response.getOutputStream()); - -// final byte[] configFile = webserviceCall(configId); -// -// if (configFile == null) { -// log.error("No or empty download received from webservice. Download request is ignored"); -// return; -// } -// -// log.debug("Sucessfully downloaded from webservice. File size: {}", configFile.length); -// -// response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); -// response.setContentLength(configFile.length); -// -// final String header = -// "attachment; filename=\"" + Utils.preventResponseSplittingAttack(downloadFileName) + "\""; -// response.setHeader(HttpHeaders.CONTENT_DISPOSITION, header); -// -// log.debug("Write the download data to response output"); -// -// response.getOutputStream().write(configFile); + webserviceCall(modelId, parentModelId, response.getOutputStream()); } catch (final Exception e) { log.error( @@ -85,6 +75,6 @@ public abstract class AbstractDownloadServiceHandler implements DownloadServiceH } } - protected abstract void webserviceCall(String configId, OutputStream downloadOut); + protected abstract void webserviceCall(String modelId, String parentModelId, OutputStream downloadOut); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/DownloadService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/DownloadService.java similarity index 82% rename from src/main/java/ch/ethz/seb/sebserver/gui/service/remote/DownloadService.java rename to src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/DownloadService.java index 25108563..53bdeddb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/DownloadService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/DownloadService.java @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.gui.service.remote; +package ch.ethz.seb.sebserver.gui.service.remote.download; import java.io.IOException; import java.util.Collection; @@ -81,6 +81,15 @@ public class DownloadService implements ServiceHandler { final Class handlerClass, final String downloadFileName) { + return createDownloadURL(modelId, null, handlerClass, downloadFileName); + } + + public String createDownloadURL( + final String modelId, + final String parentModelId, + final Class handlerClass, + final String downloadFileName) { + final StringBuilder url = new StringBuilder() .append(RWT.getServiceManager() .getServiceHandlerUrl(DownloadService.DOWNLOAD_SERVICE_NAME)) @@ -96,6 +105,14 @@ public class DownloadService implements ServiceHandler { .append(DownloadService.DOWNLOAD_FILE_NAME) .append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR) .append(downloadFileName); + + if (StringUtils.isNotBlank(parentModelId)) { + url.append(Constants.FORM_URL_ENCODED_SEPARATOR) + .append(API.PARAM_PARENT_MODEL_ID) + .append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR) + .append(parentModelId); + } + return url.toString(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/DownloadServiceHandler.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/DownloadServiceHandler.java similarity index 87% rename from src/main/java/ch/ethz/seb/sebserver/gui/service/remote/DownloadServiceHandler.java rename to src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/DownloadServiceHandler.java index e3243106..2a6c84d7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/DownloadServiceHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/DownloadServiceHandler.java @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.gui.service.remote; +package ch.ethz.seb.sebserver.gui.service.remote.download; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/SebClientConfigDownload.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/SebClientConfigDownload.java similarity index 90% rename from src/main/java/ch/ethz/seb/sebserver/gui/service/remote/SebClientConfigDownload.java rename to src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/SebClientConfigDownload.java index af1a69a3..06940051 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/SebClientConfigDownload.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/SebClientConfigDownload.java @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.gui.service.remote; +package ch.ethz.seb.sebserver.gui.service.remote.download; import java.io.IOException; import java.io.InputStream; @@ -43,7 +43,7 @@ public class SebClientConfigDownload extends AbstractDownloadServiceHandler { } @Override - protected void webserviceCall(final String modelId, final OutputStream downloadOut) { + protected void webserviceCall(final String modelId, final String parentModelId, final OutputStream downloadOut) { final InputStream input = this.restService.getBuilder(ExportClientConfig.class) .withURIVariable(API.PARAM_MODEL_ID, modelId) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/SebExamConfigDownload.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/SebExamConfigDownload.java similarity index 81% rename from src/main/java/ch/ethz/seb/sebserver/gui/service/remote/SebExamConfigDownload.java rename to src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/SebExamConfigDownload.java index c65038d7..37f088a8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/SebExamConfigDownload.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/SebExamConfigDownload.java @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.gui.service.remote; +package ch.ethz.seb.sebserver.gui.service.remote.download; import java.io.IOException; import java.io.InputStream; @@ -21,7 +21,7 @@ import org.springframework.stereotype.Component; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ExportPlainXML; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.ExportExamConfig; @Lazy @Component @@ -37,10 +37,11 @@ public class SebExamConfigDownload extends AbstractDownloadServiceHandler { } @Override - protected void webserviceCall(final String modelId, final OutputStream downloadOut) { + protected void webserviceCall(final String modelId, final String parentModelId, final OutputStream downloadOut) { - final InputStream input = this.restService.getBuilder(ExportPlainXML.class) + final InputStream input = this.restService.getBuilder(ExportExamConfig.class) .withURIVariable(API.PARAM_MODEL_ID, modelId) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, parentModelId) .call() .getOrThrow(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/SebExamConfigPlaintextDownload.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/SebExamConfigPlaintextDownload.java new file mode 100644 index 00000000..8f25bf30 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/download/SebExamConfigPlaintextDownload.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gui.service.remote.download; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.tomcat.util.http.fileupload.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ExportPlainXML; + +@Lazy +@Component +@GuiProfile +public class SebExamConfigPlaintextDownload extends AbstractDownloadServiceHandler { + + private static final Logger log = LoggerFactory.getLogger(SebExamConfigPlaintextDownload.class); + + private final RestService restService; + + protected SebExamConfigPlaintextDownload(final RestService restService) { + this.restService = restService; + } + + @Override + protected void webserviceCall(final String modelId, final String parentModelId, final OutputStream downloadOut) { + + final InputStream input = this.restService.getBuilder(ExportPlainXML.class) + .withURIVariable(API.PARAM_MODEL_ID, modelId) + .call() + .getOrThrow(); + + try { + IOUtils.copyLarge(input, downloadOut); + } catch (final IOException e) { + log.error( + "Unexpected error while streaming incomming config data from web-service to output-stream of download response: ", + e); + } finally { + try { + downloadOut.flush(); + downloadOut.close(); + } catch (final IOException e) { + log.error("Unexpected error while trying to close download output-stream"); + } + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/ExportExamConfig.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/ExportExamConfig.java new file mode 100644 index 00000000..69e12bd4 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/ExportExamConfig.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam; + +import java.io.InputStream; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.AbstractExportCall; + +@Lazy +@Component +@GuiProfile +public class ExportExamConfig extends AbstractExportCall { + + public ExportExamConfig() { + super(new TypeKey<>( + CallType.UNDEFINED, + EntityType.EXAM, + new TypeReference() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_ADMINISTRATION_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java index 389800df..12b2efa1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java @@ -189,6 +189,14 @@ public class EntityTable { this.sortOrder); } + public PageContext getPageContext() { + if (this.pageContext == null) { + return null; + } + + return this.pageContext.copy(); + } + public boolean hasAnyContent() { return this.table.getItemCount() > 0; } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/datalayer/batis/BatisConfig.java b/src/main/java/ch/ethz/seb/sebserver/webservice/datalayer/batis/BatisConfig.java index 50d986b0..f8fcef70 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/datalayer/batis/BatisConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/datalayer/batis/BatisConfig.java @@ -55,11 +55,6 @@ public class BatisConfig { factoryBean.setDataSource(dataSource); final SqlSessionFactory factory = factoryBean.getObject(); - factory.getConfiguration() - .addMappers("ch.ethz.seb.sebserver.webservice.datalayer.batis"); - factory.getConfiguration() - .addMappers("ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper"); - return factory; } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java index 1be1d520..dd00a7cb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java @@ -16,6 +16,20 @@ public interface ExamConfigurationMapDAO extends EntityDAO, BulkActionSupportDAO { + /** Get a specific ExamConfigurationMap by the mapping identifiers + * + * @param examId The Exam mapping identifier + * @param configurationNodeId the ConfigurationNode mapping identifier + * @return Result refer to the ExamConfigurationMap with specified mapping or to an exception if happened */ + public Result byMapping(Long examId, Long configurationNodeId); + + /** Get the password cipher of a specific ExamConfigurationMap by the mapping identifiers + * + * @param examId The Exam mapping identifier + * @param configurationNodeId the ConfigurationNode mapping identifier + * @return Result refer to the password cipher of specified mapping or to an exception if happened */ + public Result getConfigPasswortCipher(Long examId, Long configurationNodeId); + /** Get the ConfigurationNode identifier of the default Exam Configuration of * the Exam with specified identifier. * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java index 06ae0e94..acb737ec 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java @@ -126,6 +126,43 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { .collect(Collectors.toList())); } + @Override + @Transactional(readOnly = true) + public Result byMapping(final Long examId, final Long configurationNodeId) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper + .selectByExample() + .where( + ExamConfigurationMapRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(examId)) + .and( + ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId, + SqlBuilder.isEqualTo(configurationNodeId)) + .build() + .execute() + .stream() + .map(this::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .collect(Utils.toSingleton())); + } + + @Override + @Transactional(readOnly = true) + public Result getConfigPasswortCipher(final Long examId, final Long configurationNodeId) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper + .selectByExample() + .where( + ExamConfigurationMapRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(examId)) + .and( + ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId, + SqlBuilder.isEqualTo(configurationNodeId)) + .build() + .execute() + .stream() + .collect(Utils.toSingleton())) + .map(ExamConfigurationMapRecord::getEncryptSecret); + } + @Override @Transactional(readOnly = true) public Result getDefaultConfigurationForExam(final Long examId) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebExamConfigService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebExamConfigService.java index 5ea8769e..ef967ccc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebExamConfigService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebExamConfigService.java @@ -61,7 +61,7 @@ public interface SebExamConfigService { * @param examId the exam identifier * @return The configuration node identifier (PK) */ default Long exportForExam(final OutputStream out, final Long institutionId, final Long examId) { - return exportForExam(out, institutionId, examId, null); + return exportForExam(out, institutionId, examId, (String) null); } /** Used to export the default SEB Exam Configuration for a given exam identifier. @@ -75,7 +75,23 @@ public interface SebExamConfigService { * @return The configuration node identifier (PK) */ Long exportForExam(OutputStream out, Long institutionId, Long examId, String userId); - /** TODO */ + /** Used to export the default SEB Exam Configuration for a given exam identifier. + * either with encryption if defined or as plain text within the SEB Configuration format + * as described here: https://www.safeexambrowser.org/developer/seb-file-format.html + * + * @param out The output stream to write the export data to + * @param institutionId The identifier of the institution of the requesting user + * @param examId the exam identifier that defines the mapping + * @param configurationNodeId the configurationNodeId that defines the mapping + * @return The configuration node identifier (PK) */ + Long exportForExam(OutputStream out, Long institutionId, Long examId, Long configurationNodeId); + + /** Generates a Config-Key form the SEB exam configuration defined by configurationNodeId. + * See https://www.safeexambrowser.org/developer/seb-config-key.html for more information about the Config-Key + * + * @param institutionId the institutional id + * @param configurationNodeId the configurationNodeId + * @return Result refer to the generated Config-Key or to an error if happened. */ Result generateConfigKey(Long institutionId, Long configurationNodeId); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java index 61198165..f0a074c7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java @@ -33,11 +33,16 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationFormat; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationValueValidator; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebConfigEncryptionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebConfigEncryptionService.Strategy; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebExamConfigService; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ZipService; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.SebConfigEncryptionServiceImpl.EncryptionContext; @Lazy @Service @@ -50,17 +55,26 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { private final ConfigurationAttributeDAO configurationAttributeDAO; private final ExamConfigurationMapDAO examConfigurationMapDAO; private final Collection validators; + private final ClientCredentialService clientCredentialService; + private final ZipService zipService; + private final SebConfigEncryptionService sebConfigEncryptionService; protected SebExamConfigServiceImpl( final ExamConfigIO examConfigIO, final ConfigurationAttributeDAO configurationAttributeDAO, final ExamConfigurationMapDAO examConfigurationMapDAO, - final Collection validators) { + final Collection validators, + final ClientCredentialService clientCredentialService, + final ZipService zipService, + final SebConfigEncryptionService sebConfigEncryptionService) { this.examConfigIO = examConfigIO; this.configurationAttributeDAO = configurationAttributeDAO; this.examConfigurationMapDAO = examConfigurationMapDAO; this.validators = validators; + this.clientCredentialService = clientCredentialService; + this.zipService = zipService; + this.sebConfigEncryptionService = sebConfigEncryptionService; } @@ -114,7 +128,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { final Long institutionId, final Long configurationNodeId) { - this.exportPlain(ConfigurationFormat.XML, out, institutionId, configurationNodeId); + this.exportPlainOnly(ConfigurationFormat.XML, out, institutionId, configurationNodeId); } @Override @@ -123,7 +137,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { final Long institutionId, final Long configurationNodeId) { - this.exportPlain(ConfigurationFormat.JSON, out, institutionId, configurationNodeId); + this.exportPlainOnly(ConfigurationFormat.JSON, out, institutionId, configurationNodeId); } public Result getDefaultConfigurationIdForExam(final Long examId) { @@ -147,9 +161,83 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { : getUserConfigurationIdForExam(examId, userId) .getOrThrow(); - // TODO add header, zip and encrypt if needed + return exportForExam(out, institutionId, examId, configurationNodeId); + } - this.exportPlainXML(out, institutionId, configurationNodeId); + @Override + public Long exportForExam( + final OutputStream out, + final Long institutionId, + final Long examId, + final Long configurationNodeId) { + + final CharSequence passwordCipher = this.examConfigurationMapDAO + .getConfigPasswortCipher(examId, configurationNodeId) + .getOr(null); + + if (StringUtils.isNotBlank(passwordCipher)) { + + if (log.isDebugEnabled()) { + log.debug("*** Seb exam configuration with password based encryption"); + } + + final CharSequence encryptionPasswordPlaintext = this.clientCredentialService + .decrypt(passwordCipher); + + PipedOutputStream plainOut = null; + PipedInputStream zipIn = null; + + PipedOutputStream zipOut = null; + PipedInputStream cryptIn = null; + + PipedOutputStream cryptOut = null; + PipedInputStream in = null; + + try { + + plainOut = new PipedOutputStream(); + zipIn = new PipedInputStream(plainOut); + + zipOut = new PipedOutputStream(); + cryptIn = new PipedInputStream(zipOut); + + cryptOut = new PipedOutputStream(); + in = new PipedInputStream(cryptOut); + + // streaming... + // export plain text + this.examConfigIO.exportPlain( + ConfigurationFormat.XML, + plainOut, + institutionId, + configurationNodeId); + // zip the plain text + this.zipService.write(zipOut, zipIn); + // encrypt the zipped plain text + this.sebConfigEncryptionService.streamEncrypted( + cryptOut, + cryptIn, + EncryptionContext.contextOf( + Strategy.PASSWORD_PSWD, + encryptionPasswordPlaintext)); + + // copy to output + IOUtils.copyLarge(in, out); + + } catch (final Exception e) { + log.error("Error while zip and encrypt seb exam config stream: ", e); + } finally { + IOUtils.closeQuietly(zipIn); + IOUtils.closeQuietly(plainOut); + IOUtils.closeQuietly(cryptIn); + IOUtils.closeQuietly(zipOut); + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(cryptOut); + } + } else { + // just export in plain text XML format + this.exportPlainXML(out, institutionId, configurationNodeId); + } return configurationNodeId; } @@ -224,7 +312,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { } } - private void exportPlain( + private void exportPlainOnly( final ConfigurationFormat exportFormat, final OutputStream out, final Long institutionId, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index 79b41678..16cdef68 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -8,17 +8,25 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + import org.apache.commons.lang3.StringUtils; import org.mybatis.dynamic.sql.SqlTable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.util.MultiValueMap; import org.springframework.validation.FieldError; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -50,6 +58,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebExamConfigService; import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; @WebServiceProfile @@ -57,9 +66,12 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_ADMINISTRATION_ENDPOINT) public class ExamAdministrationController extends ActivatableEntityController { + private static final Logger log = LoggerFactory.getLogger(ExamAdministrationController.class); + private final ExamDAO examDAO; private final UserDAO userDAO; private final LmsAPIService lmsAPIService; + private final SebExamConfigService sebExamConfigService; public ExamAdministrationController( final AuthorizationService authorization, @@ -69,7 +81,8 @@ public class ExamAdministrationController extends ActivatableEntityController buildSortedExamPage( final Integer pageNumber, final Integer pageSize, diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 6dd3bb67..1504f6be 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -311,8 +311,8 @@ sebserver.exam.status.UP_COMING=Up Coming sebserver.exam.status.RUNNING=Running sebserver.exam.status.FINISHED=Finished -sebserver.exam.configuration.list.actions=SEB Configuration -sebserver.exam.configuration.list.title=SEB Configuration +sebserver.exam.configuration.list.actions=SEB Exam Configuration +sebserver.exam.configuration.list.title=SEB Exam Configuration sebserver.exam.configuration.list.column.name=Name sebserver.exam.configuration.list.column.description=Description sebserver.exam.configuration.list.column.status=Status @@ -320,10 +320,12 @@ sebserver.exam.configuration.list.empty=There is currently no SEB Configuration sebserver.exam.configuration.list.pleaseSelect=Please Select a SEB Configuration first sebserver.exam.configuration.action.noconfig.message=There is currently no SEB exam configuration to select.
Please create one in SEB Configuration / Exam Configuration -sebserver.exam.configuration.action.list.new=Add -sebserver.exam.configuration.action.list.modify=Edit -sebserver.exam.configuration.action.list.delete=Delete -sebserver.exam.configuration.action.save=Save +sebserver.exam.configuration.action.list.new=Add Configuration +sebserver.exam.configuration.action.list.modify=Edit Configuration +sebserver.exam.configuration.action.list.delete=Delete Configuration +sebserver.exam.configuration.action.save=Save Configuration +sebserver.exam.configuration.action.export-config=Export Configuration +sebserver.exam.configuration.action.get-config-key=Export Config-Key sebserver.exam.configuration.form.title.new=Add SEB Configuration Mapping sebserver.exam.configuration.form.title=SEB Configuration Mapping