1 /*
2 * Copyright (c) 2002-2025, City of Paris
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright notice
10 * and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright notice
13 * and the following disclaimer in the documentation and/or other materials
14 * provided with the distribution.
15 *
16 * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17 * contributors may be used to endorse or promote products derived from
18 * this software without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30 * POSSIBILITY OF SUCH DAMAGE.
31 *
32 * License 1.0
33 */
34 package fr.paris.lutece.util.string;
35
36 import fr.paris.lutece.portal.service.util.AppLogService;
37 import fr.paris.lutece.portal.service.util.AppPropertiesService;
38
39 import java.io.BufferedReader;
40 import java.io.ByteArrayInputStream;
41 import java.io.ByteArrayOutputStream;
42 import java.io.IOException;
43 import java.io.InputStreamReader;
44 import java.text.Normalizer;
45 import java.util.zip.GZIPInputStream;
46 import java.util.zip.GZIPOutputStream;
47
48 import org.apache.commons.lang3.StringUtils;
49
50 /**
51 * This class provides String utils.
52 */
53 public final class StringUtil
54 {
55 private static final String PROPERTY_XSS_CHARACTERS = "input.xss.characters";
56 private static final String PROPERTY_MAIL_PATTERN = "mail.accepted.pattern";
57 private static final String STRING_CODE_PATTERN = "^[\\w]+$";
58 private static final String CONSTANT_AT = "@";
59 private static final String CONSTANT_UTF8 = "UTF-8";
60 private static final String EMAIL_PATTERN = "^[\\w_.\\-]+@[\\w_.\\-]+\\.[\\w]+$";
61
62 // The characters that are considered dangerous for XSS attacks
63 private static char [ ] _aXssCharacters;
64 private static String _xssCharactersAsString;
65
66 /**
67 * Constructor with no parameter
68 */
69 private StringUtil( )
70 {
71 }
72
73 /**
74 * This function substitutes all occurences of a given bookmark by a given value
75 *
76 * @param strSource
77 * The input string that contains bookmarks to replace
78 * @param strValue
79 * The value to substitute to the bookmark
80 * @param strBookmark
81 * The bookmark name
82 * @return The output string.
83 */
84 public static String substitute( String strSource, String strValue, String strBookmark )
85 {
86 StringBuilder strResult = new StringBuilder( );
87 int nPos = strSource.indexOf( strBookmark );
88 String strModifySource = strSource;
89
90 while ( nPos != -1 )
91 {
92 strResult.append( strModifySource.substring( 0, nPos ) );
93 strResult.append( strValue );
94 strModifySource = strModifySource.substring( nPos + strBookmark.length( ) );
95 nPos = strModifySource.indexOf( strBookmark );
96 }
97
98 strResult.append( strModifySource );
99
100 return strResult.toString( );
101 }
102
103 /**
104 * This function converts French diacritics characters into non diacritics.
105 *
106 * @param strSource
107 * The String to convert
108 * @return The sTring converted to French non diacritics characters
109 */
110 public static String replaceAccent( String strSource )
111 {
112 String strNormalized = Normalizer.normalize( strSource, Normalizer.Form.NFKD );
113 strNormalized = strNormalized.replaceAll( "\\p{M}", "" );
114
115 return strNormalized;
116 }
117
118 /**
119 * Checks if a string literal contains any HTML special characters (&, ", ' <, >).
120 *
121 * @param strValue
122 * The string literal to check
123 * @return True if the string literal contains any special character
124 */
125 public static boolean containsHtmlSpecialCharacters( String strValue )
126 {
127 return ( ( strValue.indexOf( '"' ) > -1 ) || ( strValue.indexOf( '&' ) > -1 ) || ( strValue.indexOf( '<' ) > -1 ) || ( strValue.indexOf( '>' ) > -1 ) );
128 }
129
130 /**
131 * Checks if a String contains characters that could be used for a cross-site scripting attack.
132 *
133 * @param strValue
134 * a character String
135 * @return true if the String contains illegal characters
136 */
137 public static synchronized boolean containsXssCharacters( String strValue )
138 {
139 // Read XSS characters from properties file if not already initialized
140 if ( _aXssCharacters == null )
141 {
142 _aXssCharacters = AppPropertiesService.getProperty( PROPERTY_XSS_CHARACTERS ).toCharArray( );
143 }
144
145 return containsXssCharacters( strValue, _aXssCharacters );
146 }
147
148 /**
149 * Checks if a String contains characters that could be used for a cross-site scripting attack.
150 *
151 * @param strValue
152 * a character String
153 * @param aXssCharacters
154 * a Xss characters tab to check in strValue
155 * @return true if the String contains illegal characters
156 */
157 public static synchronized boolean containsXssCharacters( String strValue, char [ ] aXssCharacters )
158 {
159 // Read XSS characters from properties file if not already initialized
160 boolean bContains = false;
161
162 if ( aXssCharacters != null )
163 {
164 for ( int nIndex = 0; !bContains && ( nIndex < aXssCharacters.length ); nIndex++ )
165 {
166 bContains = strValue.lastIndexOf( aXssCharacters [nIndex] ) >= 0;
167 }
168 }
169
170 return bContains;
171 }
172
173 /**
174 * Checks if a String contains characters that could be used for a cross-site scripting attack.
175 *
176 * @param strValue
177 * a character String
178 * @param strXssCharacters
179 * a String wich contain a list of Xss characters to check in strValue
180 * @return true if the String contains illegal characters
181 */
182 public static synchronized boolean containsXssCharacters( String strValue, String strXssCharacters )
183 {
184 // Read XSS characters from properties file if not already initialized
185 if ( strXssCharacters != null )
186 {
187 return containsXssCharacters( strValue, strXssCharacters.toCharArray( ) );
188 }
189
190 return false;
191 }
192
193 /**
194 * Simple convenience method to return the XSS characters as a string, to include it in error messages.
195 *
196 * @return a String containing a comma-separated list of the XSS characters
197 */
198 public static synchronized String getXssCharactersAsString( )
199 {
200 // Read XSS characters from properties file if not already initialized
201 if ( _aXssCharacters == null )
202 {
203 _aXssCharacters = AppPropertiesService.getProperty( PROPERTY_XSS_CHARACTERS ).toCharArray( );
204 }
205
206 if ( _xssCharactersAsString == null )
207 {
208 StringBuilder sbfCharList = new StringBuilder( );
209
210 int iIndex;
211
212 for ( iIndex = 0; iIndex < ( _aXssCharacters.length - 1 ); iIndex++ )
213 {
214 sbfCharList.append( _aXssCharacters [iIndex] );
215 sbfCharList.append( ", " );
216 }
217
218 // Append last character outside of the loop to avoid trailing comma
219 sbfCharList.append( _aXssCharacters [iIndex] );
220 _xssCharactersAsString = sbfCharList.toString( );
221 }
222
223 return _xssCharactersAsString;
224 }
225
226 /**
227 * This function checks if an email is in a valid format Returns true if the email is valid
228 *
229 * @param strEmail
230 * The mail to check
231 * @return boolean true if strEmail is valid
232 */
233 public static synchronized boolean checkEmail( String strEmail )
234 {
235 return strEmail.matches( AppPropertiesService.getProperty( PROPERTY_MAIL_PATTERN, EMAIL_PATTERN ) );
236 }
237
238 /**
239 * This function checks if an email is in a valid format, and is not in a banned domain names list. Returns true if the email is valid
240 *
241 * @param strEmail
242 * The mail to check
243 * @param strBannedDomainNames
244 * The list of banned domain names. Domain names may start with a '@' or not.
245 * @return boolean true if strEmail is valid, false otherwise
246 */
247 public static synchronized boolean checkEmailAndDomainName( String strEmail, String [ ] strBannedDomainNames )
248 {
249 boolean bIsValid = strEmail.matches( AppPropertiesService.getProperty( PROPERTY_MAIL_PATTERN, EMAIL_PATTERN ) );
250
251 return bIsValid && checkEmailDomainName( strEmail, strBannedDomainNames );
252 }
253
254 /**
255 * Check if a domain name of an email address is not in a banned domain names list.
256 *
257 * @param strEmail
258 * Email addresse to check
259 * @param strBannedDomainNames
260 * List of banned domain names
261 * @return True if the email address is correct, false otherwise
262 */
263 public static synchronized boolean checkEmailDomainName( String strEmail, String [ ] strBannedDomainNames )
264 {
265 if ( ( strBannedDomainNames != null ) && ( strBannedDomainNames.length > 0 ) )
266 {
267 int nOffset;
268
269 if ( strBannedDomainNames [0].contains( CONSTANT_AT ) )
270 {
271 nOffset = 0;
272 }
273 else
274 {
275 nOffset = 1;
276 }
277
278 int nIndex = strEmail.indexOf( CONSTANT_AT );
279
280 if ( ( nIndex >= 0 ) && ( ( nIndex + nOffset ) < strEmail.length( ) ) )
281 {
282 String strDomainName = strEmail.substring( nIndex + nOffset );
283
284 for ( String strDomain : strBannedDomainNames )
285 {
286 if ( strDomainName.equals( strDomain ) )
287 {
288 return false;
289 }
290 }
291 }
292 }
293
294 return true;
295 }
296
297 /**
298 * Check a code key.<br>
299 * Return true if each character of String is
300 * <ul>
301 * <li>number</li>
302 * <li>lower case</li>
303 * <li>upper case</li>
304 * </ul>
305 *
306 * @param strCodeKey
307 * The code Key
308 * @return True if code key is valid
309 */
310 public static synchronized boolean checkCodeKey( String strCodeKey )
311 {
312 return strCodeKey != null && strCodeKey.matches( STRING_CODE_PATTERN );
313 }
314
315 /**
316 * Converts <code>strValue</code> to an int value.
317 *
318 * @param strValue
319 * the value to convert
320 * @param nDefaultValue
321 * the default returned value
322 * @return <code>strValue</code> int value, <code>nDefaultValue</code> if strValue is not an Integer.
323 */
324 public static int getIntValue( String strValue, int nDefaultValue )
325 {
326 try
327 {
328 return Integer.parseInt( strValue );
329 }
330 catch( NumberFormatException nfe )
331 {
332 AppLogService.error( nfe.getMessage( ), nfe );
333 }
334
335 return nDefaultValue;
336 }
337
338 /**
339 * Return true if any of the strings is empty, false otherwise
340 *
341 * @param strings
342 * the strings to test
343 * @return
344 */
345 public static boolean isAnyEmpty( String... strings )
346 {
347 for ( String string : strings )
348 {
349 if ( StringUtils.isEmpty( string ) )
350 {
351 return true;
352 }
353 }
354 return false;
355 }
356
357 /**
358 * compress (with default UTF-8 encoding)
359 *
360 * @param the string to compress
361 * @return the compressed string
362 * @throws IOException
363 */
364 public static byte[] compress(String str) throws IOException {
365
366 if (str == null || str.length() == 0) {
367 return "".getBytes( CONSTANT_UTF8 );
368 }
369
370 ByteArrayOutputStream out = new ByteArrayOutputStream();
371 GZIPOutputStream gzip = new GZIPOutputStream(out);
372 gzip.write( str.getBytes( CONSTANT_UTF8 ) );
373 gzip.close( );
374
375 return out.toByteArray();
376 }
377
378 /**
379 * uncompress (with default UTF-8 encoding)
380 *
381 * @param the compressed string
382 * @return the uncompressed string
383 * @throws IOException
384 */
385 public static String decompress(byte[] bytes) throws IOException {
386 return decompress( bytes, CONSTANT_UTF8);
387 }
388
389 /**
390 * uncompress
391 *
392 * @param the compressed string
393 * @param the encoding
394 * @return the uncompressed string
395 * @throws IOException
396 */
397 public static String decompress(byte[] bytes, String encoding) throws IOException {
398
399 if (bytes == null || bytes.length == 0) {
400 return "";
401 }
402
403 GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(bytes));
404
405 ByteArrayOutputStream out = new ByteArrayOutputStream();
406
407 byte[] b = new byte[4096];
408 int len;
409 while ( (len = gis.read( b ) ) >= 0 )
410 {
411 out.write(b, 0, len);
412 }
413
414 return out.toString(encoding);
415 }
416 }