Last updatedJun 19, 2018
Improve this page

How to make your add-on compatible with read-only mode

Read-Only Mode for Data Center customers helps admins perform routine maintenance, recover from unexpected problems, or prepare to migrate content to a new site. When a site is placed in read-only mode, users will be able to view pages and their history, but not create, edit, comment, copy or move content. Administration actions such as changing site configuration are not restricted.

What you need to do

To make your add-on compatible with read-only mode, there are two main things you will need to determine:

  • how you'll decide if a page UI element should be disabled or hidden when the page is refreshed after read-only mode is enabled, and
  • how you'll check if your logic should be executed in read-only mode.

Finally, you should also mark your add-on as compatible, so admins know it is safe to leave your add-on enabled while read-only mode is on.

How to enable read-only mode for testing

This feature is only available for Confluence Data Center. This means your test site will require a Data Center license. See Starting a Confluence cluster on a single machine to find out how to set up a Data Center instance for testing. This page also has a 72 hour Data Center license that you can use.

Once you've got your Data Center test instance set up, turn on read-only mode:

  1. Go to Confluence admin menu > General Configuration > Maintenance.
  2. Choose Edit and select Read-only mode.
  3. (Optional) Update the text of the banner message - this will let anyone using your test environment know that read-only mode is enabled.
  4. Choose Save to apply your changes.

How to disable or hide UI elements when read-only mode is enabled

As UI elements can be rendered in various ways, there are several ways to do this.

This is the recommended approach.

1.1 With a Confluence built-in Condition directive

If your web-item is configured with the appropriate condition, it will be hidden automatically when read-only mode is enabled. For example:

Built-in Condition directive web-item example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<web-item key="edit-page" name="Edit Tab" section="system.content.button" weight="10">
    ...
    <condition class="com.atlassian.confluence.plugin.descriptor.web.conditions.PagePermissionCondition">
        <param name="permission">edit</param>
    </condition>
    ...
</web-item>
 
<web-item key="remove-page" name="Remove Link" section="system.content.action/modify" weight="30">
    ...
    <condition class="com.atlassian.confluence.plugin.descriptor.web.conditions.PagePermissionCondition">
        <param name="permission">remove</param>
    </condition>
    ...
</web-item>

1.2 With a custom Condition directive

If you've used a custom Condition class for the web-item, you'll need to update that Condition class to check for read-only mode appropriately or add the new ReadWriteAccessModeCondition implemented as below:

ReadWriteAccessModeCondition
1
2
3
4
5
6
7
8
9
10
11
12
public class ReadWriteAccessModeCondition extends BaseConfluenceCondition {
    private AccessModeService accessModeService;
 
    @Override
    protected boolean shouldDisplay(WebInterfaceContext context) {
        return AccessMode.READ_WRITE.equals(accessModeService.getAccessMode());
    }
 
    public void setAccessModeService(AccessModeService accessModeService) {
        this.accessModeService = accessModeService;
    }
}
Custom Condition directive web-item example
1
2
3
4
5
<web-item key="remove-page" name="Remove Link" section="system.content.action/modify" weight="30">
    ...
    <condition class="com.atlassian.confluence.plugin.descriptor.web.conditions.ReadWriteAccessModeCondition"/>
    ...
</web-item>

2. If a static element is inserted to the DOM dynamically in the front-end code after the page is ready

If a web-item can't be used for any reason, upon loading the page, you can check one of the two following AJS meta tags in your Javascript to determine the read-only state:

  • render-mode ("READ_ONLY" or "READ_WRITE"): this meta tag reflects the rendering state of the page at the time it is loaded. Its value will not be changed over time unless the user refreshes the page.
  • access-mode ("READ_ONLY" or "READ_WRITE"): this meta tag reflects the state of the page at the time the JS code is run. Its value will change if the admin changes the access mode (within a 30-second window of the change being made).

The JS code will need to check if read-only mode is enabled by checking the access-mode AJS meta tag as follows:

1
2
3
function isReadOnlyModeEnabled() {
    return Meta.get('access-mode') === 'READ_ONLY';
}

