diff --git a/pom.xml b/pom.xml index 35f7d51e055cd27916e3aea985d9e8006d620451..24228ad7d53332e5f10796fc4a185fef89b3241c 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ <version.selenium>3.141.59</version.selenium> <version.drone>2.5.1</version.drone> <activemq.version>5.15.9</activemq.version> - <qtiworks.version>1.0.16</qtiworks.version> + <qtiworks.version>1.0.17</qtiworks.version> <!-- properties for testing and Q&A --> <!-- by default no tests are executed so far (April 2011). Use appropriate profiles and properties on the command line --> @@ -1411,7 +1411,7 @@ <Implementation-Build>${buildNumber}</Implementation-Build> </manifestEntries> </archive> - <warSourceExcludes>**/*.pxm, **/*.psd, **/*.scss, **/*.sh, static/bootstrap/**, **/*.README</warSourceExcludes> + <warSourceExcludes>**/*.pxm, **/*.psd, **/*.sh, static/bootstrap/**, **/*.README</warSourceExcludes> <webResources> <resource> <directory>src/main/webapp</directory> diff --git a/src/main/java/org/olat/core/commons/services/vfs/restapi/VFSDepthException.java b/src/main/java/org/olat/core/commons/services/vfs/restapi/VFSDepthException.java new file mode 100644 index 0000000000000000000000000000000000000000..bb352be525206be0e81d55caf7dd8c5cb04a9079 --- /dev/null +++ b/src/main/java/org/olat/core/commons/services/vfs/restapi/VFSDepthException.java @@ -0,0 +1,37 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.core.commons.services.vfs.restapi; + +import java.io.IOException; + +/** + * + * + * Initial date: 6 juin 2019<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class VFSDepthException extends IOException { + + private static final long serialVersionUID = 7550064282144460698L; + + + +} diff --git a/src/main/java/org/olat/core/commons/services/vfs/restapi/VFSWebservice.java b/src/main/java/org/olat/core/commons/services/vfs/restapi/VFSWebservice.java index 03f3e71be1f67f094677c6c9cf51a7458ca696fd..c6f848a68266324a528c3f43b29a45dde3f3b523 100644 --- a/src/main/java/org/olat/core/commons/services/vfs/restapi/VFSWebservice.java +++ b/src/main/java/org/olat/core/commons/services/vfs/restapi/VFSWebservice.java @@ -74,8 +74,9 @@ public class VFSWebservice { private static final String VERSION = "1.0"; private static final Logger log = Tracing.createLoggerFor(VFSWebservice.class); + private static final int MAX_FOLDER_DEPTH = 20; - public static CacheControl cc = new CacheControl(); + private static final CacheControl cc = new CacheControl(); static { cc.setMaxAge(-1); } @@ -182,8 +183,13 @@ public class VFSWebservice { public Response postFile64ToRoot(@FormParam("foldername") String foldername, @FormParam("filename") String filename, @FormParam("file") String file, @Context UriInfo uriInfo) { byte[] fileAsBytes = Base64.decodeBase64(file); - InputStream in = new ByteArrayInputStream(fileAsBytes); - return putFile(foldername, filename, in, uriInfo, Collections.<PathSegment>emptyList()); + try(InputStream in = new ByteArrayInputStream(fileAsBytes)) { + return putFile(foldername, filename, in, uriInfo, Collections.<PathSegment>emptyList()); + } catch (VFSDepthException e) { + return Response.serverError().status(Status.NOT_ACCEPTABLE).build(); + } catch (IOException e) { + return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); + } } /** @@ -224,8 +230,13 @@ public class VFSWebservice { public Response postFile64ToFolder(@FormParam("foldername") String foldername, @FormParam("filename") String filename, @FormParam("file") String file, @Context UriInfo uriInfo, @PathParam("path") List<PathSegment> path) { byte[] fileAsBytes = Base64.decodeBase64(file); - InputStream in = new ByteArrayInputStream(fileAsBytes); - return putFile(foldername, filename, in, uriInfo, path); + try(InputStream in = new ByteArrayInputStream(fileAsBytes)) { + return putFile(foldername, filename, in, uriInfo, path); + } catch (VFSDepthException e) { + return Response.serverError().status(Status.NOT_ACCEPTABLE).build(); + } catch (IOException e) { + return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); + } } /** @@ -260,8 +271,13 @@ public class VFSWebservice { @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response putFile64VOToRoot(File64VO file, @Context UriInfo uriInfo) { byte[] fileAsBytes = Base64.decodeBase64(file.getFile()); - InputStream in = new ByteArrayInputStream(fileAsBytes); - return putFile(null, file.getFilename(), in, uriInfo, Collections.<PathSegment>emptyList()); + try(InputStream in = new ByteArrayInputStream(fileAsBytes)) { + return putFile(null, file.getFilename(), in, uriInfo, Collections.<PathSegment>emptyList()); + } catch (VFSDepthException e) { + return Response.serverError().status(Status.NOT_ACCEPTABLE).build(); + } catch (IOException e) { + return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); + } } /** @@ -300,6 +316,9 @@ public class VFSWebservice { } catch (FileNotFoundException e) { log.error("", e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); + } catch (VFSDepthException e) { + log.error("", e); + return Response.serverError().status(Status.NOT_ACCEPTABLE).build(); } finally { MultipartReader.closeQuietly(partsReader); IOUtils.closeQuietly(in); @@ -323,8 +342,13 @@ public class VFSWebservice { @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response putFile64ToFolder(File64VO file, @Context UriInfo uriInfo, @PathParam("path") List<PathSegment> path) { byte[] fileAsBytes = Base64.decodeBase64(file.getFile()); - InputStream in = new ByteArrayInputStream(fileAsBytes); - return putFile(null, file.getFilename(), in, uriInfo, path); + try(InputStream in = new ByteArrayInputStream(fileAsBytes)) { + return putFile(null, file.getFilename(), in, uriInfo, path); + } catch (VFSDepthException e) { + return Response.serverError().status(Status.NOT_ACCEPTABLE).build(); + } catch(IOException e) { + return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); + } } /** @@ -368,15 +392,23 @@ public class VFSWebservice { if(container.getLocalSecurityCallback() != null && !container.getLocalSecurityCallback().canWrite()) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } + if(path.size() >= MAX_FOLDER_DEPTH) { + return Response.serverError().status(Status.NOT_ACCEPTABLE).build(); + } - VFSContainer directory = resolveContainer(path, true); - if(directory == null) { - return Response.serverError().status(Status.NOT_FOUND).build(); + try { + VFSContainer directory = resolveContainer(path, true); + if(directory == null) { + return Response.serverError().status(Status.NOT_FOUND).build(); + } + return Response.ok(createFileVO(directory, uriInfo)).build(); + } catch (VFSDepthException e) { + return Response.serverError().status(Status.NOT_ACCEPTABLE).build(); } - return Response.ok(createFileVO(directory, uriInfo)).build(); } - protected Response putFile(String foldername, String filename, InputStream file, UriInfo uriInfo, List<PathSegment> path) { + private Response putFile(String foldername, String filename, InputStream file, UriInfo uriInfo, List<PathSegment> path) + throws VFSDepthException { if(container.getLocalSecurityCallback() != null && !container.getLocalSecurityCallback().canWrite()) { return Response.serverError().status(Status.UNAUTHORIZED).build(); } @@ -476,15 +508,20 @@ public class VFSWebservice { return Response.serverError().status(Status.BAD_REQUEST).build(); } - protected VFSContainer resolveContainer(List<PathSegment> path, boolean create) { + protected VFSContainer resolveContainer(List<PathSegment> path, boolean create) + throws VFSDepthException { VFSContainer directory = container; boolean notFound = false; //remove trailing segment if a trailing / is used - if(path.size() > 0 && !StringHelper.containsNonWhitespace(path.get(path.size() - 1).getPath())) { + if(!path.isEmpty() && !StringHelper.containsNonWhitespace(path.get(path.size() - 1).getPath())) { path = path.subList(0, path.size() -1); } + if(create && path.size() >= MAX_FOLDER_DEPTH) { + throw new VFSDepthException(); + } + a_a: for(PathSegment seg:path) { String segPath = seg.getPath(); @@ -517,7 +554,7 @@ public class VFSWebservice { boolean notFound = false; //remove trailing segment if a trailing / is used - if(path.size() > 0 && !StringHelper.containsNonWhitespace(path.get(path.size() - 1).getPath())) { + if(!path.isEmpty() && !StringHelper.containsNonWhitespace(path.get(path.size() - 1).getPath())) { path = path.subList(0, path.size() -1); } diff --git a/src/main/java/org/olat/course/nodes/iq/IQRunController.java b/src/main/java/org/olat/course/nodes/iq/IQRunController.java index 3e46c174faab3df35dffbcfaad251a0d4bc2523f..f80369dcb3e37644426a0df0ad49576f30a4eb2c 100644 --- a/src/main/java/org/olat/course/nodes/iq/IQRunController.java +++ b/src/main/java/org/olat/course/nodes/iq/IQRunController.java @@ -27,7 +27,8 @@ package org.olat.course.nodes.iq; import java.io.File; import java.text.DateFormat; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; @@ -222,25 +223,34 @@ public class IQRunController extends BasicController implements GenericEventList Formatter formatter = Formatter.getInstance(ureq.getLocale()); ImsRepositoryResolver resolver = new ImsRepositoryResolver(referenceTestEntry); QTIChangeLogMessage[] qtiChangeLog = resolver.getDocumentChangeLog(); + StringBuilder qtiChangelog = new StringBuilder(); if(qtiChangeLog.length>0){ + List<QTIChangeLogMessage> qtiChangeLogList = new ArrayList<>(qtiChangeLog.length); + for (int i=qtiChangeLog.length; i-->0 ; ) { + if(qtiChangeLog[i] != null) { + qtiChangeLogList.add(qtiChangeLog[i]); + } + } //there are resource changes - Arrays.sort(qtiChangeLog); - for (int i = qtiChangeLog.length-1; i >= 0 ; i--) { + Collections.sort(qtiChangeLogList); + + for (int i = qtiChangeLogList.size()-1; i >= 0 ; i--) { + QTIChangeLogMessage qtiChangeLogEntry = qtiChangeLogList.get(i); //show latest change first - if(!showAll && qtiChangeLog[i].isPublic()){ + if(!showAll && qtiChangeLogEntry.isPublic()){ //logged in person is a normal user, hence public messages only - Date msgDate = new Date(qtiChangeLog[i].getTimestmp()); + Date msgDate = new Date(qtiChangeLogEntry.getTimestmp()); qtiChangelog.append("\nChange date: ").append(formatter.formatDateAndTime(msgDate)).append("\n"); - String msg = StringHelper.escapeHtml(qtiChangeLog[i].getLogMessage()); + String msg = StringHelper.escapeHtml(qtiChangeLogEntry.getLogMessage()); qtiChangelog.append(msg); qtiChangelog.append("\n********************************\n"); }else if (showAll){ //logged in person is an author, olat admin, owner, show all messages - Date msgDate = new Date(qtiChangeLog[i].getTimestmp()); + Date msgDate = new Date(qtiChangeLogEntry.getTimestmp()); qtiChangelog.append("\nChange date: ").append(formatter.formatDateAndTime(msgDate)).append("\n"); - String msg = StringHelper.escapeHtml(qtiChangeLog[i].getLogMessage()); + String msg = StringHelper.escapeHtml(qtiChangeLogEntry.getLogMessage()); qtiChangelog.append(msg); qtiChangelog.append("\n********************************\n"); }//else non public messages are not shown to normal user diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java index 61f1bdc31ecf9591b25f4174ba99a1b941ad8fb2..5dbb56207b0a45e28fd182dc1a312cb01d144863 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java @@ -35,6 +35,7 @@ import uk.ac.ed.ph.jqtiplus.node.item.ModalFeedback; import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; import uk.ac.ed.ph.jqtiplus.node.result.SessionStatus; import uk.ac.ed.ph.jqtiplus.node.test.AbstractPart; +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest; import uk.ac.ed.ph.jqtiplus.node.test.TestPart; @@ -174,9 +175,16 @@ public class AssessmentTestComponent extends AssessmentObjectComponent { } try { - URI itemSystemId = itemNode.getItemSystemId(); + AssessmentItemRef itemRef = getResolvedAssessmentTest().getItemRefsByIdentifierMap() + .get(itemNode.getKey().getIdentifier()); + if(itemRef == null) { + return false; + } ResolvedAssessmentItem resolvedAssessmentItem = getResolvedAssessmentTest() - .getResolvedAssessmentItemBySystemIdMap().get(itemSystemId); + .getResolvedAssessmentItem(itemRef); + if(resolvedAssessmentItem == null) { + return false; + } AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); if(assessmentItem.getAdaptive()) { return true; diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java index dc4104cdea4e0577f5c51b3bce594b4ae77b6088..6534f5f817118e862316419526fff3839387f143 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java @@ -25,7 +25,6 @@ import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.testFee import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; -import java.net.URI; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -70,6 +69,7 @@ import uk.ac.ed.ph.jqtiplus.node.content.variable.RubricBlock; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; import uk.ac.ed.ph.jqtiplus.node.item.template.declaration.TemplateDeclaration; import uk.ac.ed.ph.jqtiplus.node.outcome.declaration.OutcomeDeclaration; +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection; import uk.ac.ed.ph.jqtiplus.node.test.NavigationMode; import uk.ac.ed.ph.jqtiplus.node.test.TestFeedback; @@ -380,17 +380,23 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe private void renderTestItemBody(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestPlanNode itemNode, URLBuilder ubu, Translator translator, RenderingRequest options) { - final ItemSessionState itemSessionState = component.getItemSessionState(itemNode.getKey()); - - URI itemSystemId = itemNode.getItemSystemId(); + + AssessmentItemRef itemRef = component.getResolvedAssessmentTest() + .getItemRefsByIdentifierMap().get(itemNode.getKey().getIdentifier()); + if(itemRef == null) { + log.error("Missing assessment item ref: " + itemNode.getKey()); + renderMissingItem(sb, translator); + return; + } ResolvedAssessmentItem resolvedAssessmentItem = component.getResolvedAssessmentTest() - .getResolvedAssessmentItemBySystemIdMap().get(itemSystemId); + .getResolvedAssessmentItem(itemRef); if(resolvedAssessmentItem == null) { - log.error("Missing assessment item: " + itemSystemId); + log.error("Missing assessment item: " + itemNode.getKey()); renderMissingItem(sb, translator); return; } - + + final ItemSessionState itemSessionState = component.getItemSessionState(itemNode.getKey()); final AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); sb.append("<div class='o_assessmentitem_wrapper'>"); diff --git a/src/main/java/org/olat/user/ui/admin/UserAdminMainController.java b/src/main/java/org/olat/user/ui/admin/UserAdminMainController.java index c2635cebbc6fd1aafb34f32ff96f5bd4c00c5b09..54a689abeeab8f84ed4f1ccb34f27e38a5661be2 100644 --- a/src/main/java/org/olat/user/ui/admin/UserAdminMainController.java +++ b/src/main/java/org/olat/user/ui/admin/UserAdminMainController.java @@ -62,6 +62,7 @@ import org.olat.core.gui.components.stack.TooledStackedPanel.Align; import org.olat.core.gui.components.tree.GenericTreeModel; import org.olat.core.gui.components.tree.GenericTreeNode; import org.olat.core.gui.components.tree.MenuTree; +import org.olat.core.gui.components.tree.TreeEvent; import org.olat.core.gui.components.tree.TreeModel; import org.olat.core.gui.components.tree.TreeNode; import org.olat.core.gui.control.Controller; @@ -209,9 +210,11 @@ public class UserAdminMainController extends MainLayoutBasicController implement @Override public void event(UserRequest ureq, Component source, Event event) { if (source == menuTree) { - if (event.getCommand().equals(MenuTree.COMMAND_TREENODE_CLICKED)) { - TreeNode selTreeNode = menuTree.getSelectedNode(); - contentCtr = pushController(ureq, selTreeNode); + if (event.getCommand().equals(MenuTree.COMMAND_TREENODE_CLICKED) && event instanceof TreeEvent) { + TreeNode selTreeNode = menuTree.getTreeModel().getNodeById(((TreeEvent)event).getNodeId()); + if(selTreeNode != null) { + contentCtr = pushController(ureq, selTreeNode); + } } else { // the action was not allowed anymore content.popUpToRootController(ureq); } diff --git a/src/test/java/org/olat/restapi/CoursesFoldersTest.java b/src/test/java/org/olat/restapi/CoursesFoldersTest.java index 7a17016f1b894dda53312660b7351b34a1f8425e..53b06f5ad4279eab5ad3adb73a0431a6648a37f1 100644 --- a/src/test/java/org/olat/restapi/CoursesFoldersTest.java +++ b/src/test/java/org/olat/restapi/CoursesFoldersTest.java @@ -219,6 +219,20 @@ public class CoursesFoldersTest extends OlatJerseyTestCase { assertTrue(item2 instanceof VFSContainer); } + @Test + public void testCreateFolders_tooMany() throws IOException, URISyntaxException { + assertTrue(conn.login("administrator", "openolat")); + + URI uri = UriBuilder.fromUri(getNodeURI()).path("files").path("RootFolder") + .path("Folder").path("Folder").path("Folder").path("Folder").path("Folder") + .path("Folder").path("Folder").path("Folder").path("Folder").path("Folder") + .path("Folder").path("Folder").path("Folder").path("Folder").path("Folder") + .path("Folder").path("Folder").path("Folder").path("Folder").path("Folder").build(); + HttpPut method = conn.createPut(uri, MediaType.APPLICATION_JSON, true); + HttpResponse response = conn.execute(method); + assertEquals(406, response.getStatusLine().getStatusCode()); + } + @Test public void deleteFolder() throws IOException, URISyntaxException { //add some folders