From c9e4c4d57252814cb46c47cd2aa747889f6c51ac Mon Sep 17 00:00:00 2001
From: srosse <stephane.rosse@frentix.com>
Date: Mon, 22 Jun 2020 11:54:31 +0200
Subject: [PATCH] OO-4740: support inline images in forum

---
 .../collaboration/CollaborationTools.java     |  66 ---
 .../form/flexible/FormUIFactory.java          |  12 +-
 .../richText/RichTextConfiguration.java       |  20 +-
 .../richText/RichTextContainerMapper.java     |  70 +++
 .../PersistingCourseGroupManager.java         |   2 +-
 .../olat/course/nodes/DialogCourseNode.java   |  33 +-
 .../org/olat/course/nodes/FOCourseNode.java   |  14 +-
 .../org/olat/group/BusinessGroupService.java  |   3 +-
 .../manager/BusinessGroupImportExport.java    |  12 +-
 .../manager/BusinessGroupServiceImpl.java     |   4 +-
 .../modules/fo/archiver/ForumArchive.java     | 275 ++++++++++
 .../fo/archiver/ForumArchiveManager.java      | 204 --------
 .../formatters/ForumDownloadResource.java     |  65 +--
 .../archiver/formatters/ForumFormatter.java   | 117 -----
 .../formatters/ForumOpenXMLFormatter.java     |  36 +-
 .../formatters/ForumRTFFormatter.java         | 468 ------------------
 .../formatters/ForumStreamedRTFFormatter.java | 418 ----------------
 .../olat/modules/fo/manager/ForumManager.java |  26 +-
 .../olat/modules/fo/manager/QuoterFilter.java | 103 ++++
 .../modules/fo/ui/MessageEditController.java  | 295 +++++++----
 .../modules/fo/ui/MessageListController.java  | 166 ++++---
 .../org/olat/modules/fo/ui/MessageView.java   |  14 +-
 .../modules/fo/ui/ThreadListController.java   |   4 +-
 23 files changed, 855 insertions(+), 1572 deletions(-)
 create mode 100644 src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/richText/RichTextContainerMapper.java
 create mode 100644 src/main/java/org/olat/modules/fo/archiver/ForumArchive.java
 delete mode 100644 src/main/java/org/olat/modules/fo/archiver/ForumArchiveManager.java
 delete mode 100644 src/main/java/org/olat/modules/fo/archiver/formatters/ForumFormatter.java
 delete mode 100644 src/main/java/org/olat/modules/fo/archiver/formatters/ForumRTFFormatter.java
 delete mode 100644 src/main/java/org/olat/modules/fo/archiver/formatters/ForumStreamedRTFFormatter.java
 create mode 100644 src/main/java/org/olat/modules/fo/manager/QuoterFilter.java

diff --git a/src/main/java/org/olat/collaboration/CollaborationTools.java b/src/main/java/org/olat/collaboration/CollaborationTools.java
index 2641f530d79..d60f116dc9f 100644
--- a/src/main/java/org/olat/collaboration/CollaborationTools.java
+++ b/src/main/java/org/olat/collaboration/CollaborationTools.java
@@ -25,8 +25,6 @@
 
 package org.olat.collaboration;
 
-import java.io.File;
-import java.io.IOException;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -59,20 +57,15 @@ import org.olat.core.id.context.ContextEntry;
 import org.olat.core.logging.AssertException;
 import org.olat.core.logging.Tracing;
 import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
-import org.olat.core.util.FileUtils;
 import org.olat.core.util.Util;
-import org.olat.core.util.ZipUtil;
 import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.core.util.coordinate.SyncerCallback;
 import org.olat.core.util.coordinate.SyncerExecutor;
-import org.olat.core.util.i18n.I18nModule;
 import org.olat.core.util.mail.ContactMessage;
-import org.olat.core.util.vfs.LocalFolderImpl;
 import org.olat.core.util.vfs.NamedContainerImpl;
 import org.olat.core.util.vfs.Quota;
 import org.olat.core.util.vfs.QuotaManager;
 import org.olat.core.util.vfs.VFSContainer;
-import org.olat.core.util.vfs.VFSLeaf;
 import org.olat.core.util.vfs.VFSManager;
 import org.olat.core.util.vfs.callbacks.VFSSecurityCallback;
 import org.olat.course.CorruptedCourseException;
@@ -94,9 +87,6 @@ import org.olat.modules.co.ContactFormController;
 import org.olat.modules.fo.Forum;
 import org.olat.modules.fo.ForumCallback;
 import org.olat.modules.fo.ForumUIFactory;
-import org.olat.modules.fo.archiver.ForumArchiveManager;
-import org.olat.modules.fo.archiver.formatters.ForumFormatter;
-import org.olat.modules.fo.archiver.formatters.ForumRTFFormatter;
 import org.olat.modules.fo.manager.ForumManager;
 import org.olat.modules.openmeetings.OpenMeetingsModule;
 import org.olat.modules.openmeetings.manager.OpenMeetingsException;
@@ -117,7 +107,6 @@ import org.olat.modules.wiki.DryRunAssessmentProvider;
 import org.olat.modules.wiki.WikiManager;
 import org.olat.modules.wiki.WikiSecurityCallback;
 import org.olat.modules.wiki.WikiSecurityCallbackImpl;
-import org.olat.modules.wiki.WikiToZipUtils;
 import org.olat.properties.NarrowedPropertyManager;
 import org.olat.properties.Property;
 import org.olat.properties.PropertyManager;
@@ -1022,61 +1011,6 @@ public class CollaborationTools implements Serializable {
 		}
 	}
 
