Wednesday, April 18, 2018

Alfresco Share: Build New Share Pages with Familiar Tools

Standard Alfresco Share is based on standard HTML, CSS and Javascript, but it also uses a number of frameworks and tools, like Alfresco Surf, Alfresco Aikau, YUI2 JS framework, the Dojo Toolkit and Freemarker. Knowing about Java and the Spring Framework will help too.  All those different technologies could be a barrier to getting started with Share development.

Granted, if you build new pages in Share, you'll probably want to reuse standard Share components, like the header, footer, menus, etc.  In that case it makes sense to stick with using Share technologies.

But you might be interested in knowing that you can build and wire in a web page that interoperates with other pages in Alfresco Share using totally different technologies.

Let's look at what is needed to build a page that works in Alfresco Share but which is built using JSP, for example.  We can't totally escape the Surf configuration.  But we only need three files to define the Share page: page, template-instance and template-type. The files are relatively small.

<alfresco>/tomcat/shared/classes/alfresco/site-data/pages/sharejsp.xml

<?xml version='1.0' encoding='UTF-8'?>
<page>
   <title>Share JSP Page</title>
   <description>Share JSP Page</description>
   <template-instance>sharejsp</template-instance>
   <authentication>user</authentication>
</page>

<alfresco>/tomcat/shared/classes/alfresco/site-data/template-instances/sharejsp.xml

<?xml version='1.0' encoding='UTF-8'?>
<template-instance>
   <template-type>sharejsp</template-type>
</template-instance>

<alfresco>/tomcat/shared/classes/alfresco/site-data/template-types/sharejsp.xml

<?xml version="1.0" encoding="UTF-8"?>
<template-type>
    <title>Share JSP Page</title>
 <description>Custom page for Alfresco Share</description>
 <processor mode="view">
  <id>jsp</id>
  <jsp-path>/jsp/sharepage.jsp</jsp-path>
 </processor>
</template-type>

That's it!  Now let's define the content for the JSP page.  The JSP file needs to go within the Share expanded WAR area.  The location and file is defined by the template-type definition.

<alfresco>/tomcat/webapps/share/jsp/sharepage.jsp

<%--  Custom Alfresco Share JSP Page Example --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 
<%@ page import="org.alfresco.web.site.*" %>

<%
    String ShareHome = "http://localhost:8080/share/page";

    // Retrieve user name from the session
    String userid = (String)session.getAttribute(SlingshotUserFactory.SESSION_ATTRIBUTE_KEY_USER_ID);
 
    out.println("<center><h1>" + userid + "!  Welcome to a Custom JSP Page</h1></center>"); 
    out.println("<center><img src=\"https://svn.alfresco.com/repos/alfresco-open-mirror/alfresco/HEAD/root/projects/web-client/source/web/images/logo/alfresco3d.jpg\" /></center>"); 
 
    // Set up a button to navigate to another authenticated Share page
    out.println("<center><form>");
    out.println("<input type=\"button\" value=\"Return to Share Home\" onclick=\"window.location.href='" + ShareHome + "'\" />");
    out.println("</form></center>");  
%>

And finally, here is the rendered custom JSP page within Share:


















The new page obeys Share authentication.  For example, if you enter the URL to the page when you're not logged into Share, you will be first redirected to the standard Share login page.

On the client page in the browser, by using the proxy URL to the Alfresco repo, calls can be made to the Alfresco repo REST API with Javascript/AJAX.  Within the JSP page on the server, Java HTTP requests calls can be made to the Alfresco repo, just remember to pass along the JSESSIONID cookie from the page as part of your request to enable authentication.

Tuesday, April 3, 2018

Alfresco - How to create a custom Admin Console Component


If your extension module needs an Administration interface, you can add a custom Admin Console Component to manage your module. The Admin Console is composed of default administration pages. Each Admin Console page is a simple web script component built from a library of useful functions and macros that are imported into each Admin Console web script.

The custom Admin Console component can be added by implementing a repository web script. In this example we will add a custom component under a new section called “Formtek”. The “General Information” custom page displays the Formtek Root Directory Path and the Formtek Modules installed.



To have the new component listed under the Formtek section we will have to store the web script files in a specific directory path - org/alfresco/enterprise/repository/admin/zformtek.

