diff --git a/docker/start.py b/docker/start.py index 7daa890c5e3..48d46e61206 100755 --- a/docker/start.py +++ b/docker/start.py @@ -115,7 +115,7 @@ def index(): API endpoint for triggering reindex. :return: message describing the outcome """ - global periodic_timer + global periodic_timer # noqa: F824 if periodic_timer: logger = logging.getLogger(__name__) @@ -360,7 +360,7 @@ def indexer_no_projects(logger, uri, config_path, extra_indexer_options): indexer.execute() logger.info("Waiting for reindex to be triggered") - global periodic_timer + global periodic_timer # noqa: F824 periodic_timer.wait_for_event() @@ -420,7 +420,7 @@ def project_syncer(logger, loglevel, uri, config_path, numworkers, env, api_time save_config(logger, uri, config_path, api_timeout) logger.info("Waiting for reindex to be triggered") - global periodic_timer + global periodic_timer # noqa: F824 periodic_timer.wait_for_event() diff --git a/opengrok-indexer/src/main/java/org/opengrok/indexer/configuration/Configuration.java b/opengrok-indexer/src/main/java/org/opengrok/indexer/configuration/Configuration.java index 8dceda0346e..b3e8076a5ef 100644 --- a/opengrok-indexer/src/main/java/org/opengrok/indexer/configuration/Configuration.java +++ b/opengrok-indexer/src/main/java/org/opengrok/indexer/configuration/Configuration.java @@ -59,6 +59,7 @@ import org.opengrok.indexer.authorization.AuthorizationStack; import org.opengrok.indexer.history.RepositoryInfo; import org.opengrok.indexer.logger.LoggerFactory; +import org.opengrok.indexer.web.Util; import static org.opengrok.indexer.configuration.PatternUtil.compilePattern; @@ -1642,5 +1643,12 @@ public void checkConfiguration() throws ConfigurationException { LOGGER.log(Level.INFO, "History based reindex is on, however history cache is off. " + "History cache has to be enabled for history based reindex."); } + + if (!Objects.isNull(getBugPage()) && !Util.isHttpUri(getBugPage())) { + throw new ConfigurationException("Bug page must be valid HTTP(S) URI"); + } + if (!Objects.isNull(getReviewPage()) && !Util.isHttpUri(getReviewPage())) { + throw new ConfigurationException("Review page must be valid HTTP(S) URI"); + } } } diff --git a/opengrok-indexer/src/main/java/org/opengrok/indexer/web/Util.java b/opengrok-indexer/src/main/java/org/opengrok/indexer/web/Util.java index fecbb71c28b..26318d75193 100644 --- a/opengrok-indexer/src/main/java/org/opengrok/indexer/web/Util.java +++ b/opengrok-indexer/src/main/java/org/opengrok/indexer/web/Util.java @@ -54,11 +54,13 @@ import java.util.Locale; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; import java.util.TreeMap; import java.util.function.IntConsumer; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.MatchResult; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.IntStream; @@ -367,7 +369,7 @@ public static String breadcrumbPath(String urlPrefix, String path) { * @param path the full path from which the breadcrumb path is built * @param sep separator to use to crack the given path * - * @return HTML markup fro the breadcrumb or the path itself. + * @return HTML markup for the breadcrumb or the path itself. * @see #breadcrumbPath(String, String, char, String, boolean, boolean) */ public static String breadcrumbPath(String urlPrefix, String path, char sep) { @@ -658,7 +660,7 @@ public static void encode(String s, Appendable dest) throws IOException { // special html characters dest.append("&#").append("" + (int) c).append(";"); } else if (c == ' ') { - // non breaking space + // non-breaking space dest.append(" "); } else if (c == '\t') { dest.append("    "); @@ -671,22 +673,6 @@ public static void encode(String s, Appendable dest) throws IOException { } } - /** - * Encode URL. - * - * @param urlStr string URL - * @return the encoded URL - * @throws URISyntaxException URI syntax - * @throws MalformedURLException URL malformed - */ - public static String encodeURL(String urlStr) throws URISyntaxException, MalformedURLException { - URL url = new URL(urlStr); - URI constructed = new URI(url.getProtocol(), url.getUserInfo(), - url.getHost(), url.getPort(), - url.getPath(), url.getQuery(), url.getRef()); - return constructed.toString(); - } - /** * Write out line information wrt. to the given annotation in the format: * {@code Linenumber Blame Author} incl. appropriate links. @@ -939,26 +925,22 @@ public static String uriEncode(String q) { * @param dest a defined target * @throws IOException I/O */ - public static void uriEncode(String str, Appendable dest) - throws IOException { + public static void uriEncode(String str, Appendable dest) throws IOException { String uenc = uriEncode(str); dest.append(uenc); } /** - * Append '&name=value" to the given buffer. If the given - * value - * is {@code null}, this method does nothing. + * Append "&name=value" to the given buffer. If the given value is {@code null}, + * this method does nothing. * * @param buf where to append the query string * @param key the name of the parameter to add. Append as is! - * @param value the value for the given parameter. Gets automatically UTF-8 - * URL encoded. + * @param value the value for the given parameter. Gets automatically UTF-8 URL encoded. * @throws NullPointerException if the given buffer is {@code null}. * @see #uriEncode(String) */ - public static void appendQuery(StringBuilder buf, String key, - String value) { + public static void appendQuery(StringBuilder buf, String key, String value) { if (value != null) { buf.append(AMP).append(key).append('=').append(uriEncode(value)); @@ -1454,48 +1436,50 @@ private static String generatePageLink(int page, int offset, int limit, long siz } - /** - * Check if the string is a HTTP URL. + * Check if the string is an HTTP(S) URI (i.e. allows for relative identifiers). * * @param string the string to check - * @return true if it is http URL, false otherwise + * @return true if it is HTTP(S) URI, false otherwise */ public static boolean isHttpUri(String string) { - URL url; + URI uri; try { - url = new URL(string); - } catch (MalformedURLException ex) { + uri = new URI(string); + } catch (URISyntaxException ex) { return false; } - return url.getProtocol().equals("http") || url.getProtocol().equals("https"); + String scheme = uri.getScheme(); + if (Objects.isNull(scheme)) { + return false; + } + return uri.getScheme().equals("http") || uri.getScheme().equals("https"); } - protected static final String REDACTED_USER_INFO = "redacted_by_OpenGrok"; + static final String REDACTED_USER_INFO = "redacted_by_OpenGrok"; /** - * If given path is a URL, return the string representation with the user-info part filtered out. + * If given path is a URI, return the string representation with the user-info part filtered out. * @param path path to object - * @return either the original string or string representation of URL with the user-info part removed + * @return either the original string (if the URI is not valid) + * or string representation of the URI with the user-info part removed */ - public static String redactUrl(String path) { - URL url; + public static String redactUri(String path) { + URI uri; try { - url = new URL(path); - } catch (MalformedURLException e) { - // not an URL + uri = new URI(path); + } catch (URISyntaxException e) { return path; } - if (url.getUserInfo() != null) { - return url.toString().replace(url.getUserInfo(), - REDACTED_USER_INFO); + if (uri.getUserInfo() != null) { + return uri.toString().replace(uri.getUserInfo(), REDACTED_USER_INFO); } else { return path; } } /** - * Build a HTML link to the given HTTP URL. If the URL is not an http URL + * Build an HTML link to the given HTTP URL. If the URL is not an HTTP URL * then it is returned as it was received. This has the same effect as * invoking linkify(url, true). * @@ -1509,7 +1493,7 @@ public static String linkify(String url) { } /** - * Build a html link to the given http URL. If the URL is not an http URL + * Build an HTML link to the given HTTP URL. If the URL is not an HTTP URL * then it is returned as it was received. * * @param url the HTTP URL @@ -1535,9 +1519,9 @@ public static String linkify(String url, boolean newTab) { } /** - * Build an anchor with given name and a pack of attributes. Automatically - * escapes href attributes and automatically escapes the name into HTML - * entities. + * Build an anchor with given name and a pack of attributes. + * Assumes the href attribute value is URI encoded. + * Automatically escapes the name into HTML entities. * * @param name displayed name of the anchor * @param attrs map of attributes for the html element @@ -1556,7 +1540,7 @@ public static String buildLink(String name, Map attrs) buffer.append("=\""); String value = attr.getValue(); if (attr.getKey().equals("href")) { - value = Util.encodeURL(value); + value = new URI(value).toURL().toString(); } buffer.append(value); buffer.append("\""); @@ -1568,9 +1552,9 @@ public static String buildLink(String name, Map attrs) } /** - * Build an anchor with given name and a pack of attributes. Automatically - * escapes href attributes and automatically escapes the name into HTML - * entities. + * Build an anchor with given name and a pack of attributes. + * Assumes the href attribute value is URI encoded. + * Automatically escapes the name into HTML entities. * * @param name displayed name of the anchor * @param url anchor's URL @@ -1579,17 +1563,16 @@ public static String buildLink(String name, Map attrs) * @throws URISyntaxException URI syntax * @throws MalformedURLException bad URL */ - public static String buildLink(String name, String url) - throws URISyntaxException, MalformedURLException { + public static String buildLink(String name, String url) throws URISyntaxException, MalformedURLException { Map attrs = new TreeMap<>(); attrs.put("href", url); return buildLink(name, attrs); } /** - * Build an anchor with given name and a pack of attributes. Automatically - * escapes href attributes and automatically escapes the name into HTML - * entities. + * Build an anchor with given name and a pack of attributes. + * Assumes the href attribute value is URI encoded. + * Automatically escapes the name into HTML entities. * * @param name displayed name of the anchor * @param url anchor's URL @@ -1610,30 +1593,52 @@ public static String buildLink(String name, String url, boolean newTab) return buildLink(name, attrs); } + /** + * Callback function to provide replacement value for pattern match. + * It replaces the first group of the pattern and retains the rest of the pattern. + * + * @param result {@link MatchResult} object representing pattern match + * @param text original text containing the match + * @param url URL to be used for the replacement + * @return replacement for the first group in the pattern + */ + private static String buildLinkReplacer(MatchResult result, String text, String url) { + if (result.groupCount() < 1) { + return result.group(0); + } + final String group1 = result.group(1); + final String appendedUrl = url + uriEncode(group1); + try { + StringBuilder stringBuilder = new StringBuilder(); + if (result.start(0) < result.start(1)) { + stringBuilder.append(text.substring(result.start(0), result.start(1))); + } + stringBuilder.append(buildLink(group1, appendedUrl, true)); + if (result.end(1) < result.end(0)) { + stringBuilder.append(text.substring(result.end(1), result.end(0))); + } + return stringBuilder.toString(); + } catch (URISyntaxException | MalformedURLException e) { + LOGGER.log(Level.WARNING, "The given URL ''{0}'' is not valid", appendedUrl); + return result.group(0); + } + } + /** * Replace all occurrences of pattern in the incoming text with the link - * named name pointing to an URL. It is possible to use the regexp pattern - * groups in name and URL when they are specified in the pattern. + * named name pointing to a URL. * - * @param text text to replace all patterns + * @param text text to replace all patterns * @param pattern the pattern to match - * @param name link display name - * @param url link URL + * @param url link URL * @return the text with replaced links */ - public static String linkifyPattern(String text, Pattern pattern, String name, String url) { - try { - String buildLink = buildLink(name, url, true); - return pattern.matcher(text).replaceAll(buildLink); - } catch (URISyntaxException | MalformedURLException ex) { - LOGGER.log(Level.WARNING, "The given URL ''{0}'' is not valid", url); - return text; - } + public static String linkifyPattern(String text, Pattern pattern, String url) { + return pattern.matcher(text).replaceAll(match -> buildLinkReplacer(match, text, url)); } /** - * Try to complete the given URL part into full URL with server name, port, - * scheme, ... + * Try to complete the given URL part into full URL with server name, port, scheme, ... *
*
for request http://localhost:8080/source/xref/xxx and part * /cgi-bin/user=
@@ -1655,7 +1660,8 @@ public static String completeUrl(String url, HttpServletRequest req) { try { if (!isHttpUri(url)) { if (url.startsWith("/")) { - return new URI(req.getScheme(), null, req.getServerName(), req.getServerPort(), url, null, null).toString(); + return new URI(req.getScheme(), null, req.getServerName(), req.getServerPort(), url, + null, null).toString(); } var prepUrl = req.getRequestURL(); if (!url.isEmpty()) { @@ -1665,7 +1671,7 @@ public static String completeUrl(String url, HttpServletRequest req) { } return url; } catch (URISyntaxException ex) { - LOGGER.log(Level.INFO, + LOGGER.log(Level.WARNING, String.format("Unable to convert given URL part '%s' to complete URL", url), ex); return url; diff --git a/opengrok-indexer/src/test/java/org/opengrok/indexer/configuration/ConfigurationTest.java b/opengrok-indexer/src/test/java/org/opengrok/indexer/configuration/ConfigurationTest.java index 520459221e2..27136d307f5 100644 --- a/opengrok-indexer/src/test/java/org/opengrok/indexer/configuration/ConfigurationTest.java +++ b/opengrok-indexer/src/test/java/org/opengrok/indexer/configuration/ConfigurationTest.java @@ -18,7 +18,7 @@ */ /* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * Portions Copyright (c) 2020, Chris Fraire . */ package org.opengrok.indexer.configuration; @@ -33,17 +33,23 @@ import java.io.InputStreamReader; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.LinkedList; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.xml.XMLConstants; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.opengrok.indexer.util.ClassUtil; +import org.opengrok.indexer.util.IOUtils; import org.xml.sax.Attributes; import org.xml.sax.ext.DefaultHandler2; @@ -234,4 +240,34 @@ void testLoadingValidConfiguration() throws IOException { } } + private static Stream getArgsForTestCheckConfigurationBugPage() { + return Stream.of( + Arguments.of(true, true), + Arguments.of(true, false), + Arguments.of(false, true), + Arguments.of(false, false) + ); + } + + @ParameterizedTest + @MethodSource("getArgsForTestCheckConfigurationBugPage") + void testCheckConfigurationBugPage(boolean valid, boolean bugPage) throws IOException { + Configuration cfg = new Configuration(); + Path tmpSourceRoot = Files.createTempDirectory("sourceRoot"); + cfg.setSourceRoot(tmpSourceRoot.toString()); + Path tmpDataRoot = Files.createTempDirectory("dataRoot"); + cfg.setDataRoot(tmpDataRoot.toString()); + if (bugPage) { + cfg.setBugPage("http://example.org/bug?" + (valid ? "" : "\"")); + } else { + cfg.setReviewPage("http://example.org/review?" + (valid ? "" : "\"")); + } + if (!valid) { + assertThrows(Configuration.ConfigurationException.class, cfg::checkConfiguration); + } else { + assertDoesNotThrow(cfg::checkConfiguration); + } + IOUtils.removeRecursive(tmpSourceRoot); + IOUtils.removeRecursive(tmpDataRoot); + } } diff --git a/opengrok-indexer/src/test/java/org/opengrok/indexer/web/UtilTest.java b/opengrok-indexer/src/test/java/org/opengrok/indexer/web/UtilTest.java index a5885537845..44eb30dca4c 100644 --- a/opengrok-indexer/src/test/java/org/opengrok/indexer/web/UtilTest.java +++ b/opengrok-indexer/src/test/java/org/opengrok/indexer/web/UtilTest.java @@ -55,6 +55,7 @@ import static org.hamcrest.collection.IsIterableContainingInOrder.contains; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -375,13 +376,13 @@ void testIsUrl() { } @Test - void testRedactUrl() { - assertEquals("/foo/bar", Util.redactUrl("/foo/bar")); - assertEquals("http://foo/bar?r=xxx", Util.redactUrl("http://foo/bar?r=xxx")); + void testRedactUri() { + assertEquals("/foo/bar", Util.redactUri("/foo/bar")); + assertEquals("http://foo/bar?r=xxx", Util.redactUri("http://foo/bar?r=xxx")); assertEquals("http://" + Util.REDACTED_USER_INFO + "@foo/bar?r=xxx", - Util.redactUrl("http://user@foo/bar?r=xxx")); + Util.redactUri("http://user@foo/bar?r=xxx")); assertEquals("http://" + Util.REDACTED_USER_INFO + "@foo/bar?r=xxx", - Util.redactUrl("http://user:pass@foo/bar?r=xxx")); + Util.redactUri("http://user:pass@foo/bar?r=xxx")); } @Test @@ -412,21 +413,6 @@ void testLinkify() throws URISyntaxException, MalformedURLException { assertEquals("ldap://example.com/OpenGrok/OpenGrok", Util.linkify("ldap://example.com/OpenGrok/OpenGrok")); assertEquals("smtp://example.com/OpenGrok/OpenGrok", Util.linkify("smtp://example.com/OpenGrok/OpenGrok")); assertEquals("just some crazy text", Util.linkify("just some crazy text")); - - // escaping url - assertTrue(Util.linkify("http://www.example.com/\"quotation\"/else") - .contains("href=\"" + Util.encodeURL("http://www.example.com/\"quotation\"/else") + "\"")); - assertTrue(Util.linkify("https://example.com/><\"") - .contains("href=\"" + Util.encodeURL("https://example.com/><\"") + "\"")); - assertTrue(Util.linkify("http://www.example.com?param=1¶m2=2¶m3=\"quoted>\"") - .contains("href=\"" + Util.encodeURL("http://www.example.com?param=1¶m2=2¶m3=\"quoted>\"") + "\"")); - // escaping titles - assertTrue(Util.linkify("http://www.example.com/\"quotation\"/else") - .contains("title=\"Link to " + Util.encode("http://www.example.com/\"quotation\"/else") + "\"")); - assertTrue(Util.linkify("https://example.com/><\"") - .contains("title=\"Link to " + Util.encode("https://example.com/><\"") + "\"")); - assertTrue(Util.linkify("http://www.example.com?param=1¶m2=2¶m3=\"quoted>\"") - .contains("title=\"Link to " + Util.encode("http://www.example.com?param=1¶m2=2¶m3=\"quoted>\"") + "\"")); } @Test @@ -456,7 +442,7 @@ void testBuildLink() throws URISyntaxException, MalformedURLException { @Test void testBuildLinkInvalidUrl1() { - assertThrows(MalformedURLException.class, () -> Util.buildLink("link", "www.example.com")); // invalid protocol + assertThrows(IllegalArgumentException.class, () -> Util.buildLink("link", "www.example.com")); // invalid protocol } @Test @@ -490,23 +476,30 @@ void testLinkifyPattern() { + " fugiat nulla pariatur. Excepteur sint " + "occaecat bug6478abc cupidatat non proident, sunt in culpa qui officia " + "deserunt mollit anim id est laborum."; - String expected2 - = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, " - + "sed do eiusmod tempor incididunt as per 12345698 ut labore et dolore magna " - + "aliqua. " - + "bug3333fff" - + " Ut enim ad minim veniam, quis nostrud exercitation " - + "ullamco laboris nisi ut aliquip ex ea introduced in 9791216541 commodo consequat. " - + "Duis aute irure dolor in reprehenderit in voluptate velit " - + "esse cillum dolore eu fixes 132469187 fugiat nulla pariatur. Excepteur sint " - + "occaecat " - + "bug6478abc" - + " cupidatat non proident, sunt in culpa qui officia " - + "deserunt mollit anim id est laborum."; - assertEquals(expected, Util.linkifyPattern(text, Pattern.compile("\\b([0-9]{8,})\\b"), "$1", "http://www.example.com?bug=$1")); - assertEquals(expected2, Util.linkifyPattern(text, Pattern.compile("\\b(bug([0-9]{4})\\w{3})\\b"), "$1", - "http://www.other-example.com?bug=$2")); + assertEquals(expected, Util.linkifyPattern(text, Pattern.compile("\\b([0-9]{8,})\\b"), "http://www.example.com?bug=")); + } + + /** + * Matched pattern should be properly encoded in the resulting HTML. + */ + @Test + void testLinkifyPatternEscape() { + final String text = "foo bug <123456> bar bug 777"; + final String expected = "foo bug <123456> bar " + + "bug 777"; + + assertEquals(expected, + Util.linkifyPattern(text, Pattern.compile("[ \\t]+([0-9<>]{3,})[ \\t]*"), "http://www.example.com?bug=")); + } + + @Test + void testLinkifyPatternNoGroup() { + final String text = "foo bug <123456> bar bug 777"; + + assertEquals(text, + Util.linkifyPattern(text, Pattern.compile("[0-9]{3,}"), "http://www.example.com?bug=")); } @Test @@ -652,7 +645,9 @@ void testWriteHADNonexistentFile() throws Exception { @Test void testWriteHAD() throws Exception { TestRepository repository = new TestRepository(); - repository.create(UtilTest.class.getResource("/repositories")); + URL repositoryURL = UtilTest.class.getResource("/repositories"); + assertNotNull(repositoryURL); + repository.create(repositoryURL); RuntimeEnvironment env = RuntimeEnvironment.getInstance(); diff --git a/opengrok-web/src/main/webapp/WEB-INF/tags/repository.tag b/opengrok-web/src/main/webapp/WEB-INF/tags/repository.tag index 96099813e1a..f3479e45104 100644 --- a/opengrok-web/src/main/webapp/WEB-INF/tags/repository.tag +++ b/opengrok-web/src/main/webapp/WEB-INF/tags/repository.tag @@ -69,7 +69,7 @@ Portions Copyright (c) 2019, Krystof Tulinger . ${Util.htmlize(ObjectUtils.defaultIfNull(repositoryInfo.type, "N/A"))}: - ${Util.linkify(ObjectUtils.defaultIfNull(Util.redactUrl(repositoryInfo.parent), "N/A"))} + ${Util.linkify(ObjectUtils.defaultIfNull(Util.redactUri(repositoryInfo.parent), "N/A"))} (${Util.htmlize(ObjectUtils.defaultIfNull(repositoryInfo.branch, "N/A"))}) diff --git a/opengrok-web/src/main/webapp/history.jsp b/opengrok-web/src/main/webapp/history.jsp index 9f80cff831a..a1c1f04e31d 100644 --- a/opengrok-web/src/main/webapp/history.jsp +++ b/opengrok-web/src/main/webapp/history.jsp @@ -369,10 +369,10 @@ document.domReady.push(function() {domReadyHistory();}); String cout = Util.htmlize(entry.getMessage()); if (bugPage != null && !bugPage.isEmpty() && bugPattern != null) { - cout = Util.linkifyPattern(cout, bugPattern, "$1", Util.completeUrl(bugPage + "$1", request)); + cout = Util.linkifyPattern(cout, bugPattern, Util.completeUrl(bugPage, request)); } if (reviewPage != null && !reviewPage.isEmpty() && reviewPattern != null) { - cout = Util.linkifyPattern(cout, reviewPattern, "$1", Util.completeUrl(reviewPage + "$1", request)); + cout = Util.linkifyPattern(cout, reviewPattern, Util.completeUrl(reviewPage, request)); } boolean showSummary = false; @@ -382,10 +382,10 @@ document.domReady.push(function() {domReadyHistory();}); coutSummary = coutSummary.substring(0, summaryLength - 1); coutSummary = Util.htmlize(coutSummary); if (bugPage != null && !bugPage.isEmpty() && bugPattern != null) { - coutSummary = Util.linkifyPattern(coutSummary, bugPattern, "$1", Util.completeUrl(bugPage + "$1", request)); + coutSummary = Util.linkifyPattern(coutSummary, bugPattern, Util.completeUrl(bugPage, request)); } if (reviewPage != null && !reviewPage.isEmpty() && reviewPattern != null) { - coutSummary = Util.linkifyPattern(coutSummary, reviewPattern, "$1", Util.completeUrl(reviewPage + "$1", request)); + coutSummary = Util.linkifyPattern(coutSummary, reviewPattern, Util.completeUrl(reviewPage, request)); } }