Merge "Change check fakes to properly respond to feedback"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 9ace97d3..139d6f8 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -535,6 +535,13 @@
 +
 By default, `false`.
 
+[[auth.cookieHttpOnly]]auth.cookieHttpOnly::
++
+Sets "httpOnly" flag of the authentication cookie. If `true`, cookie
+values can't be accessed by client side scripts.
++
+By default, `false`.
+
 [[auth.emailFormat]]auth.emailFormat::
 +
 Optional format string to construct user email addresses out of
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index c6e5623..4e60a30 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6985,11 +6985,11 @@
 |=================================
 |Field Name          ||Description
 |`patch`             |required|
-|`allow_conflicts`    |optional|
-If true, tolerate conflicts and add conflict markers where required.
 The patch to be applied. Must be compatible with `git diff` output.
 For example, link:#get-patch[Get Patch] output.
 The patch must be provided as UTF-8 text, either directly or base64-encoded.
+|`allow_conflicts`    |optional|
+If true, tolerate conflicts and add conflict markers where required.
 |=================================
 
 [[applypatchpatchset-input]]
diff --git a/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 9625039..a92d4f0 100644
--- a/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -265,10 +265,10 @@
       outCookie.setDomain(domain);
     }
 
-    outCookie.setSecure(isSecure(request));
     outCookie.setPath(path);
     outCookie.setMaxAge(ageSeconds);
-    outCookie.setSecure(authConfig.getCookieSecure());
+    outCookie.setSecure(authConfig.getCookieSecure() && isSecure(request));
+    outCookie.setHttpOnly(authConfig.getCookieHttpOnly());
     response.addCookie(outCookie);
   }
 
diff --git a/java/com/google/gerrit/httpd/XsrfCookieFilter.java b/java/com/google/gerrit/httpd/XsrfCookieFilter.java
index 079efa4..1ec2649 100644
--- a/java/com/google/gerrit/httpd/XsrfCookieFilter.java
+++ b/java/com/google/gerrit/httpd/XsrfCookieFilter.java
@@ -65,6 +65,7 @@
     Cookie c = new Cookie(XsrfConstants.XSRF_COOKIE_NAME, nullToEmpty(v));
     c.setPath("/");
     c.setSecure(authConfig.getCookieSecure() && isSecure(req));
+    c.setHttpOnly(authConfig.getCookieHttpOnly());
     c.setMaxAge(
         v != null
             ? -1 // Set the cookie for this browser session.
diff --git a/java/com/google/gerrit/server/config/AuthConfig.java b/java/com/google/gerrit/server/config/AuthConfig.java
index b6ffcee..e180428 100644
--- a/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/java/com/google/gerrit/server/config/AuthConfig.java
@@ -62,6 +62,7 @@
   private final String cookiePath;
   private final String cookieDomain;
   private final boolean cookieSecure;
+  private final boolean cookieHttpOnly;
   private final SignedToken emailReg;
   private final boolean allowRegisterNewEmail;
   private final boolean userNameCaseInsensitive;
@@ -91,6 +92,7 @@
     cookiePath = cfg.getString("auth", null, "cookiepath");
     cookieDomain = cfg.getString("auth", null, "cookiedomain");
     cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
+    cookieHttpOnly = cfg.getBoolean("auth", "cookiehttponly", false);
     trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
     enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
     gitBasicAuthPolicy = getBasicAuthPolicy(cfg);
@@ -218,6 +220,10 @@
     return cookieSecure;
   }
 
+  public boolean getCookieHttpOnly() {
+    return cookieHttpOnly;
+  }
+
   public SignedToken getEmailRegistrationToken() {
     return emailReg;
   }
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 06719ab..783adf7 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 06719abee6c38d17008599f074050db5153bffa3
+Subproject commit 783adf7ddf19924d054a1596eec6dd3da9f4aafe
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts b/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts
index c13ef70..e91d9c9 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts
@@ -9,6 +9,7 @@
 import {getAppContext} from '../../services/app-context';
 import {EDIT, BasePatchSetNum, RepoName} from '../../types/common';
 import {anyLineTooLong} from '../../utils/diff-util';
+import {Timing} from '../../constants/reporting';
 import {
   DiffInfo,
   DiffLayer,
@@ -89,6 +90,8 @@
     hide_line_length_indicator: true,
   };
 
+  private readonly reporting = getAppContext().reportingService;
+
   private readonly restApiService = getAppContext().restApiService;
 
   private readonly getChangeModel = resolve(this, changeModelToken);
@@ -265,12 +268,17 @@
     if (!changeNum || !basePatchNum || !this.fixSuggestionInfo) return;
 
     this.applyingFix = true;
+    this.reporting.time(Timing.APPLY_FIX_LOAD);
     const res = await this.restApiService.applyFixSuggestion(
       changeNum,
       basePatchNum,
       this.fixSuggestionInfo.replacements
     );
     this.applyingFix = false;