The web script descriptor file shown below automatically adds this custom component to the Admin Console. In the web script descriptor file the url, family and the authentication tag values are very important. The Admin Console is available only in the Alfresco Enterprise edition and only an Administrator can access it so the url has to begin with /enterprise/admin, the custom component needs to belong to the AdminConsole family and the authentication has to be set to admin.




In the web script controller, we have to inform the Admin Console system which MBean and its properties we want to use in our custom component. In this custom component we are using the ModuleService MBean to get the list of installed modules.
The web script template contains the page layout. For this custom component it loops through the list of installed modules and displays only the Formtek modules. The template uses a number of resource properties that are fetched using the msg function.




The resource properties should be defined in a web script resource file.





Tuesday, March 13, 2018

What Happened to My Alfresco Simple Modules?

Alfresco supports two methods for packaging Repository and Share extensions:

  • Alfresco Module Package (AMP)
  • Simple Module

The Simple Module method makes use of JAR files and has the advantage of not having to modify the alfresco.war and share.war files as is required by the AMP method.

The Formtek Extensions for Alfresco (Auditing, File Linking, Peer Association, and Version Browser) are all installed as Simple Modules and each contain Repository and Share extensions. The Repository extensions are bundled in a JAR file that is placed in the modules/platform directory, where as the JAR file with the Share extensions is placed in the modules/share directory.

When Alfresco starts up, it checks for JAR files in these two directories to see if there are any modules to load. If a module is found, it is loaded and subjected to any required checks, such as its dependent Platform and Share versions. If it passes these checks, the module is started.

So far, so good. Well, most of the time. On multiple occasions when I restarted my Alfresco installation, my Simple Modules failed to load even though they had not changed since the last successful startup. For me, it seemed the Repository extensions were not being found. It is was as if the Alfresco startup process wasn't even looking in the modules/platform directory anymore. The first time this happened I figured I had done something wrong and messed up my Alfresco installation. But when it happened a third and fourth time, I began my investigation.

I figured some Tomcat-related file in my installation is telling (or should be telling) Alfresco to look in the modules/platform directory on startup for JAR files to load. I eventually landed in the tomcat/conf/Catalina/localhost directory and discovered it only contained two files: share.xml and solr4.xml. When I compared this to another installation I had that was working fine, I found that I was missing the alfresco.xml file.

Indeed, it is the alfresco.xml and share.xml files that tell Alfresco where to look for Simple Modules as follows:

alfresco.xml
<?xml version='1.0' encoding='utf-8'?>
<Context crossContext="true">
  <Loader className="org.apache.catalina.loader.VirtualWebappLoader" virtualClasspath="${catalina.base}/../modules/platform/*.jar" />
</Context>

share.xml
<?xml version='1.0' encoding='utf-8'?>
<Context crossContext="true">
  <Loader className="org.apache.catalina.loader.VirtualWebappLoader" virtualClasspath="${catalina.base}/../modules/share/*.jar" />
</Context>

Once I recreated the missing alfresco.xml file and restarted Alfresco, my Simple Module Repository extensions loaded and started just fine. What I have yet to determine, however, is why the file was missing in the first place? But, at least I now know how to fix it should it reoccur.

NOTE: If you've deviated from the traditional Alfresco installation and placed your modules directory elsewhere, it is worth noting that you will need to edit the virtualClasspath location in these two files to match your installation.

Thursday, February 8, 2018

Best Practices for Managing User Import into Alfresco from Active Directory


Alfresco has a built-in user authentication and directory service but it can also make use of Active Directory to not only handle authentication but also handle user and group memberships. In order for this to work though, you will need to add LDAP-AD as an authentication instance in the authentication.chain setting and enable the synchronization subsystem. There are a number of settings that need to be modified in order to do this but before doing so, one needs to think out how they are going to use Active Directory to manage this.

Often when I help an Alfresco administrator set up synchronization the admin has settings that are very generic like these:


ldap.synchronization.groupSearchBase=dc\=someco,dc\=com
ldap.synchronization.userSearchBase=dc\=someco,dc\=com

ldap.synchronization.groupQuery=objectclass\=group
ldap.synchronization.personQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512))

ldap.synchronization.personDifferentialQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(!(modifyTimestamp<\={0})))
ldap.synchronization.groupDifferentialQuery=(&(objectclass\=group)(!modifyTimestamp<\={0}))


