Fixes #6345 - Using square brackets in text modules.

Approved-by: Dusan Vuckovic <dv@zammad.com>
This commit is contained in:
Florian Liebe
2026-09-03 11:02:05 +02:00
parent 432e44df07
commit 203aa764f5
2 changed files with 92 additions and 2 deletions

View File

@@ -565,8 +565,8 @@
}
Collection.renderResults = function(textmodule, term) {
var reg = new RegExp(term, 'i')
var regFull = new RegExp('\\b' + term + '\\b', 'i')
var reg = new RegExp(escapeRegExp(term), 'i')
var regFull = new RegExp('\\b' + escapeRegExp(term) + '\\b', 'i')
var result = textmodule.collection.filter(function(item) {
return matchItemWithRegExp(item, reg) && !matchItemWithRegExp(item, regFull)
})

View File

@@ -129,3 +129,93 @@ QUnit.test('test text module behaviour with group_ids', assert => {
assert.equal(currentCollection[1].id, 3)
});
// Regex characters in text module names must be matched literally, not
// interpreted as a regular expression (issue #6345).
QUnit.test('test text module search with regex characters in the name', assert => {
App.User.refresh([{
"login": "hh@example.com",
"firstname": "Harald",
"lastname": "Habebe",
"email": "hh@example.com",
"role_ids": [ 1, 2, 4 ],
"group_ids": [ 1, 2 ],
"active": true,
"id": 6
}]);
App.Session.set(6)
// mock user methods
App.Session.get().allGroupIds = () => ['1','2']
App.Session.get().permission = () => true
App.TextModule.refresh([
{
id: 10,
name: '[category] Some product',
keywords: 'bracketed',
content: 'content10',
active: true,
},
{
id: 11,
name: '[product] Some category',
keywords: 'bracketed',
content: 'content11',
active: true,
},
], { clear: true })
$('body').append('<div id="text-module-regex-chars"><div contenteditable="true"></div></div>')
var el = $('#text-module-regex-chars [contenteditable]')
var textModule = new App.WidgetTextModule({
el: el,
data: {
user: App.Session.get(),
config: App.Config.all(),
},
taskKey: 'test-regex-chars',
})
var plugin = el.data('plugin_textmodule')
// Run the search the way a keystroke would: the buffer holds the trigger
// plus the term the user typed after it.
var search = (buffer) => {
plugin.buffer = buffer
plugin.result(plugin.findTrigger(buffer))
return plugin.$widget.find('ul > li').map(function() {
return $(this).attr('data-id')
}).get().sort()
}
try {
// An unterminated character class must not blow up the search.
assert.deepEqual(search('::[category'), ['10'], 'unterminated bracket matches literally')
// A complete character class must not match every entry.
assert.deepEqual(search('::[category]'), ['10'], 'complete bracket matches literally')
// Other regex metacharacters are literal too.
assert.deepEqual(search('::product) Some'), [], 'unbalanced parenthesis matches nothing')
// The term is taken literally, so the former escaping workaround now
// searches for a backslash and finds nothing.
assert.deepEqual(search('::\\[category'), [], 'backslash is no longer an escape')
// Terms without regex characters keep working.
assert.deepEqual(search('::bracketed'), ['10', '11'], 'keyword matches both')
assert.deepEqual(search('::category'), ['10', '11'], 'plain term matches both')
}
finally {
// Keep the fixture self-contained: an assertion that throws must not leave
// a detached element subscribed to the text module collection.
textModule.releaseController()
textModule.release()
$('#text-module-regex-chars').remove()
}
});