Skip to content

Commit d170a8c

Browse files
committed
Add all Lucene special characters for escaping
1 parent d8d704b commit d170a8c

File tree

2 files changed

+117
-2
lines changed

2 files changed

+117
-2
lines changed

src/app/core/shared/search/models/search-options.model.spec.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,101 @@
11
import { DSpaceObjectType } from '../../dspace-object-type.model';
22
import { SearchFilter } from './search-filter.model';
3-
import { SearchOptions } from './search-options.model';
3+
import {
4+
escapeLuceneSpecialChars,
5+
SearchOptions,
6+
} from './search-options.model';
7+
8+
describe('escapeLuceneSpecialChars', () => {
9+
10+
it('should return plain text unchanged', () => {
11+
expect(escapeLuceneSpecialChars('hello world')).toEqual('hello world');
12+
});
13+
14+
it('should escape +', () => {
15+
expect(escapeLuceneSpecialChars('a+b')).toEqual('a\\+b');
16+
});
17+
18+
it('should escape -', () => {
19+
expect(escapeLuceneSpecialChars('a-b')).toEqual('a\\-b');
20+
});
21+
22+
it('should escape & (and thereby &&)', () => {
23+
expect(escapeLuceneSpecialChars('a&&b')).toEqual('a\\&\\&b');
24+
});
25+
26+
it('should escape | (and thereby ||)', () => {
27+
expect(escapeLuceneSpecialChars('a||b')).toEqual('a\\|\\|b');
28+
});
29+
30+
it('should escape !', () => {
31+
expect(escapeLuceneSpecialChars('!a')).toEqual('\\!a');
32+
});
33+
34+
it('should escape (', () => {
35+
expect(escapeLuceneSpecialChars('(a')).toEqual('\\(a');
36+
});
37+
38+
it('should escape )', () => {
39+
expect(escapeLuceneSpecialChars('a)')).toEqual('a\\)');
40+
});
41+
42+
it('should escape {', () => {
43+
expect(escapeLuceneSpecialChars('{a')).toEqual('\\{a');
44+
});
45+
46+
it('should escape }', () => {
47+
expect(escapeLuceneSpecialChars('a}')).toEqual('a\\}');
48+
});
49+
50+
it('should escape [', () => {
51+
expect(escapeLuceneSpecialChars('[a')).toEqual('\\[a');
52+
});
53+
54+
it('should escape ]', () => {
55+
expect(escapeLuceneSpecialChars('a]')).toEqual('a\\]');
56+
});
57+
58+
it('should escape ^', () => {
59+
expect(escapeLuceneSpecialChars('^a')).toEqual('\\^a');
60+
});
61+
62+
it('should escape "', () => {
63+
expect(escapeLuceneSpecialChars('"a"')).toEqual('\\"a\\"');
64+
});
65+
66+
it('should escape ~', () => {
67+
expect(escapeLuceneSpecialChars('a~')).toEqual('a\\~');
68+
});
69+
70+
it('should escape *', () => {
71+
expect(escapeLuceneSpecialChars('a*')).toEqual('a\\*');
72+
});
73+
74+
it('should escape ?', () => {
75+
expect(escapeLuceneSpecialChars('a?')).toEqual('a\\?');
76+
});
77+
78+
it('should escape :', () => {
79+
expect(escapeLuceneSpecialChars('title:foo')).toEqual('title\\:foo');
80+
});
81+
82+
it('should escape \\', () => {
83+
expect(escapeLuceneSpecialChars('a\\b')).toEqual('a\\\\b');
84+
});
85+
86+
it('should escape /', () => {
87+
expect(escapeLuceneSpecialChars('a/b')).toEqual('a\\/b');
88+
});
89+
90+
it('should escape multiple special characters in one string', () => {
91+
expect(escapeLuceneSpecialChars('(hello+world)')).toEqual('\\(hello\\+world\\)');
92+
});
93+
94+
it('should escape all special characters when they appear together', () => {
95+
expect(escapeLuceneSpecialChars('+-&|!(){}[]^"~*?:\\/')).toEqual('\\+\\-\\&\\|\\!\\(\\)\\{\\}\\[\\]\\^\\"\\~\\*\\?\\:\\\\\\/');
96+
});
97+
98+
});
499

5100
describe('SearchOptions', () => {
6101
let options: SearchOptions;
@@ -40,5 +135,17 @@ describe('SearchOptions', () => {
40135
);
41136
});
42137

138+
it('should escape Lucene special characters in the query', () => {
139+
const specialOptions = new SearchOptions({ query: 'title:foo (bar)+baz' });
140+
const outcome = specialOptions.toRestUrl(baseUrl);
141+
expect(outcome).toEqual('www.rest.com?query=title%5C%3Afoo%20%5C(bar%5C)%5C%2Bbaz');
142+
});
143+
144+
it('should not escape the query when expert mode is enabled', () => {
145+
const expertOptions = new SearchOptions({ query: 'title:foo', expert: true });
146+
const outcome = expertOptions.toRestUrl(baseUrl);
147+
expect(outcome).toEqual('www.rest.com?query=title%3Afoo');
148+
});
149+
43150
});
44151
});

src/app/core/shared/search/models/search-options.model.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import { DSpaceObjectType } from '../../dspace-object-type.model';
88
import { ViewMode } from '../../view-mode.model';
99
import { SearchFilter } from './search-filter.model';
1010

11+
/**
12+
* Escapes all Lucene special characters in a query string so they are treated as literals.
13+
* Special characters: + - & | ! ( ) { } [ ] ^ " ~ * ? : \ /
14+
*/
15+
export function escapeLuceneSpecialChars(query: string): string {
16+
return query.replace(/[+\-&|!(){}[\]^"~*?:\\/]/g, '\\$&');
17+
}
18+
1119
/**
1220
* This model class represents all parameters needed to request information about a certain search request
1321
*/
@@ -51,7 +59,7 @@ export class SearchOptions {
5159
}
5260
if (isNotEmpty(this.query)) {
5361
if (!this.expert) {
54-
args.push(`query=${encodeURIComponent(this.query.replace(':', '\\:'))}`);
62+
args.push(`query=${encodeURIComponent(escapeLuceneSpecialChars(this.query))}`);
5563
} else {
5664
args.push(`query=${encodeURIComponent(this.query)}`);
5765
}

0 commit comments

Comments
 (0)