Technically speaking, these queries are valid but the end result is that setting it like this will import users and groups from a very plain perspective and could make user management more difficult than it has to be. What these queries and settings will do is this:

The search for groups and users will take place in the someco.com domain which is good.


ldap.synchronization.groupSearchBase=dc\=someco,dc\=com
ldap.synchronization.userSearchBase=dc\=someco,dc\=com


Based on these two queries however, Alfresco will import ALL users and ALL groups inside of someco.com.


ldap.synchronization.groupQuery=objectclass\=group
ldap.synchronization.personQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512))


It's very likely that this is not what you want.

Of course, a differential sync will import the same set of users (but only those that have changed since the last import):


ldap.synchronization.personDifferentialQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(!(modifyTimestamp<\={0})))
ldap.synchronization.groupDifferentialQuery=(&(objectclass\=group)(!modifyTimestamp<\={0}))


Now, you may have a use-case that requires all of your users in Active Directory. It's possible that you have a small company but be aware that this method does not encourage scalability and allow for filtering out those users in your company who will not need to use Alfresco at all.

Here is what I recommend instead. The best practice is to create an Alfresco organizational unit and then create a few Alfresco groups in this OU. For example:

Create an Alfresco organizational unit with a distinguished name:


OU=Alfresco,DC=someco,DC=com


Then you can create any Alfresco specific groups within the Alfresco OU:


CN=AlfrescoAdmins,OU=Alfresco,DC=someco,DC=com
CN=AlfrescoUsers,OU=Alfresco,DC=someco,DC=com


Once these groups are set up in Active Directory, you can assign your users to them. For my testing purposes I created 3 AD users (aduser1, aduser2, aduser3) and then made aduser1 a member of AlfrescoAdmins and then all three, members of the AlfrescoUsers group. Now we can change our queries so that only these users in these two groups are imported into Alfresco:


ldap.synchronization.userSearchBase=ou\=alfresco,dc\=someco,dc\=com
ldap.synchronization.groupSearchBase=ou\=alfresco,dc\=someco,dc\=com


Also, because we have now set our groupSearchBase to only look in the Alfresco OU, our groupQuery will be very simple:


ldap.synchronization.groupQuery=objectclass\=group


If we use the personQuery setting below, it will only import users who are members of AlfrescoAdmins and AlfrescoUsers and not every single user in Active Directory:


ldap.synchronization.personQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(|(memberOf=cn\=AlfrescoAdmins,ou=alfresco,dc=someco,dc=com)(memberOf=cn\=AlfrescoUsers,ou=alfresco,dc=someco,dc=com)))


Next of course, for differential imports, we’ll use the same queries except add the modifiedTimestamp directive at the end to ensure we only pick up changes to our users since our last import:


ldap.synchronization.personDifferentialQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(|(memberOf=cn\=AlfrescoAdmins,ou=alfresco,dc=someco,dc=com)(memberOf=cn\=AlfrescoUsers,ou=alfresco,dc=someco,dc=com))(!(modifyTimestamp<\={0})))

ldap.synchronization.groupDifferentialQuery=(&(objectclass\=group)(!modifyTimestamp<\={0}))


To tie it all together, at a glance, here are the settings I use for testing Active Directory:


### AD authentication only ###
authentication.chain=alfrescoNtlm1:alfrescoNtlm,ldap-ad1:ldap-ad
ldap.authentication.active=true
ldap.authentication.allowGuestLogin=true
ldap.authentication.userNameFormat=%s@someco.com
ldap.authentication.java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory
ldap.authentication.java.naming.provider.url=ldap://someco.com:389 # This points to your Active Directory server - IP Address is ok to use here as well!
ldap.authentication.java.naming.security.authentication=simple
ldap.authentication.escapeCommasInBind=false
ldap.authentication.escapeCommasInUid=false
ldap.authentication.defaultAdministratorUserNames=Administrator

ldap.synchronization.active=true
ldap.synchronization.java.naming.security.principal=administrator@someco.com
ldap.synchronization.java.naming.security.credentials=Alfr3sc0
ldap.synchronization.queryBatchSize=1000
ldap.synchronization.attributeBatchSize=1000
synchronization.synchronizeChangesOnly=false
synchronization.allowDeletions=true
synchronization.syncWhenMissingPeopleLogIn=true