If you need to insert or remove a static element from the DOM at some point after the page is loaded, you'll need to call the new /accessmode REST end-point to check whether the read-only mode is (still) enabled. This is because the read-only state may change while the user is viewing the page.

How to check if your logic should be executed in read-only mode

1. With the public Java API at the back end

Confluence will throw a ReadOnlyException if  accessModeService.isReadOnlyAccessModeEnabled() is true in the back end, as in the removeLike method of the DefaultLikeManager.

1
2
3
4
5
public void removeLike(final ContentEntityObject contentEntity, final User user) {
    if (accessModeService.isReadOnlyAccessModeEnabled()) {
        throw new ReadOnlyException(i18NBeanFactory.getI18NBean().getText("read.only.mode.default.banner.message"));
    }
}

2 With an existing REST API at the back end

2.1 Define a new RestExceptionMapper

First, make sure there is a RestExceptionMapper class in the same package of the REST resource classes as follows:

1
2
3
4
5
6
7
@Provider
public class RestExceptionMapper extends ServiceExceptionMapper {
    @Override
    protected void _annotateThisClassWithProvider() {
        //NO-OP just gets the annotated class on the bundles class path https://jira.atlassian.com/browse/CRA-733
    }
}

2.2 Throw the ReadOnlyException in the Service method

Then you can either throw a ReadOnlyException in the REST method or the Service/Manager method that it calls (as in 2.1).

DefaultLikeManager
1
2
3
4
5
public void removeLike(final ContentEntityObject contentEntity, final User user) {
    if (accessModeService.isReadOnlyAccessModeEnabled()) {
        throw new ReadOnlyException(i18NBeanFactory.getI18NBean().getText("read.only.mode.default.banner.message"));
    }
}

And then just call the Service/Manager method in the REST method as usual.

LikableContentResource
1
2
3
4
5
6
7
8
@DELETE
@Produces({APPLICATION_JSON})
@Path("/{id}/likes")
@Consumes(APPLICATION_JSON)
public Response removeLike(@PathParam("id") final Long contentId) {
    // all other lines are removed for simplicity
    likeManager.removeLike(contentEntity, AuthenticatedUserThreadLocal.get());
}

2.3 Make sure the front-end code handles the error response properly

Update your front-end Ajax request counterpart to handle the 405 status code and check if the reason equals to "READ_ONLY" or use our new MessageController module as follows:

