From 012f60fe492871c1367e41aa53134740499710b1 Mon Sep 17 00:00:00 2001
From: gnaegi <none@none>
Date: Mon, 13 Feb 2017 15:31:48 +0100
Subject: [PATCH] OO-2533 implement better filter for meta files for macOS,
 centralized code and unit test

---
 .../admin/layout/LayoutAdminController.java   |  8 ++--
 .../FolderNotificationsHandler.java           |  5 +--
 .../java/org/olat/core/util/FileUtils.java    | 29 ++++++++++++-
 .../util/vfs/filters/VFSItemMetaFilter.java   | 43 +++++++++++++++++++
 .../ui/courselayout/CourseLayoutHelper.java   |  6 +--
 .../formatters/ForumOpenXMLFormatter.java     |  5 +--
 .../ForumArtefactDetailsController.java       |  5 +--
 .../modules/fo/ui/MessageEditController.java  |  7 ++-
 .../modules/fo/ui/MessageListController.java  |  4 +-
 .../org/olat/core/util/FileUtilsTest.java     | 21 +++++++++
 10 files changed, 109 insertions(+), 24 deletions(-)
 create mode 100644 src/main/java/org/olat/core/util/vfs/filters/VFSItemMetaFilter.java

diff --git a/src/main/java/org/olat/admin/layout/LayoutAdminController.java b/src/main/java/org/olat/admin/layout/LayoutAdminController.java
index 347fba5aa7f..ce8b2c9f35a 100644
--- a/src/main/java/org/olat/admin/layout/LayoutAdminController.java
+++ b/src/main/java/org/olat/admin/layout/LayoutAdminController.java
@@ -45,6 +45,7 @@ import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.helpers.GUISettings;
 import org.olat.core.helpers.Settings;
+import org.olat.core.util.FileUtils;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.Util;
 import org.olat.core.util.WebappHelper;
@@ -318,10 +319,9 @@ public class LayoutAdminController extends FormBasicController {
 					return false;
 				}
 				// remove unwanted meta-dirs
-				if (name.equalsIgnoreCase("CVS")) return false;
-				if (name.equalsIgnoreCase(".DS_Store")) return false;
-				if (name.equalsIgnoreCase(".sass-cache")) return false;
-				if (name.equalsIgnoreCase(".hg")) return false;
+				if (FileUtils.isMetaFilename(name)) {
+					return false;
+				}
 				return true;
 		}
 	}
diff --git a/src/main/java/org/olat/core/commons/modules/bc/notifications/FolderNotificationsHandler.java b/src/main/java/org/olat/core/commons/modules/bc/notifications/FolderNotificationsHandler.java
index 61f9cdd0ccf..ab718ce7f56 100644
--- a/src/main/java/org/olat/core/commons/modules/bc/notifications/FolderNotificationsHandler.java
+++ b/src/main/java/org/olat/core/commons/modules/bc/notifications/FolderNotificationsHandler.java
@@ -26,7 +26,6 @@
 
 package org.olat.core.commons.modules.bc.notifications;
 
-import java.util.Arrays;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
@@ -51,6 +50,7 @@ import org.olat.core.id.Identity;
 import org.olat.core.id.context.BusinessControlFactory;
 import org.olat.core.logging.OLog;
 import org.olat.core.logging.Tracing;
+import org.olat.core.util.FileUtils;
 import org.olat.core.util.Util;
 import org.olat.core.util.resource.OresHelper;
 import org.olat.group.BusinessGroup;
