blob: e5b75679d67a20405a5bd82a64ca387e3631b5ed [file] [log] [blame]
// 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.
/**
* Logic for dealing with federated issue references.
*/
import loadGapi, {fetchGapiEmail} from './gapi-loader.js';
const GOOGLE_ISSUE_TRACKER_REGEX = /^b\/\d+$/;
const GOOGLE_ISSUE_TRACKER_API_ROOT = 'https://issuetracker.corp.googleapis.com';
const GOOGLE_ISSUE_TRACKER_DISCOVERY_PATH = '/$discovery/rest';
const GOOGLE_ISSUE_TRACKER_API_VERSION = 'v3';
// Returns if shortlink is valid for any federated tracker.
export function isShortlinkValid(shortlink) {
return FEDERATED_TRACKERS.some((TrackerClass) => {
try {
return new TrackerClass(shortlink);
} catch (e) {
if (e instanceof FederatedIssueError) {
return false;
} else {
throw e;
}
}
});
}
// Returns a issue instance for the first matching tracker.
export function fromShortlink(shortlink) {
for (const key in FEDERATED_TRACKERS) {
if (FEDERATED_TRACKERS.hasOwnProperty(key)) {
const TrackerClass = FEDERATED_TRACKERS[key];
try {
return new TrackerClass(shortlink);
} catch (e) {
if (e instanceof FederatedIssueError) {
continue;
} else {
throw e;
}
}
}
}
return null;
}
// FederatedIssue is an abstract class for representing one federated issue.
// Each supported tracker should subclass this class.
class FederatedIssue {
constructor(shortlink) {
if (!this.isShortlinkValid(shortlink)) {
throw new FederatedIssueError(`Invalid tracker shortlink: ${shortlink}`);
}
this.shortlink = shortlink;
}
// isShortlinkValid returns whether a given shortlink is valid.
isShortlinkValid(shortlink) {
if (!(typeof shortlink === 'string')) {
throw new FederatedIssueError('shortlink argument must be a string.');
}
return Boolean(shortlink.match(this.shortlinkRe()));
}
// shortlinkRe returns the regex used to validate shortlinks.
shortlinkRe() {
throw new Error('Not implemented.');
}
// toURL returns the URL to this issue.
toURL() {
throw new Error('Not implemented.');
}
// toIssueRef converts the FedRef's information into an object having the
// IssueRef format everywhere on the front-end expects.
toIssueRef() {
return {
extIdentifier: this.shortlink,
};
}
// trackerName should return the name of the bug tracker.
get trackerName() {
throw new Error('Not implemented.');
}
// isOpen returns a Promise that resolves either true or false.
async isOpen() {
throw new Error('Not implemented.');
}
}
// Class for Google Issue Tracker (Buganizer) logic.
export class GoogleIssueTrackerIssue extends FederatedIssue {
constructor(shortlink) {
super(shortlink);
this.issueID = Number(shortlink.substr(2));
this._federatedDetails = null;
}
shortlinkRe() {
return GOOGLE_ISSUE_TRACKER_REGEX;
}
toURL() {
return `https://issuetracker.google.com/issues/${this.issueID}`;
}
get trackerName() {
return 'Buganizer';
}
async getFederatedDetails() {
// Prevent fetching details more than once.
if (this._federatedDetails) {
return this._federatedDetails;
}
await loadGapi();
const email = await fetchGapiEmail();
if (!email) {
// Fail open.
return true;
}
const res = await this._loadGoogleIssueTrackerIssue(this.issueID);
if (!res || !res.result) {
// Fail open.
return null;
}
this._federatedDetails = res.result;
return this._federatedDetails;
}
// isOpen assumes getFederatedDetails has already been called, otherwise
// it will fail open (returning that the issue is open).
get isOpen() {
if (!this._federatedDetails) {
// Fail open.
return true;
}
// Open issues will not have a `resolvedTime`.
return !Boolean(this._federatedDetails.resolvedTime);
}
// summary assumes getFederatedDetails has already been called.
get summary() {
if (this._federatedDetails &&
this._federatedDetails.issueState &&
this._federatedDetails.issueState.title) {
return this._federatedDetails.issueState.title;
}
return null;
}
toIssueRef() {
return {
extIdentifier: this.shortlink,
summary: this.summary,
statusRef: {meansOpen: this.isOpen},
};
}
get _APIURL() {
return GOOGLE_ISSUE_TRACKER_API_ROOT + GOOGLE_ISSUE_TRACKER_DISCOVERY_PATH;
}
_loadGoogleIssueTrackerIssue(bugID) {
return new Promise((resolve, reject) => {
const version = GOOGLE_ISSUE_TRACKER_API_VERSION;
gapi.client.load(this._APIURL, version, () => {
const request = gapi.client.corp_issuetracker.issues.get({
'issueId': bugID,
});
request.execute((response) => {
resolve(response);
});
});
});
}
}
class FederatedIssueError extends Error {}
// A list of supported tracker classes.
const FEDERATED_TRACKERS = [
GoogleIssueTrackerIssue,
];