Jira::hydrateConfigForm()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 9
c 2
b 0
f 0
nc 1
nop 1
dl 0
loc 11
ccs 0
cts 10
cp 0
crap 2
rs 9.9666
1
<?php
2
/**
3
 * Created by IntelliJ IDEA.
4
 * User: michael
5
 * Date: 4/16/18
6
 * Time: 7:57 AM
7
 */
8
9
namespace dokuwiki\plugin\issuelinks\services;
10
11
use dokuwiki\Form\Form;
12
use dokuwiki\plugin\issuelinks\classes\Issue;
13
use dokuwiki\plugin\issuelinks\classes\Repository;
14
use dokuwiki\plugin\issuelinks\classes\RequestResult;
15
16
class Jira extends AbstractService
17
{
18
19
    const SYNTAX = 'jira';
20
    const DISPLAY_NAME = 'Jira';
21
    const ID = 'jira';
22
23
    protected $dokuHTTPClient;
24
    protected $jiraUrl;
25
    protected $token;
26
    protected $configError;
27
    protected $authUser;
28
    protected $total;
29
30
    // FIXME should this be rather protected?
31 1
    public function __construct()
32
    {
33 1
        $this->dokuHTTPClient = new \DokuHTTPClient();
34
        /** @var \helper_plugin_issuelinks_db $db */
35 1
        $db = plugin_load('helper', 'issuelinks_db');
36 1
        $jiraUrl = $db->getKeyValue('jira_url');
37 1
        $this->jiraUrl = $jiraUrl ? trim($jiraUrl, '/') : null;
0 ignored issues
show
Bug introduced by
It seems like $jiraUrl can also be of type true; however, parameter $str of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

37
        $this->jiraUrl = $jiraUrl ? trim(/** @scrutinizer ignore-type */ $jiraUrl, '/') : null;
Loading history...
38 1
        $authToken = $db->getKeyValue('jira_token');
39 1
        $this->token = $authToken;
40 1
        $jiraUser = $db->getKeyValue('jira_user');
41 1
        $this->authUser = $jiraUser;
42 1
    }
43
44
    /**
45
     * Decide whether the provided issue is valid
46
     *
47
     * @param Issue $issue
48
     *
49
     * @return bool
50
     */
51
    public static function isIssueValid(Issue $issue)
52
    {
53
        $summary = $issue->getSummary();
54
        $valid = !blank($summary);
55
        $status = $issue->getStatus();
56
        $valid &= !blank($status);
57
        $type = $issue->getType();
58
        $valid &= !blank($type);
59
        return $valid;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $valid returns the type integer which is incompatible with the documented return type boolean.
Loading history...
60
    }
61
62
    /**
63
     * Provide the character separation the project name from the issue number, may be different for merge requests
64
     *
65
     * @param bool $isMergeRequest
66
     *
67
     * @return string
68
     */
69
    public static function getProjectIssueSeparator($isMergeRequest)
70
    {
71
        return '-';
72
    }
73
74
    public static function isOurWebhook()
75
    {
76
        global $INPUT;
77
        $userAgent = $INPUT->server->str('HTTP_USER_AGENT');
78
        return strpos($userAgent, 'Atlassian') === 0;
79
    }
80
81
    /**
82
     * Get the url to the given issue at the given project
83
     *
84
     * @param      $projectId
85
     * @param      $issueId
86
     * @param bool $isMergeRequest ignored, GitHub routes the requests correctly by itself
87
     *
88
     * @return string
89
     */
90
    public function getIssueURL($projectId, $issueId, $isMergeRequest)
91
    {
92
        return $this->jiraUrl . '/browse/' . $projectId . '-' . $issueId;
93
    }
94
95
    /**
96
     * @param string $issueSyntax
97
     *
98
     * @return Issue
99
     */
100 2
    public function parseIssueSyntax($issueSyntax)
101
    {
102 2
        if (preg_match('/^\w+\-[1-9]\d*$/', $issueSyntax) !== 1) {
103
            return null;
104
        }
105
106 2
        list($projectKey, $issueNumber) = explode('-', $issueSyntax);
107
108 2
        $issue = Issue::getInstance('jira', $projectKey, $issueNumber, false);
109 2
        $issue->getFromDB();
110
111 2
        return $issue;
112
    }
113
114
    /**
115
     * @return bool
116
     */
117
    public function isConfigured()
118
    {
119
        if (null === $this->jiraUrl) {
120
            $this->configError = 'Jira URL not set!';
121
            return false;
122
        }
123
124
        if (empty($this->token)) {
125
            $this->configError = 'Authentication token is missing!';
126
            return false;
127
        }
128
129
        if (empty($this->authUser)) {
130
            $this->configError = 'Authentication user is missing!';
131
            return false;
132
        }
133
134
        try {
135
            $this->makeJiraRequest('/rest/webhooks/1.0/webhook', [], 'GET');
136
//            $user = $this->makeJiraRequest('/rest/api/2/user', [], 'GET');
137
        } catch (\Exception $e) {
138
            $this->configError = 'The Jira authentication failed with message: ' . hsc($e->getMessage());
139
            return false;
140
        }
141
142
        return true;
143
    }
144
145
    protected function makeJiraRequest($endpoint, array $data, $method, array $headers = [])
146
    {
147
        $url = $this->jiraUrl . $endpoint;
148
        $defaultHeaders = [
149
            'Authorization' => 'Basic ' . base64_encode("$this->authUser:$this->token"),
150
            'Content-Type' => 'application/json',
151
        ];
152
153
        $requestHeaders = array_merge($defaultHeaders, $headers);
154
155
        return $this->makeHTTPRequest($this->dokuHTTPClient, $url, $requestHeaders, $data, $method);
156
    }
157
158
    /**
159
     * @param Form $configForm
160
     *
161
     * @return void
162
     */
163
    public function hydrateConfigForm(Form $configForm)
164
    {
165
        $url = 'https://id.atlassian.com/manage/api-tokens';
166
        $link = "<a href=\"$url\">$url</a>";
167
        $message = "Please go to $link and generate a new token for this plugin.";
168
        $configForm->addHTML("<p>{$this->configError} $message</p>");
169
        $configForm->addTextInput('jira_url', 'Jira Url')->val($this->jiraUrl);
170
        $configForm->addTextInput('jira_user', 'Jira User')
171
            ->val($this->authUser)
172
            ->attr('placeholder', '[email protected]');
173
        $configForm->addPasswordInput('jira_token', 'Jira AccessToken')->useInput(false);
174
    }
175
176
    public function handleAuthorization()
177
    {
178
        global $INPUT;
179
180
        $token = $INPUT->str('jira_token');
181
        $url = $INPUT->str('jira_url');
182
        $user = $INPUT->str('jira_user');
183
184
        /** @var \helper_plugin_issuelinks_db $db */
185
        $db = plugin_load('helper', 'issuelinks_db');
186
        if (!empty($token)) {
187
            $db->saveKeyValuePair('jira_token', $token);
188
        }
189
        if (!empty($url)) {
190
            $db->saveKeyValuePair('jira_url', $url);
191
        }
192
        if (!empty($user)) {
193
            $db->saveKeyValuePair('jira_user', $user);
194
        }
195
    }
196
197
    public function getUserString()
198
    {
199
        return hsc($this->authUser);
200
    }
201
202
    public function getRepoPageText()
203
    {
204
        /** @var \helper_plugin_issuelinks_db $db */
205
        $db = plugin_load('helper', 'issuelinks_db');
206
        $jira_url = $db->getKeyValue('jira_url');
207
        $href = $jira_url . '/plugins/servlet/webhooks';
208
        $msg = $db->getLang('jira:webhook settings link');
209
        $link = "<a href=\"$href\" target='_blank'>$msg</a>";
210
        return $link;
211
    }
212
213
    public function retrieveIssue(Issue $issue)
214
    {
215
        // FIXME: somehow validate that we are allowed to retrieve that issue
216
217
        $projectKey = $issue->getProject();
218
219
        /** @var \helper_plugin_issuelinks_db $db */
220
        $db = plugin_load('helper', 'issuelinks_db');
221
        $webhooks = $db->getWebhooks('jira');
222
        $allowedRepos = explode(',', $webhooks[0]['repository_id']);
223
224
        if (!in_array($projectKey, $allowedRepos, true)) {
225
//            Jira Projects must be enabled as Webhook for on-demand fetching
226
            return;
227
        }
228
229
230
        $issueNumber = $issue->getKey();
231
        $endpoint = "/rest/api/2/issue/$projectKey-$issueNumber";
232
233
        $issueData = $this->makeJiraRequest($endpoint, [], 'GET');
234
        $this->setIssueData($issue, $issueData);
235
    }
236
237
    protected function setIssueData(Issue $issue, $issueData)
238
    {
239
        $issue->setSummary($issueData['fields']['summary']);
240
        $issue->setStatus($issueData['fields']['status']['name']);
241
        $issue->setDescription($issueData['fields']['description']);
242
        $issue->setType($issueData['fields']['issuetype']['name']);
243
        $issue->setPriority($issueData['fields']['priority']['name']);
244
245
        $issue->setUpdated($issueData['fields']['updated']);
246
        $versions = array_column($issueData['fields']['fixVersions'], 'name');
247
        $issue->setVersions($versions);
248
        $components = array_column($issueData['fields']['components'], 'name');
249
        $issue->setComponents($components);
250
        $issue->setLabels($issueData['fields']['labels']);
251
252
        if ($issueData['fields']['assignee']) {
253
            $assignee = $issueData['fields']['assignee'];
254
            $issue->setAssignee($assignee['displayName'], $assignee['avatarUrls']['48x48']);
255
        }
256
257
        if ($issueData['fields']['duedate']) {
258
            $issue->setDuedate($issueData['fields']['duedate']);
259
        }
260
261
        // FIXME: check and handle these fields:
262
//        $issue->setParent($issueData['fields']['parent']['key']);
263
    }
264
265
    public function retrieveAllIssues($projectKey, &$startat = 0)
266
    {
267
        $jqlQuery = "project=$projectKey";
268
//        $jqlQuery = urlencode("project=$projectKey ORDER BY updated DESC");
269
        $endpoint = '/rest/api/2/search?jql=' . $jqlQuery . '&maxResults=50&startAt=' . $startat;
270
        $result = $this->makeJiraRequest($endpoint, [], 'GET');
271
272
        if (empty($result['issues'])) {
273
            return [];
274
        }
275
276
        $this->total = $result['total'];
277
278
        $startat += $result['maxResults'];
279
280
        $retrievedIssues = [];
281
        foreach ($result['issues'] as $issueData) {
282
            list(, $issueNumber) = explode('-', $issueData['key']);
283
            try {
284
                $issue = Issue::getInstance('jira', $projectKey, $issueNumber, false);
285
            } catch (\InvalidArgumentException $e) {
286
                continue;
287
            }
288
            $this->setIssueData($issue, $issueData);
289
            $issue->saveToDB();
290
            $retrievedIssues[] = $issue;
291
        }
292
        return $retrievedIssues;
293
    }
294
295
    /**
296
     * Get the total of issues currently imported by retrieveAllIssues()
297
     *
298
     * This may be an estimated number
299
     *
300
     * @return int
301
     */
302
    public function getTotalIssuesBeingImported()
303
    {
304
        return $this->total;
305
    }
306
307
    /**
308
     * Get a list of all organisations a user is member of
309
     *
310
     * @return string[] the identifiers of the organisations
311
     */
312
    public function getListOfAllUserOrganisations()
313
    {
314
        return ['All projects'];
315
    }
316
317
    /**
318
     * @param $organisation
319
     *
320
     * @return Repository[]
321
     */
322
    public function getListOfAllReposAndHooks($organisation)
323
    {
324
        /** @var \helper_plugin_issuelinks_db $db */
325
        $db = plugin_load('helper', 'issuelinks_db');
326
        $webhooks = $db->getWebhooks('jira');
327
        $subscribedProjects = [];
328
        if (!empty($webhooks)) {
329
            $subscribedProjects = explode(',', $webhooks[0]['repository_id']);
330
        }
331
332
        $projects = $this->makeJiraRequest('/rest/api/2/project', [], 'GET');
333
334
        $repositories = [];
335
        foreach ($projects as $project) {
336
            $repo = new Repository();
337
            $repo->displayName = $project['name'];
338
            $repo->full_name = $project['key'];
339
            if (in_array($project['key'], $subscribedProjects)) {
340
                $repo->hookID = 1;
341
            }
342
            $repositories[] = $repo;
343
        }
344
        return $repositories;
345
    }
346
347
    public function createWebhook($project)
348
    {
349
        // get old webhook id
350
        /** @var \helper_plugin_issuelinks_db $db */
351
        $db = plugin_load('helper', 'issuelinks_db');
352
        $webhooks = $db->getWebhooks('jira');
353
        $projects = [];
354
        if (!empty($webhooks)) {
355
            $oldID = $webhooks[0]['id'];
356
            // get current webhook projects
357
            $projects = explode(',', $webhooks[0]['repository_id']);
358
            // remove old webhook
359
            $this->makeJiraRequest('/rest/webhooks/1.0/webhook/' . $oldID, [], 'DELETE');
360
            // delete old webhook from database
361
            $db->deleteWebhook('jira', $webhooks[0]['repository_id'], $oldID);
362
        }
363
364
        // add new project
365
        $projects[] = $project;
366
        $projects = array_filter(array_unique($projects));
367
        $projectsString = implode(',', $projects);
368
369
        // add new webhooks
370
        global $conf;
371
        $payload = [
372
            'name' => 'dokuwiki plugin issuelinks for Wiki: ' . $conf['title'],
373
            'url' => self::WEBHOOK_URL,
374
            'events' => [
375
                'jira:issue_created',
376
                'jira:issue_updated',
377
            ],
378
            'description' => 'dokuwiki plugin issuelinks for Wiki: ' . $conf['title'],
379
            'jqlFilter' => "project in ($projectsString)",
380
            'excludeIssueDetails' => 'false',
381
        ];
382
383
        $response = $this->makeJiraRequest('/rest/webhooks/1.0/webhook', $payload, 'POST');
384
385
        $selfLink = $response['self'];
386
        $newWebhookID = substr($selfLink, strrpos($selfLink, '/') + 1);
387
388
        // store new webhook to database
389
        $db->saveWebhook('jira', $projectsString, $newWebhookID, 'jira rest webhooks have no secrets :/');
390
        return ['status' => 200, 'data' => ['id' => $newWebhookID]];
391
    }
392
393
    /**
394
     * Delete our webhook in a source repository
395
     *
396
     * @param     $project
397
     * @param int $hookid the numerical id of the hook to be deleted
398
     *
399
     * @return array
400
     */
401
    public function deleteWebhook($project, $hookid)
402
    {
403
        // get old webhook id
404
        /** @var \helper_plugin_issuelinks_db $db */
405
        $db = plugin_load('helper', 'issuelinks_db');
406
        $webhooks = $db->getWebhooks('jira');
407
        $projects = [];
408
        if (!empty($webhooks)) {
409
            $oldID = $webhooks[0]['id'];
410
            // get current webhook projects
411
            $projects = explode(',', $webhooks[0]['repository_id']);
412
            // remove old webhook
413
            $this->makeJiraRequest('/rest/webhooks/1.0/webhook/' . $oldID, [], 'DELETE');
414
            // delete old webhook from database
415
            $db->deleteWebhook('jira', $webhooks[0]['repository_id'], $oldID);
416
        }
417
418
        // remove project
419
        $projects = array_filter(array_diff($projects, [$project]));
420
        if (empty($projects)) {
421
            return ['status' => 204, 'data' => ''];
422
        }
423
424
        $projectsString = implode(',', $projects);
425
426
        // add new webhooks
427
        global $conf;
428
        $payload = [
429
            'name' => 'dokuwiki plugin issuelinks for Wiki: ' . $conf['title'],
430
            'url' => self::WEBHOOK_URL,
431
            'events' => [
432
                'jira:issue_created',
433
                'jira:issue_updated',
434
            ],
435
            'description' => 'dokuwiki plugin issuelinks for Wiki: ' . $conf['title'],
436
            'jqlFilter' => "project in ($projectsString)",
437
            'excludeIssueDetails' => 'false',
438
        ];
439
        $response = $this->makeJiraRequest('/rest/webhooks/1.0/webhook', $payload, 'POST');
440
        $selfLink = $response['self'];
441
        $newWebhookID = substr($selfLink, strrpos($selfLink, '/') + 1);
442
443
        // store new webhook to database
444
        $db->saveWebhook('jira', $projectsString, $newWebhookID, 'jira rest webhooks have no secrets :/');
445
446
        return ['status' => 204, 'data' => ''];
447
    }
448
449
    /**
450
     * Do all checks to verify that the webhook is expected and actually ours
451
     *
452
     * @param $webhookBody
453
     *
454
     * @return true|RequestResult true if the the webhook is our and should be processed RequestResult with explanation
455
     *                            otherwise
456
     */
457
    public function validateWebhook($webhookBody)
458
    {
459
        $data = json_decode($webhookBody, true);
460
        /** @var \helper_plugin_issuelinks_db $db */
461
        $db = plugin_load('helper', 'issuelinks_db');
462
        $webhooks = $db->getWebhooks('jira');
463
        $projects = [];
464
        if (!empty($webhooks)) {
465
            // get current webhook projects
466
            $projects = explode(',', $webhooks[0]['repository_id']);
467
        }
468
469
        if (!$data['webhookEvent'] || !in_array($data['webhookEvent'], ['jira:issue_updated', 'jira:issue_created'])) {
470
            return new RequestResult(400, 'unknown webhook event');
471
        }
472
473
        list($projectKey, $issueId) = explode('-', $data['issue']['key']);
474
475
        if (!in_array($projectKey, $projects)) {
476
            return new RequestResult(202, 'Project ' . $projectKey . ' is not handled by this wiki.');
477
        }
478
479
        return true;
480
    }
481
482
    /**
483
     * Handle the contents of the webhooks body
484
     *
485
     * @param $webhookBody
486
     *
487
     * @return RequestResult
488
     */
489
    public function handleWebhook($webhookBody)
490
    {
491
        $data = json_decode($webhookBody, true);
492
        $issueData = $data['issue'];
493
        list($projectKey, $issueId) = explode('-', $issueData['key']);
494
        $issue = Issue::getInstance('jira', $projectKey, $issueId, false);
495
        $this->setIssueData($issue, $issueData);
496
        $issue->saveToDB();
497
498
        return new RequestResult(200, 'OK');
499
    }
500
}
501