Customize pdf export using Script Runner for Confluence Server

Ramakrishnan Srinivasan
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
September 1, 2020

Hi, 

I have made some customization of pdf export of a Confluence page. Sharing my approach here for others to try/benefit.

My Setup:

  • Confluence Server - 7.3.2
  • Script Runner for Confluence - 6.2.0-p5
  • Table Filter and Charts for Confluence - 5.3.25
  • Confluence has HTML macro enabled

My Approach:

  • Create Custom Menu to do the job
  • Create a temporary page which can be trashed once export job is done
  • Get the page content as string, customize it (in my case remove jira issues hyperlinks)
  • Edit the temporary page and update it with customized content, redirect the page;
  • With delay from javascript redirect it to pdf export link again

My Configurations

  • In Confluence Script Runner - Fragment -Create Script Fragments - Custom WebItem
    • What Section this should go in - system.content.action/secondary
    • Key: external-pdf
    • Menu Text - Custom
    • Weight - 1
    • Condition (such that this custom menu is displayed only in some pages)
      • if(context.space.key == "SOME_SPACE_KEY" && context.page.title.contains("SOME UNIQ TITLE STRING ") && !(context.page.title.contains("NEGATE TITLE"))) {
        return true
        } else {
        return false
        }
    • Do What - Navigate to Link
    • Link - https://HOST/wiki/rest/scriptrunner/latest/custom/exportPage_toPDF?spaceKey=<SOME SPACE KEY>&parentPageTitle=${page.title}
  • In Confluence Script Runner - Jobs - setup to trash the temporary page
    • import com.atlassian.sal.api.component.ComponentLocator
      import com.atlassian.confluence.spaces.Space
      import com.atlassian.confluence.spaces.SpaceManager
      import com.atlassian.confluence.core.DefaultDeleteContext
      import com.atlassian.confluence.pages.Page
      import com.atlassian.confluence.pages.PageManager

      def spaceManager = ComponentLocator.getComponent(SpaceManager)
      def pageManager = ComponentLocator.getComponent(PageManager)

      def parentPage=pageManager.getPage("SOME_SPACE_KEY", "TO DELETE PAGES")
      def childPages = pageManager.getDescendants(parentPage)

      if(childPages) {
           childPages.each { thisChild ->
                     pageManager.trashPage(thisChild, DefaultDeleteContext.DEFAULT)
            }
      }

  • In Confluence Script Runner - REST Endpoint
    • //This script is loaded in Confluence - Script Runner - Rest End Point
      //It takes the spaceKey and page title as url parameters
      //gets page content
      //replacesAll href of jira issue links
      //relacesAll Back to Top CDATA
      //creates a child page from replaced string
      //redirects that child page for pdf download after the page is loaded in browser

      /*import org.apache.log4j.Level
      import org.apache.log4j.Logger
      def logx = Logger.getLogger("com.acme.workflows")
      logx.setLevel(Level.DEBUG)*/

      import org.apache.http.HttpRequest
      import org.apache.http.protocol.HttpContext
      import groovy.json.JsonBuilder
      import groovy.json.JsonSlurper
      import org.apache.http.HttpRequestInterceptor
      import groovyx.net.http.HTTPBuilder
      import net.sf.json.JSONArray
      import groovyx.net.http.RESTClient
      import static groovyx.net.http.Method.GET
      import static groovyx.net.http.ContentType.JSON
      import groovy.transform.Field

      import org.codehaus.groovy.runtime.MethodClosure
      import org.apache.http.HttpEntity;
      import org.apache.http.HttpEntityEnclosingRequest

      import java.io.InputStreamReader

      import com.atlassian.confluence.setup.settings.SettingsManager
      import com.atlassian.confluence.core.BodyContent
      import com.atlassian.confluence.core.BodyType
      import com.atlassian.confluence.core.DefaultSaveContext
      import com.atlassian.confluence.core.DefaultDeleteContext
      import com.atlassian.confluence.pages.DuplicateDataRuntimeException
      import com.atlassian.confluence.pages.Page
      import com.atlassian.confluence.pages.PageManager
      import com.atlassian.confluence.pages.templates.PageTemplateManager
      import com.atlassian.confluence.security.Permission
      import com.atlassian.confluence.security.PermissionManager
      import com.atlassian.confluence.spaces.Space
      import com.atlassian.confluence.spaces.SpaceManager
      import com.atlassian.confluence.user.AuthenticatedUserThreadLocal
      import com.atlassian.sal.api.component.ComponentLocator
      import com.onresolve.scriptrunner.canned.confluence.utils.PermissionDeniedException
      import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
      import groovy.json.JsonOutput
      import groovy.transform.BaseScript
      import groovy.transform.Field
      import org.apache.log4j.Logger

      import javax.ws.rs.core.MultivaluedMap
      import javax.ws.rs.core.Response

      import java.text.SimpleDateFormat

      import groovy.time.TimeCategory
      import groovy.time.TimeDuration


      Date start = new Date()

      @Field SpaceManager spaceManager = ComponentLocator.getComponent(SpaceManager)
      @Field PageManager pageManager = ComponentLocator.getComponent(PageManager)
      @Field PermissionManager permissionManager = ComponentLocator.getComponent(PermissionManager)
      @Field PageTemplateManager pageTemplateManager = ComponentLocator.getComponent(PageTemplateManager)
      //@Field Logger log = Logger.getLogger("com.onresolve.scriptrunner.runner.ScriptRunnerImpl")

      @BaseScript CustomEndpointDelegate delegate

      //End point name is the same as the method's name
      //exportPage_toPDF_withoutJira_hyperLinks(httpMethod: "GET", groups: ["confluence-administrators", "confluence-users"]) { MultivaluedMap queryParams, String body ->
      exportPage_toPDF_withoutJira_hyperLinks(httpMethod: "GET") { MultivaluedMap queryParams, String body ->

      Date date = new Date()
      String datePart = date.format("dd/MMM/yyyy")
      String timePart = date.format("HH:mm:ss.SSS")
      String dateTimeStamp = date.format("dd/MMM/yyyy HH:mm:ss.SSS")

      pageManager = ComponentLocator.getComponent(PageManager)

      def spaceKey = queryParams.getFirst("spaceKey").toString()
      def parentPageTitle = queryParams.getFirst("parentPageTitle").toString()

      try {
      parentPage=pageManager.getPage(spaceKey, parentPageTitle)
      def contentEntityObject = parentPage.getEntity()
      def parentPageBody=parentPage.getBodyAsString()

      def space = spaceManager.getSpace(spaceKey) as Space
      def childPageTitle = parentPageTitle + " - External"
      def childPage = pageManager.getPage(spaceKey, childPageTitle)

      if(childPage) {
      pageManager.trashPage(childPage, DefaultDeleteContext.DEFAULT)
      }

      def HTML_A_HREF_TAG_PATTERN_jira = "\\s*(?i)href\\s*=\\s*\"([^\"]*jira/browse/[^\"]*\")"
      def regex_BacktoTop = "<p><ac:link ac:anchor=\"_wiki_toc\"><ri:content-entity ri:content-id=\"\\d+\" /><ac:plain-text-link-body><!\\[CDATA\\[Back to Top\\]\\]></ac:plain-text-link-body></ac:link></p>"


      def childPageContent = parentPageBody.replaceAll(HTML_A_HREF_TAG_PATTERN_jira, "dummy")
      childPageContent = childPageContent.replaceAll(regex_BacktoTop, "")


      def regex_jiraMacro = "<p style=\"font-size: 15.0px;\"><strong>G3 Issue used to create this Release Page</strong></p><p><ac:structured-macro ac:name=\"jira\" ac:schema-version=\"\\d+\" ac:macro-id=\"([a-zA-Z0-9*--]+)\"><ac:parameter ac:name=\"server\">Jira</ac:parameter><ac:parameter ac:name=\"serverId\">([a-zA-Z0-9*--]+)</ac:parameter><ac:parameter ac:name=\"key\">((?<!([A-Za-z]{1,10})-?)[A-Z]+-\\d+)</ac:parameter></ac:structured-macro></p>"
      childPageContent = childPageContent.replaceAll(regex_jiraMacro, "")

      def regex_history = "<h1>Document history</h1><p><ac:structured-macro ac:name=\"version-history\" ac:schema-version=\"\\d+\" ac:macro-id=\"([a-zA-Z0-9*--]+)\" /></p>"
      childPageContent = childPageContent.replaceAll(regex_history, "")

      def toDelete_parentPage=pageManager.getPage(spaceKey, "TO DELETE PAGES")

      def createdChildPageId = createBasicPage(space, toDelete_parentPage, childPageTitle, "Temporary Page")

      def tableFilterString = "<ac:structured-macro ac:name=\"table-filter\" ac:schema-version=\"1\""

      def loadResources = "" //tf-export-ready is Table filter element

      if(childPageContent.contains(tableFilterString)) {
      loadResources = "<ac:structured-macro ac:name=\"html\">\
      <ac:plain-text-body><![CDATA[\
      <script type=\"text/javascript\">\
      AJS.toInit(function() {\
      AJS.\$('#editPageLink').hide();\
      AJS.bind('tf-export-ready', function() {\
      setTimeout(function() {AJS.\$(location).prop('href', 'https://HOST/wiki/spaces/flyingpdf/pdfpageexport.action?pageId=" + createdChildPageId + "');}, 2000);\
      });\
      });\
      </script>]]>\
      </ac:plain-text-body>\
      </ac:structured-macro>"
      } else {
      //removed AJS.bind('tf-export-ready', function() {\
      loadResources = "<ac:structured-macro ac:name=\"html\">\
      <ac:plain-text-body><![CDATA[\
      <script type=\"text/javascript\">\
      AJS.toInit(function() {\
      AJS.\$('#editPageLink').hide();\
      setTimeout(function() {AJS.\$(location).prop('href', 'https://HOST/wiki/spaces/flyingpdf/pdfpageexport.action?pageId=" + createdChildPageId + "');}, 2000);\
      });\
      </script>]]>\
      </ac:plain-text-body>\
      </ac:structured-macro>"
      }

      childPageContent = childPageContent.replaceAll("dummy", "") + loadResources

      editPage("${createdChildPageId}", childPageTitle, childPageContent, "2")

      pageUrl = "https://HOST/wiki/pages/viewpage.action?pageId=${createdChildPageId}"
      return Response.temporaryRedirect(URI.create(pageUrl)).build()
      } catch(Exception ex) {
      //pageIdMap["Exception"] = "Exception to get page or append page for spaceKey = ${spaceKey} and title = ${title}: ${ex.toString()}"
      }
      }

      /**
      * Create a basic page. This is not linked in any hierarchy.
      *
      * @param space The space that this page belongs to.
      * @param title The title of the page we are creating.
      * @param content The content of the page we are creating.
      *
      * @return The create page object.
      */
      def createBasicPage(Space space, Page parentPage, String title, String content) {
      def pageManager = ComponentLocator.getComponent(PageManager)
      def page = new Page()
      def bodyContent = new BodyContent(page, content, BodyType.XHTML)
      page.with {
      setVersion(1)
      setSpace(space)
      setTitle(title)
      setBodyContent(bodyContent)
      setCreator(AuthenticatedUserThreadLocal.get())
      }
      linkPages(parentPage, page)
      pageManager.saveContentEntity(page, DefaultSaveContext.SUPPRESS_NOTIFICATIONS)

      return page.id
      }

      /**
      * Link a parent and a child page together. This method creates a bi-directional relationship between the two pages.
      *
      * @param parent The parent page that we wish to link.
      * @param child The child page that we wish to link.
      */
      void linkPages(Page parent, Page child) {
      // Set the parent page on the child
      child.setParentPage(parent)
      // Set the child page on the parent
      parent.addChild(child)
      // Set the ancestors on the child page
      def ancestors = []
      def parentPageAncestors = parent.ancestors as List
      if (parentPageAncestors) {
      ancestors.addAll(parentPageAncestors)
      }
      ancestors.add(parent)
      child.setAncestors(ancestors)
      }


      void editPage(String pageId, String pageTitle, String pageContent, String pageVersion) {
      //https://community.atlassian.com/t5/Confluence-questions/How-to-edit-the-page-content-using-rest-api/qaq-p/904345
      //def logx = Logger.getLogger("com.acme.workflows")
      //logx.setLevel(Level.DEBUG)

      def pageManager = ComponentLocator.getComponent(PageManager)

      def params = [
      id: pageId,
      type: "page",
      title: pageTitle,
      body: [storage:[value:pageContent, representation:"storage"]],
      version: [number:pageVersion]
      ]

      def jsonString = new JsonBuilder(params).toString()

      def rest_api_str = "https://HOST/wiki/rest/api/content/${pageId}"
      def http = new HTTPBuilder(rest_api_str)
      try { def json = http.request(groovyx.net.http.Method.PUT, groovyx.net.http.ContentType.JSON) { req ->
      req.addHeader('Authorization', 'Basic ' + 'USER:PASSWORD'.bytes.encodeBase64().toString())
      body = jsonString
      response.success = { resp, jsonData ->
      assert resp.status == 200
      return jsonData
      }
      response.failure = { resp ->
      return resp.status
      }
      }
      } catch(Exception ex) {

      }
      }

       

       

 

0 comments

Comment

Log in or Sign up to comment
TAGS
AUG Leaders

Atlassian Community Events