Merge branch 'main' into avm99963-monorail

Merged commit 3779da353b36d43cf778e7d4f468097714dd4540

GitOrigin-RevId: 6451a5c6b75afb0fd1f37b3f14521148d0722ea8
diff --git a/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-issue.js b/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-issue.js
index d9cec5e..248c7d5 100644
--- a/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-issue.js
+++ b/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-issue.js
@@ -12,7 +12,7 @@
 import {arrayToEnglish} from 'shared/helpers.js';
 import './mr-edit-metadata.js';
 import 'shared/typedef.js';
-
+import {migratedTypes} from 'shared/issue-fields.js';
 import ClientLogger from 'monitoring/client-logger.js';
 
 const DEBOUNCED_PRESUBMIT_TIME_OUT = 400;
@@ -45,11 +45,7 @@
             class="warning-icon material-icons"
             icon="warning"
           >warning</i>
-          <p>
-            This issue has moved to
-            ${this._migratedLink}. Updates should be posted in
-            ${this._migratedLink}.
-          </p>
+          ${this._migratedLink}
         </div>
         <chops-button
           class="legacy-edit"
@@ -130,6 +126,12 @@
         type: String,
       },
       /**
+       * Type of the issue migrated to.
+       */
+       migratedType: {
+        type: migratedTypes,
+      },
+      /**
        * All comments, including descriptions.
        */
       comments: {
@@ -211,7 +213,7 @@
   /** @override */
   stateChanged(state) {
     this.migratedId = issueV0.migratedId(state);
-
+    this.migratedType = issueV0.migratedType(state);
     this.issue = issueV0.viewedIssue(state);
     this.issueRef = issueV0.viewedIssueRef(state);
     this.comments = issueV0.comments(state);
@@ -347,10 +349,16 @@
   }
 
   /**
-   * @return {string} the link of the issue in Issue Tracker.
+   * @return {string} the link of the issue in Issue Tracker or Launch.
    */
-  get _migratedLink() {
-    return html`<a href="https://issuetracker.google.com/issues/${this.migratedId}">b/${this.migratedId}</a>`;
+   get _migratedLink() {
+    if (this.migratedType === migratedTypes.BUGANIZER_TYPE) {
+      const link = 
+        html`<a href="https://issuetracker.google.com/issues/${this.migratedId}">b/${this.migratedId}</a>`;
+      return html`<p>This issue has moved to ${link}. Updates should be posted in ${link}.</p>`;
+    } else {
+      return html`<p>This issue has been migrated to Launch, see link in final comment below.</p>`;
+    }
   }
 
   /**
diff --git a/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-issue.test.js b/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-issue.test.js
index 880064b..e781328 100644
--- a/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-issue.test.js
+++ b/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-issue.test.js
@@ -7,6 +7,7 @@
 import {prpcClient} from 'prpc-client-instance.js';
 import {MrEditIssue, allowRemovedRestrictions} from './mr-edit-issue.js';
 import {clientLoggerFake} from 'shared/test/fakes.js';
+import {migratedTypes} from 'shared/issue-fields.js';
 
 let element;
 let clock;
@@ -308,16 +309,34 @@
 
     it('shows notice if issue migrated', async () => {
       element.migratedId = '1234';
-
+      element.migratedType = migratedTypes.LAUNCH_TYPE
       await element.updateComplete;
 
       assert.isNotNull(element.querySelector('.migrated-banner'));
       assert.isNotNull(element.querySelector('.legacy-edit'));
     });
 
+    it('shows buganizer link when migrated to buganizer', async () => {
+      element.migratedId = '1234';
+      element.migratedType = migratedTypes.BUGANIZER_TYPE
+      await element.updateComplete;
+
+      const link = element.querySelector('.migrated-banner a');
+      assert.include(link.textContent, 'b/1234');
+    });
+
+    it('shows launch banner when migrated to launch', async () => {
+      element.migratedId = '1234';
+      element.migratedType = migratedTypes.LAUNCH_TYPE
+      await element.updateComplete;
+
+      const link = element.querySelector('.migrated-banner');
+      assert.include(link.textContent, 'This issue has been migrated to Launch, see link in final comment below');
+    });
+
     it('hides edit form if issue migrated', async () => {
       element.migratedId = '1234';
-
+      element.migratedType = migratedTypes.LAUNCH_TYPE
       await element.updateComplete;
 
       const editForm = element.querySelector('mr-edit-metadata');
@@ -326,7 +345,7 @@
 
     it('unhides edit form on button click', async () => {
       element.migratedId = '1234';
-
+      element.migratedType = migratedTypes.LAUNCH_TYPE
       await element.updateComplete;
 
       const button = element.querySelector('.legacy-edit');
diff --git a/static_src/elements/issue-detail/mr-issue-page/mr-migrated-banner.js b/static_src/elements/issue-detail/mr-issue-page/mr-migrated-banner.js
index e27a5fe..fe4ba3c 100644
--- a/static_src/elements/issue-detail/mr-issue-page/mr-migrated-banner.js
+++ b/static_src/elements/issue-detail/mr-issue-page/mr-migrated-banner.js
@@ -7,7 +7,7 @@
 import {connectStore} from 'reducers/base.js';
 import * as issueV0 from 'reducers/issueV0.js';
 import {SHARED_STYLES} from 'shared/shared-styles.js';
-
+import {migratedTypes} from 'shared/issue-fields.js';
 
 /**
  * `<mr-migrated-banner>`
@@ -57,10 +57,7 @@
         class="warning-icon material-icons"
         icon="warning"
       >warning</i>
-      <p>
-        This issue has been migrated to ${this._link}. Please see
-        ${this._link} for the latest version of this discussion.
-      </p>
+      ${this._link}
     `;
   }
 
@@ -72,6 +69,7 @@
         type: Boolean,
         reflect: true,
       },
+      migratedType: {type: migratedTypes}
     };
   }
 
@@ -85,6 +83,7 @@
   /** @override */
   stateChanged(state) {
     this.migratedId = issueV0.migratedId(state);
+    this.migratedType = issueV0.migratedType(state);
   }
 
    /** @override */
@@ -100,7 +99,13 @@
    * @return {string} the link of the issue in Issue Tracker.
    */
   get _link() {
-    return html`<a href="https://issuetracker.google.com/issues/${this.migratedId}">b/${this.migratedId}</a>`;
+    if (this.migratedType === migratedTypes.BUGANIZER_TYPE) {
+      const link = 
+        html`<a href="https://issuetracker.google.com/issues/${this.migratedId}">b/${this.migratedId}</a>`;
+      return html`<p>This issue has moved to ${link}. Updates should be posted in ${link}.</p>`;
+    } else {
+      return html`<p>This issue has been migrated to Launch, see link in final comment below.</p>`;
+    }
   }
 }
 
diff --git a/static_src/elements/issue-detail/mr-issue-page/mr-migrated-banner.test.js b/static_src/elements/issue-detail/mr-issue-page/mr-migrated-banner.test.js
index 4cceb2b..2114b61 100644
--- a/static_src/elements/issue-detail/mr-issue-page/mr-migrated-banner.test.js
+++ b/static_src/elements/issue-detail/mr-issue-page/mr-migrated-banner.test.js
@@ -4,6 +4,7 @@
 
 import {assert} from 'chai';
 import {MrMigratedBanner} from './mr-migrated-banner.js';
+import {migratedTypes} from 'shared/issue-fields.js';
 
 let element;
 
@@ -34,10 +35,29 @@
     assert.isTrue(element.hasAttribute('hidden'));
   });
 
