Showing posts with label Alfresco. Show all posts
Showing posts with label Alfresco. Show all posts

Friday, August 17, 2018

How to Increase the Timeout for Generating the Alfresco PDF Preview


The Formtek EDM Module for Alfresco provides custom transformers to convert several CAD file formats (such as AutoCAD DWG) to PDF for previewing in Alfresco Share. The default time allowed for this transformation to complete is 120,000 milliseconds or 2 minutes. For most drawings, this is more than enough time to create the preview PDF. However, some complex drawings may need a little more time. You can change the timeout period on the transformer itself, but there are a few other timeout settings you'll to change as well to make it all work properly.

Here are the steps for increasing the time allowed for transforming DWG to PDF using the custom transformer provided by the Formek EDM Module. In this example, the time is being increased to 3.5 minutes (210,000 ms):

1) Add the following properties to the alfresco-global.properties file to change both the specific transformer timeout and the system-wide timeout and error time values:

# ==========================================================================
# DWG to PDF transformer timeout defaults to 180000 ms in Formtek EDM Module 
# 3.1.0.4 and 120000 ms in previous releases
# ==========================================================================
content.transformer.dwg2pdf1.extensions.dwg.pdf.timeoutMs=210000

# =======================================================
# System-wide timeout and error time default to 120000 ms
# =======================================================
content.transformer.default.timeoutMS=210000
content.transformer.default.errorTime=210000

Although these settings (after an Alfresco restart) do provide a DWG file more time to complete its transformation, the following odd behavior is observed:

  • If the transformation continues beyond 2 minutes, a second transformation attempt is surprisingly triggered. Sometimes, but not always, that second attempt is started before the first one finishes.
  • So now there may be two transformation processes running at the same time for the same file, and competing for same system resources until the first transformation completes.
  • Furthermore, the "busy" indicator in the Alfresco Share preview window does not go away until the second 2-minute period (or a total of 4 minutes) elapses, thus making you wait longer than necessary to see the PDF preview.
  • But wait. Instead of loading the newly generated PDF preview, the following message is displayed leading you to believe that the transformation attempt may have failed:
  • A manual window refresh does load the PDF preview (once the second transformation attempt completes), but that's a not-so-obvious detail.

Here's an example of the two transformation attempts for doc2.dwg when the logger property, org.alfresco.repo.content.transform.TransformerDebug, is set to DEBUG. In this example, the second attempts starts one second after the first one completes:

2018-08-14 09:38:39,171 DEBUG [org.alfresco.repo.content.transform.TransformerDebug] [pool-13-thread-2] 11             dwg  pdf  doc2.dwg 2.5 MB -- pdf -- ContentService.transform(...)
2018-08-14 09:38:39,177 DEBUG [org.alfresco.repo.content.transform.TransformerDebug] [pool-13-thread-2] 11             workspace://SpacesStore/28a8aa18-9681-417e-83ec-0fa8de0efbd1
2018-08-14 09:38:39,178 DEBUG [org.alfresco.repo.content.transform.TransformerDebug] [pool-13-thread-2] 11             **a)  [50] dwg2pdf1<<Runtime>>           140,966 ms
2018-08-14 09:38:39,178 DEBUG [org.alfresco.repo.content.transform.TransformerDebug] [pool-13-thread-2] 11.1           dwg  pdf  doc2.dwg 2.5 MB dwg2pdf1<<Runtime>>
2018-08-14 09:41:02,873 DEBUG [org.alfresco.repo.content.transform.TransformerDebug] [pool-13-thread-2] 11             Finished in 143,702 ms

2018-08-14 09:41:03,455 DEBUG [org.alfresco.repo.content.transform.TransformerDebug] [pool-13-thread-2] 12             dwg  pdf  doc2.dwg 2.5 MB -- pdf -- ContentService.transform(...)
2018-08-14 09:41:03,455 DEBUG [org.alfresco.repo.content.transform.TransformerDebug] [pool-13-thread-2] 12             workspace://SpacesStore/28a8aa18-9681-417e-83ec-0fa8de0efbd1
2018-08-14 09:41:03,455 DEBUG [org.alfresco.repo.content.transform.TransformerDebug] [pool-13-thread-2] 12             **a)  [50] dwg2pdf1<<Runtime>>           141,512 ms
2018-08-14 09:41:03,455 DEBUG [org.alfresco.repo.content.transform.TransformerDebug] [pool-13-thread-2] 12.1           dwg  pdf  doc2.dwg 2.5 MB dwg2pdf1<<Runtime>>
2018-08-14 09:43:22,587 DEBUG [org.alfresco.repo.content.transform.TransformerDebug] [pool-13-thread-2] 12             Finished in 139,133 ms

