Skip navigation
All Places > Alfresco Content Services (ECM) > Blog > 2013 > March
2013
A few weeks back we added a new security filter to Alfresco Share. It is a regular Java Servlet filter that applies http response headers to incoming requests to Alfresco Share. The headers that are returned are defined in a config section called SecurityHeadersPolicy in alfresco-security-config.xml. We did this mainly to improve mitigation of clickjacking attacks but we also added 2 headers to tighten up the security in Internet Explorer after a tip from Jens Goldhammer at FME.



To read more about clickjacking please visit OWASP's page on the subject:

https://www.owasp.org/index.php/Clickjacking



Now let's take a look at the default config for Alfresco Enterprise 4.1.4 (and on Community on HEAD), as you can see 3 headers are added by default:

<config evaluator='string-compare' condition='SecurityHeadersPolicy'>

  <headers>

    <header>

      <name>X-Frame-Options</name>

      <value>SAMEORIGIN</value>

    </header>

    <header>

      <name>X-Content-Type-Options</name>

      <value>nosniff</value>

    </header>

    <header>

      <name>X-XSS-Protection</name>

      <value>1; mode=block</value>

    </header>

  </headers>

</config>


Lets take a look at what implications these headers will have on browsers...

X-Frame-Options



Adding this header to an http response will tell the browser if Share pages are allowed to be put inside iframes or not. In our default config we have set this to SAMEORIGIN which means that Share pages are only allowed to be '(i)framed' inside Share or other webapps that happen to live under the same domain. In other words it will i.e. be possible to include http://www.acme.com/share inside an iframe on http://www.acme.com/portal.



If you do not want this to be allowed you can override the config and set the header to return DENY instead, you can do that by placing the following config in your share-config-custom.xml file:

<config evaluator='string-compare' condition='SecurityHeadersPolicy'>

  <headers>

    <header>

      <name>X-Frame-Options</name>

      <value>DENY</value>

    </header>

  </headers>

</config>


If you instead want to open up the possibility to include Share inside iframes on any domain you can do this by using the following config instead. Note! This is not recommended since it will open up the possibility of Alfresco Share being a target of a clickjacking attack.

<config evaluator='string-compare' condition='SecurityHeadersPolicy'>

  <headers>

    <header>

      <name>X-Frame-Options</name>

      <enabled>false</enabled>

    </header>

  </headers>

</config>


Note! Since we are on the subject of iframes, feel free to read this blog post about improved mitigation of phishing attacks in Alfresco Share by the introduction of the new IFramePolicy. It will decide which urls that Alfresco Share will allow to be put inside an iframe in the Alfresco Share client.

X-Content-Type-Options



This is only valid for Internet Explorer. Older version of Internet Explorer (8 and below) will *help* developers by sniffing the content of a returned resource and then execute the content as the content type that IE thinks the resource has, instead of the content type the server returned. To stop IE from doing this we are returning nosniff in the header.

X-XSS-Protection



Yep IE trying to *help* again. Instead of trusting the developer to do a good job of mitigating XSS attacks IE just can't keep its fingers away from your code. If it finds some code that looks suspicious (XSS code) it will manipulate (a.k.a. 'sanitize' it) to become safe and then execute the sanitized code. So what's bad about that? Well IE's 'sanitization' logic can be used by an attacker to actually introduce an XSS flaw on your site. As a 'solution' to this IE introduced the X-XSS-Protection header.



By default Alfresco Share will return '1; mode=block' for this header, which means that if IE *thinks* it has found an XSS attack it will not execute the code (instead of sanitizing it and running it which is the default).



It is also possible to set it to '0' which means that IE shall not even try and inspect the code for XSS attacks.

Adding additional headers



Adding additional headers to the config is also supported. Lets take the Strict-Transport-Security header as an example, it is used to force your browser to only allow https and not http communication. It is not provided by default in Alfresco Share but can be added by placing the following code inside your share-config-custom.xml file:

<config evaluator='string-compare' condition='SecurityHeadersPolicy'>

  <headers>

    <header>

      <name>Strict-Transport-Security</name>

      <value>max-age=31536000</value>

    </header>

  </headers>

</config>


