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