diff --git a/src/main/java/org/olat/core/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/core/_i18n/LocalStrings_de.properties index a7787009158d1e56333db40843439c0f4370337f..60a1ad7e864586bdc8c0baa3638c40ad5c0d2598 100644 --- a/src/main/java/org/olat/core/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/core/_i18n/LocalStrings_de.properties @@ -126,6 +126,7 @@ warn.beta.feature=Achtung\! Diese Funktion befindet sich in einer Versuchsphase. warn.header=Achtung warn.notdispatched=Diese Seite wurde ver\u00E4ndert. Bitte beachten Sie allf\u00E4llige Meldungen. warn.reload=Bitte benutzen Sie nicht den `Neu Laden` oder `Zur\u00FCck` Button Ihres Browsers. -warning.invalid.csrf=CSRF mismatch +warning.invalid.csrf=CSRF mismatch +warning.multi.window=Sie haben mehrmals die gleiche Fenster ge\u00F6ffnet. welcome=Willkommen yes=Ja diff --git a/src/main/java/org/olat/core/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/core/_i18n/LocalStrings_en.properties index ba69474cfe0159068080d62eed1d5e7570b2df65..1d8c26e971993a58575531a57ce5311700275057 100644 --- a/src/main/java/org/olat/core/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/core/_i18n/LocalStrings_en.properties @@ -126,6 +126,7 @@ warn.beta.feature=Attention\! This is a beta feature. Be aware that using this f warn.header=Warning warn.notdispatched=This page has been modified. Please consider any new messages. warn.reload=Please do not use the `Reload` or `Back` button of your browser. -warning.invalid.csrf=CSRF mismatch +warning.invalid.csrf=CSRF mismatch +warning.multi.window=You have opened several same windows. welcome=Welcome yes=Yes diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/elements/FormLink.java b/src/main/java/org/olat/core/gui/components/form/flexible/elements/FormLink.java index b0b5f1f3e753fe6aee8a82bb319eef9082c0828a..616a6916baf1dcf3d31ea489617faa6258cd3b7e 100644 --- a/src/main/java/org/olat/core/gui/components/form/flexible/elements/FormLink.java +++ b/src/main/java/org/olat/core/gui/components/form/flexible/elements/FormLink.java @@ -117,6 +117,8 @@ public interface FormLink extends FormItem { public String getLinkTitleText(); + public void setUrl(String url); + /** * * @return The title of the link if disabled. diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/FormLinkImpl.java b/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/FormLinkImpl.java index e5ba141908fe8bae80388d23317829603162e7b6..3b8913c9b9f827208f736223e9b69f7521896d67 100644 --- a/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/FormLinkImpl.java +++ b/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/FormLinkImpl.java @@ -60,6 +60,7 @@ public class FormLinkImpl extends FormItemImpl implements FormLink { private int presentation = Link.LINK; private String i18n; private String cmd; + private String url; private boolean hasCustomEnabledCss = false; private boolean hasCustomDisabledCss = false; private boolean domReplacementWrapperRequired = false; @@ -226,6 +227,7 @@ public class FormLinkImpl extends FormItemImpl implements FormLink { component.setForceFlexiDirtyFormWarning(ownDirtyFormWarning); component.setPopup(popup); component.setNewWindow(newWindow); + component.setUrl(url); if(textReasonForDisabling != null) { component.setTextReasonForDisabling(textReasonForDisabling); } @@ -265,6 +267,14 @@ public class FormLinkImpl extends FormItemImpl implements FormLink { public String getCmd() { return cmd; } + + @Override + public void setUrl(String url) { + this.url = url; + if(component != null) { + component.setUrl(url); + } + } @Override public void setTranslator(Translator translator) { diff --git a/src/main/java/org/olat/core/gui/components/link/LinkRenderer.java b/src/main/java/org/olat/core/gui/components/link/LinkRenderer.java index 0761f9234d63e889ccec4e084798f7a329db9766..6dafc3b23f2612069bc5d48c2e5f362dbd4c78d4 100644 --- a/src/main/java/org/olat/core/gui/components/link/LinkRenderer.java +++ b/src/main/java/org/olat/core/gui/components/link/LinkRenderer.java @@ -189,7 +189,8 @@ public class LinkRenderer extends DefaultComponentRenderer { .append(";\" ") .append("onclick=\"return o2cl_dirtyCheckOnly();\" "); } else { - sb.append("href=\"javascript:;\" onclick=\"") + String href = StringHelper.containsNonWhitespace(link.getUrl()) ? link.getUrl() : "javascript:;"; + sb.append("href=\"").append(href).append("\" onclick=\"") .append(FormJSHelper.getJSFnCallFor(flexiLink.getRootForm(), elementId, 1)) .append(";"); sb.append("\" "); diff --git a/src/main/java/org/olat/core/id/context/StateMapped.java b/src/main/java/org/olat/core/id/context/StateMapped.java index b530b8fd6c5da625890036f34cc830dcdd88c90b..954fe7524e9cd5e96638df51899c6dee81c1653c 100644 --- a/src/main/java/org/olat/core/id/context/StateMapped.java +++ b/src/main/java/org/olat/core/id/context/StateMapped.java @@ -51,6 +51,11 @@ public class StateMapped implements StateEntry{ this.delegate = delegate; } + @Override + public int hashCode() { + return delegate == null ? 721567 : delegate.hashCode(); + } + @Override public String toString() { return delegate == null ? "{empty}" : delegate.toString(); diff --git a/src/main/java/org/olat/core/util/UserSession.java b/src/main/java/org/olat/core/util/UserSession.java index e4e84c785bca0a56db33a936a7a42bea58715dbd..ef3ecd5100aa7eee0478001861050ac5b51e9909 100644 --- a/src/main/java/org/olat/core/util/UserSession.java +++ b/src/main/java/org/olat/core/util/UserSession.java @@ -60,6 +60,7 @@ import org.olat.core.util.event.GenericEventListener; import org.olat.core.util.prefs.Preferences; import org.olat.core.util.prefs.PreferencesFactory; import org.olat.core.util.resource.OresHelper; +import org.olat.core.util.resource.WindowedResourceableList; import org.olat.core.util.session.UserSessionManager; import org.olat.course.assessment.model.TransientAssessmentMode; @@ -83,6 +84,8 @@ public class UserSession implements HttpSessionBindingListener, GenericEventList private TransientAssessmentMode lockMode; private List<TransientAssessmentMode> assessmentModes; + private transient final WindowedResourceableList resourceList = new WindowedResourceableList(); + private transient Map<String,Object> store; /** * things to put into that should not be clear when signing on (e.g. remember url for a direct jump) @@ -452,11 +455,15 @@ public class UserSession implements HttpSessionBindingListener, GenericEventList } } } + + public WindowedResourceableList getResourceList() { + return resourceList; + } @Override public void valueBound(HttpSessionBindingEvent be) { if (log.isDebugEnabled()) { - log.debug("Opened UserSession:" + toString()); + log.debug("Opened UserSession: {}", this); } } @@ -472,9 +479,7 @@ public class UserSession implements HttpSessionBindingListener, GenericEventList // (no user was authenticated yet but a tomcat session was created) Identity ident = identityEnvironment.getIdentity(); CoreSpringFactory.getImpl(UserSessionManager.class).signOffAndClear(this); - if (log.isDebugEnabled()) { - log.debug("Closed UserSession: identity = " + (ident == null ? "n/a" : ident.getKey())); - } + log.debug("Closed UserSession: identity = {}", (ident == null ? "n/a" : ident.getKey())); //we do not have a request in the null case (app. server triggered) and user not yet logged in //-> in this case we use the special empty activity logger if (ident == null) { @@ -495,15 +500,11 @@ public class UserSession implements HttpSessionBindingListener, GenericEventList */ @Override public void event(Event event) { - //fxdiff FXOLAT-231: event on GUI Preferences extern changes if("preferences.changed".equals(event.getCommand())) { reloadPreferences(); } } - /** - * @see java.lang.Object#toString() - */ @Override public String toString() { return "Session of " + identityEnvironment + ", " + super.toString(); diff --git a/src/main/java/org/olat/core/util/resource/WindowedResourceable.java b/src/main/java/org/olat/core/util/resource/WindowedResourceable.java new file mode 100644 index 0000000000000000000000000000000000000000..c752095beee8a13890ef3ef4600ce1d7d00dd919 --- /dev/null +++ b/src/main/java/org/olat/core/util/resource/WindowedResourceable.java @@ -0,0 +1,59 @@ +package org.olat.core.util.resource; + +import org.olat.core.id.OLATResourceable; + +/** + * + * Initial date: 2 juil. 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class WindowedResourceable { + + private final String windowId; + private final String subIdent; + private final OLATResourceable resource; + + public WindowedResourceable(String windowId, OLATResourceable resource, String subIdent) { + this.windowId = windowId; + this.subIdent = subIdent; + this.resource = resource; + } + + public String getWindowId() { + return windowId; + } + + public OLATResourceable getResource() { + return resource; + } + + public String getSubIdent() { + return subIdent; + } + + public boolean matchResourceOnDifferentWindow(WindowedResourceable wResource) { + return OresHelper.equals(resource, wResource.getResource()) + && ((subIdent == null && wResource.getSubIdent() == null) || (subIdent != null && subIdent.equals(wResource.getSubIdent()))) + && (windowId == null || !windowId.equals(wResource.getWindowId())); + } + + @Override + public int hashCode() { + return windowId.hashCode() + resource.getResourceableId().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if(this == obj) { + return true; + } + if(obj instanceof WindowedResourceable) { + WindowedResourceable wr = (WindowedResourceable)obj; + return windowId != null && windowId.equals(wr.getWindowId()) + && ((subIdent == null && wr.getSubIdent() == null) || (subIdent != null && subIdent.equals(wr.getSubIdent()))) + && resource != null && OresHelper.equals(resource, wr.getResource()); + } + return false; + } +} diff --git a/src/main/java/org/olat/core/util/resource/WindowedResourceableList.java b/src/main/java/org/olat/core/util/resource/WindowedResourceableList.java new file mode 100644 index 0000000000000000000000000000000000000000..5300c53c9d1e18b8f8d1bcd5730138c190293894 --- /dev/null +++ b/src/main/java/org/olat/core/util/resource/WindowedResourceableList.java @@ -0,0 +1,50 @@ +package org.olat.core.util.resource; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; + +import org.olat.core.gui.components.Window; +import org.olat.core.id.OLATResourceable; + +/** + * + * Initial date: 2 juil. 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class WindowedResourceableList { + + private final Deque<WindowedResourceable> registeredResources = new ArrayDeque<>(); + + public synchronized boolean registerResourceable(OLATResourceable resource, String subIdent, Window window) { + WindowedResourceable wResource = new WindowedResourceable(window.getInstanceId(), resource, subIdent); + + boolean uniqueResource = true; + + boolean add = true; + for(WindowedResourceable registeredResource:registeredResources) { + if(registeredResource.equals(wResource)) { + uniqueResource = false; + add = false; + break; + } else if(registeredResource.matchResourceOnDifferentWindow(wResource)) { + uniqueResource = false; + } + } + + if(add) { + registeredResources.add(wResource); + } + + return uniqueResource; + } + + public synchronized void deregisterResourceable(OLATResourceable resource, String subIdent, Window window) { + WindowedResourceable wResource = new WindowedResourceable(window.getInstanceId(), resource, subIdent); + Collection<WindowedResourceable> wResources = Collections.singletonList(wResource); + registeredResources.removeAll(wResources); + } + +} diff --git a/src/main/java/org/olat/course/nodes/iq/QTI21AssessmentRunController.java b/src/main/java/org/olat/course/nodes/iq/QTI21AssessmentRunController.java index 778078c1ebef279a0ee3392084898573f7d526ff..17f228aabca39ca907712d214fd75a2e15d67629 100644 --- a/src/main/java/org/olat/course/nodes/iq/QTI21AssessmentRunController.java +++ b/src/main/java/org/olat/course/nodes/iq/QTI21AssessmentRunController.java @@ -56,6 +56,7 @@ import org.olat.core.util.event.GenericEventListener; import org.olat.core.util.mail.MailBundle; import org.olat.core.util.prefs.Preferences; import org.olat.core.util.resource.OresHelper; +import org.olat.core.util.resource.WindowedResourceableList; import org.olat.core.util.vfs.VFSContainer; import org.olat.course.CourseModule; import org.olat.course.DisposedCourseRestartController; @@ -138,6 +139,7 @@ public class QTI21AssessmentRunController extends BasicController implements Gen private final QTI21OverrideOptions overrideOptions; // The test is really assessment not a self test or a survey private final boolean assessmentType = true; + private final WindowedResourceableList resourceList; private AtomicBoolean incrementAttempts = new AtomicBoolean(true); private AssessmentResultController resultCtrl; @@ -172,6 +174,12 @@ public class QTI21AssessmentRunController extends BasicController implements Gen mainVC = createVelocityContainer("assessment_run"); mainVC.setDomReplaceable(false); // DOM ID set in velocity + resourceList = userSession.getResourceList(); + if(!resourceList.registerResourceable(userCourseEnv.getCourseEnvironment().getCourseGroupManager().getCourseEntry(), + courseNode.getIdent(), getWindow())) { + showWarning("warning.multi.window"); + } + disclaimerVC = createVelocityContainer("assessment_disclaimer"); disclaimerVC.setDomReplacementWrapperRequired(false); mainVC.put("disclaimer", disclaimerVC); @@ -651,6 +659,9 @@ public class QTI21AssessmentRunController extends BasicController implements Gen singleUserEventCenter.fireEventToListenersOf(assessmentStoppedEvent, assessmentEventOres); } } + + resourceList.deregisterResourceable(userCourseEnv.getCourseEnvironment().getCourseGroupManager().getCourseEntry(), + courseNode.getIdent(), getWindow()); } @Override diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java index d0aea891e5c466122ca8bd123c3fe2e1e612811a..53ca58e8b30cfe438a9766f9dae01c02be8e5310 100644 --- a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java +++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java @@ -68,6 +68,7 @@ import org.olat.core.util.UserSession; import org.olat.core.util.coordinate.CoordinatorManager; import org.olat.core.util.event.GenericEventListener; import org.olat.core.util.resource.OresHelper; +import org.olat.core.util.resource.WindowedResourceableList; import org.olat.fileresource.FileResourceManager; import org.olat.fileresource.types.ImsQTI21Resource; import org.olat.fileresource.types.ImsQTI21Resource.PathResourceLocator; @@ -188,6 +189,7 @@ public class AssessmentTestDisplayController extends BasicController implements private RepositoryEntry testEntry; private RepositoryEntry entry; private String subIdent; + private final boolean authorMode; private final boolean anonym; private final Identity assessedIdentity; @@ -201,6 +203,7 @@ public class AssessmentTestDisplayController extends BasicController implements private final boolean showCloseResults; private OutcomesListener outcomesListener; private AssessmentSessionAuditLogger candidateAuditLogger; + private final WindowedResourceableList resourcesList; @Autowired private QTI21Module qtiModule; @@ -235,6 +238,7 @@ public class AssessmentTestDisplayController extends BasicController implements this.deliveryOptions = deliveryOptions; this.overrideOptions = overrideOptions; this.showCloseResults = showCloseResults; + this.authorMode = authorMode; UserSession usess = ureq.getUserSession(); if(usess.getRoles().isGuestOnly() || anonym) { @@ -252,6 +256,11 @@ public class AssessmentTestDisplayController extends BasicController implements // within course is this task delegated to the QTI21AssessmentRunController addLoggingResourceable(LoggingResourceable.wrapTest(entry)); } + + resourcesList = usess.getResourceList(); + if(subIdent == null && !authorMode && !resourcesList.registerResourceable(entry, subIdent, getWindow())) { + showWarning("warning.multi.window"); + } FileResourceManager frm = FileResourceManager.getInstance(); fUnzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource()); @@ -402,6 +411,10 @@ public class AssessmentTestDisplayController extends BasicController implements @Override protected void doDispose() { + if(subIdent == null && !authorMode) { + resourcesList.deregisterResourceable(entry, subIdent, getWindow()); + } + suspendAssessmentTest(new Date()); if(candidateSession != null) { OLATResourceable sessionOres = OresHelper diff --git a/src/main/java/org/olat/repository/ui/list/OverviewRepositoryListController.java b/src/main/java/org/olat/repository/ui/list/OverviewRepositoryListController.java index 13f48dd4c21bedb2cf0ca2c692fb993a9ef3350d..e5d11a0a41f5bf6acee96b46849c9ec9cb024f2b 100644 --- a/src/main/java/org/olat/repository/ui/list/OverviewRepositoryListController.java +++ b/src/main/java/org/olat/repository/ui/list/OverviewRepositoryListController.java @@ -69,6 +69,8 @@ import org.springframework.beans.factory.annotation.Autowired; */ public class OverviewRepositoryListController extends BasicController implements Activateable2, GenericEventListener { + private static final String OVERVIEW_PATH = "[MyCoursesSite:0]"; + private final VelocityContainer mainVC; private final SegmentViewComponent segmentView; private final Link myCourseLink; @@ -123,31 +125,43 @@ public class OverviewRepositoryListController extends BasicController implements if(!isGuestOnly) { favoriteLink = LinkFactory.createLink("search.mark", mainVC, this); favoriteLink.setElementCssClass("o_sel_mycourses_fav"); + favoriteLink.setUrl(BusinessControlFactory.getInstance() + .getAuthenticatedURLFromBusinessPathStrings(OVERVIEW_PATH, "[Favorits:0]")); segmentView.addSegment(favoriteLink, false); } myCourseLink = LinkFactory.createLink("search.mycourses.student", mainVC, this); + myCourseLink.setUrl(BusinessControlFactory.getInstance() + .getAuthenticatedURLFromBusinessPathStrings(OVERVIEW_PATH, "[My:0]")); myCourseLink.setElementCssClass("o_sel_mycourses_my"); segmentView.addSegment(myCourseLink, false); withCurriculums = withCurriculumTab(); if(withCurriculums) { curriculumLink = LinkFactory.createLink("search.curriculums", mainVC, this); + curriculumLink.setUrl(BusinessControlFactory.getInstance() + .getAuthenticatedURLFromBusinessPathStrings(OVERVIEW_PATH, "[Curriculum:0]")); curriculumLink.setElementCssClass("o_sel_mycurriculums"); segmentView.addSegment(curriculumLink, false); } closedCourseLink = LinkFactory.createLink("search.courses.closed", mainVC, this); + closedCourseLink.setUrl(BusinessControlFactory.getInstance() + .getAuthenticatedURLFromBusinessPathStrings(OVERVIEW_PATH, "[Closed:0]")); closedCourseLink.setElementCssClass("o_sel_mycourses_closed"); segmentView.addSegment(closedCourseLink, false); if(repositoryModule.isCatalogEnabled() && repositoryModule.isCatalogBrowsingEnabled()) { catalogLink = LinkFactory.createLink("search.catalog", mainVC, this); + catalogLink.setUrl(BusinessControlFactory.getInstance() + .getAuthenticatedURLFromBusinessPathStrings(OVERVIEW_PATH, "[Catalog:0]")); catalogLink.setElementCssClass("o_sel_mycourses_catlog"); segmentView.addSegment(catalogLink, false); } if(repositoryModule.isMyCoursesSearchEnabled()) { searchCourseLink = LinkFactory.createLink("search.courses.student", mainVC, this); + searchCourseLink.setUrl(BusinessControlFactory.getInstance() + .getAuthenticatedURLFromBusinessPathStrings(OVERVIEW_PATH, "[Search:0]")); searchCourseLink.setElementCssClass("o_sel_mycourses_search"); segmentView.addSegment(searchCourseLink, false); } diff --git a/src/main/java/org/olat/repository/ui/list/RepositoryEntryListController.java b/src/main/java/org/olat/repository/ui/list/RepositoryEntryListController.java index 97f521314f2b33c99a42c55363d1baa04bbbcf9d..85131d97689b5045915a0ae27483f1871694b0ab 100644 --- a/src/main/java/org/olat/repository/ui/list/RepositoryEntryListController.java +++ b/src/main/java/org/olat/repository/ui/list/RepositoryEntryListController.java @@ -680,8 +680,8 @@ public class RepositoryEntryListController extends FormBasicController selectLink.setIconLeftCSS("o_icon o_CourseModule_icon_closed"); } String businessPath = "[RepositoryEntry:" + row.getKey() + "]"; - String url = BusinessControlFactory.getInstance().getAuthenticatedURLFromBusinessPathString(businessPath); - //selectLink.getComponent().setUrl(url); + selectLink.setUrl(BusinessControlFactory.getInstance() + .getAuthenticatedURLFromBusinessPathString(businessPath)); selectLink.setUserObject(row); row.setSelectLink(selectLink); } @@ -701,6 +701,9 @@ public class RepositoryEntryListController extends FormBasicController startLink.setUserObject(row); startLink.setCustomEnabledLinkCSS(iconCss); startLink.setIconRightCSS("o_icon o_icon_start"); + String businessPath = "[RepositoryEntry:" + row.getKey() + "]"; + startLink.setUrl(BusinessControlFactory.getInstance() + .getAuthenticatedURLFromBusinessPathString(businessPath)); row.setStartLink(startLink); } @@ -709,6 +712,11 @@ public class RepositoryEntryListController extends FormBasicController FormLink detailsLink = uifactory.addFormLink("details_" + row.getKey(), "details", "details", null, null, Link.LINK); detailsLink.setCustomEnabledLinkCSS("o_details"); detailsLink.setUserObject(row); + if (row.isMember()) { + String businessPath = "[RepositoryEntry:" + row.getKey() + "][Infos:0]"; + detailsLink.setUrl(BusinessControlFactory.getInstance() + .getAuthenticatedURLFromBusinessPathString(businessPath)); + } row.setDetailsLink(detailsLink); }