diff --git a/config/alfresco/model/permissionDefinitions.xml b/config/alfresco/model/permissionDefinitions.xml index b6e956cba3..72e0226d8a 100644 --- a/config/alfresco/model/permissionDefinitions.xml +++ b/config/alfresco/model/permissionDefinitions.xml @@ -406,49 +406,49 @@ - --> + - --> + - --> + - --> + - --> + - --> + - --> + diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index 28f3553a6f..c48ef6c53f 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -22,6 +22,9 @@ alfresco/model/permissionDefinitions.xml + + alfresco/model/permissionSchema.dtd + diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index ec303d08db..74e86dfc93 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -350,7 +350,7 @@ audit.config.strict=false # Audit map filter for AccessAuditor - restricts recorded events to user driven events audit.filter.alfresco-access.default.enabled=true audit.filter.alfresco-access.transaction.user=~System;~null;.* -audit.filter.alfresco-access.transaction.type=cm:folder;cm:content +audit.filter.alfresco-access.transaction.type=cm:folder;cm:content;st:site audit.filter.alfresco-access.transaction.path=~/sys:archivedItem;~/ver:;.* diff --git a/source/java/org/alfresco/repo/audit/access/AccessAuditor.java b/source/java/org/alfresco/repo/audit/access/AccessAuditor.java index c08d255e82..29df9d982e 100644 --- a/source/java/org/alfresco/repo/audit/access/AccessAuditor.java +++ b/source/java/org/alfresco/repo/audit/access/AccessAuditor.java @@ -79,13 +79,13 @@ import org.springframework.beans.factory.InitializingBean; * * The following properties are set by default to discard events where the user is * 'null' or 'System', the node path is '/sys:archivedItem' or under '/ver:' or - * the node type is not 'cm:folder' or 'cm:content'. These values result in events - * only being recorded if they are initiated by users of the system. These vales may - * be overridden if required. + * the node type is not 'cm:folder', 'cm:content' or 'st:site'. These values result + * in events only being recorded for common actions initiated by users of the system. + * These vales may be overridden if required. *
  *    audit.filter.alfresco-access.default.enabled=true
  *    audit.filter.alfresco-access.transaction.user=~System;~null;.*
- *    audit.filter.alfresco-access.transaction.type=cm:folder;cm:content
+ *    audit.filter.alfresco-access.transaction.type=cm:folder;cm:content;st:site
  *    audit.filter.alfresco-access.transaction.path=~/sys:archivedItem;~/ver:;.*
  * 