I hope you have enjoyed this blog post, if you have any questions please add a comment below!
A few weeks back we added a new config section called IFramePolicy into the alfresco-security-config.xml file. This is a config section that describes which pages that Alfresco Share should allow to be '(i)framed', in other words be included inside Alfresco Share within an iframe. It is available in Alfresco Enterprise 4.1.4 and also for Community on HEAD.



The reason we added this config is to improve mitigation of phishing attacks. To read more about phishing attacks please visit OWASP's page on the subject.

https://www.owasp.org/index.php/Phishing



So how does this concern you? Well if you are a developer and you have code that creates iframe's you should honour the config before creating the iframe. If you are a system administrator you are probably interested in overriding the default config because it allows *any* pages to be iframed.



Let's start by taking a look at the default configuration defined in share-security-config.xml.

<config evaluator='string-compare' condition='IFramePolicy'>

  <!--

    Local Share pages/resources are governed by the same-domain element which

    can be set to 'allow' or 'deny'

  -->

  <same-domain>allow</same-domain>

  <!--

    Add a list of <url> elements inside this element to form a whitelist of

    allowed domains. The check will assert that the url used for the <iframe>

    starts with the value of one of the <url> elements.

  -->

  <cross-domain>

    <!--

      Allow all domains by default, it is recommended to override this

      setting and instead keep a whitelist of the domains that you trust to be

      included on Share pages.

    -->

    <url>*</url>

  </cross-domain>

</config>


Honouring the IFramePolicy in your code



Every developer creating custom code for Alfresco Share should honour the IFramePolicy config. It is very simple and all you need to do is to add the following code snipped to your code:

if (Alfresco.util.IFramePolicy && 

    !Alfresco.util.IFramePolicy.isUrlAllowed(iFrameSrcUrl))

{

   // TODO: Display error message saying the IFramePolicy doesn't allow this url

}

else

{

  // TODO: Display the iframe just like you did before

}


First we check if the IFramePolicy is there, we do this to make sure our code will continue to work in older Alfresco Share versions that don't have an IFramePolicy. Then we check if the url that we are about to display is trusted by the IFramePolicy config. If it isn't we display a friendly error message telling the user how to proceed.

Creating a whitelist of trusted domains



As an administrator you probably want to override the default configuration to keep your Alfresco Share installation as safe as possible. This is very simple to do, simply:



Copy the following code and add it to your share-config-custom.xml file:

<config evaluator='string-compare' condition='IFramePolicy' replace='true'>

  <cross-domain>

    <url>http://www.trusted-domain-1.com/</url>

    <url>http://www.trusted-domain-2.com/</url>

  </cross-domain>

</config>


As you can see we have overriden/replaced the IFramePolicy's <cross-domain> element to not include the default <url>*</url> but instead multiple <url> element each specifying the urls to trust.



The url check will be done using a 'startswith' comparison (not a regexp) meaning you can, if you like, only allow certain pages on a domain to be trusted, i.e. you could add a <url> element like below:

<url>http://www.partly-trusted-domain.com/but/only/urls/from/here</url>


Note! Avoid adding a url with only the protocol and domain that doesn't end with a front slash ('/'), since http://www.my-proxy-server.com.evil-server.se/phishing-attack.html obviously starts with http://www.my-proxy-server.com but not http://www.my-proxy-server.com/ .



That's it, I hope you enjoyed the blog post, if you have any questions please add a comment.
A few weeks back we added a CSRF filter to Share. It is now available on Alfresco Enterprise 4.1.4 and also to the Community on HEAD.



This blog post will be about how the new filter affects your current Alfresco Share installation or your custom Share extension module. In most cases the new filter will actually not affect your installation or custom code at all, however there might be a few edge cases where you need to change your code slightly. The post will also describe how you can configure the CSRF filter to work behind one or more proxies, how to run it with 3rd party plugins behaving badly, how it can be used to stop specific repository services from being accessible directly from the browser through Share's proxy and also how to turn off the filter.



To learn more about CSRF feel free to read OWASP's introduction article about CSRF: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)



Note! This post misspells the word 'referer' during the entire post to honour the spec that introduced the Referer header with the incorrect spelling :-)



How does the filter work?



