From e31ea91e51a436e27d02371a8c3d912a9df6afee Mon Sep 17 00:00:00 2001 From: Dave Ward Date: Wed, 18 Apr 2012 09:42:39 +0000 Subject: [PATCH] Merged V4.0-BUG-FIX to HEAD 35224: ALF-12038: Remove trailing JSON comma causing IE7 script error 35226: ALF-13401 - Saving PowerPoint (mac 2011) via CIFS fails in Mac OS X Lion 35239: ALF-13409: Further fix to deal with concurrent deletion of a user's site invitations in background 35245: ALF-13281: Enabled use of autocomplete in IE for forms runtime. This change also allows multiple events to be attached per validation handler 35253: ALF-13640: Fixed issues with updating task associations + added new test + fixed existing activiti-component-tests 35271: Translation updates (fixes: ALF-13434) - based on EN r35212. (Dutch still to follow) 35281: ALF-13227: Fix CSS for Wiki layout of nested lists 35284: SPANISH: Update from Gloria 35290: More debug + unit test for mac powerpoint shuffle. 35291: Added isTemporary method 35295: ALF-13453 : Remote Code Execution (can create reverse shell). - Added ability for XMLUtil parse callers to provide an optional array of XMLFilterImpl to be used while parsing. -Added secureParseXSL methods that automatically install an XMLFilterImpl that causes a parse failure if any insecure namespaces are encountered. 35303: Fix for ALF-12444 Node Browser improvement: Index single node and remove single node from indexes Part of ALF-13723 SOLR does not include the same query unit tests as lucene 35305: ALF-13723 SOLR does not include the same query unit tests as lucene - test template 35306: ALF-13723 SOLR does not include the same query unit tests as lucene - template for creating test cores 35323: ALF-13420: Natural sort on form option labels and improvement for CSS - specifically to address transform action in document details. 35328: ALF-13409: Avoid concurrency issues in unit test tear downs by deleting users before sites. User deletion deletes invitations synchronously. Site deletion deletes invitations concurrently to avoid UI timeouts. The potential to access invitations that are being concurrently deleted still exists, but always did! 35331: ALF-12126: Ensure that DND upload is disabled for users with only consumer access 35335: ALF-13708: Merged V3.4-BUG-FIX (3.4.10) to V4.0-BUG-FIX (4.0.2) 35235: ALF-13673: Amp-loaded duplicated mimetypes should be handled - Modified code to allow duplicates to replace parts of the existing mimetype definitions. - A warning is logged each time. 35336: Spanish and Dutch updates from Gloria, based on EN r35212 35355: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 35213: ALF-13686: Merged PATCHES/V3.4.8 to V3.4-BUG-FIX 34943: ALF-13121: Option to create users either as user1 or user1@domain.com after kerberos authentication - New Kerberos subsystem parameter kerberos.authentication.stripUsernameSuffix introduced - When true (the default) the @domain sufix will be stripped from Kerberos authenticated usernames in CIFS, SPP, WebDAV and the Web Client - When false, should enable a multi-domain customer to use Alfresco (says Mr Gninot) 35096: ALF-13121: Added missing stripKerberosUsernameSuffix property to sharepointAuthenticationHandler 35215: ALF-13065: Ensure Wiki new page save button is available on HTML edit action 35219: ALF-11898: Fixed TinyMCE create HTML content problem for Explorer client 35261: Translation updates based on EN r35144 35339: AD 2008 R2, user import via LDAP fails with over 1000 users - Problem discovered by Community user with simple workaround https://forums.alfresco.com/en/viewtopic.php?f=57&t=43960&sid=5569e5cfbccb3776e11ef4a8e9d50378&p=129664#p129664 35353: Merged V3.4 to V3.4-BUG-FIX 35279: ALF-13713: Merged PATCHES/V3.4.8 to V3.4 35146: Merged DEV to PATCHES/V3.4.8 35130: ALF-13472: Webdav Does not allow a user to access spaces without read permission on parent spaces Receiving of indirect lock is wrapped into AuthenticationUtil.runAs() invocation to provide a possibility of getting indirect lock for users with appropriate access rights for requested resource 35280: ALF-10353: Internet Explorer hangs when using the object picker with a larger number of documents - reviewed by DD 35318: ALF-13715: Merged HEAD to V3.4 31743: Fixed ALF-10157: Web Form Details page for the "Selected Web Content Forms": script error appears on help button click: container.jsp (line 382) 35341: ALF-13552: Merged V4.0 to V3.4 35296: ALF-13453: Remote Code Execution (can create reverse shell) - Fix by Shane 35304: ALF-13453: Extra fix to ensure xalan namespace isn't declared with global scope and can't be hijacked by an input stylesheet 35307: ALF-13453: Duplicated extra fix to duplicate code in XSLTRenderingEngine! 35354: Merged V3.4 to V3.4-BUG-FIX (RECORD ONLY) 35266: Merged V3.4-BUG-FIX to V3.4 35261: Translation updates based on EN r35144 35334: Merged V3.4-BUG-FIX to V3.4 35235: ALF-13673: Amp-loaded duplicated mimetypes should be handled - Modified code to allow duplicates to replace parts of the existing mimetype definitions. - A warning is logged each time. 35356: Merged V4.0 to V4.0-BUG-FIX 35292: ALF-13721: Merged PATCHES/V4.0.0 to V4.0 35240: Fix for ALF-13685 The SOLr textContent webscript is not protected by authentication and permission checks. 35242: Fix for ALF-13685 The SOLr textContent webscript is not protected by authentication and permission checks. - /wcs/api/solr and /wcservice/api/solr 35304: ALF-13453: Extra fix to ensure xalan namespace isn't declared with global scope and can't be hijacked by an input stylesheet 35307: ALF-13453: Duplicated extra fix to duplicate code in XSLTRenderingEngine! 35357: Merged V4.0 to V4.0-BUG-FIX (RECORD ONLY) 35048: Merged V4.0-BUG-FIX to V4.0 35031: Fix for ALF-12309: Script errors on site pages 35293: Merged V4.0-BUG-FIX to V4.0 35172: ALF-13626: category.put.json.ftl has wrong bracket 35296: Merged V4.0-BUG-FIX to V4.0 35295: ALF-13453: Remote Code Execution (can create reverse shell) - Fix by Shane git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@35359 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../messages/patch-service_de.properties | 2 + .../messages/patch-service_es.properties | 2 + .../messages/patch-service_fr.properties | 2 + .../messages/patch-service_it.properties | 2 + .../messages/patch-service_ja.properties | 2 + .../messages/patch-service_nl.properties | 2 + .../messages/templates-messages_ja.properties | 2 +- .../kerberos-authentication-context.xml | 9 +- .../kerberos-authentication.properties | 3 +- .../ldap-ad/ldap-ad-authentication.properties | 6 +- .../cifs/EnterpriseCifsAuthenticator.java | 22 +- .../filesys/repo/ContentDiskDriver2.java | 37 +- .../filesys/repo/ContentDiskDriverTest.java | 339 +++++++++++++++++- .../repo/rules/ScenarioOpenFileInstance.java | 16 +- .../invitation/InvitationServiceImpl.java | 18 +- .../executer/XSLTRenderingEngineTest.java | 39 ++ .../search/impl/lucene/ADMLuceneTest.java | 4 +- .../alfresco/repo/template/XSLTProcessor.java | 11 +- .../repo/template/XSLTProcessorTest.java | 35 ++ source/java/org/alfresco/util/XMLUtil.java | 285 ++++++++++++++- 20 files changed, 789 insertions(+), 49 deletions(-) diff --git a/config/alfresco/messages/patch-service_de.properties b/config/alfresco/messages/patch-service_de.properties index 4bcb96b00a..fbd1ffe5bd 100755 --- a/config/alfresco/messages/patch-service_de.properties +++ b/config/alfresco/messages/patch-service_de.properties @@ -425,6 +425,8 @@ patch.activitiesTemplatesUpdate.description=Updates activities email templates. patch.activitiesTemplatesUpdate.error=Error retrieving base template when trying to patch the activity email templates. patch.activitiesTemplatesUpdate.result=Updated activities email templates. +patch.activitiesEmailTemplate.description=Creates activities email templates. + patch.avmToAdmRemoteStore.description=Migrates Share Surf config from AVM sitestore to DM Sites folder. patch.avmToAdmRemoteStore.complete=Completed Share Surf config migration. diff --git a/config/alfresco/messages/patch-service_es.properties b/config/alfresco/messages/patch-service_es.properties index 4bcb96b00a..fbd1ffe5bd 100755 --- a/config/alfresco/messages/patch-service_es.properties +++ b/config/alfresco/messages/patch-service_es.properties @@ -425,6 +425,8 @@ patch.activitiesTemplatesUpdate.description=Updates activities email templates. patch.activitiesTemplatesUpdate.error=Error retrieving base template when trying to patch the activity email templates. patch.activitiesTemplatesUpdate.result=Updated activities email templates. +patch.activitiesEmailTemplate.description=Creates activities email templates. + patch.avmToAdmRemoteStore.description=Migrates Share Surf config from AVM sitestore to DM Sites folder. patch.avmToAdmRemoteStore.complete=Completed Share Surf config migration. diff --git a/config/alfresco/messages/patch-service_fr.properties b/config/alfresco/messages/patch-service_fr.properties index 4bcb96b00a..fbd1ffe5bd 100755 --- a/config/alfresco/messages/patch-service_fr.properties +++ b/config/alfresco/messages/patch-service_fr.properties @@ -425,6 +425,8 @@ patch.activitiesTemplatesUpdate.description=Updates activities email templates. patch.activitiesTemplatesUpdate.error=Error retrieving base template when trying to patch the activity email templates. patch.activitiesTemplatesUpdate.result=Updated activities email templates. +patch.activitiesEmailTemplate.description=Creates activities email templates. + patch.avmToAdmRemoteStore.description=Migrates Share Surf config from AVM sitestore to DM Sites folder. patch.avmToAdmRemoteStore.complete=Completed Share Surf config migration. diff --git a/config/alfresco/messages/patch-service_it.properties b/config/alfresco/messages/patch-service_it.properties index 4bcb96b00a..fbd1ffe5bd 100755 --- a/config/alfresco/messages/patch-service_it.properties +++ b/config/alfresco/messages/patch-service_it.properties @@ -425,6 +425,8 @@ patch.activitiesTemplatesUpdate.description=Updates activities email templates. patch.activitiesTemplatesUpdate.error=Error retrieving base template when trying to patch the activity email templates. patch.activitiesTemplatesUpdate.result=Updated activities email templates. +patch.activitiesEmailTemplate.description=Creates activities email templates. + patch.avmToAdmRemoteStore.description=Migrates Share Surf config from AVM sitestore to DM Sites folder. patch.avmToAdmRemoteStore.complete=Completed Share Surf config migration. diff --git a/config/alfresco/messages/patch-service_ja.properties b/config/alfresco/messages/patch-service_ja.properties index 4bcb96b00a..fbd1ffe5bd 100755 --- a/config/alfresco/messages/patch-service_ja.properties +++ b/config/alfresco/messages/patch-service_ja.properties @@ -425,6 +425,8 @@ patch.activitiesTemplatesUpdate.description=Updates activities email templates. patch.activitiesTemplatesUpdate.error=Error retrieving base template when trying to patch the activity email templates. patch.activitiesTemplatesUpdate.result=Updated activities email templates. +patch.activitiesEmailTemplate.description=Creates activities email templates. + patch.avmToAdmRemoteStore.description=Migrates Share Surf config from AVM sitestore to DM Sites folder. patch.avmToAdmRemoteStore.complete=Completed Share Surf config migration. diff --git a/config/alfresco/messages/patch-service_nl.properties b/config/alfresco/messages/patch-service_nl.properties index 4bcb96b00a..fbd1ffe5bd 100755 --- a/config/alfresco/messages/patch-service_nl.properties +++ b/config/alfresco/messages/patch-service_nl.properties @@ -425,6 +425,8 @@ patch.activitiesTemplatesUpdate.description=Updates activities email templates. patch.activitiesTemplatesUpdate.error=Error retrieving base template when trying to patch the activity email templates. patch.activitiesTemplatesUpdate.result=Updated activities email templates. +patch.activitiesEmailTemplate.description=Creates activities email templates. + patch.avmToAdmRemoteStore.description=Migrates Share Surf config from AVM sitestore to DM Sites folder. patch.avmToAdmRemoteStore.complete=Completed Share Surf config migration. diff --git a/config/alfresco/messages/templates-messages_ja.properties b/config/alfresco/messages/templates-messages_ja.properties index e39202984d..8efd4075b8 100755 --- a/config/alfresco/messages/templates-messages_ja.properties +++ b/config/alfresco/messages/templates-messages_ja.properties @@ -83,6 +83,6 @@ templates.doc_info.no_document_found=\u6587\u66f8\u304c\u898b\u3064\u304b\u308a\ #invite_user_email.ftl templates.invite_user_email.invited_to_space={1}\u304b\u3089''{0}''\u306b\u62db\u5f85\u3055\u308c\u3066\u3044\u307e\u3059 -templates.invite_user_email.role=\u4ee5\u4e0b\u306e\u5f79\u5272\u304c\u3042\u308a\u307e\u3059: {0} +templates.invite_user_email.role=\u3042\u306a\u305f\u306e\u5f79\u5272: {0} templates.invite_user_email.you_can_view_the_space=Alfresco\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u4f7f\u7528\u3057\u3066\u30b9\u30da\u30fc\u30b9\u3092\u8868\u793a\u3067\u304d\u307e\u3059\u3002 templates.invite_user_email.regards=\u3088\u308d\u3057\u304f\u304a\u9858\u3044\u3057\u307e\u3059\u3002 diff --git a/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication-context.xml b/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication-context.xml index a8aac32ac8..aa28f4b2ae 100644 --- a/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication-context.xml +++ b/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication-context.xml @@ -76,9 +76,12 @@ true - - ${kerberos.authentication.cifs.enableTicketCracking} - + + ${kerberos.authentication.cifs.enableTicketCracking} + + + ${kerberos.authentication.stripUsernameSuffix} + \ No newline at end of file diff --git a/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication.properties b/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication.properties index 423defd9a3..608bab381a 100644 --- a/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication.properties +++ b/config/alfresco/subsystems/Authentication/kerberos/kerberos-authentication.properties @@ -4,4 +4,5 @@ kerberos.authentication.defaultAdministratorUserNames= kerberos.authentication.cifs.configEntryName=AlfrescoCIFS kerberos.authentication.cifs.password=secret kerberos.authentication.cifs.enableTicketCracking=false -kerberos.authentication.authenticateCIFS=true \ No newline at end of file +kerberos.authentication.authenticateCIFS=true +kerberos.authentication.stripUsernameSuffix=true \ No newline at end of file diff --git a/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties index b7af1b1393..f9fa167220 100644 --- a/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties +++ b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties @@ -63,13 +63,13 @@ ldap.synchronization.attributeBatchSize=1000 ldap.synchronization.groupQuery=(objectclass\=group) # The query to select objects that represent the groups to import that have changed since a certain time. -ldap.synchronization.groupDifferentialQuery=(&(objectclass\=group)(!(modifyTimestamp<\={0}))) +ldap.synchronization.groupDifferentialQuery=(&(objectclass\=group)(!(whenChanged<\={0}))) # The query to select all objects that represent the users to import. ldap.synchronization.personQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)) # The query to select objects that represent the users to import that have changed since a certain time. -ldap.synchronization.personDifferentialQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(!(modifyTimestamp<\={0}))) +ldap.synchronization.personDifferentialQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(!(whenChanged<\={0}))) # The group search base restricts the LDAP group query to a sub section of tree on the LDAP server. ldap.synchronization.groupSearchBase=ou\=Security Groups,ou\=Alfresco,dc=domain @@ -78,7 +78,7 @@ ldap.synchronization.groupSearchBase=ou\=Security Groups,ou\=Alfresco,dc=domain ldap.synchronization.userSearchBase=ou\=User Accounts,ou=\Alfresco,dc=domain # The name of the operational attribute recording the last update time for a group or user. -ldap.synchronization.modifyTimestampAttributeName=modifyTimestamp +ldap.synchronization.modifyTimestampAttributeName=whenChanged # The timestamp format. Unfortunately, this varies between directory servers. ldap.synchronization.timestampFormat=yyyyMMddHHmmss'.0Z' diff --git a/source/java/org/alfresco/filesys/auth/cifs/EnterpriseCifsAuthenticator.java b/source/java/org/alfresco/filesys/auth/cifs/EnterpriseCifsAuthenticator.java index ef2d68dda4..d1adc9a7c8 100644 --- a/source/java/org/alfresco/filesys/auth/cifs/EnterpriseCifsAuthenticator.java +++ b/source/java/org/alfresco/filesys/auth/cifs/EnterpriseCifsAuthenticator.java @@ -112,6 +112,10 @@ public class EnterpriseCifsAuthenticator extends CifsAuthenticatorBase implement private boolean m_acceptNTLMv1 = true; + // Should we strip the @domain suffix from the Kerberos username? + + private boolean m_stripKerberosUsernameSuffix = true; + // Kerberos settings // // Account name and password for server ticket @@ -213,6 +217,11 @@ public class EnterpriseCifsAuthenticator extends CifsAuthenticatorBase implement { this.m_acceptNTLMv1 = !disallowNTLMv1; } + + public void setStripKerberosUsernameSuffix(boolean stripKerberosUsernameSuffix) + { + m_stripKerberosUsernameSuffix = stripKerberosUsernameSuffix; + } /** * Enable Kerbeors ticket cracking code that is required for Java5 @@ -1585,6 +1594,7 @@ public class EnterpriseCifsAuthenticator extends CifsAuthenticatorBase implement // Check if this is a null logon String userName = krbDetails.getUserName(); + String userId = m_stripKerberosUsernameSuffix ? krbDetails.getUserName() : krbDetails.getSourceName(); if ( userName != null) { @@ -1602,7 +1612,7 @@ public class EnterpriseCifsAuthenticator extends CifsAuthenticatorBase implement // Debug if ( logger.isDebugEnabled()) - logger.debug("Machine account logon, " + userName + ", as null logon"); + logger.debug("Machine account logon, " + userId + ", as null logon"); } else { @@ -1611,7 +1621,7 @@ public class EnterpriseCifsAuthenticator extends CifsAuthenticatorBase implement try { AlfrescoClientInfo alfClient = (AlfrescoClientInfo) client; - getAuthenticationComponent().setCurrentUser( mapUserNameToPerson(krbDetails.getUserName(), true)); + getAuthenticationComponent().setCurrentUser( mapUserNameToPerson(userId, true)); alfClient.setAuthenticationTicket(getAuthenticationService().getCurrentTicket() ); } catch (AuthenticationException e) @@ -1622,11 +1632,9 @@ public class EnterpriseCifsAuthenticator extends CifsAuthenticatorBase implement } - // Store the full user name in the client information, indicate that this is not a guest logon - - // ALF-4599: CIFS access to alfresco creates wrong users with Realm suffix - // client.setUserName( krbDetails.getSourceName()); - client.setUserName( krbDetails.getUserName()); + // Store the user name in the client information, indicate that this is not a guest logon + + client.setUserName( userId); client.setGuest( false); // Indicate that the session is logged on diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriver2.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriver2.java index 52343746da..0af000e8aa 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriver2.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriver2.java @@ -1267,6 +1267,10 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD catch (IOException ex) { // Allow I/O Exceptions to pass through + if ( logger.isDebugEnabled()) + { + logger.debug("Delete file error - pass through IO Exception", ex); + } throw ex; } catch (Exception ex) @@ -1526,6 +1530,15 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD } //TODO MER Think we may need to implement, Temporary, Hidden, System, Archive + if(info.isSystem()) + { + logger.debug("Set system aspect (not yet implemented)" + name); + } + if(info.isTemporary()) + { + logger.debug("Set temporary aspect (not yet implemented)" + name); + } + if(info.isHidden()) { // yes is hidden @@ -1561,6 +1574,24 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD } // Not yet implemented } + + if( info.hasSetFlag(FileInfo.SetFileSize)) + { + if ( logger.isDebugEnabled()) + { + logger.debug("Set file size" + name + info.getSize()); + } + // Not yet implemented + } + + if( info.hasSetFlag(FileInfo.SetMode)) + { + if ( logger.isDebugEnabled()) + { + logger.debug("Set Mode" + name + info.getMode()); + } + // Not yet implemented - set the unix mode e.g. 777 + } // Set the creation and modified date/time Map auditableProps = new HashMap(5); @@ -2634,7 +2665,7 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD { if ( logger.isDebugEnabled()) { - logger.debug("Close file:" + path); + logger.debug("Close file:" + path + ", readOnly=" + file.isReadOnly() ); } if( file instanceof PseudoNetworkFile) @@ -2650,7 +2681,7 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD { if(logger.isDebugEnabled()) { - logger.debug("closeFile has delete on close set"); + logger.debug("closeFile has delete on close set path:" + path); } try { @@ -2686,7 +2717,7 @@ public class ContentDiskDriver2 extends AlfrescoDiskDriver implements ExtendedD { if(logger.isDebugEnabled()) { - logger.debug("Got a temp network file to close"); + logger.debug("Got a temp network file to close path:" + path); } // Some content was written to the temp file. diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java index dffcaf542d..cc2913a284 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java @@ -96,6 +96,8 @@ import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.FileFilterMode; +import org.alfresco.util.FileFilterMode.Client; import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -2488,7 +2490,7 @@ public class ContentDiskDriverTest extends TestCase final String FILE_NAME = "ContentDiskDriver.docx"; //final String FILE_OLD_TEMP = "._Word Work File D_1725484373.tmp"; final String FILE_NEW_TEMP = "Word Work File D_1725484373.tmp"; - + class TestContext { NodeRef testDirNodeRef; @@ -2503,7 +2505,7 @@ public class ContentDiskDriverTest extends TestCase ServerConfiguration scfg = new ServerConfiguration("testServer"); TestServer testServer = new TestServer("testServer", scfg); - final SrvSession testSession = new TestSrvSession(666, testServer, "test", "remoteName"); + final SrvSession testSession = new TestSrvSession(666, testServer, "cifs", "remoteName"); DiskSharedDevice share = getDiskSharedDevice(); final TreeConnection testConnection = testServer.getTreeConnection(share); final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper(); @@ -2694,6 +2696,8 @@ public class ContentDiskDriverTest extends TestCase // Check that the temporary aspect has been applied. assertTrue("temporary aspect has not been removed", !nodeService.hasAspect(testContext.testNodeRef, ContentModel.ASPECT_TEMPORARY)); + assertTrue("hidden aspect has not been removed", !nodeService.hasAspect(testContext.testNodeRef, ContentModel.ASPECT_HIDDEN)); + // These metadata values should be extracted. @@ -3553,6 +3557,144 @@ public class ContentDiskDriverTest extends TestCase tran.doInTransaction(validateCB, false, true); } // testOpenCloseFileScenario + + + /** + * Test Open Close File Scenario II ALF-13401 + * Open Read Only of a file already open for read/write. + * + * 1) open(readWrite) + * 2) write some content. + * 3) open(readOnly). + * 4) read some content. + * 5) close - updates the repo + */ + public void testScenarioOpenCloseFileTwo() throws Exception + { + logger.debug("start of testScenarioOpenCloseFileTwo"); + ServerConfiguration scfg = new ServerConfiguration("testServer"); + TestServer testServer = new TestServer("testServer", scfg); + final SrvSession testSession = new TestSrvSession(666, testServer, "test", "remoteName"); + DiskSharedDevice share = getDiskSharedDevice(); + final TreeConnection testConnection = testServer.getTreeConnection(share); + final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper(); + + class TestContext + { + NodeRef testDirNodeRef; + NodeRef targetNodeRef; + }; + + final TestContext testContext = new TestContext(); + + final String FILE_NAME="testScenarioOpenFileTwo.txt"; + final String FILE_PATH= TEST_ROOT_DOS_PATH + "\\" + FILE_NAME; + + FileOpenParams dirParams = new FileOpenParams(TEST_ROOT_DOS_PATH, 0, AccessMode.ReadOnly, FileAttribute.NTDirectory, 0); + driver.createDirectory(testSession, testConnection, dirParams); + + testContext.testDirNodeRef = getNodeForPath(testConnection, TEST_ROOT_DOS_PATH); + + /** + * Clean up just in case garbage is left from a previous run + */ + RetryingTransactionCallback deleteGarbageFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + driver.deleteFile(testSession, testConnection, FILE_PATH); + return null; + } + }; + try + { + tran.doInTransaction(deleteGarbageFileCB); + } + catch (Exception e) + { + // expect to go here + } + + /** + * Step 1: Now create the file through the node service and open it. + */ + logger.debug("Step 1) Create File and Open file created by node service"); + RetryingTransactionCallback createFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + logger.debug("create file and close it immediatly"); + FileOpenParams createFileParams = new FileOpenParams(FILE_PATH, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + NetworkFile dummy = driver.createFile(testSession, testConnection, createFileParams); + driver.closeFile(testSession, testConnection, dummy); + logger.debug("after create and close"); + return null; + } + }; + tran.doInTransaction(createFileCB, false, true); + + testContext.targetNodeRef = getNodeForPath(testConnection, FILE_PATH); + + FileOpenParams openRO = new FileOpenParams(FILE_PATH, FileAction.CreateNotExist, AccessMode.ReadOnly, FileAttribute.NTNormal, 0); + FileOpenParams openRW = new FileOpenParams(FILE_PATH, FileAction.CreateNotExist, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + + /** + * First open - read write + */ + logger.debug("open file1 read only"); + NetworkFile file1 = driver.openFile(testSession, testConnection, openRW); + assertNotNull(file1); + + final String testString = "Yankee doodle went to town, riding on a donkey."; + byte[] stuff = testString.getBytes("UTF-8"); + + driver.writeFile(testSession, testConnection, file1, stuff, 0, stuff.length, 0); + + logger.debug("open file 2 for read only"); + NetworkFile file2 = driver.openFile(testSession, testConnection, openRO); + assertNotNull(file2); + + + assertTrue("file size is 0", file2.getFileSize() > 0); + + + /** + * Write Some More Content + */ + driver.writeFile(testSession, testConnection, file1, stuff, 0, stuff.length, 0); + + logger.debug("first close"); + driver.closeFile(testSession, testConnection, file2); + + logger.debug("second close"); + driver.closeFile(testSession, testConnection, file1); + + logger.debug("now validate"); + + RetryingTransactionCallback validateCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + Map props = nodeService.getProperties(testContext.targetNodeRef); + ContentData data = (ContentData)props.get(ContentModel.PROP_CONTENT); + assertNotNull("data is null", data); + assertEquals("data wrong length", testString.length(), data.getSize()); + + ContentReader reader = contentService.getReader(testContext.targetNodeRef, ContentModel.PROP_CONTENT); + String s = reader.getContentString(); + assertEquals("content not written", testString, s); + + return null; + } + }; + + tran.doInTransaction(validateCB, false, true); + + } // testOpenCloseFileScenarioTwo + @@ -4774,7 +4916,198 @@ public class ContentDiskDriverTest extends TestCase } // testScenarioLionTextEdit - + /** + * Simulates a Save from Powerpoint 2011 Mac + * 0. FileA.pptx already exists. + * 1. Create new document FileA1.pptx + * 2. Delete FileA.pptx + * 3. Rename FileA1.pptx to FileA.pptx + */ + public void testScenarioMSPowerpoint2011MacSaveShuffle() throws Exception + { + logger.debug("testScenarioMSPowerpoint2011MacSaveShuffle("); + + final String FILE_NAME = "FileA.pptx"; + final String FILE_NEW_TEMP = "FileA1.pptx"; + + class TestContext + { + NetworkFile firstFileHandle; + }; + + final TestContext testContext = new TestContext(); + + final String TEST_DIR = TEST_ROOT_DOS_PATH + "\\testScenarioMSPowerpoint2011MacSaveShuffle"; + + ServerConfiguration scfg = new ServerConfiguration("testServer"); + TestServer testServer = new TestServer("testServer", scfg); + final SrvSession testSession = new TestSrvSession(666, testServer, "cifs", "remoteName"); + DiskSharedDevice share = getDiskSharedDevice(); + final TreeConnection testConnection = testServer.getTreeConnection(share); + final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper(); + + /** + * Clean up just in case garbage is left from a previous run + */ + RetryingTransactionCallback deleteGarbageFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + driver.deleteFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NAME); + return null; + } + }; + + /** + * Create a file in the test directory + */ + + try + { + tran.doInTransaction(deleteGarbageFileCB); + } + catch (Exception e) + { + // expect to go here + } + + logger.debug("0) create new file"); + RetryingTransactionCallback createFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + + /** + * Create the test directory we are going to use + */ + FileOpenParams createRootDirParams = new FileOpenParams(TEST_ROOT_DOS_PATH, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + FileOpenParams createDirParams = new FileOpenParams(TEST_DIR, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + driver.createDirectory(testSession, testConnection, createRootDirParams); + driver.createDirectory(testSession, testConnection, createDirParams); + + /** + * Create the file we are going to use (FileA.pptx) + */ + FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + FILE_NAME, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + testContext.firstFileHandle = driver.createFile(testSession, testConnection, createFileParams); + assertNotNull(testContext.firstFileHandle); + + driver.closeFile(testSession, testConnection, testContext.firstFileHandle); + + NodeRef file1NodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME); + nodeService.addAspect(file1NodeRef, ContentModel.ASPECT_VERSIONABLE, null); + + + + return null; + } + }; + tran.doInTransaction(createFileCB, false, true); + + /** + * b) Save the new file + * Write ContentDiskDriverTest3.doc to the test file, + */ + logger.debug("b) write some content"); + RetryingTransactionCallback writeFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + FILE_NEW_TEMP, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + testContext.firstFileHandle = driver.createFile(testSession, testConnection, createFileParams); + + ClassPathResource fileResource = new ClassPathResource("filesys/ContentDiskDriverTest3.doc"); + assertNotNull("unable to find test resource filesys/ContentDiskDriverTest3.doc", fileResource); + + byte[] buffer= new byte[1000]; + InputStream is = fileResource.getInputStream(); + try + { + long offset = 0; + int i = is.read(buffer, 0, buffer.length); + while(i > 0) + { + testContext.firstFileHandle.writeFile(buffer, i, 0, offset); + offset += i; + i = is.read(buffer, 0, buffer.length); + } + } + finally + { + is.close(); + } + + driver.closeFile(testSession, testConnection, testContext.firstFileHandle); + + return null; + } + }; + tran.doInTransaction(writeFileCB, false, true); + + /** + * c) delete the old file + */ + logger.debug("c) delete old file"); + RetryingTransactionCallback renameOldFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + driver.deleteFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NAME); + return null; + } + }; + tran.doInTransaction(renameOldFileCB, false, true); + + /** + * d) Move the new file into place, stuff should get shuffled + */ + logger.debug("d) rename new file into place"); + RetryingTransactionCallback moveNewFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + driver.renameFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NEW_TEMP, TEST_DIR + "\\" + FILE_NAME); + return null; + } + }; + + tran.doInTransaction(moveNewFileCB, false, true); + + logger.debug("e) validate results"); + /** + * Now validate everything is correct + */ + RetryingTransactionCallback validateCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + NodeRef shuffledNodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME); + + Map props = nodeService.getProperties(shuffledNodeRef); + + ContentData data = (ContentData)props.get(ContentModel.PROP_CONTENT); + assertNotNull("data is null", data); + assertEquals("size is wrong", 26112, data.getSize()); + assertEquals("mimeType is wrong", "application/msword",data.getMimetype()); + + assertTrue("versionable aspect missing", nodeService.hasAspect(shuffledNodeRef, ContentModel.ASPECT_VERSIONABLE)); + assertTrue("hidden aspect still applied", !nodeService.hasAspect(shuffledNodeRef, ContentModel.ASPECT_HIDDEN)); + assertTrue("temporary aspect still applied", !nodeService.hasAspect(shuffledNodeRef, ContentModel.ASPECT_TEMPORARY)); + + return null; + } + }; + + tran.doInTransaction(validateCB, true, true); + + } // testScenarioMSPowerpoint2011MacSaveShuffle + /** diff --git a/source/java/org/alfresco/filesys/repo/rules/ScenarioOpenFileInstance.java b/source/java/org/alfresco/filesys/repo/rules/ScenarioOpenFileInstance.java index c84067c019..d80aa1e693 100644 --- a/source/java/org/alfresco/filesys/repo/rules/ScenarioOpenFileInstance.java +++ b/source/java/org/alfresco/filesys/repo/rules/ScenarioOpenFileInstance.java @@ -66,6 +66,11 @@ import org.apache.commons.logging.LogFactory; * 4) close(readWrite) - does nothing. Decrements Open Count. * 5) close(readWrite) - updates the repo. * 6) close(readOnly) - closes read only + *

