Merge "Update highlightjs to 10.5.0" into stable-3.1
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 1a9a8f6..9f7bd99 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -73,6 +73,7 @@
 * jetty:server
 * jetty:servlet
 * jetty:util
+* jetty:util-ajax
 * log:json-smart
 * log:jsonevent-layout
 * log:log4j
diff --git a/WORKSPACE b/WORKSPACE
index ee10303..f275b9d 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -873,54 +873,61 @@
     sha1 = "7e060dd5b19431e6d198e91ff670644372f60fbd",
 )
 
-JETTY_VERS = "9.4.33.v20201020"
+JETTY_VERS = "9.4.35.v20201120"
 
 maven_jar(
     name = "jetty-servlet",
     artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS,
-    sha1 = "101609e8e5365c4406e4448099459eb605ac551f",
+    sha1 = "3e61bcb471e1bfc545ce866cbbe33c3aedeec9b1",
 )
 
 maven_jar(
     name = "jetty-security",
     artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS,
-    sha1 = "c150bf2aca6cb1636e7195f844a2bb156546e50e",
+    sha1 = "80dc2f422789c78315de76d289b7a5b36c3232d5",
 )
 
 maven_jar(
     name = "jetty-server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS,
-    sha1 = "f586ff2ee048ad2575866c1833d854288f402307",
+    sha1 = "513502352fd689d4730b2935421b990ada8cc818",
 )
 
 maven_jar(
     name = "jetty-jmx",
     artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS,
-    sha1 = "56b723070eeafc51b943cd9bf1a064a037e806a7",
+    sha1 = "38812031940a466d626ab5d9bbbd9d5d39e9f735",
 )
 
 maven_jar(
     name = "jetty-continuation",
     artifact = "org.eclipse.jetty:jetty-continuation:" + JETTY_VERS,
-    sha1 = "f672e58d528fc83060558ab4fc6a797c8137dfcb",
+    sha1 = "09f021e5895471f622ec8f95e28f5815ea7ee192",
 )
 
 maven_jar(
     name = "jetty-http",
     artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS,
-    sha1 = "ad28940f89ffde6ec1bd1656fe3f8493b01ba3c2",
+    sha1 = "45d35131a35a1e76991682174421e8cdf765fb9f",
 )
 
 maven_jar(
     name = "jetty-io",
     artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS,
-    sha1 = "9e4b0048285b71f4769908780f957a470eca11da",
+    sha1 = "eb9460700b99b71ecd82a53697f5ff99f69b9e1c",
 )
 
 maven_jar(
     name = "jetty-util",
     artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS,
-    sha1 = "c88807f210ab216aa831b48569ef50bd797384bc",
+    sha1 = "ef61b83f9715c3b5355b633d9f01d2834f908ece",
+)
+
+maven_jar(
+    name = "jetty-util-ajax",
+    artifact = "org.eclipse.jetty:jetty-util-ajax:" + JETTY_VERS,
+    sha1 = "ebbb43912c6423bedb3458e44aee28eeb4d66f27",
+    src_sha1 = "b3acea974a17493afb125a9dfbe783870ce1d2f9",
 )
 
 maven_jar(
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 988d871..2324465 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -109,7 +109,10 @@
 
     return result.asList().stream()
         .map(a -> formatForException(result, a))
-        .collect(joining("\n", "Account '" + result.input() + "' is ambiguous:\n", ""));
+        .limit(3)
+        .collect(
+            joining(
+                "\n", "Account '" + result.input() + "' is ambiguous (at most 3 shown):\n", ""));
   }
 
   private static String formatForException(Result result, AccountState state) {
diff --git a/javatests/com/google/gerrit/server/account/AccountResolverTest.java b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
index 769370a..5f14d28 100644
--- a/javatests/com/google/gerrit/server/account/AccountResolverTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -269,7 +269,8 @@
             () -> search("foo", searchers, allVisible()).asUnique());
     assertThat(thrown)
         .hasMessageThat()
-        .isEqualTo("Account 'foo' is ambiguous:\n1: Anonymous Name (1)\n2: Anonymous Name (2)");
+        .isEqualTo(
+            "Account 'foo' is ambiguous (at most 3 shown):\n1: Anonymous Name (1)\n2: Anonymous Name (2)");
   }
 
   @Test
@@ -311,7 +312,8 @@
                 .new Result(
                     "foo", ImmutableList.of(newAccount(3), newAccount(1)), ImmutableList.of())))
         .hasMessageThat()