The filter is implemented in a class named org.alfresco.web.site.servlet.CSRFFilter that reads a config section in share-security-config.xml named CSRFPolicy which will describe how and when the filter shall mitigate CSRF, summarized as:



  • Each logged in user will receive a secret CSRF token.


  • The token will be communicated to the browser using a cookie named Alfresco-CSRF-Token.


  • When a logged in user performs a POST, PUT or DELETE http request against Alfresco Share the token MUST be passed in the request using one of the following methods:



    • As a custom http request header named Alfresco-CSRF-Token


    • As a url parameter named Alfresco-CSRF-Token.

      Note! Most often the header will be required, but in certain circumstances a header cannot be used and only then the token may be passed using a url parameter. The default config only accepts the url parameter when the Content-Type header starts with multipart/.





  • Every time the logged in user goes to a new Share page the token will be renewed.


  • The filter will also check the Referer and Original http request headers matches the current domain (if present in the request).



To dig into the config, take a look at the latests revision http://svn.alfresco.com/repos/alfresco-open-mirror/alfresco/HEAD/root/projects/slingshot/config/alfresco/share-security-config.xml. A detailed description of all available options can be found at the bottom of this post.

Do you need to tweak your code?



If your custom code only is reading data using GET requests you will not have to do anything.



If your custom code only is using the standard Alfresco.util.Ajaxalfresco/core/CoreXhr or Alfresco.forms.Form javascript classes when creating/updating/deleting data you will also not have to do anything. Everything will be handled for you by:



  • Alfresco.util.Ajax & alfresco/core/CoreXhr - will automatically take the token from the cookie and add it as a request header for every request.


  • Alfresco.forms.Form - will automatically take the token from the cookie and add it as a url parameter to when submitting an multipart/form-data request.

    (When submitting a form as JSON the Alfresco.util.Ajax will be used internally)



Note! In the unlikely event that you do need to change your code make sure to:



  • Always read the token from the cookie just before the request is sent.

    Reason being that if you have multiple tabs opened in your browser one tab could, if accessing a new page, refresh the token in the session. If another tab then had saved its token in a variable it would suddenly be stale. But if all tabs always read the cookie just before submission they will always get the latest token even if the user changed pages in another tab. To read the token from the cookie simply use 
    Alfresco.util.CSRFPolicy.getToken().


  • Always check if the Alfresco.util.CSRFPolicy object exists before using it

    That way your code will work in all versions of Alfresco Share.



Now let's take a look however at the scenarios when you might need to tweak your code or change the default configuration.

1. You are making an XMLHttpRequest with method POST, PUT or DELETE without using the Alfresco.util.Ajax or alfresco/core/CoreXhr classes



Perhaps you are using the native XmlHttpRequest object or a 3rd party library such as jQuery. If that is the case you will need to add code looking something like this to pass the token:

if (Alfresco.util.CSRFPolicy && Alfresco.util.CSRFPolicy.isFilterEnabled())

{

   xhrHeadersObject[Alfresco.util.CSRFPolicy.getHeader()] = Alfresco.util.CSRFPolicy.getToken();

} 


Or if your using YAHOO.util.DataSource to load data with POST requests your code shall look like this:

if (Alfresco.util.CSRFPolicy && Alfresco.util.CSRFPolicy.isFilterEnabled())

{

   yuiDataSource.connMgr.initHeader(Alfresco.util.CSRFPolicy.getHeader(), Alfresco.util.CSRFPolicy.getToken(), false);

}




2. You are making a form upload with enctype multipart/form-data without using Alfresco.forms.Form



When uploading a file by submitting a form with enctype multipart/form-data it is not possible to set a header on the request, the reason is not because of the enctype specifically but due to the fact that its not possible to set a header on any form submission in the browser. Therefor you need to pass the token as a url parameter instead. If you are using the Alfresco.forms.Form class  this will be handled for you automatically but otherwise you have to add the token as a url parameter using code looking something like this:

if (Alfresco.util.CSRFPolicy && Alfresco.util.CSRFPolicy.isFilterEnabled())

{

   url += '?' + Alfresco.util.CSRFPolicy.getParameter() + '=' + encodeURIComponent(Alfresco.util.CSRFPolicy.getToken());

}


3. You are using a flash movie inside Share to send http requests with method POST