Even though the first transformation attempt completed in about 2 minutes 23 seconds (143,702 ms), I don't see the PDF preview in Share for about 4 minutes 43 seconds (after the second attempt completes). 

To avoid this second transformation attempt, you need to change the read timeout for the HTTP socket connection, which defaults to 120,000 ms. So, in addition to Step 1 above, you'll also need to complete Step 2 as follows:

2) Create the tomcat\shared\classes\alfresco\web-extension\custom-slingshot-application-context.xml file with the following content, setting the readTimeout value accordingly (in my case, 210,000 ms).

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xmlns:context="http://www.springframework.org/schema/context" 
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
          http://www.springframework.org/schema/beans/spring-beans-2.5.xsd 
          http://www.springframework.org/schema/context 
          http://www.springframework.org/schema/context/spring-context-2.5.xsd"> 
  <bean id="connector.remoteclient" parent="connector.remoteclient.abstract" 
  class="org.alfresco.web.scripts.SlingshotRemoteClient" scope="prototype" > 
<!-- the http.connection.timeout value in milliseconds to apply to HTTP connections --> 
    <property name="connectTimeout"><value>10000</value></property> 
<!-- the http.socket.timeout value in milliseconds to apply to HTTP connections --> 
    <property name="readTimeout"><value>210000</value></property> 
  </bean> 
</beans>

3) Finally, restart Alfresco so the changes take effect.

Now, when you preview a newly upload drawing that takes more than 2 minutes to convert to PDF, only a single transformation attempt occurs and you will see the drawing preview sooner and without having to manually refresh the window.

Also note that this same configuration can be applied to other content transformers. However, you will need to configure the property that corresponds to that specific transformer in Step 1 above.

Tuesday, June 19, 2018

Alfresco: How to Overwrite Node Created Date

The Alfresco cm:created property is a protected field.  It normally can't be changed.
For example, the following simple JavaScript code tries to change the created date property to January 31, 2012.  It actually runs with no error, but when you check for the value of the property on the node during the same transaction, it appears to be set.

But query the property again or look at the value in the Node Browser and you'll find that it hasn't changed.

var dir = "Sites/swsdp/documentLibrary/Presentations/Project Overview.ppt";
var node = companyhome.childByNamePath(dir);

// Change the created date
var jan312012 = new Date(2012, 0, 31);
node.properties["cm:created"] = jan312012;
node.save();

Sometimes it's desirable to be able to change the created date of the node.  This is especially useful when content is being migrated from another source into Alfresco and the creation date from the other system needs to be preserved in Alfresco.

Using the Alfresco Java API, it's possible to work around the problem.  To do that, we'll create the following Java method which can be called from Alfresco JavaScript. It will allow us to run any JavaScript method as user System.  It's a dangerous method to have around since it can run with no restriction, so it would be good practice  to package it separately and use it only for situations that warrant its use.

Create and compile the Java file com/formtek/example/util/RunAsSystem.java:

package com.formtek.example.util;

import org.alfresco.repo.jscript.BaseScopableProcessorExtension;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.mozilla.javascript.Context;
import org.alfresco.model.ContentModel;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;

import org.alfresco.repo.policy.BehaviourFilter;

public class RunAsSystem extends BaseScopableProcessorExtension 
{
    protected BehaviourFilter behaviourFilter;
    
    public void setBehaviourFilter(BehaviourFilter behaviourFilter)
    {
      this.behaviourFilter = behaviourFilter;
    }

    public void exec(final Function func) {
        
        final Context cx = Context.getCurrentContext();
        final Scriptable scope = getScope();
 
        RunAsWork<Object> raw = new RunAsWork<Object>() {
            public Object doWork() throws Exception {
  behaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
                func.call(cx, scope, scope, new Object[] {});
                return null;
            }
        };
        AuthenticationUtil.runAs(raw, AuthenticationUtil.getSystemUserName());
    }   
}

