New $services.rendering.transform() API

Hi everyone,

I need to convert the content of a wiki page from its storage XWiki 2.1 syntax to the new UniAst syntax used by the BlockNote editor. I would like to perform this conversion from a Velocity script because the BlockNote integration relies on the Edit Module. Technically, I could do it from Java, but this means moving some HTML generating code from a template to Java. Moreover, I can’t rely on Document#getRenderedContent() API because the Edit Module decouples the edited content from the source document, and there’s no safe Document#getRenderedContent() signature to cover my case.

The rendering script service already has APIs to parse and render the content. It’s missing an API to execute the rendering transformations. Note that executing rendering transformations means executing script macros which is an important attack vector so it needs to be properly secured. The means using the right “transformation context”. In order to create this context we need to know:

  • the target syntax
  • whether to enable restricted mode or not
  • the lists of transformations
  • the context document (to evaluate script rights on)

In order to accept these parameters and possibly others in the future I suggest we use the builder pattern. The new API could be used like this:

#set ($targetSyntax = 'uniast/1.0')
#set ($xdom = $services.rendering.parse($content, $contentSyntax))
#set ($discard = $services.rendering.transform($xdom)
  .withTransformations(['macro'])
  .withTargetSyntax($targetSyntax)
  .withRestricted(true)
  .withContextDocument($editedDocument)
  .perform())
#set ($renderedContent = $services.rendering.render($xdom, $targetSyntax))

The script macros will be executed with the rights of the current context user, evaluated against the specified context document.

WDYT?

Thanks,
Marius

+1 thanks.

Small note: the javadoc should specify what is the order of the transformations executed (the order of the passed list or the transformation order).

That’s no fully accurate. The rights are evaluated on the content (secure) document, you just happen to also use it as context document by default.

+1 for the general idea, just need to refine a bit the naming (and there might be surprises requiring adding more stuff to the builder while implementing this)

While implementing this I realized that I need to introduce a new API to control the list of rendering transformations to execute per execution. We currently support customizing the list of transformations at the level of rendering configuration and at the level of HTTP request (through a request parameter). I think it makes sense to be able to set the list of transformations in the transformation context as well. This will lead to the following fallback chain:

TransformationContext > HTTP request > RenderingConfiguration

This means:

  • adding TransformationContext#get/setTransformationNames; I propose we use the same naming as in the existing RenderingConfiguration#getTransformationNames()
  • modify DefaultTransformationManager to check the list from the transformation context first

You can review these changes at XRENDERING-803: Add support for overwriting the list of transformations to execute from the transformation context by mflorea · Pull Request #370 · xwiki/xwiki-rendering · GitHub .

The rest of the changes, for the xwiki-platform side, can be seen at XWIKI-24003: Add a script service to execute rendering transformations by mflorea · Pull Request #5170 · xwiki/xwiki-platform · GitHub . I received two interesting comments so far, to which I add my own observation at the end:

  • the naming of $services.rendering.transform() is misleading because it doesn’t perform the transformation right away, but it rather creates the transformation context
  • we’re adding a new way / API to perform rendering transformations; there should be a single way ideally
  • the Velocity syntax I pasted above doesn’t work; we actually need to use something like:
    #set ($xdom = $services.rendering.parse($source.content, $source.syntax))
    #set ($discard = $services.rendering.transform($xdom
      ).withId("BlockNote:${source.documentReference}"
      ).withSyntax($source.syntax
      ).withTargetSyntax($targetSyntax
      ).withRestricted($restricted
      ).withTransformations(['macro']
      ).withContentDocument($source.documentReference
      ).execute())
    #set ($renderedContent = $services.rendering.render($xdom, $targetSyntax))
    
    which is not that nice…

Given this, I’d like to propose something else:

  • we keep a single API to perform rendering transformations, which is TransformationManager
  • we make the platform implementation, XWikiTransformationManager, safer by using AuthorExecutor
  • but AuthorExecutor needs the content / source document reference, so we extend TransformationContext with XWikiTransformationContext, adding get/setContentDocumentReference
  • XWikiTransformationManager can then check if the passed TransformationContext can be cast to XWikiTransformationManager and use the AuthorExecutor in that case
  • on the script service side we add two APIs:
    $services.rendering.createTransformationContext()
    $services.rendering.transform($xdom, $transformationContext)
    
  • if we need more information in the future (e.g. for security reasons) we can add that to the XWikiTransformationContext, and the script service doesn’t need any change.

Do you see any problem with this?

Thanks,
Marius