Remove macros from XDOM
Last modified by Raphaël Jakse on 2026/08/05 17:14
| Allows to remove one or multiple macros from document contents |
| Type | Snippet |
| Category | Other |
| Developed by | |
| Rating | |
| License | GNU Lesser General Public License 2.1 |
Table of contents
Description
This script allows to perform bulk removal of macros within XWiki documents. It is particularily useful after content migrations.
This snippet requires the Job Macro to run.
In a new page, copy-paste the following snippet :
{{velocity}}
#set ($spacePickerParams = {
'name': 'targetSpace',
'value': "$!{request.targetSpace}"
})
This script allows to perform bulk removal of macros within XWiki documents. It is particularily useful after content migrations.
For each document part of a given space, XWiki will look for either macros part of a provided blacklist, or for any macro not registered in XWiki. The macros can then be removed from the document content, with minimal impact on the content around the macro.
Programming rights are required to use this script.
{{html clean="false"}}
<form class="xform" action="#" method="post">
<dl>
<dt>
<label for="targetSpace">Space</label>
<span class="xHint">The macro removal job will execute for every document under the given space.</span>
</dt>
<dd>
#pagePicker($spacePickerParams)
</dd>
<dt>
<label for="macrosToRemove">Macros to be removed</label>
<span class="xHint">Provide a comma-separated list of macros to remove.</span>
</dt>
<dd>
<input type="text" name="macrosToRemove" id="macrosToRemove" value="$!{escapetool.xml($request.macrosToRemove)}"/>
</dd>
<dt>
<input id="removeUnknownMacros" name="removeUnknownMacros" type="checkbox" value="1" #if($request.removeUnknownMacros == '1')checked#{end}/> <label for="removeUnknownMacros">Remove every macro that are not known to XWiki</label>
<span class="xHint">If this option is checked, XWiki will verify that every macro of the document correspond to a valid and known macro in XWiki. Note that using this option could remove macros that contain a lot of content. If a list of macros to remove is provided in the field above, this option will be ignored.</span>
</dt>
<dt>
<input id="keepContent" name="keepContent" type="checkbox" value="1" #if($request.keepContent == '1')checked#{end}/> <label for="keepContent">Keep
macro content</label>
<span class="xHint">If this option is checked, the content of the macro will be kept in the document. Otherwise, the content will be removed.</span>
</dt>
<dt>
<input id="savePages" name="savePages" type="checkbox" value="save"/> <label for="savePages">Save pages</label>
<span class="xHint">By default, this script will execute in dry-mode, and will not save pages.</span>
</dt>
</dl>
<p>
<span class="buttonwrapper">
<input type="hidden" name="form_token" value="$!{services.csrf.token}"/>
<input type="hidden" name="confirm" value="true"/>
<input class="button" type="submit" value="Remove macros"/>
</span>
</p>
</form>
{{/html}}
{{/velocity}}
{{job id="removeMacrosFromXDOM" start="{{velocity}}$!{request.confirm}{{/velocity}}"}}
{{groovy}}
import org.apache.commons.lang3.StringUtils
import org.xwiki.rendering.block.Block
import org.xwiki.rendering.block.CompositeBlock
import org.xwiki.rendering.block.MacroBlock
import org.xwiki.rendering.block.XDOM
import org.xwiki.rendering.block.match.ClassBlockMatcher
import org.xwiki.rendering.macro.Macro
logger = services.logging.getLogger('RemoveMacrosFromXDOM');
services.logging.setLevel('RemoveMacrosFromXDOM', org.xwiki.logging.LogLevel.INFO);
hasMacrosToRemove = request.macrosToRemove && StringUtils.isNotBlank(request.macrosToRemove);
macrosToRemove = hasMacrosToRemove ? request.macrosToRemove.split(',') : [];
componentManager = services.component.getComponentManager();
keepContent = request.keepContent;
def getMacroXDOM(MacroBlock macroBlock, syntaxId) {
if (componentManager.hasComponent(Macro.class, macroBlock.getId())) {
def macroContentDescriptor =
componentManager.getInstance(Macro.class, macroBlock.getId()).getDescriptor().getContentDescriptor();
if (macroContentDescriptor != null && macroContentDescriptor.getType().equals(Block.LIST_BLOCK_TYPE) &&
StringUtils.isNotBlank(macroBlock.getContent()))
{
// We will take a quick shortcut here and directly parse the macro content with the syntax of the document
return services.rendering.parse(macroBlock.getContent(), syntaxId);
}
} else if (StringUtils.isNotBlank(macroBlock.getContent())) {
// Just assume that the macro content is wiki syntax if we don't know the macro.
logger.debug('Calling parse on unknown macro [{}] with syntax [{}]', macroBlock.getId(), syntaxId)
return services.rendering.parse(macroBlock.getContent(), syntaxId);
}
return null;
}
def verifyXDOM(xdom, syntaxId) {
def hasXDOMChanged = false;
xdom.getBlocks(new ClassBlockMatcher(MacroBlock.class), Block.Axes.DESCENDANT_OR_SELF).each { block ->
logger.debug('Checking block [{}] - [{}]', block.getId(), block.getClass());
def macroExists = componentManager.hasComponent(Macro.class, block.getId());
def shouldRemoveBlock = hasMacrosToRemove ? macrosToRemove.contains(block.getId()) : !macroExists;
if (shouldRemoveBlock) {
logger.info('Removing macro block [{}]', block.getId());
if (keepContent) {
def XDOM content = getMacroXDOM(block, syntaxId);
if (content != null) {
verifyXDOM(content, syntaxId);
block.getParent().replaceChild(new CompositeBlock(content.getChildren()), block);
} else {
block.getParent().removeBlock(block);
}
} else {
block.getParent().removeBlock(block);
}
hasXDOMChanged = true;
} else if (macroExists) {
// Check if the macro content is wiki syntax, in which case we'll also verify the contents of the macro
def macroXDOM = getMacroXDOM(block, syntaxId);
if (macroXDOM != null) {
def hasMacroContentChanged = verifyXDOM(macroXDOM, syntaxId);
if (hasMacroContentChanged) {
logger.debug('The content of macro [{}] has changed', block.getId());
def newMacroContent = services.rendering.render(macroXDOM, syntaxId);
// Create a new macro block and swap it
def newMacroBlock = new MacroBlock(block.getId(), block.getParameters(), newMacroContent, block.isInline());
block.getParent().replaceChild(newMacroBlock, block);
hasXDOMChanged = true;
}
}
}
}
return hasXDOMChanged;
}
if (hasProgramming && services.csrf.isTokenValid(request.form_token)) {
// Check if we have enough to work on
logger.debug('Has macros to remove : [{}]', hasMacrosToRemove);
if (request.targetSpace && StringUtils.isNotBlank(request.targetSpace)
&& (request.removeUnknownMacros || hasMacrosToRemove)) {
def spacePrefix = "${StringUtils.removeEnd(request.targetSpace, 'WebHome')}%";
if (hasMacrosToRemove) {
logger.info('Selected macros to remove : [{}]', macrosToRemove);
}
// Get every page matching the space
def documents = services.query.hql('select doc.fullName from XWikiDocument doc where doc.fullName like :spacePrefix').bindValue('spacePrefix', spacePrefix.toString()).execute();
logger.debug('Space prefix : [{}]', spacePrefix)
logger.debug('Found [{}] documents to verify', documents.size())
documents.each { documentFullName ->
try {
def document = xwiki.getDocument(documentFullName);
logger.debug('Verifying document [{}]', document.getDocumentReference());
def xdom = document.getXDOM();
hasXDOMChanged = verifyXDOM(xdom, document.getSyntaxId());
if (hasXDOMChanged && 'save'.equals(request.savePages)) {
logger.info('XDOM has changed ; saving document [{}]', document.getDocumentReference());
document.setContent(xdom);
document.save('Remove incompatible XWiki macros');
}
} catch (Exception e) {
logger.error('Uncaught exception [{}]', e);
}
}
} else {
logger.error('Insufficient parameters. Please provide a target space and define which macros to remove, or check the "remove every macro" checkbox. Aborting.');
}
} else {
logger.error('Insufficient permissions or invalid CSRF token. Aborting.')
}
{{/groovy}}
{{/job}}