ldap.synchronization.groupQuery=objectclass\=group
ldap.synchronization.groupDifferentialQuery=(&(objectclass\=group)(!(modifyTimestamp<\={0})))

ldap.synchronization.personQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(|(memberOf=cn\=AlfrescoAdmins,ou=alfresco,dc=someco,dc=com)(memberOf=cn\=AlfrescoUsers,ou=alfresco,dc=someco,dc=com)))

ldap.synchronization.personDifferentialQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(|(memberOf=cn\=AlfrescoAdmins,ou=alfresco,dc=someco,dc=com)(memberOf=cn\=AlfrescoUsers,ou=alfresco,dc=someco,dc=com))(!(modifyTimestamp<\={0})))

ldap.synchronization.groupSearchBase=ou\=alfresco,dc\=someco,dc\=com

ldap.synchronization.userSearchBase=dc\=someco,dc\=com

ldap.synchronization.modifyTimestampAttributeName=modifyTimestamp
ldap.synchronization.timestampFormat=yyyyMMddHHmmss'.0Z'
ldap.synchronization.userIdAttributeName=sAMAccountName
ldap.synchronization.userFirstNameAttributeName=givenName
ldap.synchronization.userLastNameAttributeName=sn
ldap.synchronization.userEmailAttributeName=mail
ldap.synchronization.userOrganizationalIdAttributeName=company
ldap.synchronization.defaultHomeFolderProvider=largeHomeFolderProvider
ldap.synchronization.groupIdAttributeName=cn
ldap.synchronization.groupDisplayNameAttributeName=displayName
ldap.synchronization.groupType=group
ldap.synchronization.personType=user
ldap.synchronization.groupMemberAttributeName=member
ldap.synchronization.enableProgressEstimation=true


Now, when you run the user synchronization, all groups will be imported and all users will show up in their expected groups. Most importantly, you will now only have those users imported from Active Directory who should have access to your Alfresco environment. This will make user management in Alfresco much simpler.

Tuesday, January 23, 2018

How to Change the Alfresco Version Label Format

Document versions in Alfresco are stored as a sequential history of snapshots of the state of the document made over the life of the document.  Each document version is a snapshot of the metadata and file content at the time when the version was created.

Only documents that have the versionable aspect are versioned.  New content uploaded to a document without the versionable aspect will simply be overwritten. In Alfresco Share, by default, all documents are automatically assigned the versionable aspect, so all content will be versioned.

Default version labels in Alfresco are numeric and consist of a major component, followed by the minor component, separated by a period, like "1.0" (major version is 1 and minor version is 0), and "2.5" (major version is 2 and minor version is 5). Also, by default in Share, newly uploaded content will start at version "1.0".

This format ([major].[minor]) for writing and referring to versions is somewhat hardwired into the Alfresco Share interface, but versions don't need to be stored this way in the Alfresco repository.  It is possible to format the version label in any way.

Alfresco provides a "behaviour" called calculateVersionLabel as an extension point where developers are able to wire in a new method for determining the version label.

But note that if you plan to use Alfresco Share as your main interface with the Alfresco repository, it may not be a good idea to change the format for version labels because doing that may cause problems with some assumptions that were made in the Share UI display.  Besides just changing the version label format, there would need to be additional customizations in Share to ensure that the new format for version labels are displayed and handled correctly.

To demonstrate how to change the method for calculating version labels, we will create two new files: a Javascript file that handles the calculation of the version label, and a file to wire in the configuration of this new behaviour.

Let's look first at the Javascript for the behaviour.  Alfresco behaviour code is often written in Java, but here we will just use server-side Javascript.

In this example, we will change the version format so that it still consists of a minor and major component, but instead, will be separated by an underscore.  So, instead of "1.0", the version will be "1_0", for example.

