Merged HEAD-BUG-FIX (5.0/Cloud) to HEAD (5.0/Cloud)

84028: Merged V4.2-BUG-FIX (4.2.4) to HEAD-BUG-FIX (5.0/Cloud)
      83341: Merged DEV to V4.2-BUG-FIX (4.2.4)
         82263: MNT-12259 : Outlook 2013 implements an IMAP move as a \Deleted + APPEND leading to corrupt files
         Implemented a complex move operation to support Outlook 2013.
         82465: MNT-12259 : Outlook 2013 implements an IMAP move as a \Deleted + APPEND leading to misleading copy of alfresco dummy file
         Modified the fix to use the message id to determine the complex move operation.
         Added JUnit test.
      83420: MNT-12259 : Outlook 2013 implements an IMAP move as a \Deleted + APPEND leading to misleading copy of alfresco dummy file
      Added an addition check for node existence to correctly handle invalid nodeRefs in message id.


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@84619 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Alan Davis
2014-09-18 17:20:55 +00:00
parent 49e29b3472
commit 3d61b9af7a
3 changed files with 229 additions and 5 deletions

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2005-2011 Alfresco Software Limited. * Copyright (C) 2005-2014 Alfresco Software Limited.
* *
* This file is part of Alfresco * This file is part of Alfresco
* *
@@ -43,8 +43,12 @@ import org.alfresco.service.cmr.model.FileExistsException;
import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException; import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.util.FileFilterMode;
import org.alfresco.util.GUID; import org.alfresco.util.GUID;
import org.alfresco.util.Utf7; import org.alfresco.util.Utf7;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@@ -309,12 +313,116 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab
Date internalDate) Date internalDate)
throws FileExistsException, FileNotFoundException, IOException, MessagingException throws FileExistsException, FileNotFoundException, IOException, MessagingException
{ {
long uid = createMimeMessageInFolder(this.folderInfo, message, flags); long uid;
NodeRef sourceNodeRef = extractNodeRef(message);
if (isMoveOperation(sourceNodeRef))
{
uid = moveNode(this.folderInfo, message, flags, sourceNodeRef);
}
else
{
uid = createMimeMessageInFolder(this.folderInfo, message, flags);
}
// Invalidate current folder status // Invalidate current folder status
this.folderStatus = null; this.folderStatus = null;
return uid; return uid;
} }
/**
* Moves the node <code>sourceNodeRef</code> extracted from the message id.
* A part of a complex move operation.
*
* @param folderInfo
* @param message
* @param flags
* @param sourceNodeRef
* @return UUID of the moved node
* @throws FileExistsException
* @throws FileNotFoundException
*/
@SuppressWarnings("deprecation")
private long moveNode(FileInfo folderInfo, MimeMessage message, Flags flags, NodeRef sourceNodeRef)
throws FileExistsException, FileNotFoundException
{
FileFolderService fileFolderService = serviceRegistry.getFileFolderService();
FileFilterMode.setClient(FileFilterMode.Client.imap);
fileFolderService.setHidden(sourceNodeRef, false);
FileInfo messageFile = fileFolderService.move(sourceNodeRef, folderInfo.getNodeRef(), null);
final long newMessageUid = (Long) messageFile.getProperties().get(ContentModel.PROP_NODE_DBID);
imapService.setFlag(messageFile, Flag.RECENT, true);
imapService.setFlag(messageFile, Flag.DELETED, false);
return newMessageUid;
}
/**
* Extract a <code>NodeRef</code> from the message id.
* <br>Typical message id is "<74bad8aa-75a5-4063-8e46-9d1b5737f43b@alfresco.org>"
* <br>See {@link AbstractMimeMessage#updateMessageID()}
*
* @param message
* @return null if nothing is found
*/
private NodeRef extractNodeRef(MimeMessage message)
{
String uuid = null;
String messageId = null;
NodeRef result = null;
try
{
messageId = message.getMessageID();
}
catch (MessagingException me)
{
// we cannot use message id to extract nodeRef
}
if (messageId != null)
{
if (messageId.startsWith("<"))
{
messageId = messageId.substring(1);
}
if (messageId.indexOf("@") != -1)
{
uuid = messageId.substring(0, messageId.indexOf("@"));
}
else
{
uuid = messageId;
}
result = new NodeRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore", uuid);
}
return result;
}
/**
* Determine if it is a complex move operation, which consists of a create superseded by a delete.
*
* @param sourceNodeRef
* @return
*/
@SuppressWarnings("deprecation")
private boolean isMoveOperation(NodeRef sourceNodeRef)
{
if (sourceNodeRef != null)
{
NodeService nodeService = serviceRegistry.getNodeService();
if (nodeService.exists(sourceNodeRef))
{
FileFolderService fileFolderService = serviceRegistry.getFileFolderService();
FileInfo node = fileFolderService.getFileInfo(sourceNodeRef);
if (node != null)
{
if (fileFolderService.isHidden(sourceNodeRef))
{
return true;
}
}
}
}
return false;
}
/** /**
* Copies message with the given UID to the specified {@link MailFolder}. * Copies message with the given UID to the specified {@link MailFolder}.
* *

View File

@@ -34,6 +34,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NavigableMap; import java.util.NavigableMap;
import java.util.Set; import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -158,6 +160,8 @@ public class ImapServiceImpl implements ImapService, OnRestoreNodePolicy, OnCrea
private final static Map<QName, Flags.Flag> qNameToFlag; private final static Map<QName, Flags.Flag> qNameToFlag;
private final static Map<Flags.Flag, QName> flagToQname; private final static Map<Flags.Flag, QName> flagToQname;
private static final Timer deleteDelayTimer = new Timer();
private boolean imapServerEnabled = false; private boolean imapServerEnabled = false;
static static
@@ -538,11 +542,71 @@ public class ImapServiceImpl implements ImapService, OnRestoreNodePolicy, OnCrea
Flags flags = getFlags(fileInfo); Flags flags = getFlags(fileInfo);
if (flags.contains(Flags.Flag.DELETED)) if (flags.contains(Flags.Flag.DELETED))
{ {
fileFolderService.delete(fileInfo.getNodeRef()); // See MNT-12259
//fileFolderService.delete(fileInfo.getNodeRef());
hideAndDelete(fileInfo.getNodeRef());
messageCache.remove(fileInfo.getNodeRef()); messageCache.remove(fileInfo.getNodeRef());
} }
} }
/**
* Workaround for MNT-12259
* @param nodeRef
*/
@SuppressWarnings("deprecation")
private void hideAndDelete(final NodeRef nodeRef)
{
FileFilterMode.setClient(FileFilterMode.Client.imap);
fileFolderService.setHidden(nodeRef, true);
{
// Get the current user
final String deleteDelayUser = AuthenticationUtil.getFullyAuthenticatedUser();
// Add a timed task to really delete the file
TimerTask deleteDelayTask = new TimerTask()
{
@Override
public void run()
{
RunAsWork<Void> deleteDelayRunAs = new RunAsWork<Void>()
{
@Override
public Void doWork() throws Exception
{
// Ignore if it is NOT hidden: the shuffle may have finished; the operation may have failed
if (!nodeService.exists(nodeRef) || !fileFolderService.isHidden(nodeRef))
{
return null;
}
// Since this will run in a different thread, the client thread-local must be set
// or else unhiding the node will not unhide it for IMAP.
FileFilterMode.setClient(FileFilterMode.Client.imap);
// Unhide the node, e.g. for archiving
fileFolderService.setHidden(nodeRef, false);
// This is the transaction-aware service
fileFolderService.delete(nodeRef);
return null;
}
};
try
{
AuthenticationUtil.runAs(deleteDelayRunAs, deleteDelayUser);
}
catch (Throwable e)
{
// consume exception to avoid it leaking from the TimerTask and causing the Timer to
// no longer accept tasks to be scheduled.
logger.info("Exception thrown during IMAP delete timer task.", e);
}
}
};
// Schedule a real delete 5 seconds after the current time
deleteDelayTimer.schedule(deleteDelayTask, 5000L);
}
}
public AlfrescoImapFolder getOrCreateMailbox(AlfrescoImapUser user, String mailboxName, boolean mayExist, boolean mayCreate) public AlfrescoImapFolder getOrCreateMailbox(AlfrescoImapUser user, String mailboxName, boolean mayExist, boolean mayCreate)
{ {
if (mailboxName == null) if (mailboxName == null)

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2005-2013 Alfresco Software Limited. * Copyright (C) 2005-2014 Alfresco Software Limited.
* *
* This file is part of Alfresco * This file is part of Alfresco
* *
@@ -54,6 +54,7 @@ import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException; import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.AssociationRef; import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.NodeService;
@@ -69,12 +70,15 @@ import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService; import org.alfresco.service.transaction.TransactionService;
import org.alfresco.test_category.OwnJVMTestsCategory; import org.alfresco.test_category.OwnJVMTestsCategory;
import org.alfresco.util.ApplicationContextHelper; import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.GUID;
import org.alfresco.util.PropertyMap; import org.alfresco.util.PropertyMap;
import org.alfresco.util.config.RepositoryFolderConfigBean; import org.alfresco.util.config.RepositoryFolderConfigBean;
import org.junit.experimental.categories.Category; import org.junit.experimental.categories.Category;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import com.icegreen.greenmail.store.SimpleStoredMessage;
/** /**
* Unit test for ImapServiceImpl * Unit test for ImapServiceImpl
*/ */
@@ -107,6 +111,7 @@ public class ImapServiceImplTest extends TestCase
private AlfrescoImapUser user; private AlfrescoImapUser user;
private ImapService imapService; private ImapService imapService;
private UserTransaction txn; private UserTransaction txn;
private ContentService contentService;
private NodeRef testImapFolderNodeRef; private NodeRef testImapFolderNodeRef;
private Flags flags; private Flags flags;
@@ -128,7 +133,7 @@ public class ImapServiceImplTest extends TestCase
searchService = serviceRegistry.getSearchService(); searchService = serviceRegistry.getSearchService();
namespaceService = serviceRegistry.getNamespaceService(); namespaceService = serviceRegistry.getNamespaceService();
fileFolderService = serviceRegistry.getFileFolderService(); fileFolderService = serviceRegistry.getFileFolderService();
contentService = serviceRegistry.getContentService();
flags = new Flags(); flags = new Flags();
flags.add(Flags.Flag.SEEN); flags.add(Flags.Flag.SEEN);
@@ -869,6 +874,53 @@ public class ImapServiceImplTest extends TestCase
assertPathHierarchy(fullPathList, pathAfterRenaming); assertPathHierarchy(fullPathList, pathAfterRenaming);
} }
/**
* Test for MNT-12259
* There is a 5s gap to run the test, see {@link ImapServiceImpl#hideAndDelete}
*
* @throws Exception
*/
public void testMoveViaDeleteAndAppend() throws Exception
{
AlfrescoImapUser poweredUser = new AlfrescoImapUser((USER_NAME + "@alfresco.com"), USER_NAME, USER_PASSWORD);
String fileName = "testfile" + GUID.generate();
String destinationName = "testFolder" + GUID.generate();
String destinationPath = IMAP_ROOT + AlfrescoImapConst.HIERARCHY_DELIMITER + destinationName;
String nodeContent = "test content";
NodeRef root = findCompanyHomeNodeRef();
AuthenticationUtil.setAdminUserAsFullyAuthenticatedUser();
// Create node and destination folder
FileInfo origFile = fileFolderService.create(root, fileName, ContentModel.TYPE_CONTENT);
ContentWriter contentWriter = contentService.getWriter(origFile.getNodeRef(), ContentModel.PROP_CONTENT, true);
contentWriter.setMimetype("text/plain");
contentWriter.setEncoding("UTF-8");
contentWriter.putContent(nodeContent);
FileInfo destinationNode = fileFolderService.create(root, destinationName, ContentModel.TYPE_FOLDER);
nodeService.addAspect(origFile.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT, null);
// Save the message and ensure the message id is set
SimpleStoredMessage origMessage = imapService.getMessage(origFile);
origMessage.getMimeMessage().saveChanges();
imapService.setFlag(origFile, Flags.Flag.DELETED, true);
// Delete the node
imapService.expungeMessage(origFile);
// Append the message to destination
AlfrescoImapFolder destinationMailbox = imapService.getOrCreateMailbox(poweredUser, destinationPath, true, false);
destinationMailbox.appendMessage(origMessage.getMimeMessage(), flags, null);
// Check the destination has the original file and only this file
FileInfo movedNode = fileFolderService.getFileInfo(origFile.getNodeRef());
assertNotNull("The file should exist.", movedNode);
assertEquals("The file name should not change.", fileName, movedNode.getName());
NodeRef newParentNodeRef = nodeService.getPrimaryParent(origFile.getNodeRef()).getParentRef();
assertEquals("The parent should change to destination.", destinationNode.getNodeRef(), newParentNodeRef);
assertEquals("There should be only one node in the destination folder", 1, nodeService.getChildAssocs(destinationNode.getNodeRef()).size());
}
/** /**
* @param mailbox - {@link AlfrescoImapFolder} instance which should be checked * @param mailbox - {@link AlfrescoImapFolder} instance which should be checked
*/ */