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) 2011, 2022, 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 */ 25 package org.opengrok.web; 26 27 import static org.opengrok.indexer.index.Indexer.PATH_SEPARATOR; 28 import static org.opengrok.indexer.index.Indexer.PATH_SEPARATOR_STRING; 29 30 import java.io.BufferedReader; 31 import java.io.File; 32 import java.io.FileNotFoundException; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.net.URI; 36 import java.net.URISyntaxException; 37 import java.net.URLDecoder; 38 import java.nio.charset.StandardCharsets; 39 import java.nio.file.Paths; 40 import java.security.InvalidParameterException; 41 import java.text.ParseException; 42 import java.util.ArrayList; 43 import java.util.Arrays; 44 import java.util.Collections; 45 import java.util.Comparator; 46 import java.util.Date; 47 import java.util.EnumSet; 48 import java.util.List; 49 import java.util.Objects; 50 import java.util.Set; 51 import java.util.SortedSet; 52 import java.util.TreeSet; 53 import java.util.concurrent.ExecutorService; 54 import java.util.concurrent.Future; 55 import java.util.logging.Level; 56 import java.util.logging.Logger; 57 import java.util.regex.Pattern; 58 import java.util.stream.Collectors; 59 60 import jakarta.servlet.ServletRequest; 61 import jakarta.servlet.http.Cookie; 62 import jakarta.servlet.http.HttpServletRequest; 63 import jakarta.servlet.http.HttpServletResponse; 64 import jakarta.ws.rs.core.HttpHeaders; 65 import org.apache.lucene.document.DateTools; 66 import org.apache.lucene.document.Document; 67 import org.jetbrains.annotations.Nullable; 68 import org.jetbrains.annotations.VisibleForTesting; 69 import org.opengrok.indexer.Info; 70 import org.opengrok.indexer.analysis.AbstractAnalyzer; 71 import org.opengrok.indexer.analysis.AnalyzerGuru; 72 import org.opengrok.indexer.analysis.ExpandTabsReader; 73 import org.opengrok.indexer.analysis.StreamSource; 74 import org.opengrok.indexer.authorization.AuthorizationFramework; 75 import org.opengrok.indexer.configuration.Group; 76 import org.opengrok.indexer.configuration.IgnoredNames; 77 import org.opengrok.indexer.configuration.Project; 78 import org.opengrok.indexer.configuration.RuntimeEnvironment; 79 import org.opengrok.indexer.history.Annotation; 80 import org.opengrok.indexer.history.HistoryEntry; 81 import org.opengrok.indexer.history.HistoryException; 82 import org.opengrok.indexer.history.HistoryGuru; 83 import org.opengrok.indexer.index.IndexDatabase; 84 import org.opengrok.indexer.logger.LoggerFactory; 85 import org.opengrok.indexer.search.QueryBuilder; 86 import org.opengrok.indexer.util.IOUtils; 87 import org.opengrok.indexer.util.LineBreaker; 88 import org.opengrok.indexer.util.TandemPath; 89 import org.opengrok.indexer.web.EftarFileReader; 90 import org.opengrok.indexer.web.Laundromat; 91 import org.opengrok.indexer.web.Prefix; 92 import org.opengrok.indexer.web.QueryParameters; 93 import org.opengrok.indexer.web.SearchHelper; 94 import org.opengrok.indexer.web.SortOrder; 95 import org.opengrok.indexer.web.Util; 96 import org.opengrok.indexer.web.messages.MessagesContainer.AcceptedMessage; 97 import org.suigeneris.jrcs.diff.Diff; 98 import org.suigeneris.jrcs.diff.DifferentiationFailedException; 99 100 /** 101 * A simple container to lazy initialize common vars wrt. a single request. It 102 * MUST NOT be shared between several requests and 103 * {@link #cleanup(ServletRequest)} should be called before the page context 104 * gets destroyed (e.g.when leaving the {@code service} method). 105 * <p> 106 * Purpose is to decouple implementation details from web design, so that the 107 * JSP developer does not need to know every implementation detail and normally 108 * has to deal with this class/wrapper, only (so some people may like to call 109 * this class a bean with request scope ;-)). Furthermore, it helps to keep the 110 * pages (how content gets generated) consistent and to document the request 111 * parameters used. 112 * <p> 113 * General contract for this class (i.e. if not explicitly documented): no 114 * method of this class changes neither the request nor the response. 115 * 116 * @author Jens Elkner 117 */ 118 public final class PageConfig { 119 120 private static final Logger LOGGER = LoggerFactory.getLogger(PageConfig.class); 121 122 // cookie name 123 public static final String OPEN_GROK_PROJECT = "OpenGrokProject"; 124 125 public static final String DUMMY_REVISION = "unknown"; 126 127 // query parameters 128 static final String PROJECT_PARAM_NAME = "project"; 129 static final String GROUP_PARAM_NAME = "group"; 130 private static final String DEBUG_PARAM_NAME = "debug"; 131 132 // TODO if still used, get it from the app context 133 134 private final AuthorizationFramework authFramework; 135 private RuntimeEnvironment env; 136 private IgnoredNames ignoredNames; 137 private String path; 138 private File resourceFile; 139 private String resourcePath; 140 private EftarFileReader eftarReader; 141 private String sourceRootPath; 142 private Boolean isDir; 143 private String uriEncodedPath; 144 private Prefix prefix; 145 private String pageTitle; 146 private String dtag; 147 private String rev; 148 private String fragmentIdentifier; // Also settable via match offset translation 149 private Boolean hasAnnotation; 150 private Boolean annotate; 151 private Annotation annotation; 152 private Boolean hasHistory; 153 private static final EnumSet<AbstractAnalyzer.Genre> txtGenres 154 = EnumSet.of(AbstractAnalyzer.Genre.DATA, AbstractAnalyzer.Genre.PLAIN, AbstractAnalyzer.Genre.HTML); 155 private SortedSet<String> requestedProjects; 156 private String requestedProjectsString; 157 private List<String> dirFileList; 158 private QueryBuilder queryBuilder; 159 private File dataRoot; 160 private StringBuilder headLines; 161 /** 162 * Page java scripts. 163 */ 164 private final Scripts scripts = new Scripts(); 165 166 private static final String ATTR_NAME = PageConfig.class.getCanonicalName(); 167 private HttpServletRequest req; 168 169 private final ExecutorService executor; 170 171 /** 172 * Sets current request's attribute. 173 * 174 * @param attr attribute 175 * @param val value 176 */ setRequestAttribute(String attr, Object val)177 public void setRequestAttribute(String attr, Object val) { 178 this.req.setAttribute(attr, val); 179 } 180 181 /** 182 * Gets current request's attribute. 183 * @param attr attribute 184 * @return Object attribute value or null if attribute does not exist 185 */ getRequestAttribute(String attr)186 public Object getRequestAttribute(String attr) { 187 return this.req.getAttribute(attr); 188 } 189 190 /** 191 * Removes an attribute from the current request. 192 * @param string the attribute 193 */ removeAttribute(String string)194 public void removeAttribute(String string) { 195 req.removeAttribute(string); 196 } 197 198 /** 199 * Add the given data to the <head> section of the html page to 200 * generate. 201 * 202 * @param data data to add. It is copied as is, so remember to escape 203 * special characters ... 204 */ addHeaderData(String data)205 public void addHeaderData(String data) { 206 if (data == null || data.length() == 0) { 207 return; 208 } 209 if (headLines == null) { 210 headLines = new StringBuilder(); 211 } 212 headLines.append(data); 213 } 214 215 /** 216 * Get addition data, which should be added as is to the <head> 217 * section of the html page. 218 * 219 * @return an empty string if nothing to add, the data otherwise. 220 */ getHeaderData()221 public String getHeaderData() { 222 return headLines == null ? "" : headLines.toString(); 223 } 224 225 /** 226 * Extract file path and revision strings from the URL. 227 * @param data DiffData object 228 * @param context context path 229 * @param filepath file path array (output parameter) 230 * @return true if the extraction was successful, false otherwise 231 * (in which case {@link DiffData#errorMsg} will be set) 232 */ getFileRevision(DiffData data, String context, String[] filepath)233 private boolean getFileRevision(DiffData data, String context, String[] filepath) { 234 /* 235 * Basically the request URI looks like this: 236 * http://$site/$webapp/diff/$resourceFile?r1=$fileA@$revA&r2=$fileB@$revB 237 * The code below extracts file path and revision from the URI. 238 */ 239 for (int i = 1; i <= 2; i++) { 240 String p = req.getParameter(QueryParameters.REVISION_PARAM + i); 241 if (p != null) { 242 int j = p.lastIndexOf("@"); 243 if (j != -1) { 244 filepath[i - 1] = p.substring(0, j); 245 data.rev[i - 1] = p.substring(j + 1); 246 } 247 } 248 } 249 250 if (data.rev[0] == null || data.rev[1] == null 251 || data.rev[0].length() == 0 || data.rev[1].length() == 0 252 || data.rev[0].equals(data.rev[1])) { 253 data.errorMsg = "Please pick two revisions to compare the changed " 254 + "from the <a href=\"" + context + Prefix.HIST_L 255 + getUriEncodedPath() + "\">history</a>"; 256 return false; 257 } 258 259 return true; 260 } 261 262 /** 263 * Get all data required to create a diff view w.r.t. to this request in one go. 264 * 265 * @return an instance with just enough information to render a sufficient view. 266 * If not all required parameters were given either they are supplemented 267 * with reasonable defaults if possible, otherwise the related field(s) are {@code null}. 268 * {@link DiffData#errorMsg} 269 * {@code != null} indicates that an error occurred and one should not try to render a view. 270 */ getDiffData()271 public DiffData getDiffData() { 272 DiffData data = new DiffData(getPath().substring(0, getPath().lastIndexOf(PATH_SEPARATOR)), 273 Util.htmlize(getResourceFile().getName())); 274 275 String srcRoot = getSourceRootPath(); 276 String context = req.getContextPath(); 277 String[] filepath = new String[2]; 278 279 if (!getFileRevision(data, context, filepath)) { 280 return data; 281 } 282 283 data.genre = AnalyzerGuru.getGenre(getResourceFile().getName()); 284 if (data.genre == null || txtGenres.contains(data.genre)) { 285 InputStream[] in = new InputStream[2]; 286 try { 287 // Get input stream for both older and newer file. 288 Future<?>[] future = new Future<?>[2]; 289 for (int i = 0; i < 2; i++) { 290 File f = new File(srcRoot + filepath[i]); 291 final String revision = data.rev[i]; 292 future[i] = executor.submit(() -> HistoryGuru.getInstance(). 293 getRevision(f.getParent(), f.getName(), revision)); 294 } 295 296 for (int i = 0; i < 2; i++) { 297 // The Executor used by given repository will enforce the timeout. 298 in[i] = (InputStream) future[i].get(); 299 if (in[i] == null) { 300 data.errorMsg = "Unable to get revision " 301 + Util.htmlize(data.rev[i]) + " for file: " 302 + Util.htmlize(getPath()); 303 return data; 304 } 305 } 306 307 /* 308 * If the genre of the older revision cannot be determined, 309 * (this can happen if the file was empty), try with newer 310 * version. 311 */ 312 for (int i = 0; i < 2 && data.genre == null; i++) { 313 try { 314 data.genre = AnalyzerGuru.getGenre(in[i]); 315 } catch (IOException e) { 316 data.errorMsg = "Unable to determine the file type: " 317 + Util.htmlize(e.getMessage()); 318 } 319 } 320 321 if (data.genre != AbstractAnalyzer.Genre.PLAIN && data.genre != AbstractAnalyzer.Genre.HTML) { 322 return data; 323 } 324 325 ArrayList<String> lines = new ArrayList<>(); 326 Project p = getProject(); 327 for (int i = 0; i < 2; i++) { 328 // All files under source root are read with UTF-8 as a default. 329 try (BufferedReader br = new BufferedReader( 330 ExpandTabsReader.wrap(IOUtils.createBOMStrippedReader( 331 in[i], StandardCharsets.UTF_8.name()), p))) { 332 String line; 333 while ((line = br.readLine()) != null) { 334 lines.add(line); 335 } 336 data.file[i] = lines.toArray(new String[0]); 337 lines.clear(); 338 } 339 in[i] = null; 340 } 341 } catch (Exception e) { 342 data.errorMsg = "Error reading revisions: " 343 + Util.htmlize(e.getMessage()); 344 } finally { 345 for (int i = 0; i < 2; i++) { 346 IOUtils.close(in[i]); 347 } 348 } 349 if (data.errorMsg != null) { 350 return data; 351 } 352 try { 353 data.revision = Diff.diff(data.file[0], data.file[1]); 354 } catch (DifferentiationFailedException e) { 355 data.errorMsg = "Unable to get diffs: " + Util.htmlize(e.getMessage()); 356 } 357 for (int i = 0; i < 2; i++) { 358 try { 359 URI u = new URI(null, null, null, 360 filepath[i] + "@" + data.rev[i], null); 361 data.param[i] = u.getRawQuery(); 362 } catch (URISyntaxException e) { 363 LOGGER.log(Level.WARNING, "Failed to create URI: ", e); 364 } 365 } 366 data.full = fullDiff(); 367 data.type = getDiffType(); 368 } 369 return data; 370 } 371 372 /** 373 * Get the diff display type to use wrt. the request parameter 374 * {@code format}. 375 * 376 * @return {@link DiffType#SIDEBYSIDE} if the request contains no such 377 * parameter or one with an unknown value, the recognized diff type 378 * otherwise. 379 * @see DiffType#get(String) 380 * @see DiffType#getAbbrev() 381 * @see DiffType#toString() 382 */ getDiffType()383 public DiffType getDiffType() { 384 DiffType d = DiffType.get(req.getParameter(QueryParameters.FORMAT_PARAM)); 385 return d == null ? DiffType.SIDEBYSIDE : d; 386 } 387 388 /** 389 * Check, whether a full diff should be displayed. 390 * 391 * @return {@code true} if a request parameter {@code full} with the literal 392 * value {@code 1} was found. 393 */ fullDiff()394 public boolean fullDiff() { 395 String val = req.getParameter(QueryParameters.DIFF_LEVEL_PARAM); 396 return val != null && val.equals("1"); 397 } 398 399 /** 400 * Check, whether the request contains minimal information required to 401 * produce a valid page. If this method returns an empty string, the 402 * referred file or directory actually exists below the source root 403 * directory and is readable. 404 * 405 * @return {@code null} if the referred src file, directory or history is 406 * not available, an empty String if further processing is ok and a 407 * non-empty string which contains the URI encoded redirect path if the 408 * request should be redirected. 409 * @see #resourceNotAvailable() 410 * @see #getDirectoryRedirect() 411 */ canProcess()412 public String canProcess() { 413 if (resourceNotAvailable()) { 414 return null; 415 } 416 String redir = getDirectoryRedirect(); 417 if (redir == null && getPrefix() == Prefix.HIST_L && !hasHistory()) { 418 return null; 419 } 420 // jel: outfactored from list.jsp - seems to be bogus 421 if (isDir()) { 422 if (getPrefix() == Prefix.XREF_P) { 423 if (getResourceFileList().isEmpty() 424 && !getRequestedRevision().isEmpty() && !hasHistory()) { 425 return null; 426 } 427 } else if ((getPrefix() == Prefix.RAW_P) 428 || (getPrefix() == Prefix.DOWNLOAD_P)) { 429 return null; 430 } 431 } 432 return redir == null ? "" : redir; 433 } 434 435 /** 436 * Get a list of filenames in the requested path. 437 * 438 * @return an empty list, if the resource does not exist, is not a directory 439 * or an error occurred when reading it, otherwise a list of filenames in 440 * that directory, sorted alphabetically 441 * 442 * <p> 443 * For the root directory (/xref/) an authorization is performed for each 444 * project in case that projects are used. 445 * 446 * @see #getResourceFile() 447 * @see #isDir() 448 */ getResourceFileList()449 public List<String> getResourceFileList() { 450 if (dirFileList == null) { 451 File[] files = null; 452 if (isDir() && getResourcePath().length() > 1) { 453 files = getResourceFile().listFiles(); 454 } 455 456 if (files == null) { 457 dirFileList = Collections.emptyList(); 458 } else { 459 List<String> listOfFiles = getSortedFiles(files); 460 461 if (env.hasProjects() && getPath().isEmpty()) { 462 /* 463 * This denotes the source root directory, we need to filter 464 * projects which aren't allowed by the authorization 465 * because otherwise the main xref page expose the names of 466 * all projects in OpenGrok even those which aren't allowed 467 * for the particular user. E.g. remove all which aren't 468 * among the filtered set of projects. 469 * 470 * The authorization check is made in 471 * {@link ProjectHelper#getAllProjects()} as a part of all 472 * projects filtering. 473 */ 474 List<String> modifiableListOfFiles = new ArrayList<>(listOfFiles); 475 modifiableListOfFiles.removeIf(t -> getProjectHelper().getAllProjects().stream() 476 .noneMatch(p -> p.getName().equalsIgnoreCase(t))); 477 return dirFileList = Collections.unmodifiableList(modifiableListOfFiles); 478 } 479 480 dirFileList = Collections.unmodifiableList(listOfFiles); 481 } 482 } 483 return dirFileList; 484 } 485 getFileComparator()486 private Comparator<File> getFileComparator() { 487 if (getEnv().getListDirsFirst()) { 488 return (f1, f2) -> { 489 if (f1.isDirectory() && !f2.isDirectory()) { 490 return -1; 491 } else if (!f1.isDirectory() && f2.isDirectory()) { 492 return 1; 493 } else { 494 return f1.getName().compareTo(f2.getName()); 495 } 496 }; 497 } else { 498 return Comparator.comparing(File::getName); 499 } 500 } 501 502 @VisibleForTesting 503 List<String> getSortedFiles(File[] files) { 504 return Arrays.stream(files).sorted(getFileComparator()).map(File::getName).collect(Collectors.toList()); 505 } 506 507 /** 508 * Get the time of last modification of the related file or directory. 509 * 510 * @return the last modification time of the related file or directory. 511 * @see File#lastModified() 512 */ 513 public long getLastModified() { 514 return getResourceFile().lastModified(); 515 } 516 517 /** 518 * Get all RSS related directories from the request using its {@code also} 519 * parameter. 520 * 521 * @return an empty string if the requested resource is not a directory, a 522 * space (' ') separated list of unchecked directory names otherwise. 523 */ 524 public String getHistoryDirs() { 525 if (!isDir()) { 526 return ""; 527 } 528 String[] val = req.getParameterValues("also"); 529 if (val == null || val.length == 0) { 530 return getPath(); 531 } 532 StringBuilder paths = new StringBuilder(getPath()); 533 for (String val1 : val) { 534 paths.append(' ').append(val1); 535 } 536 return paths.toString(); 537 } 538 539 /** 540 * Get the int value of the given request parameter. 541 * 542 * @param name name of the parameter to lookup. 543 * @param defaultValue value to return, if the parameter is not set, is not 544 * a number, or is < 0. 545 * @return the parsed int value on success, the given default value 546 * otherwise. 547 */ 548 public int getIntParam(String name, int defaultValue) { 549 int ret = defaultValue; 550 String s = req.getParameter(name); 551 if (s != null && s.length() != 0) { 552 try { 553 int x = Integer.parseInt(s, 10); 554 if (x >= 0) { 555 ret = x; 556 } 557 } catch (NumberFormatException e) { 558 LOGGER.log(Level.INFO, String.format("Failed to parse %s integer %s", name, s), e); 559 } 560 } 561 return ret; 562 } 563 564 /** 565 * Get the <b>start</b> index for a search result to return by looking up 566 * the {@code start} request parameter. 567 * 568 * @return 0 if the corresponding start parameter is not set or not a 569 * number, the number found otherwise. 570 */ 571 public int getSearchStart() { 572 return getIntParam(QueryParameters.START_PARAM, 0); 573 } 574 575 /** 576 * Get the number of search results to max. return by looking up the 577 * {@code n} request parameter. 578 * 579 * @return the default number of hits if the corresponding start parameter 580 * is not set or not a number, the number found otherwise. 581 */ 582 public int getSearchMaxItems() { 583 return getIntParam(QueryParameters.COUNT_PARAM, getEnv().getHitsPerPage()); 584 } 585 586 public int getRevisionMessageCollapseThreshold() { 587 return getEnv().getRevisionMessageCollapseThreshold(); 588 } 589 590 public int getCurrentIndexedCollapseThreshold() { 591 return getEnv().getCurrentIndexedCollapseThreshold(); 592 } 593 594 public int getGroupsCollapseThreshold() { 595 return getEnv().getGroupsCollapseThreshold(); 596 } 597 598 /** 599 * Get sort orders from the request parameter {@code sort} and if this list 600 * would be empty from the cookie {@code OpenGrokSorting}. 601 * 602 * @return a possible empty list which contains the sort order values in the 603 * same order supplied by the request parameter or cookie(s). 604 */ 605 public List<SortOrder> getSortOrder() { 606 List<SortOrder> sort = new ArrayList<>(); 607 List<String> vals = getParameterValues(QueryParameters.SORT_PARAM); 608 for (String s : vals) { 609 SortOrder so = SortOrder.get(s); 610 if (so != null) { 611 sort.add(so); 612 } 613 } 614 if (sort.isEmpty()) { 615 vals = getCookieVals("OpenGrokSorting"); 616 for (String s : vals) { 617 SortOrder so = SortOrder.get(s); 618 if (so != null) { 619 sort.add(so); 620 } 621 } 622 } 623 return sort; 624 } 625 626 /** 627 * Get a reference to the {@code QueryBuilder} wrt. to the current request 628 * parameters: <dl> <dt>q</dt> <dd>freetext lookup rules</dd> <dt>defs</dt> 629 * <dd>definitions lookup rules</dd> <dt>path</dt> <dd>path related 630 * rules</dd> <dt>hist</dt> <dd>history related rules</dd> </dl> 631 * 632 * @return a query builder with all relevant fields populated. 633 */ 634 public QueryBuilder getQueryBuilder() { 635 if (queryBuilder == null) { 636 queryBuilder = new QueryBuilder(). 637 setFreetext(Laundromat.launderQuery(req.getParameter(QueryBuilder.FULL))) 638 .setDefs(Laundromat.launderQuery(req.getParameter(QueryBuilder.DEFS))) 639 .setRefs(Laundromat.launderQuery(req.getParameter(QueryBuilder.REFS))) 640 .setPath(Laundromat.launderQuery(req.getParameter(QueryBuilder.PATH))) 641 .setHist(Laundromat.launderQuery(req.getParameter(QueryBuilder.HIST))) 642 .setType(Laundromat.launderQuery(req.getParameter(QueryBuilder.TYPE))); 643 } 644 645 return queryBuilder; 646 } 647 648 /** 649 * Get the <i>Eftar</i> reader for the data directory. If it has been already 650 * opened and not closed, this instance gets returned. One should not close 651 * it once used: {@link #cleanup(ServletRequest)} takes care to close it. 652 * 653 * @return {@code null} if a reader can't be established, the reader 654 * otherwise. 655 */ 656 public EftarFileReader getEftarReader() { 657 if (eftarReader == null || eftarReader.isClosed()) { 658 File f = getEnv().getDtagsEftar(); 659 if (f == null) { 660 eftarReader = null; 661 } else { 662 try { 663 eftarReader = new EftarFileReader(f); 664 } catch (FileNotFoundException e) { 665 LOGGER.log(Level.FINE, "Failed to create EftarFileReader: ", e); 666 } 667 } 668 } 669 return eftarReader; 670 } 671 672 /** 673 * Get the definition tag for the request related file or directory. 674 * 675 * @return an empty string if not found, the tag otherwise. 676 */ 677 public String getDefineTagsIndex() { 678 if (dtag != null) { 679 return dtag; 680 } 681 getEftarReader(); 682 if (eftarReader != null) { 683 try { 684 dtag = eftarReader.get(getPath()); 685 } catch (IOException e) { 686 LOGGER.log(Level.INFO, "Failed to get entry from eftar reader: ", e); 687 } 688 } 689 if (dtag == null) { 690 dtag = ""; 691 } 692 return dtag; 693 } 694 695 /** 696 * Get the revision parameter {@code r} from the request. 697 * 698 * @return revision if found, an empty string otherwise. 699 */ 700 public String getRequestedRevision() { 701 if (rev == null) { 702 String tmp = Laundromat.launderInput( 703 req.getParameter(QueryParameters.REVISION_PARAM)); 704 rev = (tmp != null && tmp.length() > 0) ? tmp : ""; 705 } 706 return rev; 707 } 708 709 /** 710 * Check, whether the request related resource has history information. 711 * 712 * @return {@code true} if history is available. 713 * @see HistoryGuru#hasHistory(File) 714 */ 715 public boolean hasHistory() { 716 if (hasHistory == null) { 717 hasHistory = HistoryGuru.getInstance().hasHistory(getResourceFile()); 718 } 719 return hasHistory; 720 } 721 722 /** 723 * Check, whether annotations are available for the related resource. 724 * 725 * @return {@code true} if annotations are available. 726 */ 727 public boolean hasAnnotations() { 728 if (hasAnnotation == null) { 729 hasAnnotation = !isDir() 730 && HistoryGuru.getInstance().hasHistory(getResourceFile()); 731 } 732 return hasAnnotation; 733 } 734 735 /** 736 * Check, whether the resource to show should be annotated. 737 * 738 * @return {@code true} if annotation is desired and available. 739 */ 740 public boolean annotate() { 741 if (annotate == null) { 742 annotate = hasAnnotations() 743 && Boolean.parseBoolean(req.getParameter(QueryParameters.ANNOTATION_PARAM)); 744 } 745 return annotate; 746 } 747 748 /** 749 * Get the annotation for the requested resource. 750 * 751 * @return {@code null} if not available or annotation was not requested, 752 * the cached annotation otherwise. 753 */ 754 public Annotation getAnnotation() { 755 if (isDir() || getResourcePath().equals("/") || !annotate()) { 756 return null; 757 } 758 if (annotation != null) { 759 return annotation; 760 } 761 getRequestedRevision(); 762 try { 763 annotation = HistoryGuru.getInstance().annotate(resourceFile, rev.isEmpty() ? null : rev); 764 } catch (IOException e) { 765 LOGGER.log(Level.WARNING, "Failed to get annotations: ", e); 766 /* ignore */ 767 } 768 return annotation; 769 } 770 771 /** 772 * Get the {@code path} parameter and display value for "Search only in" option. 773 * 774 * @return always an array of 3 fields, whereby field[0] contains the path 775 * value to use (starts and ends always with a {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}). 776 * Field[1] the contains string to show in the UI. field[2] is set to {@code disabled=""} if the 777 * current path is the "/" directory, otherwise set to an empty string. 778 */ 779 public String[] getSearchOnlyIn() { 780 if (isDir()) { 781 return getPath().length() == 0 782 ? new String[]{"/", "this directory", "disabled=\"\""} 783 : new String[]{getPath(), "this directory", ""}; 784 } 785 String[] res = new String[3]; 786 res[0] = getPath().substring(0, getPath().lastIndexOf(PATH_SEPARATOR) + 1); 787 res[1] = res[0]; 788 res[2] = ""; 789 return res; 790 } 791 792 /** 793 * Get the project {@link #getPath()} refers to. 794 * 795 * @return {@code null} if not available, the project otherwise. 796 */ 797 @Nullable 798 public Project getProject() { 799 return Project.getProject(getResourceFile()); 800 } 801 802 /** 803 * Same as {@link #getRequestedProjects()} but returns the project names as 804 * a coma separated String. 805 * 806 * @return a possible empty String but never {@code null}. 807 */ 808 public String getRequestedProjectsAsString() { 809 if (requestedProjectsString == null) { 810 requestedProjectsString = String.join(",", getRequestedProjects()); 811 } 812 return requestedProjectsString; 813 } 814 815 /** 816 * Get a reference to a set of requested projects via request parameter 817 * {@code project} or cookies or defaults. 818 * <p> 819 * NOTE: This method assumes, that project names do <b>not</b> contain a 820 * comma (','), since this character is used as name separator! 821 * <p> 822 * It is determined as follows: 823 * <ol> 824 * <li>If there is no project in the configuration an empty set is returned. Otherwise:</li> 825 * <li>If there is only one project in the configuration, 826 * this one gets returned (no matter, what the request actually says). Otherwise</li> 827 * <li>If the request parameter {@code ALL_PROJECT_SEARCH} contains a true value, 828 * all projects are added to searching. Otherwise:</li> 829 * <li>If the request parameter {@code PROJECT_PARAM_NAME} contains any available project, 830 * the set with invalid projects removed gets returned. Otherwise:</li> 831 * <li>If the request parameter {@code GROUP_PARAM_NAME} contains any available group, 832 * then all projects from that group will be added to the result set. Otherwise:</li> 833 * <li>If the request has a cookie with the name {@code OPEN_GROK_PROJECT} 834 * and it contains any available project, 835 * the set with invalid projects removed gets returned. Otherwise:</li> 836 * <li>If a default project is set in the configuration, 837 * this project gets returned. Otherwise:</li> 838 * <li>an empty set</li> 839 * </ol> 840 * 841 * @return a possible empty set of project names but never {@code null}. 842 * @see QueryParameters#ALL_PROJECT_SEARCH 843 * @see #PROJECT_PARAM_NAME 844 * @see #GROUP_PARAM_NAME 845 * @see #OPEN_GROK_PROJECT 846 */ 847 public SortedSet<String> getRequestedProjects() { 848 if (requestedProjects == null) { 849 requestedProjects = getRequestedProjects( 850 QueryParameters.ALL_PROJECT_SEARCH, PROJECT_PARAM_NAME, GROUP_PARAM_NAME, OPEN_GROK_PROJECT); 851 } 852 return requestedProjects; 853 } 854 855 private static final Pattern COMMA_PATTERN = Pattern.compile(","); 856 857 private static void splitByComma(String value, List<String> result) { 858 if (value == null || value.length() == 0) { 859 return; 860 } 861 String[] p = COMMA_PATTERN.split(value); 862 for (String p1 : p) { 863 if (p1.length() != 0) { 864 result.add(p1); 865 } 866 } 867 } 868 869 /** 870 * Get the cookie values for the given name. Splits comma separated values 871 * automatically into a list of Strings. 872 * 873 * @param cookieName name of the cookie. 874 * @return a possible empty list. 875 */ 876 public List<String> getCookieVals(String cookieName) { 877 Cookie[] cookies = req.getCookies(); 878 ArrayList<String> res = new ArrayList<>(); 879 if (cookies != null) { 880 for (int i = cookies.length - 1; i >= 0; i--) { 881 if (cookies[i].getName().equals(cookieName)) { 882 String value = URLDecoder.decode(cookies[i].getValue(), StandardCharsets.UTF_8); 883 splitByComma(value, res); 884 } 885 } 886 } 887 return res; 888 } 889 890 /** 891 * Get the parameter values for the given name. Splits comma separated 892 * values automatically into a list of Strings. 893 * 894 * @param paramName name of the parameter. 895 * @return a possible empty list. 896 */ 897 private List<String> getParameterValues(String paramName) { 898 String[] parameterValues = req.getParameterValues(paramName); 899 List<String> res = new ArrayList<>(); 900 if (parameterValues != null) { 901 for (int i = parameterValues.length - 1; i >= 0; i--) { 902 splitByComma(parameterValues[i], res); 903 } 904 } 905 return res; 906 } 907 908 /** 909 * Same as {@link #getRequestedProjects()}, but with a variable cookieName 910 * and parameter name. 911 * 912 * @param searchAllParamName the name of the request parameter corresponding to search all projects. 913 * @param projectParamName the name of the request parameter corresponding to a project name. 914 * @param groupParamName the name of the request parameter corresponding to a group name 915 * @param cookieName name of the cookie which possible contains project 916 * names used as fallback 917 * @return set of project names. Possibly empty set but never {@code null}. 918 */ 919 protected SortedSet<String> getRequestedProjects( 920 String searchAllParamName, 921 String projectParamName, 922 String groupParamName, 923 String cookieName 924 ) { 925 926 TreeSet<String> projectNames = new TreeSet<>(); 927 List<Project> projects = getEnv().getProjectList(); 928 929 if (Boolean.parseBoolean(req.getParameter(searchAllParamName))) { 930 return getProjectHelper() 931 .getAllProjects() 932 .stream() 933 .map(Project::getName) 934 .collect(Collectors.toCollection(TreeSet::new)); 935 } 936 937 // Use a project determined directly from the URL 938 if (getProject() != null && getProject().isIndexed()) { 939 projectNames.add(getProject().getName()); 940 return projectNames; 941 } 942 943 // Use a project if there is just a single project. 944 if (projects.size() == 1) { 945 Project p = projects.get(0); 946 if (p.isIndexed() && authFramework.isAllowed(req, p)) { 947 projectNames.add(p.getName()); 948 } 949 return projectNames; 950 } 951 952 // Add all projects which match the project parameter name values/ 953 List<String> names = getParameterValues(projectParamName); 954 for (String projectName : names) { 955 Project project = Project.getByName(projectName); 956 if (project != null && project.isIndexed() && authFramework.isAllowed(req, project)) { 957 projectNames.add(projectName); 958 } 959 } 960 961 // Add all projects which are part of a group that matches the group parameter name. 962 names = getParameterValues(groupParamName); 963 for (String groupName : names) { 964 Group group = Group.getByName(groupName); 965 if (group != null) { 966 projectNames.addAll(getProjectHelper().getAllGrouped(group) 967 .stream() 968 .filter(Project::isIndexed) 969 .map(Project::getName) 970 .collect(Collectors.toSet())); 971 } 972 } 973 974 // Add projects based on cookie. 975 if (projectNames.isEmpty() && getIntParam(QueryParameters.NUM_SELECTED_PARAM, -1) != 0) { 976 List<String> cookies = getCookieVals(cookieName); 977 for (String s : cookies) { 978 Project x = Project.getByName(s); 979 if (x != null && x.isIndexed() && authFramework.isAllowed(req, x)) { 980 projectNames.add(s); 981 } 982 } 983 } 984 985 // Add default projects. 986 if (projectNames.isEmpty()) { 987 Set<Project> defaultProjects = env.getDefaultProjects(); 988 if (defaultProjects != null) { 989 for (Project project : defaultProjects) { 990 if (project.isIndexed() && authFramework.isAllowed(req, project)) { 991 projectNames.add(project.getName()); 992 } 993 } 994 } 995 } 996 997 return projectNames; 998 } 999 1000 public ProjectHelper getProjectHelper() { 1001 return ProjectHelper.getInstance(this); 1002 } 1003 1004 /** 1005 * Set the page title to use. 1006 * 1007 * @param title title to set (might be {@code null}). 1008 */ 1009 public void setTitle(String title) { 1010 pageTitle = title; 1011 } 1012 1013 /** 1014 * Get the page title to use. 1015 * 1016 * @return {@code null} if not set, the page title otherwise. 1017 */ 1018 public String getTitle() { 1019 return pageTitle; 1020 } 1021 1022 /** 1023 * Get the base path to use to refer to CSS stylesheets and related 1024 * resources. Usually used to create links. 1025 * 1026 * @return the appropriate application directory prefixed with the 1027 * application's context path (e.g. "/source/default"). 1028 * @see HttpServletRequest#getContextPath() 1029 * @see RuntimeEnvironment#getWebappLAF() 1030 */ 1031 public String getCssDir() { 1032 return req.getContextPath() + PATH_SEPARATOR + getEnv().getWebappLAF(); 1033 } 1034 1035 /** 1036 * Get the current runtime environment. 1037 * 1038 * @return the runtime env. 1039 * @see RuntimeEnvironment#getInstance() 1040 */ 1041 public RuntimeEnvironment getEnv() { 1042 if (env == null) { 1043 env = RuntimeEnvironment.getInstance(); 1044 } 1045 return env; 1046 } 1047 1048 /** 1049 * Get the name patterns used to determine, whether a file should be 1050 * ignored. 1051 * 1052 * @return the corresponding value from the current runtime config.. 1053 */ 1054 public IgnoredNames getIgnoredNames() { 1055 if (ignoredNames == null) { 1056 ignoredNames = getEnv().getIgnoredNames(); 1057 } 1058 return ignoredNames; 1059 } 1060 1061 /** 1062 * Get the canonical path to root of the source tree. File separators are 1063 * replaced with a {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}. 1064 * 1065 * @return The on disk source root directory. 1066 * @see RuntimeEnvironment#getSourceRootPath() 1067 */ 1068 public String getSourceRootPath() { 1069 if (sourceRootPath == null) { 1070 String srcpath = getEnv().getSourceRootPath(); 1071 if (srcpath != null) { 1072 sourceRootPath = srcpath.replace(File.separatorChar, PATH_SEPARATOR); 1073 } 1074 } 1075 return sourceRootPath; 1076 } 1077 1078 /** 1079 * Get the prefix for the related request. 1080 * 1081 * @return {@link Prefix#UNKNOWN} if the servlet path matches any known 1082 * prefix, the prefix otherwise. 1083 */ 1084 public Prefix getPrefix() { 1085 if (prefix == null) { 1086 prefix = Prefix.get(req.getServletPath()); 1087 } 1088 return prefix; 1089 } 1090 1091 /** 1092 * Get the canonical path of the related resource relative to the source 1093 * root directory (used file separators are all {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}). 1094 * No check is made, whether the obtained path is really an accessible resource on disk. 1095 * 1096 * @see HttpServletRequest#getPathInfo() 1097 * @return a possible empty String (denotes the source root directory) but 1098 * not {@code null}. 1099 */ 1100 public String getPath() { 1101 if (path == null) { 1102 path = Util.getCanonicalPath(Laundromat.launderInput(req.getPathInfo()), PATH_SEPARATOR); 1103 if (PATH_SEPARATOR_STRING.equals(path)) { 1104 path = ""; 1105 } 1106 } 1107 return path; 1108 } 1109 1110 /** 1111 * @return true if file/directory corresponding to the request path exists however is unreadable, false otherwise 1112 */ 1113 public boolean isUnreadable() { 1114 File f = new File(getSourceRootPath(), getPath()); 1115 return f.exists() && !f.canRead(); 1116 } 1117 1118 /** 1119 * Get the on disk file for the given path. 1120 * 1121 * NOTE: If a repository contains hard or symbolic links, the returned file 1122 * may finally point to a file outside the source root directory. 1123 * 1124 * @param path the path to the file relatively to the source root 1125 * @return null if the related file or directory is not 1126 * available (can not be find below the source root directory), the readable 1127 * file or directory otherwise. 1128 * @see #getSourceRootPath() 1129 */ 1130 public File getResourceFile(String path) { 1131 File f; 1132 f = new File(getSourceRootPath(), path); 1133 if (!f.canRead()) { 1134 return null; 1135 } 1136 return f; 1137 } 1138 1139 /** 1140 * Get the on disk file to the request related file or directory. 1141 * 1142 * NOTE: If a repository contains hard or symbolic links, the returned file 1143 * may finally point to a file outside the source root directory. 1144 * 1145 * @return {@code new File({@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR_STRING })} 1146 * if the related file or directory is not available (can not be find below the source root directory), 1147 * the readable file or directory otherwise. 1148 * @see #getSourceRootPath() 1149 * @see #getPath() 1150 */ 1151 public File getResourceFile() { 1152 if (resourceFile == null) { 1153 resourceFile = getResourceFile(getPath()); 1154 if (resourceFile == null) { 1155 resourceFile = new File(PATH_SEPARATOR_STRING); 1156 } 1157 } 1158 return resourceFile; 1159 } 1160 1161 /** 1162 * Get the canonical on disk path to the request related file or directory 1163 * with all file separators replaced by a {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}. 1164 * 1165 * @return {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR_STRING} if the evaluated path is invalid 1166 * or outside the source root directory, otherwise the path to the readable file or directory. 1167 * @see #getResourceFile() 1168 */ 1169 public String getResourcePath() { 1170 if (resourcePath == null) { 1171 resourcePath = Util.fixPathIfWindows(getResourceFile().getPath()); 1172 } 1173 return resourcePath; 1174 } 1175 1176 /** 1177 * Check, whether the related request resource matches a valid file or 1178 * directory below the source root directory and whether it matches an 1179 * ignored pattern. 1180 * 1181 * @return {@code true} if the related resource does not exist or should be ignored. 1182 * @see #getIgnoredNames() 1183 * @see #getResourcePath() 1184 */ 1185 public boolean resourceNotAvailable() { 1186 getIgnoredNames(); 1187 return getResourcePath().equals(PATH_SEPARATOR_STRING) || ignoredNames.ignore(getPath()) 1188 || ignoredNames.ignore(resourceFile.getParentFile()) 1189 || ignoredNames.ignore(resourceFile); 1190 } 1191 1192 /** 1193 * Check, whether the request related path represents a directory. 1194 * 1195 * @return {@code true} if directory related request 1196 */ 1197 public boolean isDir() { 1198 if (isDir == null) { 1199 isDir = getResourceFile().isDirectory(); 1200 } 1201 return isDir; 1202 } 1203 1204 private static String trailingSlash(String path) { 1205 return path.length() == 0 || path.charAt(path.length() - 1) != PATH_SEPARATOR 1206 ? PATH_SEPARATOR_STRING 1207 : ""; 1208 } 1209 1210 private File checkFileInner(File file, File dir, String name) { 1211 File f = new File(dir, name); 1212 if (f.exists() && f.isFile()) { 1213 if (f.lastModified() >= file.lastModified()) { 1214 return f; 1215 } else { 1216 LOGGER.log(Level.WARNING, "file ''{0}'' is newer than ''{1}''", new Object[]{file, f}); 1217 } 1218 } 1219 1220 return null; 1221 } 1222 1223 private File checkFile(File file, File dir, String name, boolean compressed) { 1224 File f; 1225 if (compressed) { 1226 f = checkFileInner(file, dir, TandemPath.join(name, ".gz")); 1227 if (f != null) { 1228 return f; 1229 } 1230 } 1231 1232 return checkFileInner(file, dir, name); 1233 } 1234 1235 private File checkFileResolve(File dir, String name, boolean compressed) { 1236 File lresourceFile = new File(getSourceRootPath() + getPath(), name); 1237 if (!lresourceFile.canRead()) { 1238 lresourceFile = new File(PATH_SEPARATOR_STRING); 1239 } 1240 1241 return checkFile(lresourceFile, dir, name, compressed); 1242 } 1243 1244 /** 1245 * Find the files with the given names in the {@link #getPath()} directory 1246 * relative to the cross-file directory of the opengrok data directory. It is 1247 * tried to find the compressed file first by appending the file extension 1248 * ".gz" to the filename. If that fails or an uncompressed version of the 1249 * file is younger than its compressed version, the uncompressed file gets 1250 * used. 1251 * 1252 * @param filenames filenames to lookup. 1253 * @return an empty array if the related directory does not exist or the 1254 * given list is {@code null} or empty, otherwise an array, which may 1255 * contain {@code null} entries (when the related file could not be found) 1256 * having the same order as the given list. 1257 */ 1258 public File[] findDataFiles(List<String> filenames) { 1259 if (filenames == null || filenames.isEmpty()) { 1260 return new File[0]; 1261 } 1262 File[] res = new File[filenames.size()]; 1263 File dir = new File(getEnv().getDataRootPath() + Prefix.XREF_P + getPath()); 1264 if (dir.exists() && dir.isDirectory()) { 1265 getResourceFile(); 1266 boolean compressed = getEnv().isCompressXref(); 1267 for (int i = res.length - 1; i >= 0; i--) { 1268 res[i] = checkFileResolve(dir, filenames.get(i), compressed); 1269 } 1270 } 1271 return res; 1272 } 1273 1274 /** 1275 * Lookup the file {@link #getPath()} relative to the cross-file directory of 1276 * the opengrok data directory. It is tried to find the compressed file 1277 * first by appending the file extension ".gz" to the filename. If that 1278 * fails or an uncompressed version of the file is younger than its 1279 * compressed version, the uncompressed file gets used. 1280 * 1281 * @return {@code null} if not found, the file otherwise. 1282 */ 1283 public File findDataFile() { 1284 return checkFile(resourceFile, new File(getEnv().getDataRootPath() + Prefix.XREF_P), 1285 getPath(), env.isCompressXref()); 1286 } 1287 1288 /** 1289 * @return last revision string for {@code file} or null 1290 */ 1291 @Nullable 1292 public String getLatestRevision() { 1293 if (!getEnv().isHistoryEnabled()) { 1294 LOGGER.log(Level.FINE, "will not get latest revision for ''{0}'' as history is disabled", 1295 getResourceFile()); 1296 return null; 1297 } 1298 1299 // Try getting the history revision from the index first. 1300 String lastRev = getLastRevFromIndex(); 1301 if (lastRev != null) { 1302 LOGGER.log(Level.FINEST, "got last revision of ''{0}'' from the index", getResourceFile()); 1303 return lastRev; 1304 } 1305 1306 // If this is older index, fallback to the history (either fetch from history cache or retrieve from 1307 // the repository directly). 1308 try { 1309 return getLastRevFromHistory(); 1310 } catch (HistoryException e) { 1311 LOGGER.log(Level.WARNING, "cannot get latest revision for ''{0}'' using history", getPath()); 1312 return null; 1313 } 1314 } 1315 1316 @Nullable 1317 private String getLastRevFromHistory() throws HistoryException { 1318 File file = new File(getEnv().getSourceRootFile(), getPath()); 1319 HistoryEntry he = HistoryGuru.getInstance().getLastHistoryEntry(file, true); 1320 if (he != null) { 1321 return he.getRevision(); 1322 } 1323 1324 return null; 1325 } 1326 1327 /** 1328 * Retrieve last revision from the document matching the resource file (if any). 1329 * @return last revision or {@code null} if the document cannot be found, is out of sync 1330 * w.r.t. last modified time of the file or the last commit ID is not stored in the document. 1331 */ 1332 @Nullable 1333 @VisibleForTesting 1334 String getLastRevFromIndex() { 1335 Document doc = null; 1336 try { 1337 doc = IndexDatabase.getDocument(getResourceFile()); 1338 } catch (Exception e) { 1339 LOGGER.log(Level.WARNING, String.format("cannot get document for %s", path), e); 1340 } 1341 1342 String lastRev = null; 1343 if (doc != null) { 1344 // There is no point of checking the date if the LASTREV field is not present. 1345 lastRev = doc.get(QueryBuilder.LASTREV); 1346 if (lastRev != null) { 1347 Date docDate; 1348 try { 1349 docDate = DateTools.stringToDate(doc.get(QueryBuilder.DATE)); 1350 } catch (ParseException e) { 1351 LOGGER.log(Level.WARNING, String.format("cannot get date from the document %s", doc), e); 1352 return null; 1353 } 1354 Date fileDate = new Date(getResourceFile().lastModified()); 1355 if (docDate.compareTo(fileDate) < 0) { 1356 LOGGER.log(Level.FINER, "document for ''{0}'' is out of sync", getResourceFile()); 1357 return null; 1358 } 1359 } 1360 } 1361 1362 return lastRev; 1363 } 1364 1365 /** 1366 * Is revision the latest revision ? 1367 * @param rev revision string 1368 * @return true if latest revision, false otherwise 1369 */ 1370 public boolean isLatestRevision(String rev) { 1371 return rev.equals(getLatestRevision()); 1372 } 1373 1374 /** 1375 * Get the location of cross-reference for given file containing the given revision. 1376 * @param revStr defined revision string 1377 * @return location to redirect to 1378 */ 1379 public String getRevisionLocation(String revStr) { 1380 StringBuilder sb = new StringBuilder(); 1381 1382 sb.append(req.getContextPath()); 1383 sb.append(Prefix.XREF_P); 1384 sb.append(Util.uriEncodePath(getPath())); 1385 sb.append("?"); 1386 sb.append(QueryParameters.REVISION_PARAM_EQ); 1387 sb.append(Util.uriEncode(revStr)); 1388 1389 if (req.getQueryString() != null) { 1390 sb.append("&"); 1391 sb.append(req.getQueryString()); 1392 } 1393 if (fragmentIdentifier != null) { 1394 String anchor = Util.uriEncode(fragmentIdentifier); 1395 1396 String reqFrag = req.getParameter(QueryParameters.FRAGMENT_IDENTIFIER_PARAM); 1397 if (reqFrag == null || reqFrag.isEmpty()) { 1398 /* 1399 * We've determined that the fragmentIdentifier field must have 1400 * been set to augment request parameters. Now include it 1401 * explicitly in the next request parameters. 1402 */ 1403 sb.append("&"); 1404 sb.append(QueryParameters.FRAGMENT_IDENTIFIER_PARAM_EQ); 1405 sb.append(anchor); 1406 } 1407 sb.append("#"); 1408 sb.append(anchor); 1409 } 1410 1411 return sb.toString(); 1412 } 1413 1414 /** 1415 * Get the path the request should be redirected (if any). 1416 * 1417 * @return {@code null} if there is no reason to redirect, the URI encoded 1418 * redirect path to use otherwise. 1419 */ 1420 public String getDirectoryRedirect() { 1421 if (isDir()) { 1422 getPrefix(); 1423 // Redirect /xref -> /xref/ 1424 if (prefix == Prefix.XREF_P 1425 && getUriEncodedPath().isEmpty() 1426 && !req.getRequestURI().endsWith("/")) { 1427 return req.getContextPath() + Prefix.XREF_P + '/'; 1428 } 1429 1430 if (getPath().length() == 0) { 1431 // => / 1432 return null; 1433 } 1434 1435 if (prefix != Prefix.XREF_P && prefix != Prefix.HIST_L 1436 && prefix != Prefix.RSS_P) { 1437 // if it is an existing dir perhaps people wanted dir xref 1438 return req.getContextPath() + Prefix.XREF_P 1439 + getUriEncodedPath() + trailingSlash(getPath()); 1440 } 1441 String ts = trailingSlash(getPath()); 1442 if (ts.length() != 0) { 1443 return req.getContextPath() + prefix + getUriEncodedPath() + ts; 1444 } 1445 } 1446 return null; 1447 } 1448 1449 /** 1450 * Get the URI encoded canonical path to the related file or directory (the 1451 * URI part between the servlet path and the start of the query string). 1452 * 1453 * @return a URI encoded path which might be an empty string but not {@code null}. 1454 * @see #getPath() 1455 */ 1456 public String getUriEncodedPath() { 1457 if (uriEncodedPath == null) { 1458 uriEncodedPath = Util.uriEncodePath(getPath()); 1459 } 1460 return uriEncodedPath; 1461 } 1462 1463 /** 1464 * Add a new file script to the page by the name. 1465 * 1466 * @param name name of the script to search for 1467 * @return this 1468 * 1469 * @see Scripts#addScript(String, String, Scripts.Type) 1470 */ 1471 public PageConfig addScript(String name) { 1472 this.scripts.addScript(this.req.getContextPath(), name, isDebug() ? Scripts.Type.DEBUG : Scripts.Type.MINIFIED); 1473 return this; 1474 } 1475 1476 private boolean isDebug() { 1477 return Boolean.parseBoolean(req.getParameter(DEBUG_PARAM_NAME)); 1478 } 1479 1480 /** 1481 * Return the page scripts. 1482 * 1483 * @return the scripts 1484 * 1485 * @see Scripts 1486 */ 1487 public Scripts getScripts() { 1488 return this.scripts; 1489 } 1490 1491 /** 1492 * Get opengrok's configured data root directory. It is verified, that the 1493 * used environment has a valid opengrok data root set and that it is an 1494 * accessible directory. 1495 * 1496 * @return the opengrok data directory. 1497 * @throws InvalidParameterException if inaccessible or not set. 1498 */ 1499 public File getDataRoot() { 1500 if (dataRoot == null) { 1501 String tmp = getEnv().getDataRootPath(); 1502 if (tmp == null || tmp.length() == 0) { 1503 throw new InvalidParameterException("dataRoot parameter is not " 1504 + "set in configuration.xml!"); 1505 } 1506 dataRoot = new File(tmp); 1507 if (!(dataRoot.isDirectory() && dataRoot.canRead())) { 1508 throw new InvalidParameterException("The configured dataRoot '" 1509 + tmp 1510 + "' refers to a none-existing or unreadable directory!"); 1511 } 1512 } 1513 return dataRoot; 1514 } 1515 1516 /** 1517 * Play nice in reverse proxy environment by using pre-configured hostname 1518 * request to construct the URLs. 1519 * Will not work well if the scheme or port is different for proxied server 1520 * and original server. 1521 * @return server name 1522 */ 1523 public String getServerName() { 1524 if (env.getServerName() != null) { 1525 return env.getServerName(); 1526 } else { 1527 return req.getServerName(); 1528 } 1529 } 1530 1531 /** 1532 * Prepare a search helper with all required information, ready to execute 1533 * the query implied by the related request parameters and cookies. 1534 * <p> 1535 * NOTE: One should check the {@link SearchHelper#getErrorMsg()} as well as 1536 * {@link SearchHelper#getRedirect()} and take the appropriate action before 1537 * executing the prepared query or continue processing. 1538 * <p> 1539 * This method stops populating fields as soon as an error occurs. 1540 * 1541 * @return a search helper. 1542 */ 1543 public SearchHelper prepareSearch() { 1544 List<SortOrder> sortOrders = getSortOrder(); 1545 SearchHelper sh = prepareInternalSearch(sortOrders.isEmpty() ? SortOrder.RELEVANCY : sortOrders.get(0)); 1546 1547 if (getRequestedProjects().isEmpty() && getEnv().hasProjects()) { 1548 sh.setErrorMsg("You must select a project!"); 1549 return sh; 1550 } 1551 1552 if (sh.getBuilder().getSize() == 0) { 1553 // Entry page show the map 1554 sh.setRedirect(req.getContextPath() + '/'); 1555 return sh; 1556 } 1557 1558 return sh; 1559 } 1560 1561 /** 1562 * Prepare a search helper with required settings for an internal search. 1563 * <p> 1564 * NOTE: One should check the {@link SearchHelper#getErrorMsg()} as well as 1565 * {@link SearchHelper#getRedirect()} and take the appropriate action before 1566 * executing the prepared query or continue processing. 1567 * <p> 1568 * This method stops populating fields as soon as an error occurs. 1569 * @return a search helper. 1570 */ 1571 public SearchHelper prepareInternalSearch(SortOrder sortOrder) { 1572 String xrValue = req.getParameter(QueryParameters.NO_REDIRECT_PARAM); 1573 return new SearchHelper(getSearchStart(), sortOrder, getDataRoot(), new File(getSourceRootPath()), 1574 getSearchMaxItems(), getEftarReader(), getQueryBuilder(), getPrefix() == Prefix.SEARCH_R, 1575 req.getContextPath(), getPrefix() == Prefix.SEARCH_R || getPrefix() == Prefix.SEARCH_P, 1576 xrValue != null && !xrValue.isEmpty()); 1577 } 1578 1579 /** 1580 * Get the config w.r.t. the given request. If there is none yet, a new config 1581 * gets created, attached to the request and returned. 1582 * <p> 1583 * 1584 * @param request the request to use to initialize the config parameters. 1585 * @return always the same none-{@code null} config for a given request. 1586 * @throws NullPointerException if the given parameter is {@code null}. 1587 */ 1588 public static PageConfig get(HttpServletRequest request) { 1589 Object cfg = request.getAttribute(ATTR_NAME); 1590 if (cfg != null) { 1591 return (PageConfig) cfg; 1592 } 1593 PageConfig pcfg = new PageConfig(request); 1594 request.setAttribute(ATTR_NAME, pcfg); 1595 return pcfg; 1596 } 1597 1598 private PageConfig(HttpServletRequest req) { 1599 this.req = req; 1600 this.authFramework = RuntimeEnvironment.getInstance().getAuthorizationFramework(); 1601 this.executor = RuntimeEnvironment.getInstance().getRevisionExecutor(); 1602 this.fragmentIdentifier = req.getParameter(QueryParameters.FRAGMENT_IDENTIFIER_PARAM); 1603 } 1604 1605 /** 1606 * Cleanup all allocated resources (if any) from the instance attached to 1607 * the given request. 1608 * 1609 * @param sr request to check, cleanup. Ignored if {@code null}. 1610 * @see PageConfig#get(HttpServletRequest) 1611 */ 1612 public static void cleanup(ServletRequest sr) { 1613 if (sr == null) { 1614 return; 1615 } 1616 PageConfig cfg = (PageConfig) sr.getAttribute(ATTR_NAME); 1617 if (cfg == null) { 1618 return; 1619 } 1620 ProjectHelper.cleanup(cfg); 1621 sr.removeAttribute(ATTR_NAME); 1622 cfg.env = null; 1623 cfg.req = null; 1624 if (cfg.eftarReader != null) { 1625 cfg.eftarReader.close(); 1626 } 1627 } 1628 1629 /** 1630 * Checks if current request is allowed to access project. 1631 * @param t project 1632 * @return true if yes 1633 */ 1634 public boolean isAllowed(Project t) { 1635 return this.authFramework.isAllowed(this.req, t); 1636 } 1637 1638 /** 1639 * Checks if current request is allowed to access group. 1640 * @param g group 1641 * @return true if yes 1642 */ 1643 public boolean isAllowed(Group g) { 1644 return this.authFramework.isAllowed(this.req, g); 1645 } 1646 1647 1648 public SortedSet<AcceptedMessage> getMessages() { 1649 return env.getMessages(); 1650 } 1651 1652 public SortedSet<AcceptedMessage> getMessages(String tag) { 1653 return env.getMessages(tag); 1654 } 1655 1656 /** 1657 * Get basename of the path or "/" if the path is empty. 1658 * This is used for setting title of various pages. 1659 * @param path path 1660 * @return short version of the path 1661 */ 1662 public String getShortPath(String path) { 1663 File file = new File(path); 1664 1665 if (path.isEmpty()) { 1666 return "/"; 1667 } 1668 1669 return file.getName(); 1670 } 1671 1672 private String addTitleDelimiter(String title) { 1673 if (!title.isEmpty()) { 1674 return title + ", "; 1675 } 1676 1677 return title; 1678 } 1679 1680 /** 1681 * The search page title string should progressively reflect the search terms 1682 * so that if only small portion of the string is seen, it describes 1683 * the action as closely as possible while remaining readable. 1684 * @return string used for setting page title of search results page 1685 */ 1686 public String getSearchTitle() { 1687 String title = ""; 1688 1689 if (req.getParameter(QueryBuilder.FULL) != null && !req.getParameter(QueryBuilder.FULL).isEmpty()) { 1690 title += req.getParameter(QueryBuilder.FULL) + " (full)"; 1691 } 1692 if (req.getParameter(QueryBuilder.DEFS) != null && !req.getParameter(QueryBuilder.DEFS).isEmpty()) { 1693 title = addTitleDelimiter(title); 1694 title += req.getParameter(QueryBuilder.DEFS) + " (definition)"; 1695 } 1696 if (req.getParameter(QueryBuilder.REFS) != null && !req.getParameter(QueryBuilder.REFS).isEmpty()) { 1697 title = addTitleDelimiter(title); 1698 title += req.getParameter(QueryBuilder.REFS) + " (reference)"; 1699 } 1700 if (req.getParameter(QueryBuilder.PATH) != null && !req.getParameter(QueryBuilder.PATH).isEmpty()) { 1701 title = addTitleDelimiter(title); 1702 title += req.getParameter(QueryBuilder.PATH) + " (path)"; 1703 } 1704 if (req.getParameter(QueryBuilder.HIST) != null && !req.getParameter(QueryBuilder.HIST).isEmpty()) { 1705 title = addTitleDelimiter(title); 1706 title += req.getParameter(QueryBuilder.HIST) + " (history)"; 1707 } 1708 1709 if (req.getParameterValues(QueryBuilder.PROJECT) != null && req.getParameterValues(QueryBuilder.PROJECT).length != 0) { 1710 if (!title.isEmpty()) { 1711 title += " "; 1712 } 1713 title += "in projects: "; 1714 String[] projects = req.getParameterValues(QueryBuilder.PROJECT); 1715 title += String.join(",", projects); 1716 } 1717 1718 return Util.htmlize(title + " - OpenGrok search results"); 1719 } 1720 1721 /** 1722 * Similar as {@link #getSearchTitle()}. 1723 * @return string used for setting page title of search view 1724 */ 1725 public String getHistoryTitle() { 1726 String path = getPath(); 1727 return Util.htmlize(getShortPath(path) + " - OpenGrok history log for " + path); 1728 } 1729 1730 public String getPathTitle() { 1731 String path = getPath(); 1732 String title = getShortPath(path); 1733 if (!getRequestedRevision().isEmpty()) { 1734 title += " (revision " + getRequestedRevision() + ")"; 1735 } 1736 title += " - OpenGrok cross reference for " + (path.isEmpty() ? "/" : path); 1737 1738 return Util.htmlize(title); 1739 } 1740 1741 public void checkSourceRootExistence() throws IOException { 1742 if (getSourceRootPath() == null || getSourceRootPath().isEmpty()) { 1743 throw new FileNotFoundException("Unable to determine source root path. Missing configuration?"); 1744 } 1745 File sourceRootPathFile = RuntimeEnvironment.getInstance().getSourceRootFile(); 1746 if (!sourceRootPathFile.exists()) { 1747 throw new FileNotFoundException(String.format("Source root path \"%s\" does not exist", 1748 sourceRootPathFile.getAbsolutePath())); 1749 } 1750 if (!sourceRootPathFile.isDirectory()) { 1751 throw new FileNotFoundException(String.format("Source root path \"%s\" is not a directory", 1752 sourceRootPathFile.getAbsolutePath())); 1753 } 1754 if (!sourceRootPathFile.canRead()) { 1755 throw new IOException(String.format("Source root path \"%s\" is not readable", 1756 sourceRootPathFile.getAbsolutePath())); 1757 } 1758 } 1759 1760 /** 1761 * Get all project related messages. These include 1762 * <ol> 1763 * <li>Main messages</li> 1764 * <li>Messages with tag = project name</li> 1765 * <li>Messages with tag = project's groups names</li> 1766 * </ol> 1767 * 1768 * @return the sorted set of messages according to accept time 1769 * @see org.opengrok.indexer.web.messages.MessagesContainer#MESSAGES_MAIN_PAGE_TAG 1770 */ 1771 private SortedSet<AcceptedMessage> getProjectMessages() { 1772 SortedSet<AcceptedMessage> messages = getMessages(); 1773 1774 if (getProject() != null) { 1775 messages.addAll(getMessages(getProject().getName())); 1776 getProject().getGroups().forEach(group -> { 1777 messages.addAll(getMessages(group.getName())); 1778 }); 1779 } 1780 1781 return messages; 1782 } 1783 1784 /** 1785 * Decide if this resource has been modified since the header value in the request. 1786 * <p> 1787 * The resource is modified since the weak ETag value in the request, the ETag is 1788 * computed using: 1789 * 1790 * <ul> 1791 * <li>the source file modification</li> 1792 * <li>project messages</li> 1793 * <li>last timestamp for index</li> 1794 * <li>OpenGrok current deployed version</li> 1795 * </ul> 1796 * 1797 * <p> 1798 * If the resource was modified, appropriate headers in the response are filled. 1799 * 1800 * 1801 * @param request the http request containing the headers 1802 * @param response the http response for setting the headers 1803 * @return true if resource was not modified; false otherwise 1804 * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">HTTP ETag</a> 1805 * @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html">HTTP Caching</a> 1806 */ 1807 public boolean isNotModified(HttpServletRequest request, HttpServletResponse response) { 1808 String currentEtag = String.format("W/\"%s\"", 1809 Objects.hash( 1810 // last modified time as UTC timestamp in millis 1811 getLastModified(), 1812 // all project related messages which changes the view 1813 getProjectMessages(), 1814 // last timestamp value 1815 getEnv().getDateForLastIndexRun() != null ? getEnv().getDateForLastIndexRun().getTime() : 0, 1816 // OpenGrok version has changed since the last time 1817 Info.getVersion() 1818 ) 1819 ); 1820 1821 String headerEtag = request.getHeader(HttpHeaders.IF_NONE_MATCH); 1822 1823 if (headerEtag != null && headerEtag.equals(currentEtag)) { 1824 // weak ETag has not changed, return 304 NOT MODIFIED 1825 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 1826 return true; 1827 } 1828 1829 // return 200 OK 1830 response.setHeader(HttpHeaders.ETAG, currentEtag); 1831 return false; 1832 } 1833 1834 /** 1835 * @param root root path 1836 * @param path path 1837 * @return path relative to root 1838 */ 1839 public static String getRelativePath(String root, String path) { 1840 return Paths.get(root).relativize(Paths.get(path)).toString(); 1841 } 1842 1843 /** 1844 * Determines whether a match offset from a search result has been 1845 * indicated, and if so tries to calculate a translated xref fragment 1846 * identifier. 1847 * @return {@code true} if a xref fragment identifier was calculated by the call to this method 1848 */ 1849 public boolean evaluateMatchOffset() { 1850 if (fragmentIdentifier == null) { 1851 int matchOffset = getIntParam(QueryParameters.MATCH_OFFSET_PARAM, -1); 1852 if (matchOffset >= 0) { 1853 File resourceFile = getResourceFile(); 1854 if (resourceFile.isFile()) { 1855 LineBreaker breaker = new LineBreaker(); 1856 StreamSource streamSource = StreamSource.fromFile(resourceFile); 1857 try { 1858 breaker.reset(streamSource, in -> ExpandTabsReader.wrap(in, getProject())); 1859 int matchLine = breaker.findLineIndex(matchOffset); 1860 if (matchLine >= 0) { 1861 // Convert to 1-based offset to accord with OpenGrok line number. 1862 fragmentIdentifier = String.valueOf(matchLine + 1); 1863 return true; 1864 } 1865 } catch (IOException e) { 1866 LOGGER.log(Level.WARNING, String.format("Failed to evaluate match offset for %s", 1867 resourceFile), e); 1868 } 1869 } 1870 } 1871 } 1872 return false; 1873 } 1874 } 1875