* diff --git a/source/java/org/alfresco/repo/content/filestore/DeletionMetricsRunner.java b/source/java/org/alfresco/repo/content/filestore/DeletionMetricsRunner.java new file mode 100644 index 0000000000..248db90e24 --- /dev/null +++ b/source/java/org/alfresco/repo/content/filestore/DeletionMetricsRunner.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.repo.content.filestore; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.ContentStore; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.cleanup.ContentStoreCleaner; +import org.alfresco.repo.content.cleanup.ContentStoreCleanerListener; +import org.alfresco.repo.content.cleanup.EagerContentStoreCleaner; +import org.alfresco.repo.domain.avm.AVMNodeDAO; +import org.alfresco.repo.domain.contentdata.ContentDataDAO; +import org.alfresco.repo.lock.JobLockService; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +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.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.GUID; +import org.springframework.context.ApplicationContext; + +public class DeletionMetricsRunner +{ + private ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + private ContentService contentService; + private NodeService nodeService; + private TransactionService transactionService; + private JobLockService jobLockService; + private ContentStoreCleaner cleaner; + private EagerContentStoreCleaner eagerCleaner; + private FileContentStore store; + private ContentStoreCleanerListener listener; + private int deletedUrls; + + private final int numOrphans = 1000; + + + + public DeletionMetricsRunner() + { + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean("ServiceRegistry"); + contentService = serviceRegistry.getContentService(); + nodeService = serviceRegistry.getNodeService(); + transactionService = serviceRegistry.getTransactionService(); + jobLockService = serviceRegistry.getJobLockService(); + TransactionService transactionService = serviceRegistry.getTransactionService(); + DictionaryService dictionaryService = serviceRegistry.getDictionaryService(); + AVMNodeDAO avmNodeDAO = (AVMNodeDAO) ctx.getBean("newAvmNodeDAO"); + ContentDataDAO contentDataDAO = (ContentDataDAO) ctx.getBean("contentDataDAO"); + + // we need a store + store = (FileContentStore) ctx.getBean("fileContentStore"); + + + // and a listener + List listeners = new ArrayList(2); + listener = new CleanerListener(); + listeners.add(listener); + + // Construct the test cleaners + eagerCleaner = (EagerContentStoreCleaner) ctx.getBean("eagerContentStoreCleaner"); + eagerCleaner.setEagerOrphanCleanup(false); + eagerCleaner.setStores(Collections.singletonList((ContentStore) store)); + eagerCleaner.setListeners(listeners); + + cleaner = new ContentStoreCleaner(); + cleaner.setEagerContentStoreCleaner(eagerCleaner); + cleaner.setJobLockService(jobLockService); + cleaner.setContentDataDAO(contentDataDAO); + cleaner.setTransactionService(transactionService); + cleaner.setDictionaryService(dictionaryService); + cleaner.setContentService(contentService); + cleaner.setAvmNodeDAO(avmNodeDAO); + } + + + public static void main(String[] args) + { + DeletionMetricsRunner metrics = new DeletionMetricsRunner(); + metrics.run(); + } + + + public void run() + { + setUp(true); + time("Deleting empty parent dirs"); + tearDown(); + + setUp(false); + time("Ignoring empty parent dirs"); + tearDown(); + } + + + + private void setUp(boolean deleteEmptyDirs) + { + AuthenticationUtil.setRunAsUserSystem(); + store.setDeleteEmptyDirs(deleteEmptyDirs); + deletedUrls = 0; + } + + + private void tearDown() + { + AuthenticationUtil.clearCurrentSecurityContext(); + System.out.println("Deleted " + deletedUrls + " URLs."); + } + + + + private void time(String description) + { + long beforeClean = System.currentTimeMillis(); + + createContent(); + cleanContent(); + + long afterClean = System.currentTimeMillis(); + double timeTaken = afterClean - beforeClean; + System.out.println(); + System.out.println(String.format("%s took %6.0fms", description, timeTaken)); + } + + + private void createContent() + { + final StoreRef storeRef = nodeService.createStore("test", "timings-" + GUID.generate()); + RetryingTransactionCallback testCallback = new RetryingTransactionCallback() + { + public ContentData execute() throws Throwable + { + ContentData contentData = null; + + for (int i = 0; i < numOrphans; i++) + { + // Create some content + NodeRef rootNodeRef = nodeService.getRootNode(storeRef); + Map properties = new HashMap(13); + properties.put(ContentModel.PROP_NAME, (Serializable)"test.txt"); + + NodeRef contentNodeRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTENT, + properties).getChildRef(); + + ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + + + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.putContent("INITIAL CONTENT"); + + contentData = writer.getContentData(); + + // Delete the first node, bypassing archive + nodeService.addAspect(contentNodeRef, ContentModel.ASPECT_TEMPORARY, null); + nodeService.deleteNode(contentNodeRef); + } + + // Done + return contentData; + } + }; + transactionService.getRetryingTransactionHelper().doInTransaction(testCallback); + } + + + private void cleanContent() + { + // fire the cleaner + cleaner.setProtectDays(0); + cleaner.execute(); + + if (deletedUrls < numOrphans) + throw new IllegalStateException("Not all the orphans were cleaned."); + } + + + private class CleanerListener implements ContentStoreCleanerListener + { + public void beforeDelete(ContentStore store, String contentUrl) throws ContentIOException + { + deletedUrls++; + } + } + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentStore.java b/source/java/org/alfresco/repo/content/filestore/FileContentStore.java index 14be69fb98..0e79ea19c4 100644 --- a/source/java/org/alfresco/repo/content/filestore/FileContentStore.java +++ b/source/java/org/alfresco/repo/content/filestore/FileContentStore.java @@ -71,6 +71,7 @@ public class FileContentStore private boolean allowRandomAccess; private boolean readOnly; private ApplicationContext applicationContext; + private boolean deleteEmptyDirs = true; /** * Private: for Spring-constructed instances only. @@ -608,6 +609,12 @@ public class FileContentStore { deleted = file.delete(); } + + // Delete empty parents regardless of whether the file was ignore above. + if (deleteEmptyDirs && deleted) + { + deleteEmptyParents(file); + } // done if (logger.isDebugEnabled()) @@ -619,6 +626,39 @@ public class FileContentStore return deleted; } + /** + * Deletes the parents of the specified file. The file itself must have been + * deleted before calling this method - since only empty directories can be deleted. + * + * @param file + */ + private void deleteEmptyParents(File file) + { + String root = getRootLocation(); + File parent = file.getParentFile(); + boolean deleted = false; + do + { + try + { + if (parent.isDirectory() && !parent.getCanonicalPath().equals(root)) + { + // Only an empty directory will successfully be deleted. + deleted = parent.delete(); + } + } + catch (IOException error) + { + logger.error("Unable to construct canonical path for " + parent.getAbsolutePath()); + break; + } + + parent = parent.getParentFile(); + } + while(deleted); + + } + /** * Creates a new content URL. This must be supported by all * stores that are compatible with Alfresco. @@ -669,4 +709,14 @@ public class FileContentStore publishEvent(((ContextRefreshedEvent) event).getApplicationContext()); } } + + /** + * Configure the FileContentStore to delete empty parent directories upon deleting a content URL. + * + * @param deleteEmptyDirs the deleteEmptyDirs to set + */ + public void setDeleteEmptyDirs(boolean deleteEmptyDirs) + { + this.deleteEmptyDirs = deleteEmptyDirs; + } } diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java b/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java index b517f0654f..1ad22c7bde 100644 --- a/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java +++ b/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java @@ -50,6 +50,8 @@ public class FileContentStoreTest extends AbstractWritableContentStoreTest tempDir.getAbsolutePath() + File.separatorChar + getName()); + + store.setDeleteEmptyDirs(true); } @Override @@ -128,4 +130,101 @@ public class FileContentStoreTest extends AbstractWritableContentStoreTest assertTrue("Size must be positive", size > 0L); assertTrue("Size must not be Long.MAX_VALUE", size < Long.MAX_VALUE); } + + + /** + * Empty parent directories should be removed when a URL is removed. + */ + public void testDeleteRemovesEmptyDirs() throws Exception + { + ContentStore store = getStore(); + String url = "store://1965/12/1/13/12/file.bin"; + + // Ensure clean test data + if (store.exists(url)) store.delete(url); + + String content = "Content for test: " + getName(); + store.getWriter(new ContentContext(null, url)).putContent(content); + + File root = new File(store.getRootLocation()); + + assertDirExists(root, ""); + assertDirExists(root, "1965/12/1/13/12"); + + store.delete(url); + + assertDirNotExists(root, "1965"); + // root should be untouched. + assertDirExists(root, ""); + } + + /** + * Only non-empty directories should be deleted. + */ + public void testDeleteLeavesNonEmptyDirs() + { + ContentStore store = getStore(); + String url = "store://1965/12/1/13/12/file.bin"; + + // Ensure clean test data + if (store.exists(url)) store.delete(url); + + String content = "Content for test: " + getName(); + store.getWriter(new ContentContext(null, url)).putContent(content); + + File root = new File(store.getRootLocation()); + + assertDirExists(root, ""); + assertDirExists(root, "1965/12/1/13/12"); + + // Make a directory non-empty + String anotherUrl = "store://1965/12/3/another.bin"; + if (store.exists(anotherUrl)) store.delete(anotherUrl); + store.getWriter(new ContentContext(null, anotherUrl)); + + store.delete(url); + + // Parents of another.bin cannot be deleted + assertDirExists(root, "1965"); + assertDirExists(root, "1965/12"); + // Non-parents of another.bin could be deleted + assertDirNotExists(root, "1965/12/1"); + + // root should be untouched. + assertDirExists(root, ""); + } + + + /** + * Empty parent directories are not deleted if the store is configured not to. + */ + public void testNoParentDirsDeleted() throws Exception + { + store.setDeleteEmptyDirs(false); + FileContentStore store = (FileContentStore) getStore(); + String url = "store://1965/12/1/13/12/file.bin"; + // Ensure clean test data + if (store.exists(url)) store.delete(url); + String content = "Content for test: " + getName(); + store.getWriter(new ContentContext(null, url)).putContent(content); + File root = new File(store.getRootLocation()); + + store.delete(url); + + assertDirExists(root, "1965/12/1/13/12"); + // root should be untouched. + assertDirExists(root, ""); + } + + + private void assertDirExists(File root, String dir) + { + assertTrue("Directory [" + dir + "] should exist", new File(root, dir).exists()); + } + + + private void assertDirNotExists(File root, String dir) + { + assertFalse("Directory [" + dir + "] should NOT exist", new File(root, dir).exists()); + } } diff --git a/source/java/org/alfresco/repo/invitation/InviteHelper.java b/source/java/org/alfresco/repo/invitation/InviteHelper.java index dfa77f240f..6d3617d43d 100644 --- a/source/java/org/alfresco/repo/invitation/InviteHelper.java +++ b/source/java/org/alfresco/repo/invitation/InviteHelper.java @@ -70,8 +70,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; -import com.aetrion.flickr.auth.AuthUtilities; - /** * Helper class to house utility methods common to * more than one Invite Service Web Script @@ -145,7 +143,7 @@ public class InviteHelper implements InitializingBean return null; } }); - addSiteMembership(invitee, siteShortName, role, inviter); + addSiteMembership(invitee, siteShortName, role, inviter, false); } /** @@ -268,14 +266,18 @@ public class InviteHelper implements InitializingBean * @param role * @param runAsUser * @param siteService + * @param overrideExisting */ - public void addSiteMembership(final String invitee, final String siteName, final String role, final String runAsUser) + public void addSiteMembership(final String invitee, final String siteName, final String role, final String runAsUser, final boolean overrideExisting) { AuthenticationUtil.runAs(new RunAsWork() { public Void doWork() throws Exception { - siteService.setMembership(siteName, invitee, role); + if (overrideExisting || !siteService.isMember(siteName, invitee)) + { + siteService.setMembership(siteName, invitee, role); + } return null; } @@ -415,7 +417,7 @@ public class InviteHelper implements InitializingBean String reviewer = (String)executionVariables.get(WorkflowModelModeratedInvitation.wfVarReviewer); // Add invitee to the site - addSiteMembership(invitee, siteName, role, reviewer); + addSiteMembership(invitee, siteName, role, reviewer, true); } @SuppressWarnings("unchecked") diff --git a/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModel.java b/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModel.java index 48bb02cc40..06c2067080 100644 --- a/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModel.java +++ b/source/java/org/alfresco/repo/security/permissions/impl/model/PermissionModel.java @@ -18,8 +18,11 @@ */ package org.alfresco.repo.security.permissions.impl.model; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URL; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; @@ -36,8 +39,8 @@ import org.alfresco.repo.security.permissions.PermissionEntry; import org.alfresco.repo.security.permissions.PermissionReference; import org.alfresco.repo.security.permissions.impl.ModelDAO; import org.alfresco.repo.security.permissions.impl.RequiredPermission; -import org.alfresco.repo.security.permissions.impl.SimplePermissionReference; import org.alfresco.repo.security.permissions.impl.RequiredPermission.On; +import org.alfresco.repo.security.permissions.impl.SimplePermissionReference; import org.alfresco.service.cmr.dictionary.AspectDefinition; import org.alfresco.service.cmr.dictionary.ClassDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; @@ -53,8 +56,12 @@ import org.alfresco.util.Pair; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.DocumentException; +import org.dom4j.DocumentType; import org.dom4j.Element; +import org.dom4j.io.OutputFormat; import org.dom4j.io.SAXReader; +import org.dom4j.io.XMLWriter; +import org.dom4j.tree.DefaultDocumentType; /** * The implementation of the model DAO Reads and stores the top level model information Encapsulates access to this @@ -93,6 +100,8 @@ public class PermissionModel implements ModelDAO // Instance variables private String model; + private String dtdSchema; + private boolean validate = true; /* * (non-Javadoc) @@ -1142,6 +1151,26 @@ public class PermissionModel implements ModelDAO this.model = model; } + /** + * Set the dtd schema that is used to validate permission model + * + * @param dtdSchema + */ + public void setDtdSchema(String dtdSchema) + { + this.dtdSchema = dtdSchema; + } + + /** + * Indicates whether model should be validated on initialization against specified dtd + * + * @param validate + */ + public void setValidate(boolean validate) + { + this.validate = validate; + } + /** * Set the dictionary service * @@ -1261,6 +1290,9 @@ public class PermissionModel implements ModelDAO private Document createDocument(String model) { InputStream is = this.getClass().getClassLoader().getResourceAsStream(model); + URL dtdSchemaUrl = (dtdSchema == null) + ? null + : this.getClass().getClassLoader().getResource(dtdSchema); if (is == null) { throw new PermissionModelException("File not found: " + model); @@ -1268,21 +1300,156 @@ public class PermissionModel implements ModelDAO SAXReader reader = new SAXReader(); try { + if (validate) + { + if (dtdSchemaUrl != null) + { + is = processModelDocType(is, dtdSchemaUrl.toString()); + reader.setValidation(true); + } + else + { + throw new PermissionModelException("Couldn't obtain DTD schema to validate permission model."); + } + } + Document document = reader.read(is); is.close(); return document; } catch (DocumentException e) { - throw new PermissionModelException("Failed to create permission model document ", e); + throw new PermissionModelException("Failed to create permission model document: " + model, e); } catch (IOException e) { - throw new PermissionModelException("Failed to close permission model document ", e); + throw new PermissionModelException("Failed to close permission model document: " + model, e); } - + +// TODO Do something like the following so that we don't need to modify the source xml +// to validate it. The following does not work for DTDs. + +// InputStream is = this.getClass().getClassLoader().getResourceAsStream(model); +// if (is == null) +// { +// throw new PermissionModelException("File not found: " + model); +// } +// +// InputStream dtdSchemaIs = (dtdSchema == null) +// ? null +// : this.getClass().getClassLoader().getResourceAsStream(dtdSchema); +// +// try +// { +// Document document; +// SAXReader reader; +// if (validate) +// { +// if (dtdSchemaIs != null) +// { +// SAXParserFactory factory = SAXParserFactory.newInstance(); +// +// SchemaFactory schemaFactory = +// SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema"); +// +// try +// { +// factory.setSchema(schemaFactory.newSchema( +// new Source[] {new StreamSource(dtdSchemaIs)})); +// SAXParser parser = factory.newSAXParser(); +// reader = new SAXReader(parser.getXMLReader()); +// reader.setValidation(false); +// reader.setErrorHandler(new XMLErrorHandler()); +// } +// catch (SAXException e) +// { +// throw new PermissionModelException("Failed to read DTD/schema: " + dtdSchema, e); +// } +// catch (ParserConfigurationException e) +// { +// throw new PermissionModelException("Failed to configure DTD/schema: " + dtdSchema, e); +// } +// } +// else +// { +// throw new PermissionModelException("Couldn't obtain DTD/schema to validate permission model."); +// } +// } +// else +// { +// reader = new SAXReader(); +// } +// document = reader.read(is); +// return document; +// } +// catch (DocumentException e) +// { +// throw new PermissionModelException("Failed to create permission model document: " + model, e); +// } +// finally +// { +// if (is != null) +// { +// try +// { +// is.close(); +// } +// catch (IOException e) +// { +// throw new PermissionModelException("Failed to close permission model document: " + model, e); +// } +// } +// if (dtdSchemaIs != null) +// { +// try +// { +// dtdSchemaIs.close(); +// } +// catch (IOException e) +// { +// throw new PermissionModelException("Couldn't close DTD/schema to validate permission model."); +// } +// } +// } } + /* + * Replace or add correct DOCTYPE to the xml to allow validation against dtd + */ + private InputStream processModelDocType(InputStream is, String dtdSchemaUrl) throws DocumentException, IOException + { + SAXReader reader = new SAXReader(); + // read document without validation + Document doc = reader.read(is); + DocumentType docType = doc.getDocType(); + if (docType != null) + { + // replace DOCTYPE setting the full path to the xsd + docType.setSystemID(dtdSchemaUrl); + } + else + { + // add the DOCTYPE + docType = new DefaultDocumentType(doc.getRootElement().getName(), dtdSchemaUrl); + doc.setDocType(docType); + } + + ByteArrayOutputStream fos = new ByteArrayOutputStream(); + try + { + OutputFormat format = OutputFormat.createPrettyPrint(); // uses UTF-8 + XMLWriter writer = new XMLWriter(fos, format); + writer.write(doc); + writer.flush(); + } + finally + { + fos.close(); + } + + return new ByteArrayInputStream(fos.toByteArray()); + } + /** * Set the default access status *