Once upon a time, things were simple.  If you wanted to retrieve a page using the Confluence Java API, you simply called getPage().    Fetching Spaces was similarly easy, and intuitive.

Those days are over.  The methods are deprecated. Instead, we now need to use SpaceService and ContentService to manage spaces and content, respectively.  Let’s take a look at some examples of how a task would have been accomplished with the PageManager and SpaceManager, and compare that to how those tasks would be accomplished today.

This the code required to return all page content for all spaces, using PageManager and Spacemanager:
 import com.atlassian.confluence.pages.PageManager
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.confluence.spaces.SpaceManager

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

def spaces = spaceManager.getAllSpaces()

spaces.each { space ->
    def pagesInSpace = pageManager.getPages(space, true)
    
    pagesInSpace.each { page ->
        log.warn(page.getBodyAsString())
    }
}
 

 

Here’s the same code, using the SpaceService and ContentService classes:

 import com.atlassian.confluence.api.model.Expansions
import com.atlassian.confluence.api.model.content.ContentRepresentation
import com.atlassian.confluence.api.model.content.ContentBody
import com.atlassian.confluence.api.model.content.Content
import com.atlassian.confluence.api.model.content.Space
import com.atlassian.confluence.api.model.Expansion
import com.atlassian.confluence.api.model.pagination.PageResponse
import com.atlassian.confluence.api.service.content.SpaceService
import com.atlassian.confluence.api.service.content.ContentService
import com.atlassian.confluence.api.model.content.ContentType
import com.onresolve.scriptrunner.runner.ScriptRunnerImpl
import com.atlassian.confluence.api.model.pagination.SimplePageRequest


def contentService = ScriptRunnerImpl.getPluginComponent(ContentService)
def spaceService = ScriptRunnerImpl.getPluginComponent(SpaceService)
 
SimplePageRequest pageRequest = new SimplePageRequest(0, 10)
 
PageResponse < Space > spaceResults = spaceService.find(new Expansion('name')).fetchMany(new SimplePageRequest(0, 10))
 
List < Space > 

Mitigating CORS Errors With Custom Jira REST API Endpoints

If you dive into the world of REST requests and APIs, you may encounter a CORS error that prevents your request from completing. CORS stands for Cross-Origin Resource Sharing.  Same-origin is a security feature in browsers that prevents requests coming from one place (origin) to access resources in a different domain.  CORS allows web pages to access resources on a different network by providing a standard for safely allowing cross-origin requests.

Let’s talk about the example that I encountered.  I wrote a JavaScript macro for Confluence Server, and I was trying to access a third-party API using that macro.  However, Confluence macros run in the browser when the page loads, rather than running on the back-end Confluence server itself.   Thus, while the Confluence server may be set up to address CORS, your browser almost certainly is not, and the request gets blocked.

We can address this by creating a custom REST API endpoint in Confluence (or Jira).   In this way, we have the server making the request to the third party API, and the macro makes the request to the internal API.

In other words, the custom REST API endpoint acts

There may come a day when you’re asked to create a large number of Confluence pages. Rather than doing it by hand, why not script it?

This Python script essentially does two things: it reads the CSV file, and it sends page creation requests to a Confluence server.   

For each row in the CSV file, it assumes the page name should be the value in the first cell of the row.  It then generates an HTML table that is sent as part of the page creation request. 

Rather than generating HTML, this could be useful for setting up a large number of template pages, to be filled in by various departments.  It could also run as a job, and automatically create a certain selection of pages every week or month, to store meeting notes or reports.

Please note that in order to connect to the Confluence server, you’ll need to generate a Personal Access Token.

 

 import csv
import requests
import json
import html
import logging

# Initialize logging
logging.basicConfig(level=logging.ERROR)

api_url = 'https://<url>.com/rest/api/content/'
#What's the URL to your Confluence DC instance?


file_path = "<your CSV file path>"
#where is the file stored locally?

parent_page_id = "<your parent page ID>"

Confluence Meta-Macros and the joy of being organized

Let’s talk about meta-macros.  That is, macros that examine other macros.   I just made up the term, so don’t be concerned if you can’t find other examples on the internet.

If you wanted some insight into which pages in your Confluence Instance were using a specific macro, how would you find that information?

You could certainly check each page manually, but that sounds dreadful.

One option to get Macro information is this ScriptRunner script that I wrote, which examines the latest version of each page in each Space for references to the specified macro:

 import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.confluence.spaces.SpaceManager
import com.atlassian.confluence.pages.PageManager
import com.atlassian.confluence.pages.Page
def pageManager = ComponentLocator.getComponent(PageManager)
def spaceManager = ComponentLocator.getComponent(SpaceManager)
def spaces = spaceManager.getAllSpaces()
  
def macroRef = 'ac:name="info"'
  
spaces.each {
  spaceObj ->
    def pages = pageManager.getPages(spaceObj, false)
  pages.each {
    page ->
      if (page.getBodyContent().properties.toString().contains(macroRef) && page.version == page.latestVersion.version) {
        log.warn("'${page.title}' (version ${page.version}) is the latest version of the page, and contains the target macro")
      }
  }
}
 

 

But what if you wanted MORE information?  What if you wanted to know every macro running on every page in the system, and you didn’t have ScriptRunner to do it for you?  In that

Introduction

Well, it finally happened.  I finally had to start learning JavaScript.

It’s actually not that bad, I probably should have learned a while ago.  My use case for it is writing Confluence Macros and plugins for both Confluence and Jira. I started with the plugins, for simplicity’s sake.  
My inspiration came from a post on the Atlassian Community Forums. Someone had requested a way to essentially mirror the setup of a macro. But they wanted to mirror the most recent child page, of a parent page.
I think that without pretty strong knowledge of Confluence and the REST API, I’d have struggled to complete this.  It enough work to learn JavaScript’s basic tenets as I went.

 

