xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/web/Util.java (revision d6df19e1b22784c78f567cf74c42f18e3901b900)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
22  * Portions Copyright (c) 2011, Jens Elkner.
23  * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
24  * Portions Copyright (c) 2019, Krystof Tulinger <k.tulinger@seznam.cz>.
25  */
26 package org.opengrok.indexer.web;
27 
28 import static org.opengrok.indexer.index.Indexer.PATH_SEPARATOR;
29 
30 import java.io.BufferedInputStream;
31 import java.io.File;
32 import java.io.FileInputStream;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.io.InputStreamReader;
36 import java.io.Reader;
37 import java.io.Writer;
38 import java.net.MalformedURLException;
39 import java.net.URI;
40 import java.net.URISyntaxException;
41 import java.net.URL;
42 import java.net.URLDecoder;
43 import java.net.URLEncoder;
44 import java.nio.charset.StandardCharsets;
45 import java.text.DecimalFormat;
46 import java.text.NumberFormat;
47 import java.util.Collection;
48 import java.util.HashMap;
49 import java.util.LinkedList;
50 import java.util.List;
51 import java.util.Locale;
52 import java.util.Map;
53 import java.util.Map.Entry;
54 import java.util.TreeMap;
55 import java.util.function.Function;
56 import java.util.logging.Level;
57 import java.util.logging.Logger;
58 import java.util.regex.Matcher;
59 import java.util.regex.Pattern;
60 import java.util.zip.GZIPInputStream;
61 
62 import jakarta.servlet.http.HttpServletRequest;
63 import org.apache.commons.lang3.SystemUtils;
64 import org.apache.lucene.queryparser.classic.QueryParser;
65 import org.opengrok.indexer.configuration.RuntimeEnvironment;
66 import org.opengrok.indexer.history.Annotation;
67 import org.opengrok.indexer.history.HistoryException;
68 import org.opengrok.indexer.history.HistoryGuru;
69 import org.opengrok.indexer.logger.LoggerFactory;
70 
71 /**
72  * Class for useful functions.
73  */
74 public final class Util {
75 
76     private static final Logger LOGGER = LoggerFactory.getLogger(Util.class);
77 
78     private static final int BOLD_COUNT_THRESHOLD = 1000;
79 
80     private static final String anchorLinkStart = "<a href=\"";
81     private static final String anchorClassStart = "<a class=\"";
82     private static final String anchorEnd = "</a>";
83     private static final String closeQuotedTag = "\">";
84 
85     private static final String RE_Q_ESC_AMP_AMP = "\\?|&amp;|&";
86     private static final String RE_Q_E_A_A_COUNT_EQ_VAL = "(" + RE_Q_ESC_AMP_AMP + "|\\b)" +
87             QueryParameters.COUNT_PARAM_EQ + "\\d+";
88     private static final String RE_Q_E_A_A_START_EQ_VAL = "(" + RE_Q_ESC_AMP_AMP + "|\\b)" +
89             QueryParameters.START_PARAM_EQ + "\\d+";
90     private static final String RE_A_ANCHOR_Q_E_A_A = "^(" + RE_Q_ESC_AMP_AMP + ")";
91 
92     /** Private to enforce static. */
Util()93     private Util() {
94     }
95 
96     /**
97      * Return a string that represents <code>s</code> in HTML by calling
98      * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
99      * with {@code s}, a transient {@link StringBuilder}, and {@code true}.
100      * <p>
101      * (N.b. if no special characters are present, {@code s} is returned as is,
102      * without the expensive call.)
103      *
104      * @param s a defined string
105      * @return a string representing the character sequence in HTML
106      */
prehtmlize(String s)107     public static String prehtmlize(String s) {
108         if (!needsHtmlize(s, true)) {
109             return s;
110         }
111 
112         StringBuilder sb = new StringBuilder(s.length() * 2);
113         try {
114             htmlize(s, sb, true);
115         } catch (IOException ioe) {
116             // IOException cannot happen when the destination is a
117             // StringBuilder. Wrap in an AssertionError so that callers
118             // don't have to check for an IOException that should never
119             // happen.
120             throw new AssertionError("StringBuilder threw IOException", ioe);
121         }
122         return sb.toString();
123     }
124 
125     /**
126      * Calls
127      * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
128      * with {@code q}, a transient {@link StringBuilder}, and {@code true}.
129      * @param q a character sequence
130      * @return a string representing the character sequence in HTML
131      */
prehtmlize(CharSequence q)132     public static String prehtmlize(CharSequence q) {
133         StringBuilder sb = new StringBuilder(q.length() * 2);
134         try {
135             htmlize(q, sb, true);
136         } catch (IOException ioe) {
137             // IOException cannot happen when the destination is a
138             // StringBuilder. Wrap in an AssertionError so that callers
139             // don't have to check for an IOException that should never
140             // happen.
141             throw new AssertionError("StringBuilder threw IOException", ioe);
142         }
143         return sb.toString();
144     }
145 
146     /**
147      * Append to {@code dest} the UTF-8 URL-encoded representation of the
148      * Lucene-escaped version of {@code str}.
149      * @param str a defined instance
150      * @param dest a defined target
151      * @throws IOException I/O exception
152      */
qurlencode(String str, Appendable dest)153     public static void qurlencode(String str, Appendable dest) throws IOException {
154         uriEncode(QueryParser.escape(str), dest);
155     }
156 
157     /**
158      * Return a string that represents a <code>CharSequence</code> in HTML by
159      * calling
160      * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
161      * with {@code s}, a transient {@link StringBuilder}, and {@code false}.
162      * <p>
163      * (N.b. if no special characters are present, {@code s} is returned as is,
164      * without the expensive call.)
165      *
166      * @param s a defined string
167      * @return a string representing the character sequence in HTML
168      */
htmlize(String s)169     public static String htmlize(String s) {
170         if (!needsHtmlize(s, false)) {
171             return s;
172         }
173 
174         StringBuilder sb = new StringBuilder(s.length() * 2);
175         try {
176             htmlize(s, sb, false);
177         } catch (IOException ioe) {
178             // IOException cannot happen when the destination is a
179             // StringBuilder. Wrap in an AssertionError so that callers
180             // don't have to check for an IOException that should never
181             // happen.
182             throw new AssertionError("StringBuilder threw IOException", ioe);
183         }
184         return sb.toString();
185     }
186 
187     /**
188      * Return a string which represents a <code>CharSequence</code> in HTML by
189      * calling
190      * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
191      * with {@code q}, a transient {@link StringBuilder}, and {@code false}.
192      *
193      * @param q a character sequence
194      * @return a string representing the character sequence in HTML
195      */
htmlize(CharSequence q)196     public static String htmlize(CharSequence q) {
197         StringBuilder sb = new StringBuilder(q.length() * 2);
198         try {
199             htmlize(q, sb, false);
200         } catch (IOException ioe) {
201             // IOException cannot happen when the destination is a
202             // StringBuilder. Wrap in an AssertionError so that callers
203             // don't have to check for an IOException that should never
204             // happen.
205             throw new AssertionError("StringBuilder threw IOException", ioe);
206         }
207         return sb.toString();
208     }
209 
210     /**
211      * Append a character sequence to the given destination whereby special
212      * characters for HTML or characters that are not printable ASCII are
213      * escaped accordingly.
214      *
215      * @param q a character sequence to escape
216      * @param dest where to append the character sequence to
217      * @param pre a value indicating whether the output is pre-formatted -- if
218      * true then LFs will not be converted to &lt;br&gt; elements
219      * @throws IOException if an error occurred when writing to {@code dest}
220      */
htmlize(CharSequence q, Appendable dest, boolean pre)221     public static void htmlize(CharSequence q, Appendable dest, boolean pre)
222             throws IOException {
223         for (int i = 0; i < q.length(); i++) {
224             htmlize(q.charAt(i), dest, pre);
225         }
226     }
227 
228     /**
229      * Calls
230      * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
231      * with {@code q}, {@code dest}, and {@code false}.
232      *
233      * @param q a character sequence to escape
234      * @param dest where to append the character sequence to
235      * @throws IOException if an error occurred when writing to {@code dest}
236      */
htmlize(CharSequence q, Appendable dest)237     public static void htmlize(CharSequence q, Appendable dest)
238             throws IOException {
239         htmlize(q, dest, false);
240     }
241 
242     /**
243      * Append a character array to the given destination whereby special
244      * characters for HTML or characters that are not printable ASCII are
245      * escaped accordingly.
246      *
247      * @param cs characters to escape
248      * @param length max. number of characters to append, starting from index 0.
249      * @param dest where to append the character sequence to
250      * @throws IOException if an error occurred when writing to {@code dest}
251      */
htmlize(char[] cs, int length, Appendable dest)252     public static void htmlize(char[] cs, int length, Appendable dest)
253             throws IOException {
254         int len = length;
255         if (cs.length < length) {
256             len = cs.length;
257         }
258         for (int i = 0; i < len; i++) {
259             htmlize(cs[i], dest, false);
260         }
261     }
262 
263     /**
264      * Append a character to the given destination whereby special characters
265      * special for HTML or characters that are not printable ASCII are
266      * escaped accordingly.
267      *
268      * @param c the character to append
269      * @param dest where to append the character to
270      * @param pre a value indicating whether the output is pre-formatted -- if
271      * true then LFs will not be converted to &lt;br&gt; elements
272      * @throws IOException if an error occurred when writing to {@code dest}
273      * @see #needsHtmlize(char, boolean)
274      */
htmlize(char c, Appendable dest, boolean pre)275     private static void htmlize(char c, Appendable dest, boolean pre)
276             throws IOException {
277         switch (c) {
278             case '\'':
279                 dest.append("&apos;");
280                 break;
281             case '"':
282                 dest.append("&quot;");
283                 break;
284             case '&':
285                 dest.append("&amp;");
286                 break;
287             case '>':
288                 dest.append("&gt;");
289                 break;
290             case '<':
291                 dest.append("&lt;");
292                 break;
293             case '\n':
294                 if (pre) {
295                     dest.append(c);
296                 } else {
297                     dest.append("<br/>");
298                 }
299                 break;
300             default:
301                 if ((c >= ' ' && c <= '~') || (c < ' ' &&
302                     Character.isWhitespace(c))) {
303                     dest.append(c);
304                 } else {
305                     dest.append("&#").append(Integer.toString(c)).append(';');
306                 }
307                 break;
308         }
309     }
310 
311     /**
312      * Determine if a character is a special character needing HTML escaping or
313      * is a character that is not printable ASCII.
314      * @param c the character to examine
315      * @param pre a value indicating whether the output is pre-formatted -- if
316      * true then LFs will not be converted to &lt;br&gt; elements
317      * @see #htmlize(char, java.lang.Appendable, boolean)
318      */
needsHtmlize(char c, boolean pre)319     private static boolean needsHtmlize(char c, boolean pre) {
320         switch (c) {
321             case '\'':
322             case '"':
323             case '&':
324             case '>':
325             case '<':
326                 return true;
327             case '\n':
328                 if (!pre) {
329                     return true;
330                 }
331             default:
332                 return (c < ' ' || c > '~') && (c >= ' ' || !Character.isWhitespace(c));
333         }
334     }
335 
needsHtmlize(CharSequence q, boolean pre)336     private static boolean needsHtmlize(CharSequence q, boolean pre) {
337         for (int i = 0; i < q.length(); ++i) {
338             if (needsHtmlize(q.charAt(i), pre)) {
339                 return true;
340             }
341         }
342         return false;
343     }
344 
345     /**
346      * Convenience method for {@code breadcrumbPath(urlPrefix, path, PATH_SEPARATOR)}.
347      *
348      * @param urlPrefix prefix to add to each url
349      * @param path path to crack
350      * @return HTML markup for the breadcrumb or the path itself.
351      *
352      * @see #breadcrumbPath(String, String, char)
353      */
breadcrumbPath(String urlPrefix, String path)354     public static String breadcrumbPath(String urlPrefix, String path) {
355         return breadcrumbPath(urlPrefix, path, PATH_SEPARATOR);
356     }
357 
358     /**
359      * Convenience method for
360      * {@code breadcrumbPath(urlPrefix, path, sep, "", false)}.
361      *
362      * @param urlPrefix prefix to add to each url
363      * @param path path to crack
364      * @param sep separator to use to crack the given path
365      *
366      * @return HTML markup fro the breadcrumb or the path itself.
367      * @see #breadcrumbPath(String, String, char, String, boolean, boolean)
368      */
breadcrumbPath(String urlPrefix, String path, char sep)369     public static String breadcrumbPath(String urlPrefix, String path, char sep) {
370         return breadcrumbPath(urlPrefix, path, sep, "", false);
371     }
372 
373     /**
374      * Convenience method for
375      * {@code breadcrumbPath(urlPrefix, path, sep, "", false, path.endsWith(sep)}.
376      *
377      * @param urlPrefix prefix to add to each url
378      * @param path path to crack
379      * @param sep separator to use to crack the given path
380      * @param urlPostfix suffix to add to each url
381      * @param compact if {@code true} the given path gets transformed into its
382      * canonical form (.i.e. all '.' and '..' and double separators removed, but
383      * not always resolves to an absolute path) before processing starts.
384      * @return HTML markup fro the breadcrumb or the path itself.
385      * @see #breadcrumbPath(String, String, char, String, boolean, boolean)
386      * @see #getCanonicalPath(String, char)
387      */
breadcrumbPath(String urlPrefix, String path, char sep, String urlPostfix, boolean compact)388     public static String breadcrumbPath(String urlPrefix, String path,
389             char sep, String urlPostfix, boolean compact) {
390         if (path == null || path.length() == 0) {
391             return path;
392         }
393         return breadcrumbPath(urlPrefix, path, sep, urlPostfix, compact,
394                 path.charAt(path.length() - 1) == sep);
395     }
396 
397     /**
398      * Create a breadcrumb path to allow navigation to each element of a path.
399      * Consecutive separators (<var>sep</var>) in the given <var>path</var> are
400      * always collapsed into a single separator automatically. If
401      * <var>compact</var> is {@code true} path gets translated into a canonical
402      * path similar to {@link File#getCanonicalPath()}, however the current
403      * working directory is assumed to be "/" and no checks are done (e.g.
404      * neither whether the path [component] exists nor which type it is).
405      *
406      * @param urlPrefix what should be prepend to the constructed URL
407      * @param path the full path from which the breadcrumb path is built.
408      * @param sep the character that separates the path components in
409      * <var>path</var>
410      * @param urlPostfix what should be append to the constructed URL
411      * @param compact if {@code true}, a canonical path gets constructed before
412      * processing.
413      * @param isDir if {@code true} a "/" gets append to the last path
414      * component's link and <var>sep</var> to its name
415      * @return <var>path</var> if it resolves to an empty or "/" or {@code null}
416      * path, the HTML markup for the breadcrumb path otherwise.
417      */
breadcrumbPath(String urlPrefix, String path, char sep, String urlPostfix, boolean compact, boolean isDir)418     public static String breadcrumbPath(String urlPrefix, String path,
419             char sep, String urlPostfix, boolean compact, boolean isDir) {
420         if (path == null || path.length() == 0) {
421             return path;
422         }
423         String[] pnames = normalize(path.split(escapeForRegex(sep)), compact);
424         if (pnames.length == 0) {
425             return path;
426         }
427         String prefix = urlPrefix == null ? "" : urlPrefix;
428         String postfix = urlPostfix == null ? "" : urlPostfix;
429         StringBuilder pwd = new StringBuilder(path.length() + pnames.length);
430         StringBuilder markup
431                 = new StringBuilder((pnames.length + 3 >> 1) * path.length()
432                         + pnames.length
433                         * (17 + prefix.length() + postfix.length()));
434         int k = path.indexOf(pnames[0]);
435         if (path.lastIndexOf(sep, k) != -1) {
436             pwd.append(PATH_SEPARATOR);
437             markup.append(sep);
438         }
439         for (int i = 0; i < pnames.length; i++) {
440             pwd.append(uriEncodePath(pnames[i]));
441             if (isDir || i < pnames.length - 1) {
442                 pwd.append(PATH_SEPARATOR);
443             }
444             markup.append(anchorLinkStart).append(prefix).append(pwd)
445                     .append(postfix).append(closeQuotedTag).append(pnames[i])
446                     .append(anchorEnd);
447             if (isDir || i < pnames.length - 1) {
448                 markup.append(sep);
449             }
450         }
451         return markup.toString();
452     }
453 
454     /**
455      * Normalize the given <var>path</var> to its canonical form. I.e. all
456      * separators (<var>sep</var>) are replaced with a slash ('/'), all double
457      * slashes are replaced by a single slash, all single dot path components
458      * (".") of the formed path are removed and all double dot path components
459      * (".." ) of the formed path are replaced with its parent or '/' if there
460      * is no parent.
461      * <p>
462      * So the difference to {@link File#getCanonicalPath()} is, that this method
463      * does not hit the disk (just string manipulation), resolves
464      * <var>path</var>
465      * always against '/' and thus always returns an absolute path, which may
466      * actually not exist, and which has a single trailing '/' if the given
467      * <var>path</var> ends with the given <var>sep</var>.
468      *
469      * @param path path to mangle. If not absolute or {@code null}, the current
470      * working directory is assumed to be '/'.
471      * @param sep file separator to use to crack <var>path</var> into path
472      * components
473      * @return always a canonical path which starts with a '/'.
474      */
getCanonicalPath(String path, char sep)475     public static String getCanonicalPath(String path, char sep) {
476         if (path == null || path.length() == 0) {
477             return "/";
478         }
479         String[] pnames = normalize(path.split(escapeForRegex(sep)), true);
480         if (pnames.length == 0) {
481             return "/";
482         }
483         StringBuilder buf = new StringBuilder(path.length());
484         buf.append('/');
485         for (String pname : pnames) {
486             buf.append(pname).append('/');
487         }
488         if (path.charAt(path.length() - 1) != sep) {
489             // since is not a general purpose method. So we waive to handle
490             // cases like:
491             // || path.endsWith("/..") || path.endsWith("/.")
492             buf.setLength(buf.length() - 1);
493         }
494         return buf.toString();
495     }
496 
497     private static final Pattern EMAIL_PATTERN
498             = Pattern.compile("([^<\\s]+@[^>\\s]+)");
499 
500     /**
501      * Get email address of the author.
502      *
503      * @param author string containing author and possibly email address.
504      * @return email address of the author or full author string if the author
505      * string does not contain an email address.
506      */
getEmail(String author)507     public static String getEmail(String author) {
508         Matcher emailMatcher = EMAIL_PATTERN.matcher(author);
509         String email = author;
510         if (emailMatcher.find()) {
511             email = emailMatcher.group(1).trim();
512         }
513 
514         return email;
515     }
516 
517     /**
518      * Remove all empty and {@code null} string elements from the given
519      * <var>names</var> and optionally all redundant information like "." and
520      * "..".
521      *
522      * @param names names to check
523      * @param canonical if {@code true}, remove redundant elements as well.
524      * @return a possible empty array of names all with a length &gt; 0.
525      */
normalize(String[] names, boolean canonical)526     private static String[] normalize(String[] names, boolean canonical) {
527         LinkedList<String> res = new LinkedList<>();
528         if (names == null || names.length == 0) {
529             return new String[0];
530         }
531         for (String name : names) {
532             if (name == null || name.length() == 0) {
533                 continue;
534             }
535             if (canonical) {
536                 if (name.equals("..")) {
537                     if (!res.isEmpty()) {
538                         res.removeLast();
539                     }
540                 } else if (!name.equals(".")) {
541                     res.add(name);
542                 }
543             } else {
544                 res.add(name);
545             }
546         }
547         return res.size() == names.length ? names : res.toArray(new String[0]);
548     }
549 
550     /**
551      * Generate a regexp that matches the specified character. Escape it in case
552      * it is a character that has a special meaning in a regexp.
553      *
554      * @param c the character that the regexp should match
555      * @return a six-character string in the form of <code>&#92;u</code><i>hhhh</i>
556      */
escapeForRegex(char c)557     private static String escapeForRegex(char c) {
558         StringBuilder sb = new StringBuilder(6);
559         sb.append("\\u");
560         String hex = Integer.toHexString(c);
561         sb.append("0".repeat(4 - hex.length()));
562         sb.append(hex);
563         return sb.toString();
564     }
565 
566     private static final NumberFormat FORMATTER = new DecimalFormat("#,###,###,###.#");
567 
568     private static final NumberFormat COUNT_FORMATTER = new DecimalFormat("#,###,###,###");
569 
570     /**
571      * Convert the given size into a human readable string.
572      *
573      * NOTE: when changing the output of this function make sure to adapt the
574      * jQuery tablesorter custom parsers in web/httpheader.jspf
575      *
576      * @param num size to convert.
577      * @return a readable string
578      */
readableSize(long num)579     public static String readableSize(long num) {
580         float l = num;
581         NumberFormat formatter = (NumberFormat) FORMATTER.clone();
582         if (l < 1024) {
583             return formatter.format(l) + ' '; // for none-dirs append 'B'? ...
584         } else if (l < 1048576) {
585             return (formatter.format(l / 1024) + " KiB");
586         } else if (l < 1073741824) {
587             return ("<b>" + formatter.format(l / 1048576) + " MiB</b>");
588         } else {
589             return ("<b>" + formatter.format(l / 1073741824) + " GiB</b>");
590         }
591     }
592 
593     /**
594      * Convert the specified {@code count} into a human readable string.
595      * @param count value to convert.
596      * @return a readable string
597      */
readableCount(long count)598     public static String readableCount(long count) {
599         return readableCount(count, false);
600     }
601 
602     /**
603      * Convert the specified {@code count} into a human readable string.
604      * @param isKnownDirectory a value indicating if {@code count} is known to
605      *                         be for a directory
606      * @param count value to convert.
607      * @return a readable string
608      */
readableCount(long count, boolean isKnownDirectory)609     public static String readableCount(long count, boolean isKnownDirectory) {
610         NumberFormat formatter = (NumberFormat) COUNT_FORMATTER.clone();
611         if (isKnownDirectory || count < BOLD_COUNT_THRESHOLD) {
612             return formatter.format(count);
613         } else {
614             return "<b>" + formatter.format(count) + "</b>";
615         }
616     }
617 
618     /**
619      * Converts different HTML special characters into their encodings used in
620      * html.
621      *
622      * @param s input text
623      * @return encoded text for use in &lt;a title=""&gt; tag
624      */
encode(String s)625     public static String encode(String s) {
626         /**
627          * Make sure that the buffer is long enough to contain the whole string
628          * with the expanded special characters. We use 1.5*length as a
629          * heuristic.
630          */
631         StringBuilder sb = new StringBuilder((int) Math.max(10, s.length() * 1.5));
632         try {
633             encode(s, sb);
634         } catch (IOException ex) {
635             // IOException cannot happen when the destination is a
636             // StringBuilder. Wrap in an AssertionError so that callers
637             // don't have to check for an IOException that should never
638             // happen.
639             throw new AssertionError("StringBuilder threw IOException", ex);
640         }
641         return sb.toString();
642     }
643 
644     /**
645      * Converts different HTML special characters into their encodings used in
646      * html.
647      *
648      * @param s input text
649      * @param dest appendable destination for appending the encoded characters
650      * @throws java.io.IOException I/O exception
651      */
encode(String s, Appendable dest)652     public static void encode(String s, Appendable dest) throws IOException {
653         for (int i = 0; i < s.length(); i++) {
654             char c = s.charAt(i);
655             if (c > 127 || c == '"' || c == '<' || c == '>' || c == '&' || c == '\'') {
656                 // special html characters
657                 dest.append("&#").append("" + (int) c).append(";");
658             } else if (c == ' ') {
659                 // non breaking space
660                 dest.append("&nbsp;");
661             } else if (c == '\t') {
662                 dest.append("&nbsp;&nbsp;&nbsp;&nbsp;");
663             } else if (c == '\n') {
664                 // <br/>
665                 dest.append("&lt;br/&gt;");
666             } else {
667                 dest.append(c);
668             }
669         }
670     }
671 
672     /**
673      * Encode URL.
674      *
675      * @param urlStr string URL
676      * @return the encoded URL
677      * @throws URISyntaxException URI syntax
678      * @throws MalformedURLException URL malformed
679      */
encodeURL(String urlStr)680     public static String encodeURL(String urlStr) throws URISyntaxException, MalformedURLException {
681         URL url = new URL(urlStr);
682         URI constructed = new URI(url.getProtocol(), url.getUserInfo(),
683                 url.getHost(), url.getPort(),
684                 url.getPath(), url.getQuery(), url.getRef());
685         return constructed.toString();
686     }
687 
688     /**
689      * Write out line information wrt. to the given annotation in the format:
690      * {@code Linenumber Blame Author} incl. appropriate links.
691      *
692      * @param num linenumber to print
693      * @param out print destination
694      * @param annotation annotation to use. If {@code null} only the linenumber
695      * gets printed.
696      * @param userPageLink see {@link RuntimeEnvironment#getUserPage()}
697      * @param userPageSuffix see {@link RuntimeEnvironment#getUserPageSuffix()}
698      * @param project project that is used
699      * @throws IOException depends on the destination (<var>out</var>).
700      */
readableLine(int num, Writer out, Annotation annotation, String userPageLink, String userPageSuffix, String project)701     public static void readableLine(int num, Writer out, Annotation annotation,
702             String userPageLink, String userPageSuffix, String project)
703             throws IOException {
704         readableLine(num, out, annotation, userPageLink, userPageSuffix, project, false);
705     }
706 
readableLine(int num, Writer out, Annotation annotation, String userPageLink, String userPageSuffix, String project, boolean skipNewline)707     public static void readableLine(int num, Writer out, Annotation annotation, String userPageLink,
708             String userPageSuffix, String project, boolean skipNewline)
709             throws IOException {
710         // this method should go to JFlexXref
711         String snum = String.valueOf(num);
712         if (num > 1 && !skipNewline) {
713             out.write("\n");
714         }
715         out.write(anchorClassStart);
716         out.write(num % 10 == 0 ? "hl" : "l");
717         out.write("\" name=\"");
718         out.write(snum);
719         out.write("\" href=\"#");
720         out.write(snum);
721         out.write(closeQuotedTag);
722         out.write(snum);
723         out.write(anchorEnd);
724 
725         if (annotation != null) {
726             writeAnnotation(num, out, annotation, userPageLink, userPageSuffix, project);
727         }
728     }
729 
writeAnnotation(int num, Writer out, Annotation annotation, String userPageLink, String userPageSuffix, String project)730     private static void writeAnnotation(int num, Writer out, Annotation annotation, String userPageLink,
731                                         String userPageSuffix, String project) throws IOException {
732         String r = annotation.getRevision(num);
733         boolean enabled = annotation.isEnabled(num);
734         out.write("<span class=\"blame\">");
735         if (enabled) {
736             out.write(anchorClassStart);
737             out.write("r");
738             out.write("\" style=\"background-color: ");
739             out.write(annotation.getColors().getOrDefault(r, "inherit"));
740             out.write("\" href=\"");
741             out.write(uriEncode(annotation.getFilename()));
742             out.write("?");
743             out.write(QueryParameters.ANNOTATION_PARAM_EQ_TRUE);
744             out.write("&amp;");
745             out.write(QueryParameters.REVISION_PARAM_EQ);
746             out.write(uriEncode(r));
747             String msg = annotation.getDesc(r);
748             out.write("\" title=\"");
749             if (msg != null) {
750                 out.write(Util.encode(msg));
751             }
752             if (annotation.getFileVersion(r) != 0) {
753                 out.write("&lt;br/&gt;version: " + annotation.getFileVersion(r) + "/"
754                         + annotation.getRevisions().size());
755             }
756             out.write(closeQuotedTag);
757         }
758         StringBuilder buf = new StringBuilder();
759         final boolean most_recent_revision = annotation.getFileVersion(r) == annotation.getRevisions().size();
760         // print an asterisk for the most recent revision
761         if (most_recent_revision) {
762             buf.append("<span class=\"most_recent_revision\">");
763             buf.append('*');
764         }
765         htmlize(r, buf);
766         if (most_recent_revision) {
767             buf.append("</span>"); // recent revision span
768         }
769         out.write(buf.toString());
770         buf.setLength(0);
771         if (enabled) {
772             RuntimeEnvironment env = RuntimeEnvironment.getInstance();
773 
774             out.write(anchorEnd);
775 
776             // Write link to search the revision in current project.
777             out.write(anchorClassStart);
778             out.write("search\" href=\"" + env.getUrlPrefix());
779             out.write(QueryParameters.DEFS_SEARCH_PARAM_EQ);
780             out.write("&amp;");
781             out.write(QueryParameters.REFS_SEARCH_PARAM_EQ);
782             out.write("&amp;");
783             out.write(QueryParameters.PATH_SEARCH_PARAM_EQ);
784             out.write(project);
785             out.write("&amp;");
786             out.write(QueryParameters.HIST_SEARCH_PARAM_EQ);
787             out.write("&quot;");
788             out.write(uriEncode(r));
789             out.write("&quot;&amp;");
790             out.write(QueryParameters.TYPE_SEARCH_PARAM_EQ);
791             out.write("\" title=\"Search history for this revision");
792             out.write(closeQuotedTag);
793             out.write("S");
794             out.write(anchorEnd);
795         }
796         String a = annotation.getAuthor(num);
797         if (userPageLink == null) {
798             out.write(HtmlConsts.SPAN_A);
799             htmlize(a, buf);
800             out.write(buf.toString());
801             out.write(HtmlConsts.ZSPAN);
802             buf.setLength(0);
803         } else {
804             out.write(anchorClassStart);
805             out.write("a\" href=\"");
806             out.write(userPageLink);
807             out.write(uriEncode(a));
808             if (userPageSuffix != null) {
809                 out.write(userPageSuffix);
810             }
811             out.write(closeQuotedTag);
812             htmlize(a, buf);
813             out.write(buf.toString());
814             buf.setLength(0);
815             out.write(anchorEnd);
816         }
817         out.write("</span>");
818     }
819 
820     /**
821      * Generate a string from the given path and date in a way that allows
822      * stable lexicographic sorting (i.e. gives always the same results) as a
823      * walk of the file hierarchy. Thus null character (\u0000) is used both to
824      * separate directory components and to separate the path from the date.
825      *
826      * @param path path to mangle.
827      * @param date date string to use.
828      * @return the mangled path.
829      */
path2uid(String path, String date)830     public static String path2uid(String path, String date) {
831         return path.replace('/', '\u0000') + "\u0000" + date;
832     }
833 
834     /**
835      * The reverse operation for {@link #path2uid(String, String)} - re-creates
836      * the unmangled path from the given uid.
837      *
838      * @param uid uid to unmangle.
839      * @return the original path.
840      */
uid2url(String uid)841     public static String uid2url(String uid) {
842         String url = uid.replace('\u0000', PATH_SEPARATOR);
843         return url.substring(0, url.lastIndexOf(PATH_SEPARATOR)); // remove date from end
844     }
845 
846     /**
847      * Sanitizes Windows path delimiters (if {@link SystemUtils#IS_OS_WINDOWS}
848      * is {@code true}) as
849      * {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR} in order not
850      * to conflict with the Lucene escape character and also so {@code path}
851      * appears as a correctly formed URI in search results.
852      */
fixPathIfWindows(String path)853     public static String fixPathIfWindows(String path) {
854         if (path != null && SystemUtils.IS_OS_WINDOWS) {
855             return path.replace(File.separatorChar, PATH_SEPARATOR);
856         }
857         return path;
858     }
859 
860     /**
861      * Write the 'H A D' links. This is used for search results and directory
862      * listings.
863      *
864      * @param out writer for producing output
865      * @param ctxE URI encoded prefix
866      * @param entry file/directory name to write
867      * @param isDir is directory
868      * @throws IOException depends on the destination (<var>out</var>).
869      */
writeHAD(Writer out, String ctxE, String entry, boolean isDir)870     public static void writeHAD(Writer out, String ctxE, String entry, boolean isDir) throws IOException {
871 
872         String downloadPrefixE = ctxE + Prefix.DOWNLOAD_P;
873         String xrefPrefixE = ctxE + Prefix.XREF_P;
874 
875         out.write("<td class=\"q\">");
876         if (RuntimeEnvironment.getInstance().isHistoryEnabled()) {
877             String histPrefixE = ctxE + Prefix.HIST_L;
878 
879             out.write("<a href=\"");
880             out.write(histPrefixE);
881             if (!entry.startsWith("/")) {
882                 entry = "/" + entry;
883             }
884             out.write(entry);
885             out.write("\" title=\"History\">H</a>");
886         }
887 
888         if (!isDir) {
889             out.write(" <a href=\"");
890             out.write(xrefPrefixE);
891             out.write(entry);
892             out.write("?");
893             out.write(QueryParameters.ANNOTATION_PARAM_EQ_TRUE);
894             out.write("\" title=\"Annotate\">A</a> ");
895             out.write("<a href=\"");
896             out.write(downloadPrefixE);
897             out.write(entry);
898             out.write("\" title=\"Download\">D</a>");
899         }
900 
901         out.write("</td>");
902     }
903 
904     /**
905      * Wrapper around UTF-8 URL encoding of a string.
906      *
907      * @param q query to be encoded. If {@code null}, an empty string will be used instead.
908      * @return null if failed, otherwise the encoded string
909      * @see URLEncoder#encode(String, String)
910      */
uriEncode(String q)911     public static String uriEncode(String q) {
912         return q == null ? "" : URLEncoder.encode(q, StandardCharsets.UTF_8);
913     }
914 
915     /**
916      * Append to {@code dest} the UTF-8 URL-encoded representation of
917      * {@code str}.
918      * @param str a defined instance
919      * @param dest a defined target
920      * @throws IOException I/O
921      */
uriEncode(String str, Appendable dest)922     public static void uriEncode(String str, Appendable dest)
923             throws IOException {
924         String uenc = uriEncode(str);
925         dest.append(uenc);
926     }
927 
928     /**
929      * Append '&amp;name=value" to the given buffer. If the given
930      * <var>value</var>
931      * is {@code null}, this method does nothing.
932      *
933      * @param buf where to append the query string
934      * @param key the name of the parameter to add. Append as is!
935      * @param value the value for the given parameter. Gets automatically UTF-8
936      * URL encoded.
937      * @throws NullPointerException if the given buffer is {@code null}.
938      * @see #uriEncode(String)
939      */
appendQuery(StringBuilder buf, String key, String value)940     public static void appendQuery(StringBuilder buf, String key,
941             String value) {
942 
943         if (value != null) {
944             buf.append("&amp;").append(key).append('=').append(uriEncode(value));
945         }
946     }
947 
948     /**
949      * URI encode the given path.
950      *
951      * @param path path to encode.
952      * @return the encoded path.
953      * @throws NullPointerException if a parameter is {@code null}
954      */
uriEncodePath(String path)955     public static String uriEncodePath(String path) {
956         // Bug #19188: Ideally, we'd like to use the standard class library to
957         // encode the paths. We're aware of the following two methods:
958         //
959         // 1) URLEncoder.encode() - this method however transforms space to +
960         // instead of %20 (which is right for HTML form data, but not for
961         // paths), and it also does not preserve the separator chars (/).
962         //
963         // 2) URI.getRawPath() - transforms space and / as expected, but gets
964         // confused when the path name contains a colon character (it thinks
965         // parts of the path is schema in that case)
966         //
967         // For now, encode manually the way we want it.
968         StringBuilder sb = new StringBuilder(path.length());
969         for (byte b : path.getBytes(StandardCharsets.UTF_8)) {
970             // URLEncoder's javadoc says a-z, A-Z, ., -, _ and * are safe
971             // characters, so we preserve those to make the encoded string
972             // shorter and easier to read. We also preserve the separator
973             // chars (/). All other characters are encoded (as UTF-8 byte
974             // sequences).
975             if ((b == '/') || (b >= 'a' && b <= 'z')
976                     || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
977                     || (b == '.') || (b == '-') || (b == '_') || (b == '*')) {
978                 sb.append((char) b);
979             } else {
980                 sb.append('%');
981                 int u = b & 0xFF;  // Make the byte value unsigned.
982                 if (u <= 0x0F) {
983                     // Add leading zero if required.
984                     sb.append('0');
985                 }
986                 sb.append(Integer.toHexString(u).toUpperCase(Locale.ROOT));
987             }
988         }
989         return sb.toString();
990     }
991 
992     /**
993      * Escape a string for use as in an HTML attribute value. The returned value
994      * is not enclosed in double quotes. The caller needs to add those.
995      *
996      * @param q string to escape.
997      * @return an empty string if a parameter is {@code null}, the mangled
998      * string otherwise.
999      */
formQuoteEscape(String q)1000     public static String formQuoteEscape(String q) {
1001         if (q == null || q.isEmpty()) {
1002             return "";
1003         }
1004         StringBuilder sb = new StringBuilder();
1005         char c;
1006         for (int i = 0; i < q.length(); i++) {
1007             c = q.charAt(i);
1008             switch (c) {
1009                 case '"':
1010                     sb.append("&quot;");
1011                     break;
1012                 case '&':
1013                     sb.append("&amp;");
1014                     break;
1015                 default:
1016                     sb.append(c);
1017                     break;
1018             }
1019         }
1020         return sb.toString();
1021     }
1022 
1023     /**
1024      * Tag changes in the given <var>line1</var> and <var>line2</var>
1025      * for highlighting. Removed parts are tagged with CSS class {@code d}, new
1026      * parts are tagged with CSS class {@code a} using a {@code span} element.
1027      * The input parameters must not have any HTML escapes in them.
1028      *
1029      * @param line1 line of the original file
1030      * @param line2 line of the changed/new file
1031      * @return the tagged lines (field[0] ~= line1, field[1] ~= line2).
1032      * @throws NullPointerException if one of the given parameters is
1033      * {@code null}.
1034      */
diffline(StringBuilder line1, StringBuilder line2)1035     public static String[] diffline(StringBuilder line1, StringBuilder line2) {
1036         String[] ret = new String[2];
1037         int s = 0;
1038         int m = line1.length() - 1;
1039         int n = line2.length() - 1;
1040         while (s <= m && s <= n && (line1.charAt(s) == line2.charAt(s))) {
1041             s++;
1042         }
1043 
1044         while (s <= m && s <= n && (line1.charAt(m) == line2.charAt(n))) {
1045             m--;
1046             n--;
1047         }
1048 
1049         // deleted
1050         if (s <= m) {
1051             String sb = Util.htmlize(line1.substring(0, s)) +
1052                     HtmlConsts.SPAN_D +
1053                     Util.htmlize(line1.substring(s, m + 1)) +
1054                     HtmlConsts.ZSPAN +
1055                     Util.htmlize(line1.substring(m + 1, line1.length()));
1056             ret[0] = sb;
1057         } else {
1058             ret[0] = Util.htmlize(line1.toString()); // no change
1059         }
1060 
1061         // added
1062         if (s <= n) {
1063             String sb = Util.htmlize(line2.substring(0, s)) +
1064                     HtmlConsts.SPAN_A +
1065                     Util.htmlize(line2.substring(s, n + 1)) +
1066                     HtmlConsts.ZSPAN +
1067                     Util.htmlize(line2.substring(n + 1, line2.length()));
1068             ret[1] = sb;
1069         } else {
1070             ret[1] = Util.htmlize(line2.toString()); // no change
1071         }
1072 
1073         return ret;
1074     }
1075 
1076     /**
1077      * Dump the configuration as an HTML table.
1078      *
1079      * @param out destination for the HTML output
1080      * @throws IOException if an error happens while writing to {@code out}
1081      * @throws HistoryException if the history guru cannot be accesses
1082      */
1083     @SuppressWarnings("boxing")
dumpConfiguration(Appendable out)1084     public static void dumpConfiguration(Appendable out) throws IOException,
1085             HistoryException {
1086         out.append("<table border=\"1\" width=\"100%\">");
1087         out.append("<tr><th>Variable</th><th>Value</th></tr>");
1088         RuntimeEnvironment env = RuntimeEnvironment.getInstance();
1089         printTableRow(out, "Source root", env.getSourceRootPath());
1090         printTableRow(out, "Data root", env.getDataRootPath());
1091         printTableRow(out, "CTags", env.getCtags());
1092         printTableRow(out, "Bug page", env.getBugPage());
1093         printTableRow(out, "Bug pattern", env.getBugPattern());
1094         printTableRow(out, "User page", env.getUserPage());
1095         printTableRow(out, "User page suffix", env.getUserPageSuffix());
1096         printTableRow(out, "Review page", env.getReviewPage());
1097         printTableRow(out, "Review pattern", env.getReviewPattern());
1098         printTableRow(out, "Using projects", env.isProjectsEnabled());
1099         out.append("<tr><td>Ignored files</td><td>");
1100         printUnorderedList(out, env.getIgnoredNames().getItems());
1101         out.append("</td></tr>");
1102         printTableRow(out, "lucene RAM_BUFFER_SIZE_MB", env.getRamBufferSize());
1103         printTableRow(out, "Allow leading wildcard in search",
1104                 env.isAllowLeadingWildcard());
1105         printTableRow(out, "History cache", HistoryGuru.getInstance()
1106                 .getCacheInfo());
1107         printTableRow(out, "Authorization plugin directory", env.getPluginDirectory());
1108         printTableRow(out, "Authorization watchdog directory", env.getPluginDirectory());
1109         printTableRow(out, "Authorization watchdog enabled", env.isAuthorizationWatchdog());
1110         printTableRow(out, "Authorization stack", "<pre>" + env.getAuthorizationFramework().getStack().hierarchyToString() + "</pre>");
1111         out.append("</table>");
1112     }
1113 
1114     /**
1115      * Just read the given source and dump as is to the given destination. Does
1116      * nothing, if one or more of the parameters is {@code null}.
1117      *
1118      * @param out write destination
1119      * @param in source to read
1120      * @throws IOException as defined by the given reader/writer
1121      * @throws NullPointerException if a parameter is {@code null}.
1122      */
dump(Writer out, Reader in)1123     public static void dump(Writer out, Reader in) throws IOException {
1124         if (in == null || out == null) {
1125             return;
1126         }
1127         char[] buf = new char[8192];
1128         int len = 0;
1129         while ((len = in.read(buf)) >= 0) {
1130             out.write(buf, 0, len);
1131         }
1132     }
1133 
1134     /**
1135      * Silently dump a file to the given destination. All {@link IOException}s
1136      * gets caught and logged, but not re-thrown.
1137      *
1138      * @param out dump destination
1139      * @param dir directory, which should contains the file.
1140      * @param filename the basename of the file to dump.
1141      * @param compressed if {@code true} the denoted file is assumed to be
1142      * gzipped.
1143      * @return {@code true} on success (everything read and written).
1144      * @throws NullPointerException if a parameter is {@code null}.
1145      */
dump(Writer out, File dir, String filename, boolean compressed)1146     public static boolean dump(Writer out, File dir, String filename,
1147             boolean compressed) {
1148         return dump(out, new File(dir, filename), compressed);
1149     }
1150 
1151     /**
1152      * Silently dump a file to the given destination. All {@link IOException}s
1153      * gets caught and logged, but not re-thrown.
1154      *
1155      * @param out dump destination
1156      * @param file file to dump.
1157      * @param compressed if {@code true} the denoted file is assumed to be
1158      * gzipped.
1159      * @return {@code true} on success (everything read and written).
1160      * @throws NullPointerException if a parameter is {@code null}.
1161      */
dump(Writer out, File file, boolean compressed)1162     public static boolean dump(Writer out, File file, boolean compressed) {
1163         if (!file.exists()) {
1164             return false;
1165         }
1166         /**
1167          * For backward compatibility, read the OpenGrok-produced document
1168          * using the system default charset.
1169          */
1170         try (InputStream iss = new BufferedInputStream(new FileInputStream(file))) {
1171             try (Reader in = compressed ? new InputStreamReader(new GZIPInputStream(iss)) : new InputStreamReader(iss)) {
1172                 dump(out, in);
1173             }
1174             return true;
1175         } catch (IOException e) {
1176             LOGGER.log(Level.WARNING,
1177                     "An error occurred while piping file " + file + ": ", e);
1178         }
1179         return false;
1180     }
1181 
1182     /**
1183      * Silently dump an xref file to the given destination. All
1184      * {@link IOException}s get caught and logged, but not re-thrown.
1185      * @param out dump destination
1186      * @param file file to dump
1187      * @param compressed if {@code true} the denoted file is assumed to be gzipped
1188      * @param contextPath an optional override of "/source/" as the context path
1189      * @return {@code true} on success (everything read and written)
1190      * @throws NullPointerException if a parameter is {@code null}.
1191      */
dumpXref(Writer out, File file, boolean compressed, String contextPath)1192     public static boolean dumpXref(Writer out, File file, boolean compressed, String contextPath) {
1193 
1194         if (!file.exists()) {
1195             return false;
1196         }
1197 
1198         /*
1199          * For backward compatibility, read the OpenGrok-produced document
1200          * using the system default charset.
1201          */
1202         try (InputStream iss = new BufferedInputStream(new FileInputStream(file));
1203             Reader in = compressed ? new InputStreamReader(new GZIPInputStream(iss)) : new InputStreamReader(iss)) {
1204                 dumpXref(out, in, contextPath);
1205                 return true;
1206         } catch (IOException e) {
1207             LOGGER.log(Level.WARNING, "An error occurred while piping file " + file, e);
1208         }
1209 
1210         return false;
1211     }
1212 
1213     /**
1214      * Silently dump an xref file to the given destination. All
1215      * {@link IOException}s get caught and logged, but not re-thrown.
1216      * @param out dump destination
1217      * @param in source to read
1218      * @param contextPath an optional override of "/source/" as the context path
1219      * @throws IOException as defined by the given reader/writer
1220      * @throws NullPointerException if a parameter is {@code null}.
1221      */
dumpXref(Writer out, Reader in, String contextPath)1222     public static void dumpXref(Writer out, Reader in, String contextPath)
1223             throws IOException {
1224         if (in == null || out == null) {
1225             return;
1226         }
1227         XrefSourceTransformer xform = new XrefSourceTransformer(in);
1228         xform.setWriter(out);
1229         xform.setContextPath(contextPath);
1230         while (xform.yylex()) {
1231             // Nothing else to do.
1232         }
1233     }
1234 
1235     /**
1236      * Print a row in an HTML table.
1237      *
1238      * @param out destination for the HTML output
1239      * @param cells the values to print in the cells of the row
1240      * @throws IOException if an error happens while writing to {@code out}
1241      */
printTableRow(Appendable out, Object... cells)1242     private static void printTableRow(Appendable out, Object... cells)
1243             throws IOException {
1244         out.append("<tr>");
1245         StringBuilder buf = new StringBuilder(256);
1246         for (Object cell : cells) {
1247             out.append("<td>");
1248             String str = (cell == null) ? "null" : cell.toString();
1249             htmlize(str, buf);
1250             out.append(str);
1251             buf.setLength(0);
1252             out.append("</td>");
1253         }
1254         out.append("</tr>");
1255     }
1256 
1257     /**
1258      * Print an unordered list (HTML).
1259      *
1260      * @param out destination for the HTML output
1261      * @param items the list items
1262      * @throws IOException if an error happens while writing to {@code out}
1263      */
printUnorderedList(Appendable out, Collection<String> items)1264     private static void printUnorderedList(Appendable out,
1265             Collection<String> items) throws IOException {
1266         out.append("<ul>");
1267         StringBuilder buf = new StringBuilder(256);
1268         for (String item : items) {
1269             out.append("<li>");
1270             htmlize(item, buf);
1271             out.append(buf);
1272             buf.setLength(0);
1273             out.append("</li>");
1274         }
1275         out.append("</ul>");
1276     }
1277 
1278     /**
1279      * Create a string literal for use in JavaScript functions.
1280      *
1281      * @param str the string to be represented by the literal
1282      * @return a JavaScript string literal
1283      */
jsStringLiteral(String str)1284     public static String jsStringLiteral(String str) {
1285         StringBuilder sb = new StringBuilder();
1286         sb.append('"');
1287         for (int i = 0; i < str.length(); i++) {
1288             char c = str.charAt(i);
1289             switch (c) {
1290                 case '"':
1291                     sb.append("\\\"");
1292                     break;
1293                 case '\\':
1294                     sb.append("\\\\");
1295                     break;
1296                 case '\n':
1297                     sb.append("\\n");
1298                     break;
1299                 case '\r':
1300                     sb.append("\\r");
1301                     break;
1302                 default:
1303                     sb.append(c);
1304             }
1305         }
1306         sb.append('"');
1307         return sb.toString();
1308     }
1309 
1310     /**
1311      * Make a path relative by stripping off a prefix. If the path does not have
1312      * the given prefix, return the full path unchanged.
1313      *
1314      * @param prefix the prefix to strip off
1315      * @param fullPath the path from which to remove the prefix
1316      * @return a path relative to {@code prefix} if {@code prefix} is a parent
1317      * directory of {@code fullPath}; otherwise, {@code fullPath}
1318      */
stripPathPrefix(String prefix, String fullPath)1319     public static String stripPathPrefix(String prefix, String fullPath) {
1320         // Find the length of the prefix to strip off. The prefix should
1321         // represent a directory, so it could end with a slash. In case it
1322         // doesn't end with a slash, increase the length by one so that we
1323         // strip off the leading slash from the relative path.
1324         int prefixLength = prefix.length();
1325         if (!prefix.endsWith("/")) {
1326             prefixLength++;
1327         }
1328 
1329         // If the full path starts with the prefix, strip off the prefix.
1330         if (fullPath.length() > prefixLength && fullPath.startsWith(prefix)
1331                 && fullPath.charAt(prefixLength - 1) == '/') {
1332             return fullPath.substring(prefixLength);
1333         }
1334 
1335         // Otherwise, return the full path.
1336         return fullPath;
1337     }
1338 
1339     /**
1340      * Creates a HTML slider for pagination. This has the same effect as
1341      * invoking <code>createSlider(offset, limit, size, null)</code>.
1342      *
1343      * @param offset start of the current page
1344      * @param limit max number of items per page
1345      * @param size number of total hits to paginate
1346      * @return string containing slider html
1347      */
createSlider(int offset, int limit, int size)1348     public static String createSlider(int offset, int limit, int size) {
1349         return createSlider(offset, limit, size, null);
1350     }
1351 
1352     /**
1353      * Creates a HTML slider for pagination.
1354      *
1355      * @param offset start of the current page
1356      * @param limit max number of items per page
1357      * @param size number of total hits to paginate
1358      * @param request request containing URL parameters which should be appended
1359      * to the page URL
1360      * @return string containing slider html
1361      */
createSlider(int offset, int limit, long size, HttpServletRequest request)1362     public static String createSlider(int offset, int limit, long size, HttpServletRequest request) {
1363         String slider = "";
1364         if (limit < size) {
1365             final StringBuilder buf = new StringBuilder(4096);
1366             int lastPage = (int) Math.ceil((double) size / limit);
1367             // startingResult is the number of a first result on the current page
1368             int startingResult = offset - limit * (offset / limit % 10 + 1);
1369             int myFirstPage = startingResult < 0 ? 1 : startingResult / limit + 1;
1370             int myLastPage = Math.min(lastPage, myFirstPage + 10 + (myFirstPage == 1 ? 0 : 1));
1371 
1372             // function taking the page number and appending the desired content into the final buffer
1373             Function<Integer, Void> generatePageLink = page -> {
1374                 int myOffset = Math.max(0, (page - 1) * limit);
1375                 if (myOffset <= offset && offset < myOffset + limit) {
1376                     // do not generate anchor for current page
1377                     buf.append("<span class=\"sel\">").append(page).append("</span>");
1378                 } else {
1379                     buf.append("<a class=\"more\" href=\"?");
1380                     // append request parameters
1381                     if (request != null && request.getQueryString() != null) {
1382                         String query = request.getQueryString();
1383                         query = query.replaceFirst(RE_Q_E_A_A_COUNT_EQ_VAL, "");
1384                         query = query.replaceFirst(RE_Q_E_A_A_START_EQ_VAL, "");
1385                         query = query.replaceFirst(RE_A_ANCHOR_Q_E_A_A, "");
1386                         if (!query.isEmpty()) {
1387                             buf.append(query);
1388                             buf.append("&amp;");
1389                         }
1390                     }
1391                     buf.append(QueryParameters.COUNT_PARAM_EQ).append(limit);
1392                     if (myOffset != 0) {
1393                         buf.append("&amp;").append(QueryParameters.START_PARAM_EQ).
1394                                 append(myOffset);
1395                     }
1396                     buf.append("\">");
1397                     // add << or >> if this link would lead to another section
1398                     if (page == myFirstPage && page != 1) {
1399                         buf.append("&lt;&lt");
1400                     } else if (page == myLastPage && myOffset + limit < size) {
1401                         buf.append("&gt;&gt;");
1402                     } else {
1403                         buf.append(page);
1404                     }
1405                     buf.append("</a>");
1406                 }
1407                 return null;
1408             };
1409 
1410             // slider composition
1411             if (myFirstPage != 1) {
1412                 generatePageLink.apply(1);
1413                 buf.append("<span>...</span>");
1414             }
1415             for (int page = myFirstPage; page <= myLastPage; page++) {
1416                 generatePageLink.apply(page);
1417             }
1418             if (myLastPage != lastPage) {
1419                 buf.append("<span>...</span>");
1420                 generatePageLink.apply(lastPage);
1421             }
1422             return buf.toString();
1423         }
1424         return slider;
1425     }
1426 
1427     /**
1428      * Check if the string is a HTTP URL.
1429      *
1430      * @param string the string to check
1431      * @return true if it is http URL, false otherwise
1432      */
isHttpUri(String string)1433     public static boolean isHttpUri(String string) {
1434         URL url;
1435         try {
1436             url = new URL(string);
1437         } catch (MalformedURLException ex) {
1438             return false;
1439         }
1440         return url.getProtocol().equals("http") || url.getProtocol().equals("https");
1441     }
1442 
1443     protected static final String REDACTED_USER_INFO = "redacted_by_OpenGrok";
1444 
1445     /**
1446      * If given path is a URL, return the string representation with the user-info part filtered out.
1447      * @param path path to object
1448      * @return either the original string or string representation of URL with the user-info part removed
1449      */
redactUrl(String path)1450     public static String redactUrl(String path) {
1451         URL url;
1452         try {
1453             url = new URL(path);
1454         } catch (MalformedURLException e) {
1455             // not an URL
1456             return path;
1457         }
1458         if (url.getUserInfo() != null) {
1459             return url.toString().replace(url.getUserInfo(),
1460                     REDACTED_USER_INFO);
1461         } else {
1462             return path;
1463         }
1464     }
1465 
1466     /**
1467      * Build a HTML link to the given HTTP URL. If the URL is not an http URL
1468      * then it is returned as it was received. This has the same effect as
1469      * invoking <code>linkify(url, true)</code>.
1470      *
1471      * @param url the text to be linkified
1472      * @return the linkified string
1473      *
1474      * @see #linkify(java.lang.String, boolean)
1475      */
linkify(String url)1476     public static String linkify(String url) {
1477         return linkify(url, true);
1478     }
1479 
1480     /**
1481      * Build a html link to the given http URL. If the URL is not an http URL
1482      * then it is returned as it was received.
1483      *
1484      * @param url the HTTP URL
1485      * @param newTab if the link should open in a new tab
1486      * @return HTML code containing the link &lt;a&gt;...&lt;/a&gt;
1487      */
linkify(String url, boolean newTab)1488     public static String linkify(String url, boolean newTab) {
1489         if (isHttpUri(url)) {
1490             try {
1491                 Map<String, String> attrs = new TreeMap<>();
1492                 attrs.put("href", url);
1493                 attrs.put("title", String.format("Link to %s", Util.encode(url)));
1494                 if (newTab) {
1495                     attrs.put("target", "_blank");
1496                     attrs.put("rel", "noreferrer");
1497                 }
1498                 return buildLink(url, attrs);
1499             } catch (URISyntaxException | MalformedURLException ex) {
1500                 return url;
1501             }
1502         }
1503         return url;
1504     }
1505 
1506     /**
1507      * Build an anchor with given name and a pack of attributes. Automatically
1508      * escapes href attributes and automatically escapes the name into HTML
1509      * entities.
1510      *
1511      * @param name displayed name of the anchor
1512      * @param attrs map of attributes for the html element
1513      * @return string containing the result
1514      *
1515      * @throws URISyntaxException URI syntax
1516      * @throws MalformedURLException malformed URL
1517      */
buildLink(String name, Map<String, String> attrs)1518     public static String buildLink(String name, Map<String, String> attrs)
1519             throws URISyntaxException, MalformedURLException {
1520         StringBuilder buffer = new StringBuilder();
1521         buffer.append("<a");
1522         for (Entry<String, String> attr : attrs.entrySet()) {
1523             buffer.append(" ");
1524             buffer.append(attr.getKey());
1525             buffer.append("=\"");
1526             String value = attr.getValue();
1527             if (attr.getKey().equals("href")) {
1528                 value = Util.encodeURL(value);
1529             }
1530             buffer.append(value);
1531             buffer.append("\"");
1532         }
1533         buffer.append(">");
1534         buffer.append(Util.htmlize(name));
1535         buffer.append("</a>");
1536         return buffer.toString();
1537     }
1538 
1539     /**
1540      * Build an anchor with given name and a pack of attributes. Automatically
1541      * escapes href attributes and automatically escapes the name into HTML
1542      * entities.
1543      *
1544      * @param name displayed name of the anchor
1545      * @param url anchor's URL
1546      * @return string containing the result
1547      *
1548      * @throws URISyntaxException URI syntax
1549      * @throws MalformedURLException bad URL
1550      */
buildLink(String name, String url)1551     public static String buildLink(String name, String url)
1552             throws URISyntaxException, MalformedURLException {
1553         Map<String, String> attrs = new TreeMap<>();
1554         attrs.put("href", url);
1555         return buildLink(name, attrs);
1556     }
1557 
1558     /**
1559      * Build an anchor with given name and a pack of attributes. Automatically
1560      * escapes href attributes and automatically escapes the name into HTML
1561      * entities.
1562      *
1563      * @param name displayed name of the anchor
1564      * @param url anchor's URL
1565      * @param newTab a flag if the link should be opened in a new tab
1566      * @return string containing the result
1567      *
1568      * @throws URISyntaxException URI syntax
1569      * @throws MalformedURLException bad URL
1570      */
buildLink(String name, String url, boolean newTab)1571     public static String buildLink(String name, String url, boolean newTab)
1572             throws URISyntaxException, MalformedURLException {
1573         Map<String, String> attrs = new TreeMap<>();
1574         attrs.put("href", url);
1575         if (newTab) {
1576             attrs.put("target", "_blank");
1577             attrs.put("rel", "noreferrer");
1578         }
1579         return buildLink(name, attrs);
1580     }
1581 
1582     /**
1583      * Replace all occurrences of pattern in the incoming text with the link
1584      * named name pointing to an URL. It is possible to use the regexp pattern
1585      * groups in name and URL when they are specified in the pattern.
1586      *
1587      * @param text text to replace all patterns
1588      * @param pattern the pattern to match
1589      * @param name link display name
1590      * @param url link URL
1591      * @return the text with replaced links
1592      */
linkifyPattern(String text, Pattern pattern, String name, String url)1593     public static String linkifyPattern(String text, Pattern pattern, String name, String url) {
1594         try {
1595             String buildLink = buildLink(name, url, true);
1596             return pattern.matcher(text).replaceAll(buildLink);
1597         } catch (URISyntaxException | MalformedURLException ex) {
1598             LOGGER.log(Level.WARNING, "The given URL ''{0}'' is not valid", url);
1599             return text;
1600         }
1601     }
1602 
1603     /**
1604      * Try to complete the given URL part into full URL with server name, port,
1605      * scheme, ...
1606      * <dl>
1607      * <dt>for request http://localhost:8080/source/xref/xxx and part
1608      * /cgi-bin/user=</dt>
1609      * <dd>http://localhost:8080/cgi-bin/user=</dd>
1610      * <dt>for request http://localhost:8080/source/xref/xxx and part
1611      * cgi-bin/user=</dt>
1612      * <dd>http://localhost:8080/source/xref/xxx/cgi-bin/user=</dd>
1613      * <dt>for request http://localhost:8080/source/xref/xxx and part
1614      * http://users.com/user=</dt>
1615      * <dd>http://users.com/user=</dd>
1616      * </dl>
1617      *
1618      * @param url the given URL part, may be already full URL
1619      * @param req the request containing the information about the server
1620      * @return the converted URL or the input parameter if there was an error
1621      */
completeUrl(String url, HttpServletRequest req)1622     public static String completeUrl(String url, HttpServletRequest req) {
1623         try {
1624             if (!isHttpUri(url)) {
1625                 if (url.startsWith("/")) {
1626                     return new URI(req.getScheme(), null, req.getServerName(), req.getServerPort(), url, null, null).toString();
1627                 }
1628                 StringBuffer prepUrl = req.getRequestURL();
1629                 if (!url.isEmpty()) {
1630                     prepUrl.append('/').append(url);
1631                 }
1632                 return new URI(prepUrl.toString()).toString();
1633             }
1634             return url;
1635         } catch (URISyntaxException ex) {
1636             LOGGER.log(Level.INFO,
1637                     String.format("Unable to convert given URL part '%s' to complete URL", url),
1638                     ex);
1639             return url;
1640         }
1641     }
1642 
1643     /**
1644      * Parses the specified URL and returns its query params.
1645      * @param url URL to retrieve the query params from
1646      * @return query params of {@code url}
1647      */
getQueryParams(final URL url)1648     public static Map<String, List<String>> getQueryParams(final URL url) {
1649         if (url == null) {
1650             throw new IllegalArgumentException("Cannot get query params from the null url");
1651         }
1652         Map<String, List<String>> returnValue = new HashMap<>();
1653 
1654         if (url.getQuery() == null) {
1655             return returnValue;
1656         }
1657 
1658         String[] pairs = url.getQuery().split("&");
1659 
1660         for (String pair : pairs) {
1661             if (pair.isEmpty()) {
1662                 continue;
1663             }
1664 
1665             int idx = pair.indexOf('=');
1666             if (idx == -1) {
1667                 returnValue.computeIfAbsent(pair, k -> new LinkedList<>());
1668                 continue;
1669             }
1670 
1671             String key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8);
1672             String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8);
1673 
1674             List<String> paramValues = returnValue.computeIfAbsent(key, k -> new LinkedList<>());
1675             paramValues.add(value);
1676         }
1677         return returnValue;
1678     }
1679 
1680 }
1681