Liferay – Advanced Hook Customization

I have recently been working on a task to customize some of the aspects of Liferay’s built in Login portlet. This article describes how to take advantage of the simple configuration items provided by Liferay and also how to do more advanced customization.

Topics include:

  • Customizing the Create Account page of the Login portlet
  • Creating Rules for the Password format
  • Preventing Display of Password after successfully creating account
  • Customizing the First Time Login experience
  • Adding New Fields to the Liferay User entity with Expando
  • Persisting New Fields created with Expando

Workflow for Liferay Customization

There is no hard and fast process for customizing Liferay, however here is a general rule of thumb I am following for this example. My goal is trying the simplest method and then move on to more complex methods. The first two are about a tie for simplicity.

  • Using Liferay’s Control Panel
  • Changing portal-ext.properties
  • Writing Hooks
  • Overriding Struts Actions provided by Liferay
  • Using the Liferay source code as an example

Customizing the Create Account page of the Login portlet

I needed to hide a few of the fields that the Login portlet displayed and also add a few that weren’t.

The ones to hide were:

  • Gender
  • Birthday
  • Middle Name

The ones to add were for

  • Create Password
  • Confirm Password

For hiding the fields mentioned above we’ll first examine what properties may be modified in portal-ext.properties. Use the documentation for Liferay Portal Properties to initially see if we can limit ourselves to just changing properties to get what we want.

It turns out that we can hide the Gender and Birthday fields as documented in Liferay Portal Properties section titled Fields. Here is a snippet of those properties:

#
# Set the following fields to false so users cannot see them. Some company
# policies require gender and birthday information to always be hidden.
#
field.enable.com.liferay.portal.model.Contact.male=true
field.enable.com.liferay.portal.model.Contact.birthday=true

 

Simply set each of those properties to ‘false’ to hide them in the Create Account page.

So it looks like portal-ext.properties will get us part of the way there in our customization. But now what do we do about the field Middle Name? Where does that even come from anyway?

The way to understand most of the front end of Liferay is to examine your Liferay home directory. I am using Tomcat bundled with Liferay EE 6.0 SP2. All of the web content for built in portlets can be found in the subdirectory webapps/ROOT/html and I know I am searching for something like ‘Middle Name’.

I did the following search from my command line (I’m on Mac OS X by the way):

~/lrp/tc/webapps/ROOT/html > grep -r -i -l 'middle.*name' * | grep jsp
portlet/directory/user_search.jsp
portlet/enterprise_admin/edit_ldap_server.jsp
portlet/enterprise_admin/user/details.jsp
portlet/enterprise_admin/user_search.jsp
portlet/enterprise_admin/user_search_results_database.jspf
portlet/enterprise_admin/user_search_results_index.jspf
portlet/journal_content_search/search.jsp
portlet/login/create_account.jsp
portlet/search/search.jsp

 

The one that’s bolded above looks promising because it is in the login subdirectory of portlet and is called create_account.jsp

We must write a hook to customize create_account.jsp for the Login portlet . If you have never developed a hook, please read this introduction to get yourself started.

Once you have created a hook project, copy the create_account.jsp file from your Liferay installation into your custom jsp’s directory and open the file for editing. Searching for the term ‘middle’ we find the following in the section of the jsp that creates the form:

111         <aui:fieldset>
112                 <aui:column>
113                         <aui:input name="firstName" />
114
115 <aui:input name="middleName" />
116
117                         <aui:input name="lastName" />
118
119                         <c:if test="<%= !PrefsPropsUtil.getBoolean(company.getCompanyId(),PropsKeys.USERS_SCREEN_NAME_ALWAYS_AUTOGENERA    TE) %>">
120                                 <aui:input bean="<%= user2 %>" model="<%= User.class %>" name="screenName" />
121                         </c:if>
122
123                         <aui:input bean="<%= user2 %>" model="<%= User.class %>" name="emailAddress" />
124                 </aui:column>

 

Now all we need to do is remove the aui:input from line 115 and save the file. We can now build our hook, deploy it to Liferay and see that the Create Account page no longer displays a Middle Name field.

Next we need to allow the user to enter their own password when creating their account. This part is easy and just requires setting the following in portal-ext.properties.

login.create.account.allow.custom.password=true

Creating Rules for Password Format

Liferay does not by default enforce any rules for passwords created by users. However it is easy to change that. Examine the documentation for Liferay Portal Properties and you will find that the properties passwords.toolkit and passwords.regexptoolkit.pattern can be changed to control the rules for creating passwords.

passwords.toolkit=com.liferay.portal.security.pwd.RegExpToolkit

You may create a rule for what constitutes a valid password with a regular expression. A very typical rule would be something like the following

  • Password length must be at least 6 characters and no more than 20 characters
  • Must contain a lowercase letter
  • Must contain an uppercase letter
  • Must contain a digit
  • Must contain one of the following special characters ‘@’, ‘#’, ‘$’, ‘%’