Digging Into The Problem

Okay so what do we actually need the script to do? We need it to:

  •  Figure out the most recently updated child page of a parent page
  •  Fetch the macro setup of the child page
  • Update the parent page accordingly

These are the three high-level functions that the macro needs to accomplish. 

Figuring out the most recently updated child page wasn’t hard.  You can make a call to baseURL + pageID + “/child/page?limit=1000&expand=history.lastUpdated. This returns a list

As is often the case, the point of this blog isn’t so much to explain how to do something complicated. The point is that I’m trying to explain something simple that should be easy to find an answer for, but was not. In this case my question was “what on earth is a UserTemplate (User Template) in Confluence”?

On the surface, it seems like creating a new user in Confluence should be a pretty straightforward process.   There’s a UserAccessor class, and that class has a createUser() method.   However, the expected inputs to that method are a User Template object and a Credentials object. From the class documentation:

  • createUser

     ConfluenceUser createUser(com.atlassian.user.User userTemplate,
                              com.atlassian.user.security.password.Credential password)

The import required to work with Credential is spelled out for us, but the userTemplate is a different story.   There’s virtually no documentation on what that means, and no amount of Googling “Confluence User Template”, “UserTemplate”, “Confluence Create User Template” will actually tell you what to do. Part of the issue is that “template” means several different things in the context of Confluence, so that muddies the waters.

Let’s cut to the chase. Here’s the code that I eventually came up with:

 import com.atlassian.confluence.api.model.people.User
import com.atlassian.sal.api.component.ComponentLocator
import 

Introduction

In a bid to contribute more to the Atlassian Community, I took a look at the most recent requests and questions on the Forums.  One that caught my eye was a request for a Confluence Macro that would:

“…display on the restricted page exactly who has access (including a breakout of all group members, not just the group name) to create transparency and build confidence in the selected user group that they are posting in the appropriately restricted area.”

I’d never created a Confluence Macro before, and this seemed like a challenge I could meet.

Please note that this isn’t a how-to on creating Macros, but really just an accounting of my experience learning the tool.

Getting Started

The first thing I did was check to see what Atlassian has to say on the subject. Confluence Macros are written in Apache Velocity, which is quite different from the Groovy that I’m used to working with.

All of the functional lines in Velocity start with a #, which makes a Velocity script look like one big page of commented-out code.  The truth is that Velocity is very old and pretty clunky.  The last news update to the project was

This script returns a list of pages in a Confluence instance that use a specified macro.

There are a number of references to “MacroManager” in the Confluence API documentation, but none of the implementations seemed to work for me.

For that reason, our best bet for checking on Macro usage is to examine the body content of each page, and look for a specific reference to the macro in question.

We also need to check that the page in question is the latest version of the page. Otherwise the script checks all versions of all pages.

 

 import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.confluence.spaces.SpaceManager
import com.atlassian.confluence.pages.PageManager
import com.atlassian.confluence.pages.Page
def pageManager = ComponentLocator.getComponent(PageManager)
def spaceManager = ComponentLocator.getComponent(SpaceManager)
def spaces = spaceManager.getAllSpaces()
  
def macroRef = 'ac:name="info"'
//Replace "info" with the name of the macro you want to assess
  
spaces.each{  spaceObj ->
//Get all Spaces in the instance
  def pages = pageManager.getPages(spaceObj, false)
  pages.each{    page ->
//Get all pages in the instances
      if (page.getBodyContent().properties.toString().contains(macroRef) && page.version == page.latestVersion.version) {
      //Check if the page contains the macro, then check to see if it's the most current version of the page
        log.warn("'${page.title}' (version ${page.version}) is the latest version of the page, and contains the target macro")
      }
  

I admit, this one is a very specific use case.  But I had a reason to create the script, so maybe someone will find it useful.  I also got to use the .collect method in a useful way!

This script identifies all of the pages that are linked from a target page.  It then compares that list of links to a list of all the pages in the Space.

By doing this, it identifies any pages in the Space that aren’t linked on the target page.  This could be useful if you had a wiki or something, and wanted to know which pages weren’t linked on the front page.

One interesting thing I discovered while doing this is the outgoingLinks method of the page class.   Instead of having to use regex to find the URLs on a page, I simply had to call this method and all of the urls were returned for me.

 

 import com.atlassian.confluence.pages.PageManager
import com.atlassian.confluence.spaces.SpaceManager
import com.atlassian.sal.api.component.ComponentLocator

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

def targetSpace = spaceManager.getSpace("<Space Key>")
def spacePages = pageManager.getPages(targetSpace, true)
def targetPage = pageManager.getPage(<page ID as Long>)

def outgoingLinks = targetPage.outgoingLinks.collect { link -> 

link.toString().minus("ds:")
}

spacePages.each {
  page ->

    

I am entirely self-taught when it comes to ScriptRunner and Groovy.   Everything I’ve learned has been through trial and error, and Googling different permutations of words until I find a solution.

A great deal of the information out there assumes that you already know how to work with ScriptRunner, Groovy, and Jira or Confluence.   I found this to be terrifically frustrating when I first started, as I did not have the requisite knowledge to make use of the information that I was finding.  I didn’t have the skills to put it into context, never mind making use of it in the specific use case to which I was trying to apply it.

For that reason, I’m going back to the beginning.  I’m starting an ongoing series of blog posts about how to get started with ScriptRunner for both Jira and Confluence.  You need to learn to walk before you can run, so for that reason I am calling this series the ScriptWalker series.

Not only will this hopefully be a resource for persons just starting out with ScriptRunner, but it will also force me to be sure that I can teach what I’m doing. In the end, that will make