Then wire this file into Alfresco with the file alfresco/extension/runas-context.xml:

<?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="com.formtek.RunAsSystem" parent="baseJavaScriptExtension" class="com.formtek.example.util.RunAsSystem">
      <property name="extensionName">
          <value>ftk</value>
      </property>
   <property name="behaviourFilter" ref="policyBehaviourFilter" />
    </bean>
</beans>

With that in place, it is possible then to write the following JavaScript:

function doit()
{
 // Find a test node
 var dir = "Sites/swsdp/documentLibrary/Presentations/Project Overview.ppt";
     var node = companyhome.childByNamePath(dir);
 // Change the created date
 var jan312009 = new Date(2009, 0, 31);
 node.properties["cm:created"] = jan312009;
 node.save();
}
ftk.exec(doit);

The method ftk.exec() will run any JavaScript method as user System.
In this example, it changes the creation dates on one of the standard sample documents that comes with Alfresco.

Wednesday, May 2, 2018

How to Connect to Alfresco with JConsole for JMX Access

The most straight-forward and basic way to make configuration changes in Alfresco is to stop Alfresco. Open alfresco-global.properties file (in tomcat/shared/classes sub-directory), make settings changes, save them and then start Alfresco. If you are making long-term changes to a production environment, this is the best way to go about doing that. However, before you can make these well-thought-out changes to production, it is a good idea to test them first in a test, staging or QA environment. But making changes to the configuration in global properties can be a bit time consuming. Were you aware that you can make most changes to Alfresco using JMX without a restart? Most Java applications do allow for this if they have enabled the JMX protocol.

The Java Management Extension (JMX) interface allows you to make changes to Alfresco Content Services through a JMX client that supports JMX Remoting (JSR-160). This client called JConsole allows you to:

  • Manage subsystems (includes many configuration setting changes)
  • Change log levels
  • Enable or disable file servers (FTP/CIFS)
  • Set the server to read-only mode
  • Set the server to single-user mode
  • Set server maximum user limit, including ability to prevent further logins
  • Count user sessions/tickets
  • User session/ticket invalidation

Setting up Alfresco for JMX connections

In order to allow for Alfresco to make use of JMX, we'll need to add a few settings to a Alfresco.

In alfresco-global.properties we add:

alfresco.jmx.connector.enabled=true
alfresco.rmi.services.port=50500
alfresco.rmi.services.host=<hostname>

For alfresco.rmi.services.host, make sure you are setting a hostname that is externally and internally resolvable. When you start Alfresco, you can make sure that JMX is enabled by running in Linux:

# sudo lsof -i :50500
COMMAND   PID USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
java    13248 root  552u  IPv6 13356213      0t0  TCP *:50500 (LISTEN)

You should of course, see a running process listening on 50500.

Running JConsole

If you happen to have GUI access on the server you are running Alfresco, you can run the following to start jconsole:

If you have a full JDK installed, you can typically run:

# jconsole

or

# java/bin/jconsole

If using Alfresco's JRE:

# java/bin/java -jar java/lib/jconsole.jar

If you can pull up jconsole, you can typically connect to Alfresco using the Local Process option. This won't require a long and complex URL or username/password. If you use the jconsole executable, you choose Local Process (make sure you select the running Tomcat process)



Next, you'll see a pop-up box showing insecure connection. Go ahead and click on the Insecure connection button.


Once it starts up, you'll see the start up window.



Click on the MBeans tab at the top and you should then see the Alfresco drop-down line.



If you have to use the Alfresco JRE, you will first need to add the JMX url and a username/password combo to get in.



You will need to add the following:


  • URL: service:jmx:rmi:///jndi/rmi://localhost:50500/alfresco/jmxrmi
  • Username: controlRole
  • Password: change_asap


Keep in mind that these are the default properties. It is possible that maybe an admin may have changed the username and passwords. In that case, you'll need to have a look through these files to get that information:


  • alfresco-jmxrmi.password
  • alfresco-jmxrmi.access


