@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorJIRAAuthProvider
4 extends PhabricatorOAuth1AuthProvider
5 implements DoorkeeperRemarkupURIInterface {
6
7 public function getProviderName() {
8 return pht('JIRA');
9 }
10
11 public function getDescriptionForCreate() {
12 return pht('Configure JIRA OAuth. NOTE: Only supports JIRA 6.');
13 }
14
15 public function getConfigurationHelp() {
16 return $this->getProviderConfigurationHelp();
17 }
18
19 protected function getProviderConfigurationHelp() {
20 if ($this->isSetup()) {
21 return pht(
22 "**Step 1 of 2**: Provide the name and URI for your JIRA install.\n\n".
23 "In the next step, you will configure JIRA.");
24 } else {
25 $login_uri = PhabricatorEnv::getURI($this->getLoginURI());
26 return pht(
27 "**Step 2 of 2**: In this step, you will configure JIRA.\n\n".
28 "**Create a JIRA Application**: Log into JIRA and go to ".
29 "**Administration**, then **Add-ons**, then **Application Links**. ".
30 "Click the button labeled **Add Application Link**, and use these ".
31 "settings to create an application:\n\n".
32 " - **Server URL**: `%s`\n".
33 " - Then, click **Next**. On the second page:\n".
34 " - **Application Name**: `%s`\n".
35 " - **Application Type**: `Generic Application`\n".
36 " - Then, click **Create**.\n\n".
37 "**Configure Your Application**: Find the application you just ".
38 "created in the table, and click the **Configure** link under ".
39 "**Actions**. Select **Incoming Authentication** and click the ".
40 "**OAuth** tab (it may be selected by default). Then, use these ".
41 "settings:\n\n".
42 " - **Consumer Key**: Set this to the \"Consumer Key\" value in the ".
43 "form above.\n".
44 " - **Consumer Name**: `%s`\n".
45 " - **Public Key**: Set this to the \"Public Key\" value in the ".
46 "form above.\n".
47 " - **Consumer Callback URL**: `%s`\n".
48 "Click **Save** in JIRA. Authentication should now be configured, ".
49 "and this provider should work correctly.",
50 PhabricatorEnv::getProductionURI('/'),
51 PlatformSymbols::getPlatformServerName(),
52 PlatformSymbols::getPlatformServerName(),
53 $login_uri);
54 }
55 }
56
57 protected function newOAuthAdapter() {
58 $config = $this->getProviderConfig();
59
60 return id(new PhutilJIRAAuthAdapter())
61 ->setAdapterDomain($config->getProviderDomain())
62 ->setJIRABaseURI($config->getProperty(self::PROPERTY_JIRA_URI))
63 ->setPrivateKey(
64 new PhutilOpaqueEnvelope(
65 $config->getProperty(self::PROPERTY_PRIVATE_KEY)));
66 }
67
68 protected function getLoginIcon() {
69 return 'Jira';
70 }
71
72 private function isSetup() {
73 return !$this->getProviderConfig()->getID();
74 }
75
76 const PROPERTY_JIRA_NAME = 'oauth1:jira:name';
77 const PROPERTY_JIRA_URI = 'oauth1:jira:uri';
78 const PROPERTY_PUBLIC_KEY = 'oauth1:jira:key:public';
79 const PROPERTY_PRIVATE_KEY = 'oauth1:jira:key:private';
80 const PROPERTY_REPORT_LINK = 'oauth1:jira:report:link';
81 const PROPERTY_REPORT_COMMENT = 'oauth1:jira:report:comment';
82
83
84 public function readFormValuesFromProvider() {
85 $config = $this->getProviderConfig();
86 $uri = $config->getProperty(self::PROPERTY_JIRA_URI);
87
88 return array(
89 self::PROPERTY_JIRA_NAME => $this->getProviderDomain(),
90 self::PROPERTY_JIRA_URI => $uri,
91 );
92 }
93
94 public function readFormValuesFromRequest(AphrontRequest $request) {
95 $is_setup = $this->isSetup();
96 if ($is_setup) {
97 $name = $request->getStr(self::PROPERTY_JIRA_NAME);
98 } else {
99 $name = $this->getProviderDomain();
100 }
101
102 return array(
103 self::PROPERTY_JIRA_NAME => $name,
104 self::PROPERTY_JIRA_URI => $request->getStr(self::PROPERTY_JIRA_URI),
105 self::PROPERTY_REPORT_LINK =>
106 $request->getInt(self::PROPERTY_REPORT_LINK, 0),
107 self::PROPERTY_REPORT_COMMENT =>
108 $request->getInt(self::PROPERTY_REPORT_COMMENT, 0),
109 );
110 }
111
112 public function processEditForm(
113 AphrontRequest $request,
114 array $values) {
115 $errors = array();
116 $issues = array();
117
118 $is_setup = $this->isSetup();
119
120 $key_name = self::PROPERTY_JIRA_NAME;
121 $key_uri = self::PROPERTY_JIRA_URI;
122
123 if (!strlen($values[$key_name])) {
124 $errors[] = pht('JIRA instance name is required.');
125 $issues[$key_name] = pht('Required');
126 } else if (!preg_match('/^[a-z0-9.]+\z/', $values[$key_name])) {
127 $errors[] = pht(
128 'JIRA instance name must contain only lowercase letters, digits, and '.
129 'period.');
130 $issues[$key_name] = pht('Invalid');
131 }
132
133 if (!strlen($values[$key_uri])) {
134 $errors[] = pht('JIRA base URI is required.');
135 $issues[$key_uri] = pht('Required');
136 } else {
137 $uri = new PhutilURI($values[$key_uri]);
138 if (!$uri->getProtocol()) {
139 $errors[] = pht(
140 'JIRA base URI should include protocol (like "https://").');
141 $issues[$key_uri] = pht('Invalid');
142 }
143 }
144
145 if (!$errors && $is_setup) {
146 $config = $this->getProviderConfig();
147
148 $config->setProviderDomain($values[$key_name]);
149
150 $consumer_key = 'phjira.'.Filesystem::readRandomCharacters(16);
151 list($public, $private) = PhutilJIRAAuthAdapter::newJIRAKeypair();
152
153 $config->setProperty(self::PROPERTY_PUBLIC_KEY, $public);
154 $config->setProperty(self::PROPERTY_PRIVATE_KEY, $private);
155 $config->setProperty(self::PROPERTY_CONSUMER_KEY, $consumer_key);
156 }
157
158 return array($errors, $issues, $values);
159 }
160
161 public function extendEditForm(
162 AphrontRequest $request,
163 AphrontFormView $form,
164 array $values,
165 array $issues) {
166
167 if (!function_exists('openssl_pkey_new')) {
168 // TODO: This could be a bit prettier.
169 throw new Exception(
170 pht(
171 "The PHP 'openssl' extension is not installed. You must install ".
172 "this extension in order to add a JIRA authentication provider, ".
173 "because JIRA OAuth requests use the RSA-SHA1 signing algorithm. ".
174 "Install the 'openssl' extension, restart everything, and try ".
175 "again."));
176 }
177
178 $form->appendRemarkupInstructions(
179 pht(
180 'NOTE: This provider **only supports JIRA 6**. It will not work with '.
181 'JIRA 5 or earlier.'));
182
183 $is_setup = $this->isSetup();
184 $viewer = $request->getViewer();
185
186 $e_required = $request->isFormPost() ? null : true;
187
188 $v_name = $values[self::PROPERTY_JIRA_NAME];
189 if ($is_setup) {
190 $e_name = idx($issues, self::PROPERTY_JIRA_NAME, $e_required);
191 } else {
192 $e_name = null;
193 }
194
195 $v_uri = $values[self::PROPERTY_JIRA_URI];
196 $e_uri = idx($issues, self::PROPERTY_JIRA_URI, $e_required);
197
198 if ($is_setup) {
199 $form
200 ->appendRemarkupInstructions(
201 pht(
202 "**JIRA Instance Name**\n\n".
203 "Choose a permanent name for this instance of JIRA. This name is ".
204 "used internally to keep track of this particular instance of ".
205 "JIRA, in case the URL changes later.\n\n".
206 "Use lowercase letters, digits, and period. For example, ".
207 "`jira`, `jira.mycompany` or `jira.engineering` are reasonable ".
208 "names."))
209 ->appendChild(
210 id(new AphrontFormTextControl())
211 ->setLabel(pht('JIRA Instance Name'))
212 ->setValue($v_name)
213 ->setName(self::PROPERTY_JIRA_NAME)
214 ->setError($e_name));
215 } else {
216 $form
217 ->appendChild(
218 id(new AphrontFormStaticControl())
219 ->setLabel(pht('JIRA Instance Name'))
220 ->setValue($v_name));
221 }
222
223 $form
224 ->appendChild(
225 id(new AphrontFormTextControl())
226 ->setLabel(pht('JIRA Base URI'))
227 ->setValue($v_uri)
228 ->setName(self::PROPERTY_JIRA_URI)
229 ->setCaption(
230 pht(
231 'The URI where JIRA is installed. For example: %s',
232 phutil_tag('tt', array(), 'https://jira.mycompany.com/')))
233 ->setError($e_uri));
234
235 if (!$is_setup) {
236 $config = $this->getProviderConfig();
237
238
239 $ckey = $config->getProperty(self::PROPERTY_CONSUMER_KEY);
240 $ckey = phutil_tag('tt', array(), $ckey);
241
242 $pkey = $config->getProperty(self::PROPERTY_PUBLIC_KEY);
243 $pkey = phutil_escape_html_newlines($pkey);
244 $pkey = phutil_tag('tt', array(), $pkey);
245
246 $form
247 ->appendRemarkupInstructions(
248 pht(
249 'NOTE: **To complete setup**, copy and paste these keys into JIRA '.
250 'according to the instructions below.'))
251 ->appendChild(
252 id(new AphrontFormStaticControl())
253 ->setLabel(pht('Consumer Key'))
254 ->setValue($ckey))
255 ->appendChild(
256 id(new AphrontFormStaticControl())
257 ->setLabel(pht('Public Key'))
258 ->setValue($pkey));
259
260 $form
261 ->appendRemarkupInstructions(
262 pht(
263 '= Integration Options = '."\n".
264 'Configure how to record Revisions on JIRA tasks.'."\n\n".
265 'Note you\'ll have to restart the daemons for this to take '.
266 'effect.'))
267 ->appendChild(
268 id(new AphrontFormCheckboxControl())
269 ->addCheckbox(
270 self::PROPERTY_REPORT_LINK,
271 1,
272 new PHUIRemarkupView(
273 $viewer,
274 pht(
275 'Create **Issue Link** to the Revision, as an "implemented '.
276 'in" relationship.')),
277 $this->shouldCreateJIRALink()))
278 ->appendChild(
279 id(new AphrontFormCheckboxControl())
280 ->addCheckbox(
281 self::PROPERTY_REPORT_COMMENT,
282 1,
283 new PHUIRemarkupView(
284 $viewer,
285 pht(
286 '**Post a comment** in the JIRA task.')),
287 $this->shouldCreateJIRAComment()));
288 }
289
290 }
291
292 /**
293 * JIRA uses a setup step to generate public/private keys.
294 */
295 public function hasSetupStep() {
296 return true;
297 }
298
299 public static function getJIRAProvider() {
300 $providers = self::getAllEnabledProviders();
301
302 foreach ($providers as $provider) {
303 if ($provider instanceof PhabricatorJIRAAuthProvider) {
304 return $provider;
305 }
306 }
307
308 return null;
309 }
310
311 public function newJIRAFuture(
312 PhabricatorExternalAccount $account,
313 $path,
314 $method,
315 $params = array()) {
316
317 $adapter = clone $this->getAdapter();
318 $adapter->setToken($account->getProperty('oauth1.token'));
319 $adapter->setTokenSecret($account->getProperty('oauth1.token.secret'));
320
321 return $adapter->newJIRAFuture($path, $method, $params);
322 }
323
324 public function shouldCreateJIRALink() {
325 $config = $this->getProviderConfig();
326 return $config->getProperty(self::PROPERTY_REPORT_LINK, true);
327 }
328
329 public function shouldCreateJIRAComment() {
330 $config = $this->getProviderConfig();
331 return $config->getProperty(self::PROPERTY_REPORT_COMMENT, true);
332 }
333
334/* -( DoorkeeperRemarkupURIInterface )------------------------------------- */
335
336 public function getDoorkeeperURIRef(PhutilURI $uri) {
337 $uri_string = phutil_string_cast($uri);
338
339 $pattern = '((https?://\S+?)/browse/([A-Z][A-Z0-9]*-[1-9]\d*))';
340 $matches = null;
341 if (!preg_match($pattern, $uri_string, $matches)) {
342 return null;
343 }
344
345 if (strlen($uri->getFragment())) {
346 return null;
347 }
348
349 if ($uri->getQueryParamsAsPairList()) {
350 return null;
351 }
352
353 $domain = $matches[1];
354 $issue = $matches[2];
355
356 $config = $this->getProviderConfig();
357 $base_uri = $config->getProperty(self::PROPERTY_JIRA_URI);
358
359 if ($domain !== rtrim($base_uri, '/')) {
360 return null;
361 }
362
363 return id(new DoorkeeperURIRef())
364 ->setURI($uri)
365 ->setApplicationType(DoorkeeperBridgeJIRA::APPTYPE_JIRA)
366 ->setApplicationDomain($this->getProviderDomain())
367 ->setObjectType(DoorkeeperBridgeJIRA::OBJTYPE_ISSUE)
368 ->setObjectID($issue);
369 }
370
371}