Revert "Make chumpdetector plugin compatible with Polymer2"

This reverts commit 158f1f51260ca59a2dc8b9461bfdfe9ca510d710.

Reason for revert: copybara import failed. need to fix

Change-Id: Iad07565b4d2abde9207900617023d53775efb7b3
Reviewed-on: https://chromium-review.googlesource.com/c/infra/gerrit-plugins/chumpdetector/+/1762801
Reviewed-by: Edward Lesmes <ehmaldonado@chromium.org>
diff --git a/BUILD b/BUILD
index 891be4d..305398c 100644
--- a/BUILD
+++ b/BUILD
@@ -14,6 +14,6 @@
 
 polygerrit_plugin(
     name = "chumpdetector_ui",
-    app = "src/main/resources/static/chumpdetector.html",
+    app = "src/main/resources/static/chumpdetector.js",
     plugin_name = "chumpdetector",
 )
diff --git a/src/main/java/com/googlesource/chromium/plugins/chumpdetector/ChumpDetectorModule.java b/src/main/java/com/googlesource/chromium/plugins/chumpdetector/ChumpDetectorModule.java
index b95077a..66efd39 100644
--- a/src/main/java/com/googlesource/chromium/plugins/chumpdetector/ChumpDetectorModule.java
+++ b/src/main/java/com/googlesource/chromium/plugins/chumpdetector/ChumpDetectorModule.java
@@ -14,7 +14,7 @@
 public class ChumpDetectorModule extends RestApiModule {
   @Override
   protected void configure() {
-    DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(WebUiPlugin.js("chumpdetector.html"));
+    DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(WebUiPlugin.js("chumpdetector.js"));
 
     get(PROJECT_KIND, "config").to(GetProjectChumpConfig.class);
   }
diff --git a/src/main/resources/static/chumpdetector.html b/src/main/resources/static/chumpdetector.html
deleted file mode 100644
index a564a9d..0000000
--- a/src/main/resources/static/chumpdetector.html
+++ /dev/null
@@ -1,530 +0,0 @@
-// Copyright 2019 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-<dom-module id="common-helpers">
-  <script>
-    // Milliseconds between tree status checks.
-    const STATUS_INTERVAL_MS = 60 * 1000;
-
-    // Date used in status if real one can't be fetched.
-    const UNKNOWN_DATE = new Date(0);
-
-    // Gerrit JSON response prefix.
-    const JSON_PREFIX = ')]}\'';
-
-    // Placeholder status when actual one is unknown yet.
-    const UNKNOWN_TREE_STATUS = {
-      isOpen: false,
-      generalState: 'unknown',
-      message: 'Tree status unknown.',
-      username: 'Pending',
-      date: UNKNOWN_DATE,
-      url: null
-    };
-
-    // Placeholder status when we're waiting for an answer.
-    const PENDING_TREE_STATUS = {
-      isOpen: false,
-      generalState: 'unknown',
-      message: 'Waiting for tree status...',
-      username: 'Pending',
-      date: UNKNOWN_DATE,
-      url: null
-    };
-
-    // Placeholder status when the current branch is not the trunk.
-    const DISABLED_BRANCH_TREE_STATUS = {
-      isOpen: true,
-      generalState: 'unknown',
-      message: 'No tree status for this branch.',
-      username: 'Warning',
-      date: UNKNOWN_DATE,
-      url: null
-    };
-
-
-    // A message div to add inside the confirm submit dialog.
-    let confirmSubmitMessage = document.createElement('p');
-
-    const MESSAGE_STYLES = {
-      styleConfirmSubmit: 'color: red; width: 400px;',
-      styleHidden: 'display: none;',
-    }
-
-    /**
-     * Discard any the message in the confirm submit dialog.
-     * (No message to show.)
-     */
-    const disableConfirmSubmitMessage = function() {
-      confirmSubmitMessage.style = MESSAGE_STYLES.styleHidden;
-      confirmSubmitMessage.textContent = '';
-    }
-
-    /**
-     * Set the message that will be shown in the confirm submit dialog.
-     * @param {string} message The message to display.
-     */
-    const setConfirmSubmitMessage = function(message) {
-      confirmSubmitMessage.style = MESSAGE_STYLES.styleConfirmSubmit;
-      confirmSubmitMessage.textContent = message;
-    }
-
-    // Cache of StatusFetchers. Config's treeName -> StatusFetcher.
-    let fetchers = {};
-    // TreeStatus object that correspond to current change.
-    let installedTreeStatus = null;
-
-    // Fetches JSON from URL, returns Promise.
-    const fetchJSON = async (url, options) => {
-      const response = await fetch(url, options);
-      if (response.status == 204 || !response.ok) {
-        console.error(`${url} returned with status ${response.status}.`);
-        return null;
-      }
-      let responseText = await response.text();
-      try {
-        if (responseText.startsWith(JSON_PREFIX)) {
-          responseText = responseText.substring(JSON_PREFIX.length);
-        }
-        return JSON.parse(responseText);
-      } catch (e) {
-        console.error(`${url} returned invalid JSON '${responseText}'`);
-        return null;
-      }
-    };
-
-    /**
-     * Returns configuration for a tree status (status URL to use, login URL, etc.)
-     * @param {string} project Gerrit project name to search config for
-     * @return {Promise} Promise, resolving to project config.
-     */
-    const getProjectConfig = function(project) {
-      // Expected format of a project config:
-      // {
-      //   treeName: English label to describe the project, eg: 'Chrome'.
-      //   viewURL: URL to the user facing status, eg: 'http://foo/'
-      //   statusURL: URL to the tree status, eg: 'http://foo/current?format=json'
-      //   withCredentials: True if status requests require HTTP cookies.
-      //   disabledBranchPattern: Regex object or string pattern to exclude a branch,
-      //   enforceCommitQueue: True if project is using Commit Queue
-      // }
-
-      // If we've fetched it already, use the config set on the window.
-      let config = window.chumpDetectorConfig;
-      if (config !== undefined) {
-        return Promise.resolve(config);
-      }
-
-      //Otherwise, fetch it and store it for reuse.
-      let chumpdetectorURL = `/projects/${encodeURIComponent(project)}` +
-        `/chumpdetector~config`;
-      return fetchJSON(chumpdetectorURL).then(function(cfg) {
-        window.chumpDetectorConfig = cfg || null;
-        if (!window.chumpDetectorConfig) {
-          console.log('Chump detector is not configured.');
-          return null;
-        }
-        return getProjectConfig(project);
-      });
-    }
-
-    /**
-     * Creates a new one or returns existing StatusFetcher object.
-     * @param {Object} config Project configuration as returned by getProjectConfig
-     * @return {Object} StatusFetcher object
-     */
-    const getStatusFetcher = function(config) {
-      if (!fetchers.hasOwnProperty(config.treeName)) {
-        fetchers[config.treeName] = new StatusFetcher(config.viewURL,
-                                                      config.statusURL,
-                                                      config.withCredentials,
-                                                      config.loginURL);
-      }
-      return fetchers[config.treeName];
-    }
-  </script>
-</dom-module>
-
-<dom-module id="tree-status">
-  <script>
-    const TREE_STYLES = {
-      styleOuterDiv:
-        'align-items: center;' +
-        'display: flex;' +
-        'font-size: 1.1em;' +
-        'justify-content: center;' +
-        'margin: 5px 0 8px;' +
-        'padding: .4em;' +
-        'max-width: 20em;',
-      unknownStatus:
-        'color: black;' +
-        'background-color: #FFFC6C;',
-      openStatus:
-        'color: black;' +
-        'background-color: #BCE889;',
-      closedStatus:
-        'color: white;' +
-        'background-color: #E98080;',
-    };
-
-    /**
-     * Tree status UI for some single change.
-     * @param {Object} change Change object provided by Gerrit
-     * @param {Object} config Project config this change belongs to
-     */
-    const TreeStatus = function(change, config) {
-      this.change = change;
-      this.config = config;
-      this.treeStatus = null;
-
-      this.fetcher = null;
-      this.fetcherCallback = null;
-      this.installed = false;
-
-      // Build DOM for tree status UI.
-      this.outerDiv = document.createElement('div');
-      this.outerDiv.style = TREE_STYLES.styleOuterDiv;
-      this.innerDiv = document.createElement('div');
-      this.outerDiv.appendChild(this.innerDiv);
-
-      // Set default state to 'Unknown'.
-      this.setTreeStatus(UNKNOWN_TREE_STATUS);
-    };
-
-    /**
-     * Add DOM elements to the document, subscribe to status fetcher
-     * notifications.
-     * @param {Element} element to hook the tree-status to.
-     * @param {Object} fetcher StatusFetcher object that fetches tree status
-     */
-    TreeStatus.prototype.install = function(hookElement, fetcher) {
-      if (!this.installed) {
-        console.log('Installing tree status for change:', this.change.id);
-        hookElement.appendChild(this.outerDiv);
-        this.fetcher = fetcher;
-        if (this.fetcher) {
-          this.fetcherCallback = this.setTreeStatus.bind(this);
-          this.fetcher.subscribe(this.fetcherCallback);
-        }
-        this.installed = true;
-      }
-    };
-
-    /**
-     * Remove DOM elements from the document, unsubscribe from status fetcher.
-     */
-    TreeStatus.prototype.uninstall = function() {
-      if (this.installed) {
-        console.log('Uninstalling tree status for change:', this.change.id);
-        if (this.fetcher) {
-          this.fetcher.unsubscribe(this.fetcherCallback);
-          this.fetcher = null;
-          this.fetcherCallback = null;
-        }
-        if (this.outerDiv.parentNode) {
-            this.outerDiv.parentNode.removeChild(this.outerDiv);
-        }
-        this.installed = false;
-      }
-    };
-
-    /**
-     * Update DOM to display given tree status.
-     * @param {Object} treeStatus Tree status object produced by StatusFetcher
-     */
-    TreeStatus.prototype.setTreeStatus = function(treeStatus) {
-      this.treeStatus = treeStatus;
-      // Update content.
-      if (!treeStatus.url) {
-        this.innerDiv.textContent = `${treeStatus.username}: ${treeStatus.message}`;
-      } else {
-        // Create <a></a>.
-        let anchor = document.createElement('a');
-        anchor.href = treeStatus.url;
-        anchor.target = '_blank';
-        anchor.innerText = treeStatus.message;
-        // Replace whatever in innerDiv with that link.
-        this.innerDiv.textContent = '';
-        this.innerDiv.appendChild(anchor);
-      }
-
-      // Update CSS style.
-      let style = (treeStatus.generalState || 'unknown') + 'Status';
-      style = TREE_STYLES[style] || TREE_STYLES.unknownStatus;
-      this.outerDiv.style = TREE_STYLES.styleOuterDiv + style;
-    }
-  </script>
-</dom-module>
-
-<dom-module id="status-fetcher">
-  <script>
-    /**
-     * Periodically fetches tree status for some project.
-     * @param {string} viewURL URL to link to normally for users to navigate to
-     * @param {string} statusURL URL to fetch status from
-     * @param {boolean} withCredentials True to send cookies with the request
-     * @param {string} loginURL URL to use for login (if required)
-     */
-    const StatusFetcher = function(viewURL, statusURL, withCredentials, loginURL) {
-      this.viewURL = viewURL;
-      this.statusURL = statusURL;
-      this.withCredentials = withCredentials;
-      this.loginURL = loginURL;
-      this.lastKnownStatus = PENDING_TREE_STATUS;
-      this.callbacks = [];
-      this.request = null;
-      this.timer = null;
-    };
-
-    /**
-     * Register callback to be called when tree status is fetched.
-     * @param {Function} callback Will be called with single treeStatus argument
-     */
-    StatusFetcher.prototype.subscribe = function(callback) {
-      this.callbacks.push(callback);
-    };
-
-    /**
-     * Removes previously registered callback.
-     * @param {Function} callback Exact same function object that was registered
-     */
-    StatusFetcher.prototype.unsubscribe = function(callback) {
-      const index = this.callbacks.indexOf(callback);
-      if (index != -1) {
-        this.callbacks.splice(index, 1);
-      }
-    };
-
-    /**
-     * Asynchronously fetches (or refetches) tree status and calls callbacks.
-     */
-    StatusFetcher.prototype.fetch = function() {
-      // Already fetching?
-      if (this.request) {
-        return;
-      }
-      // Fetch restarts refetch timer (by launching it again when request finishes).
-      if (this.timer) {
-        clearTimeout(this.timer);
-        this.timer = null;
-      }
-      // Launch async GET.
-      this.lastKnownStatus = PENDING_TREE_STATUS;
-      this.request = new XMLHttpRequest();
-      var that = this;
-      this.request.onreadystatechange = function() {
-        if (this.readyState == 4) {
-          console.log('Fetched', that.statusURL);
-          that.request = null;
-          that.onFetchCompleted.call(that, this);
-        }
-      };
-      this.request.open('GET', this.statusURL);
-      this.request.withCredentials = this.withCredentials;
-      this.request.send();
-    };
-
-    /**
-     * Called when HTTP request finishes (successfully or not).
-     * @param {Object} request Completed XMLHttpRequest object.
-     */
-    StatusFetcher.prototype.onFetchCompleted = function(request) {
-      let treeStatus;
-      if (request.status == 200) {
-        try {
-          let result = JSON.parse(request.responseText);
-          treeStatus = {
-            isOpen: result.can_commit_freely,
-            generalState: result.general_state,
-            message: result.message,
-            username: result.username,
-            date: new Date(result.date.replace(' ', 'T')),
-            url: this.viewURL
-          };
-        } catch (ex) {
-          console.log('Error parsing json: ' + ex);
-          treeStatus = {
-            isOpen: false,
-            generalState: 'unknown',
-            message: String(ex),
-            username: 'ParseError',
-            date: UNKNOWN_DATE,
-            url: this.viewURL
-          };
-        }
-      } else {
-        let message;
-        let url;
-        if (this.withCredentials) {
-          url = this.loginURL;
-          message = 'Login required';
-        } else {
-          url = this.viewURL;
-          message = (request.statusText ||
-                     'Error ' + request.status + ' requesting tree status');
-        }
-        treeStatus = {
-          isOpen: false,
-          generalState: 'unknown',
-          message: message,
-          username: 'RequestError',
-          date: UNKNOWN_DATE,
-          url: url
-        };
-      }
-      // Remember this status until next refetch cycle.
-      this.lastKnownStatus = treeStatus;
-      // Iterate over a copy. Callbacks may modify the list during iteration.
-      let copy = this.callbacks.slice(0);
-      for (let i = 0; i < copy.length; i++) {
-        copy[i](treeStatus);
-      }
-      // Auto refetch status later (if still have subscribers).
-      var that = this;
-      this.timer = setTimeout(function() {
-        that.timer = null;
-        // Known status is too old. Better to forget it.
-        that.lastKnownStatus = PENDING_TREE_STATUS;
-        if (that.callbacks.length) {
-          that.fetch();
-        }
-      }, STATUS_INTERVAL_MS);
-    };
-
-  </script>
-</dom-module>
-
-<dom-module>
-  <script>
-    const installTreeStatus = function(element) {
-      const change = element.change;
-      const revision = element.revision;
-
-      // Uninstall previous one (if any).
-      if (installedTreeStatus) {
-        installedTreeStatus.uninstall();
-        installedTreeStatus = null;
-      }
-
-      getProjectConfig(change.project).then(config => {
-        if (!config || !config.statusURL) {
-          console.log('No status url for this project.');
-          return;
-        } else if (config.disabled) {
-          console.log('Chumpdetector is disabled for this project.');
-          return;
-        }
-        // Install new one if the project is supported.
-        let fetcher;
-        if (config.disabledBranchPattern &&
-            new RegExp(config.disabledBranchPattern).test(change.branch)) {
-          console.log('Disabling chump detector based on branch name.');
-          fetcher = null;
-        } else {
-          console.log(`Using status url: ${config.statusURL}`);
-          fetcher = getStatusFetcher(config);
-        }
-
-        // Bind tree status to status fetcher, perform initial fetch.
-        installedTreeStatus = new TreeStatus(change, config);
-        installedTreeStatus.install(element, fetcher);
-        if (fetcher) {
-          installedTreeStatus.setTreeStatus(fetcher.lastKnownStatus);
-          fetcher.fetch();
-        } else {
-          installedTreeStatus.setTreeStatus(DISABLED_BRANCH_TREE_STATUS);
-        }
-      }, function() {
-        console.log('Error fetching config.');
-      });
-    }
-  </script>
-</dom-module>
-
-<dom-module id="uninstall-tree">
-  <script>
-    const maybeUninstallTreeStatus = function(token) {
-      // Navigated away from a change page? Uninstall tree status UI.
-      if (token.substring(0, 2) != '/c' && installedTreeStatus) {
-        installedTreeStatus.uninstall();
-        installedTreeStatus = null;
-      }
-    }
-  </script>
-</dom-module>
-
-<dom-module id="confirm-message">
-  <script>
-    const installConfirmMessage = function(element) {
-      element.appendChild(confirmSubmitMessage);
-    }
-  </script>
-</dom-module>
-
-<dom-module id="check-tree">
-  <script>
-    const checkTreeBeforeSubmit = function() {
-      if (!installedTreeStatus) {
-        disableConfirmSubmitMessage();
-        return true;
-      }
-
-      let treeStatus = installedTreeStatus.treeStatus;
-      let config = installedTreeStatus.config;
-
-      // Change is in unsupported branch, do not block it.
-      if (treeStatus == DISABLED_BRANCH_TREE_STATUS) {
-        disableConfirmSubmitMessage();
-        return true;
-      }
-
-      // Change should be submitted via CQ (not directly via Gerrit).
-      //if (config.enforceCommitQueue) {
-      if (config.enforceCommitQueue) {
-        setConfirmSubmitMessage(
-            `The ${config.treeName} project uses the commit queue (CQ). ` +
-            `You should submit your change by clicking "Reply" and setting the ` +
-            `"Commit-Queue" label to the appropriate value.\n` +
-            `Do you want to commit the change directly, bypassing CQ (dangerous)?`);
-        return true;
-      }
-
-      // Tree is open, do not block the change.
-      if (treeStatus.isOpen) {
-        disableConfirmSubmitMessage();
-        return true;
-      }
-
-      // Unknown tree state. Double check with the user before submitting.
-      if (treeStatus.generalState == 'unknown') {
-        setConfirmSubmitMessage(
-            `${config.treeName} tree status is unknown, submitting this ` +
-            `change now might be dangerous.  Submit anyway?`);
-        return true;
-      }
-
-      // Tree is closed (or in some entirely unexpected state). Warn the user.
-      setConfirmSubmitMessage(
-          `The ${config.treeName} tree is closed, submitting this ` +
-          `change is dangerous.  Submit anyway?`);
-      return true;
-    }
-  </script>
-</dom-module>
-
-<dom-module id="install-plugin">
-  <script>
-    Gerrit.install(plugin => {
-      plugin.hook('change-metadata-item').onAttached(installTreeStatus);
-      // Called by Gerrit whenever document.location changes.
-      // @param {string} token Fragment part of a page URL
-      plugin.on('history', maybeUninstallTreeStatus);
-      plugin.hook('confirm-submit-change').onAttached(
-          installConfirmMessage);
-      // Called by gerrit whenever 'Submit' is clicked.
-      // @return {boolean} False to block submit
-      plugin.on('submitchange', checkTreeBeforeSubmit);
-    });
-  </script>
-</dom-module>
diff --git a/src/main/resources/static/chumpdetector.js b/src/main/resources/static/chumpdetector.js
new file mode 100644
index 0000000..1d865e0
--- /dev/null
+++ b/src/main/resources/static/chumpdetector.js
@@ -0,0 +1,577 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+Gerrit.install(function(self) {
+
+
+/**
+ * Milliseconds between tree status checks.
+ */
+var STATUS_INTERVAL_MS = 60 * 1000;
+
+
+/**
+ * Date used in status if real one can't be fetched.
+ */
+var UNKNOWN_DATE = new Date(0);
+
+
+/**
+ * Gerrit JSON response prefix.
+ */
+var JSON_PREFIX = ')]}\'';
+
+
+/**
+ * Placeholder status when actual one is unknown yet.
+ */
+var UNKNOWN_TREE_STATUS = {
+  isOpen: false,
+  generalState: 'unknown',
+  message: 'Tree status unknown.',
+  username: 'Pending',
+  date: UNKNOWN_DATE,
+  url: null
+};
+
+
+/**
+ * Placeholder status when we're waiting for an answer.
+ */
+var PENDING_TREE_STATUS = {
+  isOpen: false,
+  generalState: 'unknown',
+  message: 'Waiting for tree status...',
+  username: 'Pending',
+  date: UNKNOWN_DATE,
+  url: null
+};
+
+
+/**
+ * Placeholder status when the current branch is not the trunk.
+ */
+var DISABLED_BRANCH_TREE_STATUS = {
+  isOpen: true,
+  generalState: 'unknown',
+  message: 'No tree status for this branch.',
+  username: 'Warning',
+  date: UNKNOWN_DATE,
+  url: null
+};
+
+
+// Register CSS classes only once, during plugin loading.
+var STYLES = {
+  // Base style for div that contains tree status box.
+  outerDiv: Gerrit.css(
+    'align-items: center;' +
+    'display: flex;' +
+    'font-size: 1.1em;' +
+    'justify-content: center;' +
+    'margin: 5px 0 8px;' +
+    'padding: .4em;'),
+
+  // Additional styles for PolyGerrit UI.
+  outerDivPolyGerrit: Gerrit.css('max-width: 20em;'),
+
+  // Tree status is not yet known.
+  unknownStatus: Gerrit.css(
+    'color: black;' +
+    'background-color: #FFFC6C;'),
+
+  // Tree is open.
+  openStatus: Gerrit.css(
+    'color: black;' +
+    'background-color: #BCE889;'),
+
+  // Tree is closed.
+  closedStatus: Gerrit.css(
+    'color: white;' +
+    'background-color: #E98080;'),
+
+  confirmSubmitMessage: Gerrit.css(
+    'color: red;' +
+    'width: 400px;'),
+
+  hiddenConfirmSubmitMessage: Gerrit.css(
+    'display: none;')
+};
+
+
+/**
+ * Tree status UI for some single change.
+ * @param {Object} change Change object provided by Gerrit
+ * @param {Object} config Project config this change belongs to
+ */
+var TreeStatus = function(change, config) {
+  this.change = change;
+  this.config = config;
+  this.treeStatus = null;
+
+  this.fetcher = null;
+  this.fetcherCallback = null;
+  this.installed = false;
+
+  // Build DOM for tree status UI.
+  this.outerDiv = document.createElement('div');
+  this.outerDiv.classList.add(STYLES.outerDiv);
+  if (window.Polymer) {
+    this.outerDiv.classList.add(STYLES.outerDivPolyGerrit);
+  }
+  this.innerDiv = document.createElement('div');
+  this.outerDiv.appendChild(this.innerDiv);
+
+  // Set default state to 'Unknown'.
+  this.setTreeStatus(UNKNOWN_TREE_STATUS);
+};
+
+
+/**
+ * Add DOM elements to the document, subscribe to status fetcher notifications.
+ * @param {Object} fetcher StatusFetcher object that fetches tree status
+ */
+TreeStatus.prototype.install = function(fetcher) {
+  if (!this.installed) {
+    console.log('Installing tree status for change:', this.change.id);
+    document.getElementById('change_plugins').appendChild(this.outerDiv);
+    this.fetcher = fetcher;
+    if (this.fetcher) {
+      this.fetcherCallback = this.setTreeStatus.bind(this);
+      this.fetcher.subscribe(this.fetcherCallback);
+    }
+    this.installed = true;
+  }
+};
+
+
+/**
+ * Remove DOM elements from the document, unsubscribe from status fetcher.
+ */
+TreeStatus.prototype.uninstall = function() {
+  if (this.installed) {
+    console.log('Uninstalling tree status for change:', this.change.id);
+    if (this.fetcher) {
+      this.fetcher.unsubscribe(this.fetcherCallback);
+      this.fetcher = null;
+      this.fetcherCallback = null;
+    }
+    if (this.outerDiv.parentNode)
+      this.outerDiv.parentNode.removeChild(this.outerDiv);
+    this.installed = false;
+  }
+};
+
+
+/**
+ * Update DOM to display given tree status.
+ * @param {Object} treeStatus Tree status object produced by StatusFetcher
+ */
+TreeStatus.prototype.setTreeStatus = function(treeStatus) {
+  this.treeStatus = treeStatus;
+
+  // Update content.
+  if (!treeStatus.url) {
+    this.innerDiv.textContent = treeStatus.username + ': ' + treeStatus.message;
+  } else {
+    // Create <a></a>.
+    var anchor = document.createElement('a');
+    anchor.href = treeStatus.url;
+    anchor.target = '_blank';
+    anchor.innerText = treeStatus.message;
+    // Replace whatever in innerDiv with that link.
+    this.innerDiv.textContent = '';
+    this.innerDiv.appendChild(anchor);
+  }
+
+  // Update CSS style.
+  var styleName = (treeStatus.generalState || 'unknown') + 'Status';
+  var cssClass = STYLES[styleName] || STYLES.unknownStatus;
+  this.outerDiv.className = STYLES.outerDiv + ' ' + cssClass;
+  if (window.Polymer) {
+    this.outerDiv.classList.add(STYLES.outerDivPolyGerrit);
+  }
+};
+
+
+/**
+ * Periodically fetches tree status for some project.
+ * @param {string} viewURL URL to link to normally for users to navigate to
+ * @param {string} statusURL URL to fetch status from
+ * @param {boolean} withCredentials True to send cookies with the request
+ */
+var StatusFetcher = function(viewURL, statusURL, withCredentials) {
+  this.viewURL = viewURL;
+  this.statusURL = statusURL;
+  this.withCredentials = withCredentials;
+  this.lastKnownStatus = PENDING_TREE_STATUS;
+
+  this.callbacks = [];
+  this.request = null;
+  this.timer = null;
+};
+
+
+/**
+ * Register callback to be called when tree status is fetched.
+ * @param {Function} callback Will be called with single treeStatus argument
+ */
+StatusFetcher.prototype.subscribe = function(callback) {
+  this.callbacks.push(callback);
+};
+
+
+/**
+ * Removes previously registered callback.
+ * @param {Function} callback Exact same function object that was registered
+ */
+StatusFetcher.prototype.unsubscribe = function(callback) {
+  var index = this.callbacks.indexOf(callback);
+  if (index != -1) {
+    this.callbacks.splice(index, 1);
+  }
+};
+
+
+/**
+ * Asynchronously fetches (or refetches) tree status and calls callbacks.
+ */
+StatusFetcher.prototype.fetch = function() {
+  // Already fetching?
+  if (this.request)
+    return;
+
+  // Fetch restarts refetch timer (by launching it again when request finishes).
+  if (this.timer) {
+    clearTimeout(this.timer);
+    this.timer = null;
+  }
+
+  // Launch async GET.
+  this.lastKnownStatus = PENDING_TREE_STATUS;
+  this.request = new XMLHttpRequest();
+
+  var that = this;
+  this.request.onreadystatechange = function() {
+    if (this.readyState == 4) {
+      console.log('Fetched', that.statusURL);
+      that.request = null;
+      that.onFetchCompleted.call(that, this);
+    }
+  };
+
+  this.request.open('GET', this.statusURL);
+  this.request.withCredentials = this.withCredentials;
+  this.request.send();
+};
+
+
+/**
+ * Called when HTTP request finishes (successfully or not).
+ * @param {Object} request Completed XMLHttpRequest object.
+ */
+StatusFetcher.prototype.onFetchCompleted = function(request) {
+  var treeStatus;
+  if (request.status == 200) {
+    try {
+      var result = JSON.parse(request.responseText);
+      treeStatus = {
+        isOpen: result.can_commit_freely,
+        generalState: result.general_state,
+        message: result.message,
+        username: result.username,
+        date: new Date(result.date.replace(' ', 'T')),
+        url: this.viewURL
+      };
+    } catch (ex) {
+      console.log('Error parsing json: ' + ex);
+      treeStatus = {
+        isOpen: false,
+        generalState: 'unknown',
+        message: String(ex),
+        username: 'ParseError',
+        date: UNKNOWN_DATE,
+        url: this.viewURL
+      };
+    }
+  } else {
+    var message;
+    var url;
+    if (this.withCredentials) {
+      message = 'Login required';
+    } else {
+      url = this.viewURL;
+      message = (request.statusText ||
+                 'Error ' + request.status + ' requesting tree status');
+    }
+    treeStatus = {
+      isOpen: false,
+      generalState: 'unknown',
+      message: message,
+      username: 'RequestError',
+      date: UNKNOWN_DATE,
+      url: url
+    };
+  }
+
+  // Remember this status until next refetch cycle.
+  this.lastKnownStatus = treeStatus;
+
+  // Iterate over a copy. Callbacks may modify the list during iteration.
+  var copy = this.callbacks.slice(0);
+  for (var i = 0; i < copy.length; i++) {
+    copy[i](treeStatus);
+  }
+
+  // Auto refetch status later (if still have subscribers).
+  var that = this;
+  this.timer = setTimeout(function() {
+    that.timer = null;
+    // Known status is too old. Better to forget it.
+    that.lastKnownStatus = PENDING_TREE_STATUS;
+    if (that.callbacks.length) {
+      that.fetch();
+    }
+  }, STATUS_INTERVAL_MS);
+};
+
+
+
+// Cache of StatusFetchers. Config's treeName -> StatusFetcher.
+var fetchers = {};
+// TreeStatus object that correspond to current change.
+var installedTreeStatus = null;
+
+// Fetches JSON from URL, returns Promise.
+function fetchJSON(url) {
+  return new Promise(function(resolve, reject) {
+    var xhr = new XMLHttpRequest();
+    xhr.open('GET', url);
+    xhr.onload = function() {
+      var result = null;
+      if (this.status === 204) {
+        resolve(null);
+      } else if (this.status >= 200 && this.status < 300) {
+        try {
+          result = JSON.parse(xhr.response.substring(JSON_PREFIX.length));
+          resolve(result);
+        } catch (_) {
+          reject("Unable to parse JSON");
+        }
+      } else {
+        reject("Got non-200 response");
+      }
+    };
+    xhr.onerror = function() {
+      reject("Unhandled error");
+    };
+    xhr.send();
+  });
+}
+
+/**
+ * Returns configuration for a tree status (status URL to use, login URL, etc.)
+ * @param {string} project Gerrit project name to search config for
+ * @return {Promise} Promise, resolving to project config.
+ */
+function getProjectConfig(project) {
+  /*
+  Expected format of a project config:
+  {
+    treeName: English label to describe the project, eg: 'Chrome'.
+    viewURL: URL to the user facing status, eg: 'http://foo/'
+    statusURL: URL to the tree status, eg: 'http://foo/current?format=json'
+    withCredentials: True if status requests require HTTP cookies.
+    disabledBranchPattern: Regex object or string pattern to exclude a branch,
+    enforceCommitQueue: True if project is using Commit Queue
+  }
+  */
+  // If we've fetched it already, use the config set on the window.
+  var config = window.chumpDetectorConfig;
+  if (config !== undefined) {
+    return Promise.resolve(config);
+  }
+
+  // Otherwise, fetch it and store it for reuse.
+  var chumpdetectorURL = '/projects/' + encodeURIComponent(project) +
+      '/chumpdetector~config';
+  return fetchJSON(chumpdetectorURL).then(function(cfg) {
+    window.chumpDetectorConfig = cfg || null;
+    if (!window.chumpDetectorConfig) {
+      console.log('Chump detector is not configured.');
+      return null;
+    }
+    return getProjectConfig(project);
+  });
+}
+
+
+/**
+ * Creates a new one or returns existing StatusFetcher object.
+ * @param {Object} config Project configuration as returned by getProjectConfig
+ * @return {Object} StatusFetcher object
+ */
+function getStatusFetcher(config) {
+  if (!fetchers.hasOwnProperty(config.treeName)) {
+    fetchers[config.treeName] = new StatusFetcher(config.viewURL,
+                                                  config.statusURL,
+                                                  config.withCredentials);
+  }
+  return fetchers[config.treeName];
+}
+
+
+self.on('showchange',
+  /**
+   * Called by Gerrit when change screen is shown.
+   * @param {Object} change Object with change details
+   * @param {Object} revision Object with revision details
+   */
+  function(change, revision) {
+  // Uninstall previous one (if any).
+  if (installedTreeStatus) {
+    installedTreeStatus.uninstall();
+    installedTreeStatus = null;
+  }
+
+  getProjectConfig(change.project).then(function(config) {
+    if (!config || !config.statusURL) {
+      console.log('No status url for this project.');
+      return;
+    } else if (config.disabled) {
+      console.log('Chumpdetector is disabled for this project.');
+      return;
+    }
+    // Install new one if the project is supported.
+    var fetcher;
+    if (config.disabledBranchPattern &&
+        new RegExp(config.disabledBranchPattern).test(change.branch)) {
+      console.log('Disabling chump detector based on branch name.');
+      fetcher = null;
+    } else {
+      console.log('Using status url: ' + config.statusURL);
+      fetcher = getStatusFetcher(config);
+    }
+
+    function setupTreeStatus() {
+      // Bind tree status to status fetcher, perform initial fetch.
+      installedTreeStatus = new TreeStatus(change, config);
+      installedTreeStatus.install(fetcher);
+      if (fetcher) {
+        installedTreeStatus.setTreeStatus(fetcher.lastKnownStatus);
+        fetcher.fetch();
+      } else {
+        installedTreeStatus.setTreeStatus(DISABLED_BRANCH_TREE_STATUS);
+      }
+    }
+
+    // If the configuration specifies loading an image URL first then
+    // request the image. Whether it succeeds or fails immediately
+    // move into setting up the tree status request. This allows users
+    // the opportunity to use the image load to establish cookies the
+    // browser may need in order to make the status request which can
+    // work even without the image request succeeding.
+    if (config.preloadImageURL) {
+      var img = new Image();
+      img.addEventListener("load", setupTreeStatus);
+      img.addEventListener("error", setupTreeStatus);
+      img.src = config.preloadImageURL;
+    } else {
+      setupTreeStatus();
+    }
+  }, function() {
+    console.log('Error fetching config.');
+  });
+});
+
+
+self.on('history',
+  /**
+   * Called by Gerrit whenever document.location changes.
+   * @param {string} token Fragment part of a page URL
+   */
+  function(token) {
+  // Navigated away from a change page? Uninstall tree status UI.
+  if (token.substring(0, 2) != '/c' && installedTreeStatus) {
+    installedTreeStatus.uninstall();
+    installedTreeStatus = null;
+  }
+});
+
+// Add a message div to appear inside the confirm submit dialog.
+var confirmSubmitMessage = document.createElement('p');
+self.hook('confirm-submit-change').onAttached(function(element) {
+  element.appendChild(confirmSubmitMessage);
+});
+
+/**
+ * Discard any the message in the confirm submit dialog. (No message to show.)
+ */
+function disableConfirmSubmitMessage() {
+  confirmSubmitMessage.style = STYLES.hiddenConfirmSubmitMessage;
+  confirmSubmitMessage.textContent = '';
+}
+
+/**
+ * Set the message that will be shown in the confirm submit tialog.
+ * @param {string} message The message to appear.
+ */
+function setConfirmSubmitMessage(message) {
+  confirmSubmitMessage.style = STYLES.confirmSubmitMessage;
+  confirmSubmitMessage.textContent = message;
+}
+
+self.on('submitchange',
+  /**
+   * Called by Gerrit whenever 'Submit' is clicked.
+   * @return {boolean} False to block submit
+   */
+  function() {
+  // Change is in unsupported project, do not block it.
+  if (!installedTreeStatus) {
+    disableConfirmSubmitMessage();
+    return true;
+  }
+
+  var treeStatus = installedTreeStatus.treeStatus;
+  var config = installedTreeStatus.config;
+
+  // Change is in unsupported branch, do not block it.
+  if (treeStatus == DISABLED_BRANCH_TREE_STATUS) {
+    disableConfirmSubmitMessage();
+    return true;
+  }
+
+  // Change should be submitted via CQ (not directly via Gerrit).
+  if (config.enforceCommitQueue) {
+    setConfirmSubmitMessage(
+        'The ' + config.treeName + ' project uses the commit queue (CQ). ' +
+        'You should submit your change by clicking "Reply" and setting the ' +
+        '"Commit-Queue" label to the appropriate value.\n' +
+        'Do you want to commit the change directly, bypassing CQ (dangerous)?');
+    return true;
+  }
+
+  // Tree is open, do not block the change.
+  if (treeStatus.isOpen) {
+    disableConfirmSubmitMessage();
+    return true;
+  }
+
+  // Unknown tree state. Double check with the user before submitting.
+  if (treeStatus.generalState == 'unknown') {
+    setConfirmSubmitMessage(
+        config.treeName + ' tree status is unknown, submitting this ' +
+        'change now might be dangerous.  Submit anyway?');
+    return true;
+  }
+
+  // Tree is closed (or in some entirely unexpected state). Warn the user.
+  setConfirmSubmitMessage(
+      'The ' + config.treeName + ' tree is closed, submitting this ' +
+      'change is dangerous.  Submit anyway?');
+  return true;
+});
+
+});