Here is how to express the above with a regular expression and use it in portal-ext.properties.

# Start of group (
#   must contains one digit from 0-9 (?=.*\d)>
#   must contains one lowercase characters (?=.*[a-z])
#   must contains one uppercase characters (?=.*[A-Z])>
#   must contains one special symbols in the list "@#$%" (?=.*[@#$%])
#   must not contain 'password' (case insensitve) (?!.*(?i)password)
#     match anything with previous condition checking .
#        length at least 6 characters and maximum of 20 {6,20}
# End of group )
passwords.regexptoolkit.pattern=((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%])(?!.*(?i)password).{6,20}) 

Preventing Liferay from Displaying Password for Newly Created Account

Liferay’s default behavior after successfully creating an account is to display the password created in the Create Account Page. This is great if you have configured Liferay to generate a password for the user. However in this case we are allowing the user to create their own password and maybe we do not want this shown after they successfully create their account.

Getting Liferay to not display the user’s newly created password after they successfully create their account is relatively easy also but requires some more jsp customization.

This time it appears that the message that will display the password for a newly created account is in line 67 of the file:

webapps/ROOT/html/portlet/login/login.jsp
61         <div>
62                 <c:choose>
63                         <c:when test="<%= company.isStrangersVerify() || Validator.isNull(userPassword) %>">
64                                 <%= LanguageUtil.get(pageContext, "thank-you-for-creating-an-account") %>
65                         </c:when>
66                         <c:otherwise>
67 <%= LanguageUtil.format(pageContext, "thank-you-for-creating-an-account.-your-password-is-x", userPassword, false) %>
68                         </c:otherwise>
69                 </c:choose>
70
71                 <c:if test="<%= PrefsPropsUtil.getBoolean(company.getCompanyId(), PropsKeys.ADMIN_EMAIL_USER_ADDED_ENABLED) %>">
72                         <%= LanguageUtil.format(pageContext, "your-password-has-been-sent-to-x", userEmailAddress) %>
73                 </c:if>
74         </div>

 

Copy the login.jsp file to your custom jsp directory and change line 67 to the following:

<%= LanguageUtil.format(pageContext, "thank-you-for-creating-an-account.") %>

First Time Login Experience

Liferay has the following First Time Login Experience by default:

  • Presents the user with a Terms and Conditions that they must agree to
  • Asks the user to update their password
  • Asks the user to choose a question they must answer to reset their password
  • Takes the user to the portal home page

In this case we’ll look at changing the middle two items from above.

Prevent Liferay from Requiring User to Change Password

Getting Liferay to not ask the user to update their password at first login is easy. Navigate to the Password Policy settings under the Control Panel and click the Action button for the “Default Password Policy” and then click Edit in the popup menu. Then uncheck the box labeled ‘Change Required’ and click the Save button.

Customize the Password Reset Question Page

Completing the process of creating an account requires the user to choose a password reset question and provide an answer. But what if you would like the user to answer some additional information? And what if you want user information that isn’t provided by Liferay by default?

Creating Additional information fields for Liferay User entity

The typical way of adding fields for a Liferay entity (e.g. User) is using Liferay’s Expando mechanism. Open the Control Panel in Liferay and click on the ‘Custom Fields’ entry in the left hand navigation. You will see that it presents a table of the different Liferay entities that you can add custom fields to. Luckily one of these is User.

Click the row for User and then click on the ‘Add’ button. For this example lets choose ‘industry’ for the key and for the type choose Selection of Text Values.

Once that’s completed you will see this entry added to the custom fields table in the Control Panel for the User entity. Next click on the ‘Actions’ button and then click on the ‘Edit’ entry offered in the context menu. Add the following values in the text box presented for the ‘Default Attribute’ field:

Education
Energy
Tourism
Information Technology
Entertainment

Be sure to save those changes and go back to the previous page that displays the ‘Actions’ button. Click it again and this time mark the checkboxes for update and view for the roles labeled as ‘Guest’, ‘User’, and ‘Power User’. If you forget to do so you are likely to see exceptions on your console when you later add this field to your password reset question page.

Adding More fields to Password Reset Question Page

We will add another customized jsp page to our hook to add fields to this page. First I had to figure out which page to customize. Although I have been using the term ‘Password Reset Question’, Liferay uses the term ‘Password Reminder Query’. I examined ROOT directory under the webapps folder and found a jsp named update_reminder_query.jsp. I copied this file into my custom jsp’s directory. In this example lets add our new Expando field and also add the User entity Phone Number field too. Enter code similar to the following between the ending <aui:fieldset> tag and the <aui:button> tag in your custom jsp page you just created.

 71         <aui:fieldset label="additional-information">
 72                 <div>
 73                         <liferay-ui:custom-attribute
 74                                 className="<%= User.class.getName() %>"
 75                                 classPK="<%= 0 %>"
 76                                 label="<%= true %>"
 77                                 editable="<%= true %>"
 78                                 name="industry" />
 79                 </div>
 80 