If you are using a flash movie to upload files (it uses the flash.net.FileReference ActionScript class which will perform a multipart/form-data request) make sure to add the token as a url parameter in your Javascript before passing in the url to the flash movie. If your Flash movie is performing application/json or other text based POST requests (it uses the flash.net.URLRequest and/or flash.net.navigateToURL ActionScript classes and methods) then make sure to pass in the token and the name of the header so it can be set from the flash movie.



Note! Flash cannot use ActionScript to directly get hold of the token since it may not read the browser cookies or read http response headers. It is possible however to use the flash.external.ExternalInterface ActionScript class to call a custom javascript method you have included on the page.

4. You are writing a non-browser client, i.e. a mobile app



Such app should be targeted against the repo in which there is no CSRF filter, meaning you don't have to do anything.



That's it! If your don't have code matching any of the described patterns you should not have to modify your code in any way. Please continue reading to be aware of other scenarios when you might be required to alter the default configuration.

Another system is sending POST requests to your Alfresco Share server



If there is a scenario in your system environment where servers from other domains actually shall be allowed to POST requests you will need to reconfigure the CSRFPolicy config in your share-config-custom.xml file to not check for a token or a Referer or Origin header. To do so simply:



  1. Copy the entire CSRFPolicy config in share-security-config.xml 


  2. Paste it into your share-config-custom.xml file and make sure it is replacing the old config section:

    <config evaluator='string-compare' condition='CSRFPolicy' replace='true'>


  3. Place the following snippet as the first child to the <filter> element:



<rule>

   <request>

      <method>POST</method>

      <path>/page/trusted/call/1|/page/trusted/call/2</path>

   </request>

   <action name='assertReferer'>

      <param name='always'>false</param>

      <param name='referer'>https://www.trustedserver.com/.*</param>

   </action>

   <action name='assertOrigin'>

      <param name='always'>false</param>

      <param name='origin'>https://www.trustedserver.com</param>

   </action>

</rule>


The CSRF filter will compare the incoming request with all the rules' request elements to find one that match, when it does it will invoke all the defined actions for that rule and then let the normal Share processing begin. In this case it means that if the external page was POST:ing to /page/trusted/call/1 or /page/trusted/call/2 this (and no other) rule will be used and its actions will run. The first action will assert that full page url in the Referer header (if present) equals https://www.trustedserver.com/.* and that the protocol and domain in the Origin header (if present) equals https://www.trustedserver.com .

I am running Alfresco Share behind one or more proxy server(s) and I get errors...



If you have placed Alfresco Share behind one or more proxy servers that rewrites all the urls before Alfresco Share is reached you might see errors in your log about the Referer or Origin headers not matching the current server. If that is the case see if its possible to also rewrite the Origin and Referer headers if they a) are present and b) match your proxy servers domain.

Note! When doing so make sure to not use a start-with-like-comparison without ending the proxy server's domain with a front slash ('/'), since http://www.my-proxy-server.com.evil-server.se/csrf.html obviously starts with http://www.my-proxy-server.com but not http://www.my-proxy-server.com/ .



If that is not possible you will have reconfigure the filter to accept requests from specific domains by simply:



  1. Copy the entire CSRFPolicy config in share-security-config.xml


  2. Paste it into your share-config-custom.xml file and make sure it is replacing the old config section:<config evaluator='string-compare' condition='CSRFPolicy' replace='true'>


  3. Modify every referer action to accept additional urls by changing:

    <action name='assertReferer'>

       <param name='always'>false</param>

    </action>


    ...to...

    <action name='assertReferer'>

       <param name='always'>false</param>

       <param name='referer'>https://www.proxyserver1.com/.*|https://www.proxyserver2.com/.*</param>

    </action>




  4. Modify every origin action to accept additional urls by changing:

    <action name='assertOrigin'>

       <param name='always'>false</param>

    </action>


    ...to...

    <action name='assertOrigin'>

       <param name='always'>false</param>

      <param name='origin'>https://www.proxyserver1.com|https://www.proxyserver2.com</param>

    </action>





The Referer will contain the entire url from which the request was submitted but the Origin will only include the protocol and domain (hence the .* wildcard at the end of the referer parameter).



In case you wondered, the current domain will continue to be accepted, meaning that you can login and use Share using its own 'internal' domain.



 

I just want to disable the filter!