Keep in mind that this process will also allow you to connect to a remote Alfresco server provided you are on the same network.

Now, if your Alfresco install is in an AWS environment, you will need to follow a little bit of a different process. For reasons I don't fully understand, jconsole seems to have a difficult time making a direct connection to Alfresco running in AWS. What I've found that works is using an ssh tunnel to get to it.

To get this to work, you can run ssh client using a console on your workstation:

# ssh -D 1234 username@hostname

Next, you can run jconsole using a socket proxy:

# jconsole -J-DsocksProxyHost=localhost -J-DsocksProxyPort=1234

Note that this also works with jvisualvm:

# jvisualvm -J-DsocksProxyHost=localhost -J-DsocksProxyPort=1234

For jconsole, you then would fill in the rest of the URL, username and password in the dialog box and you should be able to get in.

service:jmx:rmi:///jndi/rmi://<hostname>:50500/alfresco/jmxrmi


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.

Monday, November 6, 2017

Why ImageMagick Fails in Alfresco 5.2.1 and How to Fix It

The Problem

If you are using Alfresco Content Services 5.2.1 on Windows, you may have noticed some image files fail to display their thumbnail and preview renditions in Alfresco Share. When you look at the ImageMagick configuration in the Alfresco Admin Console, you see that ImageMagick is disabled:


And, the following ImageMagick error is seen the alfresco.log file:

