From c458fb7a5d1f96c9e1b76b341cccd37b310fec0f Mon Sep 17 00:00:00 2001 From: srosse <none@none> Date: Tue, 11 Nov 2014 11:24:07 +0100 Subject: [PATCH] OO-1245: make the courses accessible via WebDAV to students --- .../calendar/CalendarWebDAVProvider.java | 6 +- .../filechooser/FileChooserController.java | 2 +- .../modules/bc/BriefcaseWebDAVProvider.java | 11 +- .../commons/services/webdav/WebDAVModule.java | 110 +++++++++-------- .../services/webdav/WebDAVProvider.java | 4 +- .../services/webdav/_spring/webdavContext.xml | 64 ++-------- .../webdav/manager/WebDAVAuthManager.java | 30 +---- .../webdav/manager/WebDAVManagerImpl.java | 55 +++------ .../manager/WebDAVProviderNamedContainer.java | 12 +- .../webdav/servlets/WebDAVDispatcherImpl.java | 31 +---- .../webdav/ui/WebDAVAdminController.java | 22 +++- .../ui/_i18n/LocalStrings_de.properties | 2 + .../ui/_i18n/LocalStrings_en.properties | 2 + .../java/org/olat/course/CourseFactory.java | 2 +- .../course/CoursefolderWebDAVMergeSource.java | 68 ++++++++--- .../CoursefolderWebDAVNamedContainer.java | 7 +- .../course/CoursefolderWebDAVProvider.java | 9 +- src/main/java/org/olat/course/ICourse.java | 3 + .../olat/course/MergedCourseContainer.java | 115 +++++++++++++++++- .../org/olat/course/PersistingCourseImpl.java | 27 +++- .../course/run/CourseRuntimeController.java | 2 +- .../group/GroupfoldersWebDAVProvider.java | 8 +- .../PersonalFolderControllerCreator.java | 4 +- .../SharedFolderWebDAVProvider.java | 7 +- .../olat/repository/RepositoryManager.java | 64 +++++++++- .../olat/repository/RepositoryService.java | 1 - .../RepositorySearchController.java | 2 +- .../manager/RepositoryEntryDAO.java | 3 +- .../user/restapi/UserCoursesWebService.java | 4 +- .../services/webdav/WebDAVCommandsTest.java | 87 ++++++++++++- .../commons/services/webdav/webdav_course.zip | Bin 0 -> 24513 bytes .../repository/RepositoryManagerTest.java | 39 +++++- .../manager/RepositoryEntryDAOTest.java | 7 ++ .../java/org/olat/test/JunitTestHelper.java | 26 ++++ .../olat/test/file_resources/Basic_course.zip | Bin 0 -> 14021 bytes 35 files changed, 573 insertions(+), 263 deletions(-) create mode 100644 src/test/java/org/olat/core/commons/services/webdav/webdav_course.zip create mode 100644 src/test/java/org/olat/test/file_resources/Basic_course.zip diff --git a/src/main/java/org/olat/commons/calendar/CalendarWebDAVProvider.java b/src/main/java/org/olat/commons/calendar/CalendarWebDAVProvider.java index 63a6f7f6d6b..28474840361 100644 --- a/src/main/java/org/olat/commons/calendar/CalendarWebDAVProvider.java +++ b/src/main/java/org/olat/commons/calendar/CalendarWebDAVProvider.java @@ -28,7 +28,7 @@ package org.olat.commons.calendar; import java.io.File; import org.olat.core.commons.services.webdav.WebDAVProvider; -import org.olat.core.id.Identity; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.util.vfs.LocalFileImpl; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VirtualContainer; @@ -38,12 +38,12 @@ public class CalendarWebDAVProvider implements WebDAVProvider { private static final String MOUNT_POINT = "calendars"; - public VFSContainer getContainer(Identity identity) { + public VFSContainer getContainer(IdentityEnvironment identityEnv) { VirtualContainer calendars = new VirtualContainer("calendars"); calendars.setLocalSecurityCallback(new ReadOnlyCallback()); // get private calendar CalendarManager calendarManager = CalendarManagerFactory.getInstance().getCalendarManager(); - File fPersonalCalendar = calendarManager.getCalendarICalFile(CalendarManager.TYPE_USER, identity.getName()); + File fPersonalCalendar = calendarManager.getCalendarICalFile(CalendarManager.TYPE_USER, identityEnv.getIdentity().getName()); calendars.addItem(new LocalFileImpl(fPersonalCalendar)); return calendars; } diff --git a/src/main/java/org/olat/commons/file/filechooser/FileChooserController.java b/src/main/java/org/olat/commons/file/filechooser/FileChooserController.java index c4a5cb6bc4f..7046f845c91 100644 --- a/src/main/java/org/olat/commons/file/filechooser/FileChooserController.java +++ b/src/main/java/org/olat/commons/file/filechooser/FileChooserController.java @@ -179,7 +179,7 @@ public class FileChooserController extends BasicController { } selectedContainer = null; if (selectedFolder == 0) { // personal folder - selectedContainer = PersonalFolderManager.getInstance().getContainer(ureq.getIdentity()); + selectedContainer = PersonalFolderManager.getInstance().getContainer(ureq.getUserSession().getIdentityEnvironment()); } else { // process other folders selectedContainer = containerRefs.get(selectedFolder - 1); } diff --git a/src/main/java/org/olat/core/commons/modules/bc/BriefcaseWebDAVProvider.java b/src/main/java/org/olat/core/commons/modules/bc/BriefcaseWebDAVProvider.java index 8111a26ee74..47dbbb2c85f 100644 --- a/src/main/java/org/olat/core/commons/modules/bc/BriefcaseWebDAVProvider.java +++ b/src/main/java/org/olat/core/commons/modules/bc/BriefcaseWebDAVProvider.java @@ -28,6 +28,7 @@ package org.olat.core.commons.modules.bc; import org.olat.core.commons.services.webdav.WebDAVProvider; import org.olat.core.id.Identity; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.util.vfs.VFSContainer; /** * @@ -39,13 +40,19 @@ public class BriefcaseWebDAVProvider implements WebDAVProvider { public String getMountPoint() { return MOUNTPOINT; } + + public VFSContainer getContainer(Identity identity) { + // merge /public and /private + return new BriefcaseWebDAVMergeSource(identity); + } /** * @see org.olat.core.commons.services.webdav.WebDAVProvider#getContainer(org.olat.core.id.Identity) */ - public VFSContainer getContainer(Identity identity) { + @Override + public VFSContainer getContainer(IdentityEnvironment identityEnv) { // merge /public and /private - return new BriefcaseWebDAVMergeSource(identity); + return getContainer(identityEnv.getIdentity()); } protected String getRootPathFor(Identity identity) { diff --git a/src/main/java/org/olat/core/commons/services/webdav/WebDAVModule.java b/src/main/java/org/olat/core/commons/services/webdav/WebDAVModule.java index ec0b089f5cd..1400accfc9b 100644 --- a/src/main/java/org/olat/core/commons/services/webdav/WebDAVModule.java +++ b/src/main/java/org/olat/core/commons/services/webdav/WebDAVModule.java @@ -26,34 +26,47 @@ package org.olat.core.commons.services.webdav; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.olat.core.configuration.AbstractOLATModule; +import org.olat.core.configuration.AbstractSpringModule; import org.olat.core.configuration.ConfigOnOff; -import org.olat.core.configuration.PersistedProperties; -import org.olat.core.logging.AssertException; -import org.olat.core.logging.OLog; -import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; +import org.olat.core.util.coordinate.CoordinatorManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; -public class WebDAVModule extends AbstractOLATModule implements ConfigOnOff { - - private static final OLog log = Tracing.createLoggerFor(WebDAVModule.class); +@Service("webdavModule") +public class WebDAVModule extends AbstractSpringModule implements ConfigOnOff { private static final String WEBDAV_ENABLED = "webdav.enabled"; private static final String WEBDAV_LINKS_ENABLED = "webdav.links.enabled"; private static final String DIGEST_AUTH_ENABLED = "auth.digest.enabled"; private static final String TERMS_FOLDERS_ENABLED = "webdav.termsfolders.enabled"; + private static final String LEARNERS_BOOKMARKS_COURSE = "webdav.learners.bookmarks.courses"; + private static final String LEARNERS_PARTICIPATING_COURSES = "webdav.learners.participating.courses"; - private Map<String, WebDAVProvider> webdavProviders; + @Autowired + private List<WebDAVProvider> webdavProviders; + @Value("${webdav.enabled:true}") private boolean enabled; + @Value("${webdav.links.enabled:true}") private boolean linkEnabled; + @Value("${auth.digest.enabled:true}") private boolean digestAuthenticationEnabled; + @Value("${webdav.termsfolders.enabled:true}") private boolean termsFoldersEnabled; + + private boolean enableLearnersBookmarksCourse; + private boolean enableLearnersParticipatingCourses; + + @Autowired + public WebDAVModule(CoordinatorManager coordinatorManager) { + super(coordinatorManager); + } @Override public void init() { @@ -77,33 +90,25 @@ public class WebDAVModule extends AbstractOLATModule implements ConfigOnOff { if(StringHelper.containsNonWhitespace(termsFoldersEnabledObj)) { termsFoldersEnabled = "true".equals(termsFoldersEnabledObj); } - + + String learnersBookmarksCourseObj = getStringPropertyValue(LEARNERS_BOOKMARKS_COURSE, true); + enableLearnersBookmarksCourse = "true".equals(learnersBookmarksCourseObj); + String learnersParticipatingCoursesObj = getStringPropertyValue(LEARNERS_PARTICIPATING_COURSES, true); + enableLearnersParticipatingCourses = "true".equals(learnersParticipatingCoursesObj); } - @Override - protected void initDefaultProperties() { - enabled = getBooleanConfigParameter(WEBDAV_ENABLED, true); - linkEnabled = getBooleanConfigParameter(WEBDAV_LINKS_ENABLED, true); - digestAuthenticationEnabled = getBooleanConfigParameter(DIGEST_AUTH_ENABLED, true); - termsFoldersEnabled = getBooleanConfigParameter(TERMS_FOLDERS_ENABLED, true); - } - @Override protected void initFromChangedProperties() { init(); } - - @Override - public void setPersistedProperties(PersistedProperties persistedProperties) { - this.moduleConfigProperties = persistedProperties; - } - + @Override public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { + this.enabled = enabled; String enabledStr = enabled ? "true" : "false"; setStringProperty(WEBDAV_ENABLED, enabledStr, true); } @@ -113,6 +118,7 @@ public class WebDAVModule extends AbstractOLATModule implements ConfigOnOff { } public void setLinkEnabled(boolean linkEnabled) { + this.linkEnabled = linkEnabled; String enabledStr = linkEnabled ? "true" : "false"; setStringProperty(WEBDAV_LINKS_ENABLED, enabledStr, true); } @@ -122,52 +128,50 @@ public class WebDAVModule extends AbstractOLATModule implements ConfigOnOff { } public void setDigestAuthenticationEnabled(boolean digestAuthenticationEnabled) { + this.digestAuthenticationEnabled = digestAuthenticationEnabled; String enabledStr = digestAuthenticationEnabled ? "true" : "false"; setStringProperty(DIGEST_AUTH_ENABLED, enabledStr, true); } + public boolean isTermsFoldersEnabled() { + return termsFoldersEnabled; + } public void setTermsFoldersEnabled(boolean termsFoldersEnabled) { + this.termsFoldersEnabled = termsFoldersEnabled; String enabledStr = termsFoldersEnabled ? "true" : "false"; setStringProperty(TERMS_FOLDERS_ENABLED, enabledStr, true); } - public boolean isTermsFoldersEnabled() { - return termsFoldersEnabled; + public boolean isEnableLearnersBookmarksCourse() { + return enableLearnersBookmarksCourse; } + public void setEnableLearnersBookmarksCourse(boolean enabled) { + this.enableLearnersBookmarksCourse = enabled; + setStringProperty(LEARNERS_BOOKMARKS_COURSE, enabled ? "true" : "false", true); + } + + public boolean isEnableLearnersParticipatingCourses() { + return enableLearnersParticipatingCourses; + } + + public void setEnableLearnersParticipatingCourses(boolean enabled) { + this.enableLearnersParticipatingCourses = enabled; + setStringProperty(LEARNERS_PARTICIPATING_COURSES, enabled ? "true" : "false", true); + } /** * Return an unmodifiable map * @return */ public Map<String, WebDAVProvider> getWebDAVProviders() { - return Collections.unmodifiableMap(webdavProviders); - } - - /** - * Set the list of webdav providers. - * @param webdavProviders - */ - public void setWebdavProviderList(List<WebDAVProvider> webdavProviders) { - if (webdavProviders == null) return;//nothing to do - - for (WebDAVProvider provider : webdavProviders) { - addWebdavProvider(provider); - } - } - - /** - * Add a new webdav provider. - * @param provider - */ - public void addWebdavProvider(WebDAVProvider provider) { - if (webdavProviders == null) { - webdavProviders = new HashMap<String, WebDAVProvider>(); + Map<String,WebDAVProvider> providerMap = new HashMap<>(); + if(webdavProviders != null) { + for(WebDAVProvider webdavProvider:webdavProviders) { + providerMap.put(webdavProvider.getMountPoint(), webdavProvider); + } } - if (webdavProviders.containsKey(provider.getMountPoint())) - throw new AssertException("May not add two providers with the same mount point."); - webdavProviders.put(provider.getMountPoint(), provider); - log.info("Adding webdav mountpoint '" + provider.getMountPoint() + "'."); + return providerMap; } } \ No newline at end of file diff --git a/src/main/java/org/olat/core/commons/services/webdav/WebDAVProvider.java b/src/main/java/org/olat/core/commons/services/webdav/WebDAVProvider.java index 010e902f966..e0901275e19 100644 --- a/src/main/java/org/olat/core/commons/services/webdav/WebDAVProvider.java +++ b/src/main/java/org/olat/core/commons/services/webdav/WebDAVProvider.java @@ -26,7 +26,7 @@ package org.olat.core.commons.services.webdav; -import org.olat.core.id.Identity; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.util.vfs.VFSContainer; public interface WebDAVProvider { @@ -41,6 +41,6 @@ public interface WebDAVProvider { * @param identity * @return */ - public VFSContainer getContainer(Identity identity); + public VFSContainer getContainer(IdentityEnvironment identityEnv); } diff --git a/src/main/java/org/olat/core/commons/services/webdav/_spring/webdavContext.xml b/src/main/java/org/olat/core/commons/services/webdav/_spring/webdavContext.xml index d10b1624773..17cae7aa32e 100644 --- a/src/main/java/org/olat/core/commons/services/webdav/_spring/webdavContext.xml +++ b/src/main/java/org/olat/core/commons/services/webdav/_spring/webdavContext.xml @@ -1,64 +1,18 @@ <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans - http://www.springframework.org/schema/beans/spring-beans.xsd"> - - <!-- WebDAV module --> - <bean id="webdavModule" class="org.olat.core.commons.services.webdav.WebDAVModule" depends-on="org.olat.core.util.WebappHelper"> - <property name="persistedProperties"> - <bean class="org.olat.core.configuration.PersistedProperties" scope="prototype" init-method="init" destroy-method="destroy" - depends-on="coordinatorManager,org.olat.core.util.WebappHelper"> - <constructor-arg index="0" ref="coordinatorManager"/> - <constructor-arg index="1" ref="webdavModule" /> - </bean> - </property> - <property name="webdavProviderList"> - <list> - <ref bean="webdav_briefcase"/> - <ref bean="webdav_coursefolders"/> - <ref bean="webdav_sharedfolders"/> - <ref bean="webdav_groupfolders"/> - </list> - </property> - </bean> + http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context + http://www.springframework.org/schema/context/spring-context.xsd"> - <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> - <property name="targetObject" ref="webdavModule" /> - <property name="targetMethod" value="init" /> - <property name="arguments"> - <value> - webdav.enabled=${webdav.enabled} - webdav.links.enabled=${webdav.links.enabled} - auth.digest.enabled=${auth.digest.enabled} - webdav.termsfolders.enabled=${webdav.termsfolders.enabled} - </value> - </property> - </bean> - - <bean id="webDAVManager" class="org.olat.core.commons.services.webdav.manager.WebDAVManagerImpl" init-method="init"> - <constructor-arg ref="coordinatorManager"/> - <property name="sessionManager" ref="userSessionManager" /> - <property name="webDAVAuthManager" ref="webDAVAuthenticationSpi" /> - <property name="webDAVModule" ref="webdavModule" /> - </bean> - - <bean id="webDAVDispatcher" class="org.olat.core.commons.services.webdav.servlets.WebDAVDispatcherImpl"> - <property name="lockManager" ref="vfsLockManager" /> - <property name="webDAVManager" ref="webDAVManager" /> - <property name="webDAVModule" ref="webdavModule" /> - </bean> - - <bean id="webDAVAuthenticationSpi" class="org.olat.core.commons.services.webdav.manager.WebDAVAuthManager" > - <property name="securityManager" ref="baseSecurityManager" /> - <property name="olatAuthenticationSpi" ref="olatAuthenticationSpi" /> - <property name="webDAVModule" ref="webdavModule" /> - </bean> + <context:component-scan base-package="org.olat.core.commons.services.webdav" /> - <bean id="webdav_briefcase" class="org.olat.core.commons.modules.bc.BriefcaseWebDAVProvider" scope="prototype" /> - <bean id="webdav_coursefolders" class="org.olat.course.CoursefolderWebDAVProvider" scope="prototype" /> - <bean id="webdav_sharedfolders" class="org.olat.modules.sharedfolder.SharedFolderWebDAVProvider" scope="prototype" > + <bean id="webdav_briefcase" class="org.olat.core.commons.modules.bc.BriefcaseWebDAVProvider" /> + <bean id="webdav_coursefolders" class="org.olat.course.CoursefolderWebDAVProvider" /> + <bean id="webdav_sharedfolders" class="org.olat.modules.sharedfolder.SharedFolderWebDAVProvider" > <!-- Optional configuration: specify shared folder that should be visible to normal users. By default, shared folders are only mounted for shared folder owners (read/write). By @@ -84,7 +38,7 @@ </property> --> </bean> - <bean id="webdav_groupfolders" class="org.olat.group.GroupfoldersWebDAVProvider" scope="prototype"> + <bean id="webdav_groupfolders" class="org.olat.group.GroupfoldersWebDAVProvider"> <property name="collaborationManager" ref="collaborationManager" /> </bean> diff --git a/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVAuthManager.java b/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVAuthManager.java index b7d781086df..6a7fd75d551 100644 --- a/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVAuthManager.java +++ b/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVAuthManager.java @@ -32,6 +32,8 @@ import org.olat.core.util.Encoder.Algorithm; import org.olat.login.LoginModule; import org.olat.login.auth.AuthenticationSPI; import org.olat.login.auth.OLATAuthManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; /** @@ -43,6 +45,7 @@ import org.olat.login.auth.OLATAuthManager; * Initial Date: 13 apr. 2010 <br> * @author srosse, stephane.rosse@frentix.com */ +@Service("webDAVAuthenticationSpi") public class WebDAVAuthManager implements AuthenticationSPI { public static final String PROVIDER_WEBDAV = "WEBDAV"; @@ -50,34 +53,13 @@ public class WebDAVAuthManager implements AuthenticationSPI { private static final OLog log = Tracing.createLoggerFor(WebDAVAuthManager.class); + @Autowired private WebDAVModule webDAVModule; + @Autowired private BaseSecurity securityManager; + @Autowired private OLATAuthManager olatAuthenticationSpi; - /** - * [used by Spring] - * @param webDAVModule - */ - public void setWebDAVModule(WebDAVModule webDAVModule) { - this.webDAVModule = webDAVModule; - } - - /** - * [used by Spring] - * @param securityManager - */ - public void setSecurityManager(BaseSecurity securityManager) { - this.securityManager = securityManager; - } - - /** - * [used by Spring] - * @param olatAuthenticationSpi - */ - public void setOlatAuthenticationSpi(OLATAuthManager olatAuthenticationSpi) { - this.olatAuthenticationSpi = olatAuthenticationSpi; - } - public Identity digestAuthentication(String httpMethod, DigestAuthentication digestAuth) { String username = digestAuth.getUsername(); diff --git a/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVManagerImpl.java b/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVManagerImpl.java index 91cdde54f83..c9d042f2246 100644 --- a/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVManagerImpl.java +++ b/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVManagerImpl.java @@ -43,6 +43,7 @@ import org.olat.core.commons.services.webdav.WebDAVProvider; import org.olat.core.commons.services.webdav.servlets.WebResourceRoot; import org.olat.core.helpers.Settings; import org.olat.core.id.Identity; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.id.Roles; import org.olat.core.id.User; import org.olat.core.id.UserConstants; @@ -56,6 +57,9 @@ import org.olat.core.util.vfs.MergeSource; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VirtualContainer; import org.olat.core.util.vfs.callbacks.ReadOnlyCallback; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; /** * Initial Date: 16.04.2003 @@ -66,50 +70,29 @@ import org.olat.core.util.vfs.callbacks.ReadOnlyCallback; * Comment: * */ -public class WebDAVManagerImpl implements WebDAVManager { +@Service("webDAVManager") +public class WebDAVManagerImpl implements WebDAVManager, InitializingBean { private static boolean enabled = true; public static final String BASIC_AUTH_REALM = "OLAT WebDAV Access"; private CoordinatorManager coordinatorManager; private CacheWrapper<CacheKey,UserSession> timedSessionCache; - + + @Autowired private UserSessionManager sessionManager; + @Autowired private WebDAVAuthManager webDAVAuthManager; + @Autowired private WebDAVModule webdavModule; - /** - * [spring] - */ - private WebDAVManagerImpl(CoordinatorManager coordinatorManager) { + @Autowired + public WebDAVManagerImpl(CoordinatorManager coordinatorManager) { this.coordinatorManager = coordinatorManager; } - /** - * [used by Spring] - * @param sessionManager - */ - public void setSessionManager(UserSessionManager sessionManager) { - this.sessionManager = sessionManager; - } - - /** - * [used by Spring] - * @param webDAVAuthManager - */ - public void setWebDAVAuthManager(WebDAVAuthManager webDAVAuthManager) { - this.webDAVAuthManager = webDAVAuthManager; - } - - /** - * [used by Spring] - * @param webdavModule - */ - public void setWebDAVModule(WebDAVModule webdavModule) { - this.webdavModule = webdavModule; - } - - public void init() { + @Override + public void afterPropertiesSet() throws Exception { timedSessionCache = coordinatorManager.getCoordinator().getCacher().getCache(WebDAVManager.class.getSimpleName(), "webdav"); } @@ -126,15 +109,15 @@ public class WebDAVManagerImpl implements WebDAVManager { return fdc; } - Identity identity = usess.getIdentity(); - VFSContainer webdavContainer = getMountableRoot(identity); + IdentityEnvironment identityEnv = usess.getIdentityEnvironment(); + VFSContainer webdavContainer = getMountableRoot(identityEnv); //create the / folder VirtualContainer rootContainer = new VirtualContainer(""); rootContainer.addItem(webdavContainer); rootContainer.setLocalSecurityCallback(new ReadOnlyCallback()); - fdc = new VFSResourceRoot(identity, rootContainer); + fdc = new VFSResourceRoot(identityEnv.getIdentity(), rootContainer); usess.putEntry("_DIRCTX", fdc); return fdc; } @@ -143,11 +126,11 @@ public class WebDAVManagerImpl implements WebDAVManager { * Returns a mountable root containing all entries which will be exposed to the webdav mount. * @return */ - private VFSContainer getMountableRoot(Identity identity) { + private VFSContainer getMountableRoot(IdentityEnvironment identityEnv) { MergeSource vfsRoot = new MergeSource(null, "webdav"); for (Map.Entry<String, WebDAVProvider> entry : webdavModule.getWebDAVProviders().entrySet()) { WebDAVProvider provider = entry.getValue(); - vfsRoot.addContainer(new WebDAVProviderNamedContainer(identity, provider)); + vfsRoot.addContainer(new WebDAVProviderNamedContainer(identityEnv, provider)); } return vfsRoot; } diff --git a/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVProviderNamedContainer.java b/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVProviderNamedContainer.java index d1d9589fdaf..f1dfd83c336 100644 --- a/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVProviderNamedContainer.java +++ b/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVProviderNamedContainer.java @@ -20,7 +20,7 @@ package org.olat.core.commons.services.webdav.manager; import org.olat.core.commons.services.webdav.WebDAVProvider; -import org.olat.core.id.Identity; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.util.vfs.NamedContainerImpl; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.filters.VFSItemFilter; @@ -31,25 +31,25 @@ import org.olat.core.util.vfs.filters.VFSItemFilter; */ public class WebDAVProviderNamedContainer extends NamedContainerImpl { - private Identity identity; + private IdentityEnvironment identityEnv; private final WebDAVProvider provider; private VFSContainer parentContainer; - public WebDAVProviderNamedContainer(Identity identity, WebDAVProvider provider) { + public WebDAVProviderNamedContainer(IdentityEnvironment identityEnv, WebDAVProvider provider) { super(provider.getMountPoint(), null); this.provider = provider; - this.identity = identity; + this.identityEnv = identityEnv; } @Override public VFSContainer getDelegate() { if(super.getDelegate() == null) { - setDelegate(provider.getContainer(identity)); + setDelegate(provider.getContainer(identityEnv)); if(parentContainer != null) { super.setParentContainer(parentContainer); parentContainer = null; } - identity = null; + identityEnv = null; } return super.getDelegate(); } diff --git a/src/main/java/org/olat/core/commons/services/webdav/servlets/WebDAVDispatcherImpl.java b/src/main/java/org/olat/core/commons/services/webdav/servlets/WebDAVDispatcherImpl.java index 7d6d535ae1d..137a9b6761a 100644 --- a/src/main/java/org/olat/core/commons/services/webdav/servlets/WebDAVDispatcherImpl.java +++ b/src/main/java/org/olat/core/commons/services/webdav/servlets/WebDAVDispatcherImpl.java @@ -59,6 +59,8 @@ import org.olat.core.util.vfs.QuotaExceededException; import org.olat.core.util.vfs.VFSItem; import org.olat.core.util.vfs.lock.LockInfo; import org.olat.core.util.vfs.lock.VFSLockManagerImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -130,7 +132,7 @@ import org.xml.sax.SAXException; * @author Remy Maucherat * @version $Id$ */ - +@Service("webDAVDispatcher") public class WebDAVDispatcherImpl extends DefaultDispatcher implements WebDAVDispatcher, Dispatcher { @@ -225,37 +227,16 @@ public class WebDAVDispatcherImpl */ private boolean allowSpecialPaths = false; + @Autowired private VFSLockManagerImpl lockManager; + @Autowired private WebDAVManager webDAVManager; + @Autowired private WebDAVModule webDAVModule; public WebDAVDispatcherImpl() { // } - - /** - * [used by Spring] - * @param lockManager - */ - public void setLockManager(VFSLockManagerImpl lockManager) { - this.lockManager = lockManager; - } - - /** - * [used by Spring] - * @param webDAVManager - */ - public void setWebDAVManager(WebDAVManager webDAVManager) { - this.webDAVManager = webDAVManager; - } - - /** - * [used by Spring] - * @param webDAVModule - */ - public void setWebDAVModule(WebDAVModule webDAVModule) { - this.webDAVModule = webDAVModule; - } @Override protected WebResourceRoot getResources(HttpServletRequest req) { diff --git a/src/main/java/org/olat/core/commons/services/webdav/ui/WebDAVAdminController.java b/src/main/java/org/olat/core/commons/services/webdav/ui/WebDAVAdminController.java index 7093868a9c4..0b9a4a83f38 100644 --- a/src/main/java/org/olat/core/commons/services/webdav/ui/WebDAVAdminController.java +++ b/src/main/java/org/olat/core/commons/services/webdav/ui/WebDAVAdminController.java @@ -37,7 +37,9 @@ import org.olat.core.gui.control.WindowControl; */ public class WebDAVAdminController extends FormBasicController { - private MultipleSelectionElement enableModuleEl, enableLinkEl, enableDigestEl, enableTermsFoldersEl; + private MultipleSelectionElement enableModuleEl, enableLinkEl, enableDigestEl, enableTermsFoldersEl, + learnersAsParticipantEl, learnersBookmarkEl; + private final WebDAVModule webDAVModule; public WebDAVAdminController(UserRequest ureq, WindowControl wControl) { @@ -76,8 +78,18 @@ public class WebDAVAdminController extends FormBasicController { enableTermsFoldersEl.select("xx", webDAVModule.isTermsFoldersEnabled()); enableTermsFoldersEl.addActionListener(FormEvent.ONCHANGE); enableTermsFoldersEl.setEnabled(enabled); + + uifactory.addSpacerElement("spacer2", formLayout, false); + learnersAsParticipantEl = uifactory.addCheckboxesHorizontal("learnersParticipants", "webdav.for.learners.participants", formLayout, new String[]{"xx"}, values); + learnersAsParticipantEl.select("xx", webDAVModule.isEnableLearnersParticipatingCourses()); + learnersAsParticipantEl.addActionListener(FormEvent.ONCHANGE); + learnersAsParticipantEl.setEnabled(enabled); + learnersBookmarkEl = uifactory.addCheckboxesHorizontal("learnerBookmarks", "webdav.for.learners.bookmarks", formLayout, new String[]{"xx"}, values); + learnersBookmarkEl.select("xx", webDAVModule.isEnableLearnersBookmarksCourse()); + learnersBookmarkEl.addActionListener(FormEvent.ONCHANGE); + learnersBookmarkEl.setEnabled(enabled); } @Override @@ -93,6 +105,8 @@ public class WebDAVAdminController extends FormBasicController { enableLinkEl.setEnabled(enabled); enableDigestEl.setEnabled(enabled); enableTermsFoldersEl.setEnabled(enabled); + learnersAsParticipantEl.setEnabled(enabled); + learnersBookmarkEl.setEnabled(enabled); } else if(source == enableLinkEl) { boolean enabled = enableLinkEl.isAtLeastSelected(1); webDAVModule.setLinkEnabled(enabled); @@ -102,6 +116,12 @@ public class WebDAVAdminController extends FormBasicController { } else if(source == enableTermsFoldersEl) { boolean enabled = enableTermsFoldersEl.isAtLeastSelected(1); webDAVModule.setTermsFoldersEnabled(enabled); + } else if(source == learnersAsParticipantEl) { + boolean enabled = learnersAsParticipantEl.isAtLeastSelected(1); + webDAVModule.setEnableLearnersParticipatingCourses(enabled); + } else if(source == learnersBookmarkEl) { + boolean enabled = learnersBookmarkEl.isAtLeastSelected(1); + webDAVModule.setEnableLearnersBookmarksCourse(enabled); } super.formInnerEvent(ureq, source, event); } diff --git a/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_de.properties index 90ed3399016..d5261b43932 100644 --- a/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_de.properties @@ -18,3 +18,5 @@ webdav.link=WebDAV Links anzeigen webdav.module=WebDAV Zugang webdav.on=ein webdav.termsfolders=Kurse nach Semesterdaten gruppieren +webdav.for.learners.participants=Zugriff für Studenten Kursen +webdav.for.learners.bookmarks=Zugriff für Studenten Favoriten diff --git a/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_en.properties index e7f3ad82c50..e7d2f00badd 100644 --- a/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/core/commons/services/webdav/ui/_i18n/LocalStrings_en.properties @@ -18,4 +18,6 @@ webdav.link=Show WebDAV links webdav.module=WebDAV access webdav.on=enabled webdav.termsfolders=Group courses by semester terms +webdav.for.learners.participants=Enable access for courses where user is participant +webdav.for.learners.bookmarks=Enable for courses that users marked as favorite diff --git a/src/main/java/org/olat/course/CourseFactory.java b/src/main/java/org/olat/course/CourseFactory.java index d10df68b53c..d7883276dff 100644 --- a/src/main/java/org/olat/course/CourseFactory.java +++ b/src/main/java/org/olat/course/CourseFactory.java @@ -429,7 +429,7 @@ public class CourseFactory extends BasicManager { targetCourse.saveEditorTreeModel(); // copy course folder - File fSourceCourseFolder = sourceCourse.getIsolatedCourseFolder().getBasefile(); + File fSourceCourseFolder = sourceCourse.getIsolatedCourseBaseFolder(); if (fSourceCourseFolder.exists()) FileUtils.copyDirToDir(fSourceCourseFolder, fTargetCourseBasePath, false, "copy course folder"); // copy folder nodes directories diff --git a/src/main/java/org/olat/course/CoursefolderWebDAVMergeSource.java b/src/main/java/org/olat/course/CoursefolderWebDAVMergeSource.java index 49907ab320f..363f6eb095f 100644 --- a/src/main/java/org/olat/course/CoursefolderWebDAVMergeSource.java +++ b/src/main/java/org/olat/course/CoursefolderWebDAVMergeSource.java @@ -21,14 +21,16 @@ package org.olat.course; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.olat.core.CoreSpringFactory; import org.olat.core.commons.services.webdav.WebDAVModule; import org.olat.core.commons.services.webdav.manager.WebDAVMergeSource; import org.olat.core.commons.services.webdav.servlets.RequestUtil; -import org.olat.core.id.Identity; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.util.vfs.NamedContainerImpl; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VirtualContainer; @@ -42,21 +44,27 @@ import org.olat.repository.model.RepositoryEntryLifecycle; * * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com */ -class CoursefolderWebDAVMergeSource extends WebDAVMergeSource { - public CoursefolderWebDAVMergeSource(Identity identity) { - super(identity); +class CoursefolderWebDAVMergeSource extends WebDAVMergeSource { + + private final IdentityEnvironment identityEnv; + + private final WebDAVModule webDAVModule; + private final RepositoryManager repositoryManager; + + public CoursefolderWebDAVMergeSource(IdentityEnvironment identityEnv) { + super(identityEnv.getIdentity()); + this.identityEnv = identityEnv; + webDAVModule = CoreSpringFactory.getImpl(WebDAVModule.class); + repositoryManager = CoreSpringFactory.getImpl(RepositoryManager.class); } @Override protected List<VFSContainer> loadMergedContainers() { - RepositoryManager rm = RepositoryManager.getInstance(); - List<RepositoryEntry> courseEntries = rm.queryByEditor(getIdentity(), CourseModule.getCourseTypeName()); List<VFSContainer> containers = new ArrayList<>(); Map<String, VFSContainer> terms = null; VirtualContainer noTermContainer = null; - WebDAVModule webDAVModule = CoreSpringFactory.getImpl(WebDAVModule.class); boolean useTerms = webDAVModule.isTermsFoldersEnabled(); if (useTerms) { // prepare no-terms folder for all resources without semester term info or private date @@ -64,10 +72,45 @@ class CoursefolderWebDAVMergeSource extends WebDAVMergeSource { noTermContainer = new VirtualContainer("other"); } + Set<RepositoryEntry> duplicates = new HashSet<>(); + List<RepositoryEntry> editorEntries = repositoryManager.queryByEditor(getIdentity(), "CourseModule"); + appendCourses(editorEntries, true, containers, useTerms, terms, noTermContainer, duplicates); + + //add courses as participant + if(webDAVModule.isEnableLearnersParticipatingCourses()) { + List<RepositoryEntry> participantEntries = repositoryManager.getLearningResourcesAsStudent(getIdentity(), "CourseModule", 0, -1); + appendCourses(participantEntries, false, containers, useTerms, terms, noTermContainer, duplicates); + } + + //add bookmarked courses + if(webDAVModule.isEnableLearnersBookmarksCourse()) { + List<RepositoryEntry> bookmarkedEntries = repositoryManager.getLearningResourcesAsBookmark(getIdentity(), identityEnv.getRoles(), "CourseModule", 0, -1); + appendCourses(bookmarkedEntries, false, containers, useTerms, terms, noTermContainer, duplicates); + } + + if (useTerms) { + // add no-terms folder if any have been found + if (noTermContainer.getItems().size() > 0) { + addContainerToList(noTermContainer, containers); + } + } + + return containers; + } + + private void appendCourses(List<RepositoryEntry> courseEntries, boolean editor, List<VFSContainer> containers, + boolean useTerms, Map<String, VFSContainer> terms, VirtualContainer noTermContainer, + Set<RepositoryEntry> duplicates) { + // Add all found repo entries to merge source for (RepositoryEntry re:courseEntries) { + if(duplicates.contains(re)) { + continue; + } + duplicates.add(re); + String courseTitle = RequestUtil.normalizeFilename(re.getDisplayname()); - NamedContainerImpl cfContainer = new CoursefolderWebDAVNamedContainer(courseTitle, re.getOlatResource()); + NamedContainerImpl cfContainer = new CoursefolderWebDAVNamedContainer(courseTitle, re.getOlatResource(), editor ? null : identityEnv); if (useTerms) { RepositoryEntryLifecycle lc = re.getLifecycle(); @@ -92,14 +135,5 @@ class CoursefolderWebDAVMergeSource extends WebDAVMergeSource { addContainerToList(cfContainer, containers); } } - - if (useTerms) { - // add no-terms folder if any have been found - if (noTermContainer.getItems().size() > 0) { - addContainerToList(noTermContainer, containers); - } - } - - return containers; } } \ No newline at end of file diff --git a/src/main/java/org/olat/course/CoursefolderWebDAVNamedContainer.java b/src/main/java/org/olat/course/CoursefolderWebDAVNamedContainer.java index b7895f1f04e..9b33e6266ed 100644 --- a/src/main/java/org/olat/course/CoursefolderWebDAVNamedContainer.java +++ b/src/main/java/org/olat/course/CoursefolderWebDAVNamedContainer.java @@ -19,6 +19,7 @@ */ package org.olat.course; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.id.OLATResourceable; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; @@ -37,10 +38,12 @@ class CoursefolderWebDAVNamedContainer extends NamedContainerImpl { private OLATResourceable res; private VFSContainer parentContainer; + private IdentityEnvironment identityEnv; - public CoursefolderWebDAVNamedContainer(String courseTitle, OLATResourceable res) { + public CoursefolderWebDAVNamedContainer(String courseTitle, OLATResourceable res, IdentityEnvironment identityEnv) { super(courseTitle, null); this.res = OresHelper.clone(res); + this.identityEnv = identityEnv; } @Override @@ -53,7 +56,7 @@ class CoursefolderWebDAVNamedContainer extends NamedContainerImpl { if(super.getDelegate() == null) { try { ICourse course = CourseFactory.loadCourse(res.getResourceableId()); - VFSContainer courseFolder = course.getCourseFolderContainer(); + VFSContainer courseFolder = course.getCourseFolderContainer(identityEnv); setDelegate(courseFolder); if(parentContainer != null) { super.setParentContainer(parentContainer); diff --git a/src/main/java/org/olat/course/CoursefolderWebDAVProvider.java b/src/main/java/org/olat/course/CoursefolderWebDAVProvider.java index 9a1f3f062e7..52eccf277dc 100644 --- a/src/main/java/org/olat/course/CoursefolderWebDAVProvider.java +++ b/src/main/java/org/olat/course/CoursefolderWebDAVProvider.java @@ -26,22 +26,23 @@ package org.olat.course; import org.olat.core.commons.services.webdav.WebDAVProvider; -import org.olat.core.id.Identity; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.util.vfs.VFSContainer; /** * * Description:<br> - * TODO: guido Class Description for CoursefolderWebDAVProvider */ public class CoursefolderWebDAVProvider implements WebDAVProvider { private static final String MOUNTPOINT = "coursefolders"; + @Override public String getMountPoint() { return MOUNTPOINT; } - public VFSContainer getContainer(Identity identity) { - return new CoursefolderWebDAVMergeSource(identity); + @Override + public VFSContainer getContainer(IdentityEnvironment identityEnv) { + return new CoursefolderWebDAVMergeSource(identityEnv); } } \ No newline at end of file diff --git a/src/main/java/org/olat/course/ICourse.java b/src/main/java/org/olat/course/ICourse.java index bb0e53b718d..5f08b9540fe 100644 --- a/src/main/java/org/olat/course/ICourse.java +++ b/src/main/java/org/olat/course/ICourse.java @@ -28,6 +28,7 @@ package org.olat.course; import java.io.File; import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.id.OLATResourceable; import org.olat.core.util.vfs.VFSContainer; import org.olat.course.config.CourseConfig; @@ -88,6 +89,8 @@ public interface ICourse extends OLATResourceable { */ public VFSContainer getCourseFolderContainer(); + public VFSContainer getCourseFolderContainer(IdentityEnvironment identityEnv); + public OlatRootFolderImpl getCourseExportDataDir(); /** diff --git a/src/main/java/org/olat/course/MergedCourseContainer.java b/src/main/java/org/olat/course/MergedCourseContainer.java index ca868725a24..b06c5f0b40e 100644 --- a/src/main/java/org/olat/course/MergedCourseContainer.java +++ b/src/main/java/org/olat/course/MergedCourseContainer.java @@ -22,6 +22,9 @@ package org.olat.course; import org.olat.core.commons.modules.bc.vfs.OlatNamedContainerImpl; import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl; import org.olat.core.commons.services.webdav.servlets.RequestUtil; +import org.olat.core.gui.components.tree.GenericTreeModel; +import org.olat.core.gui.components.tree.TreeNode; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; @@ -34,9 +37,14 @@ import org.olat.core.util.vfs.filters.VFSItemFilter; import org.olat.course.config.CourseConfig; import org.olat.course.nodes.BCCourseNode; import org.olat.course.nodes.CourseNode; +import org.olat.course.run.userview.NodeEvaluation; +import org.olat.course.run.userview.TreeEvaluation; +import org.olat.course.run.userview.UserCourseEnvironment; +import org.olat.course.run.userview.UserCourseEnvironmentImpl; import org.olat.modules.sharedfolder.SharedFolderManager; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryManager; +import org.olat.repository.model.RepositoryEntrySecurity; /** * @@ -47,10 +55,16 @@ public class MergedCourseContainer extends MergeSource { private static final OLog log = Tracing.createLoggerFor(MergedCourseContainer.class); private final Long courseId; + private final IdentityEnvironment identityEnv; public MergedCourseContainer(Long courseId, String name) { + this(courseId, name, null); + } + + public MergedCourseContainer(Long courseId, String name, IdentityEnvironment identityEnv) { super(null, name); this.courseId = courseId; + this.identityEnv = identityEnv; } @Override @@ -59,8 +73,16 @@ public class MergedCourseContainer extends MergeSource { ICourse course = CourseFactory.loadCourse(courseId); if(course instanceof PersistingCourseImpl) { PersistingCourseImpl persistingCourse = (PersistingCourseImpl)course; - addContainersChildren(persistingCourse.getIsolatedCourseFolder(), true); - + if(identityEnv == null || identityEnv.getRoles().isOLATAdmin()) { + addContainersChildren(persistingCourse.getIsolatedCourseFolder(), true); + } else { + RepositoryEntry re = course.getCourseEnvironment().getCourseGroupManager().getCourseEntry(); + RepositoryEntrySecurity reSecurity = RepositoryManager.getInstance() + .isAllowed(identityEnv.getIdentity(), identityEnv.getRoles(), re); + if(reSecurity.isEntryAdmin()) { + addContainersChildren(persistingCourse.getIsolatedCourseFolder(), true); + } + } // grab any shared folder that is configured OlatRootFolderImpl sharedFolder = null; String sfSoftkey = persistingCourse.getCourseConfig().getSharedFolderSoftkey(); @@ -69,7 +91,7 @@ public class MergedCourseContainer extends MergeSource { RepositoryEntry re = rm.lookupRepositoryEntryBySoftkey(sfSoftkey, false); if (re != null) { sharedFolder = SharedFolderManager.getInstance().getSharedFolder(re.getOlatResource()); - if (sharedFolder != null){ + if (sharedFolder != null) { sharedFolder.setLocalSecurityCallback(new ReadOnlyCallback()); //add local course folder's children as read/write source and any sharedfolder as subfolder addContainer(new NamedContainerImpl("_sharedfolder", sharedFolder)); @@ -79,13 +101,98 @@ public class MergedCourseContainer extends MergeSource { // add all course building blocks of type BC to a virtual folder MergeSource nodesContainer = new MergeSource(null, "_courseelementdata"); - addFolderBuildingBlocks(persistingCourse, nodesContainer, persistingCourse.getRunStructure().getRootNode()); + if(identityEnv == null) { + CourseNode rootNode = course.getRunStructure().getRootNode(); + addFolderBuildingBlocks(persistingCourse, nodesContainer, rootNode); + } else { + TreeEvaluation treeEval = new TreeEvaluation(); + GenericTreeModel treeModel = new GenericTreeModel(); + UserCourseEnvironment userCourseEnv = new UserCourseEnvironmentImpl(identityEnv, course.getCourseEnvironment()); + CourseNode rootCn = userCourseEnv.getCourseEnvironment().getRunStructure().getRootNode(); + NodeEvaluation rootNodeEval = rootCn.eval(userCourseEnv.getConditionInterpreter(), treeEval); + TreeNode treeRoot = rootNodeEval.getTreeNode(); + treeModel.setRootNode(treeRoot); + addFolderBuildingBlocks(persistingCourse, nodesContainer, treeRoot); + } + if (nodesContainer.getItems().size() > 0) { addContainer(nodesContainer); } } } + private void addFolderBuildingBlocks(PersistingCourseImpl course, MergeSource nodesContainer, TreeNode courseNode) { + for (int i = 0; i < courseNode.getChildCount(); i++) { + TreeNode child = (TreeNode)courseNode.getChildAt(i); + + NodeEvaluation nodeEval; + if(child.getUserObject() instanceof NodeEvaluation) { + nodeEval = (NodeEvaluation)child.getUserObject(); + } else { + continue; + } + + if(nodeEval != null && nodeEval.getCourseNode() != null) { + CourseNode courseNodeChild = nodeEval.getCourseNode(); + String folderName = RequestUtil.normalizeFilename(courseNodeChild.getShortTitle()); + MergeSource courseNodeContainer; + if (courseNodeChild instanceof BCCourseNode) { + final BCCourseNode bcNode = (BCCourseNode) courseNodeChild; + // add folder not to merge source. Use name and node id to have unique name + String path = BCCourseNode.getFoldernodePathRelToFolderBase(course.getCourseEnvironment(), bcNode); + OlatRootFolderImpl rootFolder = new OlatRootFolderImpl(path, null); + + boolean canDownload = nodeEval.isCapabilityAccessible("download"); + if(canDownload) { + if(nodeEval.isCapabilityAccessible("upload")) { + //inherit the security callback from the course as for author + } else { + rootFolder.setLocalSecurityCallback(new ReadOnlyCallback()); + } + + // add node ident if multiple files have same name + if (nodesContainer.getItems(new VFSItemFilter() { + @Override + public boolean accept(VFSItem vfsItem) { + return (bcNode.getShortTitle().equals(RequestUtil.normalizeFilename(bcNode.getShortTitle()))); + } + }).size() > 0) { + folderName = folderName + " (" + bcNode.getIdent() + ")"; + } + + // Create a container for this node content and wrap it with a merge source which is attached to tree + VFSContainer nodeContentContainer = new OlatNamedContainerImpl(folderName, rootFolder); + courseNodeContainer = new MergeSource(nodesContainer, folderName); + courseNodeContainer.addContainersChildren(nodeContentContainer, true); + nodesContainer.addContainer(courseNodeContainer); + // Do recursion for all children + addFolderBuildingBlocks(course, courseNodeContainer, child); + + } else { + // For non-folder course nodes, add merge source (no files to show) ... + courseNodeContainer = new MergeSource(null, folderName); + // , then do recursion for all children ... + addFolderBuildingBlocks(course, courseNodeContainer, child); + // ... but only add this container if it contains any children with at least one BC course node + if (courseNodeContainer.getItems().size() > 0) { + nodesContainer.addContainer(courseNodeContainer); + } + } + } else { + // For non-folder course nodes, add merge source (no files to show) ... + courseNodeContainer = new MergeSource(null, folderName); + // , then do recursion for all children ... + addFolderBuildingBlocks(course, courseNodeContainer, child); + // ... but only add this container if it contains any children with at least one BC course node + if (courseNodeContainer.getItems().size() > 0) { + nodesContainer.addContainer(courseNodeContainer); + } + } + } + } + } + + /** * internal method to recursively add all course building blocks of type * BC to a given VFS container. This should only be used for an author view, diff --git a/src/main/java/org/olat/course/PersistingCourseImpl.java b/src/main/java/org/olat/course/PersistingCourseImpl.java index 26c9f60dcbf..e8406d6379e 100644 --- a/src/main/java/org/olat/course/PersistingCourseImpl.java +++ b/src/main/java/org/olat/course/PersistingCourseImpl.java @@ -31,6 +31,7 @@ import java.io.Serializable; import org.olat.admin.quota.QuotaConstants; import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl; import org.olat.core.commons.persistence.DBFactory; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.id.OLATResourceable; import org.olat.core.logging.AssertException; import org.olat.core.logging.OLATRuntimeException; @@ -163,16 +164,26 @@ public class PersistingCourseImpl implements ICourse, OLATResourceable, Serializ /** * @see org.olat.course.ICourse#getCourseFolderPath() */ + @Override public VFSContainer getCourseFolderContainer() { // add local course folder's children as read/write source and any sharedfolder as subfolder MergedCourseContainer courseFolderContainer = new MergedCourseContainer(resourceableId, getCourseTitle()); courseFolderContainer.init(); return courseFolderContainer; } + + @Override + public VFSContainer getCourseFolderContainer(IdentityEnvironment identityEnv) { + // add local course folder's children as read/write source and any sharedfolder as subfolder + MergedCourseContainer courseFolderContainer = new MergedCourseContainer(resourceableId, getCourseTitle(), identityEnv); + courseFolderContainer.init(); + return courseFolderContainer; + } /** * @see org.olat.course.ICourse#getCourseEnvironment() */ + @Override public CourseEnvironment getCourseEnvironment() { return courseEnvironment; } @@ -207,20 +218,28 @@ public class PersistingCourseImpl implements ICourse, OLATResourceable, Serializ OlatRootFolderImpl isolatedCourseFolder = new OlatRootFolderImpl(courseRootContainer.getRelPath() + File.separator + COURSEFOLDER, null); // generate course folder File fCourseFolder = isolatedCourseFolder.getBasefile(); - if (!fCourseFolder.exists() && !fCourseFolder.mkdirs()) throw new OLATRuntimeException(this.getClass(), - "could not create course's coursefolder path:" + fCourseFolder.getAbsolutePath(), null); + if (!fCourseFolder.exists() && !fCourseFolder.mkdirs()) { + throw new OLATRuntimeException(this.getClass(), + "could not create course's coursefolder path:" + fCourseFolder.getAbsolutePath(), null); + } QuotaManager qm = QuotaManager.getInstance(); Quota q = qm.getCustomQuota(isolatedCourseFolder.getRelPath()); if (q == null){ Quota defQuota = qm.getDefaultQuota(QuotaConstants.IDENTIFIER_DEFAULT_COURSE); - q = QuotaManager.getInstance().createQuota(isolatedCourseFolder.getRelPath(), defQuota.getQuotaKB(), defQuota.getUlLimitKB()); + q = qm.createQuota(isolatedCourseFolder.getRelPath(), defQuota.getQuotaKB(), defQuota.getUlLimitKB()); } FullAccessWithQuotaCallback secCallback = new FullAccessWithQuotaCallback(q); isolatedCourseFolder.setLocalSecurityCallback(secCallback); return isolatedCourseFolder; } + protected File getIsolatedCourseBaseFolder() { + // create local course folder + OlatRootFolderImpl isolatedCourseFolder = new OlatRootFolderImpl(courseRootContainer.getRelPath() + File.separator + COURSEFOLDER, null); + return isolatedCourseFolder.getBasefile(); + } + /** * Save the run structure to disk, persist to the xml file */ @@ -283,7 +302,7 @@ public class PersistingCourseImpl implements ICourse, OLATResourceable, Serializ // fxdiff: export layout-folder FileUtils.copyDirToDir(new OlatRootFolderImpl(courseRootContainer.getRelPath() + File.separator + "layout", null).getBasefile(), exportDirectory, "course export layout folder"); // export course folder - FileUtils.copyDirToDir(getIsolatedCourseFolder().getBasefile(), exportDirectory, "course export folder"); + FileUtils.copyDirToDir(getIsolatedCourseBaseFolder(), exportDirectory, "course export folder"); // export any node data log.info("exportToFilesystem: exporting course "+this+": exporting all nodes..."); Visitor visitor = new NodeExportVisitor(fExportedDataDir, this); diff --git a/src/main/java/org/olat/course/run/CourseRuntimeController.java b/src/main/java/org/olat/course/run/CourseRuntimeController.java index cca25898df9..feadc681e0d 100644 --- a/src/main/java/org/olat/course/run/CourseRuntimeController.java +++ b/src/main/java/org/olat/course/run/CourseRuntimeController.java @@ -85,8 +85,8 @@ import org.olat.course.assessment.CoachingGroupAccessAssessmentCallback; import org.olat.course.assessment.EfficiencyStatementManager; import org.olat.course.assessment.FullAccessAssessmentCallback; import org.olat.course.assessment.UserEfficiencyStatement; -import org.olat.course.certificate.ui.CertificatesOptionsController; import org.olat.course.certificate.ui.CertificateAndEfficiencyStatementController; +import org.olat.course.certificate.ui.CertificatesOptionsController; import org.olat.course.config.CourseConfig; import org.olat.course.config.CourseConfigEvent; import org.olat.course.config.ui.CourseOptionsController; diff --git a/src/main/java/org/olat/group/GroupfoldersWebDAVProvider.java b/src/main/java/org/olat/group/GroupfoldersWebDAVProvider.java index a5086002366..71f8579d588 100644 --- a/src/main/java/org/olat/group/GroupfoldersWebDAVProvider.java +++ b/src/main/java/org/olat/group/GroupfoldersWebDAVProvider.java @@ -27,7 +27,7 @@ package org.olat.group; import org.olat.collaboration.CollaborationManager; import org.olat.core.commons.services.webdav.WebDAVProvider; -import org.olat.core.id.Identity; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.util.vfs.VFSContainer; /** * @@ -47,12 +47,14 @@ public class GroupfoldersWebDAVProvider implements WebDAVProvider { this.collaborationManager = collaborationManager; } + @Override public String getMountPoint() { return MOUNTPOINT; } - public VFSContainer getContainer(Identity identity) { - return new GroupfoldersWebDAVMergeSource(identity, collaborationManager); + @Override + public VFSContainer getContainer(IdentityEnvironment identityEnv) { + return new GroupfoldersWebDAVMergeSource(identityEnv.getIdentity(), collaborationManager); } } diff --git a/src/main/java/org/olat/home/controllerCreators/PersonalFolderControllerCreator.java b/src/main/java/org/olat/home/controllerCreators/PersonalFolderControllerCreator.java index 7218773897a..0ac8f3b7668 100644 --- a/src/main/java/org/olat/home/controllerCreators/PersonalFolderControllerCreator.java +++ b/src/main/java/org/olat/home/controllerCreators/PersonalFolderControllerCreator.java @@ -24,6 +24,7 @@ import org.olat.core.gui.UserRequest; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.creator.AutoCreator; +import org.olat.core.id.IdentityEnvironment; import org.olat.user.PersonalFolderManager; /** @@ -54,6 +55,7 @@ public class PersonalFolderControllerCreator extends AutoCreator { */ @Override public Controller createController(UserRequest ureq, WindowControl lwControl) { - return new FolderRunController(PersonalFolderManager.getInstance().getContainer(ureq.getIdentity()), true, true, true, ureq, lwControl); + IdentityEnvironment identityEnv = ureq.getUserSession().getIdentityEnvironment(); + return new FolderRunController(PersonalFolderManager.getInstance().getContainer(identityEnv), true, true, true, ureq, lwControl); } } diff --git a/src/main/java/org/olat/modules/sharedfolder/SharedFolderWebDAVProvider.java b/src/main/java/org/olat/modules/sharedfolder/SharedFolderWebDAVProvider.java index deb89ffae25..8fa86c8ca16 100644 --- a/src/main/java/org/olat/modules/sharedfolder/SharedFolderWebDAVProvider.java +++ b/src/main/java/org/olat/modules/sharedfolder/SharedFolderWebDAVProvider.java @@ -28,7 +28,7 @@ package org.olat.modules.sharedfolder; import java.util.List; import org.olat.core.commons.services.webdav.WebDAVProvider; -import org.olat.core.id.Identity; +import org.olat.core.id.IdentityEnvironment; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.callbacks.ReadOnlyCallback; import org.olat.core.util.vfs.callbacks.VFSSecurityCallback; @@ -86,7 +86,8 @@ public class SharedFolderWebDAVProvider implements WebDAVProvider { /** * @see org.olat.core.commons.services.webdav.WebDAVProvider#getContainer(org.olat.core.id.Identity) */ - public VFSContainer getContainer(Identity identity) { - return new SharedFolderWebDAVMergeSource(identity, publiclyReadableFolders); + @Override + public VFSContainer getContainer(IdentityEnvironment identityEnv) { + return new SharedFolderWebDAVMergeSource(identityEnv.getIdentity(), publiclyReadableFolders); } } \ No newline at end of file diff --git a/src/main/java/org/olat/repository/RepositoryManager.java b/src/main/java/org/olat/repository/RepositoryManager.java index 1c9d4be0a80..814b9f63af2 100644 --- a/src/main/java/org/olat/repository/RepositoryManager.java +++ b/src/main/java/org/olat/repository/RepositoryManager.java @@ -1766,7 +1766,7 @@ public class RepositoryManager extends BasicManager { * @param identity * @return list of RepositoryEntries */ - public List<RepositoryEntry> getLearningResourcesAsStudent(Identity identity, int firstResult, int maxResults, RepositoryEntryOrder... orderby) { + public List<RepositoryEntry> getLearningResourcesAsStudent(Identity identity, String type, int firstResult, int maxResults, RepositoryEntryOrder... orderby) { StringBuilder sb = new StringBuilder(1200); sb.append("select v from ").append(RepositoryEntry.class.getName()).append(" as v ") .append(" inner join fetch v.olatResource as res ") @@ -1777,6 +1777,10 @@ public class RepositoryManager extends BasicManager { .append(" inner join baseGroup.members as membership") .append(" where (v.access>=3 or (v.access=").append(RepositoryEntry.ACC_OWNERS).append(" and v.membersOnly=true))") .append(" and membership.identity.key=:identityKey and membership.role='").append(GroupRoles.participant.name()).append("'"); + if(StringHelper.containsNonWhitespace(type)) { + sb.append(" and res.resName=:resourceType"); + } + appendOrderBy(sb, "v", orderby); TypedQuery<RepositoryEntry> query = dbInstance.getCurrentEntityManager() @@ -1786,6 +1790,56 @@ public class RepositoryManager extends BasicManager { if(maxResults > 0) { query.setMaxResults(maxResults); } + if(StringHelper.containsNonWhitespace(type)) { + query.setParameter("resourceType", type); + } + List<RepositoryEntry> repoEntries = query.getResultList(); + return repoEntries; + } + + public List<RepositoryEntry> getLearningResourcesAsBookmark(Identity identity, Roles roles, String type, int firstResult, int maxResults, RepositoryEntryOrder... orderby) { + if(roles.isGuestOnly()) { + return Collections.emptyList(); + } + + StringBuilder sb = new StringBuilder(1200); + sb.append("select v from ").append(RepositoryEntry.class.getName()).append(" as v ") + .append(" inner join fetch v.olatResource as res ") + .append(" inner join fetch v.statistics as statistics") + .append(" left join fetch v.lifecycle as lifecycle") + .append(" where exists (select mark.key from ").append(MarkImpl.class.getName()).append(" as mark ") + .append(" where mark.creator.key=:identityKey and mark.resId=v.key and mark.resName='RepositoryEntry'") + .append(" ) "); + if(StringHelper.containsNonWhitespace(type)) { + sb.append(" and res.resName=:resourceType"); + } + sb.append(" and (v.access >= "); + if (roles.isAuthor()) { + sb.append(RepositoryEntry.ACC_OWNERS_AUTHORS); + } else { + sb.append(RepositoryEntry.ACC_USERS); + } + sb.append(" or (") + .append(" v.access=").append(RepositoryEntry.ACC_OWNERS).append(" and v.membersOnly=true") + .append(" and exists (select rel from repoentrytogroup as rel, bgroup as baseGroup, bgroupmember as membership") + .append(" where rel.entry=v and rel.group=baseGroup and membership.group=baseGroup and membership.identity.key=:identityKey") + .append(" and membership.role in ('").append(GroupRoles.owner.name()).append("','").append(GroupRoles.coach.name()).append("','").append(GroupRoles.participant.name()).append("')") + .append(" )") + .append(" )") + .append(")"); + + appendOrderBy(sb, "v", orderby); + + TypedQuery<RepositoryEntry> query = dbInstance.getCurrentEntityManager() + .createQuery(sb.toString(), RepositoryEntry.class) + .setParameter("identityKey", identity.getKey()) + .setFirstResult(firstResult); + if(maxResults > 0) { + query.setMaxResults(maxResults); + } + if(StringHelper.containsNonWhitespace(type)) { + query.setParameter("resourceType", type); + } List<RepositoryEntry> repoEntries = query.getResultList(); return repoEntries; } @@ -1795,10 +1849,10 @@ public class RepositoryManager extends BasicManager { sb.append("select v from repoentrylight as v ") .append(" inner join fetch v.olatResource as res ") .append(" where exists (select rel from repoentrytogroup as rel, bgroup as baseGroup, bgroupmember as membership ") - .append(" where rel.entry=v and rel.group=baseGroup and membership.group=baseGroup and membership.identity.key=:identityKey ") - .append(" and membership.role='").append(GroupRoles.participant.name()).append("')") - .append(" )") - .append(" )") + .append(" where rel.entry=v and rel.group=baseGroup and membership.group=baseGroup and membership.identity.key=:identityKey ") + .append(" and membership.role='").append(GroupRoles.participant.name()).append("')") + .append(" )") + .append(" )") .append(" and (v.access>=3 or (v.access=").append(RepositoryEntry.ACC_OWNERS).append(" and v.membersOnly=true))"); appendOrderBy(sb, "v", orderby); diff --git a/src/main/java/org/olat/repository/RepositoryService.java b/src/main/java/org/olat/repository/RepositoryService.java index 169cdf4c412..98ebfdb60f5 100644 --- a/src/main/java/org/olat/repository/RepositoryService.java +++ b/src/main/java/org/olat/repository/RepositoryService.java @@ -141,5 +141,4 @@ public interface RepositoryService { public int countAuthorView(SearchAuthorRepositoryEntryViewParams params); public List<RepositoryEntryAuthorView> searchAuthorView(SearchAuthorRepositoryEntryViewParams params, int firstResult, int maxResults); - } diff --git a/src/main/java/org/olat/repository/controllers/RepositorySearchController.java b/src/main/java/org/olat/repository/controllers/RepositorySearchController.java index d3043a5437d..89dae1a9b96 100644 --- a/src/main/java/org/olat/repository/controllers/RepositorySearchController.java +++ b/src/main/java/org/olat/repository/controllers/RepositorySearchController.java @@ -432,7 +432,7 @@ public class RepositorySearchController extends BasicController implements Activ private void doSearchMyCoursesStudent(UserRequest ureq, String limitType, boolean updateFilters) { searchType = SearchType.myAsStudent; RepositoryManager rm = RepositoryManager.getInstance(); - List<RepositoryEntry> entries = rm.getLearningResourcesAsStudent(ureq.getIdentity(), 0, -1); + List<RepositoryEntry> entries = rm.getLearningResourcesAsStudent(ureq.getIdentity(), null, 0, -1); filterRepositoryEntries(entries); doSearchMyRepositoryEntries(ureq, entries, limitType, updateFilters); } diff --git a/src/main/java/org/olat/repository/manager/RepositoryEntryDAO.java b/src/main/java/org/olat/repository/manager/RepositoryEntryDAO.java index aae66366f94..7073be4b758 100644 --- a/src/main/java/org/olat/repository/manager/RepositoryEntryDAO.java +++ b/src/main/java/org/olat/repository/manager/RepositoryEntryDAO.java @@ -120,5 +120,4 @@ public class RepositoryEntryDAO { .setMaxResults(maxResults) .getResultList(); } - -} +} \ No newline at end of file diff --git a/src/main/java/org/olat/user/restapi/UserCoursesWebService.java b/src/main/java/org/olat/user/restapi/UserCoursesWebService.java index 273c4feef6b..95ae4397ba7 100644 --- a/src/main/java/org/olat/user/restapi/UserCoursesWebService.java +++ b/src/main/java/org/olat/user/restapi/UserCoursesWebService.java @@ -78,7 +78,7 @@ public class UserCoursesWebService { RepositoryManager rm = RepositoryManager.getInstance(); if(MediaTypeVariants.isPaged(httpRequest, request)) { - List<RepositoryEntry> repoEntries = rm.getLearningResourcesAsStudent(identity, start, limit, RepositoryEntryOrder.nameAsc); + List<RepositoryEntry> repoEntries = rm.getLearningResourcesAsStudent(identity, null, start, limit, RepositoryEntryOrder.nameAsc); int totalCount= rm.countLearningResourcesAsStudent(identity); CourseVO[] vos = toCourseVo(repoEntries); @@ -87,7 +87,7 @@ public class UserCoursesWebService { voes.setTotalCount(totalCount); return Response.ok(voes).build(); } else { - List<RepositoryEntry> repoEntries = rm.getLearningResourcesAsStudent(identity, 0, -1, RepositoryEntryOrder.nameAsc); + List<RepositoryEntry> repoEntries = rm.getLearningResourcesAsStudent(identity, null, 0, -1, RepositoryEntryOrder.nameAsc); CourseVO[] vos = toCourseVo(repoEntries); return Response.ok(vos).build(); } diff --git a/src/test/java/org/olat/core/commons/services/webdav/WebDAVCommandsTest.java b/src/test/java/org/olat/core/commons/services/webdav/WebDAVCommandsTest.java index 5278ff015ef..c52d4e80bce 100644 --- a/src/test/java/org/olat/core/commons/services/webdav/WebDAVCommandsTest.java +++ b/src/test/java/org/olat/core/commons/services/webdav/WebDAVCommandsTest.java @@ -42,6 +42,7 @@ import org.apache.http.entity.InputStreamEntity; import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.apache.poi.util.IOUtils; +import org.junit.After; import org.junit.Test; import org.olat.basesecurity.BaseSecurity; import org.olat.basesecurity.GroupRoles; @@ -60,6 +61,7 @@ import org.olat.course.CourseFactory; import org.olat.course.ICourse; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryService; +import org.olat.repository.manager.RepositoryEntryRelationDAO; import org.olat.restapi.CoursePublishTest; import org.olat.test.JunitTestHelper; import org.springframework.beans.factory.annotation.Autowired; @@ -76,11 +78,21 @@ public class WebDAVCommandsTest extends WebDAVTestCase { @Autowired private DB dbInstance; @Autowired + private WebDAVModule webDAVModule; + @Autowired + private VFSLockManager lockManager; + @Autowired private BaseSecurity securityManager; @Autowired private RepositoryService repositoryService; @Autowired - private VFSLockManager lockManager; + private RepositoryEntryRelationDAO repositoryEntryRelationDao; + + @After + public void resetWebDAVModule() { + webDAVModule.setEnableLearnersBookmarksCourse(false); + webDAVModule.setEnableLearnersParticipatingCourses(false); + } /** * Check the DAV, Ms-Author and Allow header @@ -587,6 +599,71 @@ public class WebDAVCommandsTest extends WebDAVTestCase { IOUtils.closeQuietly(conn); } + @Test + public void coursePermissions_participant() + throws IOException, URISyntaxException { + webDAVModule.setEnableLearnersBookmarksCourse(true); + webDAVModule.setEnableLearnersParticipatingCourses(true); + + Identity author = JunitTestHelper.createAndPersistIdentityAsAuthor("auth-webdav"); + Identity participant = JunitTestHelper.createAndPersistIdentityAsRndUser("participant-webdav"); + URL courseWithForumsUrl = WebDAVCommandsTest.class.getResource("webdav_course.zip"); + RepositoryEntry course = deployTestCourse(author, null, courseWithForumsUrl); + repositoryEntryRelationDao.addRole(participant, course, GroupRoles.participant.name()); + dbInstance.commitAndCloseSession(); + + WebDAVConnection conn = new WebDAVConnection(); + conn.setCredentials(participant.getName(), "A6B7C8"); + + URI courseUri = conn.getBaseURI().path("webdav").path("coursefolders").build(); + String publicXml = conn.propfind(courseUri, 2); + //cannot access course storage + Assert.assertFalse(publicXml.contains("<D:href>/webdav/coursefolders/other/WebDAV%20course/Course%20storage/</D:href>")); + //can access course elements + Assert.assertTrue(publicXml.contains("<D:href>/webdav/coursefolders/other/WebDAV%20course/_courseelementdata/</D:href>")); + + URI courseElementUri = conn.getBaseURI().path("webdav").path("coursefolders") + .path("other").path("WebDAV%20course").path("_courseelementdata").build(); + String publicElementXml = conn.propfind(courseElementUri, 2); + Assert.assertTrue(publicElementXml.contains("<D:href>/webdav/coursefolders/other/WebDAV%20course/_courseelementdata/Folder%20for%20all/</D:href>")); + Assert.assertFalse(publicElementXml.contains("<D:href>/webdav/coursefolders/other/WebDAV%20course/_courseelementdata/Student%20read-only%20%2890600786058954%29/Readonly%20students/</D:href>")); + Assert.assertFalse(publicElementXml.contains("<D:href>/webdav/coursefolders/other/WebDAV%20course/_courseelementdata/Not%20for%20students%20%2890600786058958%29/Not%20for%20students/</D:href>")); + + conn.close(); + } + + @Test + public void coursePermissions_owner() + throws IOException, URISyntaxException { + webDAVModule.setEnableLearnersBookmarksCourse(true); + webDAVModule.setEnableLearnersParticipatingCourses(true); + + Identity author = JunitTestHelper.createAndPersistIdentityAsAuthor("auth-webdav"); + URL courseWithForumsUrl = WebDAVCommandsTest.class.getResource("webdav_course.zip"); + deployTestCourse(author, null, courseWithForumsUrl); + dbInstance.commitAndCloseSession(); + + WebDAVConnection conn = new WebDAVConnection(); + conn.setCredentials(author.getName(), "A6B7C8"); + + URI courseUri = conn.getBaseURI().path("webdav").path("coursefolders").build(); + String publicXml = conn.propfind(courseUri, 2); + //cane access course storage + Assert.assertTrue(publicXml.contains("<D:href>/webdav/coursefolders/other/WebDAV%20course/Course%20storage/</D:href>")); + //can access course elements + Assert.assertTrue(publicXml.contains("<D:href>/webdav/coursefolders/other/WebDAV%20course/_courseelementdata/</D:href>")); + + URI courseElementUri = conn.getBaseURI().path("webdav").path("coursefolders") + .path("other").path("WebDAV%20course").path("_courseelementdata").build(); + String publicElementXml = conn.propfind(courseElementUri, 2); + //can access all 3 course nodes + Assert.assertTrue(publicElementXml.contains("<D:href>/webdav/coursefolders/other/WebDAV%20course/_courseelementdata/Folder%20for%20all/</D:href>")); + Assert.assertTrue(publicElementXml.contains("<D:href>/webdav/coursefolders/other/WebDAV%20course/_courseelementdata/Student%20read-only%20%2890600786058954%29/Readonly%20students/</D:href>")); + Assert.assertTrue(publicElementXml.contains("<D:href>/webdav/coursefolders/other/WebDAV%20course/_courseelementdata/Not%20for%20students%20%2890600786058958%29/Not%20for%20students/</D:href>")); + + conn.close(); + } + private VFSItem createFile(VFSContainer container, String filename) throws IOException { VFSLeaf testLeaf = container.createChildLeaf(filename); InputStream in = WebDAVCommandsTest.class.getResourceAsStream("text.txt"); @@ -598,8 +675,14 @@ public class WebDAVCommandsTest extends WebDAVTestCase { return container.resolve(filename); } - private RepositoryEntry deployTestCourse(Identity author, Identity coAuthor) throws URISyntaxException { + private RepositoryEntry deployTestCourse(Identity author, Identity coAuthor) + throws URISyntaxException { URL courseWithForumsUrl = CoursePublishTest.class.getResource("myCourseWS.zip"); + return deployTestCourse(author, coAuthor, courseWithForumsUrl); + } + + private RepositoryEntry deployTestCourse(Identity author, Identity coAuthor, URL courseWithForumsUrl) + throws URISyntaxException { Assert.assertNotNull(courseWithForumsUrl); File courseWithForums = new File(courseWithForumsUrl.toURI()); String softKey = UUID.randomUUID().toString().replace("-", "").substring(0, 30); diff --git a/src/test/java/org/olat/core/commons/services/webdav/webdav_course.zip b/src/test/java/org/olat/core/commons/services/webdav/webdav_course.zip new file mode 100644 index 0000000000000000000000000000000000000000..b216bb6e6aaff6897cfd89bc5d8f5c01c231db4a GIT binary patch literal 24513 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 S@=FNxHTb6n{Cww!c=tcm%xro9 literal 0 HcmV?d00001 diff --git a/src/test/java/org/olat/repository/RepositoryManagerTest.java b/src/test/java/org/olat/repository/RepositoryManagerTest.java index cc03446ac80..6c830df2c73 100644 --- a/src/test/java/org/olat/repository/RepositoryManagerTest.java +++ b/src/test/java/org/olat/repository/RepositoryManagerTest.java @@ -276,7 +276,7 @@ public class RepositoryManagerTest extends OlatTestCase { repositoryEntryRelationDao.addRole(id, re, GroupRoles.participant.name()); dbInstance.commitAndCloseSession(); - List<RepositoryEntry> entries = repositoryManager.getLearningResourcesAsStudent(id, 0, -1, RepositoryEntryOrder.nameAsc); + List<RepositoryEntry> entries = repositoryManager.getLearningResourcesAsStudent(id, null, 0, -1, RepositoryEntryOrder.nameAsc); Assert.assertNotNull(entries); Assert.assertFalse(entries.isEmpty()); Assert.assertTrue(entries.contains(re)); @@ -298,7 +298,7 @@ public class RepositoryManagerTest extends OlatTestCase { businessGroupRelationDao.addRole(id, group, GroupRoles.participant.name()); dbInstance.commitAndCloseSession(); - List<RepositoryEntry> entries = repositoryManager.getLearningResourcesAsStudent(id, 0, -1); + List<RepositoryEntry> entries = repositoryManager.getLearningResourcesAsStudent(id, null, 0, -1); Assert.assertNotNull(entries); Assert.assertFalse(entries.isEmpty()); Assert.assertTrue(entries.contains(re)); @@ -311,6 +311,41 @@ public class RepositoryManagerTest extends OlatTestCase { } } } + + @Test + public void getLearningResourcesAsBookmark() { + Identity owner = JunitTestHelper.createAndPersistIdentityAsRndUser("webdav-courses-1"); + Identity participant = JunitTestHelper.createAndPersistIdentityAsRndUser("webdav-courses-2"); + RepositoryEntry course = JunitTestHelper.deployBasicCourse(owner); + markManager.setMark(course, participant, null, "[RepositoryEntry:" + course.getKey() + "]"); + dbInstance.commitAndCloseSession(); + + //participant bookmarks + Roles roles = new Roles(false, false, false, false, false, false, false); + List<RepositoryEntry> courses = repositoryManager.getLearningResourcesAsBookmark(participant, roles, "CourseModule", 0, -1); + Assert.assertNotNull(courses); + Assert.assertEquals(1, courses.size()); + } + + /** + * Check that the method return only courses within the permissions of the user. + */ + @Test + public void getLearningResourcesAsBookmark_noPermissions() { + Identity owner = JunitTestHelper.createAndPersistIdentityAsRndUser("webdav-courses-1"); + Identity participant = JunitTestHelper.createAndPersistIdentityAsRndUser("webdav-courses-2"); + RepositoryEntry course = JunitTestHelper.deployBasicCourse(owner); + markManager.setMark(course, participant, null, "[RepositoryEntry:" + course.getKey() + "]"); + dbInstance.commitAndCloseSession(); + repositoryManager.setAccess(course, RepositoryEntry.ACC_OWNERS, false); + dbInstance.commitAndCloseSession(); + + //participant bookmarks + Roles roles = new Roles(false, false, false, false, false, false, false); + List<RepositoryEntry> courses = repositoryManager.getLearningResourcesAsBookmark(participant, roles, "CourseModule", 0, -1); + Assert.assertNotNull(courses); + Assert.assertEquals(0, courses.size()); + } @Test public void getParticipantRepositoryEntry() { diff --git a/src/test/java/org/olat/repository/manager/RepositoryEntryDAOTest.java b/src/test/java/org/olat/repository/manager/RepositoryEntryDAOTest.java index 1c8258409ae..5ac7201d22c 100644 --- a/src/test/java/org/olat/repository/manager/RepositoryEntryDAOTest.java +++ b/src/test/java/org/olat/repository/manager/RepositoryEntryDAOTest.java @@ -25,6 +25,7 @@ import junit.framework.Assert; import org.junit.Test; import org.olat.core.commons.persistence.DB; +import org.olat.core.commons.services.mark.MarkManager; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryService; import org.olat.test.OlatTestCase; @@ -41,9 +42,13 @@ public class RepositoryEntryDAOTest extends OlatTestCase { @Autowired private DB dbInstance; @Autowired + private MarkManager markManager; + @Autowired private RepositoryService repositoryService; @Autowired private RepositoryEntryDAO repositoryEntryDao; + @Autowired + private RepositoryEntryRelationDAO repositoryEntryRelationDao; @Test public void loadByKey() { @@ -69,4 +74,6 @@ public class RepositoryEntryDAOTest extends OlatTestCase { Assert.assertFalse(allRes.isEmpty()); Assert.assertTrue(allRes.size() < 26); } + + } \ No newline at end of file diff --git a/src/test/java/org/olat/test/JunitTestHelper.java b/src/test/java/org/olat/test/JunitTestHelper.java index 48c3101a6f2..d242dc949c5 100644 --- a/src/test/java/org/olat/test/JunitTestHelper.java +++ b/src/test/java/org/olat/test/JunitTestHelper.java @@ -219,4 +219,30 @@ public class JunitTestHelper { } return re; } + + /** + * Deploy a course with only a single page. + * @param initialAuthor + * @return + */ + public static RepositoryEntry deployBasicCourse(Identity initialAuthor) { + String displayname = "Basic course (" + CodeHelper.getForeverUniqueID() + ")"; + String description = "A course with only a single page"; + + RepositoryEntry re = null; + try { + URL courseUrl = JunitTestHelper.class.getResource("file_resources/Basic_course.zip"); + File courseFile = new File(courseUrl.toURI()); + + RepositoryHandler courseHandler = RepositoryHandlerFactory.getInstance() + .getRepositoryHandler(CourseModule.getCourseTypeName()); + re = courseHandler.importResource(initialAuthor, null, displayname, description, true, Locale.ENGLISH, courseFile, null); + + ICourse course = CourseFactory.loadCourse(re.getOlatResource()); + CourseFactory.publishCourse(course, RepositoryEntry.ACC_USERS, false, initialAuthor, Locale.ENGLISH); + } catch (Exception e) { + log.error("", e); + } + return re; + } } diff --git a/src/test/java/org/olat/test/file_resources/Basic_course.zip b/src/test/java/org/olat/test/file_resources/Basic_course.zip new file mode 100644 index 0000000000000000000000000000000000000000..6e887df4e24022d15eb5f85c75d83544892a963e GIT binary patch literal 14021 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 literal 0 HcmV?d00001 -- GitLab