There is no real reason why you should need to turn off the filter if it is configured correctly and you are running a standard Share installation. However if you have installed a 3rd party plugin that is not using the Alfresco provided classes for sending XMLHttpRequests or submitting forms you should contact the plugin developer and ask him to read this blog post so he/she can update the plugin. You will then have to make a decision to either uninstall your plugin OR lower the security level in the filter and not check for tokens anymore (at least until a new version of the plugin has been released). To stop checking for tokens, but continuing to check the Origin and Referer headers when available for logged in users, just add the following code snippet in your share-config-custom.xml file:

<config evaluator='string-compare' condition='CSRFPolicy' replace='true'>

   <filter>

      <rule>

         <request>

            <method>POST|PUT|DELETE</method>

            <session>

               <attribute name='_alf_USER_ID'>.*</attribute>

            </session>

         </request>

         <action name='assertReferer'>

            <param name='always'>false</param>

         </action>

         <action name='assertOrigin'>

            <param name='always'>false</param>

         </action>

      </rule>

   </filter>

</config>


If you have custom code that fails for the reason mentioned in the previous section make sure to fix them instead of disabling the filter. It shouldn't take long.



If you still, for what ever reason, want to disable the filter just add the following code snippet in your share-config-custom.xml file:

<config evaluator='string-compare' condition='CSRFPolicy' replace='true'>

   <filter/>

</config>


 

I have a repository webscript or service that I don't want to be accessible through Share's proxy...



This is a bonus feature of having a configurable CSRF filter, that it can be used to completely block certain services in the repository. Perhaps you have an API that only shall be accessible from a) other clients than Share OR b) from server side Java or Javascript code running on the Share server (rather than in the browser as a Java Applet or client side Javascript).



If that is the case you can add the urls to those services to the CSRF filter  and make sure it throws an error when they are accessed. To do this simply:



  1. Copy the entire CSRFPolicy config in share-security-config.xml


  2. Paste it into your share-config-custom.xml file and make sure it is replacing the old config section:

    <config evaluator='string-compare' condition='CSRFPolicy' replace='true'>


  3. Add the following code snippet as the first child to the <filter> element:

    <rule>

       <request>

          <path>/proxy/alfresco/acme/special/services/.*</path>

       </request>

       <action name='throwError'>

          <param name='message'>It is not allowed to access this url from your browser</param>

       </action>

    </rule>





 

A detailed description of the CSRFPolicy configuration



The next code snippet is will give you a detailed description of all available options in the CSRFPolicy configuration. It is probably only worth reading in case you're really interested or have run into trouble.



Cheers and thanks for reading this far!