2017-10-24 11:07:20,320 ERROR [org.alfresco.repo.content.transform.magick.AbstractImageMagickContentTransformerWorker] [localhost-startStop-1] ImageMagickContentTransformerWorker not available: 09240020 Failed to perform ImageMagick transformation: 
Execution result: 
   os:         Windows Server 2012 R2
   command:    C:\Alfresco\5.2.1\imagemagick\convert.exe C:\Alfresco\52D9B8~1.1\tomcat\temp\Alfresco\ImageMagickContentTransformerWorker_init_source_137991397234068809.gif -strip -quiet C:\Alfresco\52D9B8~1.1\tomcat\temp\Alfresco\ImageMagickContentTransformerWorker_init_target_9151095455871890262.png
   succeeded:  false
   exit code:  1
   out:        
   err:        convert.exe: RegistryKeyLookupFailed `CoderModulesPath' @ error/module.c/GetMagickModulePath/670.
convert.exe: no decode delegate for this image format `GIF' @ error/constitute.c/ReadImage/509.
convert.exe: no images defined `C:\Alfresco\52D9B8~1.1
20

In the Alfresco 5.2.1 release, Alfresco replaced the previously used Ghostscript PDF interpreter with the new Alfresco PDF Render transformer (i.e., alfresco-pdf-renderer). This application is responsible for generating thumbnails and previews of various document formats. Unfortunately, the Windows version of this transformer conflicts with the ImageMagick transformer causing ImageMagick transformations to fail. The conflict occurs because two different Spring bean references were given the same name. As a result, the same parameters are being passed to both ImageMagick and the Alfresco PDF Render.

It's worth noting that this issue is fixed in the Alfresco 5.2.2 release, but if you are still using Alfresco 5.2.1, here's how you can fix it.

The Fix

You can resolve the transformer conflict by placing a copy of the custom-alfresco-pdf-renderer-transform-context.xml and custom-imagemagick-transform-context.xml files (contents shown below) in the tomcat/shared/classes/alfresco/extension folder and restarting Alfresco. Before you do the restart, ensure the img.gslib property does not exist (or is commented out) in the alfresco-global.properties file.

The Files

custom-alfresco-pdf-renderer-transform-context.xml
<?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="transformer.worker.subsys.alfresco-pdf-renderer" class="org.alfresco.repo.content.transform.pdfrenderer.AlfrescoPdfRendererContentTransformerWorker">
      <property name="mimetypeService">
         <ref bean="mimetypeService" />
      </property>
      <property name="executer">
         <bean name="transformer.alfresco-pdf-renderer.command" class="org.alfresco.util.exec.RuntimeExec">
            <property name="commandsAndArguments">
               <map>
                  <entry key=".*">
                     <list>
                        <value>${alfresco-pdf-renderer.exe}</value>
                        <value>SPLIT:${options}</value>
                        <value>${source}</value>
                        <value>${target}</value>
                     </list>
                  </entry>
               </map>
            </property>
            <property name="processProperties" ref="#{systemProperties['os.name'].contains('Windows') ? 'transformer.worker.subsys.alfresco-pdf-renderer.processPropertiesWindows' : 'transformer.worker.subsys.alfresco-pdf-renderer.processPropertiesUnix'}" />
            <property name="defaultProperties">
               <props>
                  <prop key="options"></prop>
               </props>
            </property>
            <property name="errorCodes" >
               <value>1</value>
            </property>
         </bean>
      </property>
      <property name="checkCommand">
         <bean name="transformer.Pdfium.CheckCommand" class="org.alfresco.util.exec.RuntimeExec">
            <property name="commandsAndArguments">
               <map>
                  <entry key=".*">
                     <list>
                        <value>${alfresco-pdf-renderer.exe}</value>
                        <value>--version</value>
                     </list>
                  </entry>
               </map>
            </property>
         </bean>
      </property>
   </bean>

   <bean id="transformer.worker.subsys.alfresco-pdf-renderer.processPropertiesWindows" class="org.springframework.beans.factory.config.MapFactoryBean">
      <property name="sourceMap"> 
         <map>
            <entry key="ALFRESCO-PDF-RENDERER_HOME">
               <value>${alfresco-pdf-renderer.root}</value>
            </entry>
         </map>
      </property>
   </bean>

   <bean id="transformer.worker.subsys.alfresco-pdf-renderer.processPropertiesUnix" class="org.springframework.beans.factory.config.MapFactoryBean">
      <property name="sourceMap">
         <map>
            <entry key="ALFRESCO-PDF-RENDERER_HOME">
               <value>${alfresco-pdf-renderer.root}</value>
            </entry>
         </map>
      </property>
   </bean>

</beans>

custom-imagemagick-transform-context.xml
<?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="transformer.worker.ImageMagick" class="org.alfresco.repo.content.transform.magick.ImageMagickContentTransformerWorker">
      <property name="mimetypeService">
         <ref bean="mimetypeService" />
      </property>
      <property name="executer">
         <bean name="transformer.ImageMagick.Command" class="org.alfresco.util.exec.RuntimeExec">
            <property name="commandsAndArguments">
               <map>
                  <entry key=".*">
                     <list>
                        <value>${img.exe}</value>
                        <value>${source}</value>
                        <value>SPLIT:${options}</value>
                        <value>-strip</value>
                        <value>-quiet</value>
                        <value>${target}</value>
                     </list>
                  </entry>
               </map>
            </property>
            <property name="processProperties" ref="#{systemProperties['os.name'].contains('Windows') ? 'transformer.worker.ImageMagick.processPropertiesWindows' : 'transformer.worker.ImageMagick.processPropertiesUnix'}" />
            <property name="defaultProperties">
               <props>
                  <prop key="options"></prop>
               </props>
            </property>
            <property name="errorCodes" >
               <!-- The published error and fatal error codes are in the 400 and 700 ranges, but 1 is the most common and have seen 255 (could that by -1) -->
               <value>1,2,255,400,405,410,415,420,425,430,435,440,450,455,460,465,470,475,480,485,490,495,499,700,705,710,715,720,725,730,735,740,750,755,760,765,770,775,780,785,790,795,799</value>
            </property>
         </bean>
      </property>
      <property name="checkCommand">
         <bean name="transformer.ImageMagick.CheckCommand" class="org.alfresco.util.exec.RuntimeExec">
            <property name="commandsAndArguments">
               <map>
                  <entry key=".*">
                     <list>
                        <value>${img.exe}</value>
                        <value>-version</value>
                     </list>
                  </entry>
               </map>
            </property>
         </bean>
      </property>
   </bean>

   <bean id="transformer.worker.ImageMagick.processPropertiesWindows" class="org.springframework.beans.factory.config.MapFactoryBean">
      <property name="sourceMap"> 
         <map>
            <entry key="MAGICK_HOME">
               <value>${img.root}</value>
            </entry>
            <entry key="MAGICK_CODER_MODULE_PATH">
               <value>${img.coders}</value>
            </entry>
            <entry key="MAGICK_CONFIGURE_PATH">
               <value>${img.config}</value>
            </entry>
            <entry key="DYLD_FALLBACK_LIBRARY_PATH">
               <value>${img.dyn}</value>
            </entry>
            <entry key="LD_LIBRARY_PATH">
               <value>${img.dyn}</value>
            </entry>
         </map>
      </property>
   </bean>

   <bean id="transformer.worker.ImageMagick.processPropertiesUnix" class="org.springframework.beans.factory.config.MapFactoryBean">
      <property name="sourceMap">
         <map>
            <entry key="MAGICK_HOME">
               <value>${img.root}</value>
            </entry>
            <entry key="DYLD_FALLBACK_LIBRARY_PATH">
               <value>${img.dyn}</value>
            </entry>
            <entry key="LD_LIBRARY_PATH">
               <value>${img.dyn}</value>
            </entry>
         </map>
      </property>
   </bean>

</beans>

Friday, October 27, 2017

Alfresco Web Scripts: Limiting Picker Search for Users

Both the Alfresco repository and Alfresco Share products extensively use Alfresco Web Scripts. Web Scripts are an important concept to understand in order to be able to customize Alfresco.

In this post we'll look at a requested customization for Alfresco Share and how it can be implemented by overriding the controller of an existing Alfresco Web Script.

First let's look at the following functionality in Alfresco Share.

By default, any user in Alfresco can start a workflow on a document which they have access to. If a user chooses to create a New Task (ad hoc task) workflow on a document, the workflow creation form is displayed, and on that form, the user is able to select a user to assign the new task to.

In the "Select..." dialog that pops up, the user is able to search across all registered Alfresco users.

It looks like this:


Customization Requirement


The requirement for the customization is, for all users other than admin users, to limit the search result to include only users that belong to the same groups which they are members of.

Implementation

How can we achieve that?

After a little investigation, we find that the user assignment popup is a kind of picker object, and that the client JavaScript code for the picker searches for users by making a call to a repository Web Script.  The controller for that Web Script is the following file:

alfresco/templates/webscripts/org/alfresco/repository/forms/pickerchildren.get.js.

[Note that this file can be found bundled in the repo jar file: alfresco-remote-api-5.x.x.jar]

Within the Web Script, the function that gets called to search for users is findUsers().  We'll change this method to limit the search results from the Picker to include only those users that belong to same groups that the user belongs to.








function findUsers(searchTerm, maxResults, results)
{
   var personRefs = people.getPeople(searchTerm, maxResults, "lastName", true);
   
   // create person object for each result
   for each(var personRef in personRefs)
   {
      if (logger.isLoggingEnabled())
         logger.log("found personRefs = " + personRefs);
      
      //filter out  the disabled users
      var daname = (search.findNode(personRef)).properties.userName;
 
      if(people.isAccountEnabled(daname)){
          results.push(
          {
              item: createPersonResult(search.findNode(personRef)),
              selectable: true
          });
      }
      else{
          results.push(
       {
           item: createPersonResult(search.findNode(personRef)),
           selectable: false
       });   
          if (logger.isLoggingEnabled())
              logger.log("User not added to results not enabled" + daname);
      }
   }
}




We see here that searches are currently being made using the Alfresco JavaScript API using root object 'people':

 var personRefs = people.getPeople(searchTerm, maxResults, "lastName", true);

The requirement for the customization is to change the search by limiting the results to only those users that belong to the same group as the current user.  We can again use the 'people' root-scoped object to find which groups the user belongs to:

var userGroups = people.getContainerGroups(person);

But the problem with the method getContainerGroups() is that it does not include system groups with the results.  It would be great if this method took another parameter as a flag to specify that system groups should be included in the result, but it doesn't. System groups include the Share groups created for each site for managing the four default Site roles: site manager, collaborator, contributor, and consumer.



If a user belongs to a Share site, we also want to include in the results any users which belong to any of the four Share site groups for that site. To find the additional site groups corresponding to the sites that a user belongs to, we will write a new method.  That is shown in the following code which takes a list and adds the Share site groups to the list.




function addSiteGroups(g)
{
    var s = siteService.findSites("", null, 0);
    for(var j=0; j<s.length; j++)
      {
 var perms = s[j].getSitePermissionGroups();
 for(var k=0;k<perms.length;k++)
 {
     g.push( groups.getGroupForFullAuthorityName(perms[k]).getGroupNode() );
 }
      }
  }

Here we query the site service to find all sites that the current user is able to access.  Then we get the names of the roles for the site -- there typically will be just the four we mentioned earlier. Then we get the group corresponding to each of those permissions and push the group nodes onto the list of groups:



groups.getGroupForFullAuthorityName(perms[k]).getGroupNode()

Once we have the list of groups that the user belongs to, we can make a query to find only those people that match the search criteria and also are members of those groups.

First we a build a string which contains part of the condition for the query that lists the groups that the user belongs:

 grpQuery = " AND (";
  
 // make concatenated list of groups
 for(var i=0; i<userGroups.length; i++)
   {
      if(i>0) 
      {
  grpQuery = grpQuery + " OR ";
      }
      grpQuery = grpQuery + "PARENT:\"" + userGroups[i].nodeRef.toString() + "\" ";
    }
    grpQuery = grpQuery + ")";



The query is as follows.  Note that 'filterTerm' corresponds to the text entered by the user within the UI. 'grpQuery' corresponds to the criteria for the group restriction. Note that we search for objects that are type cm:person and which do not have the cm:personDisabled flag applied.

var def = 
{ 
  query: "+TYPE:\"cm:person\" AND (-ASPECT:\"cm:personDisabled\") AND(cm:lastName:\"*" + filterTerm + "*\" OR cm:userName:\"*" + filterTerm + "*\")" + grpQuery, 
  store: "workspace://SpacesStore", 
    language: "fts-alfresco",
    page: paging
  }; 

Putting it all together, the combined changes to the Web Script are as follows:



function addSiteGroups(g)
{
 var s = siteService.findSites("", null, 0);
 for(var j=0; j<s.length; j++)
 {
  var perms = s[j].getSitePermissionGroups();
  for(var k=0;k<perms.length;k++)
  {
      g.push( groups.getGroupForFullAuthorityName(perms[k]).getGroupNode() );
  }
 }
}

// Modified by Formtek to find only group to which the user belongs
function findUsers(filterTerm, maxResults, results)
{  
 var paging = 
 { 
     maxItems: maxResults, 
     skipCount: 0 
 }; 
   
 var grpQuery = "";
 
 
 // If the user is not an admin, restrict the search
 if(!people.isAdmin(people.getPerson(person.properties["cm:userName"])))
 {
     // Find all the groups that the user is in
     var userGroups = people.getContainerGroups(person);
  addSiteGroups(userGroups);
  grpQuery = " AND (";
  
     // make concatenated list of groups
     for(var i=0; i<userGroups.length; i++)
     {
      if(i>0) 
      {
       grpQuery = grpQuery + " OR ";
      }
      grpQuery = grpQuery + "PARENT:\"" + userGroups[i].nodeRef.toString() + "\" ";
     }
     grpQuery = grpQuery + ")";
    }
 
 var def = 
 { 
   query: "+TYPE:\"cm:person\" AND -ASPECT:\"cm:personDisabled\" AND (cm:firstName:\"*" + filterTerm + "*\" OR cm:lastName:\"*" + filterTerm + "*\" OR cm:userName:\"*" + filterTerm + "*\")" + grpQuery, 
   store: "workspace://SpacesStore", 
   language: "fts-alfresco",
   page: paging
 }; 
 
 var personRefs = search.query(def); 
   
 // create person object for each result
 for each(var personRef in personRefs)
 {
    // add to results
    results.push(
    {
       item: createPersonResult(search.findNode(personRef.nodeRef)),
       selectable: true
    });
 }
}

To override the existing Web Script file, copy the original file into the extensions area and make the change to the findUsers() method as shown above here:

tomcat/shared/WEB-INF/classes/alfresco/extension/templates/webscripts/org/alfresco/repository/forms/pickerchildren.get.js.

One additional thing to notice is that filtering by group is only applied when the user is not an admin:

people.isAdmin(people.getPerson(person.properties["cm:userName"]))
 

Workflow assignments of tasks to users will now use the override method defined here and users will be filtered by group.