blob: e5b75679d67a20405a5bd82a64ca387e3631b5ed [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * Logic for dealing with federated issue references.
7 */
8
9import loadGapi, {fetchGapiEmail} from './gapi-loader.js';
10
11const GOOGLE_ISSUE_TRACKER_REGEX = /^b\/\d+$/;
12
13const GOOGLE_ISSUE_TRACKER_API_ROOT = 'https://issuetracker.corp.googleapis.com';
14const GOOGLE_ISSUE_TRACKER_DISCOVERY_PATH = '/$discovery/rest';
15const GOOGLE_ISSUE_TRACKER_API_VERSION = 'v3';
16
17// Returns if shortlink is valid for any federated tracker.
18export function isShortlinkValid(shortlink) {
19 return FEDERATED_TRACKERS.some((TrackerClass) => {
20 try {
21 return new TrackerClass(shortlink);
22 } catch (e) {
23 if (e instanceof FederatedIssueError) {
24 return false;
25 } else {
26 throw e;
27 }
28 }
29 });
30}
31
32// Returns a issue instance for the first matching tracker.
33export function fromShortlink(shortlink) {
34 for (const key in FEDERATED_TRACKERS) {
35 if (FEDERATED_TRACKERS.hasOwnProperty(key)) {
36 const TrackerClass = FEDERATED_TRACKERS[key];
37 try {
38 return new TrackerClass(shortlink);
39 } catch (e) {
40 if (e instanceof FederatedIssueError) {
41 continue;
42 } else {
43 throw e;
44 }
45 }
46 }
47 }
48 return null;
49}
50
51// FederatedIssue is an abstract class for representing one federated issue.
52// Each supported tracker should subclass this class.
53class FederatedIssue {
54 constructor(shortlink) {
55 if (!this.isShortlinkValid(shortlink)) {
56 throw new FederatedIssueError(`Invalid tracker shortlink: ${shortlink}`);
57 }
58 this.shortlink = shortlink;
59 }
60
61 // isShortlinkValid returns whether a given shortlink is valid.
62 isShortlinkValid(shortlink) {
63 if (!(typeof shortlink === 'string')) {
64 throw new FederatedIssueError('shortlink argument must be a string.');
65 }
66 return Boolean(shortlink.match(this.shortlinkRe()));
67 }
68
69 // shortlinkRe returns the regex used to validate shortlinks.
70 shortlinkRe() {
71 throw new Error('Not implemented.');
72 }
73
74 // toURL returns the URL to this issue.
75 toURL() {
76 throw new Error('Not implemented.');
77 }
78
79 // toIssueRef converts the FedRef's information into an object having the
80 // IssueRef format everywhere on the front-end expects.
81 toIssueRef() {
82 return {
83 extIdentifier: this.shortlink,
84 };
85 }
86
87 // trackerName should return the name of the bug tracker.
88 get trackerName() {
89 throw new Error('Not implemented.');
90 }
91
92 // isOpen returns a Promise that resolves either true or false.
93 async isOpen() {
94 throw new Error('Not implemented.');
95 }
96}
97
98// Class for Google Issue Tracker (Buganizer) logic.
99export class GoogleIssueTrackerIssue extends FederatedIssue {
100 constructor(shortlink) {
101 super(shortlink);
102 this.issueID = Number(shortlink.substr(2));
103 this._federatedDetails = null;
104 }
105
106 shortlinkRe() {
107 return GOOGLE_ISSUE_TRACKER_REGEX;
108 }
109
110 toURL() {
111 return `https://issuetracker.google.com/issues/${this.issueID}`;
112 }
113
114 get trackerName() {
115 return 'Buganizer';
116 }
117
118 async getFederatedDetails() {
119 // Prevent fetching details more than once.
120 if (this._federatedDetails) {
121 return this._federatedDetails;
122 }
123
124 await loadGapi();
125 const email = await fetchGapiEmail();
126 if (!email) {
127 // Fail open.
128 return true;
129 }
130 const res = await this._loadGoogleIssueTrackerIssue(this.issueID);
131 if (!res || !res.result) {
132 // Fail open.
133 return null;
134 }
135
136 this._federatedDetails = res.result;
137 return this._federatedDetails;
138 }
139
140 // isOpen assumes getFederatedDetails has already been called, otherwise
141 // it will fail open (returning that the issue is open).
142 get isOpen() {
143 if (!this._federatedDetails) {
144 // Fail open.
145 return true;
146 }
147
148 // Open issues will not have a `resolvedTime`.
149 return !Boolean(this._federatedDetails.resolvedTime);
150 }
151
152 // summary assumes getFederatedDetails has already been called.
153 get summary() {
154 if (this._federatedDetails &&
155 this._federatedDetails.issueState &&
156 this._federatedDetails.issueState.title) {
157 return this._federatedDetails.issueState.title;
158 }
159 return null;
160 }
161
162 toIssueRef() {
163 return {
164 extIdentifier: this.shortlink,
165 summary: this.summary,
166 statusRef: {meansOpen: this.isOpen},
167 };
168 }
169
170 get _APIURL() {
171 return GOOGLE_ISSUE_TRACKER_API_ROOT + GOOGLE_ISSUE_TRACKER_DISCOVERY_PATH;
172 }
173
174 _loadGoogleIssueTrackerIssue(bugID) {
175 return new Promise((resolve, reject) => {
176 const version = GOOGLE_ISSUE_TRACKER_API_VERSION;
177 gapi.client.load(this._APIURL, version, () => {
178 const request = gapi.client.corp_issuetracker.issues.get({
179 'issueId': bugID,
180 });
181 request.execute((response) => {
182 resolve(response);
183 });
184 });
185 });
186 }
187}
188
189class FederatedIssueError extends Error {}
190
191// A list of supported tracker classes.
192const FEDERATED_TRACKERS = [
193 GoogleIssueTrackerIssue,
194];