-        .isEqualTo("Account 'foo' is ambiguous:\n1: Anonymous Name (1)\n3: Anonymous Name (3)");
+        .isEqualTo(
+            "Account 'foo' is ambiguous (at most 3 shown):\n1: Anonymous Name (1)\n3: Anonymous Name (3)");
   }
 
   @Test
diff --git a/lib/jetty/BUILD b/lib/jetty/BUILD
index 6417385..86d455f 100644
--- a/lib/jetty/BUILD
+++ b/lib/jetty/BUILD
@@ -4,7 +4,10 @@
     name = "servlet",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
-    exports = ["@jetty-servlet//jar"],
+    exports = [
+        ":util-ajax",
+        "@jetty-servlet//jar",
+    ],
     runtime_deps = [":security"],
 )
 
@@ -69,3 +72,9 @@
     data = ["//lib:LICENSE-Apache2.0"],
     exports = ["@jetty-util//jar"],
 )
+
+java_library(
+    name = "util-ajax",
+    data = ["//lib:LICENSE-Apache2.0"],
+    exports = ["@jetty-util-ajax//jar"],
+)
diff --git a/plugins/replication b/plugins/replication
index 22ca0b4..141f223 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 22ca0b406a4efb9aebbbfcac8d2d986812423f01
+Subproject commit 141f223240b15d3576f2d45525ed12b6ce7bc9bf
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index 0c6f4a3..b159b51 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -163,6 +163,11 @@
      * })|null|!Object}
      */
     _getNormalizedRange(selection) {
+      /* On Safari the ShadowRoot.getSelection() isn't there and the only thing
+         we can get is a single Range */
+      if (selection instanceof Range) {
+        return this._normalizeRange(selection);
+      }
       const rangeCount = selection.rangeCount;
       if (rangeCount === 0) {
         return null;
@@ -323,12 +328,19 @@
     },
 
     _handleSelection(selection, isMouseUp) {
+      /* On Safari, the selection events may return a null range that should
+         be ignored */
+      if (!selection) {
+        return;
+      }
       const normalizedRange = this._getNormalizedRange(selection);
       if (!this._isRangeValid(normalizedRange)) {
         this._removeActionBox();
         return;
       }
-      const domRange = selection.getRangeAt(0);
+      /* On Safari the ShadowRoot.getSelection() isn't there and the only thing
+         we can get is a single Range */
+      const domRange = selection instanceof Range ? selection:selection.getRangeAt(0);
       const start = normalizedRange.start;
       const end = normalizedRange.end;
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 1c36745..f6abb14 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -411,5 +411,7 @@
   </template>
   <script src="gr-diff-line.js"></script>
   <script src="gr-diff-group.js"></script>
+  <!-- gr-diff.js contains an 'import' statement, which is allowed only in modules -->
+  <script src="../../../scripts/shadow.js"></script>
   <script src="gr-diff.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 4f07664..c6b2e3a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -296,10 +296,10 @@
       }
 
       if (loggedIn && isAttached) {
-        this.listen(document, 'selectionchange', '_handleSelectionChange');
+        this.listen(document, '-shadow-selectionchange', '_handleSelectionChange');
         this.listen(document, 'mouseup', '_handleMouseUp');
       } else {
-        this.unlisten(document, 'selectionchange', '_handleSelectionChange');
+        this.unlisten(document, '-shadow-selectionchange', '_handleSelectionChange');
         this.unlisten(document, 'mouseup', '_handleMouseUp');
       }
     },
@@ -328,7 +328,8 @@
       // This takes the shadow DOM selection if one exists.
       return this.root.getSelection ?
         this.root.getSelection() :
