diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java index 31c9949e..74fe7982 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java @@ -35,7 +35,10 @@ public class ProctoringServiceSettings implements Entity { TOWN_HALL, ONE_TO_ONE, BROADCAST, - ENABLE_CHAT + ENABLE_CHAT, + WAITING_ROOM, + SEND_REJOIN_COLLECTING_ROOM, + RESET_BROADCAST_ON_LAVE } public static final String ATTR_ENABLE_PROCTORING = "enableProctoring"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/LoginPage.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/LoginPage.java index 77acbecc..32537cea 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/LoginPage.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/LoginPage.java @@ -12,10 +12,12 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.rap.rwt.RWT; import org.eclipse.swt.SWT; +import org.eclipse.swt.internal.widgets.IControlAdapter; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.MessageBox; import org.eclipse.swt.widgets.Text; @@ -27,6 +29,8 @@ import org.springframework.stereotype.Component; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.ComposerService; 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; @@ -43,6 +47,11 @@ public class LoginPage implements TemplateComposer { private static final Logger log = LoggerFactory.getLogger(LoginPage.class); + private static final LocTextKey TEXT_REGISTER = new LocTextKey("sebserver.login.register"); + private static final LocTextKey TEXT_LOGIN = new LocTextKey("sebserver.login.login"); + private static final LocTextKey TEXT_PWD = new LocTextKey("sebserver.login.pwd"); + private static final LocTextKey TEXT_USERNAME = new LocTextKey("sebserver.login.username"); + private final PageService pageService; private final AuthorizationContextHolder authorizationContextHolder; private final WidgetFactory widgetFactory; @@ -67,7 +76,6 @@ public class LoginPage implements TemplateComposer { public void compose(final PageContext pageContext) { final Composite parent = pageContext.getParent(); WidgetFactory.setTestId(parent, "login-page"); - WidgetFactory.setARIARole(parent, "composite"); final Composite loginGroup = new Composite(parent, SWT.NONE); final GridLayout rowLayout = new GridLayout(); @@ -76,16 +84,17 @@ public class LoginPage implements TemplateComposer { loginGroup.setLayout(rowLayout); loginGroup.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN.key); - final Label name = this.widgetFactory.labelLocalized(loginGroup, "sebserver.login.username"); + final Label name = this.widgetFactory.labelLocalized(loginGroup, TEXT_USERNAME); name.setLayoutData(new GridData(300, -1)); name.setAlignment(SWT.BOTTOM); - final Text loginName = this.widgetFactory.textInput(loginGroup); + final Text loginName = this.widgetFactory.textInput(loginGroup, TEXT_USERNAME); loginName.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false)); + GridData gridData = new GridData(SWT.FILL, SWT.TOP, false, false); gridData.verticalIndent = 10; - final Label pwd = this.widgetFactory.labelLocalized(loginGroup, "sebserver.login.pwd"); + final Label pwd = this.widgetFactory.labelLocalized(loginGroup, TEXT_PWD); pwd.setLayoutData(gridData); - final Text loginPassword = this.widgetFactory.passwordInput(loginGroup); + final Text loginPassword = this.widgetFactory.passwordInput(loginGroup, TEXT_PWD); loginPassword.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false)); final Composite buttons = new Composite(loginGroup, SWT.NONE); @@ -93,7 +102,7 @@ public class LoginPage implements TemplateComposer { buttons.setLayout(new GridLayout(2, false)); buttons.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN_BACK.key); - final Button loginButton = this.widgetFactory.buttonLocalized(buttons, "sebserver.login.login"); + final Button loginButton = this.widgetFactory.buttonLocalized(buttons, TEXT_LOGIN); gridData = new GridData(SWT.LEFT, SWT.TOP, false, false); gridData.verticalIndent = 10; loginButton.setLayoutData(gridData); @@ -127,12 +136,17 @@ public class LoginPage implements TemplateComposer { }); if (this.registeringEnabled) { - final Button registerButton = this.widgetFactory.buttonLocalized(buttons, "sebserver.login.register"); + final Button registerButton = this.widgetFactory.buttonLocalized(buttons, TEXT_REGISTER); gridData = new GridData(SWT.LEFT, SWT.TOP, false, false); gridData.verticalIndent = 10; registerButton.setLayoutData(gridData); registerButton.addListener(SWT.Selection, event -> pageContext.forwardToPage(this.defaultRegisterPage)); } + + ComposerService.traversePageTree( + parent, + comp -> comp instanceof Control, + comp -> comp.getAdapter(IControlAdapter.class).setTabIndex(0)); } private void login( diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java index 049a7d1a..29ae39a6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java @@ -119,10 +119,10 @@ public final class TextFieldBuilder extends FieldBuilder { } final Text textInput = (this.isNumber) - ? builder.widgetFactory.numberInput(fieldGrid, this.numberCheck, readonly) + ? builder.widgetFactory.numberInput(fieldGrid, this.numberCheck, readonly, this.label) : (this.isArea) - ? builder.widgetFactory.textAreaInput(fieldGrid, readonly) - : builder.widgetFactory.textInput(fieldGrid, this.isPassword, readonly); + ? builder.widgetFactory.textAreaInput(fieldGrid, readonly, this.label) + : builder.widgetFactory.textInput(fieldGrid, this.isPassword, readonly, this.label); if (builder.pageService.getFormTooltipMode() == PageService.FormTooltipMode.INPUT) { builder.pageService.getPolyglotPageService().injectI18nTooltip( diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultPageLayout.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultPageLayout.java index 063050b7..7b4d057e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultPageLayout.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultPageLayout.java @@ -46,6 +46,7 @@ import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder; import ch.ethz.seb.sebserver.gui.widget.Message; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.AriaRole; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; @Lazy @@ -285,6 +286,8 @@ public class DefaultPageLayout implements TemplateComposer { log.error("Invalid markup for 'Imprint'", e); } }); + + WidgetFactory.setARIARole(imprint, AriaRole.link); } if (StringUtils.isNoneBlank(i18nSupport.getText(ABOUT_TEXT_KEY, ""))) { final Label about = this.widgetFactory.labelLocalized( @@ -299,6 +302,8 @@ public class DefaultPageLayout implements TemplateComposer { log.error("Invalid markup for 'About'", e); } }); + + WidgetFactory.setARIARole(about, AriaRole.link); } if (StringUtils.isNoneBlank(i18nSupport.getText(HELP_TEXT_KEY, ""))) { final Label help = this.widgetFactory.labelLocalized( @@ -318,6 +323,7 @@ public class DefaultPageLayout implements TemplateComposer { } }); + WidgetFactory.setARIARole(help, AriaRole.link); } this.widgetFactory.labelLocalized( footerRight, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableFilter.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableFilter.java index a1bf0515..ff8dfdd5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableFilter.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableFilter.java @@ -321,7 +321,9 @@ public class TableFilter { final Composite innerComposite = createInnerComposite(parent); final GridData gridData = new GridData(SWT.FILL, SWT.END, true, true); - this.textInput = TableFilter.this.entityTable.widgetFactory.textInput(innerComposite); + this.textInput = TableFilter.this.entityTable.widgetFactory.textInput( + innerComposite, + super.attribute.columnName); this.textInput.setLayoutData(gridData); return this; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/MultiSelectionCombo.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/MultiSelectionCombo.java index 10509f4c..4f1628b8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/MultiSelectionCombo.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/MultiSelectionCombo.java @@ -8,9 +8,11 @@ package ch.ethz.seb.sebserver.gui.widget; -import ch.ethz.seb.sebserver.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.util.Tuple; -import ch.ethz.seb.sebserver.gui.service.page.PageService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + import org.apache.commons.lang3.StringUtils; import org.eclipse.rap.rwt.widgets.DropDown; import org.eclipse.swt.SWT; @@ -25,10 +27,9 @@ import org.eclipse.swt.widgets.Text; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.util.Tuple; +import ch.ethz.seb.sebserver.gui.service.page.PageService; public final class MultiSelectionCombo extends Composite implements Selection { @@ -68,7 +69,7 @@ public final class MultiSelectionCombo extends Composite implements Selection { setLayout(gridLayout); this.addListener(SWT.Resize, this::adaptColumnWidth); - this.textInput = widgetFactory.textInput(this); + this.textInput = widgetFactory.textInput(this, "selection"); this.textCell = new GridData(SWT.LEFT, SWT.CENTER, true, true); this.textInput.setLayoutData(this.textCell); this.dropDown = new DropDown(this.textInput, SWT.NONE); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/ThresholdList.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/ThresholdList.java index 45282501..b25568c4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/ThresholdList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/ThresholdList.java @@ -152,7 +152,8 @@ public final class ThresholdList extends Composite { } else { Double.parseDouble(s); } - }); + }, + VALUE_TEXT_KEY); final GridData valueCell = new GridData(SWT.FILL, SWT.CENTER, true, false); valueInput.setLayoutData(valueCell); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java index 142525c1..b60d9732 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java @@ -70,6 +70,10 @@ public class WidgetFactory { private static final String ADD_HTML_ATTR_TEST_ID = "test-id"; private static final String SUB_TITLE_TExT_SUFFIX = ".subtitle"; + public enum AriaRole { + link + } + private static final Logger log = LoggerFactory.getLogger(WidgetFactory.class); public static final int TEXT_AREA_INPUT_MIN_HEIGHT = 100; @@ -363,18 +367,21 @@ public class WidgetFactory { public Button buttonLocalized(final Composite parent, final String locTextKey) { final Button button = new Button(parent, SWT.NONE); + setAttribute(button, "role", "button"); this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey)); return button; } public Button buttonLocalized(final Composite parent, final LocTextKey locTextKey) { final Button button = new Button(parent, SWT.NONE); + setAttribute(button, "role", "button"); this.polyglotPageService.injectI18n(button, locTextKey); return button; } public Button buttonLocalized(final Composite parent, final CustomVariant variant, final String locTextKey) { final Button button = new Button(parent, SWT.NONE); + setAttribute(button, "role", "button"); this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey)); button.setData(RWT.CUSTOM_VARIANT, variant.key); return button; @@ -387,6 +394,7 @@ public class WidgetFactory { final LocTextKey toolTipKey) { final Button button = new Button(parent, type); + setAttribute(button, "role", "button"); this.polyglotPageService.injectI18n(button, locTextKey, toolTipKey); return button; } @@ -453,42 +461,85 @@ public class WidgetFactory { return labelLocalized; } - public Text textInput(final Composite content) { - return textInput(content, false, false); + public Text textInput(final Composite content, final LocTextKey label) { + return textInput(content, false, false, this.i18nSupport.getText(label)); } - public Text textLabel(final Composite content) { - return textInput(content, false, true); + public Text textInput(final Composite content, final String label) { + return textInput(content, false, false, label); } - public Text passwordInput(final Composite content) { - return textInput(content, true, false); + public Text passwordInput(final Composite content, final LocTextKey label) { + return textInput(content, true, false, this.i18nSupport.getText(label)); } - public Text textAreaInput(final Composite content, final boolean readonly) { - return readonly + public Text passwordInput(final Composite content, final String label) { + return textInput(content, true, false, label); + } + + public Text textAreaInput( + final Composite content, + final boolean readonly, + final LocTextKey label) { + + return textAreaInput(content, readonly, this.i18nSupport.getText(label)); + } + + public Text textAreaInput( + final Composite content, + final boolean readonly, + final String label) { + + final Text input = readonly ? new Text(content, SWT.LEFT | SWT.MULTI) : new Text(content, SWT.LEFT | SWT.BORDER | SWT.MULTI); + if (label != null) { + WidgetFactory.setAttribute(input, "aria-label", label); + } + return input; } - public Text textInput(final Composite content, final boolean password, final boolean readonly) { - return readonly + public Text textInput( + final Composite content, + final boolean password, + final boolean readonly, + final LocTextKey label) { + + return textInput(content, password, readonly, this.i18nSupport.getText(label)); + } + + public Text textInput( + final Composite content, + final boolean password, + final boolean readonly, + final String label) { + + final Text input = readonly ? new Text(content, SWT.LEFT) : new Text(content, (password) ? SWT.LEFT | SWT.BORDER | SWT.PASSWORD : SWT.LEFT | SWT.BORDER); - } - public Text numberInput(final Composite content, final Consumer numberCheck) { - return numberInput(content, numberCheck, false); - } - - public Text numberInput(final Composite content, final Consumer numberCheck, final boolean readonly) { - if (readonly) { - return new Text(content, SWT.LEFT | SWT.READ_ONLY); + if (label != null) { + WidgetFactory.setAttribute(input, "aria-label", label); } + return input; + } - final Text numberInput = new Text(content, SWT.RIGHT | SWT.BORDER); + public Text numberInput(final Composite content, final Consumer numberCheck, final LocTextKey label) { + return numberInput(content, numberCheck, false, label); + } + + public Text numberInput( + final Composite content, + final Consumer numberCheck, + final boolean readonly, + final LocTextKey label) { + + final Text numberInput = new Text(content, (readonly) ? SWT.LEFT | SWT.READ_ONLY : SWT.RIGHT | SWT.BORDER); + if (label != null) { + WidgetFactory.setAttribute(numberInput, "aria-label", this.i18nSupport.getText(label)); + } if (numberCheck != null) { numberInput.addListener(SWT.Verify, event -> { final String value = event.text; @@ -884,11 +935,11 @@ public class WidgetFactory { setAttribute(widget, ADD_HTML_ATTR_TEST_ID, value); } - public static void setARIARole(final Widget widget, final String value) { - setAttribute(widget, ADD_HTML_ATTR_ARIA_ROLE, value); + public static void setARIARole(final Widget widget, final AriaRole role) { + setAttribute(widget, ADD_HTML_ATTR_ARIA_ROLE, role.name()); } - private static void setAttribute(final Widget widget, final String name, final String value) { + public static void setAttribute(final Widget widget, final String name, final String value) { if (!widget.isDisposed()) { final String $el = widget instanceof Text ? "$input" : "$el"; final String id = WidgetUtil.getId(widget); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java index 2f6615b0..e8157a49 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java @@ -27,6 +27,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; +import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType; @@ -230,7 +231,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService } else if (remoteProctoringRoom.townhallRoom) { closeTownhall(examId, settings, examProctoringService); } else { - closeCollectingRoom(examId, roomName, examProctoringService); + closeCollectingRoom(examId, roomName, settings, examProctoringService); } }); } @@ -377,11 +378,13 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService .getActiveConnectionTokens(examId) .getOrThrow(); - // Send default settings to clients - this.sendReconfigurationInstructions( - examId, - connectionTokens, - examProctoringService.getDefaultReconfigInstructionAttributes()); + // Send default settings to clients if fearture is enabled + if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.RESET_BROADCAST_ON_LAVE)) { + this.sendReconfigurationInstructions( + examId, + connectionTokens, + examProctoringService.getDefaultReconfigInstructionAttributes()); + } // Close and delete town-hall room this.remoteProctoringRoomDAO @@ -403,6 +406,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService private void closeCollectingRoom( final Long examId, final String roomName, + final ProctoringServiceSettings proctoringSettings, final ExamProctoringService examProctoringService) { // get all connections of the room @@ -412,11 +416,13 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService .map(cc -> cc.connectionToken) .collect(Collectors.toList()); - // Send default settings to clients - this.sendReconfigurationInstructions( - examId, - connectionTokens, - examProctoringService.getDefaultReconfigInstructionAttributes()); + // Send default settings to clients if feature is enabled + if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.RESET_BROADCAST_ON_LAVE)) { + this.sendReconfigurationInstructions( + examId, + connectionTokens, + examProctoringService.getDefaultReconfigInstructionAttributes()); + } } private void cleanupBreakOutRooms(final ClientConnectionRecord cc) { @@ -453,11 +459,13 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService final ExamProctoringService examProctoringService, final RemoteProctoringRoom remoteProctoringRoom) { - // Send default settings to clients - this.sendReconfigurationInstructions( - examId, - remoteProctoringRoom.breakOutConnections, - examProctoringService.getDefaultReconfigInstructionAttributes()); + // Send default settings to clients if feature is enabled + if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.RESET_BROADCAST_ON_LAVE)) { + this.sendReconfigurationInstructions( + examId, + remoteProctoringRoom.breakOutConnections, + examProctoringService.getDefaultReconfigInstructionAttributes()); + } // Dispose the proctoring room on service side examProctoringService diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java index b13a4234..80fd682b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java @@ -57,6 +57,7 @@ import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; +import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; @@ -443,8 +444,13 @@ public class ZoomProctoringService implements ExamProctoringService { return Result.tryCatch(() -> { + if (!proctoringSettings.enabledFeatures.contains(ProctoringFeature.SEND_REJOIN_COLLECTING_ROOM)) { + // do nothing if the rejoin feature is not enabled + return; + } + if (this.remoteProctoringRoomDAO.isTownhallRoomActive(proctoringSettings.examId)) { - // do nothing is the town-hall of this exam is open. The clients will automatically join + // do nothing if the town-hall of this exam is open. The clients will automatically join // the meeting once the town-hall has been closed return; } @@ -502,6 +508,7 @@ public class ZoomProctoringService implements ExamProctoringService { proctoringSettings.serverURL, credentials, roomName); + final UserResponse userResponse = this.jsonMapper.readValue( createUser.getBody(), UserResponse.class); @@ -514,7 +521,9 @@ public class ZoomProctoringService implements ExamProctoringService { userResponse.id, subject, duration, - meetingPwd); + meetingPwd, + proctoringSettings.enabledFeatures.contains(ProctoringFeature.WAITING_ROOM)); + final MeetingResponse meetingResponse = this.jsonMapper.readValue( createMeeting.getBody(), MeetingResponse.class); @@ -525,6 +534,7 @@ public class ZoomProctoringService implements ExamProctoringService { userResponse.id, meetingResponse.start_url, meetingResponse.join_url); + final String additionalZoomRoomDataString = this.jsonMapper .writeValueAsString(additionalZoomRoomData); @@ -563,23 +573,29 @@ public class ZoomProctoringService implements ExamProctoringService { final StringBuilder builder = new StringBuilder(); final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding(); - final String jwtHeaderPart = urlEncoder.encodeToString( - ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8)); + final String jwtHeaderPart = urlEncoder + .encodeToString(ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8)); + final String jwtPayload = String.format( - ZOOM_API_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""), + ZOOM_API_ACCESS_TOKEN_PAYLOAD + .replaceAll(" ", "") + .replaceAll("\n", ""), credentials.clientIdAsString(), expTime); - final String jwtPayloadPart = urlEncoder.encodeToString( - jwtPayload.getBytes(StandardCharsets.UTF_8)); + + final String jwtPayloadPart = urlEncoder + .encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8)); + final String message = jwtHeaderPart + "." + jwtPayloadPart; final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG); final SecretKeySpec secret_key = new SecretKeySpec( Utils.toByteArray(decryptedSecret), TOKEN_ENCODE_ALG); + sha256_HMAC.init(secret_key); - final String hash = urlEncoder.encodeToString( - sha256_HMAC.doFinal(Utils.toByteArray(message))); + final String hash = urlEncoder + .encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message))); builder.append(message) .append(".") @@ -710,7 +726,8 @@ public class ZoomProctoringService implements ExamProctoringService { final String userId, final String topic, final int duration, - final CharSequence password) { + final CharSequence password, + final boolean waitingRoom) { try { @@ -723,7 +740,8 @@ public class ZoomProctoringService implements ExamProctoringService { final CreateMeetingRequest createRoomRequest = new CreateMeetingRequest( topic, duration, - password); + password, + waitingRoom); final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createRoomRequest); final HttpHeaders headers = getHeaders(credentials); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomRoomRequestResponse.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomRoomRequestResponse.java index 691500af..5ea1492e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomRoomRequestResponse.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomRoomRequestResponse.java @@ -123,7 +123,8 @@ public interface ZoomRoomRequestResponse { public CreateMeetingRequest( final String topic, final int duration, - final CharSequence password) { + final CharSequence password, + final boolean waitingRoom) { this.type = 2; // Scheduled Meeting this.start_time = DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH:mm:ss"); @@ -131,18 +132,25 @@ public interface ZoomRoomRequestResponse { this.timezone = DateTimeZone.UTC.getID(); this.topic = topic; this.password = password; - this.settings = new Settings(); + this.settings = new Settings(waitingRoom); } @JsonIgnoreProperties(ignoreUnknown = true) static class Settings { - @JsonProperty final boolean host_video = true; - @JsonProperty final boolean participant_video = true; - @JsonProperty final boolean join_before_host = true; + @JsonProperty final boolean host_video = false; + @JsonProperty final boolean participant_video = false; + @JsonProperty final boolean mute_upon_entry = true; + @JsonProperty final boolean join_before_host; @JsonProperty final int jbh_time = 0; @JsonProperty final boolean use_pmi = false; @JsonProperty final String audio = "voip"; - @JsonProperty final boolean waiting_room = false; + @JsonProperty final boolean waiting_room; + @JsonProperty final boolean allow_multiple_devices = false; + + public Settings(final boolean waitingRoom) { + this.join_before_host = !waitingRoom; + this.waiting_room = waitingRoom; + } } } diff --git a/src/main/resources/ch/ethz/seb/sebserver/gui/service/session/proctoring/zoomWindow.html b/src/main/resources/ch/ethz/seb/sebserver/gui/service/session/proctoring/zoomWindow.html index b0dd4266..d6a9de3b 100644 --- a/src/main/resources/ch/ethz/seb/sebserver/gui/service/session/proctoring/zoomWindow.html +++ b/src/main/resources/ch/ethz/seb/sebserver/gui/service/session/proctoring/zoomWindow.html @@ -99,6 +99,9 @@ }) window.addEventListener('unload', () => { + ZoomMtg.muteAll({ + muteAll: true + }); ZoomMtg.endMeeting({}); }); diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 09c103a8..ecda90f4 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -666,6 +666,9 @@ sebserver.exam.proctoring.form.features.TOWN_HALL=Town-Hall Room sebserver.exam.proctoring.form.features.ONE_TO_ONE=One to One Room sebserver.exam.proctoring.form.features.BROADCAST=Broadcasting Feature sebserver.exam.proctoring.form.features.ENABLE_CHAT=Chat Feature +sebserver.exam.proctoring.form.features.WAITING_ROOM=Enable waiting room for collecting rooms +sebserver.exam.proctoring.form.features.SEND_REJOIN_COLLECTING_ROOM=Force rejoin for collecting rooms +sebserver.exam.proctoring.form.features.RESET_BROADCAST_ON_LAVE=Reset broadcast on leave sebserver.exam.proctoring.type.servertype.JITSI_MEET=Jitsi Meet Server sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet server for proctoring diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolverTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolverTest.java index 17d4a852..aeb42f99 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolverTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolverTest.java @@ -166,6 +166,9 @@ public class ZoomWindowScriptResolverTest { + " })\r\n" + " \r\n" + " window.addEventListener('unload', () => {\r\n" + + " ZoomMtg.muteAll({\r\n" + + " muteAll: true\r\n" + + " });\r\n" + " ZoomMtg.endMeeting({});\r\n" + " });\r\n" + " \r\n"