@@ -69,7 +69,6 @@ import org.olat.repository.RepositoryManager;
  */
 public class FolderNotificationsHandler implements NotificationsHandler {
 	private static final OLog log = Tracing.createLoggerFor(FolderNotificationsHandler.class);
-	public static final List<String> EXCLUDE_PREFIXES = Arrays.asList(".DS_Store",".CVS",".nfs",".sass-cache",".hg");
 	
 	/**
 	 * 
@@ -113,7 +112,7 @@ public class FolderNotificationsHandler implements NotificationsHandler {
 					// don't show changes in meta-directories. first quick check
 					// for any dot files and then compare with our black list of
 					// known exclude prefixes
-					if (title != null && title.indexOf("/.") != -1 && EXCLUDE_PREFIXES.parallelStream().anyMatch(title::contains)) {
+					if (title != null && title.indexOf("/.") != -1 && FileUtils.isMetaFilename(title)) {
 						// skip this file, continue with next item in folder
 						continue;
 					}						
diff --git a/src/main/java/org/olat/core/util/FileUtils.java b/src/main/java/org/olat/core/util/FileUtils.java
index 3ed9394e828..b212417d4f8 100644
--- a/src/main/java/org/olat/core/util/FileUtils.java
+++ b/src/main/java/org/olat/core/util/FileUtils.java
@@ -46,6 +46,7 @@ import java.nio.file.attribute.BasicFileAttributes;
 import java.text.Normalizer;
 import java.util.Arrays;
 import java.util.Iterator;
+import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -78,7 +79,8 @@ public class FileUtils {
 	public static char[] FILE_NAME_FORBIDDEN_CHARS = { '/', '\n', '\r', '\t', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':', ',' };
   //private static char[] FILE_NAME_ACCEPTED_CHARS = { 'ä', 'Ä', 'ü', 'Ü', 'ö', 'Ö', ' '};
 	public static char[] FILE_NAME_ACCEPTED_CHARS = { '\u0228', '\u0196', '\u0252', '\u0220', '\u0246', '\u0214', ' '};
-
+	// known metadata files
+	public static final List<String> META_FILENAMES = Arrays.asList(".DS_Store",".CVS",".nfs",".sass-cache",".hg");
 
 	/**
 	 * @param sourceFile
@@ -1010,6 +1012,31 @@ public class FileUtils {
 		}
 	}
 	
+	/**
+	 * Check if the given filename is a metadata filename generated by macOS or
+	 * windows when browsing a directory or generated by one of the known
+	 * repository systems.
+	 * 
+	 * @param filename
+	 * @return
+	 */
+	public static boolean isMetaFilename(String filename) {
+		boolean isMeta = false;
+		if (filename != null) {
+			// 1) check for various known filenames 
+			isMeta = META_FILENAMES.parallelStream().anyMatch(filename::contains);
+			if (!isMeta) {
+				// 2) macOS meta files generated with WebDAV starts with ._
+				isMeta = filename.startsWith("._");
+			}
+			
+		}
+		return isMeta;
+	}
+	
+
+	
+	
 	public static String rename(File f) {
 		String filename = f.getName();
 		String newName = filename;
diff --git a/src/main/java/org/olat/core/util/vfs/filters/VFSItemMetaFilter.java b/src/main/java/org/olat/core/util/vfs/filters/VFSItemMetaFilter.java
new file mode 100644
index 00000000000..f8ba823fccf
--- /dev/null
+++ b/src/main/java/org/olat/core/util/vfs/filters/VFSItemMetaFilter.java
@@ -0,0 +1,43 @@
+/**
+ * <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.util.vfs.filters;
+
+import org.olat.core.util.FileUtils;
+import org.olat.core.util.vfs.VFSItem;
+
+/**
+ * Initial date: 13.02.2017<br>
+ * @author gnaegi, gnaegi@frentix.com, http://www.frentix.com
+ *
+ * Check if the given filename is a metadata filename generated by macOS or
+ * windows when browsing a directory or generated by one of the known
+ * repository systems.
+ */
+public class VFSItemMetaFilter implements VFSItemFilter {
+
+	@Override
+	public boolean accept(VFSItem vfsItem) {
+		if (vfsItem == null) {
+			return false;
+		}
+		return !FileUtils.isMetaFilename(vfsItem.getName());
+	}
+
+}
diff --git a/src/main/java/org/olat/course/config/ui/courselayout/CourseLayoutHelper.java b/src/main/java/org/olat/course/config/ui/courselayout/CourseLayoutHelper.java
index 2a8a76c27d3..cf5f7f46834 100644
--- a/src/main/java/org/olat/course/config/ui/courselayout/CourseLayoutHelper.java
+++ b/src/main/java/org/olat/course/config/ui/courselayout/CourseLayoutHelper.java
@@ -29,6 +29,7 @@ import org.olat.core.commons.services.image.ImageService;
 import org.olat.core.gui.components.htmlheader.jscss.CustomCSS;
 import org.olat.core.helpers.GUISettings;
 import org.olat.core.helpers.Settings;
+import org.olat.core.util.FileUtils;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.UserSession;
 import org.olat.core.util.WebappHelper;
@@ -70,10 +71,7 @@ public class CourseLayoutHelper {
 		public boolean accept(VFSItem it) {
 			if (!(it instanceof VFSContainer)) return false;
 			// remove unwanted meta-dirs
-			else if (it.getName().equalsIgnoreCase("CVS")) return false;
-			else if (it.getName().equalsIgnoreCase(".DS_Store")) return false;
-			else if (it.getName().equalsIgnoreCase(".sass-cache")) return false;
-			else if (it.getName().equalsIgnoreCase(".hg")) return false;
+			else if (FileUtils.isMetaFilename(it.getName())) return false;
 			// last check is blacklist
 			return !(layoutBlacklist.contains(it.getName()));
 		}
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 d89aff44d36..31189da9291 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
@@ -40,9 +40,8 @@ import org.olat.core.util.openxml.OpenXMLDocument.Style;
 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.VFSItemExcludePrefixFilter;
+import org.olat.core.util.vfs.filters.VFSItemMetaFilter;
 import org.olat.modules.fo.archiver.MessageNode;
-import org.olat.modules.fo.ui.MessageEditController;
 
 /**
  * 
@@ -52,7 +51,7 @@ import org.olat.modules.fo.ui.MessageEditController;
  */
 public class ForumOpenXMLFormatter extends ForumFormatter {
 	
-	private final VFSItemExcludePrefixFilter filter = new VFSItemExcludePrefixFilter(MessageEditController.ATTACHMENT_EXCLUDE_PREFIXES);
+	private final VFSItemMetaFilter filter = new VFSItemMetaFilter();
 
 	private boolean firstThread = true;
 	
diff --git a/src/main/java/org/olat/modules/fo/portfolio/ForumArtefactDetailsController.java b/src/main/java/org/olat/modules/fo/portfolio/ForumArtefactDetailsController.java
index 8cdeadc198b..06d75311145 100644
--- a/src/main/java/org/olat/modules/fo/portfolio/ForumArtefactDetailsController.java
+++ b/src/main/java/org/olat/modules/fo/portfolio/ForumArtefactDetailsController.java
@@ -34,7 +34,7 @@ import org.olat.core.gui.util.CSSHelper;
 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.filters.VFSItemExcludePrefixFilter;
+import org.olat.core.util.vfs.filters.VFSItemMetaFilter;
 import org.olat.portfolio.manager.EPFrontendManager;
 import org.olat.portfolio.model.artefacts.AbstractArtefact;
 
@@ -50,7 +50,6 @@ import org.olat.portfolio.model.artefacts.AbstractArtefact;
 public class ForumArtefactDetailsController extends BasicController {
 
 	private final VelocityContainer vC;
-	protected static final String[] ATTACHMENT_EXCLUDE_PREFIXES = new String[]{".nfs", ".CVS", ".DS_Store"}; // see: MessageEditController.ATTACHMENT_EXCLUDE_PREFIXES
 
 	public ForumArtefactDetailsController(UserRequest ureq, WindowControl wControl, AbstractArtefact artefact) {
 		super(ureq, wControl);
@@ -60,7 +59,7 @@ public class ForumArtefactDetailsController extends BasicController {
 		vC.contextPut("text", ePFMgr.getArtefactFullTextContent(fArtefact));
 		VFSContainer artContainer = ePFMgr.getArtefactContainer(artefact);
 		if (artContainer!=null && artContainer.getItems().size()!=0){
-			List<VFSItem> attachments = new ArrayList<VFSItem>(artContainer.getItems(new VFSItemExcludePrefixFilter(ATTACHMENT_EXCLUDE_PREFIXES)));
+			List<VFSItem> attachments = new ArrayList<VFSItem>(artContainer.getItems(new VFSItemMetaFilter()));
 			int i=1; //vc-shift!
 			for (VFSItem vfsItem : attachments) {
 				VFSLeaf file = (VFSLeaf) vfsItem;
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 b624ee1f563..2337cb57929 100644
--- a/src/main/java/org/olat/modules/fo/ui/MessageEditController.java
+++ b/src/main/java/org/olat/modules/fo/ui/MessageEditController.java
@@ -66,7 +66,7 @@ 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.filters.VFSItemExcludePrefixFilter;
+import org.olat.core.util.vfs.filters.VFSItemMetaFilter;
 import org.olat.modules.fo.Forum;
 import org.olat.modules.fo.ForumCallback;
 import org.olat.modules.fo.ForumChangedEvent;
@@ -99,7 +99,6 @@ public class MessageEditController extends FormBasicController {
 	// attached files anywhere at the time of deleting it
 	// likely to be resolved after user logs out, caches get cleared - and if not the server
 	// restart overnight definitely removes those .nfs files.
-	public static final String[] ATTACHMENT_EXCLUDE_PREFIXES = new String[]{".nfs", ".CVS", ".DS_Store"};
 	private static final String[] enableKeys = new String[]{ "on" };
 	
 	private RichTextElement bodyEl;
@@ -114,7 +113,7 @@ public class MessageEditController extends FormBasicController {
 	private VFSContainer tempUploadFolder;
 	private boolean userIsMsgCreator;
 	private boolean msgHasChildren;
-	private VFSItemExcludePrefixFilter exclFilter;
+	private VFSItemMetaFilter exclFilter;
 
 	private final Forum forum;
 	private final EditMode editMode;
@@ -157,7 +156,7 @@ public class MessageEditController extends FormBasicController {
 		this.guestOnly = ureq.getUserSession().getRoles().isGuestOnly();
 
 		tempUploadFolder = new LocalFolderImpl(new File(WebappHelper.getTmpDir(), CodeHelper.getUniqueID()));
-		exclFilter = new VFSItemExcludePrefixFilter(ATTACHMENT_EXCLUDE_PREFIXES);
+		exclFilter = new VFSItemMetaFilter();
 		
 		initForm(ureq);
 	}
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 18db1bf3298..5e3b3395efb 100644
--- a/src/main/java/org/olat/modules/fo/ui/MessageListController.java
+++ b/src/main/java/org/olat/modules/fo/ui/MessageListController.java
@@ -73,7 +73,7 @@ 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.VFSItemExcludePrefixFilter;
+import org.olat.core.util.vfs.filters.VFSItemMetaFilter;
 import org.olat.modules.fo.Forum;
 import org.olat.modules.fo.ForumCallback;
 import org.olat.modules.fo.ForumChangedEvent;
@@ -593,7 +593,7 @@ public class MessageListController extends BasicController implements GenericEve
 		// message attachments
 		VFSContainer msgContainer = forumManager.getMessageContainer(forum.getKey(), m.getKey());
 		messageView.setMessageContainer(msgContainer);
-		List<VFSItem> attachments = new ArrayList<VFSItem>(msgContainer.getItems(new VFSItemExcludePrefixFilter(MessageEditController.ATTACHMENT_EXCLUDE_PREFIXES)));				
+		List<VFSItem> attachments = new ArrayList<VFSItem>(msgContainer.getItems(new VFSItemMetaFilter()));				
 		messageView.setAttachments(attachments);
 
 		// number of children and modify/delete permissions
diff --git a/src/test/java/org/olat/core/util/FileUtilsTest.java b/src/test/java/org/olat/core/util/FileUtilsTest.java
index 435fe60afd2..2c193d28265 100644
--- a/src/test/java/org/olat/core/util/FileUtilsTest.java
+++ b/src/test/java/org/olat/core/util/FileUtilsTest.java
@@ -46,4 +46,25 @@ public class FileUtilsTest {
 		String normalized = FileUtils.normalizeFilename(smorrebrod);
 		Assert.assertEquals(normalized, "Smorrebrod");
 	}
+
+
+	@Test
+	public void testMetaFiles() {
+		Assert.assertFalse(FileUtils.isMetaFilename(null));
+		Assert.assertFalse(FileUtils.isMetaFilename(""));
+		Assert.assertFalse(FileUtils.isMetaFilename("gugus"));
+		Assert.assertFalse(FileUtils.isMetaFilename(".Jüdelidü"));
+		Assert.assertFalse(FileUtils.isMetaFilename("./dings"));
+		
+		Assert.assertTrue(FileUtils.isMetaFilename(".DS_Store"));
+		Assert.assertTrue(FileUtils.isMetaFilename(".CVS"));
+		Assert.assertTrue(FileUtils.isMetaFilename(".nfs"));
+		Assert.assertTrue(FileUtils.isMetaFilename(".sass-cache"));
+		Assert.assertTrue(FileUtils.isMetaFilename(".hg"));
+
+		Assert.assertTrue(FileUtils.isMetaFilename("._"));
+		Assert.assertTrue(FileUtils.isMetaFilename("._gugus"));
+
+	}
+
 }
-- 
GitLab