Working with the CrafterCMS Groovy Sandbox

CrafterCMS supports a powerful Groovy-based scripting layer for server side programming. CrafterCMS's configurable sandbox policies determine what the scripting layer can and cannot do. Some syntax is more likely to trigger sandbox flags than others. In this tutorial, we will offer some convenient tips to work with the Groovy Sandbox. You can also refer to this documentation on how to configure the sandbox.

Tips for coding

In the following examples, we will use a sample site that is created with the blueprint Editorial. Check this documentation on how to create a site.

Looking up a Bean by ID

We will demonstrate how to look up a bean by ID, access a request parameter, and import an external library to your script. Following beans are enabled by default without any additional configuration: https://docs.craftercms.org/en/4.0/developers/projects/engine/api/groovy-api.html#groovy-api

Here is the steps in case you want to include a bean that is not in the list:

  1. Add the bean to Engine Site Application Context : In this example, we include the bean crafter.textEncryptor


  1. Add a new script bean.get.groovy with the following content:



 def   result = [:]  

def textEncryptor = applicationContext.getBean("crafter.textEncryptor")

def rawText = "This is a test"

def encryptedText = textEncryptor.encrypt(rawText)

def decryptedText = textEncryptor.decrypt(encryptedText)

result.rawText = rawText
result.encryptedText = encryptedText
result.decryptedText = decryptedText

return result

Note that we use applicationContext.getBean(“crafter.textEncryptor”) instead of applicationContext[“crafter.textEncryptor”] to get the new bean which we have included in the previous step. 

Using GetBean avoids having to update the sandbox policies to allow the ["..."] syntax which can be unsafe.   You can learn more about the groovy sandbox black list polices by referring to this documentation 

For reference here is the policy we did not need to modify on the Groovy Sandbox Black list.

 # staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods getAt java.lang.Object java.lang.String 



3. Verify the script is running correctly


 curl   'http://localhost:8080/api/bean.json'   \  
  -H 'Cookie: crafterSite=<YOUR_SITE>; JSESSIONID=<YOUR_SESSION>'
{ 
    "rawText": "This is a test" ,
    "encryptedText": "CCE-V1#sT17oILeZqfHQvLSRvo8Bo2Kys6oYSTVz7K8RL0+npc=" ,
    "decryptedText": "This is a test"
}


Accessing a Request Parameter

Let’s take a look on how to access a parameter from HTTP GET and POST requests.


HTTP GET Request

In order to get parameters from a GET request, use `params` variable which is available by default.

Let take a look at the script `search.get.groovy`:



 import   org.craftercms.sites.editorial.SearchHelper  

def userTerm = params.userTerm
def categories = params[ "categories[]" ]
def start = params.start ? params.start as Integer : 0
def rows = params.rows ? params.rows as Integer : 10
def searchHelper = new SearchHelper(elasticsearchClient, urlTransformationService)
def results = searchHelper.search(userTerm, categories, start, rows)

return results


 curl   'http://localhost:8080/api/search.json?useTerm=winter&start=0&rows=2&categories%5B%5D=style'   \  
  -H 'Cookie: crafterSite=<YOUR_SITE>; JSESSIONID=<YOUR_SESSION>'


In this example,  each variable has following value:

  • userTerm : winter

  • categories : style (Note that [] has been encoded to %5B%5D as a part of the URI)

  • start : 0

  • rows : 2


HTTP POST request

In order to get parameters from a POST body, we must read POST body first. The search example above can be rewrite to use POST as of following:

  import   org.craftercms.sites.editorial.SearchHelper  
import groovy.json.JsonSlurper

def reader = request.getReader()
def body = ""

def content = reader.readLine()
while (content != null ) {
    body += content
    content = reader.readLine()
}

def searchParams = new JsonSlurper().parseText(body)

def userTerm = searchParams.userTerm
def categories = searchParams[ "categories[]" ]
def start = searchParams.start ? searchParams.start as Integer : 0
def rows = searchParams.rows ? searchParams.rows as Integer : 10
def searchHelper = new SearchHelper(elasticsearchClient, urlTransformationService)
def results = searchHelper.search(userTerm, categories, start, rows)
return results


A sample request:

 curl --location --request POST   'http://localhost:8080/api/search.json'   \  
--header 'authorization: Bearer <Your_Auth_Key>' \
--header 'Cookie: crafterSite=<YOUR_SITE>; XSRF-TOKEN=<YOUR_TOKEN>; JSESSIONID=<YOUR_SESSION>' \
--header 'Content-Type: application/json' \
--data-raw '{
    "userTerm": "winter",
    "categories[]": "style",
    "start": 0,
    "rows": 2
}'


Loading Grapes

There are cases that you may want to include an external library to your Groovy script. In order to achieve this, you can use grapes annotation @Grab .

Here is an example script that load the module commons-math3 from org.apache.commons . 

First, create a new script under scripts > rest with name grape.get.groovy :


 @Grab  (group=  'org.apache.commons'  , module=  'commons-math3'  , version=  '3.6.1'  , initClass=  false  )  

import org.apache.commons.math3.fraction.Fraction
import org.apache.commons.math3.fraction.FractionFormat

def lhs = new Fraction( 1 , 4 );
def rhs = new Fraction( 2 , 7 );
def sum = lhs.add(rhs);

def str = new FractionFormat().format(sum);

return str

Send an HTTP GET request:

 curl   'http://localhost:8080/api/grape.json'   \  
  -H 'Cookie: crafterSite=<YOUR_SITE>; JSESSIONID=<YOUR_SESSION>'  

You should get the result:

 "15 / 28" 

Note : If your Groovy code need to use external dependencies you can use Grapes, however, when the Groovy sandbox is enabled dependencies can only be downloaded during the initial compilation and not during runtime. For this reason it is required to add an extra parameter initClass=false in the annotations to prevent them to be copied to the classes.