<config evaluator='string-compare' condition='CSRFPolicy'>



  <!--

    (Mandatory) Only 1 client element is allowed.

    Describes what names are used to communicate the token back and forth

    between the server and the client.

  -->

  <client>



    <!--

      (Mandatory) A client element must have exactly 1 cookie element.

      Name of the cookie that will hold the token, used by the client side to

      grab the value.

    -->

    <cookie>



    <!--

      (Mandatory) A client element must have exactly 1 header element.

      Name of the custom Http header to place the token in when sending a request

    -->

    <header/>



    <!--

      (Mandatory) A client element must have exactly 1 parameter element.

      Name of the parameter to place the token in when sending a request

    -->

    <parameter/>

  </client>



  <!--

    (Mandatory) Only 1 filter element is allowed.

    The filter will look for 1 rule with a matching request and execute its

    actions (if any). An empty filter element means the CSRF filter is disabled,

    in other words will allow all requests to pass.

  -->

  <filter>



    <!--

      (Optional) Zero or more rule elements are allowed.

      A rule contains a description of a request and a set of actions to execute.

    -->

    <rule>



      <!-- (Mandatory) A rule element must have exactly 1 request element -->

      <request>



        <!--

          (Optional) A request element may have exactly 1 method element.

          Holds a regular expression that will be matched against the request's

          method.

        -->

        <method/>



        <!--

          (Optional) A request element may have exactly 1 path element.

          Holds a regular expression that will be matched against the request's

          'share path', i.e. /page/start-workflow or /proxy/alfresco/api/people

        -->

        <path/>



        <!--

          (Optional) A request element may have any number of header elements.

          Holds a regular expression that will be matched by the header specified

          by the name attribute.

        -->

        <header name=''/>



        <!-- (Optional) A request element may have exactly 1 session element -->

        <session>

          <!--

            (Optional) A session may have multipe attribute elements.

            Holds a regular expression that will be matched by the session

            attribute specified by the name attribute. A closed attribute element

            indicates that the session attribute does not exist.

            I.e. <attribute name='Alfresco-CSRFToken'/> means that the token has

            not yet been created.

          -->

          <attribute name=''/>

        </session>



      </request>



      <!--

        (Optional) A rule element may have multiple action elements.

        Below is a list of all available actions:

      -->



      <!-- Generate the token -->

      <action name='generateToken'>

        <!--

          (Mandatory) An 'generateToken' action may have exactly 1 'session'

          param.

          Holds the name of the session attribute in which to place the token,

          shall match the client element's session element above.

        -->

        <param name='session'/>



        <!--

          (Mandatory) A 'generateToken' action may have exactly 1 'cookie' param.

          Holds the name of the cookie in which to place the token, shall match

          the client element's cookie element above.

        -->

        <param name='cookie'/>

      </action>



      <!-- Clear the token value -->

      <action name='clearToken'>

        <!--

          (Mandatory) A 'clearToken' action may have exactly 1 'session' param.

          Holds the name of the session attribute which value shall be cleared,

          shall match the client element's session element above.

        -->

        <param name='session'/>



        <!--

          (Mandatory) A 'clearToken' action may have exactly 1 'session' param.

          Holds the name of the cookie which value shall be cleared, shall match

          the client element's cookie element above.

        -->

        <param name='cookie'/>

      </action>



      <!--

        Assert the request's Referer header matches the current domain.

        If not an error will be thrown.

      -->

      <action name='assertReferer'>

        <!--

          (Mandatory) An 'assertReferer' action may have exactly 1 'always' param.

          Decides when to compare the incoming requests Referer header to the

          current domain, if set to:

          - true: Always compare, even when no Referer header was provided in the

                  request.

          - false: Only compare if a Referer header was provided in the request

        -->

        <param name='always'/>



        <!--

          (Optional) An 'assertReferer' action may have 0 or 1 'referer' param.

          Holds a regular expression that will be matched against the incoming

          Referer header if the incoming Referer header does not match Share's domain.

        -->

        <param name='referer'/>

      </action>



      <!--

        Assert the requets's Origin header matches the current domain.

        If not an error will be thrown.

      -->

      <action name='assertOrigin'>

        <!--

          (Mandatory) An 'assertOrigin' action may have exactly 1 'always' param.

          Decides when to compare the incoming requests Origin header to the

          current domain, if set to:

          - true: Always compare, even when no Origin header was provided in the

                 request.

          - false: only compare if a Origin header was provided in the request

        -->

        <param name='always'/>



        <!--

          (Optional) An 'assertOrigin' action may have 0 or 1 'origin' param.

          Holds a regular expression that will be matched against the incoming

          Origin header if the incoming Origin header does not match Share's

          domain.

        -->

        <param name='origin'/>

      </action>



      <!-- Will throw an error -->

      <action name='throwError'>



        <!--

          (Optional) A 'throwError' action may have exactly 1 'message' param.

          Holds the error message that will be used when throwing the error.

        -->

        <param name='message'/>

      </action>

    </rule>



  </filter>



</config>


 



Introduction

In a previous post I described a simpler approach to constructing a page. This post will delve deeper into exactly what is happening to render that page, how the dependency analysis works and what ultimately ends up on the page.

 

 

Page Rendering

The URL “/share/page/dp/ws/my-new-page” was used to access the page. Once this request is mapped to the Share application the following occurs:

    1. /share/page” is Spring MVC dispatcher servlet context
    2. /share/page/dp/ws/my-new-page” is matched to the template-uri “{pageid}/ws/{webscript}” (“dp” is the id of a page)
    3. Surf maps the “dp” page to the Template-Instance “share-template
    4. The “share-template” is an instance of the “share-template-type” template which (unlike the standard Share page template) is rendered by the WebScript “full-page.get
    5. The FreeMarker template for “full-page.get” sets up the skeleton of the page (including all the standard Share JavaScript variables and resources) and also creates a region the Surf Dojo bootstrap WebScript. It creates the Component and Region for the WebScript passed as the “webscript” argument in the template-uri
    6. Surf binds the Components to the Regions and renders them.
    7. The controller for the WebScript passed on the URL creates a JSON model defining the structure of the page
    8. The WebScript template uses the <@processJsonModel> directive to convert that JSON model into JavaScript, CSS, HTML and i18n resources that are automatically loaded into the page.


