Passed
Push — master ( cf401d...252787 )
by Michael
02:49
created

Jira   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 472
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 472
rs 8.3999
c 0
b 0
f 0
wmc 46

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 2
A parseIssueSyntax() 0 12 2
A getProjectIssueSeparator() 0 3 1
B isConfigured() 0 26 5
A isOurWebhook() 0 5 1
A getIssueURL() 0 3 1
A isIssueValid() 0 9 1
A handleWebhook() 0 10 1
B createWebhook() 0 44 2
A hydrateConfigForm() 0 10 1
A getTotalIssuesBeingImported() 0 3 1
B validateWebhook() 0 23 5
A retrieveIssue() 0 22 2
A getListOfAllUserOrganisations() 0 3 1
B getListOfAllReposAndHooks() 0 23 4
A setIssueData() 0 22 3
B retrieveAllIssues() 0 28 4
A getUserString() 0 3 1
B deleteWebhook() 0 46 3
A handleAuthorization() 0 18 4
A makeJiraRequest() 0 11 1

How to fix   Complexity   

Complex Class

Complex classes like Jira often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Jira, and based on these observations, apply Extract Interface, too.

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
12
use dokuwiki\Form\Form;
13
use dokuwiki\plugin\issuelinks\classes\ExternalServerException;
14
use dokuwiki\plugin\issuelinks\classes\HTTPRequestException;
15
use dokuwiki\plugin\issuelinks\classes\Issue;
16
use dokuwiki\plugin\issuelinks\classes\Repository;
17
use dokuwiki\plugin\issuelinks\classes\RequestResult;
18
19
class Jira extends AbstractService
20
{
21
22
    const SYNTAX = 'jira';
23
    const DISPLAY_NAME = 'Jira';
24
    const ID = 'jira';
25
26
    protected $dokuHTTPClient;
27
    protected $jiraUrl;
28
    protected $token;
29
    protected $configError;
30
    protected $authUser;
31
    protected $total;
32
33
    // FIXME should this be rather protected?
34
    public function __construct()
35
    {
36
        $this->dokuHTTPClient = new \DokuHTTPClient();
37
        /** @var \helper_plugin_issuelinks_db $db */
38
        $db = plugin_load('helper', 'issuelinks_db');
39
        $jiraUrl = $db->getKeyValue('jira_url');
40
        $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

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