like.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
define('confluence-like/like', [
    // ...
    'confluence/message-controller'
], function(
    // ...
    MessageController
) {
    // ...
    $.ajax({
        type: type === LIKE ? "POST" : "DELETE",
        url: getRestUrl(contentId),
        contentType: 'application/json',
        data: {
            "atlassian-token": Meta.get("atlassian-token")
        },
        dataType: "json",
        timeout: 5000
    }).fail(function (jqXhr) {
        MessageController.showError(MessageController.parseError(jqXhr), MessageController.Location.FLAG);
    });
}

or:

like.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$.ajax({
    type: "DELETE",
    url: getRestUrl(contentId),
    contentType: 'application/json',
    data: {
        "atlassian-token": Meta.get("atlassian-token")
    },
    dataType: "json",
    timeout: 5000
}).fail(function (xhr) {
	var data = $.parseJSON(xhr.responseText) || xhr.responseText;
	if (xhr.status === 405 && data.reason === "READ_ONLY") {
        new Flag($.extend({}, {
            body: data.message
        }, flagDefaults));
    } else {
    	// handle the other errors here
    }
}

Here's the format of the response:

XHR data
1
2
3
4
5
6
{  
   "statusCode":405,
   "data":{  },
   "message":"",
   "reason":"READ_ONLY"
}

MessageController usage:

1
var errorMessage = MessageController.parseError(jqXhr);
1
MessageController.showError(MessageController.parseError(jqXhr), MessageController.Location.FLAG);
1
2
var defaultErrorMessage = "Some default error message";
MessageController.showError(MessageController.parseError(jqXhr, defaultErrorMessage), MessageController.Location.FLAG)

2.3 By calling the new public /accessmode REST end point

If it's not appropriate to throw a ReadOnlyException from the current REST API, the front-end code will need to check for read-only mode at runtime by sending an Ajax request to the end-point /rest/api/accessmode before deciding to execute the next logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
$.ajax({
    type: "GET",
    url: AJS.contextPath() + "/rest/api/accessmode",
    contentType: 'application/json',
    dataType: "json",
    timeout: 5000
}).done(function (xhr) {
	if (xhr.responseText === "READ_ONLY") {
		// show an error message
	} else {
		// continue
	}
}

The REST end-point will return either a "READ_ONLY" or "READ_WRITE" response.

Make sure the add-on REST resources conform with read-only mode

REST resources

Any POST/PUT/DELETE REST requests will be effectively blocked by a request filter, with the option for allowing them to pass through via the @ReadOnlyAccessAllowed annotation on the relevant REST resource’s method/class/package. We allow GET requests to pass through the request filter. In some special cases, where you also want to block GET requests, you can use the @ReadOnlyAccessBlocked annotation on the relevant REST resource’s method/class/package.

XWork actions

Any POST/PUT/DELETE action requests will be effectively blocked by an interceptor, with the option for allowing them to pass through via the @ReadOnlyAccessAllowed annotation on the relevant Action method/class/package. We allow GET requests to pass through the interceptor. In some special cases, where you also want to block GET requests, you can use the @ReadOnlyAccessBlocked annotation on the relevant XWork action’s method/class/package.

Maintain backwards compatibility with earlier Confluence versions

Most add-ons will want to maintain backwards compatibility with versions prior to Confluence 6.10. We've provided a compatibility library to help with this.

To use the compatibility library in you add-on, add a new dependency with the compile scope to your plugin’s pom.xml file as follows:

1
2
3
4
5
<dependency>
    <groupId>com.atlassian.confluence.compat</groupId>
    <artifactId>confluence-compat-lib</artifactId>
    <version>1.2.1</version>
</dependency> 

Then, import the following OSGi package if you're using amps to build the plugin:

1
2
3
4
5
6
<Import-Package>
   ...
   com.atlassian.confluence.api.service.accessmode;resolution:="optional",
   ...
   *;resolution:=optional
</Import-Package>

Note: you must import it as an optional package with resolution:="optional", or the add-on won't work in older Confluence versions.

The library provides you the following classes/services to make your add-on work with read-only mode:

  • ReadWriteAccessModeCondition
  • ReadWriteAccessModeUrlReadingCondition
  • AccessModeCompatService
  • @ReadOnlyAccessAllowed and @ReadOnlyAccessBlocked annotations

Using the ReadWriteAccessModeCondition in a web-item or web-panel descriptor

The ReadWriteAccessModeCondition can be used to make a web-item or web-panel definition visible when the access mode is READ_WRITE (when Confluence is not in read-only mode). For example:

1
2
3
4
5
6
7
8
9
10
<web-item key="awesomeWebItem" name="AwesomeWebItem" section="system.content.button" weight="150">
    <label key="i18n.label.key"/>
    <tooltip key="i18n.tooltip.key"/>
    <condition class="com.atlassian.confluence.plugin.descriptor.web.conditions.ReadWriteAccessModeCondition"/>
    <condition class="com.atlassian.confluence.plugin.descriptor.web.conditions.PagePermissionCondition">
        <param name="permission">view</param>
    </condition>
    <styleClass>customCssStyle</styleClass>
    <param name="iconClass">customIconClass</param>
</web-item>

In the example above, the awesomeWebItem is only visible if the user has VIEW permission on the page and Confluence is not in read-only mode.

Using the ReadWriteAccessModeUrlReadingCondition in a web-resource descriptor

The ReadWriteAccessModeUrlReadingCondition can be used to make a web-resource definition available for download when the access mode is READ_WRITE (when Confluence is not in read-only mode). For example:

1
2
3
4
5
<web-resource key="awesome-resources" name="Awesome Resources">
    <condition class="com.atlassian.confluence.plugin.descriptor.web.urlreadingconditions.ReadWriteAccessModeUrlReadingCondition">
    </condition>
    <resource name="awesome-view.js" type="download" location="awesome-view.js"/>
</web-resource>

In the example above, the awesome-resources is only visible if Confluence is not in read-only mode.

Using the AccessModeCompatService

If your add-on needs to check the access mode in its logic, you can declare a Spring bean and inject the AccessModeCompatService component into a class (preferably in a Service component, XWork action or REST resource) as follows:

1
<beans:bean id="accessModeCompatService" class="com.atlassian.confluence.compat.api.service.accessmode.impl.DefaultAccessModeCompatService"/>

Another option is to use the Atlassian Spring Scanner library to look up this class at compile time and inject it to the caller service with the @ClasspathComponent annotation.

1
2
3
4
5
6
7
8
@Component
public class AwesomeContentService {
    final AccessModeCompatService accessModeCompatService;
 @Autowired
    public AwesomeContentService(final AccessModeCompatService accessModeCompatService) {
        // your awesome business logic
    }
}

Using the @ReadOnlyAccessAllowed vs the @ReadOnlyAccessBlocked annotation

@ReadOnlyAccessAllowed

The @ReadOnlyAccessAllowed annotation is to bypass the read-only check when a request is served by an XWork action or a REST resource. You can add the annotation to a method, a class, or a package.

For example, an action executed by a sysadmin should be bypassed in read-only mode as follows:

1
2
3
4
5
6
7
8
@ReadOnlyAccessAllowed
@WebSudoRequired
public  class ReIndexAwesomeContentAction extends ConfluenceActionSupport {
    @Override
     public String execute () throws Exception {
       return SUCCESS;
    }
}

WARNING: This annotation must be used for admin actions only or user usage tracking services, for example, recently viewed and analytics.

@ReadOnlyAccessBlocked

Normally, an action that serves a POST/PUT/DELETE request is blocked in read-only mode by default. However, it can be also blocked while serving a GET request if the action is annotated with @ReadOnlyAccessBlocked as follows:

1
2
3
4
5
6
7
@ReadOnlyAccessBlocked
public  class ViewAwesomeContentAction extends ConfluenceActionSupport {
    @Override
     public String execute () throws Exception {
       return SUCCESS;
    }
}

Running code with read-only mode temporarily disabled

When enabled, read-only mode alters how content editing permissions are checked, and by default blocks all content editing permissions for all users regardless of whether they have these permissions normally. This can complicate permission checking, when you need to determine if a permission is blocked because of read-only mode or because the user is denied access normally.

To execute some code and have it behave as if read-only mode is disabled, use withReadOnlyAccessExemption() as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class AwesomeContentPermissionManager {
    @Autowired
    private AccessModeService accessModeService;
    
    public boolean hasAwesomeEditPermission() {
        try {
            return accessService.withReadOnlyAccessExemption(
                    () -> spacePermissionManager.hasPermission(SpacePermission.CREATEEDIT_PAGE_PERMISSION, null, authorizationContext.getAuthenticatedUser()));
        } catch (ServiceException e) {
            return spacePermissionManager.hasPermission(SpacePermission.CREATEEDIT_PAGE_PERMISSION, null, authorizationContext.getAuthenticatedUser());
        }
    }
}

This method is also available in the AccessModeCompatService.

NOTE: Only code that uses shouldEnforceReadOnlyAccess() will execute as if read-only mode is disabled when run with this method. Code paths that use isReadOnlyAccessModeEnabled() will not be affected.

Mark your add-on as compatible

To confirm that your add-on is compatible, and ensure it does not write content to the database when read-only mode is enabled, you should enable read-only mode and test what actions are possible with your add-on. Don't forget to consider users who may already be editing a page or in the midst of an operation at the point read-only mode is enabled. 

Mark your add-on as compatible by setting the read-only-access-mode-compatible parameter to true in the plugin-info tag, as shown in the example below:

1
2
3
4
5
6
7
<plugin-info>
    <param name="atlassian-data-center-compatible">true</param>
    <param name="read-only-access-mode-compatible">true</param>
    <description>${project.description}</description>
    <version>${project.version}</version>
    <vendor name="${project.organization.name}" url="${project.organization.url}" />
</plugin-info>

Incompatible add-ons will be listed on the read-only mode admin screen.