If you view the source of a page you should see the Dojo bootstrap configuration and a request to load the main “dojo.js” bootstrap module.


Screenshot of page source showing Dojo bootstrap

If you inspect the resources loaded into the page (e.g. in the Web Developer Tools or Firebug) you should also see the following files imported:

    • “/share/res/890525bee5cf4228da1a0e45b491ed.css”
    • “/share/res/40ef2558ae7e1d0a225e277cc6672d0.js”
    • “/share/res/js/surf/b4dc46167c64e043e227df402bf7bc1.js”


The names of the resources are MD5 checksums generated from the resource contents. This allows the browser to cache each resource indefinitely to avoid the need to download it again. Should the contents of the page change (e.g. an update to an individual modules source code or the page model being customized) then the resource contents will change, a new checksum will be generated and the browser will download the updated version.

 

 

Screenshot of Web Development Tools showing resources imported

 

“/share/res/890525bee5cf4228da1a0e45b491ed.css”

This contains all of the CSS resources referenced by widgets included in the page. Note that images are directly encoded into the resource to avoid additional HTTP requests.

 

Screenshot showing Web Development Tools showing aggregated CSS resource

 

“/share/res/40ef2558ae7e1d0a225e277cc6672d0.js”

This contains the messages object construction code and the call to instantiate the main “page” widget (shown highlighted). Note that it is declaring a requirement on the final resource loaded – it is requesting our dynamically built resource that includes all the dependencies.

 

Screenshot showing Web Development Tools with Page widget instantiation

 

“/share/res/js/surf/b4dc46167c64e043e227df402bf7bc1.js”

This is the layer that has been built through dynamic dependency analysis. Note how it contains core Dojo modules that have been identified as dependencies.

 

Screenshot showing Web Development Tools showing dependencies source

 

Dependency Analysis Beans

If you look in the “spring-surf-services-context.xml” file (this is included in “spring-surf-<n>.<n>.<n>.jar”) you’ll find a number of new bean definitions. Of these new beans the “dojo.dependency.handler” defines the main class for analysing the dependencies on a page. It has a property called “dependencyRules” that is a list of all the individual dependency handling beans. By default the following beans are referenced:

    • “define.dojo.js.dependency.rule”
    • “define.dojo.css.dependency.rule”
    • “define.dojo.widgets.dependency.rule”
    • “define.dojo.i18n.dependency.rule”
    • 'define.dojo.non.amd.dependency.rule'