-        document.getSelection();
+        // This is coming from shadow.js
+        getRange(this.root);
     },
 
     _observeNodes() {
diff --git a/polygerrit-ui/app/scripts/shadow.js b/polygerrit-ui/app/scripts/shadow.js
new file mode 100644
index 0000000..df43766
--- /dev/null
+++ b/polygerrit-ui/app/scripts/shadow.js
@@ -0,0 +1,421 @@
+/**
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+ /* Downloaded from [1] and adapted to be used as HTML-imported JavaScript
+  * rather than import module.
+  * TODO: to be removed when merging to stable-3.2, where the NPM package
+  * management would allow to actually consume the artifact directly without
+  * adaptation.
+  *
+  * [1] https://raw.githubusercontent.com/GoogleChromeLabs/shadow-selection-polyfill/master/shadow.js
+  */
+
+const debug = false;
+
+const hasShadow = 'attachShadow' in Element.prototype && 'getRootNode' in Element.prototype;
+const hasSelection = !!(hasShadow && document.createElement('div').attachShadow({ mode: 'open' }).getSelection);
+const hasShady = window.ShadyDOM && window.ShadyDOM.inUse;
+const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) ||
+  /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
+const useDocument = !hasShadow || hasShady || (!hasSelection && !isSafari);
+
+const invalidPartialElements = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|script|source|style|template|track|wbr)$/;
+
+const eventName = '-shadow-selectionchange';
+
+
+const validNodeTypes = [Node.ELEMENT_NODE, Node.TEXT_NODE, Node.DOCUMENT_FRAGMENT_NODE];
+function isValidNode(node) {
+  return validNodeTypes.includes(node.nodeType);
+}
+
+
+/**
+ * @param {!Selection} s selection to use
+ * @param {!Node} node to find caret position within a shadow root
+ * @return {!Node|!ShadowRoot}
+ */
+function findCaretFocus(s, node) {
+  const pending = [];
+  const pushAll = (nodeList) => {
+    for (let i = 0; i < nodeList.length; ++i) {
+      if (nodeList[i].shadowRoot) {
+        pending.push(nodeList[i].shadowRoot);
+      }
+    }
+  };
+
+  // We're told by Safari that a node containing a child with a Shadow Root is selected, but check
+  // the node directly too (just in case they change their mind later).
+  if (node.shadowRoot) {
+    pending.push(node.shadowRoot);
+  }
+  pushAll(node.childNodes);
+
+  while (pending.length) {
+    const root = pending.shift();
+
+    for (let i = 0; i < root.childNodes.length; ++i) {
+      if (s.containsNode(root.childNodes[i], true)) {
+        return root;
+      }
+    }
+
+    // The selection must be inside a further Shadow Root, but there's no good way to get a list of
+    // them. Safari won't tell you what regular node contains the root which has a selection. So,
+    // unfortunately if you stack them this will be slow(-ish).
+    pushAll(root.querySelectorAll('*'));
+  }
+
+  return null;
+}
+
+
+function findNode(s, parentNode, isLeft) {
+  const nodes = parentNode.childNodes || parentNode.children;
+  if (!nodes) {
+    return parentNode;  // found it, probably text
+  }
+
+  for (let i = 0; i < nodes.length; ++i) {
+    const j = isLeft ? i : (nodes.length - 1 - i);
+    const childNode = nodes[j];
+    if (!isValidNode(childNode)) {
+      continue;
+    }
+
+    debug && console.debug('checking child', childNode, 'IsLeft', isLeft);
+    if (s.containsNode(childNode, true)) {
+      if (s.containsNode(childNode, false)) {
+        debug && console.info('found child', childNode);
+        return childNode;
+      }
+      // Special-case elements that cannot have feasible children.
+      if (!invalidPartialElements.exec(childNode.localName || '')) {
+        debug && console.info('descending child', childNode);
+        return findNode(s, childNode, isLeft);
+      }
+    }
+    debug && console.info(parentNode, 'does NOT contain', childNode);
+  }
+  return parentNode;
+}
+
+
+let recentCaretRange = {node: null, offset: -1};
+
+
+(function() {
+  if (hasSelection || useDocument) {
+    // getSelection exists or document API can be used
+    document.addEventListener('selectionchange', (ev) => {
+      document.dispatchEvent(new CustomEvent(eventName));
+    });
+    return () => {};
+  }
+
+  let withinInternals = false;
+
+  document.addEventListener('selectionchange', (ev) => {
+    if (withinInternals) {
+      return;
+    }
+
+    withinInternals = true;
+
+    const s = window.getSelection();
+    if (s.type === 'Caret') {
+      const root = findCaretFocus(s, s.anchorNode);
+      if (root instanceof window.ShadowRoot) {
+        const range = getRange(root);
+        if (range) {
+          const node = range.startContainer;
+          const offset = range.startOffset;
+          recentCaretRange = {node, offset};
+        }
+      }
+    }
+
+    document.dispatchEvent(new CustomEvent('-shadow-selectionchange'));
+    window.requestAnimationFrame(() => {
+      withinInternals = false;
+    });
+  });
+})();
+
+
+/**
+ * @param {!Selection} s the window selection to use
+ * @param {!Node} node the node to walk from
+ * @param {boolean} walkForward should this walk in natural direction
+ * @return {boolean} whether the selection contains the following node (even partially)
+ */
+function containsNextElement(s, node, walkForward) {
+  const start = node;
+  while (node = walkFromNode(node, walkForward)) {
+    // walking (left) can contain our own parent, which we don't want
+    if (!node.contains(start)) {
+      break;
+    }
+  }
+  if (!node) {
+    return false;
+  }
+  // we look for Element as .containsNode says true for _every_ text node, and we only care about
+  // elements themselves
+  return node instanceof Element && s.containsNode(node, true);
+}
+
+
+/**
+ * @param {!Selection} s the window selection to use
+ * @param {!Node} leftNode the left node
+ * @param {!Node} rightNode the right node
+ * @return {boolean|undefined} whether this has natural direction
+ */
+function getSelectionDirection(s, leftNode, rightNode) {
+  if (s.type !== 'Range') {
+    return undefined;  // no direction
+  }
+  const measure = () => s.toString().length;
+
+  const initialSize = measure();
+  debug && console.info(`initial selection: "${s.toString()}"`)
+
+  let updatedSize;
+
+  // Try extending forward and seeing what happens.
+  s.modify('extend', 'forward', 'character');
+  updatedSize = measure();
+  debug && console.info(`forward selection: "${s.toString()}"`)
+
+  if (updatedSize > initialSize || containsNextElement(s, rightNode, true)) {
+    debug && console.info('got forward >, moving right')
+    s.modify('extend', 'backward', 'character');
+    return true;
+  } else if (updatedSize < initialSize || !s.containsNode(leftNode)) {
+    debug && console.info('got forward <, moving left')
+    s.modify('extend', 'backward', 'character');
+    return false;
+  }
+
+  // Maybe we were at the end of something. Extend backwards instead.
+  s.modify('extend', 'backward', 'character');
+  updatedSize = measure();
+  debug && console.info(`backward selection: "${s.toString()}"`)
+
+  if (updatedSize > initialSize || containsNextElement(s, leftNode, false)) {
+    debug && console.info('got backwards >, moving left')
+    s.modify('extend', 'forward', 'character');
+    return false;
+  } else if (updatedSize < initialSize || !s.containsNode(rightNode)) {
+    debug && console.info('got backwards <, moving right')
+    s.modify('extend', 'forward', 'character');
+    return true;
+  }
+
+  // This is likely a select-all.
+  return undefined;
+}
+
+/**
+ * Returns the next valid node (element or text). This is needed as Safari doesn't support
+ * TreeWalker inside Shadow DOM. Don't escape shadow roots.
+ *
+ * @param {!Node} node to start from
+ * @param {boolean} walkForward should this walk in natural direction
+ * @return {Node} node found, if any
+ */
+function walkFromNode(node, walkForward) {
+  if (!walkForward) {
+    return node.previousSibling || node.parentNode || null;
+  }
+  while (node) {
+    if (node.nextSibling) {
+      return node.nextSibling;
+    }
+    node = node.parentNode;
+  }
+  return null;
+}
+
+
+const cachedRange = new Map();
+function getRange(root) {
+  if (hasShady) {
+    const s = document.getSelection();
+    return s.rangeCount ? s.getRangeAt(0) : null;
+  } else if (useDocument) {
+    // Document pierces Shadow Root for selection, so actively filter it down to the right node.
+    // This is only for Firefox, which does not allow selection across Shadow Root boundaries.
+    const s = document.getSelection();
+    if (s.containsNode(root, true)) {
+      return s.getRangeAt(0);
+    }
+    return null;
+  } else if (hasSelection) {
+    const s = root.getSelection();
+    return s.rangeCount ? s.getRangeAt(0) : null;
+  }
+
+  const thisFrame = cachedRange.get(root);
+  if (thisFrame) {
+    return thisFrame;
+  }
+
+  const result = internalGetShadowSelection(root);
+
+  cachedRange.set(root, result.range);
+  window.setTimeout(() => {
+    cachedRange.delete(root);
+  }, 0);
+  debug && console.debug('getRange got', result);
+  return result.range;
+}
+
+
+function internalGetShadowSelection(root) {
+  // nb. We used to check whether the selection contained the host, but this broke in Safari 13.
+  // This is "nicely formatted" whitespace as per the browser's renderer. This is fine, and we only
+  // provide selection information at this granularity.
+  const s = window.getSelection();
+
+  if (s.type === 'None') {
+    return {range: null, type: 'none'};
+  } else if (!(s.type === 'Caret' || s.type === 'Range')) {
+    throw new TypeError('unexpected type: ' + s.type);
+  }
+
+  const leftNode = findNode(s, root, true);
+  if (leftNode === root) {
+    return {range: null, mode: 'none'};
+  }
+
+  const range = document.createRange();
+
+  let rightNode = null;
+  let isNaturalDirection = undefined;
+  if (s.type === 'Range') {
+    rightNode = findNode(s, root, false);  // get right node here _before_ getSelectionDirection
+    isNaturalDirection = getSelectionDirection(s, leftNode, rightNode);
+
+    // isNaturalDirection means "going right"
+
+    if (isNaturalDirection === undefined) {
+      // This occurs when we can't move because we can't extend left or right to measure the
+      // direction we're moving in... because it's the entire range. Hooray!
+      range.setStart(leftNode, 0);
+      range.setEnd(rightNode, rightNode.length);
+      return {range, mode: 'all'};
+    }
+  }
+
+  const initialSize = s.toString().length;
+
+  // Dumbest possible approach: remove characters from left side until no more selection,
+  // re-add.
+
+  // Try right side first, as we can trim characters until selection gets shorter.
+
+  let leftOffset = 0;
+  let rightOffset = 0;
+
+  if (rightNode === null) {
+    // This is a caret selection, do nothing.
+  } else if (rightNode.nodeType === Node.TEXT_NODE) {
+    const rightText = rightNode.textContent;
+    const existingNextSibling = rightNode.nextSibling;
+
+    for (let i = rightText.length - 1; i >= 0; --i) {
+      rightNode.splitText(i);
+      const updatedSize = s.toString().length;
+      if (updatedSize !== initialSize) {
+        rightOffset = i + 1;
+        break;
+      }
+    }
+
+    // We don't use .normalize() here, as the user might already have a weird node arrangement
+    // they need to maintain.
+    rightNode.insertData(rightNode.length, rightText.substr(rightNode.length));
+    while (rightNode.nextSibling !== existingNextSibling) {
+      rightNode.nextSibling.remove();
+    }
+  }
+
+  if (leftNode.nodeType === Node.TEXT_NODE) {
+    if (leftNode !== rightNode) {
+      // If we're at the end of a text node, it's impossible to extend the selection, so add an
+      // extra character to select (that we delete later).
+      leftNode.appendData('?');
+      s.collapseToStart();
+      s.modify('extend', 'right', 'character');
+    }
+
+    const leftText = leftNode.textContent;
+    const existingNextSibling = leftNode.nextSibling;
+
+    const start = (leftNode === rightNode ? rightOffset : leftText.length - 1);
+
+    for (let i = start; i >= 0; --i) {
+      leftNode.splitText(i);
+      if (s.toString() === '') {
+        leftOffset = i;
+        break;
+      }
+    }
+
+    // As above, we don't want to use .normalize().
+    leftNode.insertData(leftNode.length, leftText.substr(leftNode.length));
+    while (leftNode.nextSibling !== existingNextSibling) {
+      leftNode.nextSibling.remove();
+    }
+
+    if (leftNode !== rightNode) {
+      leftNode.deleteData(leftNode.length - 1, 1);
+    }
+
+    if (rightNode === null) {
+      rightNode = leftNode;
+      rightOffset = leftOffset;
+    }
+
+  } else if (rightNode === null) {
+    rightNode = leftNode;
+  }
+
+  // Work around common browser bug. Single character selction is always seen as 'forward'. Check
+  // if it's actually supposed to be backward.
+  if (initialSize === 1 && recentCaretRange && recentCaretRange.node === leftNode) {
+    if (recentCaretRange.offset > leftOffset && isNaturalDirection) {
+      isNaturalDirection = false;
+    }
+  }
+
+  if (isNaturalDirection === true) {
+    s.collapse(leftNode, leftOffset);
+    s.extend(rightNode, rightOffset);
+  } else if (isNaturalDirection === false) {
+    s.collapse(rightNode, rightOffset);
+    s.extend(leftNode, leftOffset);
+  } else {
+    s.setPosition(leftNode, leftOffset);
+  }
+
+  range.setStart(leftNode, leftOffset);
+  range.setEnd(rightNode, rightOffset);
+  return {range, mode: 'normal'};
+}