-	/**
-	 * It is assumed that this is only called by an administrator
-	 * (e.g. at deleteGroup) 
-	 * @param archivFilePath
-	 */
-	public void archive(String archivFilePath) {
-		if (isToolEnabled(CollaborationTools.TOOL_FORUM)) {
-			archiveForum(archivFilePath);
-		}
-		if (isToolEnabled(CollaborationTools.TOOL_WIKI)) {
-			archiveWiki(archivFilePath);
-		}
-		if (isToolEnabled(CollaborationTools.TOOL_FOLDER)) {
-			archiveFolder(archivFilePath);
-		}
-	}
-
-	private void archiveForum(String archivFilePath) {
-		Property forumKeyProperty = NarrowedPropertyManager.getInstance(ores).findProperty(null, null, PROP_CAT_BG_COLLABTOOLS, KEY_FORUM);
-		if (forumKeyProperty != null) {
-			VFSContainer archiveContainer = new LocalFolderImpl(new File(archivFilePath));
-			String archiveForumName = "del_forum_" + forumKeyProperty.getLongValue();
-			VFSContainer archiveForumContainer = archiveContainer.createChildContainer(archiveForumName);
-			ForumFormatter ff = new ForumRTFFormatter(archiveForumContainer, false, I18nModule.getDefaultLocale());
-			CoreSpringFactory.getImpl(ForumArchiveManager.class).applyFormatter(ff, forumKeyProperty.getLongValue(), null);
-		}
-	}
-
-	private void archiveWiki(String archivFilePath) { 
-		VFSContainer wikiContainer = WikiManager.getInstance().getWikiRootContainer(ores);
-		VFSLeaf wikiZip = WikiToZipUtils.getWikiAsZip(wikiContainer);
-		String exportFileName = "del_wiki_" + ores.getResourceableId() + ".zip";
-		File archiveDir = new File(archivFilePath);
-		if (!archiveDir.exists()) {
-			archiveDir.mkdir();
-		}
-		String fullFilePath = archivFilePath + File.separator + exportFileName;
-		
-		try {
-			FileUtils.bcopy(wikiZip.getInputStream(), new File(fullFilePath), "archive wiki");
-		} catch (IOException ioe) {
-			log.warn("Can not archive wiki repoEntry={}", ores.getResourceableId());
-		}		
-	}
-
-	private void archiveFolder(String archiveFilePath) {
-		LocalFolderImpl folderContainer = VFSManager.olatRootContainer(getFolderRelPath(), null);
-		File fFolderRoot = folderContainer.getBasefile();
-		if (fFolderRoot.exists()) {
-			String zipFileName = "del_folder_" + ores.getResourceableId() + ".zip";
-			String fullZipFilePath = archiveFilePath + File.separator + zipFileName;
-			ZipUtil.zipAll(fFolderRoot, new File(fullZipFilePath), true);
-		}
-	}
-
 	/**
 	 * whole object gets cached, if tool gets added or deleted the object becomes dirty and will be removed from cache.
 	 * @return
diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/FormUIFactory.java b/src/main/java/org/olat/core/gui/components/form/flexible/FormUIFactory.java
index 774d2cfa86d..c654ce7e746 100644
--- a/src/main/java/org/olat/core/gui/components/form/flexible/FormUIFactory.java
+++ b/src/main/java/org/olat/core/gui/components/form/flexible/FormUIFactory.java
@@ -775,20 +775,26 @@ public class FormUIFactory {
 	 *          text element
 	 * @param usess The user session that dispatches the images
 	 * @param wControl the current window controller
-	 * @param wControl
-	 *            the current window controller
+
 	 * @return The rich text element instance
 	 */
 	public RichTextElement addRichTextElementForStringData(String name, String i18nLabel, String initialHTMLValue, int rows,
 			int cols, boolean fullProfile, VFSContainer baseContainer, CustomLinkTreeModel customLinkTreeModel,
 			FormItemContainer formLayout, UserSession usess, WindowControl wControl) {
+		return addRichTextElementForStringData(name, i18nLabel, initialHTMLValue, rows, cols,
+				fullProfile, baseContainer, null, customLinkTreeModel, formLayout, usess, wControl);
+	}
+	
+	public RichTextElement addRichTextElementForStringData(String name, String i18nLabel, String initialHTMLValue, int rows,
+			int cols, boolean fullProfile, VFSContainer baseContainer, String relFilePath, CustomLinkTreeModel customLinkTreeModel,
+			FormItemContainer formLayout, UserSession usess, WindowControl wControl) {
 		// Create richt text element with bare bone configuration
 		WindowBackOffice backoffice = wControl.getWindowBackOffice();
 		RichTextElement rte = new RichTextElementImpl(name, initialHTMLValue, rows, cols, formLayout.getRootForm(), formLayout.getTranslator().getLocale());
 		setLabelIfNotNull(i18nLabel, rte);
 		// Now configure editor
 		Theme theme = backoffice.getWindow().getGuiTheme();
-		rte.getEditorConfiguration().setConfigProfileFormEditor(fullProfile, usess, theme, baseContainer, customLinkTreeModel);			
+		rte.getEditorConfiguration().setConfigProfileFormEditor(fullProfile, usess, theme, baseContainer, relFilePath, customLinkTreeModel);
 		// Add to form and finish
 		formLayout.add(rte);
 		return rte;
diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/richText/RichTextConfiguration.java b/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/richText/RichTextConfiguration.java
index 1e0704d5b66..aaf1e0ca22f 100644
--- a/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/richText/RichTextConfiguration.java
+++ b/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/richText/RichTextConfiguration.java
@@ -47,6 +47,7 @@ import org.apache.logging.log4j.Logger;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.CodeHelper;
 import org.olat.core.util.Formatter;
+import org.olat.core.util.StringHelper;
 import org.olat.core.util.UserSession;
 import org.olat.core.util.Util;
 import org.olat.core.util.WebappHelper;
@@ -315,7 +316,7 @@ public class RichTextConfiguration implements Disposable {
 	 * @param customLinkTreeModel
 	 */
 	public void setConfigProfileFormEditor(boolean fullProfile, UserSession usess, Theme guiTheme,
-			VFSContainer baseContainer, CustomLinkTreeModel customLinkTreeModel) {
+			VFSContainer baseContainer, String relFilePath, CustomLinkTreeModel customLinkTreeModel) {
 		setConfigBasics(guiTheme);
 
 		TinyMCECustomPluginFactory customPluginFactory = CoreSpringFactory.getImpl(TinyMCECustomPluginFactory.class);
@@ -339,7 +340,7 @@ public class RichTextConfiguration implements Disposable {
 			tinyConfig = tinyConfig.enableImageAndMedia();
 			setFileBrowserCallback(baseContainer, customLinkTreeModel, IMAGE_SUFFIXES_VALUES, MEDIA_SUFFIXES_VALUES, FLASH_PLAYER_SUFFIXES_VALUES);
 			// since in form editor mode and not in file mode we use null as relFilePath
-			setDocumentMediaBase(baseContainer, null, usess);			
+			setDocumentMediaBase(baseContainer, relFilePath, usess);			
 		}
 	}
 
@@ -809,12 +810,15 @@ public class RichTextConfiguration implements Disposable {
 	private void setDocumentMediaBase(final VFSContainer documentBaseContainer, String relFilePath, UserSession usess) {
 		linkBrowserRelativeFilePath = relFilePath;
 		// get a usersession-local mapper for the file storage (and tinymce's references to images and such)
-		Mapper contentMapper = new VFSContainerMapper(documentBaseContainer);
+		Mapper contentMapper;
+		if(StringHelper.containsNonWhitespace(relFilePath)) {
+			contentMapper = new RichTextContainerMapper(documentBaseContainer, relFilePath);
+		} else {
+			contentMapper = new VFSContainerMapper(documentBaseContainer);
+		}
+		
 		// Register mapper for this user. This mapper is cleaned up in the
 		// dispose method (RichTextElementImpl will clean it up)
-
-		
-		
 		// Register mapper as cacheable
 		String mapperID = VFSManager.getRealPath(documentBaseContainer);
 		if (mapperID == null) {
@@ -829,7 +833,7 @@ public class RichTextConfiguration implements Disposable {
 		
 		if (relFilePath != null) {
 			// remove filename, path must end with slash
-			int lastSlash = relFilePath.lastIndexOf("/");
+			int lastSlash = relFilePath.lastIndexOf('/');
 			if (lastSlash == -1) {
 				relFilePath = "";
 			} else if (lastSlash + 1 < relFilePath.length()) {
@@ -841,7 +845,7 @@ public class RichTextConfiguration implements Disposable {
 					LocalFolderImpl folder = (LocalFolderImpl) documentBaseContainer;
 					containerPath = folder.getBasefile().getAbsolutePath();
 				}
-				log.warn("Could not parse relative file path::" + relFilePath + " in container::" + containerPath);
+				log.warn("Could not parse relative file path::{} in container::{}", relFilePath, containerPath);
 			}
 		} else {
 			relFilePath = "";
diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/richText/RichTextContainerMapper.java b/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/richText/RichTextContainerMapper.java
new file mode 100644
index 00000000000..c4e5d788123
--- /dev/null
+++ b/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/richText/RichTextContainerMapper.java
@@ -0,0 +1,70 @@
+/**
+ * <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.gui.components.form.flexible.impl.elements.richText;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.olat.core.dispatcher.mapper.Mapper;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.gui.media.NotFoundMediaResource;
+import org.olat.core.util.vfs.VFSContainer;
+import org.olat.core.util.vfs.VFSItem;
+import org.olat.core.util.vfs.VFSLeaf;
+import org.olat.core.util.vfs.VFSMediaResource;
+
+/**
+ * 
+ * Initial date: 18 juin 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class RichTextContainerMapper implements Mapper {
+	
+	private String path;
+	private final VFSContainer container;
+	
+	public RichTextContainerMapper(VFSContainer container, String relFilePath) {
+		this.container = container;
+		path = relFilePath;
+		if(!path.startsWith("/")) {
+			path = "/" + path;
+		}
+		if(!path.endsWith("/")) {
+			path += "/";
+		}
+	}
+	
+	@Override
+	public MediaResource handle(String relPath, HttpServletRequest request) {
+		VFSItem vfsItem = container.resolve(relPath);
+		if(vfsItem == null && relPath.startsWith(path)) {
+			String fallback = relPath.substring(path.length(), relPath.length());
+			vfsItem = container.resolve(fallback);
+		}
+		
+		MediaResource mr;
+		if (vfsItem instanceof VFSLeaf) {
+			mr = new VFSMediaResource((VFSLeaf) vfsItem);
+		} else {
+			mr = new NotFoundMediaResource();
+		}
+		return mr;
+	}
+}
diff --git a/src/main/java/org/olat/course/groupsandrights/PersistingCourseGroupManager.java b/src/main/java/org/olat/course/groupsandrights/PersistingCourseGroupManager.java
index d0754bb7b27..5925e1f0a64 100644
--- a/src/main/java/org/olat/course/groupsandrights/PersistingCourseGroupManager.java
+++ b/src/main/java/org/olat/course/groupsandrights/PersistingCourseGroupManager.java
@@ -375,7 +375,7 @@ public class PersistingCourseGroupManager implements CourseGroupManager {
 		BusinessGroupEnvironment bgEnv = new BusinessGroupEnvironment();
 		bgEnv.getGroups().addAll(courseEnv.getGroups());
 		bgEnv.getAreas().addAll(courseEnv.getAreas());
-		businessGroupService.exportGroups(groups, areas, fExportFile, false);
+		businessGroupService.exportGroups(groups, areas, fExportFile);
 	}
 	
 	/**
diff --git a/src/main/java/org/olat/course/nodes/DialogCourseNode.java b/src/main/java/org/olat/course/nodes/DialogCourseNode.java
index dfe2f94e38b..e427085f1ad 100644
--- a/src/main/java/org/olat/course/nodes/DialogCourseNode.java
+++ b/src/main/java/org/olat/course/nodes/DialogCourseNode.java
@@ -26,11 +26,13 @@
 package org.olat.course.nodes;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 import java.util.zip.ZipOutputStream;
 
+import org.apache.logging.log4j.Logger;
 import org.olat.core.CoreSpringFactory;
 import org.olat.core.commons.services.notifications.NotificationsManager;
 import org.olat.core.commons.services.notifications.SubscriptionContext;
@@ -39,6 +41,7 @@ import org.olat.core.gui.components.stack.BreadcrumbPanel;
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.generic.tabbable.TabbableController;
+import org.olat.core.logging.Tracing;
 import org.olat.core.util.Formatter;
 import org.olat.core.util.Util;
 import org.olat.core.util.ZipUtil;
@@ -68,10 +71,8 @@ import org.olat.course.run.userview.CourseNodeSecurityCallback;
 import org.olat.course.run.userview.NodeEvaluation;
 import org.olat.course.run.userview.UserCourseEnvironment;
 import org.olat.modules.ModuleConfiguration;
-import org.olat.modules.fo.archiver.ForumArchiveManager;
-import org.olat.modules.fo.archiver.formatters.ForumFormatter;
-import org.olat.modules.fo.archiver.formatters.ForumRTFFormatter;
-import org.olat.modules.fo.archiver.formatters.ForumStreamedRTFFormatter;
+import org.olat.modules.fo.Forum;
+import org.olat.modules.fo.archiver.ForumArchive;
 import org.olat.repository.RepositoryEntry;
 
 /**
@@ -81,6 +82,7 @@ import org.olat.repository.RepositoryEntry;
  */
 public class DialogCourseNode extends AbstractAccessableCourseNode {
 
+	private static final Logger log = Tracing.createLoggerFor(DialogCourseNode.class);
 	public static final String TYPE = "dialog";
 	
 	@SuppressWarnings("deprecation")
@@ -266,10 +268,13 @@ public class DialogCourseNode extends AbstractAccessableCourseNode {
 		// don't check quota
 		diaNodeElemExportContainer.setLocalSecurityCallback(new FullAccessCallback());
 		diaNodeElemExportContainer.copyFrom(dialogFile);
-
-		ForumArchiveManager fam = CoreSpringFactory.getImpl(ForumArchiveManager.class);
-		ForumFormatter ff = new ForumRTFFormatter(diaNodeElemExportContainer, false, locale);
-		fam.applyFormatter(ff, element.getForum().getKey(), null);
+		
+		try {
+			ForumArchive archiver = new ForumArchive(element.getForum(), null, locale, null);
+			archiver.export("Forum.docx", diaNodeElemExportContainer);
+		} catch (IOException e) {
+			log.error("", e);
+		}
 	}
 	
 	@Override
@@ -304,10 +309,14 @@ public class DialogCourseNode extends AbstractAccessableCourseNode {
 		for(VFSItem item: forumContainer.getItems(new VFSLeafFilter())) {
 			ZipUtil.addToZip(item, exportDirName, exportStream);
 		}
-
-		ForumArchiveManager fam = CoreSpringFactory.getImpl(ForumArchiveManager.class);
-		ForumFormatter ff = new ForumStreamedRTFFormatter(exportStream, exportDirName, false, locale);
-		fam.applyFormatter(ff, element.getForum().getKey(), null);
+		
+		try {
+			Forum forum = element.getForum();
+			ForumArchive archiver = new ForumArchive(forum, null, locale, null);
+			archiver.export("Dialogs.docx", exportDirName, exportStream);
+		} catch (IOException e) {
+			log.error("", e);
+		}
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/course/nodes/FOCourseNode.java b/src/main/java/org/olat/course/nodes/FOCourseNode.java
index fe64bd3ce46..8719c7d5007 100644
--- a/src/main/java/org/olat/course/nodes/FOCourseNode.java
+++ b/src/main/java/org/olat/course/nodes/FOCourseNode.java
@@ -25,6 +25,7 @@
 
 package org.olat.course.nodes;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -79,8 +80,7 @@ import org.olat.modules.ModuleConfiguration;
 import org.olat.modules.fo.Forum;
 import org.olat.modules.fo.ForumCallback;
 import org.olat.modules.fo.ForumModule;
-import org.olat.modules.fo.archiver.ForumArchiveManager;
-import org.olat.modules.fo.archiver.formatters.ForumStreamedRTFFormatter;
+import org.olat.modules.fo.archiver.ForumArchive;
 import org.olat.modules.fo.manager.ForumManager;
 import org.olat.properties.Property;
 import org.olat.repository.RepositoryEntry;
@@ -457,8 +457,14 @@ public class FOCourseNode extends AbstractAccessableCourseNode {
 		String forumName = "forum_" + Formatter.makeStringFilesystemSave(getShortTitle()) + "_"
 				+ Formatter.formatDatetimeFilesystemSave(new Date(System.currentTimeMillis()));
 		forumName = ZipUtil.concat(archivePath, forumName);
-		ForumStreamedRTFFormatter rtff = new ForumStreamedRTFFormatter(exportStream, forumName, false, locale);
-		CoreSpringFactory.getImpl(ForumArchiveManager.class).applyFormatter(rtff, forumKey, null);
+
+		try {
+			Forum forum = loadOrCreateForum(course.getCourseEnvironment());
+			ForumArchive archiver = new ForumArchive(forum, null, locale, null);
+			archiver.export(forumName + ".docx", exportStream);
+		} catch (IOException e) {
+			log.error("", e);
+		}
 		return true;
 	}
 
diff --git a/src/main/java/org/olat/group/BusinessGroupService.java b/src/main/java/org/olat/group/BusinessGroupService.java
index cd54c80d567..3e21cc4742a 100644
--- a/src/main/java/org/olat/group/BusinessGroupService.java
+++ b/src/main/java/org/olat/group/BusinessGroupService.java
@@ -641,8 +641,7 @@ public interface BusinessGroupService {
 	 * @param groups
 	 * @param fExportFile
 	 */
-	public void exportGroups(List<BusinessGroup> groups, List<BGArea> areas, File fExportFile,
-			boolean runtimeDatas);
+	public void exportGroups(List<BusinessGroup> groups, List<BGArea> areas, File fExportFile);
 
 	/**
 	 * Import previously exported group definitions.
diff --git a/src/main/java/org/olat/group/manager/BusinessGroupImportExport.java b/src/main/java/org/olat/group/manager/BusinessGroupImportExport.java
index aab622da7f6..ba6f6db7580 100644
--- a/src/main/java/org/olat/group/manager/BusinessGroupImportExport.java
+++ b/src/main/java/org/olat/group/manager/BusinessGroupImportExport.java
@@ -70,7 +70,7 @@ public class BusinessGroupImportExport {
 		this.groupModule = groupModule;
 	}
 	
-	public void exportGroups(List<BusinessGroup> groups, List<BGArea> areas, File fExportFile, boolean runtimeDatas) {
+	public void exportGroups(List<BusinessGroup> groups, List<BGArea> areas, File fExportFile) {
 		if (groups == null || groups.isEmpty()) {
 			return; // nothing to do... says Florian.
 		}
@@ -92,13 +92,13 @@ public class BusinessGroupImportExport {
 		root.getGroups().setGroups(new ArrayList<Group>());
 		for (BusinessGroup group : groups) {
 			String groupName = null;
-			Group newGroup = exportGroup(fExportFile, group, groupName, runtimeDatas);
+			Group newGroup = exportGroup(fExportFile, group, groupName);
 			root.getGroups().getGroups().add(newGroup);
 		}
 		saveGroupConfiguration(fExportFile, root);
 	}
 	
-	private Group exportGroup(File fExportFile, BusinessGroup group, String groupName, boolean runtimeDatas) {
+	private Group exportGroup(File fExportFile, BusinessGroup group, String groupName) {
 		Group newGroup = new Group();
 		newGroup.setKey(group.getKey());
 		newGroup.setName(StringHelper.containsNonWhitespace(groupName) ? groupName : group.getName());
@@ -147,10 +147,8 @@ public class BusinessGroupImportExport {
 			newGroup.setInfo(info.trim());
 		}
 
-		log.debug("fExportFile.getParent()=" + fExportFile.getParent());
-		if(runtimeDatas) {
-			ct.archive(fExportFile.getParent());
-		}
+		log.debug("fExportFile.getParent()={}", fExportFile.getParent());
+
 		// export membership
 		List<BGArea> bgAreas = areaManager.findBGAreasOfBusinessGroup(group);
 		newGroup.setAreaRelations(new ArrayList<String>());
diff --git a/src/main/java/org/olat/group/manager/BusinessGroupServiceImpl.java b/src/main/java/org/olat/group/manager/BusinessGroupServiceImpl.java
index 6c565d2c0a4..652047904fa 100644
--- a/src/main/java/org/olat/group/manager/BusinessGroupServiceImpl.java
+++ b/src/main/java/org/olat/group/manager/BusinessGroupServiceImpl.java
@@ -1814,9 +1814,9 @@ public class BusinessGroupServiceImpl implements BusinessGroupService {
 	}
 
 	@Override
-	public void exportGroups(List<BusinessGroup> groups, List<BGArea> areas, File fExportFile, boolean runtimeDatas) {
+	public void exportGroups(List<BusinessGroup> groups, List<BGArea> areas, File fExportFile) {
 		BusinessGroupImportExport exporter = new BusinessGroupImportExport(dbInstance, areaManager, this, groupModule);
-		exporter.exportGroups(groups, areas, fExportFile, runtimeDatas);
+		exporter.exportGroups(groups, areas, fExportFile);
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/modules/fo/archiver/ForumArchive.java b/src/main/java/org/olat/modules/fo/archiver/ForumArchive.java
new file mode 100644
index 00000000000..d279b1a339f
--- /dev/null
+++ b/src/main/java/org/olat/modules/fo/archiver/ForumArchive.java
@@ -0,0 +1,275 @@
+/**
+ * <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.modules.fo.archiver;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.logging.log4j.Logger;
+import org.olat.core.CoreSpringFactory;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.ZipUtil;
+import org.olat.core.util.io.ShieldOutputStream;
+import org.olat.core.util.openxml.DocReference;
+import org.olat.core.util.openxml.OpenXMLDocumentWriter;
+import org.olat.core.util.tree.TreeVisitor;
+import org.olat.core.util.vfs.VFSContainer;
+import org.olat.core.util.vfs.VFSLeaf;
+import org.olat.core.util.vfs.VFSManager;
+import org.olat.modules.fo.Forum;
+import org.olat.modules.fo.ForumCallback;
+import org.olat.modules.fo.Message;
+import org.olat.modules.fo.archiver.formatters.ForumOpenXMLFormatter;
+import org.olat.modules.fo.manager.ForumManager;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 22 juin 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class ForumArchive {
+	
+	private static final Logger log = Tracing.createLoggerFor(ForumArchive.class);
+	
+	@Autowired
+	private ForumManager forumManager;
+	
+	private final Locale locale;
+	private final Forum forum;
+	private final Long topMessageId;
+	private final ForumCallback forumCallback;
+	
+	public ForumArchive(Forum forum, Long topMessageId, Locale locale, ForumCallback forumCallback) {
+		CoreSpringFactory.autowireObject(this);
+		this.forum = forum;
+		this.locale = locale;
+		this.topMessageId = topMessageId;
+		this.forumCallback = forumCallback;
+	}
+	
+	public void export(String name, VFSContainer exportDir)
+	throws IOException {
+
+		Map<File,DocReference> attachments = null;
+		VFSLeaf forumDoc = exportDir.createChildLeaf(name);
+		try(OutputStream out = forumDoc.getOutputStream(false)) {
+			attachments = exportForum(out);
+		} catch(IOException e) {
+			log.error("", e);
+		}
+		
+		if(attachments != null && attachments.size() > 0) {
+			VFSContainer attachmentsContainer = VFSManager.getOrCreateContainer(exportDir, "attachments");
+			for(Map.Entry<File,DocReference> attachmentEntry : attachments.entrySet()) {
+				File attachment = attachmentEntry.getKey();
+				DocReference ref = attachmentEntry.getValue();
+				VFSLeaf leaf = attachmentsContainer.createChildLeaf(ref.getFilename());
+				VFSManager.copyContent(attachment, leaf);	
+			}
+		}
+	}
+	
+	public void export(String name, ZipOutputStream zout)
+	throws IOException {
+		export(name, "", zout);
+	}
+	
+	public void export(String name, String path, ZipOutputStream zout)
+	throws IOException {
+		ZipEntry test = new ZipEntry(ZipUtil.concat(path, name));
+		zout.putNextEntry(test);
+		Map<File,DocReference> attachments = null;
+		try(ShieldOutputStream sOut = new ShieldOutputStream(zout)) {
+			attachments = exportForum(sOut);
+		} catch(IOException e) {
+			log.error("", e);
+		}
+		zout.closeEntry();
+		
+		if(attachments != null && attachments.size() > 0) {
+			for(Map.Entry<File,DocReference> attachmentEntry : attachments.entrySet()) {
+				File attachment = attachmentEntry.getKey();
+				DocReference ref = attachmentEntry.getValue();
+				zout.putNextEntry(new ZipEntry(ZipUtil.concat(path, "attachments/" + ref.getFilename())));
+				copyShielded(attachment, zout);
+				zout.closeEntry();
+			}
+		}
+	}
+	
+	private Map<File,DocReference> exportForum(OutputStream out) {
+		try(ZipOutputStream zout = new ZipOutputStream(out)) {
+			zout.setLevel(9);
+			
+			VFSContainer mediaContainer = forumManager.getForumContainer(forum.getKey());
+			
+			ForumOpenXMLFormatter openXmlFormatter = new ForumOpenXMLFormatter(mediaContainer, locale);
+			if(topMessageId != null) {
+				applyFormatterForOneThread(openXmlFormatter, topMessageId);
+			} else {
+				applyFormatter(openXmlFormatter);
+			}
+			
+			OpenXMLDocumentWriter writer = new OpenXMLDocumentWriter();
+			writer.createDocument(zout, openXmlFormatter.getOpenXMLDocument());
+			return openXmlFormatter.getAttachments();
+		} catch (Exception e) {
+			log.error("", e);
+			return null;
+		}
+	}
+	
+	private void copyShielded(File attachment, ZipOutputStream zout) {
+		try(OutputStream out = new ShieldOutputStream(zout)) {
+			Files.copy(attachment.toPath(), out);
+		} catch(Exception e) {
+			log.error("", e);
+		}
+	}
+	
+	/**
+	 * If the forumCallback is null no restriction applies to the forum archiver. 
+	 * (that is it can archive all threads no matter the status)
+	 * @param forumFormatter The formatter
+	 * @return
+	 */
+	private void applyFormatter(ForumOpenXMLFormatter forumFormatter) {
+		log.info("Archiving complete forum: {}", forum);
+		//convert forum structure to trees
+		List<MessageNode> threadTreesList = convertToThreadTrees();
+		//format forum trees by using the formatter given by the callee
+		formatForum(threadTreesList, forumFormatter);
+	}
+	
+	/**
+	 * It is assumed that if the caller of this method is allowed to see the forum thread
+	 * starting from topMessageId, then he also has the right to archive it, so no need for a ForumCallback.
+	 * @param forumFormatter The formatter
+	 * @param messageId The root message id
+	 */
+	private void applyFormatterForOneThread(ForumOpenXMLFormatter forumFormatter, Long messageId){
+		MessageNode topMessageNode = convertToThreadTree(messageId);
+		formatThread(topMessageNode, forumFormatter);
+	}
+
+	/**
+	 * If the forumCallback is null no filtering is executed, 
+	 * else if a thread is hidden and the user doesn't have moderator rights the
+	 * hidden thread is not included into the archive.
+	 * 
+	 * @return all top message nodes together with their children in a list
+	 */
+	private List<MessageNode> convertToThreadTrees() {
+		List<MessageNode> topNodeList = new ArrayList<>();
+
+		List<Message> messages = forumManager.getMessagesByForum(forum);
+		for (Iterator<Message> iterTop = messages.iterator(); iterTop.hasNext();) {
+			Message msg = iterTop.next();
+			if (msg.getParent() == null) {
+				iterTop.remove();
+				MessageNode topNode = new MessageNode(msg);
+				if(!topNode.isHidden()
+						|| (topNode.isHidden() && (forumCallback == null || forumCallback.mayEditMessageAsModerator()))) {
+					addChildren(messages, topNode);
+					topNodeList.add(topNode);
+				}
+			}
+		}
+		Collections.sort(topNodeList, new MessageNodeComparator());
+		return topNodeList;
+	}
+	
+	public static class MessageNodeComparator implements Comparator<MessageNode> {
+		@Override
+		public int compare(final MessageNode m1, final MessageNode m2) {			
+			if(m1.isSticky() && m2.isSticky()) {
+				return m2.getModifiedDate().compareTo(m1.getModifiedDate()); //last first
+			} else if(m1.isSticky()) {
+				return -1;
+			} else if(m2.isSticky()){
+				return 1;
+			} else {
+				return m2.getModifiedDate().compareTo(m1.getModifiedDate()); //last first
+			}				
+		}
+	}
+	
+	/**
+	 * 
+	 * @param messageId
+	 * @param metaInfo
+	 * @return the top message node with all its children
+	 */
+	private MessageNode convertToThreadTree(Long messagedId) {
+		MessageNode topNode = null;
+		List<Message> messages = forumManager.getThread(messagedId);
+		for (Iterator<Message> iterTop = messages.iterator(); iterTop.hasNext();) {
+			Message msg = iterTop.next();
+			if (msg.getParent() == null) {
+				iterTop.remove();
+				topNode = new MessageNode(msg);
+				addChildren(messages, topNode);
+			}
+		}
+		return topNode;
+	}
+	
+	private void addChildren(List<Message> messages, MessageNode mn){
+		for(Iterator<Message> iterMsg = messages.iterator(); iterMsg.hasNext(); ) {
+			Message msg = iterMsg.next();
+			if (msg.getParent() != null && msg.getParent().getKey().equals(mn.getKey())){
+				MessageNode childNode = new MessageNode(msg);
+				mn.addChild(childNode);
+				childNode.setParent(mn);
+				addChildren(messages, childNode);
+			}
+		}
+	}
+	
+	private void formatForum(List<MessageNode> topNodeList, ForumOpenXMLFormatter forumFormatter) { 
+		forumFormatter.openForum();
+		for (Iterator<MessageNode> iterTop = topNodeList.iterator(); iterTop.hasNext();){
+			MessageNode mn = iterTop.next();
+			//a new top thread starts, inform formatter
+			forumFormatter.openThread();
+			TreeVisitor tv = new TreeVisitor(forumFormatter, mn, false);
+			tv.visitAll();
+		}
+	}
+	
+	private void formatThread(MessageNode mn, ForumOpenXMLFormatter forumFormatter) {
+		forumFormatter.openThread();
+		TreeVisitor tv = new TreeVisitor(forumFormatter, mn, false);
+		tv.visitAll();
+	}
+}
diff --git a/src/main/java/org/olat/modules/fo/archiver/ForumArchiveManager.java b/src/main/java/org/olat/modules/fo/archiver/ForumArchiveManager.java
deleted file mode 100644
index 8db9ce38360..00000000000
--- a/src/main/java/org/olat/modules/fo/archiver/ForumArchiveManager.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/**
-* OLAT - Online Learning and Training<br>
-* http://www.olat.org
-* <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
-* <p>
-* http://www.apache.org/licenses/LICENSE-2.0
-* <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>
-* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
-* University of Zurich, Switzerland.
-* <hr>
-* <a href="http://www.openolat.org">
-* OpenOLAT - Online Learning and Training</a><br>
-* This file has been modified by the OpenOLAT community. Changes are licensed
-* under the Apache 2.0 license as the original file.
-*/
-
-package org.olat.modules.fo.archiver;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
-
-import org.apache.logging.log4j.Logger;
-import org.olat.core.logging.Tracing;
-import org.olat.core.util.tree.TreeVisitor;
-import org.olat.modules.fo.Forum;
-import org.olat.modules.fo.ForumCallback;
-import org.olat.modules.fo.Message;
-import org.olat.modules.fo.archiver.formatters.ForumFormatter;
-import org.olat.modules.fo.manager.ForumManager;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-/**
- *          Initial Date: Nov 11, 2005 <br>
- * @author Alexander Schneider
- */
-@Service
-public class ForumArchiveManager {
-	
-	private static final Logger log = Tracing.createLoggerFor(ForumArchiveManager.class);
-	
-	@Autowired
-	private ForumManager forumManager;
-	
-	/**
-	 * If the forumCallback is null no restriction applies to the forum archiver. 
-	 * (that is it can archive all threads no matter the status)
-	 * @param forumFormatter
-	 * @param forumId
-	 * @param forumCallback
-	 * @return
-	 */
-	public String applyFormatter(ForumFormatter forumFormatter, Long forumId, ForumCallback forumCallback){
-		log.info("Archiving complete forum: " + forumId);
-		//convert forum structure to trees
-		List<MessageNode> threadTreesList = convertToThreadTrees(forumId, forumCallback);
-		//format forum trees by using the formatter given by the callee
-		return formatForum(threadTreesList, forumFormatter, forumId);
-	}
-	/**
-	 * It is assumed that if the caller of this method is allowed to see the forum thread
-	 * starting from topMessageId, then he also has the right to archive it, so no need for a ForumCallback.
-	 * @param forumFormatter
-	 * @param forumId
-	 * @param topMessageId
-	 * @return the message thread as String formatted
-	 */
-	public String applyFormatterForOneThread(ForumFormatter forumFormatter, Long forumId, Long topMessageId){
-		MessageNode topMessageNode = convertToThreadTree(topMessageId);
-		return formatThread(topMessageNode, forumFormatter, forumId);
-	}
-
-	/**
-	 * If the forumCallback is null no filtering is executed, 
-	 * else if a thread is hidden and the user doesn't have moderator rights the
-	 * hidden thread is not included into the archive.
-	 * @param forumId
-	 * @param metaInfo
-	 * @return all top message nodes together with their children in a list
-	 */
-	private List<MessageNode> convertToThreadTrees(Long forumId, ForumCallback forumCallback){
-		List<MessageNode> topNodeList = new ArrayList<>();
-
-		Forum f = forumManager.loadForum(forumId);
-		List<Message> messages = forumManager.getMessagesByForum(f);
-		
-		for (Iterator<Message> iterTop = messages.iterator(); iterTop.hasNext();) {
-			Message msg = iterTop.next();
-			if (msg.getParent() == null) {
-				iterTop.remove();
-				MessageNode topNode = new MessageNode(msg);
-				if(topNode.isHidden() && (forumCallback==null || (forumCallback!=null && forumCallback.mayEditMessageAsModerator()))) {
-					addChildren(messages, topNode);
-					topNodeList.add(topNode);
-				}	else if(!topNode.isHidden()) {
-					addChildren(messages, topNode);
-					topNodeList.add(topNode);
-				}
-			}
-		}
-		Collections.sort(topNodeList, new MessageNodeComparator());
-		return topNodeList;
-	}
-	
-	public static class MessageNodeComparator implements Comparator<MessageNode> {
-		@Override
-		public int compare(final MessageNode m1, final MessageNode m2) {			
-			if(m1.isSticky() && m2.isSticky()) {
-				return m2.getModifiedDate().compareTo(m1.getModifiedDate()); //last first
-			} else if(m1.isSticky()) {
-				return -1;
-			} else if(m2.isSticky()){
-				return 1;
-			} else {
-				return m2.getModifiedDate().compareTo(m1.getModifiedDate()); //last first
-			}				
-		}
-	}
-	
-	/**
-	 * 
-	 * @param messageId
-	 * @param metaInfo
-	 * @return the top message node with all its children
-	 */
-	private MessageNode convertToThreadTree(Long topMessageId){
-		MessageNode topNode = null;
-		List<Message> messages = forumManager.getThread(topMessageId);
-		for (Iterator<Message> iterTop = messages.iterator(); iterTop.hasNext();) {
-			Message msg = iterTop.next();
-			if (msg.getParent() == null) {
-				iterTop.remove();
-				topNode = new MessageNode(msg);
-				addChildren(messages, topNode);
-			}
-		}
-		return topNode;
-	}
-	
-	private void addChildren(List<Message> messages, MessageNode mn){
-		for(Iterator<Message> iterMsg = messages.iterator(); iterMsg.hasNext(); ) {
-			Message msg = iterMsg.next();
-			if ((msg.getParent() != null) && (msg.getParent().getKey() == mn.getKey())){
-				MessageNode childNode = new MessageNode(msg);
-				mn.addChild(childNode);
-				//FIXME:as:c next line is not necessary
-				childNode.setParent(mn);
-				addChildren(messages, childNode);
-			}
-		}
-	}
-	
-	/**
-	 * 
-	 * @param topNodeList
-	 * @param forumFormatter
-	 * @param metaInfo
-	 * @return
-	 */
-	private String formatForum(List<MessageNode> topNodeList, ForumFormatter forumFormatter, Long forumId) { 
-		forumFormatter.setForumKey(forumId);
-		StringBuilder formattedForum = new StringBuilder();
-		forumFormatter.openForum();
-		for (Iterator<MessageNode> iterTop = topNodeList.iterator(); iterTop.hasNext();){
-			MessageNode mn = iterTop.next();
-			//a new top thread starts, inform formatter
-			forumFormatter.openThread();
-			TreeVisitor tv = new TreeVisitor(forumFormatter, mn, false);
-			tv.visitAll();
-			//commit
-			formattedForum.append(forumFormatter.closeThread());
-		}
-		return formattedForum.append(forumFormatter.closeForum().toString()).toString();
-	}
-	
-	/**
-	 * 
-	 * @param mn
-	 * @param forumFormatter
-	 * @param metaInfo
-	 * @return
-	 */
-	private String formatThread(MessageNode mn, ForumFormatter forumFormatter, Long forumId){
-		forumFormatter.setForumKey(forumId);
-		StringBuilder formattedThread = new StringBuilder();
-		forumFormatter.openThread();
-		TreeVisitor tv = new TreeVisitor(forumFormatter, mn, false);
-		tv.visitAll();
-		return formattedThread.append(formattedThread.append(forumFormatter.closeThread())).toString();
-	}
-	
-}
diff --git a/src/main/java/org/olat/modules/fo/archiver/formatters/ForumDownloadResource.java b/src/main/java/org/olat/modules/fo/archiver/formatters/ForumDownloadResource.java
index c9882bd9034..7a874af96fd 100644
--- a/src/main/java/org/olat/modules/fo/archiver/formatters/ForumDownloadResource.java
+++ b/src/main/java/org/olat/modules/fo/archiver/formatters/ForumDownloadResource.java
@@ -19,30 +19,20 @@
  */
 package org.olat.modules.fo.archiver.formatters;
 
-import java.io.File;
 import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Files;
 import java.util.Locale;
-import java.util.Map;
-import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
 import javax.servlet.http.HttpServletResponse;
 
-import org.olat.core.CoreSpringFactory;
+import org.apache.logging.log4j.Logger;
 import org.olat.core.gui.media.MediaResource;
 import org.olat.core.gui.media.ServletUtil;
-import org.apache.logging.log4j.Logger;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.StringHelper;
-import org.olat.core.util.io.ShieldOutputStream;
-import org.olat.core.util.openxml.DocReference;
-import org.olat.core.util.openxml.OpenXMLDocumentWriter;
-import org.olat.core.util.vfs.VFSContainer;
 import org.olat.modules.fo.Forum;
 import org.olat.modules.fo.ForumCallback;
-import org.olat.modules.fo.archiver.ForumArchiveManager;
+import org.olat.modules.fo.archiver.ForumArchive;
 
 /**
  * 
@@ -59,16 +49,14 @@ public class ForumDownloadResource implements MediaResource {
 	private Long topMessageId;
 	
 	private String label;
-	private VFSContainer mediaContainer;
 	private Locale locale;
 	
-	public ForumDownloadResource(String label, Forum forum, ForumCallback foCallback, Long topMessageId, VFSContainer mediaContainer, Locale locale) {
+	public ForumDownloadResource(String label, Forum forum, ForumCallback foCallback, Long topMessageId, Locale locale) {
 		this.locale = locale;
 		this.forum = forum;
 		this.label = label;
 		this.foCallback = foCallback;
 		this.topMessageId = topMessageId;
-		this.mediaContainer = mediaContainer;
 	}
 	
 	@Override
@@ -105,7 +93,6 @@ public class ForumDownloadResource implements MediaResource {
 	public void release() {
 		//
 	}
-	
 
 	@Override
 	public void prepare(HttpServletResponse hres) {
@@ -121,53 +108,11 @@ public class ForumDownloadResource implements MediaResource {
 			String file = secureLabel + ".zip";
 			hres.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + StringHelper.urlEncodeUTF8(file));			
 			hres.setHeader("Content-Description", StringHelper.urlEncodeUTF8(label));
-
-			ZipEntry test = new ZipEntry(secureLabel + ".docx");
-			zout.putNextEntry(test);
-			Map<File,DocReference> attachments = exportForum(zout);
-			zout.closeEntry();
-			
-			if(attachments != null && attachments.size() > 0) {
-				for(Map.Entry<File,DocReference> attachmentEntry : attachments.entrySet()) {
-					File attachment = attachmentEntry.getKey();
-					DocReference ref = attachmentEntry.getValue();
-					zout.putNextEntry(new ZipEntry("attachments/" + ref.getFilename()));
-					copyShielded(attachment, zout);
-					zout.closeEntry();
-				}
-			}
-		} catch (Exception e) {
-			log.error("", e);
-		}
-	}
-	
-	private void copyShielded(File attachment, ZipOutputStream zout) {
-		try(OutputStream out = new ShieldOutputStream(zout)) {
-			Files.copy(attachment.toPath(), out);
-		} catch(Exception e) {
-			log.error("", e);
-		}
-	}
-	
-	private Map<File,DocReference> exportForum(OutputStream out) {
-		try(ZipOutputStream zout = new ZipOutputStream(out)) {
-			zout.setLevel(9);
-			
-			ForumOpenXMLFormatter openXmlFormatter = new ForumOpenXMLFormatter(mediaContainer, locale);
-			if(topMessageId != null) {
-				CoreSpringFactory.getImpl(ForumArchiveManager.class)
-					.applyFormatterForOneThread(openXmlFormatter, forum.getKey(), topMessageId);
-			} else {
-				CoreSpringFactory.getImpl(ForumArchiveManager.class)
-					.applyFormatter(openXmlFormatter, forum.getKey(), foCallback);
-			}
 			
-			OpenXMLDocumentWriter writer = new OpenXMLDocumentWriter();
-			writer.createDocument(zout, openXmlFormatter.getOpenXMLDocument());
-			return openXmlFormatter.getAttachments();
+			ForumArchive archive = new ForumArchive(forum, topMessageId, locale, foCallback);
+			archive.export(secureLabel + ".docx", zout);
 		} catch (Exception e) {
 			log.error("", e);
-			return null;
 		}
 	}
 }
diff --git a/src/main/java/org/olat/modules/fo/archiver/formatters/ForumFormatter.java b/src/main/java/org/olat/modules/fo/archiver/formatters/ForumFormatter.java
deleted file mode 100644
index 60d6635108e..00000000000
--- a/src/main/java/org/olat/modules/fo/archiver/formatters/ForumFormatter.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
-* OLAT - Online Learning and Training<br>
-* http://www.olat.org
-* <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
-* <p>
-* http://www.apache.org/licenses/LICENSE-2.0
-* <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>
-* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
-* University of Zurich, Switzerland.
-* <hr>
-* <a href="http://www.openolat.org">
-* OpenOLAT - Online Learning and Training</a><br>
-* This file has been modified by the OpenOLAT community. Changes are licensed
-* under the Apache 2.0 license as the original file.
-*/
-
-package org.olat.modules.fo.archiver.formatters;
-
-import java.util.Locale;
-
-import org.olat.core.gui.translator.Translator;
-import org.olat.core.util.Util;
-import org.olat.core.util.nodes.INode;
-import org.olat.core.util.tree.Visitor;
-import org.olat.modules.fo.Forum;
-
-/**
- * Initial Date: Nov 11, 2005 <br>
- * @author Patrick Brunner, Alexander Schneider
- */
-
-public abstract class ForumFormatter implements Visitor {
-	protected StringBuilder sb;
-	protected boolean isTopThread = false;
-	protected boolean filePerThread = false;
-	private Long forumKey;
-
-	protected final Translator translator;
-
-	/**
-	 * init string buffer
-	 *
-	 */
-	protected ForumFormatter(Locale locale){
-		sb = new StringBuilder(4096);
-		translator = Util.createPackageTranslator(Forum.class, locale);
-	}
-	/**
-	 * contains (translation keys, value) pairs such as:
-	 * forum.metainfo.key,1234556
-	 * forum.metainfo.topthreadcnt, number of top threads
-	 * forum.metainfo.msgcnt, number of messages
-	 * 
-	 * @param metaInfo
-	 */
-	public void setForumKey(Long forumKey){
-		this.forumKey = forumKey;
-	}
-	/**
-	 * inform formatter that a new top thread has started, 
-	 * e.g. ForumArchiveManager sets this if next node in topnode list is worked on
-	 *
-	 */
-	public void openThread(){
-		isTopThread = true;
-	}
-	/**
-	 * inform formatter, that the top thread is completly consumed, thus create the formatted result for this thread
-	 * @return
-	 */
-	public StringBuilder closeThread(){
-		StringBuilder retVal = sb;
-		sb = new StringBuilder();
-		return retVal;
-	}
-	/**
-	 * 
-	 * @param key
-	 * @return value of key
-	 */
-	public Long getForumKey() {
-		return forumKey;
-	}
-	
-	/**
-	 * 
-	 * @return true if every thread is saved in his own file; false if all threads are saved in one file
-	 */
-	public boolean isFilePerThread(){
-		return filePerThread;
-	}
-	
-	/**
-	 * inform formatter that the forum is opened, and it must expect top threads being opened.
-	 */
-	public abstract void openForum();
-	
-	/**
-	 *inform formatter that all top thread of the forum are consumed.
-	 */
-	public abstract StringBuilder closeForum();
-	
-	/**
-	 * 
-	 * @see org.olat.core.util.tree.Visitor#visit(org.olat.core.util.nodes.INode)
-	 */
-	public abstract void visit(INode node);
-}
diff --git a/src/main/java/org/olat/modules/fo/archiver/formatters/ForumOpenXMLFormatter.java b/src/main/java/org/olat/modules/fo/archiver/formatters/ForumOpenXMLFormatter.java
index f28264bb4bb..7d08751e50c 100644
--- a/src/main/java/org/olat/modules/fo/archiver/formatters/ForumOpenXMLFormatter.java
+++ b/src/main/java/org/olat/modules/fo/archiver/formatters/ForumOpenXMLFormatter.java
@@ -28,19 +28,24 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
+import org.olat.core.gui.translator.Translator;
 import org.olat.core.id.Identity;
 import org.olat.core.id.UserConstants;
 import org.olat.core.util.Formatter;
 import org.olat.core.util.StringHelper;
+import org.olat.core.util.Util;
+import org.olat.core.util.filter.FilterFactory;
 import org.olat.core.util.nodes.INode;
 import org.olat.core.util.openxml.DocReference;
 import org.olat.core.util.openxml.OpenXMLDocument;
 import org.olat.core.util.openxml.OpenXMLDocument.Spacing;
 import org.olat.core.util.openxml.OpenXMLDocument.Style;
+import org.olat.core.util.tree.Visitor;
 import org.olat.core.util.vfs.LocalFileImpl;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.core.util.vfs.VFSItem;
-import org.olat.core.util.vfs.filters.VFSItemMetaFilter;
+import org.olat.core.util.vfs.filters.VFSLeafButSystemFilter;
+import org.olat.modules.fo.Forum;
 import org.olat.modules.fo.archiver.MessageNode;
 
 /**
@@ -49,11 +54,12 @@ import org.olat.modules.fo.archiver.MessageNode;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class ForumOpenXMLFormatter extends ForumFormatter {
-	
-	private final VFSItemMetaFilter filter = new VFSItemMetaFilter();
+public class ForumOpenXMLFormatter implements Visitor {
 
 	private boolean firstThread = true;
+	private boolean isTopThread = false;
+	
+	private final Translator translator;
 	
 	private final Formatter formatter;
 	private final VFSContainer forumContainer;
@@ -63,7 +69,7 @@ public class ForumOpenXMLFormatter extends ForumFormatter {
 	private final Map<File,DocReference> fileToAttachmentsMap = new HashMap<>();
 
 	public ForumOpenXMLFormatter(VFSContainer forumContainer, Locale locale) {
-		super(locale);
+		translator = Util.createPackageTranslator(Forum.class, locale);
 		document.setMediaContainer(forumContainer);
 		this.forumContainer = forumContainer;
 		formatter = Formatter.getInstance(locale);
@@ -76,20 +82,18 @@ public class ForumOpenXMLFormatter extends ForumFormatter {
 	public Map<File,DocReference> getAttachments() {
 		return fileToAttachmentsMap;
 	}
-
-	@Override
+	
 	public void openForum() {
 		//
 	}
 
-	@Override
 	public void openThread() {
 		if(firstThread) {
 			firstThread = false;
 		} else {
 			document.appendPageBreak();
 		}
-		super.openThread();
+		isTopThread = true;
 	}
 
 	@Override
@@ -151,6 +155,9 @@ public class ForumOpenXMLFormatter extends ForumFormatter {
 		if(body != null) {
 			body = body.replace("<p>&nbsp;", "<p>");
 		}
+		
+		String mapperPath = m.getKey().toString();
+		body = FilterFactory.getBaseURLToMediaRelativeURLFilter(mapperPath).filter(body);
 		document.appendHtmlText(body, new Spacing(180, 0));
 		
 		// message attachments
@@ -161,15 +168,14 @@ public class ForumOpenXMLFormatter extends ForumFormatter {
 	}
 	
 	private void processAttachments(VFSContainer attachmentsContainer) {
-		List<VFSItem> attachments = new ArrayList<>(attachmentsContainer.getItems(filter));
+		List<VFSItem> attachments = new ArrayList<>(attachmentsContainer.getItems(new VFSLeafButSystemFilter()));
 		for(VFSItem attachment:attachments) {
 			if(attachment instanceof LocalFileImpl) {
 				//add the text
 				document.appendText(translator.translate("attachments"), true, Style.bold);
 			}
 		}
-		
-		
+
 		for(VFSItem attachment:attachments) {
 			if(attachment instanceof LocalFileImpl) {
 				File file = ((LocalFileImpl)attachment).getBasefile();
@@ -196,12 +202,6 @@ public class ForumOpenXMLFormatter extends ForumFormatter {
 		}
 	}
 
-	@Override
-	public StringBuilder closeThread() {
-		return super.closeThread();
-	}
-
-	@Override
 	public StringBuilder closeForum() {
 		return new StringBuilder();
 	}
diff --git a/src/main/java/org/olat/modules/fo/archiver/formatters/ForumRTFFormatter.java b/src/main/java/org/olat/modules/fo/archiver/formatters/ForumRTFFormatter.java
deleted file mode 100644
index 3ec2be445ac..00000000000
--- a/src/main/java/org/olat/modules/fo/archiver/formatters/ForumRTFFormatter.java
+++ /dev/null
@@ -1,468 +0,0 @@
-/**
-* OLAT - Online Learning and Training<br>
-* http://www.olat.org
-* <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
-* <p>
-* http://www.apache.org/licenses/LICENSE-2.0
-* <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>
-* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
-* University of Zurich, Switzerland.
-* <hr>
-* <a href="http://www.openolat.org">
-* OpenOLAT - Online Learning and Training</a><br>
-* This file has been modified by the OpenOLAT community. Changes are licensed
-* under the Apache 2.0 license as the original file.
-*/
-
-package org.olat.modules.fo.archiver.formatters;
-
-import java.io.BufferedOutputStream;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.olat.core.CoreSpringFactory;
-import org.olat.core.id.Identity;
-import org.olat.core.id.UserConstants;
-import org.olat.core.logging.AssertException;
-import org.apache.logging.log4j.Logger;
-import org.olat.core.logging.Tracing;
-import org.olat.core.util.Formatter;
-import org.olat.core.util.StringHelper;
-import org.olat.core.util.WebappHelper;
-import org.olat.core.util.ZipUtil;
-import org.olat.core.util.filter.FilterFactory;
-import org.olat.core.util.nodes.INode;
-import org.olat.core.util.vfs.LocalFileImpl;
-import org.olat.core.util.vfs.VFSContainer;
-import org.olat.core.util.vfs.VFSItem;
-import org.olat.core.util.vfs.VFSLeaf;
-import org.olat.core.util.vfs.VFSManager;
-import org.olat.core.util.vfs.filters.VFSSystemItemFilter;
-import org.olat.modules.fo.archiver.MessageNode;
-import org.olat.modules.fo.manager.ForumManager;
-
-/**
- * Initial Date: Nov 09, 2005 <br>
- * 
- * @author Patrick Brunner, Alexander Schneider
- */
-
-public class ForumRTFFormatter extends ForumFormatter {
-	
-	private static final Logger log = Tracing.createLoggerFor(ForumRTFFormatter.class);
-
-	private VFSContainer container;
-	private VFSItem vfsFil = null;
-	private VFSContainer tempContainer;
-	
-	private final Pattern PATTERN_HTML_BOLD = Pattern.compile("<strong>(.*?)</strong>", Pattern.CASE_INSENSITIVE);
-	private final Pattern PATTERN_HTML_ITALIC = Pattern.compile("<em>(.*?)</em>", Pattern.CASE_INSENSITIVE);
-	private final Pattern PATTERN_HTML_BREAK = Pattern.compile("<br />", Pattern.CASE_INSENSITIVE);
-	private final Pattern PATTERN_HTML_PARAGRAPH = Pattern.compile("<p>(.*?)</p>", Pattern.CASE_INSENSITIVE);
-	private final Pattern PATTERN_HTML_AHREF = Pattern.compile("<a href=\"([^\"]+)\"[^>]*>(.*?)</a>", Pattern.CASE_INSENSITIVE);
-	private final Pattern PATTERN_HTML_LIST = Pattern.compile("<li>(.*?)</li>", Pattern.CASE_INSENSITIVE);
-	private final Pattern HTML_SPACE_PATTERN = Pattern.compile("&nbsp;");
-	
-	private final Pattern PATTERN_CSS_O_FOQUOTE = Pattern.compile("<div class=\"o_quote_wrapper\">\\s*<div class=\"b_quote_author mceNonEditable\">(.*?)</div>\\s*<blockquote class=\"b_quote\">\\s*(.*?)\\s*</blockquote>\\s*</div>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
-
-	private final Pattern PATTERN_THREEPOINTS = Pattern.compile("&#8230;", Pattern.CASE_INSENSITIVE);
-	private final String THREEPOINTS = "...";
-	
-	private String HIDDEN_STR = "VERBORGEN";
-	
-	private final ForumManager forumManager;
-	
-	/**
-	 * 
-	 * @param container
-	 * @param filePerThread
-	 */
-
-	public ForumRTFFormatter(VFSContainer container, boolean filePerThread, Locale locale) {
-		// init String Buffer in ForumFormatter
-		super(locale);
-		// where to write
-		this.container = container;
-		this.filePerThread = filePerThread;
-		forumManager = CoreSpringFactory.getImpl(ForumManager.class);
-	}
-
-	/**
-	 * @see org.olat.core.util.tree.Visitor#visit(org.olat.core.util.nodes.INode)
-	 */
-	public void visit(INode node) {
-		MessageNode mn = (MessageNode) node;
-
-		if (isTopThread) {
-			if(filePerThread){
-				//make a file per thread
-				//to have a meaningful filename we create the file here
-				String filName = "Thread_" + mn.getKey().toString();
-				tempContainer = makeTempVFSContainer();			
-				vfsFil=tempContainer.resolve(filName + ".rtf");
-				if(vfsFil==null){
-					tempContainer.createChildLeaf(filName + ".rtf");
-					vfsFil=tempContainer.resolve(filName + ".rtf");
-				}
-			}
-			//important!
-			isTopThread = false;
-		}
-		// Message Title
-		sb.append("{\\pard \\brdrb\\brdrs\\brdrw10 \\f1\\fs30\\b ");
-		sb.append(getImageRTF(mn));
-		sb.append(getTitlePrefix(mn));
-		sb.append(mn.getTitle());
-		sb.append("\\par}");
-		// Message Body
-		sb.append("{\\pard \\f0");
-		sb.append(convertHTMLMarkupToRTF(mn.getBody()));
-		sb.append("\\par}");
-		// Message key
-		sb.append("{\\pard \\f0\\fs15 Message key: ");
-		sb.append(mn.getKey());
-		sb.append("} \\line ");
-		sb.append("{\\pard \\f0\\fs15 created: ");
-		// Creator and creation date
-		if(StringHelper.containsNonWhitespace(mn.getPseudonym())) {
-			sb.append(mn.getPseudonym())
-			  .append(" ")
-			  .append(translator.translate("pseudonym.suffix"));
-		} else if(mn.isGuest()) {
-			sb.append(translator.translate("guest"));
-		} else {
-			sb.append(mn.getCreator().getUser().getProperty(UserConstants.FIRSTNAME, null));
-			sb.append(", ");
-			sb.append(mn.getCreator().getUser().getProperty(UserConstants.LASTNAME, null));
-		}
-		sb.append(" ");
-		sb.append(mn.getCreationDate().toString());
-		// Modifier and modified date
-		Identity modifier = mn.getModifier();
-		if (modifier != null) {
-			sb.append(" \\line modified: ");
-			sb.append(modifier.getUser().getProperty(UserConstants.FIRSTNAME, null));
-			sb.append(", ");
-			sb.append(modifier.getUser().getProperty(UserConstants.LASTNAME,  null));
-			sb.append(" ");
-			sb.append(mn.getModifiedDate().toString());
-		}
-		sb.append(" \\par}");
-		// attachment(s)
-		VFSContainer msgContainer = forumManager.getMessageContainer(getForumKey(), mn.getKey());
-		List<VFSItem> attachments = msgContainer.getItems(new VFSSystemItemFilter());
-		if (attachments != null && !attachments.isEmpty()){
-			VFSItem item = container.resolve("attachments");
-			if (item == null){
-				item = container.createChildContainer("attachments");
-			}
-			VFSContainer attachmentContainer = (VFSContainer)item;
-			attachmentContainer.copyFrom(msgContainer);
-			
-			sb.append("{\\pard \\f0\\fs15 Attachment(s): ");
-			boolean commaFlag = false;
-			for (VFSItem attachment: attachments) {
-				if (commaFlag) sb.append(", ");
-				sb.append(attachment.getName());
-				commaFlag = true;
-			}
-			sb.append("} \\line");
-		}
-		sb.append("{\\pard \\brdrb\\brdrs\\brdrw10 \\par}");
-	}
-
-	@Override
-	public void openThread() {
-		super.openThread();
-		if(filePerThread){
-			sb.append("{\\rtf1\\ansi\\deff0");
-			sb.append("{\\fonttbl {\\f0\\fswiss Arial;}} ");
-			sb.append("\\deflang1033\\plain");
-		}
-		sb.append("{\\pard \\brdrb \\brdrs \\brdrdb \\brsp20 \\par}{\\pard\\par}");
-	}
-
-	@Override
-	public StringBuilder closeThread() {
-		boolean append = !filePerThread;
-		String footerThread = "{\\pard \\brdrb \\brdrs \\brdrw20 \\brsp20 \\par}{\\pard\\par}";
-		sb.append(footerThread);
-		if(filePerThread){
-			sb.append("}");
-		}
-		writeToFile(append, sb);
-		if(filePerThread) {
-			zipContainer(tempContainer);			
-			tempContainer.delete();	
-		}
-		return super.closeThread();
-	}
-
-	@Override
-	public void openForum(){
-		if(!filePerThread){
-			//make one ForumFile
-			Long forumKey = getForumKey();
-			String filName = forumKey.toString();
-			filName = "Threads_" + filName + ".rtf";
-			
-			tempContainer = makeTempVFSContainer();					
-			this.vfsFil=tempContainer.resolve(filName);
-			if(vfsFil==null){
-				tempContainer.createChildLeaf(filName);
-				vfsFil = tempContainer.resolve(filName);
-			}
-			sb.append("{\\rtf1\\ansi\\deff0");
-			sb.append("{\\fonttbl {\\f0\\fswiss Arial;}} ");
-			sb.append("\\deflang1033\\plain");
-		}
-	}
-
-	@Override
-	public StringBuilder closeForum(){
-		if(!filePerThread){
-			boolean append = !filePerThread;
-			String footerForum = "}";
-			sb.append(footerForum);
-			writeToFile(append, sb);
-			zipContainer(tempContainer);			
-			tempContainer.delete();					
-		}
-		return sb;
-	}
-
-	
-	/**
-	 * 
-	 * @param append
-	 * @param buff
-	 */
-	private void writeToFile(boolean append, StringBuilder buff){
-		BufferedOutputStream bos = new BufferedOutputStream(((VFSLeaf) vfsFil).getOutputStream(append));
-		OutputStreamWriter w;
-		try {
-			w = new OutputStreamWriter(bos, "utf-8");
-			BufferedWriter bw = new BufferedWriter(w);
-			String s = buff.toString();
-			StringBuilder out = new StringBuilder();
-			int len = s.length();
-			for (int i = 0; i < len; i++) {
-				char c = s.charAt(i);
-				int val = c;
-				if (val > 127) {
-					out.append("\\u").append(String.valueOf(val)).append("?");
-				} else {
-					out.append(c);
-				}
-			}
-			
-			String encoded = out.toString();
-			bw.write(encoded);
-			bw.close();
-			bos.close();						
-		} catch (UnsupportedEncodingException ueEx) {
-				throw new AssertException("could not encode stream from forum export file: " + ueEx);
-		} catch (IOException e) {
-				throw new AssertException("could not write to forum export file: " + e);
-		}
-	}
-	
-	/**
-	 * 
-	 * @param originalText
-	 * @return
-	 */
-	private String convertHTMLMarkupToRTF(String originalText){
-		String htmlText = originalText;
-
-		Matcher mb = PATTERN_HTML_BOLD.matcher(htmlText);
-		StringBuffer bolds = new StringBuffer();
-		while (mb.find()) {
-			mb.appendReplacement(bolds, "{\\\\b $1} ");
-		}
-		mb.appendTail(bolds);
-		htmlText = bolds.toString();
-
-		Matcher mi = PATTERN_HTML_ITALIC.matcher(htmlText);
-		StringBuffer italics = new StringBuffer();
-		while (mi.find()) {
-			mi.appendReplacement(italics, "{\\\\i $1} ");
-		}
-		mi.appendTail(italics);
-		htmlText = italics.toString();
-		
-		Matcher mbr = PATTERN_HTML_BREAK.matcher(htmlText);
-		StringBuffer breaks = new StringBuffer();
-		while(mbr.find()){
-			mbr.appendReplacement(breaks, "\\\\line ");
-		}
-		mbr.appendTail(breaks);
-		htmlText = breaks.toString();
-		
-		Matcher mofo = PATTERN_CSS_O_FOQUOTE.matcher(htmlText);
-		StringBuffer foquotes = new StringBuffer();
-		while(mofo.find()){
-			mofo.appendReplacement(foquotes, "\\\\line {\\\\i $1} {\\\\pard $2\\\\par}");
-		}
-		mofo.appendTail(foquotes);
-		htmlText = foquotes.toString();
-		
-		Matcher mp = PATTERN_HTML_PARAGRAPH.matcher(htmlText);
-		StringBuffer paragraphs = new StringBuffer();
-		while(mp.find()){
-			mp.appendReplacement(paragraphs, "\\\\line $1 \\\\line");
-		}
-		mp.appendTail(paragraphs);
-		htmlText = paragraphs.toString();
-		
-		Matcher mahref = PATTERN_HTML_AHREF.matcher(htmlText);
-		StringBuffer ahrefs = new StringBuffer();
-		while(mahref.find()){
-			mahref.appendReplacement(ahrefs, "{\\\\field{\\\\*\\\\fldinst{HYPERLINK\"$1\"}}{\\\\fldrslt{\\\\ul $2}}}");
-		}
-		mahref.appendTail(ahrefs);
-		htmlText = ahrefs.toString();
-		
-		Matcher mli = PATTERN_HTML_LIST.matcher(htmlText);
-		StringBuffer lists = new StringBuffer();
-		while(mli.find()){
-			mli.appendReplacement(lists, "$1\\\\line ");
-		}
-		mli.appendTail(lists);
-		htmlText = lists.toString();
-		
-		Matcher mtp = PATTERN_THREEPOINTS.matcher(htmlText);
-		StringBuffer tps = new StringBuffer();
-		while (mtp.find()) {
-			mtp.appendReplacement(tps, THREEPOINTS);
-		}
-		mtp.appendTail(tps);
-		htmlText = tps.toString();
-
-		// strip all other html-fragments, because not convertable that easy
-		htmlText = FilterFactory.getHtmlTagsFilter().filter(htmlText);
-		// Remove all &nbsp;
-		Matcher tmp = HTML_SPACE_PATTERN.matcher(htmlText);
-		htmlText = tmp.replaceAll(" ");
-		htmlText = StringHelper.unescapeHtml(htmlText);
-
-		return htmlText;
-	}
-	
-	/**
-	 * 
-	 * @param messageNode
-	 * @return title prefix for hidden forum threads.
-	 */
-	private String getTitlePrefix(MessageNode messageNode) {
-		StringBuffer stringBuffer = new StringBuffer();
-		if(messageNode.isHidden()) {
-			stringBuffer.append(HIDDEN_STR);
-		} 		
-		if(stringBuffer.length()>1) {
-			stringBuffer.append(": ");
-		}
-		return stringBuffer.toString();
-	}
-	
-	/**
-	 * Gets the RTF image section for the input messageNode.
-	 * @param messageNode
-	 * @return the RTF image section for the input messageNode.
-	 */
-	private String getImageRTF(MessageNode messageNode) {
-					
-		StringBuffer stringBuffer = new StringBuffer();
-		List<String> fileNameList = addImagesToVFSContainer(messageNode, tempContainer);
-		Iterator<String> listIterator = fileNameList.iterator();
-		while(listIterator.hasNext()) {
-			String fileName = listIterator.next();
-			
-			stringBuffer.append("{\\field\\fldedit{\\*\\fldinst { INCLUDEPICTURE ");			
-			stringBuffer.append("\"").append(fileName).append("\"");
-			stringBuffer.append(" \\\\d }}{\\fldrslt {}}}");			
-		}				
-		return stringBuffer.toString();
-	}
-	
-	/**
-	 * Retrieves the appropriate images for the input messageNode, if any, 
-	 * and adds it to the input container.
-	 * 
-	 * @param messageNode
-	 * @param imageContainer
-	 * @return
-	 */
-	private List<String> addImagesToVFSContainer(MessageNode messageNode, VFSContainer imageContainer) {
-		List<String> fileNameList = new ArrayList<>();
-		String iconPath = null;
-		if(messageNode.isClosed() && messageNode.isSticky()) {
-			iconPath = getImagePath("fo_sticky_closed");
-		} else if(messageNode.isClosed()) {
-			iconPath = getImagePath("fo_closed");			
-		} else if(messageNode.isSticky()) {
-			iconPath = getImagePath("fo_sticky");			
-		}		
-		if (iconPath != null) {
-			File file = new File(iconPath);
-			if (file.exists()) {
-				LocalFileImpl imgFile = new LocalFileImpl(file);
-				imageContainer.copyFrom(imgFile);
-				fileNameList.add(file.getName());
-			} else {
-				log.error("Could not find image for forum RTF formatter::" + iconPath);
-			}
-		}
-		return fileNameList;
-	}
-	
-	/**
-	 * Gets the image path.
-	 * @param val
-	 * @return the path of the static icon image.
-	 */
-	private String getImagePath(Object val) { 		
-		return WebappHelper.getContextRealPath("/static/images/forum/" + val.toString() + ".png");				
-	}
-	
-	/**
-	 * Generates a new temporary VFSContainer. 
-	 * @return the temp container.
-	 */
-	private VFSContainer makeTempVFSContainer() {		
-		Long forumKey = getForumKey();
-		String dateStamp = String.valueOf(System.currentTimeMillis());
-		String fileName = "forum" + forumKey.toString() + "_" + dateStamp; 
-		return VFSManager.olatRootContainer("/tmp/" + fileName, null);
-	}
-	
-	/**
-	 * Zips the input vFSContainer into the container.
-	 * @param vFSContainer
-	 */
-	private void zipContainer(VFSContainer vFSContainer) {
-		String dateStamp = Formatter.formatDatetimeFilesystemSave(new Date(System.currentTimeMillis()));		
-		VFSLeaf zipVFSLeaf = container.createChildLeaf("forum_archive-"+dateStamp+".zip");		
-		ZipUtil.zip(vFSContainer.getItems(), zipVFSLeaf, true);
-	}
-	
-}
diff --git a/src/main/java/org/olat/modules/fo/archiver/formatters/ForumStreamedRTFFormatter.java b/src/main/java/org/olat/modules/fo/archiver/formatters/ForumStreamedRTFFormatter.java
deleted file mode 100644
index d71d676d498..00000000000
--- a/src/main/java/org/olat/modules/fo/archiver/formatters/ForumStreamedRTFFormatter.java
+++ /dev/null
@@ -1,418 +0,0 @@
-/**
-* OLAT - Online Learning and Training<br>
-* http://www.olat.org
-* <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
-* <p>
-* http://www.apache.org/licenses/LICENSE-2.0
-* <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>
-* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
-* University of Zurich, Switzerland.
-* <hr>
-* <a href="http://www.openolat.org">
-* OpenOLAT - Online Learning and Training</a><br>
-* This file has been modified by the OpenOLAT community. Changes are licensed
-* under the Apache 2.0 license as the original file.
-*/
-
-package org.olat.modules.fo.archiver.formatters;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.IOUtils;
-import org.olat.core.CoreSpringFactory;
-import org.olat.core.id.Identity;
-import org.olat.core.id.UserConstants;
-import org.olat.core.logging.AssertException;
-import org.apache.logging.log4j.Logger;
-import org.olat.core.logging.Tracing;
-import org.olat.core.util.StringHelper;
-import org.olat.core.util.WebappHelper;
-import org.olat.core.util.ZipUtil;
-import org.olat.core.util.filter.FilterFactory;
-import org.olat.core.util.nodes.INode;
-import org.olat.core.util.vfs.VFSContainer;
-import org.olat.core.util.vfs.VFSItem;
-import org.olat.core.util.vfs.filters.VFSSystemItemFilter;
-import org.olat.modules.fo.archiver.MessageNode;
-import org.olat.modules.fo.manager.ForumManager;
-
-/**
- * Initial Date: Dec 19, 2013 <br>
- * 
- * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
- * @author Patrick Brunner, Alexander Schneider
- */
-public class ForumStreamedRTFFormatter extends ForumFormatter {
-	
-	private static final Logger log = Tracing.createLoggerFor(ForumStreamedRTFFormatter.class);
-
-	private ZipOutputStream exportStream;
-	
-	final Pattern PATTERN_HTML_BOLD = Pattern.compile("<strong>(.*?)</strong>", Pattern.CASE_INSENSITIVE);
-	final Pattern PATTERN_HTML_ITALIC = Pattern.compile("<em>(.*?)</em>", Pattern.CASE_INSENSITIVE);
-	final Pattern PATTERN_HTML_BREAK = Pattern.compile("<br />", Pattern.CASE_INSENSITIVE);
-	final Pattern PATTERN_HTML_PARAGRAPH = Pattern.compile("<p>(.*?)</p>", Pattern.CASE_INSENSITIVE);
-	final Pattern PATTERN_HTML_AHREF = Pattern.compile("<a href=\"([^\"]+)\"[^>]*>(.*?)</a>", Pattern.CASE_INSENSITIVE);
-	final Pattern PATTERN_HTML_LIST = Pattern.compile("<li>(.*?)</li>", Pattern.CASE_INSENSITIVE);
-	final Pattern HTML_SPACE_PATTERN = Pattern.compile("&nbsp;");
-	
-	final Pattern PATTERN_CSS_O_FOQUOTE = Pattern.compile("<div class=\"o_quote_wrapper\">\\s*<div class=\"b_quote_author mceNonEditable\">(.*?)</div>\\s*<blockquote class=\"b_quote\">\\s*(.*?)\\s*</blockquote>\\s*</div>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
-
-	final Pattern PATTERN_THREEPOINTS = Pattern.compile("&#8230;", Pattern.CASE_INSENSITIVE);
-	final String THREEPOINTS = "...";
-	
-	private String HIDDEN_STR = "VERBORGEN";
-	private final String path;
-	
-	private final ForumManager forumManager;
-		
-	/**
-	 * 
-	 * @param container
-	 * @param filePerThread
-	 */
-
-	public ForumStreamedRTFFormatter(ZipOutputStream exportStream, String path, boolean filePerThread, Locale locale) {
-		// init String Buffer in ForumFormatter
-		super(locale);
-		
-		forumManager = CoreSpringFactory.getImpl(ForumManager.class);
-		
-		// where to write
-		this.exportStream = exportStream;
-		this.filePerThread = filePerThread;
-		this.path = path;
-		addStandardImages();
-	}
-	
-	private String fileName;
-
-	@Override
-	public void visit(INode node) {
-		MessageNode mn = (MessageNode)node;
-		if (isTopThread) {
-			//important!
-			fileName = "Thread_" + mn.getKey().toString() + ".rtf";
-			isTopThread = false;
-		}
-		// Message Title
-		sb.append("{\\pard \\brdrb\\brdrs\\brdrw10 \\f1\\fs30\\b ");
-		sb.append(getImageRTF(mn));
-		sb.append(getTitlePrefix(mn));
-		sb.append(mn.getTitle());
-		sb.append("\\par}");
-		// Message Body
-		sb.append("{\\pard \\f0");
-		sb.append(convertHTMLMarkupToRTF(mn.getBody()));
-		sb.append("\\par}");
-		// Message key
-		sb.append("{\\pard \\f0\\fs15 Message key: ");
-		sb.append(mn.getKey());
-		sb.append("} \\line ");
-		sb.append("{\\pard \\f0\\fs15 created: ");
-		// Creator and creation date
-		if(StringHelper.containsNonWhitespace(mn.getPseudonym())) {
-			sb.append(mn.getPseudonym())
-			  .append(" ")
-			  .append(translator.translate("pseudonym.suffix"));
-		} else if(mn.isGuest()) {
-			sb.append(translator.translate("guest"));
-		} else {
-			sb.append(mn.getCreator().getUser().getProperty(UserConstants.FIRSTNAME, null));
-			sb.append(", ");
-			sb.append(mn.getCreator().getUser().getProperty(UserConstants.LASTNAME, null));
-		}
-		sb.append(" ");
-		sb.append(mn.getCreationDate().toString());
-		// Modifier and modified date
-		Identity modifier = mn.getModifier();
-		if (modifier != null) {
-			sb.append(" \\line modified: ");
-			sb.append(modifier.getUser().getProperty(UserConstants.FIRSTNAME, null));
-			sb.append(", ");
-			sb.append(modifier.getUser().getProperty(UserConstants.LASTNAME,  null));
-			sb.append(" ");
-			sb.append(mn.getModifiedDate().toString());
-		}
-		sb.append(" \\par}");
-		// attachment(s)
-		VFSContainer msgContainer = forumManager.getMessageContainer(getForumKey(), mn.getKey());
-		List<VFSItem> attachments = msgContainer.getItems(new VFSSystemItemFilter());
-		if (attachments != null && !attachments.isEmpty()){
-			sb.append("{\\pard \\f0\\fs15 Attachment(s): ");
-			boolean commaFlag = false;
-			for (VFSItem attachment: attachments) {
-				if (commaFlag) sb.append(", ");
-				sb.append(attachment.getName());
-				commaFlag = true;
-				
-				ZipUtil.addToZip(attachment, path + "/attachments", exportStream);
-			}
-			sb.append("} \\line");
-		}
-		sb.append("{\\pard \\brdrb\\brdrs\\brdrw10 \\par}");
-	}
-	
-	private void addStandardImages() {
-		String[] images = new String[]{ "fo_sticky_closed", "fo_closed", "fo_sticky"};
-		try {
-			for(String image:images) {
-				String iconPath = getImagePath(image);		
-				if (iconPath != null) {
-					File file = new File(iconPath);
-					if (file.exists()) {
-						exportStream.putNextEntry(new ZipEntry(path + "/" + file.getName()));
-						FileUtils.copyFile(file, exportStream);
-						exportStream.closeEntry();
-					}
-				}
-			}
-		} catch (IOException e) {
-			log.error("", e);
-		}
-	}
-
-	@Override
-	public void openThread() {
-		super.openThread();
-		if(filePerThread){
-			sb.append("{\\rtf1\\ansi\\deff0");
-			sb.append("{\\fonttbl {\\f0\\fswiss Arial;}} ");
-			sb.append("\\deflang1033\\plain");
-		}
-		sb.append("{\\pard \\brdrb \\brdrs \\brdrdb \\brsp20 \\par}{\\pard\\par}");
-	}
-
-	@Override
-	public StringBuilder closeThread() {
-		String footerThread = "{\\pard \\brdrb \\brdrs \\brdrw20 \\brsp20 \\par}{\\pard\\par}";
-		sb.append(footerThread);
-		if(filePerThread){
-			sb.append("}");
-			writeToFile(sb);
-			sb = new StringBuilder();
-		}
-		return sb;
-	}
-	
-	@Override
-	public void openForum(){
-		if(!filePerThread){
-			//make one ForumFile
-			Long forumKey = getForumKey();
-			fileName = "Threads_" + forumKey.toString() + ".rtf";
-			sb.append("{\\rtf1\\ansi\\deff0");
-			sb.append("{\\fonttbl {\\f0\\fswiss Arial;}} ");
-			sb.append("\\deflang1033\\plain");
-		}
-	}
-
-	@Override
-	public StringBuilder closeForum(){
-		if(!filePerThread){
-			String footerForum = "}";
-			sb.append(footerForum);
-			writeToFile(sb);			
-		}
-		return sb;
-	}
-	
-	/**
-	 * 
-	 * @param append
-	 * @param buff
-	 */
-	private void writeToFile(StringBuilder buff){
-		try {
-			StringBuilder out = new StringBuilder();
-			int len = buff.length();
-			for (int i = 0; i < len; i++) {
-				char c = buff.charAt(i);
-				int val = c;
-				if (val > 127) {
-					out.append("\\u").append(String.valueOf(val)).append("?");
-				} else {
-					out.append(c);
-				}
-			}
-			String encoded = out.toString();
-			exportStream.putNextEntry(new ZipEntry(path + "/" + fileName));
-			IOUtils.write(encoded, exportStream, "UTF-8");
-			exportStream.closeEntry();
-		} catch (UnsupportedEncodingException ueEx) {
-			throw new AssertException("could not encode stream from forum export file: " + ueEx);
-		} catch (IOException e) {
-			throw new AssertException("could not write to forum export file: " + e);
-		}
-	}
-	
-	/**
-	 * 
-	 * @param originalText
-	 * @return
-	 */
-	private String convertHTMLMarkupToRTF(String originalText){
-		String htmlText = originalText;
-
-		Matcher mb = PATTERN_HTML_BOLD.matcher(htmlText);
-		StringBuffer bolds = new StringBuffer();
-		while (mb.find()) {
-			mb.appendReplacement(bolds, "{\\\\b $1} ");
-		}
-		mb.appendTail(bolds);
-		htmlText = bolds.toString();
-
-		Matcher mi = PATTERN_HTML_ITALIC.matcher(htmlText);
-		StringBuffer italics = new StringBuffer();
-		while (mi.find()) {
-			mi.appendReplacement(italics, "{\\\\i $1} ");
-		}
-		mi.appendTail(italics);
-		htmlText = italics.toString();
-		
-		Matcher mbr = PATTERN_HTML_BREAK.matcher(htmlText);
-		StringBuffer breaks = new StringBuffer();
-		while(mbr.find()){
-			mbr.appendReplacement(breaks, "\\\\line ");
-		}
-		mbr.appendTail(breaks);
-		htmlText = breaks.toString();
-		
-		Matcher mofo = PATTERN_CSS_O_FOQUOTE.matcher(htmlText);
-		StringBuffer foquotes = new StringBuffer();
-		while(mofo.find()){
-			mofo.appendReplacement(foquotes, "\\\\line {\\\\i $1} {\\\\pard $2\\\\par}");
-		}
-		mofo.appendTail(foquotes);
-		htmlText = foquotes.toString();
-		
-		Matcher mp = PATTERN_HTML_PARAGRAPH.matcher(htmlText);
-		StringBuffer paragraphs = new StringBuffer();
-		while(mp.find()){
-			mp.appendReplacement(paragraphs, "\\\\line $1 \\\\line");
-		}
-		mp.appendTail(paragraphs);
-		htmlText = paragraphs.toString();
-		
-		Matcher mahref = PATTERN_HTML_AHREF.matcher(htmlText);
-		StringBuffer ahrefs = new StringBuffer();
-		while(mahref.find()){
-			mahref.appendReplacement(ahrefs, "{\\\\field{\\\\*\\\\fldinst{HYPERLINK\"$1\"}}{\\\\fldrslt{\\\\ul $2}}}");
-		}
-		mahref.appendTail(ahrefs);
-		htmlText = ahrefs.toString();
-		
-		Matcher mli = PATTERN_HTML_LIST.matcher(htmlText);
-		StringBuffer lists = new StringBuffer();
-		while(mli.find()){
-			mli.appendReplacement(lists, "$1\\\\line ");
-		}
-		mli.appendTail(lists);
-		htmlText = lists.toString();
-		
-		Matcher mtp = PATTERN_THREEPOINTS.matcher(htmlText);
-		StringBuffer tps = new StringBuffer();
-		while (mtp.find()) {
-			mtp.appendReplacement(tps, THREEPOINTS);
-		}
-		mtp.appendTail(tps);
-		htmlText = tps.toString();
-
-		// strip all other html-fragments, because not convertable that easy
-		htmlText = FilterFactory.getHtmlTagsFilter().filter(htmlText);
-		// Remove all &nbsp;
-		Matcher tmp = HTML_SPACE_PATTERN.matcher(htmlText);
-		htmlText = tmp.replaceAll(" ");
-		htmlText = StringHelper.unescapeHtml(htmlText);
-
-		return htmlText;
-	}
-	
-	/**
-	 * 
-	 * @param messageNode
-	 * @return title prefix for hidden forum threads.
-	 */
-	private String getTitlePrefix(MessageNode messageNode) {
-		StringBuilder titleSb = new StringBuilder();
-		if(messageNode.isHidden()) {
-			titleSb.append(HIDDEN_STR);
-		} 		
-		if(titleSb.length()>1) {
-			titleSb.append(": ");
-		}
-		return titleSb.toString();
-	}
-	
-	/**
-	 * Gets the RTF image section for the input messageNode.
-	 * @param messageNode
-	 * @return the RTF image section for the input messageNode.
-	 */
-	private String getImageRTF(MessageNode messageNode) {			
-		StringBuilder imgSb = new StringBuilder();
-		for(String imageName : addImagesToVFSContainer(messageNode)) {
-			imgSb.append("{\\field\\fldedit{\\*\\fldinst { INCLUDEPICTURE ")
-			  .append("\"").append(imageName).append("\"")
-			  .append(" \\\\d }}{\\fldrslt {}}}");			
-		}				
-		return imgSb.toString();
-	}
-	
-	/**
-	 * Retrieves the appropriate images for the input messageNode, if any, 
-	 * and adds it to the input container.
-	 * 
-	 * @param messageNode
-	 * @param container
-	 * @return
-	 */
-	private List<String> addImagesToVFSContainer(MessageNode messageNode) {
-		List<String> fileNameList = new ArrayList<>();
-		String iconPath = null;
-		if(messageNode.isClosed() && messageNode.isSticky()) {
-			iconPath = getImagePath("fo_sticky_closed");
-		} else if(messageNode.isClosed()) {
-			iconPath = getImagePath("fo_closed");			
-		} else if(messageNode.isSticky()) {
-			iconPath = getImagePath("fo_sticky");			
-		}		
-		if (iconPath != null) {
-			File file = new File(iconPath);
-			if (file.exists()) {
-				fileNameList.add(file.getName());
-			} else {
-				log.error("Could not find image for forum RTF formatter::" + iconPath);
-			}
-		}
-		return fileNameList;
-	}
-	
-	/**
-	 * Gets the image path.
-	 * @param val
-	 * @return the path of the static icon image.
-	 */
-	private String getImagePath(Object val) { 		
-		return WebappHelper.getContextRealPath("/static/images/forum/" + val.toString() + ".png");				
-	}
-}
diff --git a/src/main/java/org/olat/modules/fo/manager/ForumManager.java b/src/main/java/org/olat/modules/fo/manager/ForumManager.java
index bc7ef6aaaf8..1b2e4d042f0 100644
--- a/src/main/java/org/olat/modules/fo/manager/ForumManager.java
+++ b/src/main/java/org/olat/modules/fo/manager/ForumManager.java
@@ -111,7 +111,7 @@ public class ForumManager {
 	}
 
 	/**
-	 * @param msgid msg id of the topthread
+	 * @param msgid The message id of the top thread
 	 * @return List messages
 	 */
 	public List<Message> getThread(Long msgid) {
@@ -140,24 +140,24 @@ public class ForumManager {
 
 	/**
 	 * 
-	 * @param forum_id
-	 * @return
+	 * @param forumId The forum ID
+	 * @return The number of messages in the specified forum
 	 */
-	public int countThreadsByForumID(Long forum_id) {
-		return countMessagesByForumID(forum_id, true);
+	public int countThreadsByForumID(Long forumId) {
+		return countMessagesByForumID(forumId, true);
 	}
 	
 	/**
 	 * 
-	 * @param forum_id
+	 * @param forumId
 	 * @param start
 	 * @param limit
 	 * @param orderBy
 	 * @param asc
 	 * @return
 	 */
-	public List<Message> getThreadsByForumID(Long forum_id, int firstResult, int maxResults, Message.OrderBy orderBy, boolean asc) {
-		return getMessagesByForumID(forum_id, firstResult, maxResults, true, orderBy, asc);
+	public List<Message> getThreadsByForumID(Long forumId, int firstResult, int maxResults, Message.OrderBy orderBy, boolean asc) {
+		return getMessagesByForumID(forumId, firstResult, maxResults, true, orderBy, asc);
 	}
 	
 	/**
@@ -166,7 +166,7 @@ public class ForumManager {
 	 * @return
 	 */
 	public List<Message> getMessagesByForum(Forum forum){
-		if (forum == null) return new ArrayList<>(0); // fxdiff: while indexing it can somehow occur, that forum is null!
+		if (forum == null) return new ArrayList<>(0); //while indexing it can somehow occur, that forum is null!
 		return getMessagesByForumID(forum.getKey(),  0, -1, false, null, true);
 	}
 	
@@ -905,9 +905,7 @@ public class ForumManager {
 			markingService.getMarkManager().deleteMarks(ores, m.getKey().toString());
 		}
 		
-		if(log.isDebugEnabled()){
-			log.debug("Deleting message: " + m.getKey());
-		}
+		log.debug("Deleting message: {}", m.getKey());
 	}
 
 	/**
@@ -920,7 +918,7 @@ public class ForumManager {
 				.createQuery(q, Number.class)
 				.setParameter("parentKey", m.getKey())
 				.getResultList();
-		return count == null || count.isEmpty() || count.get(0) == null ? false : count.get(0).longValue() > 0;
+		return count != null && !count.isEmpty() && count.get(0) != null && count.get(0).longValue() > 0;
 	}
 	
 	public int countMessageChildren(Long messageKey ) {
@@ -965,7 +963,7 @@ public class ForumManager {
 		} else if(messageContainer instanceof VFSContainer) {
 			return (VFSContainer)messageContainer;
 		}
-		log.error("The following message container is not a directory: " + messageContainer);
+		log.error("The following message container is not a directory: {}", messageContainer);
 		return null;
 	}
 	
diff --git a/src/main/java/org/olat/modules/fo/manager/QuoterFilter.java b/src/main/java/org/olat/modules/fo/manager/QuoterFilter.java
new file mode 100644
index 00000000000..e8e47cb786f
--- /dev/null
+++ b/src/main/java/org/olat/modules/fo/manager/QuoterFilter.java
@@ -0,0 +1,103 @@
+/**
+ * <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.modules.fo.manager;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+
+import org.apache.logging.log4j.Logger;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.filter.Filter;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import nu.validator.htmlparser.common.XmlViolationPolicy;
+import nu.validator.htmlparser.sax.HtmlParser;
+import nu.validator.htmlparser.sax.HtmlSerializer;
+
+/**
+ * 
+ * Initial date: 18 juin 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class QuoterFilter implements Filter {
+	
+	private static final Logger log = Tracing.createLoggerFor(QuoterFilter.class);
+	
+	@Override
+	public String filter(String original) {
+		if(original == null) return null;
+		if(original.isEmpty()) return "";
+		
+		try {
+			HtmlParser parser = new HtmlParser(XmlViolationPolicy.ALTER_INFOSET);
+			Writer writer = new StringWriter();
+			QuoteSerializer contentHandler = new QuoteSerializer(writer);
+			parser.setContentHandler(contentHandler);
+			parser.parse(new InputSource(new StringReader(original)));
+			return writer.toString();
+		} catch (Exception e) {
+			log.error("", e);
+			return null;
+		}
+	}
+	
+	private class QuoteSerializer extends HtmlSerializer {
+		
+		public QuoteSerializer(Writer writer) {
+			super(writer);
+		}
+
+		@Override
+		public void startDocument() throws SAXException {
+			// no doctype
+		}
+		
+		@Override
+		public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
+			if("img".equals(localName)) {
+				String img = "[" + atts.getValue("src") + "]";
+				char[] imgChArr = img.toCharArray();
+				characters(imgChArr, 0, imgChArr.length);
+				return;
+			}
+			if(ignore(localName)) {
+				return;
+			}
+			super.startElement(uri, localName, qName, atts);
+		}
+
+		@Override
+		public void endElement(String uri, String localName, String qName) throws SAXException {
+			if(ignore(localName)) {
+				return;
+			}
+			super.endElement(uri, localName, qName);
+		}
+		
+		private boolean ignore(String localName) {
+			return "html".equals(localName) || "head".equals(localName)
+					|| "body".equals(localName) || "img".equals(localName);
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/modules/fo/ui/MessageEditController.java b/src/main/java/org/olat/modules/fo/ui/MessageEditController.java
index c8ce1e7c51b..63643a782d0 100644
--- a/src/main/java/org/olat/modules/fo/ui/MessageEditController.java
+++ b/src/main/java/org/olat/modules/fo/ui/MessageEditController.java
@@ -27,12 +27,14 @@ import java.util.Comparator;
 import java.util.List;
 
 import javax.persistence.PersistenceException;
+import javax.servlet.http.HttpServletRequest;
 
 import org.olat.basesecurity.BaseSecurity;
 import org.olat.basesecurity.IdentityShort;
 import org.olat.core.commons.modules.bc.FolderConfig;
 import org.olat.core.commons.persistence.DBFactory;
 import org.olat.core.commons.services.notifications.NotificationsManager;
+import org.olat.core.dispatcher.mapper.Mapper;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.form.flexible.FormItem;
 import org.olat.core.gui.components.form.flexible.FormItemContainer;
@@ -50,8 +52,10 @@ import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.generic.modal.DialogBoxController;
 import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.gui.media.NotFoundMediaResource;
+import org.olat.core.helpers.Settings;
 import org.olat.core.id.Identity;
-import org.olat.core.logging.AssertException;
 import org.olat.core.logging.DBRuntimeException;
 import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
 import org.olat.core.util.CodeHelper;
@@ -59,12 +63,16 @@ import org.olat.core.util.StringHelper;
 import org.olat.core.util.Util;
 import org.olat.core.util.WebappHelper;
 import org.olat.core.util.coordinate.CoordinatorManager;
+import org.olat.core.util.filter.FilterFactory;
 import org.olat.core.util.vfs.LocalFolderImpl;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.core.util.vfs.VFSItem;
 import org.olat.core.util.vfs.VFSLeaf;
 import org.olat.core.util.vfs.VFSManager;
-import org.olat.core.util.vfs.filters.VFSItemMetaFilter;
+import org.olat.core.util.vfs.VFSMediaResource;
+import org.olat.core.util.vfs.filters.VFSItemFilter;
+import org.olat.core.util.vfs.filters.VFSLeafButSystemFilter;
+import org.olat.core.util.vfs.filters.VFSSystemItemFilter;
 import org.olat.modules.fo.Forum;
 import org.olat.modules.fo.ForumCallback;
 import org.olat.modules.fo.ForumChangedEvent;
@@ -100,25 +108,26 @@ public class MessageEditController extends FormBasicController {
 	private static final String[] enableKeys = new String[]{ "on" };
 	
 	private RichTextElement bodyEl;
-	private TextElement titleEl, pseudonymEl, passwordEl;
+	private TextElement titleEl;
+	private TextElement pseudonymEl;
+	private TextElement passwordEl;
 	private MultipleSelectionElement usePseudonymEl;
 	private FileElement fileUpload;
 
-	
 	private DisplayPortraitController portraitCtr;
 	private DialogBoxController confirmDeleteAttachmentCtrl;
 	
 	private VFSContainer tempUploadFolder;
 	private boolean userIsMsgCreator;
 	private boolean msgHasChildren;
-	private VFSItemMetaFilter exclFilter;
 
 	private final Forum forum;
 	private final EditMode editMode;
 	private final boolean guestOnly;
 	private String proposedPseudonym;
 	private final ForumCallback foCallback;
-	private Message message, parentMessage;
+	private Message message;
+	private Message parentMessage;
 
 	@Autowired
 	private ForumManager fm;
@@ -154,15 +163,10 @@ public class MessageEditController extends FormBasicController {
 		this.guestOnly = ureq.getUserSession().getRoles().isGuestOnly();
 
 		tempUploadFolder = new LocalFolderImpl(new File(WebappHelper.getTmpDir(), CodeHelper.getUniqueID()));
-		exclFilter = new VFSItemMetaFilter();
-		
+
 		initForm(ureq);
 	}
 
-	/**
-	 * @see org.olat.core.gui.components.form.flexible.impl.FormBasicController#initForm(org.olat.core.gui.components.form.flexible.FormItemContainer,
-	 *      org.olat.core.gui.control.Controller, org.olat.core.gui.UserRequest)
-	 */
 	@Override
 	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
 		formLayout.setElementCssClass("o_sel_forum_message_form");
@@ -171,7 +175,15 @@ public class MessageEditController extends FormBasicController {
 		titleEl.setElementCssClass("o_sel_forum_message_title");
 		titleEl.setMandatory(true);
 		titleEl.setNotEmptyCheck("error.field.not.empty");
-		bodyEl = uifactory.addRichTextElementForStringData("msgBody", "msg.body", message.getBody(), 15, -1, true, null, null,
+		
+		VFSContainer msgContainer;
+		if(message.getKey() != null) {
+			msgContainer = fm.getMessageContainer(forum.getKey(), message.getKey());
+		} else {
+			msgContainer = tempUploadFolder;
+		}
+		msgContainer = VFSManager.getOrCreateContainer(msgContainer, "media");
+		bodyEl = uifactory.addRichTextElementForStringData("msgBody", "msg.body", message.getBody(), 15, -1, true, msgContainer, "media", null,
 				formLayout, ureq.getUserSession(), getWindowControl());
 		bodyEl.setElementCssClass("o_sel_forum_message_body");
 		bodyEl.setMandatory(true);
@@ -181,6 +193,8 @@ public class MessageEditController extends FormBasicController {
 		bodyEl.getEditorConfiguration().enableCharCount();
 		bodyEl.getEditorConfiguration().setRelativeUrls(false);
 		bodyEl.getEditorConfiguration().setRemoveScriptHost(false);
+		bodyEl.getEditorConfiguration().disableMedia();
+		bodyEl.getEditorConfiguration().disableTinyMedia();
 		
 		setEditPermissions(message);
 		// list existing attachments. init attachment layout now, to place it in
@@ -188,7 +202,7 @@ public class MessageEditController extends FormBasicController {
 		createOrUpdateAttachmentListLayout(formLayout);
 
 		// provide upload field
-		if (foCallback.mayEditMessageAsModerator() || ((userIsMsgCreator) && (msgHasChildren == false))) {
+		if (foCallback.mayEditMessageAsModerator() || (userIsMsgCreator && !msgHasChildren)) {
 			fileUpload = uifactory.addFileElement(getWindowControl(), "msg.upload", formLayout);
 			fileUpload.addActionListener(FormEvent.ONCHANGE);
 			fileUpload.setMaxUploadSizeKB((int) FolderConfig.getLimitULKB(), "attachments.too.big", new String[] { ((Long) (FolderConfig
@@ -248,27 +262,41 @@ public class MessageEditController extends FormBasicController {
 		// save and cancel buttons
 		FormLayoutContainer buttonLayout = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
 		formLayout.add(buttonLayout);
-		uifactory.addFormSubmitButton("msg.save", buttonLayout);
 		uifactory.addFormCancelButton("msg.cancel", buttonLayout, ureq, getWindowControl());
+		uifactory.addFormSubmitButton("msg.save", buttonLayout);
 
 		// show message replying to, if in reply modus
 		if (editMode == EditMode.reply) {
-			String previewPage = Util.getPackageVelocityRoot(this.getClass()) + "/msg-preview.html";
-			FormLayoutContainer replyMsgLayout = FormLayoutContainer.createCustomFormLayout("replyMsg", getTranslator(), previewPage);
-			uifactory.addSpacerElement("spacer1", formLayout, false);
-			formLayout.add(replyMsgLayout);
-			
-			replyMsgLayout.setLabel("label.replytomsg", new String[] { StringHelper.escapeHtml(parentMessage.getTitle()) });
-			replyMsgLayout.contextPut("messageBody", parentMessage.getBody());
-			replyMsgLayout.contextPut("message", parentMessage);
-			replyMsgLayout.contextPut("guestOnly", new Boolean(guestOnly));
-
-			Identity creator = parentMessage.getCreator();
-			if(creator != null) {
-				replyMsgLayout.contextPut("identity", creator);
-				portraitCtr = new DisplayPortraitController(ureq, getWindowControl(), creator, true, true);
-				replyMsgLayout.put("portrait", portraitCtr.getInitialComponent());
-			}
+			initReply(formLayout, ureq);
+		}
+	}
+	
+	private void initReply(FormItemContainer formLayout, UserRequest ureq) {
+		String previewPage = Util.getPackageVelocityRoot(this.getClass()) + "/msg-preview.html";
+		FormLayoutContainer replyMsgLayout = FormLayoutContainer.createCustomFormLayout("replyMsg", getTranslator(), previewPage);
+		uifactory.addSpacerElement("spacer1", formLayout, false);
+		formLayout.add(replyMsgLayout);
+		
+		replyMsgLayout.setLabel("label.replytomsg", new String[] { StringHelper.escapeHtml(parentMessage.getTitle()) });
+		String body = parentMessage.getBody();
+		
+		VFSContainer parentMessageContainer = fm.getMessageContainer(forum.getKey(), parentMessage.getKey());
+		VFSItem parentMediaItem = parentMessageContainer.resolve("media");
+		if(parentMediaItem instanceof VFSContainer) {
+			String mapper = registerCacheableMapper(ureq, "fo_reply_" + parentMessage.getKey(),
+					new BodyMediaMapper((VFSContainer)parentMediaItem));
+			String messageMapperUri = mapper + "/" + parentMessage.getKey() + "/";
+			body = FilterFactory.getBaseURLToMediaRelativeURLFilter(messageMapperUri).filter(body);
+		}
+		replyMsgLayout.contextPut("messageBody", body);
+		replyMsgLayout.contextPut("message", parentMessage);
+		replyMsgLayout.contextPut("guestOnly", Boolean.valueOf(guestOnly));
+
+		Identity creator = parentMessage.getCreator();
+		if(creator != null) {
+			replyMsgLayout.contextPut("identity", creator);
+			portraitCtr = new DisplayPortraitController(ureq, getWindowControl(), creator, true, true);
+			replyMsgLayout.put("portrait", portraitCtr.getInitialComponent());
 		}
 	}
 
@@ -296,10 +324,10 @@ public class MessageEditController extends FormBasicController {
 		// add already existing attachments:
 		if (message.getKey() != null) {
 			VFSContainer msgContainer = fm.getMessageContainer(message.getForum().getKey(), message.getKey());
-			attachments.addAll(msgContainer.getItems(exclFilter));
+			attachments.addAll(msgContainer.getItems(new VFSLeafButSystemFilter()));
 		}
 		// add files from TempFolder
-		attachments.addAll(getTempFolderFileList());
+		attachments.addAll(getTempFolderFileList(new VFSLeafButSystemFilter()));
 		
 		Collections.sort(attachments, new Comparator<VFSItem>(){
 			final Collator c = Collator.getInstance(getLocale());
@@ -321,7 +349,7 @@ public class MessageEditController extends FormBasicController {
 		int attNr = 1;
 		for (VFSItem tmpFile : attachments) {
 			FormLink tmpLink = uifactory.addFormLink(CMD_DELETE_ATTACHMENT + attNr, tmpLayout, Link.BUTTON_XSMALL);
-			if (!(foCallback.mayEditMessageAsModerator() || ((userIsMsgCreator) && (msgHasChildren == false)))) {
+			if (!(foCallback.mayEditMessageAsModerator() || (userIsMsgCreator && !msgHasChildren))) {
 				tmpLink.setEnabled(false);  
 				tmpLink.setVisible(false);
 			}
@@ -331,9 +359,6 @@ public class MessageEditController extends FormBasicController {
 		}
 	}
 
-	/**
-	 * @see org.olat.core.gui.components.form.flexible.impl.FormBasicController#doDispose()
-	 */
 	@Override
 	protected void doDispose() {
 		removeTempUploadedFiles();
@@ -349,7 +374,7 @@ public class MessageEditController extends FormBasicController {
 
 	@Override
 	protected boolean validateFormLogic(UserRequest ureq) {
-		boolean allOk = true;
+		boolean allOk = super.validateFormLogic(ureq);
 		if(usePseudonymEl != null) {
 			pseudonymEl.clearError();
 			passwordEl.clearError();
@@ -368,7 +393,7 @@ public class MessageEditController extends FormBasicController {
 				}
 			}
 		}
-		return allOk & super.validateFormLogic(ureq);
+		return allOk;
 	}
 	
 	private boolean validatePseudonym(String value) {
@@ -408,7 +433,7 @@ public class MessageEditController extends FormBasicController {
 		
 		if(StringHelper.containsNonWhitespace(password)) {
 			List<Pseudonym> pseudonyms = fm.getPseudonyms(value);
-			if(pseudonyms.size() > 0) {
+			if(!pseudonyms.isEmpty()) {
 				boolean authenticated = false;
 				for(Pseudonym pseudonym:pseudonyms) {
 					if(fm.authenticatePseudonym(pseudonym, password)) {
@@ -455,11 +480,28 @@ public class MessageEditController extends FormBasicController {
 		}
 
 		// set values from form to message
+		commitBody();
+		commitPseudonym(ureq);
+
+		if(editMode == EditMode.newThread) {
+			commitNewThreadMode();
+		} else if(editMode == EditMode.edit) { 
+			commitEditMode();
+		} else if(editMode == EditMode.reply) { 
+			commitReplyMode();
+		}
+	}
+	
+	private void commitBody() {
 		message.setTitle(titleEl.getValue());
 		String body = bodyEl.getValue();
 		body = body.replace("<p>&nbsp;", "<p>");
-
+		String editorMapperUri = Settings.createServerURI() + bodyEl.getEditorConfiguration().getMapperURI();
+		body = body.replace(editorMapperUri, "media/");
 		message.setBody(body.trim());
+	}
+
+	private void commitPseudonym(UserRequest ureq) {
 		if(usePseudonymEl != null && (usePseudonymEl.isAtLeastSelected(1) || guestOnly)) {
 			String password = passwordEl.getValue();
 			String pseudonym = pseudonymEl.getValue();
@@ -492,58 +534,61 @@ public class MessageEditController extends FormBasicController {
 		} else if(message.getCreator() != null && message.getCreator().equals(getIdentity())) {
 			message.setPseudonym(null);
 		}
-
-		if(editMode == EditMode.newThread) {
-			if(foCallback.mayOpenNewThread()) {
-				// save a new thread
-				message = fm.addTopMessage(message);
-				fm.markNewMessageAsRead(getIdentity(), forum, message);
-				persistTempUploadedFiles(message);
-				// if notification is enabled -> notify the publisher about news
-				notifiySubscription();
-				addLoggingResourceable(LoggingResourceable.wrap(message));
-				//commit before sending events
-				DBFactory.getInstance().commit();
-				ForumChangedEvent event = new ForumChangedEvent(ForumChangedEvent.NEW_MESSAGE, message.getKey(), message.getKey(), getIdentity());
-				CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(event, forum);	
-				ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_MESSAGE_CREATE, getClass());
-			} else {
-				showWarning("may.not.save.msg.as.author");
-			}
-
-		} else if(editMode == EditMode.edit) { 
-			boolean children = fm.countMessageChildren(message.getKey()) > 0;
-			if (foCallback.mayEditMessageAsModerator() || (userIsMsgCreator && !children)) {
-				message.setModifier(getIdentity());	
-				message = fm.updateMessage(message, true);
-				persistTempUploadedFiles(message);
-				notifiySubscription();
-				//commit before sending events
-				DBFactory.getInstance().commit();
-				Long threadTopKey = message.getThreadtop() == null ? null : message.getThreadtop().getKey();
-				ForumChangedEvent event = new ForumChangedEvent(ForumChangedEvent.CHANGED_MESSAGE, threadTopKey, message.getKey(), getIdentity());
-				CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(event, forum);
-				ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_MESSAGE_EDIT, getClass(),
-						LoggingResourceable.wrap(message));
-			} else {
-				showWarning("may.not.save.msg.as.author");
-			}
-		} else if(editMode == EditMode.reply) { 
-			message = fm.replyToMessage(message, parentMessage);
+	}
+	
+	private void commitNewThreadMode() {
+		if(foCallback.mayOpenNewThread()) {
+			// save a new thread
+			message = fm.addTopMessage(message);
 			fm.markNewMessageAsRead(getIdentity(), forum, message);
 			persistTempUploadedFiles(message);
+			// if notification is enabled -> notify the publisher about news
 			notifiySubscription();
-			Long threadTopKey = message.getThreadtop() == null ? null : message.getThreadtop().getKey();
-
+			addLoggingResourceable(LoggingResourceable.wrap(message));
 			//commit before sending events
 			DBFactory.getInstance().commit();
-			ForumChangedEvent event = new ForumChangedEvent(ForumChangedEvent.NEW_MESSAGE, threadTopKey, message.getKey(), getIdentity());
+			ForumChangedEvent event = new ForumChangedEvent(ForumChangedEvent.NEW_MESSAGE, message.getKey(), message.getKey(), getIdentity());
 			CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(event, forum);	
-			ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_REPLY_MESSAGE_CREATE, getClass(),
+			ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_MESSAGE_CREATE, getClass());
+		} else {
+			showWarning("may.not.save.msg.as.author");
+		}
+	}
+	
+	private void commitEditMode() {
+		boolean children = fm.countMessageChildren(message.getKey()) > 0;
+		if (foCallback.mayEditMessageAsModerator() || (userIsMsgCreator && !children)) {
+			message.setModifier(getIdentity());	
+			message = fm.updateMessage(message, true);
+			persistTempUploadedFiles(message);
+			notifiySubscription();
+			//commit before sending events
+			DBFactory.getInstance().commit();
+			Long threadTopKey = message.getThreadtop() == null ? null : message.getThreadtop().getKey();
+			ForumChangedEvent event = new ForumChangedEvent(ForumChangedEvent.CHANGED_MESSAGE, threadTopKey, message.getKey(), getIdentity());
+			CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(event, forum);
+			ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_MESSAGE_EDIT, getClass(),
 					LoggingResourceable.wrap(message));
+		} else {
+			showWarning("may.not.save.msg.as.author");
 		}
 	}
 	
+	private void commitReplyMode() {
+		message = fm.replyToMessage(message, parentMessage);
+		fm.markNewMessageAsRead(getIdentity(), forum, message);
+		persistTempUploadedFiles(message);
+		notifiySubscription();
+		Long threadTopKey = message.getThreadtop() == null ? null : message.getThreadtop().getKey();
+
+		//commit before sending events
+		DBFactory.getInstance().commit();
+		ForumChangedEvent event = new ForumChangedEvent(ForumChangedEvent.NEW_MESSAGE, threadTopKey, message.getKey(), getIdentity());
+		CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(event, forum);	
+		ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_REPLY_MESSAGE_CREATE, getClass(),
+				LoggingResourceable.wrap(message));
+	}
+	
 	private void notifiySubscription() {
 		if (foCallback.getSubscriptionContext() != null) {
 			notificationsManager.markPublisherNews(foCallback.getSubscriptionContext(), getIdentity(), true);
@@ -570,7 +615,7 @@ public class MessageEditController extends FormBasicController {
 
 					// checking tmp-folder and msg-container for filename
 					boolean fileExists = false;
-					if (getTempFolderFileList().contains(fileName)) {
+					if (getTempFolderFilenameList(new VFSSystemItemFilter()).contains(fileName)) {
 						fileExists = true;
 					}
 					if (message.getKey() != null) {
@@ -612,7 +657,7 @@ public class MessageEditController extends FormBasicController {
 						confirmDeleteAttachmentCtrl = activateYesNoDialog(ureq, null, translate("reallydeleteAtt"), confirmDeleteAttachmentCtrl);
 						confirmDeleteAttachmentCtrl.setUserObject(file);
 					} else {
-						if ((userIsMsgCreator) && (msgHasChildren == true)) {
+						if (userIsMsgCreator && msgHasChildren) {
 							// user is author of the current message but it has already at
 							// least one child
 							showWarning("may.not.delete.att.as.author");
@@ -626,22 +671,27 @@ public class MessageEditController extends FormBasicController {
 		}
 	}
 
-	private List<VFSItem> getTempFolderFileList() {
+	private List<VFSItem> getTempFolderFileList(VFSItemFilter filter) {
 		if (tempUploadFolder == null) {
 			tempUploadFolder = VFSManager.olatRootContainer(File.separator + "tmp/" + CodeHelper.getGlobalForeverUniqueID() + "/", null);
 		}		
-		return tempUploadFolder.getItems(exclFilter);
+		return tempUploadFolder.getItems(filter);
+	}
+	
+	private List<String>getTempFolderFilenameList(VFSItemFilter filter) {
+		List<VFSItem> items = getTempFolderFileList(filter);
+		List<String> filenames = new ArrayList<>(items.size());
+		for(VFSItem item:items) {
+			filenames.add(item.getName());
+		}
+		return filenames;
 	}
 
-	/**
-	 * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest,
-	 *      org.olat.core.gui.control.Controller, org.olat.core.gui.control.Event)
-	 */
 	@Override
 	protected void event(UserRequest ureq, Controller source, Event event) {
 		super.event(ureq, source, event);
 		if (source == confirmDeleteAttachmentCtrl) {
-			if (DialogBoxUIFactory.isYesEvent(event)) { // ok to really delete this																					// attachment
+			if (DialogBoxUIFactory.isYesEvent(event)) { // ok to really delete this attachment
 				Object userObj = confirmDeleteAttachmentCtrl.getUserObject();
 				if (userObj instanceof VFSLeaf) {
 					((VFSLeaf)userObj).delete();
@@ -680,23 +730,40 @@ public class MessageEditController extends FormBasicController {
 	 * @param tmpMessage
 	 */
 	public void persistTempUploadedFiles(Message tmpMessage) {
-		if (tmpMessage == null) throw new AssertException("Message may not be null to persist temp files");
+		if (tmpMessage == null) return;
+		
 		VFSContainer msgContainer = fm.getMessageContainer(forum.getKey(), message.getKey());
 		if (msgContainer != null) {
-			List<VFSItem> tmpFList = getTempFolderFileList();
+			List<VFSItem> tmpFList = getTempFolderFileList(new VFSSystemItemFilter());
 			for (VFSItem file : tmpFList) {
-				VFSLeaf leaf = (VFSLeaf) file;
-				try {
-					VFSLeaf targetFile = msgContainer.createChildLeaf(leaf.getName());
-					VFSManager.copyContent(leaf, targetFile, false);
-				} catch (Exception e) {
-					removeTempUploadedFiles();
-					throw new RuntimeException ("I/O error saving uploaded file:" + msgContainer + "/" + leaf.getName());
+				if(file instanceof VFSLeaf) {
+					copyTempContent((VFSLeaf) file, msgContainer);
+				} else if(file instanceof VFSContainer && "media".equals(file.getName())) {
+					copyTempMediaContent((VFSContainer)file, msgContainer);	
 				}
 			}
 		}
 		removeTempUploadedFiles();
 	}
+	
+	private void copyTempMediaContent(VFSContainer tempContainer, VFSContainer msgContainer) {
+		List<VFSItem> tempEmbededFList = tempContainer.getItems(new VFSSystemItemFilter());
+		VFSContainer mediaContainer = VFSManager.getOrCreateContainer(msgContainer, "media");
+		for(VFSItem file:tempEmbededFList) {
+			if(file instanceof VFSLeaf) {
+				copyTempContent((VFSLeaf) file, mediaContainer);
+			}
+		}
+	}
+	
+	private void copyTempContent(VFSLeaf leaf, VFSContainer msgContainer) {
+		try {
+			VFSLeaf targetFile = msgContainer.createChildLeaf(leaf.getName());
+			VFSManager.copyContent(leaf, targetFile, false);
+		} catch (Exception e) {
+			logError("Cannot move files", e);
+		}
+	}
 
 	private void removeTempUploadedFiles() {
 		if (tempUploadFolder != null) {
@@ -704,4 +771,30 @@ public class MessageEditController extends FormBasicController {
 			tempUploadFolder = null;
 		}
 	}
+	
+	private class BodyMediaMapper implements Mapper {
+		
+		private final VFSContainer mediaContainer;
+		
+		public BodyMediaMapper(VFSContainer mediaContainer) {
+			this.mediaContainer = mediaContainer;
+		}
+		
+		@Override
+		public MediaResource handle(String relPath, HttpServletRequest request) {
+			String[] query = relPath.split("/"); // expected path looks like this /messageId/attachmentUUID/filename	
+			MediaResource resource = null;
+			if (query.length == 4) {
+				VFSItem item = mediaContainer.resolve(query[3]);
+				if(item instanceof VFSLeaf) {
+					resource = new VFSMediaResource((VFSLeaf)item);
+				}	
+			}
+			// In any error case, send not found
+			if(resource == null) {
+				resource = new NotFoundMediaResource();
+			}
+			return resource;
+		}
+	}
 }
diff --git a/src/main/java/org/olat/modules/fo/ui/MessageListController.java b/src/main/java/org/olat/modules/fo/ui/MessageListController.java
index 566214a0616..7d05902624a 100644
--- a/src/main/java/org/olat/modules/fo/ui/MessageListController.java
+++ b/src/main/java/org/olat/modules/fo/ui/MessageListController.java
@@ -78,13 +78,14 @@ import org.olat.core.util.StringHelper;
 import org.olat.core.util.Util;
 import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.core.util.event.GenericEventListener;
+import org.olat.core.util.filter.FilterFactory;
 import org.olat.core.util.prefs.Preferences;
 import org.olat.core.util.resource.OresHelper;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.core.util.vfs.VFSItem;
 import org.olat.core.util.vfs.VFSLeaf;
 import org.olat.core.util.vfs.VFSMediaResource;
-import org.olat.core.util.vfs.filters.VFSItemMetaFilter;
+import org.olat.core.util.vfs.filters.VFSLeafButSystemFilter;
 import org.olat.course.nodes.FOCourseNode;
 import org.olat.modules.fo.Forum;
 import org.olat.modules.fo.ForumCallback;
@@ -99,6 +100,7 @@ import org.olat.modules.fo.export.FinishCallback;
 import org.olat.modules.fo.export.SendMailStepForm;
 import org.olat.modules.fo.export.Step_1_SelectCourse;
 import org.olat.modules.fo.manager.ForumManager;
+import org.olat.modules.fo.manager.QuoterFilter;
 import org.olat.modules.fo.portfolio.ForumMediaHandler;
 import org.olat.modules.fo.ui.MessageEditController.EditMode;
 import org.olat.modules.fo.ui.events.DeleteMessageEvent;
@@ -440,8 +442,11 @@ public class MessageListController extends BasicController implements GenericEve
 		for(MarkResourceStat stat:statList) {
 			stats.put(stat.getSubPath(), stat);
 		}
-
-		MessageView view = new MessageView(message, userPropertyHandlers, getLocale());
+		
+		String body = message.getBody();
+		String messageMapperUri = thumbnailMapper + "/" + message.getKey() + "/";
+		body = FilterFactory.getBaseURLToMediaRelativeURLFilter(messageMapperUri).filter(body);
+		MessageView view = new MessageView(message, body, userPropertyHandlers, getLocale());
 		view.setNumOfChildren(0);
 		addMessageToCurrentMessagesAndVC(ureq, message, view, marks, stats, rms);
 		return view;
@@ -475,7 +480,10 @@ public class MessageListController extends BasicController implements GenericEve
 		List<MessageView> views = new ArrayList<>(messages.size());
 		Map<Long,MessageView> keyToViews = new HashMap<>();
 		for(MessageLight msg:messages) {
-			MessageView view = new MessageView(msg, userPropertyHandlers, getLocale());
+			String body = msg.getBody();
+			String messageMapperUri = thumbnailMapper + "/" + msg.getKey() + "/";
+			body = FilterFactory.getBaseURLToMediaRelativeURLFilter(messageMapperUri).filter(body);
+			MessageView view = new MessageView(msg, body, userPropertyHandlers, getLocale());
 			view.setNumOfChildren(0);
 			views.add(view);
 			keyToViews.put(msg.getKey(), view);
@@ -610,7 +618,13 @@ public class MessageListController extends BasicController implements GenericEve
 		VFSContainer msgContainer = forumManager.getMessageContainer(forum.getKey(), m.getKey());
 		if(msgContainer != null) {
 			messageView.setMessageContainer(msgContainer);
-			List<VFSItem> attachments = new ArrayList<>(msgContainer.getItems(new VFSItemMetaFilter()));				
+			List<VFSItem> attachmentsItem = msgContainer.getItems(new VFSLeafButSystemFilter());
+			List<VFSLeaf> attachments = new ArrayList<>(attachmentsItem.size());
+			for(VFSItem attachmentItem:attachmentsItem) {
+				if(attachmentItem instanceof VFSLeaf) {
+					attachments.add((VFSLeaf)attachmentItem);
+				}
+			}
 			messageView.setAttachments(attachments);
 		} else {
 			messageView.setAttachments(new ArrayList<>());
@@ -806,11 +820,11 @@ public class MessageListController extends BasicController implements GenericEve
 			String messageKey = cmd.substring(index + 1);
 			
 			int position = Integer.parseInt(attachmentPosition);
-			Long key = new Long(messageKey);
+			Long key = Long.valueOf(messageKey);
 			for(MessageView view:backupViews) {
 				if(view.getKey().equals(key)) {
-					List<VFSItem> attachments = view.getAttachments();
-					VFSLeaf attachment = (VFSLeaf)attachments.get(position - 1);//velocity counter start with 1
+					List<VFSLeaf> attachments = view.getAttachments();
+					VFSLeaf attachment = attachments.get(position - 1);//velocity counter start with 1
 					VFSMediaResource fileResource = new VFSMediaResource(attachment);
 					fileResource.setDownloadable(true); // prevent XSS attack
 					res = fileResource;
@@ -949,26 +963,8 @@ public class MessageListController extends BasicController implements GenericEve
 			}			
 			newMessage.setTitle(reString + parentMessage.getTitle());
 			if (quote) {
-				// load message to form as quotation				
-				StringBuilder quoteSb = new StringBuilder();
-				quoteSb.append("<p></p><div class=\"o_quote_wrapper\"><div class=\"o_quote_author mceNonEditable\">");
-				String date = formatter.formatDateAndTime(parentMessage.getCreationDate());
-				String creatorName;
-				if(StringHelper.containsNonWhitespace(parentMessage.getPseudonym())) {
-					creatorName = parentMessage.getPseudonym();
-				} else if(parentMessage.isGuest()) {
-					creatorName = translate("guest");
-				} else {
-					User creator = parentMessage.getCreator().getUser();
-					creatorName = creator.getProperty(UserConstants.FIRSTNAME, getLocale()) + " " + creator.getProperty(UserConstants.LASTNAME, getLocale());
-				}
-				
-				quoteSb.append(translate("msg.quote.intro", new String[]{ date, creatorName}))
-				     .append("</div><blockquote class=\"o_quote\">")
-				     .append(parentMessage.getBody())
-				     .append("</blockquote></div>")
-				     .append("<p></p>");
-				newMessage.setBody(quoteSb.toString());
+				String quoted = buildReplyWithQuote(parentMessage);
+				newMessage.setBody(quoted);
 			}
 
 			replyMessageCtrl = new MessageEditController(ureq, getWindowControl(), forum, foCallback, newMessage, parentMessage, EditMode.reply);
@@ -983,6 +979,31 @@ public class MessageListController extends BasicController implements GenericEve
 		}
 	}
 	
+	private String buildReplyWithQuote(Message parentMessage) {
+		// load message to form as quotation				
+		StringBuilder quoteSb = new StringBuilder();
+		quoteSb.append("<p></p><div class=\"o_quote_wrapper\"><div class=\"o_quote_author mceNonEditable\">");
+		String date = formatter.formatDateAndTime(parentMessage.getCreationDate());
+		String creatorName;
+		if(StringHelper.containsNonWhitespace(parentMessage.getPseudonym())) {
+			creatorName = parentMessage.getPseudonym();
+		} else if(parentMessage.isGuest()) {
+			creatorName = translate("guest");
+		} else {
+			User creator = parentMessage.getCreator().getUser();
+			creatorName = creator.getProperty(UserConstants.FIRSTNAME, getLocale()) + " " + creator.getProperty(UserConstants.LASTNAME, getLocale());
+		}
+		
+		String originalBody = parentMessage.getBody();
+		String filteredBody = new QuoterFilter().filter(originalBody);
+		quoteSb.append(translate("msg.quote.intro", new String[]{ date, creatorName}))
+		     .append("</div><blockquote class=\"o_quote\">")
+		     .append(filteredBody)
+		     .append("</blockquote></div>")
+		     .append("<p></p>");
+		return quoteSb.toString();
+	}
+	
 	private void doConfirmDeleteMessage(UserRequest ureq, MessageView message) {
 		// user has clicked on button 'delete'
 		// -> display modal dialog 'Do you really want to delete this message?'
@@ -1114,9 +1135,7 @@ public class MessageListController extends BasicController implements GenericEve
 	private void doArchiveThread(UserRequest ureq, Message currMsg) {
 		Message m = currMsg.getThreadtop();
 		Long topMessageId = (m == null) ? currMsg.getKey() : m.getKey();
-		
-		VFSContainer forumContainer = forumManager.getForumContainer(forum.getKey());
-		ForumDownloadResource download = new ForumDownloadResource("Forum", forum, foCallback, topMessageId, forumContainer, getLocale());
+		ForumDownloadResource download = new ForumDownloadResource("Forum", forum, foCallback, topMessageId, getLocale());
 		ureq.getDispatchResult().setResultingMediaResource(download);
 	}
 	
@@ -1490,37 +1509,70 @@ public class MessageListController extends BasicController implements GenericEve
 		@Override
 		public MediaResource handle(String relPath, HttpServletRequest request) {
 			String[] query = relPath.split("/"); // expected path looks like this /messageId/attachmentUUID/filename
+			
+			MediaResource resource = null;
 			if (query.length == 4) {
-				try {
-					Long mId = Long.valueOf(Long.parseLong(query[1]));
-					MessageView view = null;
-					for (MessageView m : backupViews) {
-						// search for message in current message map
-						if (m.getKey().equals(mId)) {
-							view = m;
-							break;
-						}
-					}
-					if (view != null) {
-						List<VFSItem> attachments = view.getAttachments();
-						for (VFSItem vfsItem : attachments) {
-							VFSMetadata meta = vfsItem.getMetaInfo();
-							if (meta instanceof VFSLeaf && meta.getUuid().equals(query[2])) {
-								VFSLeaf thumb = vfsRepositoryService.getThumbnail((VFSLeaf)vfsItem, meta, 200, 200, false);
-								if(thumb != null) {
-									// Positive lookup, send as response
-									return new VFSMediaResource(thumb);
-								}
-								break;
-							}
-						}
+				MessageView view = getView(query[1]);
+				if (view != null) {
+					if("media".equals(query[2])) {
+						resource = getMedia(view, query[3]);
+					} else {
+						resource = getThumbnail(view, query[2]);
 					}
-				} catch (NumberFormatException e) {
-					//
 				}
 			}
 			// In any error case, send not found
-			return new NotFoundMediaResource();
+			if(resource == null) {
+				resource = new NotFoundMediaResource();
+			}
+			return resource;
+		}
+		 
+		private MessageView getView(String queryParam) {
+			MessageView view = null;
+			try {
+				Long mId = Long.valueOf(Long.parseLong(queryParam ));
+				for (MessageView m : backupViews) {
+					// search for message in current message map
+					if (m.getKey().equals(mId)) {
+						view = m;
+						break;
+					}
+				}
+			} catch (NumberFormatException e) {
+				//
+			}
+			return view;
+		}
+		
+		private MediaResource getMedia(MessageView view, String queryParam) {
+			VFSContainer messageContainer = view.getMessageContainer();
+			if(messageContainer == null) return null;
+			VFSItem mediaItem = messageContainer.resolve("media");
+			if(mediaItem instanceof VFSContainer) {
+				VFSContainer mediaContainer = (VFSContainer)mediaItem;
+				VFSItem media = mediaContainer.resolve(queryParam);
+				if(media instanceof VFSLeaf) {
+					return new VFSMediaResource((VFSLeaf)media);
+				}
+			}
+			return null;
+		}
+		
+		private MediaResource getThumbnail(MessageView view, String queryParam) {
+			List<VFSLeaf> attachments = view.getAttachments();
+			for (VFSLeaf attachment : attachments) {
+				VFSMetadata meta = attachment.getMetaInfo();
+				if (meta.getUuid().equals(queryParam)) {
+					VFSLeaf thumb = vfsRepositoryService.getThumbnail(attachment, meta, 200, 200, false);
+					if(thumb != null) {
+						// Positive lookup, send as response
+						return new VFSMediaResource(thumb);
+					}
+					break;
+				}
+			}
+			return null;
 		}
 	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/modules/fo/ui/MessageView.java b/src/main/java/org/olat/modules/fo/ui/MessageView.java
index 64bace8dce5..c92e46ad79b 100644
--- a/src/main/java/org/olat/modules/fo/ui/MessageView.java
+++ b/src/main/java/org/olat/modules/fo/ui/MessageView.java
@@ -23,7 +23,7 @@ import java.util.List;
 import java.util.Locale;
 
 import org.olat.core.util.vfs.VFSContainer;
-import org.olat.core.util.vfs.VFSItem;
+import org.olat.core.util.vfs.VFSLeaf;
 import org.olat.modules.fo.MessageLight;
 import org.olat.user.DisplayPortraitController;
 import org.olat.user.propertyhandlers.UserPropertyHandler;
@@ -54,14 +54,14 @@ public class MessageView extends MessageLightView {
 	private boolean closed;
 	private boolean moved;
 	
-	private List<VFSItem> attachments;
+	private List<VFSLeaf> attachments;
 	private VFSContainer messageContainer;
 	
 	private DisplayPortraitController portrait;
 	
-	public MessageView(MessageLight message, List<UserPropertyHandler> userPropertyHandlers, Locale locale) {
+	public MessageView(MessageLight message, String body, List<UserPropertyHandler> userPropertyHandlers, Locale locale) {
 		super(message, userPropertyHandlers, locale);
-		body = message.getBody();
+		this.body = body;
 	}
 
 	public String getBody() {
@@ -165,16 +165,16 @@ public class MessageView extends MessageLightView {
 		this.closed = closed;
 	}
 
-	public List<VFSItem> getAttachments() {
+	public List<VFSLeaf> getAttachments() {
 		return attachments;
 	}
 
-	public void setAttachments(List<VFSItem> attachments) {
+	public void setAttachments(List<VFSLeaf> attachments) {
 		this.attachments = attachments;
 	}
 	
 	public boolean hasAttachments() {
-		return attachments != null && attachments.size() > 0;
+		return attachments != null && !attachments.isEmpty();
 	}
 
 	public VFSContainer getMessageContainer() {
diff --git a/src/main/java/org/olat/modules/fo/ui/ThreadListController.java b/src/main/java/org/olat/modules/fo/ui/ThreadListController.java
index 6c3f9eef2b3..c58cf5733f2 100644
--- a/src/main/java/org/olat/modules/fo/ui/ThreadListController.java
+++ b/src/main/java/org/olat/modules/fo/ui/ThreadListController.java
@@ -48,7 +48,6 @@ import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
 import org.olat.core.id.Identity;
 import org.olat.core.util.Util;
-import org.olat.core.util.vfs.VFSContainer;
 import org.olat.modules.fo.Forum;
 import org.olat.modules.fo.ForumCallback;
 import org.olat.modules.fo.Message;
@@ -260,8 +259,7 @@ public class ThreadListController extends FormBasicController {
 	}
 	
 	private void doArchiveForum(UserRequest ureq) {
-		VFSContainer forumContainer = forumManager.getForumContainer(forum.getKey());
-		ForumDownloadResource download = new ForumDownloadResource("Forum", forum, foCallback, null, forumContainer, getLocale());
+		ForumDownloadResource download = new ForumDownloadResource("Forum", forum, foCallback, null, getLocale());
 		ureq.getDispatchResult().setResultingMediaResource(download);
 	}
 	
-- 
GitLab