Class | Line # | Actions | |||||
---|---|---|---|---|---|---|---|
EscapeTool | 50 | 48 | 0% | 26 | 1 |
1 | /* | |
2 | * See the NOTICE file distributed with this work for additional | |
3 | * information regarding copyright ownership. | |
4 | * | |
5 | * This is free software; you can redistribute it and/or modify it | |
6 | * under the terms of the GNU Lesser General Public License as | |
7 | * published by the Free Software Foundation; either version 2.1 of | |
8 | * the License, or (at your option) any later version. | |
9 | * | |
10 | * This software is distributed in the hope that it will be useful, | |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
13 | * Lesser General Public License for more details. | |
14 | * | |
15 | * You should have received a copy of the GNU Lesser General Public | |
16 | * License along with this software; if not, write to the Free | |
17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA | |
18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. | |
19 | */ | |
20 | package org.xwiki.velocity.tools; | |
21 | ||
22 | import java.io.UnsupportedEncodingException; | |
23 | import java.net.URLEncoder; | |
24 | import java.util.Collection; | |
25 | import java.util.Map; | |
26 | ||
27 | import org.apache.commons.codec.EncoderException; | |
28 | import org.apache.commons.codec.net.BCodec; | |
29 | import org.apache.commons.codec.net.QCodec; | |
30 | import org.apache.commons.codec.net.QuotedPrintableCodec; | |
31 | import org.apache.commons.lang3.StringEscapeUtils; | |
32 | import org.slf4j.Logger; | |
33 | import org.slf4j.LoggerFactory; | |
34 | import org.xwiki.xml.XMLUtils; | |
35 | ||
36 | /** | |
37 | * <p> | |
38 | * Tool for working with escaping in Velocity templates. It provides methods to escape outputs for Velocity, Java, | |
39 | * JavaScript, HTML, XML and SQL. | |
40 | * </p> | |
41 | * <p> | |
42 | * Extends the default EscapeTool from velocity-tools since the XML escape performed by it doesn't work inside HTML | |
43 | * content, since {@code apos} is not a valid HTML entity name, and it always escapes non-ASCII characters, which | |
44 | * increases the HTML length considerably, while also making the source unreadable. | |
45 | * </p> | |
46 | * | |
47 | * @version $Id: d6ce71c040c3bc2ccb9d686a7deed23215737d2d $ | |
48 | * @since 2.7RC1 | |
49 | */ | |
50 | public class EscapeTool extends org.apache.velocity.tools.generic.EscapeTool | |
51 | { | |
52 | private static final Logger LOGGER = LoggerFactory.getLogger(EscapeTool.class); | |
53 | ||
54 | /** Equals sign. */ | |
55 | private static final String EQUALS = "="; | |
56 | ||
57 | /** And sign. */ | |
58 | private static final String AND = "&"; | |
59 | ||
60 | /** | |
61 | * Escapes the XML special characters in a <code>String</code> using numerical XML entities. This overrides the base | |
62 | * implementation from Velocity, which is over-zealous and escapes any non-ASCII character. Since XWiki works with | |
63 | * Unicode-capable encodings (UTF-8), there is no need to escape non-special characters. | |
64 | * | |
65 | * @param content the text to escape, may be {@code null} | |
66 | * @return a new escaped {@code String}, {@code null} if {@code null} input | |
67 | */ | |
68 | 106133 | @Override |
69 | public String xml(Object content) | |
70 | { | |
71 | 106133 | return XMLUtils.escape(content); |
72 | } | |
73 | ||
74 | /** | |
75 | * Escapes the characters in a <code>String</code> using JSON String rules: escapes with backslash double quotes, | |
76 | * back and forward slashes, newlines, the control characters {@code \b}, {@code \t} and {@code \f}, and with | |
77 | * {@code \}{@code uXXXX} any non-ASCII characters. Unlike {@link #javascript(Object)}, it does not escape {@code '} | |
78 | * , which is not a special character in JSON, and it would be a syntax error to do so. | |
79 | * | |
80 | * @param string the string to escape, may be {@code null}; any non-string object will be converted to a string | |
81 | * first, using {@code String.valueOf(obj)} | |
82 | * @return String with escaped values, {@code null} if {@code null} input | |
83 | * @since 6.1M1 | |
84 | */ | |
85 | 8 | public String json(Object string) |
86 | { | |
87 | 8 | if (string == null) { |
88 | 1 | return null; |
89 | } | |
90 | 7 | return StringEscapeUtils.escapeJson(String.valueOf(string)); |
91 | } | |
92 | ||
93 | /** | |
94 | * Encode a text using the Quoted-Printable format, as specified in section 6.7 of <a | |
95 | * href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>. UTF-8 is used as the character encoding, and no line | |
96 | * breaking is performed. | |
97 | * | |
98 | * @param content the text to encode | |
99 | * @return the text converted into the Quoted-Printable format | |
100 | */ | |
101 | 4 | public String quotedPrintable(Object content) |
102 | { | |
103 | 4 | if (content != null) { |
104 | 3 | try { |
105 | 3 | return new QuotedPrintableCodec().encode(String.valueOf(content)); |
106 | } catch (EncoderException ex) { | |
107 | // Just return null | |
108 | } | |
109 | } | |
110 | 1 | return null; |
111 | } | |
112 | ||
113 | /** | |
114 | * Encode a text using the Q encoding specified in <a href="http://www.ietf.org/rfc/rfc2047.txt">RFC 2047</a>. UTF-8 | |
115 | * is used as the character encoding, and no line breaking is performed. The resulting text is already wrapped with | |
116 | * the encoded word markers, starting with {@code =?UTF-8?Q?} and ending with {@code ?=}. | |
117 | * | |
118 | * @param content the text to encode | |
119 | * @return the text converted into an encoded word using the Q encoding | |
120 | */ | |
121 | 5 | public String q(Object content) |
122 | { | |
123 | 5 | if (content != null) { |
124 | 4 | try { |
125 | 4 | return new QCodec().encode(String.valueOf(content)).replace(' ', '_'); |
126 | } catch (EncoderException ex) { | |
127 | // Just return null | |
128 | } | |
129 | } | |
130 | 1 | return null; |
131 | } | |
132 | ||
133 | /** | |
134 | * Encode a text using the B encoding specified in <a href="http://www.ietf.org/rfc/rfc2047.txt">RFC 2047</a>. UTF-8 | |
135 | * is used as the character encoding, and no line breaking is performed. The resulting text is already wrapped with | |
136 | * the encoded word markers, starting with {@code =?UTF-8?B?} and ending with {@code ?=}. | |
137 | * | |
138 | * @param content the text to encode | |
139 | * @return the text converted into an encoded word using the B encoding | |
140 | */ | |
141 | 4 | public String b(Object content) |
142 | { | |
143 | 4 | if (content != null) { |
144 | 3 | try { |
145 | 3 | return new BCodec().encode(String.valueOf(content)); |
146 | } catch (EncoderException ex) { | |
147 | // Just return null | |
148 | } | |
149 | } | |
150 | 1 | return null; |
151 | } | |
152 | ||
153 | /** | |
154 | * Properly escape a parameter map representing a query string, so that it can be safely used in an URL. Parameters | |
155 | * can have multiple values in which case the value in the map is either an array or a {@link Collection}. If the | |
156 | * parameter name is {@code null} (the key is {@code null}) then the parameter is ignored. {@code null} values are | |
157 | * serialized as an empty string. | |
158 | * | |
159 | * @param parametersMap Map representing the query string. | |
160 | * @return the safe query string representing the passed parameters | |
161 | * @since 5.2M1 | |
162 | */ | |
163 | 13510 | public String url(Map<String, ?> parametersMap) |
164 | { | |
165 | 13510 | StringBuilder queryStringBuilder = new StringBuilder(); |
166 | 13510 | for (Map.Entry<String, ?> entry : parametersMap.entrySet()) { |
167 | 22603 | if (entry.getKey() == null) { |
168 | // Skip the parameter if its name is null. | |
169 | 1 | continue; |
170 | } | |
171 | 22603 | String cleanKey = this.url(entry.getKey()); |
172 | 22603 | Object mapValues = entry.getValue(); |
173 | 22603 | if (mapValues != null && mapValues.getClass().isArray()) { |
174 | // A parameter with multiple values. | |
175 | 1 | Object[] values = (Object[]) mapValues; |
176 | 1 | for (Object value : values) { |
177 | 3 | addQueryStringPair(cleanKey, value, queryStringBuilder); |
178 | } | |
179 | 22603 | } else if (mapValues != null && Collection.class.isAssignableFrom(mapValues.getClass())) { |
180 | // A parameter with multiple values. | |
181 | 9056 | Collection<?> values = (Collection<?>) mapValues; |
182 | 9056 | for (Object value : values) { |
183 | 9058 | addQueryStringPair(cleanKey, value, queryStringBuilder); |
184 | } | |
185 | } else { | |
186 | 13547 | addQueryStringPair(cleanKey, mapValues, queryStringBuilder); |
187 | } | |
188 | } | |
189 | 13509 | return queryStringBuilder.toString(); |
190 | } | |
191 | ||
192 | /** | |
193 | * Method to add an key / value pair to a query String. | |
194 | * | |
195 | * @param cleanKey Already escaped key | |
196 | * @param rawValue Raw value associated to the key | |
197 | * @param queryStringBuilder String Builder containing the current query string | |
198 | */ | |
199 | 22605 | private void addQueryStringPair(String cleanKey, Object rawValue, StringBuilder queryStringBuilder) |
200 | { | |
201 | // Serialize null values as an empty string. | |
202 | 22608 | String valueAsString = rawValue == null ? "" : String.valueOf(rawValue); |
203 | 22609 | String cleanValue = this.url(valueAsString); |
204 | 22607 | if (queryStringBuilder.length() != 0) { |
205 | 9110 | queryStringBuilder.append(AND); |
206 | } | |
207 | 22608 | queryStringBuilder.append(cleanKey).append(EQUALS).append(cleanValue); |
208 | } | |
209 | ||
210 | /** | |
211 | * Escapes a CSS identifier. | |
212 | * <p> | |
213 | * See https://drafts.csswg.org/cssom/#serialize-an-identifier. | |
214 | * </p> | |
215 | * | |
216 | * @param identifier the identifier to escape | |
217 | * @return the escaped identifier | |
218 | * @since 6.4.7 | |
219 | * @since 7.1.4 | |
220 | * @since 7.4M1 | |
221 | */ | |
222 | 2 | public String css(String identifier) |
223 | { | |
224 | 2 | try { |
225 | 2 | return new CSSIdentifierSerializer().serialize(identifier); |
226 | } catch (IllegalArgumentException e) { | |
227 | 1 | LOGGER.warn("Failed to escape CSS identifier. {}", e.getMessage()); |
228 | 1 | return null; |
229 | } | |
230 | } | |
231 | ||
232 | /** | |
233 | * We override the implementation so that we sync it with the encoding strategy we use for generating URLs. Namely | |
234 | * we encode all characters and we encode space as {@code %20} and not as {@code +} in the query string. | |
235 | * | |
236 | * @param string the url to encode | |
237 | * @return the encoded URL | |
238 | * @since 8.3M1 | |
239 | */ | |
240 | 55802 | @Override |
241 | public String url(Object string) | |
242 | { | |
243 | // TODO: Introduce a xwiki-commons-url module and move this code in it so that we can share it with | |
244 | // platform's XWikiServletURLFactory and functional test TestUtils class. | |
245 | 55802 | String encodedURL = null; |
246 | 55802 | if (string != null) { |
247 | 55483 | try { |
248 | 55482 | encodedURL = URLEncoder.encode(String.valueOf(string), "UTF-8"); |
249 | } catch (UnsupportedEncodingException e) { | |
250 | // Should not happen (UTF-8 is always available) | |
251 | 0 | throw new RuntimeException("Missing charset [UTF-8]", e); |
252 | } | |
253 | // The previous call will convert " " into "+" (and "+" into "%2B") so we need to convert "+" into "%20" | |
254 | // It's ok since %20 is allowed in both the URL path and the query string (and anchor). | |
255 | 55483 | encodedURL = encodedURL.replaceAll("\\+", "%20"); |
256 | } | |
257 | 55803 | return encodedURL; |
258 | } | |
259 | } |