First create the Javascript file: <Tomcat>/shared/classes/alfresco/scripts/onCalculateVersionLabel.js. It contains the following content:

 // Calculate the Version Label  
 // Have a look at the behaviour object that should have been passed  
 if (behaviour == null)   
 {  
   logger.log("The behaviour object has not been set.");  
   scriptFailed = true;  
 }  
 // Check the name of the behaviour  
 if (behaviour.name == null && behaviour.name != "calculateVersionLabel")   
 {  
   logger.log("The behaviour name has not been set correctly.");  
   scriptFailed = true;  
 }   
 else   
 {  
   logger.log("Behaviour name: " + behaviour.name);  
 }  
 // Check the arguments  
 if (behaviour.args == null)   
 {  
   logger.log("The args have not been set.");  
   scriptFailed = true;  
 }   
 else   
 {  
   if (behaviour.args.length == 4)   
   {  
           // Set initial version to 0  
     var currentVersion = (behaviour.args[1]==null)? "0_0" : behaviour.args[1].getVersionLabel();  
           var versionType = behaviour.args[3].versionType;  
     var num = currentVersion.split('_');  
           if(num.length!=2)  
           {  
             scriptFailed = true;  
           }  
           else  
           {  
                var major = parseInt(num[0]);  
                var minor = parseInt(num[1]);  
                if(versionType.toString().equals("MINOR")) currentVersion = major + "_" + (minor + 1);  
                else if(versionType.toString().equals("MAJOR")) currentVersion = (major + 1) + "_0";  
           }  
           currentVersion;  
   }   
   else   
   {  
     logger.log("The number of arguments is incorrect.");  
     scriptFailed = true;  
   }    
 }  

A behaviour object is passed into the Javascript code that contains the parameters of the behaviour.  behaviour.args[1] contains the version object, and behaviour.args[3] is an object with parameters that specify how to construct the new version.

The current version label is stored as behaviour.args[1].getVersionLabel().  The version label will be null for new documents.

The type of versioning, for example, whether to create a new minor or major version, is specified by the value of behaviour.args[3].versionType.

Note that the last line executed in the Javascript code is the return value for the behaviour.  In this case, that value represents the new version label, which is stored in the variable currentVersion.

The Javascript code can then be wired into the behaviour with the following Spring context file.  Place this file in the following location: <Tomcat>/shared/classes/alfresco/extension/version-context.xml.  The configuration calls out the location of the Javascript file to be used for the Alfresco policy calculateVersionLabel.  Two beans are defined in the file.  The first overwrites and undefines the standard method for handling the calculation of version labels, and the second bean defines the new bean for version label calculation.

 <?xml version='1.0' encoding='UTF-8'?>  
 <!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>  
 <beans>  
   <bean id="registerContentWithVersionService" class="org.alfresco.repo.version.VersionServiceVersionLabelRegistrationBean"></bean>  
   <!-- Define a custom behavior on Standard Content -->  
   <bean id="customOnVersionLabelChange" class="org.alfresco.repo.policy.registration.ClassPolicyRegistration" parent="policyRegistration" >  
     <property name="policyName">  
       <value>{http://www.alfresco.org}calculateVersionLabel</value>  
     </property>  
     <property name="className">  
       <value>{http://www.alfresco.org/model/content/1.0}content</value>  
     </property>  
     <property name="behaviour">  
       <bean class="org.alfresco.repo.jscript.ScriptBehaviour" parent="scriptBehaviour">  
         <property name="location">  
           <bean class="org.alfresco.repo.jscript.ClasspathScriptLocation">  
             <constructor-arg>  
               <value>alfresco/scripts/onCalculateVersionLabel.js</value>  
             </constructor-arg>  
           </bean>  
         </property>  
       </bean>  
     </property>  
   </bean>  
 </beans>  

Now, when we create new versions, the new format for the version label will be used. Only newly created versions will use the new method for calculating the version label.

For example, here is the banner on the details page for version 1_3 of a document:








And here is how the version history for that document is displayed on the details page:



There are some problems in Share though.  There are places in Share that assume the version label format.  For example, the following code snippet from the file dnd-upload.js is used when the upload dialog pops up with checkboxes to select the major or minor version.  There is code here that tries to parse the current version label by using the split() method with a "." delimiter to break apart the minor and major components of the label.  This code will have problems when it encounters the new version label in a format like "1_3".



For example, we'll see the following:



Note the "(1.NaN)" which appears for the minor change.  There are likely other problems that will occur within Share because of the change of the version label format too.

But despite those problems, if your application has a need for reformatting version labels, you might consider redefining the method used to calculate the labels.