The purpose of each bean is to analyse the source code of each widget and identify one or more of 4 different types of dependency to include in the page:

    • Text (e.g. HTML templates)
    • JavaScript (both AMD and non-AMD modules)
    • CSS
    • i18n properties


    All of these beans extend the “org.springframework.extensions.surf.DojoDependencyRule” class and use regular expressions configured in the bean context to identify the different dependencies. The “dojo.dependency.handler” passes the minified source of each module to each rule processor to identify additional dependencies (this means that the Regular Expression doesn't need to address whitespace characters or comments).

     

    It’s possible to either reconfigure the regular expressions for the beans provided or create and add entirely new beans to the “dependencyRules” list to satisfy your specific requirements.

     

    Dependencies are recursively processed until they have all been analysed. As each module is analysed it is added to a cache and if it is referenced again the cached version will be used. Surf does not allow a module to be analysed twice so even if circular dependencies are declared it will not fall into an infinite loop.

     

    Even if it is not possible to identify all of the dependencies it is not a major problem. Ultimately Surf is just making an attempt to construct a layer containing all of the resources required for that page. If a resource is missing then the Dojo loader will simply asynchronously request it. In fact if you view the source on Share pages you will notice that the occasional AMD module is still getting asynchronously requested – there’s obviously a little bit of tightening up we could do on the dependency analysis expressions!

     

    Page Widget Configuration

    By default Share uses the “alfresco/core/Page” module as the root widget on the page (the default in a “vanilla” Surf application is “surf/core/Page” which is a much simpler version). However it is possible to update the “surf.xml” configuration to use any widget that you would prefer – simply update the “page-widget” element to map to whatever widget you’d like to use.

     

    Summary

    Hopefully this post has gone some way to explaining how Surf performs dependency analysis of the widgets included in a page in order to construct a build layer that can then be requested on the page. Although this series of posts relates specifically to Alfresco the Dojo dependency analysis code is part of Surf and as such could be used in independent applications.

    Localization Considerations

    Posted by ddraper Mar 1, 2013

    Introduction

    In my second post in this series I showed how to create a page from existing widgets and in my last post I showed how to create a custom widget. In custom widget showed how to specify i18n properties files of different locales in order to ensure that the widget labels could be rendered in different languages, however I didn't demonstrate how to localize pages nor how the two approaches work together.

     

    How it Works Currently...

    If you’re familiar with WebScripts you’ll know that you can provide i18n properties files with the same prefix that can then be accessed in the JavaScript controller by calling:

    msg.get(<key>)

     

    or from the FreeMarker template by calling:

    ${msg(“<key>”)}


    You may also know that traditionally Share would pass all of the messages from a WebScript into the widgets that it instantiated by calling its '.setMessages()' function.

    Finally, you should also be aware that there is a global properties files that can be used throughout Share (“common.properties” and “slingshot.properties” that can be found in '/share/WEB-INF/classes/alfresco/messages').

    The contents of all of these files ultimately end up on the page in the JavaScript global variable “Alfresco.messages”. This is a map with the following attributes, “global” and “scope”.

      • “global” contains all the messages from the global properties
      • “scope” is a map of widget name to messages map

     

    The Share widgets “.setMessages()” function adds its own name as a key into the scope map and assigns all the supplied messages as an object against that key. For example, if the “Alfresco.DocumentList” widget is instantiated then “Alfresco.messages.scope['Alfresco.DocumentList']' can be used to access it’s messages.

     

    How the Updated Approach Works...

    We've ensured that the updated development approach is consistent with this old pattern and have intentionally not followed the standard Dojo pattern. The new approach uses the same “Alfresco.messages” object (although this can be reconfigured if you want to use a different root variable) and still sets the “global” and “scope” attributes.

    If you create a widget with an “i18nScope” attribute then this is the scope into which the widgets encapsulated messages will be added. If no “i18nScope' attribute is defined then the messages will go into a scope called “default” (unless the widget extends another widget in which case it will inherit its “i18nScope” attribute).

    The i18n properties from the WebScript that processes the JSON model will automatically be placed into a new attribute of “Alfresco.messages” called “page”.

    Whenever the “.message” function is called from “Alfresco/core/Core” (see previous post) all applicable scopes are searched, e.g.

      • global
      • page
      • default scope
      • all inherited scopes
      • widget scopes


    ...and the most specific result will “win”.


    When creating a custom widgets there is obviously a distinction to be drawn between

      • labels that never change
      • variable labels that can be selected from
      • completely custom labels

     

    For example, the label for a menu item cannot realistically be included as part of the widget but an error message could be. When accepting configurable labels its worth passing them through the “.message()” function in case a message key (rather than a localized message) has been provided as if no match has found then the supplied value is returned.

     

    This means that when constructing the JSON model for a page you could provide:

    config: {
       label: “message.key”
    }

    or

    config: {
       label: msg.get('message.key')
    }

     

    At first glance these might appear identical, but if the widget defines a message with the key “message.key” then this will “win” over any message that the WebScript might be able to resolve.

     

    Language Variations

    It’s also worth bearing in mind that because the widgets process locale specific properties files in exactly the same way as WebScripts it is possible to simply reference a WebScripts properties file in the “i18nRequirements” attribute of a Widget. In a future post you’ll see how this can help you to wrap existing widgets easily so that they can be mixed with the our new library of widgets.

     

    Summary

    Hopefully this post has explained how i18n properties are handled in the new approach to page and widget construction. We have made efforts to ensure that the updates are compatible with the existing messages handling and have deliberately kept with the Alfresco approach rather than adopting the standard Dojo approach to avoid creating a divide. Ultimately we've done as much as possible to ensure that Surf takes care of all of the hard work for you.

    Filter Blog

    By date: By tag: