From 327c8756c2de6312a989a0def4697bb5fe9670d6 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Mon, 31 Jul 2017 11:27:38 +0200
Subject: [PATCH] OO-2931: allow images and videos in the rubric of a QTI 2.1
 section

---
 .../model/xml/AssessmentHtmlBuilder.java      | 63 ++++++++++++++++++-
 .../ui/AssessmentTestDisplayController.java   |  4 +-
 .../AssessmentTestComponentRenderer.java      |  3 +-
 .../AssessmentSectionEditorController.java    | 15 ++++-
 ...essmentSectionOptionsEditorController.java | 22 +++++--
 .../AssessmentTestComposerController.java     |  4 +-
 6 files changed, 97 insertions(+), 14 deletions(-)

diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentHtmlBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentHtmlBuilder.java
index 9b19ebdd7da..6f54181f45b 100644
--- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentHtmlBuilder.java
+++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentHtmlBuilder.java
@@ -21,6 +21,7 @@ package org.olat.ims.qti21.model.xml;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -33,7 +34,7 @@ import org.olat.core.gui.render.StringOutput;
 import org.olat.core.logging.OLog;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.StringHelper;
-import org.olat.core.util.filter.FilterFactory;
+import org.olat.core.util.filter.impl.NekoHTMLFilter;
 import org.w3c.dom.Attr;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
@@ -41,6 +42,7 @@ import org.w3c.dom.Node;
 import org.xml.sax.Attributes;
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
 
 import uk.ac.ed.ph.jqtiplus.JqtiExtensionManager;
 import uk.ac.ed.ph.jqtiplus.exception.QtiModelException;
@@ -76,7 +78,24 @@ public class AssessmentHtmlBuilder {
 	}
 	
 	public boolean containsSomething(String html) {
-		return StringHelper.containsNonWhitespace(FilterFactory.getHtmlTagsFilter().filter(html));
+		if(!StringHelper.containsNonWhitespace(html)) return false;
+
+		try {
+			SAXParser parser = new SAXParser();
+			ContentDetectionHandler contentHandler = new ContentDetectionHandler();
+			parser.setContentHandler(contentHandler);
+			parser.parse(new InputSource(new StringReader(html)));
+			return contentHandler.isContentAvailable();
+		} catch (SAXException e) {
+			log.error("", e);
+			return false;
+		} catch (IOException e) {
+			log.error("", e);
+			return false;
+		} catch (Exception e) {
+			log.error("", e);
+			return false;
+		}
 	}
 	
 	public String flowStaticString(List<? extends FlowStatic> statics) {
@@ -497,4 +516,44 @@ public class AssessmentHtmlBuilder {
 			//
 		}
 	}
+	
+	private static class ContentDetectionHandler extends DefaultHandler {
+		
+		private boolean collect = false;
+		private boolean content = false;
+		
+		public boolean isContentAvailable() {
+			return content;
+		}
+
+		@Override
+		public void startElement(String uri, String localName, String qName, Attributes attributes) {
+			String elem = localName.toLowerCase();
+			if("script".equals(elem)) {
+				collect = false;
+			} else if(!NekoHTMLFilter.blockTags.contains(localName)) {
+				content = true;
+			}
+		}
+		
+		@Override
+		public void characters(char[] chars, int offset, int length) {
+			if(!content && collect && offset >= 0 && length > 0) {
+				String text = new String(chars, offset, length);
+				if(text.trim().length() > 0) {
+					content = true;
+				}
+			}
+		}
+
+		@Override
+		public void endElement(String uri, String localName, String qName) {
+			String elem = localName.toLowerCase();
+			if("script".equals(elem)) {
+				collect = true;
+			} else if(!NekoHTMLFilter.blockTags.contains(localName)) {
+				content = true;
+			}
+		}
+	}
 }
diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java
index 4c57f68c40a..af2d32f20c8 100644
--- a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java
@@ -367,8 +367,8 @@ public class AssessmentTestDisplayController extends BasicController implements
 	
 	private void initQtiWorks(UserRequest ureq) {
 		qtiWorksCtrl = new QtiWorksController(ureq, getWindowControl());
-    	listenTo(qtiWorksCtrl);
-    	mainVC.put("qtirun", qtiWorksCtrl.getInitialComponent());
+		listenTo(qtiWorksCtrl);
+		mainVC.put("qtirun", qtiWorksCtrl.getInitialComponent());
 	}
 	
 	@Override
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 94438a7f5f9..323a255b914 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
@@ -297,7 +297,7 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe
 		}
 		
 		if(writeRubrics) {
-			sb.append("<div class='o_info o_assessmentsection_rubrics'>");
+			sb.append("<div class='o_info o_assessmentsection_rubrics clearfix'>");
 			//write the titles first
 			if(writeTitles) {
 				sb.append("<h4>");
@@ -316,7 +316,6 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe
 				sb.append("</h4>");
 			}
 			
-
 			for(int i=sectionParentLine.size(); i-->0; ) {
 				AssessmentSection selectedSection = sectionParentLine.get(i);
 				for(RubricBlock rubricBlock:selectedSection.getRubricBlocks()) {
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionEditorController.java
index 3289b05ecbb..ddf06f62955 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionEditorController.java
@@ -19,6 +19,8 @@
  */
 package org.olat.ims.qti21.ui.editor;
 
+import java.io.File;
+
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.tabbedpane.TabbedPane;
@@ -28,6 +30,7 @@ import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
 import org.olat.core.util.Util;
+import org.olat.core.util.vfs.VFSContainer;
 import org.olat.ims.qti21.ui.AssessmentTestDisplayController;
 import org.olat.ims.qti21.ui.editor.events.AssessmentSectionEvent;
 
@@ -44,6 +47,10 @@ public class AssessmentSectionEditorController extends BasicController {
 	private final TabbedPane tabbedPane;
 	private final VelocityContainer mainVC;
 	
+	private final File testFile;
+	private final File rootDirectory;
+	private final VFSContainer rootContainer;
+	
 	private final AssessmentSection section;
 	
 	private final boolean editable;
@@ -53,10 +60,14 @@ public class AssessmentSectionEditorController extends BasicController {
 	private AssessmentSectionExpertOptionsEditorController expertOptionsCtrl;
 	
 	public AssessmentSectionEditorController(UserRequest ureq, WindowControl wControl,
-			AssessmentSection section, boolean restrictedEdit, boolean editable) {
+			AssessmentSection section, File rootDirectory, VFSContainer rootContainer, File testFile,
+			boolean restrictedEdit, boolean editable) {
 		super(ureq, wControl, Util.createPackageTranslator(AssessmentTestDisplayController.class, ureq.getLocale()));
 		this.section = section;
 		this.editable = editable;
+		this.testFile = testFile;
+		this.rootDirectory = rootDirectory;
+		this.rootContainer = rootContainer;
 		this.restrictedEdit = restrictedEdit;
 		
 		mainVC = createVelocityContainer("assessment_test_editor");
@@ -71,7 +82,7 @@ public class AssessmentSectionEditorController extends BasicController {
 	}
 
 	private void initSectionEditor(UserRequest ureq) {
-		optionsCtrl = new AssessmentSectionOptionsEditorController(ureq, getWindowControl(), section, restrictedEdit, editable);
+		optionsCtrl = new AssessmentSectionOptionsEditorController(ureq, getWindowControl(), section, rootDirectory, rootContainer, testFile, restrictedEdit, editable);
 		listenTo(optionsCtrl);
 		expertOptionsCtrl = new AssessmentSectionExpertOptionsEditorController(ureq, getWindowControl(), section, restrictedEdit, editable);
 		listenTo(expertOptionsCtrl);
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionOptionsEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionOptionsEditorController.java
index a38369ed5fb..9b67a4ecb88 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionOptionsEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionOptionsEditorController.java
@@ -19,6 +19,7 @@
  */
 package org.olat.ims.qti21.ui.editor;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -35,6 +36,7 @@ import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.Util;
+import org.olat.core.util.vfs.VFSContainer;
 import org.olat.ims.qti21.model.xml.AssessmentHtmlBuilder;
 import org.olat.ims.qti21.ui.AssessmentTestDisplayController;
 import org.olat.ims.qti21.ui.editor.events.AssessmentSectionEvent;
@@ -57,6 +59,10 @@ public class AssessmentSectionOptionsEditorController extends FormBasicControlle
 	private SingleSelection shuffleEl, randomSelectedEl;
 	private List<RichTextElement> rubricEls = new ArrayList<>();
 	
+	private final File testFile;
+	private final File rootDirectory;
+	private final VFSContainer rootContainer;
+	
 	private final AssessmentSection section;
 	private final AssessmentHtmlBuilder htmlBuilder;
 	
@@ -66,10 +72,14 @@ public class AssessmentSectionOptionsEditorController extends FormBasicControlle
 	private static final String[] yesnoKeys = new String[]{ "y", "n"};
 	
 	public AssessmentSectionOptionsEditorController(UserRequest ureq, WindowControl wControl,
-			AssessmentSection section, boolean restrictedEdit, boolean editable) {
+			AssessmentSection section, File rootDirectory, VFSContainer rootContainer, File testFile,
+			boolean restrictedEdit, boolean editable) {
 		super(ureq, wControl, Util.createPackageTranslator(AssessmentTestDisplayController.class, ureq.getLocale()));
 		this.section = section;
 		this.editable = editable;
+		this.testFile = testFile;
+		this.rootDirectory = rootDirectory;
+		this.rootContainer = rootContainer;
 		this.restrictedEdit = restrictedEdit;
 		htmlBuilder = new AssessmentHtmlBuilder();
 		initForm(ureq);
@@ -87,9 +97,11 @@ public class AssessmentSectionOptionsEditorController extends FormBasicControlle
 		titleEl = uifactory.addTextElement("title", "form.metadata.title", 255, title, formLayout);
 		titleEl.setEnabled(editable);
 		titleEl.setMandatory(true);
-		
+
+		String relativePath = rootDirectory.toPath().relativize(testFile.toPath().getParent()).toString();
+		VFSContainer itemContainer = (VFSContainer)rootContainer.resolve(relativePath);
 		if(section.getRubricBlocks().isEmpty()) {
-			RichTextElement rubricEl = uifactory.addRichTextElementForQTI21("rubric" + counter++, "form.imd.rubric", "", 8, -1, null,
+			RichTextElement rubricEl = uifactory.addRichTextElementForQTI21("rubric" + counter++, "form.imd.rubric", "", 12, -1, itemContainer,
 					formLayout, ureq.getUserSession(), getWindowControl());
 			rubricEl.getEditorConfiguration().setFileBrowserUploadRelPath("media");
 			rubricEl.setEnabled(editable);
@@ -97,7 +109,7 @@ public class AssessmentSectionOptionsEditorController extends FormBasicControlle
 		} else {
 			for(RubricBlock rubricBlock:section.getRubricBlocks()) {
 				String rubric = htmlBuilder.blocksString(rubricBlock.getBlocks());
-				RichTextElement rubricEl = uifactory.addRichTextElementForQTI21("rubric" + counter++, "form.imd.rubric", rubric, 8, -1, null,
+				RichTextElement rubricEl = uifactory.addRichTextElementForQTI21("rubric" + counter++, "form.imd.rubric", rubric, 12, -1, itemContainer,
 						formLayout, ureq.getUserSession(), getWindowControl());
 				rubricEl.getEditorConfiguration().setFileBrowserUploadRelPath("media");
 				rubricEl.setEnabled(editable);
@@ -191,7 +203,7 @@ public class AssessmentSectionOptionsEditorController extends FormBasicControlle
 		//rubrics
 		List<RubricBlock> rubricBlocks = new ArrayList<>();
 		for(RichTextElement rubricEl:rubricEls) {
-			String rubric = rubricEl.getValue();
+			String rubric = rubricEl.getRawValue();
 			if(htmlBuilder.containsSomething(rubric)) {
 				RubricBlock rubricBlock = (RubricBlock)rubricEl.getUserObject();
 				if(rubricBlock == null) {
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java
index bd2159fa6e1..3ee92648531 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java
@@ -1120,8 +1120,10 @@ public class AssessmentTestComposerController extends MainLayoutBasicController
 			currentEditorCtrl = new AssessmentTestPartEditorController(ureq, getWindowControl(), (TestPart)uobject,
 					restrictedEdit, assessmentTestBuilder.isEditable());
 		} else if(uobject instanceof AssessmentSection) {
+			URI testURI = resolvedAssessmentTest.getTestLookup().getSystemId();
+			File testFile = new File(testURI);
 			currentEditorCtrl = new AssessmentSectionEditorController(ureq, getWindowControl(), (AssessmentSection)uobject,
-					restrictedEdit, assessmentTestBuilder.isEditable());
+					unzippedDirRoot, unzippedContRoot, testFile, restrictedEdit, assessmentTestBuilder.isEditable());
 		} else if(uobject instanceof AssessmentItemRef) {
 			AssessmentItemRef itemRef = (AssessmentItemRef)uobject;
 			ResolvedAssessmentItem item = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef);
-- 
GitLab