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 = "\\?|&|&"; 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 <br> 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 <br> 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("'"); 280 break; 281 case '"': 282 dest.append("""); 283 break; 284 case '&': 285 dest.append("&"); 286 break; 287 case '>': 288 dest.append(">"); 289 break; 290 case '<': 291 dest.append("<"); 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 <br> 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 > 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>\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 <a title=""> 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(" "); 661 } else if (c == '\t') { 662 dest.append(" "); 663 } else if (c == '\n') { 664 // <br/> 665 dest.append("<br/>"); 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("&"); 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("<br/>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("&"); 781 out.write(QueryParameters.REFS_SEARCH_PARAM_EQ); 782 out.write("&"); 783 out.write(QueryParameters.PATH_SEARCH_PARAM_EQ); 784 out.write(project); 785 out.write("&"); 786 out.write(QueryParameters.HIST_SEARCH_PARAM_EQ); 787 out.write("""); 788 out.write(uriEncode(r)); 789 out.write(""&"); 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 '&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("&").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("""); 1011 break; 1012 case '&': 1013 sb.append("&"); 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("&"); 1389 } 1390 } 1391 buf.append(QueryParameters.COUNT_PARAM_EQ).append(limit); 1392 if (myOffset != 0) { 1393 buf.append("&").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("<<"); 1400 } else if (page == myLastPage && myOffset + limit < size) { 1401 buf.append(">>"); 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 <a>...</a> 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