...

108         <label > Phone Contact </label>
109 <%
110         for (int i = 0; i < phonesIndexes.length; i++)
111         {
112                 int phonesIndex = phonesIndexes[i];
113                 Phone phone = phones.get(i);
114 %>
115
116                 <aui:model-context bean="<%= phone %>" model="<%= Phone.class %>" />
117                 <aui:input name='<%= "phoneId" + phonesIndex %>' type="hidden" value="<%= phone.getPhoneId() %>" />
118                 <aui:input inlineField="true" fieldParam='<%= "phoneNumber" + phonesIndex %>' name="number" />
119                 <aui:input inlineField="true" fieldParam='<%= "phoneExtension" + phonesIndex %>' name="extension" />
120
121 <%
122         }
123 %>
124
125         </aui:fieldset>

 

Now go ahead and build and deploy your hook. You will see that you now also have a dropdown asking the user to choose a value from a dropdown labeled ‘Industry’ and to fill out their phone number. At this point you will notice that you can interact with the form but none of the information will get saved. Now what to do?

Overriding Liferay Struts Actions

Liferay’s Login Portlet is built upon Struts technology. Until recently, if Liferay did not provide a property in portal-ext.properties that allowed you to override the action for an event you had quite a bit of work to do. Now however it is possible to override the actions.

Examine the struts-config.xml file in your webapps/ROOT/WEB-INF folder. You’ll remember from a few steps ago that the jsp page we customized was named update_reminder_query.jsp. Searching for ‘update_reminder_query’ in the struts config file reveals that the action class for this jsp is ‘com.liferay.portal.action.UpdateReminderQueryAction’. Further inspection of this class reveals that it extends org.apache.struts.action.Action.

Now here’s the relatively new way to inject our own logic for this struts action without resorting to the Ext mechanism. (The current documentation on how to do this may be found in this Liferay Blog posting. What follows is less comprehensive than the posting.)

  • Write a class that extends com.liferay.portal.kernel.struts.BaseStrutsAction
  • Configure your hook to add this action

First add a class to your hook plugin that extends BaseStrutsAction class. Note that if the action you are overriding extends the com.liferay.portal.struts.PortletActionClass you will need to extend BaseStrutsPortletAction instead.

Now put some logic in your action class to save the additional fields as desired by overriding the execute method. Below is an example snippet

 26 public class UpdateReminderQueryActionWrapper extends BaseStrutsAction {
 27
 28         @Override
 29         public String execute(StrutsAction originalStrutsAction, HttpServletRequest request, HttpServletResponse response)
 30                         throws Exception {
 31
 32                 long userId = PortalUtil.getUserId(request);
 33                 User user = UserServiceUtil.getUserById(userId);

// Put in your logic here for persisting custom field information

 85                 return originalStrutsAction.execute(request, response);
 86         }

 

At the end of your overridden method it is important to make the very last call unless you really do not want to preserve the behavior the original action provides.

Persisting Expando Fields

Now we add some logic to our overriden execute method to persist our new custom fields. The code presented below is specific for persisting the users choices from a series of dropdowns. So if you are dealing with a different type, hopefully it will be even easier for you.

 54                 Enumeration<String> attrNames = user.getExpandoBridge().getAttributeNames();
 55
 56                 if (attrNames != null && attrNames.hasMoreElements()) {
 57
 58                         while (attrNames.hasMoreElements()) {
 59
 60                                 String an = attrNames.nextElement();
 61                                 String value = request.getParameter("ExpandoAttribute--" + an + "--");
 62
 63                                 if (value != null) {
 64
 65                                         String[] valList = (String[]) user.getExpandoBridge().getAttribute(an);
 66
 67                                         boolean valueOk = false;
 68                                         for (String s : valList) {
 69
 70                                                 if (value.equals(s)) {
 71                                                         valueOk = true;
 72                                                 }
 73                                         }
 74
 75                                         if (valueOk) {
 76                                                 String[] newValList = new String[] { value };
 77                                                 user.getExpandoBridge().setAttribute(an, newValList);
 78                                         }
 79                                 }
 80                         }
 81                 }

Wire your action class into your hook

 

The last remaining step is to wire your logic into your hook and is very simple to do. Add the following to your login-hook.xml file after the service tag.

 12         <struts-action>
 13                 <struts-action-path>/portal/update_reminder_query</struts-action-path>
 14                 <struts-action-impl>za.co.is.hook.UpdateReminderQueryActionWrapper</struts-action-impl>
 15         </struts-action>

 

Now you may rebuild your hook and redeploy it to Liferay. This time when you click the submit button your custom fields will be persisted to the database. I have left out persisting the phone number but this isn’t difficult to implement. Now we have a complete example of how to add customized fields to the first time login experience in Liferay. Enjoy!