+ * 1) open (readWrite) + * 2) open (readOnly) - file already open for read/write + * 3) close + * 4) close * */ class ScenarioOpenFileInstance implements ScenarioInstance @@ -307,7 +312,7 @@ class ScenarioOpenFileInstance implements ScenarioInstance if(name != null && name.equalsIgnoreCase(o.getName())) { if(o.getMode() == OpenFileMode.READ_WRITE) - { + { // This is an open of a read write access if(openReadWriteCount == 0) { @@ -329,6 +334,15 @@ class ScenarioOpenFileInstance implements ScenarioInstance else { // This is an open for read only access + + if(openReadWriteCount > 0) + { + //however the file is already open for read/write + openReadWriteCount++; + logger.debug("Return already open read/write file handle from scenario:" + this); + return new ReturnValueCommand(fileHandleReadWrite); + } + if(openReadOnlyCount == 0) { logger.debug("Open first read only from scenario:" + this); diff --git a/source/java/org/alfresco/repo/invitation/InvitationServiceImpl.java b/source/java/org/alfresco/repo/invitation/InvitationServiceImpl.java index 4229a6c50a..c684ef7fcb 100644 --- a/source/java/org/alfresco/repo/invitation/InvitationServiceImpl.java +++ b/source/java/org/alfresco/repo/invitation/InvitationServiceImpl.java @@ -469,14 +469,22 @@ public class InvitationServiceImpl implements InvitationService, NodeServicePoli */ public Invitation cancel(String invitationId) { - WorkflowTask startTask = getStartTask(invitationId); - if(taskTypeMatches(startTask, WorkflowModelModeratedInvitation.WF_START_TASK)) + try { - return cancelModeratedInvitation(startTask); + WorkflowTask startTask = getStartTask(invitationId); + if (taskTypeMatches(startTask, WorkflowModelModeratedInvitation.WF_START_TASK)) + { + return cancelModeratedInvitation(startTask); + } + else + { + return cancelNominatedInvitation(startTask); + } } - else + catch (InvitationExceptionNotFound e) { - return cancelNominatedInvitation(startTask); + // Invitation already deleted or deleted in background + return null; } } diff --git a/source/java/org/alfresco/repo/rendition/executer/XSLTRenderingEngineTest.java b/source/java/org/alfresco/repo/rendition/executer/XSLTRenderingEngineTest.java index 13d7043555..438acb8fa5 100644 --- a/source/java/org/alfresco/repo/rendition/executer/XSLTRenderingEngineTest.java +++ b/source/java/org/alfresco/repo/rendition/executer/XSLTRenderingEngineTest.java @@ -35,6 +35,7 @@ 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.cmr.repository.TemplateException; import org.alfresco.service.cmr.repository.TemplateProcessor; import org.alfresco.service.cmr.repository.TemplateService; import org.alfresco.service.cmr.search.ResultSet; @@ -45,6 +46,7 @@ import org.alfresco.util.GUID; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + /** * @author Brian * @@ -82,6 +84,33 @@ public class XSLTRenderingEngineTest extends BaseAlfrescoSpringTest "/app:company_home"); this.companyHome = rs.getNodeRef(0); } + + public void testSecurityFilter() throws Exception + { + try + { + FileInfo file = createXmlFile(companyHome); + FileInfo xslFile = createXmlFile(companyHome, insecureVerySimpleXSLT); + + RenditionDefinition def = renditionService.createRenditionDefinition(QName.createQName("Test"), XSLTRenderingEngine.NAME); + def.setParameterValue(XSLTRenderingEngine.PARAM_TEMPLATE_NODE, xslFile.getNodeRef()); + + ChildAssociationRef rendition = renditionService.render(file.getNodeRef(), def); + log.error("This insecure template should not process!"); + fail(); + + } + catch (TemplateException e) + { + //pass! + } + catch (Exception ex) + { + + log.error("Error!", ex); + fail(); + } + } public void testSimplestStringTemplate() throws Exception { @@ -318,6 +347,16 @@ public class XSLTRenderingEngineTest extends BaseAlfrescoSpringTest "" + "" + "" + "" + "" + ""; + private String insecureVerySimpleXSLT = "" + " " + + "xmlns:fn=\"http://www.w3.org/2005/02/xpath-functions\"> " + "" + + + "" + + + "" + "" + + "" + "" + "" + ""; + private String callParseXmlDocument = "" + " " + "" + diff --git a/source/java/org/alfresco/repo/search/impl/lucene/ADMLuceneTest.java b/source/java/org/alfresco/repo/search/impl/lucene/ADMLuceneTest.java index 1da81bc917..0459a1a30a 100644 --- a/source/java/org/alfresco/repo/search/impl/lucene/ADMLuceneTest.java +++ b/source/java/org/alfresco/repo/search/impl/lucene/ADMLuceneTest.java @@ -5036,11 +5036,11 @@ public class ADMLuceneTest extends TestCase implements DictionaryListener assertEquals(1, results.length()); results.close(); - results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "ASPECT:\"" + testAspect.toString() + "\"", null); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "EXACTASPECT:\"" + testAspect.toString() + "\"", null); assertEquals(1, results.length()); results.close(); - results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "ASPECT:\"" + testAspect.toPrefixString(namespacePrefixResolver) + "\"", null); + results = searcher.query(rootNodeRef.getStoreRef(), "lucene", "EXACTASPECT:\"" + testAspect.toPrefixString(namespacePrefixResolver) + "\"", null); assertEquals(1, results.length()); results.close(); diff --git a/source/java/org/alfresco/repo/template/XSLTProcessor.java b/source/java/org/alfresco/repo/template/XSLTProcessor.java index e988073d36..619b5342f3 100644 --- a/source/java/org/alfresco/repo/template/XSLTProcessor.java +++ b/source/java/org/alfresco/repo/template/XSLTProcessor.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; +import java.io.StringWriter; import java.io.Writer; import java.util.Arrays; import java.util.HashMap; @@ -134,7 +135,7 @@ public class XSLTProcessor extends BaseProcessor implements TemplateProcessor Document xslTemplate; try { - xslTemplate = XMLUtil.parse(templateSource.getReader(defaultEncoding)); + xslTemplate = XMLUtil.secureParseXSL(templateSource.getReader(defaultEncoding)); } catch (IOException ex) { @@ -314,18 +315,17 @@ public class XSLTProcessor extends BaseProcessor implements TemplateProcessor final Element docEl = xslTemplate.getDocumentElement(); final String XALAN_NS = Constants.S_BUILTIN_EXTENSIONS_URL; final String XALAN_NS_PREFIX = "xalan"; - docEl.setAttribute("xmlns:" + XALAN_NS_PREFIX, XALAN_NS); final Set excludePrefixes = new HashSet(); if (docEl.hasAttribute("exclude-result-prefixes")) { excludePrefixes.addAll(Arrays.asList(docEl.getAttribute("exclude-result-prefixes").split(" "))); } - excludePrefixes.add(XALAN_NS_PREFIX); final List result = new LinkedList(); for (QName ns : methods.keySet()) { + final String prefix = ns.getLocalName(); docEl.setAttribute("xmlns:" + prefix, ns.getNamespaceURI()); excludePrefixes.add(prefix); @@ -365,6 +365,11 @@ public class XSLTProcessor extends BaseProcessor implements TemplateProcessor } docEl.setAttribute("exclude-result-prefixes", StringUtils.join(excludePrefixes .toArray(new String[excludePrefixes.size()]), " ")); + if (log.isDebugEnabled()) { + StringWriter writer = new StringWriter(); + XMLUtil.print(xslTemplate, writer); + log.debug(writer); + } return result; } diff --git a/source/java/org/alfresco/repo/template/XSLTProcessorTest.java b/source/java/org/alfresco/repo/template/XSLTProcessorTest.java index 21c9413a66..e7a0d18444 100644 --- a/source/java/org/alfresco/repo/template/XSLTProcessorTest.java +++ b/source/java/org/alfresco/repo/template/XSLTProcessorTest.java @@ -30,6 +30,7 @@ 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.cmr.repository.TemplateException; import org.alfresco.service.cmr.repository.TemplateProcessor; import org.alfresco.service.cmr.repository.TemplateService; import org.alfresco.service.cmr.search.ResultSet; @@ -75,6 +76,30 @@ public class XSLTProcessorTest extends BaseAlfrescoSpringTest "/app:company_home"); this.companyHome = rs.getNodeRef(0); } + + public void testSecurityFilter() throws Exception + { + try + { + FileInfo file = createXmlFile(companyHome); + XSLTemplateModel model = new XSLTemplateModel(); + model.put(XSLTProcessor.ROOT_NAMESPACE, XMLUtil.parse(file.getNodeRef(), contentService)); + + StringWriter writer = new StringWriter(); + xsltProcessor.processString(insecureVerySimpleXSLT, model, writer); + log.error("This insecure template should not process!"); + fail(); + } + catch (TemplateException e) + { + //pass! + } + catch (Exception ex) + { + log.error("Error!", ex); + fail(); + } + } public void testSimplestStringTemplate() throws Exception { @@ -267,4 +292,14 @@ public class XSLTProcessorTest extends BaseAlfrescoSpringTest "" + "" + "" + "" + "" + ""; + + private String insecureVerySimpleXSLT = "" + " " + + "xmlns:fn=\"http://www.w3.org/2005/02/xpath-functions\"> " + "" + + + "" + + + "" + "" + + "" + "" + "" + ""; } diff --git a/source/java/org/alfresco/util/XMLUtil.java b/source/java/org/alfresco/util/XMLUtil.java index 683e356805..7738c8b166 100644 --- a/source/java/org/alfresco/util/XMLUtil.java +++ b/source/java/org/alfresco/util/XMLUtil.java @@ -19,7 +19,18 @@ package org.alfresco.util; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.CharArrayReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.LinkedList; +import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -28,8 +39,12 @@ import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMResult; import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TransformerHandler; import javax.xml.transform.stream.StreamResult; + import org.alfresco.model.ContentModel; import org.alfresco.service.cmr.avm.AVMService; import org.alfresco.service.cmr.repository.ContentReader; @@ -37,9 +52,19 @@ import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.NodeRef; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.w3c.dom.*; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.Text; +import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import org.xml.sax.XMLFilter; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLFilterImpl; +import org.xml.sax.helpers.XMLReaderFactory; /** * XML utility functions. @@ -114,75 +139,229 @@ public class XMLUtil } /** utility function for parsing xml */ - public static Document parse(final String source) + public static Document parse(final String source, final XMLFilter... filters) throws SAXException, IOException { - return XMLUtil.parse(new ByteArrayInputStream(source.getBytes("UTF-8"))); + return XMLUtil.parse(new CharArrayReader(source.toCharArray()), filters); + } + + public static Document secureParseXSL (final String source, final XMLFilter... filters) + throws SAXException, + IOException + { + return parse(new CharArrayReader(source.toCharArray()), addSecurityFilter(filters)); } /** utility function for parsing xml */ public static Document parse(final NodeRef nodeRef, - final ContentService contentService) + final ContentService contentService, + final XMLFilter... filters) throws SAXException, IOException { final ContentReader contentReader = contentService.getReader(nodeRef, ContentModel.TYPE_CONTENT); final InputStream in = contentReader.getContentInputStream(); - return XMLUtil.parse(in); + return XMLUtil.parse(in, filters); } + + public static Document secureParseXSL(final NodeRef nodeRef, + final ContentService contentService, + final XMLFilter... filters) + throws SAXException, + IOException + { + final ContentReader contentReader = + contentService.getReader(nodeRef, ContentModel.TYPE_CONTENT); + final InputStream in = contentReader.getContentInputStream(); + return parse(in, addSecurityFilter(filters)); + } /** utility function for parsing xml */ public static Document parse(final int version, final String path, - final AVMService avmService) + final AVMService avmService, + final XMLFilter...filters) throws SAXException, IOException { - return XMLUtil.parse(avmService.getFileInputStream(version, path)); + return XMLUtil.parse(avmService.getFileInputStream(version, path), filters); + } + + public static Document secureParseXSL(final int version, + final String path, + final AVMService avmService, + final XMLFilter... filters) + throws SAXException, + IOException + { + return parse(avmService.getFileInputStream(version, path), addSecurityFilter(filters)); } /** utility function for parsing xml */ - public static Document parse(final File source) + public static Document parse(final File source, + final XMLFilter... filters) throws SAXException, IOException { - return XMLUtil.parse(new FileInputStream(source)); + return XMLUtil.parse(new FileInputStream(source), filters); + } + + public static Document secureParseXSL(final File source, + final XMLFilter... filters) + throws SAXException, + IOException + { + return parse(new FileInputStream(source), addSecurityFilter(filters)); + } + + private static Document parseWithXMLFilters(final InputSource source, + final XMLFilter... filters) + throws SAXException, + IOException + { + return parseWithXMLFilters(source, false, filters); + } + + private static Document parseWithXMLFilters(final InputSource source, + final boolean validating, + final XMLFilter... filters) + throws SAXException, + IOException + { + TransformerFactory tf = TransformerFactory.newInstance(); + // Check to make sure this is a SAX TransformerFactory + if (!tf.getFeature(SAXTransformerFactory.FEATURE)) + { + throw new SAXException("SAX Transformation factory not found."); + } + // Cast to appropriate factory class + SAXTransformerFactory stf = (SAXTransformerFactory) tf; + final DocumentBuilder db = XMLUtil.getDocumentBuilder(true, validating); + + if (filters == null || filters.length == 0) + { + // No filters. Process this as normal. + return db.parse(source); + } + else + { + // Process with filters + try + { + final Document doc = db.newDocument(); + final TransformerHandler th = stf.newTransformerHandler(); + // Specify transformation to DOMResult with empty Node container (Document) + th.setResult(new DOMResult(doc)); + XMLReader reader = XMLReaderFactory.createXMLReader(); + + //emulate what the document builder parser supports + //all readers are required to support namespaces and namespace-prefixes + reader.setFeature("http://xml.org/sax/features/namespaces", db.isNamespaceAware()); + reader.setFeature("http://xml.org/sax/features/namespace-prefixes", db.isNamespaceAware() ? true : false); + + // Chain multiple filters together + int i = 0; + XMLFilter filter = null; + for (XMLFilter f : filters) + { + // there can be no null in the filter list + if (f == null) + throw new SAXException("Nulls are not allowed in XML filter list."); + // if first item then set new reader + if (i == 0) + f.setParent(reader); + else + // set parent filter to previous element in the array + f.setParent(filters[i - 1]); + + filter = f; + i++; + } + //not sure how filter could be null + if (filter != null) + { + filter.setContentHandler(th); + filter.parse(source); + try + { + //try to activate/deactivate validation + filter.setFeature("http://xml.org/sax/features/validation", db.isValidating()); + } + catch (SAXException se) + { + LOGGER.warn("XML reader does not support validation feature.", se); + } + } + else + { + //not sure how we could get here + throw new SAXException("No XML filters available to process this request."); + } + if (LOGGER.isDebugEnabled()) { + StringWriter writer = new StringWriter(); + XMLUtil.print(doc, writer); + LOGGER.debug(writer); + } + return doc; + } + catch (TransformerException tce) + { + throw new SAXException(tce); + } + } } /** utility function for parsing xml */ - public static Document parse(final InputStream source) + public static Document parse(final InputStream source, final XMLFilter... filters) throws SAXException, IOException { try { - final DocumentBuilder db = XMLUtil.getDocumentBuilder(); - return db.parse(source); + return parseWithXMLFilters(new InputSource(source), filters); } finally { source.close(); } } + + /** secure parse for InputStream source */ + public static Document secureParseXSL(final InputStream source, + final XMLFilter... filters) + throws SAXException, + IOException + { + return parse(source, addSecurityFilter(filters)); + } /** utility function for parsing xml */ - public static Document parse(final Reader source) + public static Document parse(final Reader source, + final XMLFilter... filters) throws SAXException, IOException { try { - final DocumentBuilder db = XMLUtil.getDocumentBuilder(); - return db.parse(new InputSource(source)); + return parseWithXMLFilters(new InputSource(source), filters); } finally { source.close(); } } - + + /** secure parse for Reader source **/ + public static Document secureParseXSL(final Reader source, + final XMLFilter... filters) + throws SAXException, + IOException + { + return parse(source, addSecurityFilter(filters)); + } + /** provides a document builder that is namespace aware but not validating by default */ public static DocumentBuilder getDocumentBuilder() { @@ -293,4 +472,76 @@ public class XMLUtil } }; } + + /** + * returns a new array of filters with the security filter at the head of the array + */ + private static XMLFilter[] addSecurityFilter(XMLFilter...filters) { + if (filters == null || filters.length == 0) { + return new XMLFilter[] {new FastFailSecureXMLFilter()}; + } else { + XMLFilter[] xmlfilters = new XMLFilter[filters.length + 1]; + xmlfilters[0] = new FastFailSecureXMLFilter(); + System.arraycopy(filters, 0, xmlfilters, 1, filters.length); + return xmlfilters; + } + } + + /** + * XMLFilter that throws an exception when it comes across any insecure namespaces + */ + private static class FastFailSecureXMLFilter extends XMLFilterImpl + { + + private static final List insecureURIs = new LinkedList() + { + private static final long serialVersionUID = 1L; + + { + add("xalan://"); + add("http://xml.apache.org/xalan/java"); + add("http://xml.apache.org/xslt/java"); + add("http://xml.apache.org/java"); + } + }; + + public FastFailSecureXMLFilter() + { + }; + + public void startPrefixMapping(String prefix, String uri) + throws SAXException + { + if (isInsecureURI(uri)) + { + throw new SAXException("Insecure namespace: " + uri); + } + super.startPrefixMapping(prefix, uri); + } + + + public void startElement (String uri, + String localName, + String qName, + final Attributes atts) + throws SAXException + { + + if (isInsecureURI(uri)) + { + throw new SAXException("Insecure namespace: " + uri); + } + super.startElement(uri, localName, qName, atts); + } + + private boolean isInsecureURI(String uri) + { + for (String insecureURI : insecureURIs) + { + if (uri.startsWith(insecureURI)) return true; + } + return false; + } + + } }