Bulk replace content in pages
Last modified by Clemens Robbenhaar on 2025/03/31 13:25
![]() | Allows to perfrom bulk search and replace of contents in wiki pages |
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 search and replace of content in wiki pages.
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 search and replace of XWiki document contents.
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 script will look for documents containing the given search text within the following space.</span>
</dt>
<dd>
#pagePicker($spacePickerParams)
</dd>
<dt>
<input type="checkbox" name="allSpaces" id="allSpaces"#if($request.allSpaces) checked="checked"#end />
<label for="allSpaces">All spaces</label>
<span class="xHint">The macro replace job will execute for every document in all spaces (excerpt the XWiki system space).</span>
</dt>
<dt>
<label for="search">Text to be searched</label>
<span class="xHint">The script will look for the following text in documents. The text in document contents has to match exactly with the search.</span>
</dt>
<dd>
<textarea name="search" id="search">#if($request.search)$escapetool.xml($request.search)#end</textarea>
</dd>
<dt>
<label for="replace">Text to replace</label>
</dt>
<dd>
<textarea name="replace" id="replace">#if($request.replace)$escapetool.xml($request.replace)#end</textarea>
</dd>
<dt>
<input id="regex" name="regex" type="checkbox"#if($request.regex) checked="checked"#end /> <label for="regex">Use regex</label>
<span class="xHint">By default, this script will do a raw search / replace. Checking this allows you to use a regular expression, and reference captured groups in the replacement as well (using \0 (for the whole match), \1 (for the first captured group), \2, …), using the Java regex syntax</span>
</dt>
<dt>
<input id="logContent" name="logContent" type="checkbox"#if($request.logContent) checked="checked"#end /> <label for="logContent">Log content</label>
<span class="xHint">By default, this script won't log content, but you may want these logs it you want to perform a careful review</span>
</dt>
<dt>
<input id="updateInPlace" name="updateInPlace" type="checkbox"#if($request.updateInPlace) checked="checked"#end /> <label for="updateInPlace">Update in place</label>
<span class="xHint">By default, this script adds a revision. This option will save the document in place instead, without adding a revision (DANGEROUS).</span>
</dt>
<dt>
<input id="savePages" name="savePages" type="checkbox"#if($request.savePages) checked="checked"#end /> <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>
<dt>
<label for="comment">Revision comment</label>
<span class="xHint">The revision comment to use when updating documents (does not apply when using the update in place option)</span>
<input id="comment" name="comment" type="text" size="30"#if($request.comment) value="$escapetool.xml($request.comment)"#end/>
</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="Replace contents"/>
</span>
</p>
</form>
{{/html}}
{{/velocity}}
{{job id="bulkReplace" start="{{velocity}}$!{request.confirm}{{/velocity}}"}}
{{groovy}}
import com.xpn.xwiki.api.Document;
import org.apache.commons.lang3.StringUtils;
import org.xwiki.model.reference.DocumentReference
import org.xwiki.query.Query;
import java.util.regex.Pattern;
logger = services.logging.getLogger('BulkReplaceContent');
services.logging.setLevel('BulkReplaceContent', org.xwiki.logging.LogLevel.INFO);
if (!hasProgramming || !services.csrf.isTokenValid(request.form_token)) {
logger.error('Insufficient permissions or invalid CSRF token. Aborting.');
return;
}
boolean allSpaces = request.allSpaces == 'on';
if ((!request.targetSpace || StringUtils.isBlank(request.targetSpace)) && !allSpaces) {
logger.error('Missing a target space. Aborting.');
return;
}
if (StringUtils.isBlank(request.search)) {
logger.error('Please provide a search string.');
return;
}
String spacePrefix = "${StringUtils.removeEnd(request.targetSpace, 'WebHome')}%".toString();
String sanitizedSearchString = request.search.replace('\r\n','\n');
String sanitizedReplaceString = request.replace ? request.replace.replace('\r\n','\n') : "";
String comment = StringUtils.isEmpty(request.comment) ? "" : request.comment;
// Get every page matching the space
Pattern regex = request.regex == "on" ? Pattern.compile(sanitizedSearchString) : null;
boolean updateInPlace = request.updateInPlace == "on";
boolean logContent = request.logContent == "on";
boolean savePages = request.savePages == "on";
String filterContent = regex ? "" : " and doc.content like :searchContent";
Query q = (
allSpaces
? services.query.hql("select doc.fullName from XWikiDocument doc where doc.fullName not like 'XWiki.%'" + filterContent)
: services.query.hql('select doc.fullName from XWikiDocument doc where doc.fullName like :spacePrefix' + filterContent).bindValue('spacePrefix', spacePrefix)
);
q.addFilter('language')
if (filterContent) {
q = q.bindValue('searchContent').anyChars().literal(sanitizedSearchString).anyChars().query();
}
List<String[]> documents = q.execute();
logger.info('Found [{}] documents to verify', documents.size());
for (String[] docs : documents) {
try {
String documentFullName = docs[0]
String language = docs[1]
DocumentReference documentReference = services.model.resolveDocument(documentFullName, 'current')
if (language != null) {
Locale locale = Locale.forLanguageTag(language)
documentReference = new DocumentReference(documentReference, locale)
}
Document document = xwiki.getDocument(documentReference);
logger.info('Verifying document [{}]', document.getDocumentReferenceWithLocale());
String oldContent = document.getContent();
String newContent = (
regex == null
? StringUtils.replace(oldContent, sanitizedSearchString, sanitizedReplaceString)
: regex.matcher(oldContent).replaceAll(sanitizedReplaceString)
);
boolean hasContentChanged = !oldContent.equals(newContent);
if (hasContentChanged) {
if (savePages) {
logger.info('Content has changed; saving document [{}]', document.getDocumentReferenceWithLocale());
document.setContent(newContent);
if (updateInPlace) {
document.getDocument().setContentDirty(false);
document.getDocument().setMetaDataDirty(false);
}
document.save(comment);
} else {
logger.info('Content for document [{}] would change (but saving is not enabled)',
document.getDocumentReferenceWithLocale());
}
if (logContent) {
logger.info("Before:\n{}", oldContent);
logger.info("After:\n{}", newContent);
}
}
} catch (Exception e) {
logger.error('Uncaught exception [{}]', e);
}
}
{{/groovy}}
{{/job}}