-  it('shows element when migratedId is set', async () => {
+  it('shows element when migratedId and migratedType is set', async () => {
     element.migratedId = '1234';
+    element.migratedType = migratedTypes.BUGANIZER_TYPE
     await element.updateComplete;
 
     assert.isFalse(element.hasAttribute('hidden'));
   });
-});
+
+  it('shows bugnizer link when migrate to bugnizer', async () => {
+    element.migratedId = '1234';
+    element.migratedType = migratedTypes.BUGANIZER_TYPE
+    await element.updateComplete;
+
+    const link = element.shadowRoot.querySelector('a');
+    assert.include(link.textContent, 'b/1234');
+  });
+
+  it('shows launch link when migrate to launch', async () => {
+    element.migratedId = '1234';
+    element.migratedType = migratedTypes.LAUNCH_TYPE
+    await element.updateComplete;
+
+    const link = element.shadowRoot.querySelector('p');
+    assert.include(link.textContent, 'This issue has been migrated to Launch, see link in final comment below');
+  });
+});
\ No newline at end of file
diff --git a/static_src/reducers/issueV0.js b/static_src/reducers/issueV0.js
index 880ebbc..8f670c9 100644
--- a/static_src/reducers/issueV0.js
+++ b/static_src/reducers/issueV0.js
@@ -14,7 +14,7 @@
 import {createSelector} from 'reselect';
 import {autolink} from 'autolink.js';
 import {fieldTypes, extractTypeForIssue,
-  fieldValuesToMap} from 'shared/issue-fields.js';
+  fieldValuesToMap, migratedTypes} from 'shared/issue-fields.js';
 import {removePrefix, objectToMap} from 'shared/helpers.js';
 import {issueRefToString, issueToIssueRefString,
   issueStringToRef, issueNameToRefString} from 'shared/convertersV0.js';