+    this.reporting.timeEnd(Timing.APPLY_FIX_LOAD, {
+      method: '1-click',
+      description: this.fixSuggestionInfo.description,
+    });
     if (res?.ok) this.navigateToEditPatchset();
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index b57e4ff..d183211 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -126,7 +126,7 @@
         this.repoCommentLinks = repoCommentLinks;
         // Always linkify URLs starting with https?://
         this.repoCommentLinks['ALWAYS_LINK_HTTP'] = {
-          match: '(https?://((?!&(gt|lt|amp|quot|apos);)\\S)+[\\w/~-])',
+          match: '(https?://((?!&(gt|lt|quot|apos);)\\S)+[\\w/~-])',
           link: '$1',
           enabled: true,
         };
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index 23f1594..c1b38d5 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -235,6 +235,11 @@
       await checkLinking('https://www.google.com/');
       await checkLinking('https://www.google.com/asdf~');
       await checkLinking('https://www.google.com/asdf-');
+      await checkLinking('https://www.google.com/asdf-');
+      // matches & part as well, even we first linkify and then htmlEscape
+      await checkLinking(
+        'https://google.com/traces/list?project=gerrit&tid=123'
+      );
     });
   });
 
@@ -710,6 +715,10 @@
       await checkLinking('http://www.google.com');
       await checkLinking('https://www.google.com');
       await checkLinking('https://www.google.com/');
+      // matches & part as well, even we first linkify and then htmlEscape
+      await checkLinking(
+        'https://google.com/traces/list?project=gerrit&tid=123'
+      );
     });
 
     suite('user suggest fix', () => {
diff --git a/polygerrit-ui/app/embed/gr-textarea.ts b/polygerrit-ui/app/embed/gr-textarea.ts
index 05bef49..27948fd 100644
--- a/polygerrit-ui/app/embed/gr-textarea.ts
+++ b/polygerrit-ui/app/embed/gr-textarea.ts
@@ -173,6 +173,8 @@
 
   private focused = false;
 
+  private currentCursorPosition = -1;
+
   private readonly isPlaintextOnlySupported = supportsPlainTextEditing();
 
   static override get styles() {
@@ -488,6 +490,19 @@
       event.preventDefault();
       this.fire('saveShortcut');
     }
+    // Prevent looping of cursor position when CTRL+ARROW_LEFT/ARROW_RIGHT is
+    // pressed.
+    if (event.ctrlKey || event.metaKey || event.altKey) {
+      if (event.key === 'ArrowLeft' && this.currentCursorPosition === 0) {
+        event.preventDefault();
+      }
+      if (
+        event.key === 'ArrowRight' &&
+        this.currentCursorPosition === (this.value?.length ?? 0)
+      ) {
+        event.preventDefault();
+      }
+    }
     await this.toggleHintVisibilityIfAny();
   }
 
@@ -597,7 +612,9 @@
   }
 
   private onCursorPositionChange() {
-    this.fire('cursorPositionChange', {position: this.getCursorPosition()});
+    const cursorPosition = this.getCursorPosition();
+    this.fire('cursorPositionChange', {position: cursorPosition});
+    this.currentCursorPosition = cursorPosition;
   }
 
   private async updateValueInDom() {
diff --git a/polygerrit-ui/app/embed/gr-textarea_test.ts b/polygerrit-ui/app/embed/gr-textarea_test.ts
index b701dcb..d125d2f 100644
--- a/polygerrit-ui/app/embed/gr-textarea_test.ts
+++ b/polygerrit-ui/app/embed/gr-textarea_test.ts
@@ -232,4 +232,56 @@
 
     assert.equal(element.value, oldValue + hint);
   });
+
+  test('when cursor is at end, Mod + ArrowRight does not change cursor position', async () => {
+    const CURSOR_POSITION_CHANGE_EVENT = 'cursorPositionChange';
+    let cursorPosition = -1;
+    const value = 'Hola amigos';
+    const editableDiv = element.shadowRoot!.querySelector(
+      '.editableDiv'
+    ) as HTMLDivElement;
+    element.addEventListener(CURSOR_POSITION_CHANGE_EVENT, (event: Event) => {
+      const detail = (event as CustomEvent<CursorPositionChangeEventDetail>)
+        .detail;
+      cursorPosition = detail.position;
+    });
+    await element.updateComplete;
+    element.value = value;
+    await element.putCursorAtEnd();
+    await element.updateComplete;
+
+    editableDiv.dispatchEvent(
+      new KeyboardEvent('keydown', {key: 'ArrowRight', metaKey: true})
+    );
+    await element.updateComplete;
+    await rafPromise();
+
+    assert.equal(cursorPosition, value.length);
+  });
+
+  test('when cursor is at 0, Mod + ArrowLeft does not change cursor position', async () => {
+    const CURSOR_POSITION_CHANGE_EVENT = 'cursorPositionChange';
+    let cursorPosition = -1;
+    const value = 'Hola amigos';
+    const editableDiv = element.shadowRoot!.querySelector(
+      '.editableDiv'
+    ) as HTMLDivElement;
+    element.addEventListener(CURSOR_POSITION_CHANGE_EVENT, (event: Event) => {
+      const detail = (event as CustomEvent<CursorPositionChangeEventDetail>)
+        .detail;
+      cursorPosition = detail.position;
+    });
+    await element.updateComplete;
+    element.value = value;
+    element.setCursorPosition(0);
+    await element.updateComplete;
+
+    editableDiv.dispatchEvent(
+      new KeyboardEvent('keydown', {key: 'ArrowLeft', metaKey: true})
+    );
+    await element.updateComplete;
+    await rafPromise();
+
+    assert.equal(cursorPosition, 0);
+  });
 });