View Javadoc
1   /*
2    * Copyright (c) 2002-2022, 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 }