@@ -493,7 +493,9 @@
 const RESTRICT_VIEW_PREFIX = 'restrict-view-';
 const RESTRICT_EDIT_PREFIX = 'restrict-editissue-';
 const RESTRICT_COMMENT_PREFIX = 'restrict-addissuecomment-';
-const MIGRATED_ISSUE_PREFIX = 'migrated-to-b-';
+const MIGRATED_ISSUE_PREFIX = 'migrated-to-';
+const MIGRATED_BUGANIZER_ISSUE_PREFIX = 'migrated-to-b-';
+const MIGRATED_LAUNCH_ISSUE_PREFIX = 'migrated-to-launch-';
 
 /**
  * Selector to retrieve all normalized Issue data in the Redux store,
@@ -704,22 +706,49 @@
     },
 );
 
-// Gets the Issue Tracker ID of a moved issue.
+// Gets the Issue Tracker or Launch ID of a moved issue.
 export const migratedId = createSelector(
   labelRefs,
   (labelRefs) => {
     if (!labelRefs) return '';
 
-    // Assume that there's only one migrated-to-b-* label. Or at least drop any
+    // Assume that there's only one migrated-to-* label. Or at least drop any
+    // labels besides the first one.
+    const migrationLabel = labelRefs.find((labelRef) => {
+      return labelRef.label.toLowerCase().startsWith(MIGRATED_ISSUE_PREFIX);
+    });
+    
+    if (migrationLabel) {
+      if (migrationLabel.label.toLowerCase().startsWith(MIGRATED_BUGANIZER_ISSUE_PREFIX)) {
+        return migrationLabel.label.substring(MIGRATED_BUGANIZER_ISSUE_PREFIX.length);
+      } else if (migrationLabel.label.toLowerCase().startsWith(MIGRATED_LAUNCH_ISSUE_PREFIX)) {
+        return migrationLabel.label.substring(MIGRATED_LAUNCH_ISSUE_PREFIX.length);
+      }
+    }
+    return '';
+  },
+);
+
+// Gets the Issue Migrated Type of a moved issue.
+export const migratedType = createSelector(
+  labelRefs,
+  (labelRefs) => {
+    if (!labelRefs) return migratedTypes.NONE;
+
+    // Assume that there's only one migrated-to-* label. Or at least drop any
     // labels besides the first one.
     const migrationLabel = labelRefs.find((labelRef) => {
       return labelRef.label.toLowerCase().startsWith(MIGRATED_ISSUE_PREFIX);
     });
 
     if (migrationLabel) {
-      return migrationLabel.label.substring(MIGRATED_ISSUE_PREFIX.length);
+      if (migrationLabel.label.toLowerCase().startsWith(MIGRATED_BUGANIZER_ISSUE_PREFIX)) {
+        return migratedTypes.BUGANIZER_TYPE;
+      } else if (migrationLabel.label.toLowerCase().startsWith(MIGRATED_LAUNCH_ISSUE_PREFIX)) {
+        return migratedTypes.LAUNCH_TYPE;
+      }
     }
-    return '';
+    return migratedTypes.NONE;
   },
 );
 
diff --git a/static_src/reducers/issueV0.test.js b/static_src/reducers/issueV0.test.js
index 33b63c1..b79cdb5 100644
--- a/static_src/reducers/issueV0.test.js
+++ b/static_src/reducers/issueV0.test.js
@@ -12,6 +12,7 @@
 import {issueToIssueRef, issueRefToString} from 'shared/convertersV0.js';
 import {prpcClient} from 'prpc-client-instance.js';
 import {getSigninInstance} from 'shared/gapi-loader.js';
+import {migratedTypes} from 'shared/issue-fields.js';
 
 let prpcCall;
 let dispatch;
@@ -323,6 +324,48 @@
       {label: 'migrated-to-b-1234'},
       {label: 'migrated-to-b-6789'},
     ]})), '1234');
+
+    assert.equal(issueV0.migratedId(wrapIssue({labelRefs: [
+      {label: 'IgnoreThis'},
+      {label: 'IgnoreThis2'},
+      {label: 'migrated-to-launch-6789'},
+    ]})), '6789');
+
+    assert.equal(issueV0.migratedId(wrapIssue({labelRefs: [
+      {label: 'migrated-to-launch-1234'},
+    ]})), '1234');
+
+    // We assume there's only one migrated-to-* label.
+    assert.equal(issueV0.migratedId(wrapIssue({labelRefs: [
+      {label: 'migrated-to-launch-1234'},
+      {label: 'migrated-to-b-6789'},
+    ]})), '1234');
+  });
+
+  it('migratedType', () => {
+    assert.equal(issueV0.migratedType(wrapIssue()), migratedTypes.NONE);
+    assert.equal(issueV0.migratedType(wrapIssue({labelRefs: []})), migratedTypes.NONE);
+
+    assert.equal(issueV0.migratedType(wrapIssue({labelRefs: [
+      {label: 'IgnoreThis'},
+      {label: 'IgnoreThis2'},
+    ]})), migratedTypes.NONE);
+
+    assert.equal(issueV0.migratedType(wrapIssue({labelRefs: [
+      {label: 'IgnoreThis'},
+      {label: 'IgnoreThis2'},
+      {label: 'migrated-to-b-6789'},
+    ]})), migratedTypes.BUGANIZER_TYPE);
+
+    assert.equal(issueV0.migratedType(wrapIssue({labelRefs: [
+      {label: 'migrated-to-launch-1234'},
+    ]})), migratedTypes.LAUNCH_TYPE);
+
+    // We assume there's only one migrated-to-b-* label.
+    assert.equal(issueV0.migratedType(wrapIssue({labelRefs: [
+      {label: 'migrated-to-launch-1234'},
+      {label: 'migrated-to-b-6789'},
+    ]})), migratedTypes.LAUNCH_TYPE);
   });
 
 
diff --git a/static_src/shared/issue-fields.js b/static_src/shared/issue-fields.js
index 09ac7d3..0acbe60 100644
--- a/static_src/shared/issue-fields.js
+++ b/static_src/shared/issue-fields.js
@@ -37,6 +37,13 @@
   PROJECT_TYPE: 'PROJECT_TYPE',
 });
 
+/** @enum {string} */
+export const migratedTypes = Object.freeze({
+  BUGANIZER_TYPE: 'BUGANIZER_TYPE',
+  LAUNCH_TYPE: 'LAUNCH_TYPE',
+  NONE: 'NONE',
+});
+
 const GROUPABLE_FIELD_TYPES = new Set([
   fieldTypes.DATE_TYPE,
   fieldTypes.ENUM_TYPE,
diff --git a/static_src/shared/md-helper.js b/static_src/shared/md-helper.js
index fdceebc..8a22b0d 100644
--- a/static_src/shared/md-helper.js
+++ b/static_src/shared/md-helper.js
@@ -38,7 +38,7 @@
 const SANITIZE_OPTIONS = Object.freeze({
   RETURN_TRUSTED_TYPE: true,
   FORBID_TAGS: ['style'],
-  FORBID_ATTR: ['style', 'autoplay'],
+  FORBID_ATTR: ['style', 'autoplay', 'src'],
 });
 
 /**
@@ -50,31 +50,6 @@
   return raw.replace(/<b>|<\/b>/g, '**');
 };
 
-/** @const {Object} Basic HTML character escape mapping */
-const HTML_ESCAPE_MAP = Object.freeze({
-  '&': '&amp;',
-  '<': '&lt;',
-  '>': '&gt;',
-  '"': '&quot;',
-  '\'': '&#39;',
-  '/': '&#x2F;',
-  '`': '&#x60;',
-  '=': '&#x3D;',
-});
-
-/**
- * Escapes HTML characters, used to render HTML blocks in Markdown. This
- * alleviates security flaws but is not the primary security barrier, that is
- * handled by DOMPurify.
- * @param {string} text Content that looks to Marked parser to contain HTML.
- * @return {string} Same text content after escaping HTML characters.
- */
-const escapeHtml = (text) => {
-  return text.replace(/[<>"']/g, (s) => {
-    return HTML_ESCAPE_MAP[s];
-  });
-};
-
 /**
 * Checks to see if input string is a valid HTTP link.
  * @param {string} string
@@ -139,8 +114,7 @@
   // autolinking.
   // TODO(crbug.com/monorail/9310): Integrate autolink
   const preprocessed = replaceBoldTag(raw);
-  const escaped = escapeHtml(preprocessed);
-  const converted = marked(escaped);
+  const converted = marked(preprocessed);
   const sanitized = DOMPurify.sanitize(converted, SANITIZE_OPTIONS);
   return sanitized.toString();
 };
diff --git a/static_src/shared/md-helper.test.js b/static_src/shared/md-helper.test.js
index 6056849..52ba279 100644
--- a/static_src/shared/md-helper.test.js
+++ b/static_src/shared/md-helper.test.js
@@ -104,22 +104,8 @@
     assert.equal(actual, expected);
   });
 
-  it('escapes HTML content', () => {
-    let actual = renderMarkdown('<input></input>');
-    assert.equal(actual, '<p>&lt;input&gt;&lt;/input&gt;</p>\n');
-
-    actual = renderMarkdown('<a href="https://google.com">clickme</a>');
-    assert.equal(actual,
-      `<p>&lt;a href="<span class="annotated-link"><a title="" ` +
-      `href="https://google.com&quot;>clickme</a"><span ` +
-      `class="material-icons link_off">link_off</span>` +
-      `https://google.com"&gt;clickme&lt;/a</a><span ` +
-      `class="tooltip">Link may be malformed: ` +
-      `https://google.com"&gt;clickme&lt;/a</span></span>&gt;</p>\n`);
-  });
-
-  it('escapes video content', () => {
-    const actual = renderMarkdown('<video src="//youtube" control></video>');
-    assert.equal(actual, '<p>&lt;video src="//youtube" control&gt;&lt;/video&gt;</p>\n');
+  it('forbids src', () => {
+    const actual = renderMarkdown('<video id="foo" src="//youtube" control></video>');
+    assert.equal(actual, '<p><video id="foo"></video></p>\n');
   });
 });