Passed
Pull Request — master (#1)
by Michael
02:10
created

Jira::handleAuthorization()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 8
nop 0
dl 0
loc 18
rs 9.2
c 0
b 0
f 0
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;
1 ignored issue
show
Bug introduced by
The type dokuwiki\Form\Form was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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();
1 ignored issue
show
Bug introduced by
The type DokuHTTPClient was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
37
        /** @var \helper_plugin_issuelinks_db $db */
38
        $db = plugin_load('helper', 'issuelinks_db');
1 ignored issue
show
Bug introduced by
The function plugin_load was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

38
        $db = /** @scrutinizer ignore-call */ plugin_load('helper', 'issuelinks_db');
Loading history...
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
     * Get the url to the given issue at the given project
49
     *
50
     * @param      $projectId
51
     * @param      $issueId
52
     * @param bool $isMergeRequest ignored, GitHub routes the requests correctly by itself
53
     *
54
     * @return string
55
     */
56
    public function getIssueURL($projectId, $issueId, $isMergeRequest)
57
    {
58
        return $this->jiraUrl . '/browse/' . $projectId . '-' . $issueId;
59
    }
60
61
    /**
62
     * Decide whether the provided issue is valid
63
     *
64
     * @param Issue $issue
65
     *
66
     * @return bool
67
     */
68
    public static function isIssueValid(Issue $issue)
69
    {
70
        $summary = $issue->getSummary();
71
        $valid = !blank($summary);
1 ignored issue
show
Bug introduced by
The function blank was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

71
        $valid = !/** @scrutinizer ignore-call */ blank($summary);
Loading history...
72
        $status = $issue->getStatus();
73
        $valid &= !blank($status);
74
        $type = $issue->getType();
75
        $valid &= !blank($type);
76
        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...
77
    }
78
79
    /**
80
     * Provide the character separation the project name from the issue number, may be different for merge requests
81
     *
82
     * @param bool $isMergeRequest
83
     *
84
     * @return string
85
     */
86
    public static function getProjectIssueSeparator($isMergeRequest)
87
    {
88
        return '-';
89
    }
90
91
    /**
92
     * @param string $issueSyntax
93
     *
94
     * @return Issue
95
     */
96
    public function parseIssueSyntax($issueSyntax)
97
    {
98
        if (preg_match('/^\w+\-[1-9]\d*$/', $issueSyntax) !== 1) {
99
            return null;
100
        }
101
102
        list($projectKey, $issueNumber) = explode('-', $issueSyntax);
103
104
        $issue = Issue::getInstance('jira', $projectKey, $issueNumber, false);
105
        $issue->getFromDB();
106
107
        return $issue;
108
    }
109
110
    /**
111
     * @return bool
112
     */
113
    public function isConfigured()
114
    {
115
        if (null === $this->jiraUrl) {
116
            $this->configError = 'Jira URL not set!';
117
            return false;
118
        }
119
120
        if (empty($this->token)) {
121
            $this->configError = 'Authentication token is missing!';
122
            return false;
123
        }
124
125
        if (empty($this->authUser)) {
126
            $this->configError = 'Authentication user is missing!';
127
            return false;
128
        }
129
130
        try {
131
            $this->makeJiraRequest('/rest/webhooks/1.0/webhook', [], 'GET');
132
//            $user = $this->makeJiraRequest('/rest/api/2/user', [], 'GET');
133
        } catch (\Exception $e) {
134
            $this->configError = 'Attempt to verify the Jira authentication failed with message: ' . hsc($e->getMessage());
1 ignored issue
show
Bug introduced by
The function hsc was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

134
            $this->configError = 'Attempt to verify the Jira authentication failed with message: ' . /** @scrutinizer ignore-call */ hsc($e->getMessage());
Loading history...
135
            return false;
136
        }
137
138
        return true;
139
    }
140
141
    /**
142
     * @param Form $configForm
143
     *
144
     * @return void
145
     */
146
    public function hydrateConfigForm(Form $configForm)
147
    {
148
        $url = 'https://id.atlassian.com/manage/api-tokens';
149
        $link = "<a href=\"$url\">$url</a>";
150
        $configForm->addHTML("<p>{$this->configError} Please go to $link and generate a new token for this plugin.</p>");
151
        $configForm->addTextInput('jira_url', 'Jira Url')->val($this->jiraUrl);
152
        $configForm->addTextInput('jira_user', 'Jira User')
153
            ->val($this->authUser)
154
            ->attr('placeholder', '[email protected]');
155
        $configForm->addPasswordInput('jira_token', 'Jira AccessToken')->useInput(false);
156
    }
157
158
    public function handleAuthorization()
159
    {
160
        global $INPUT;
161
162
        $token = $INPUT->str('jira_token');
163
        $url = $INPUT->str('jira_url');
164
        $user = $INPUT->str('jira_user');
165
166
        /** @var \helper_plugin_issuelinks_db $db */
167
        $db = plugin_load('helper', 'issuelinks_db');
1 ignored issue
show
Bug introduced by
The function plugin_load was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

167
        $db = /** @scrutinizer ignore-call */ plugin_load('helper', 'issuelinks_db');
Loading history...
168
        if (!empty($token)) {
169
            $db->saveKeyValuePair('jira_token', $token);
170
        }
171
        if (!empty($url)) {
172
            $db->saveKeyValuePair('jira_url', $url);
173
        }
174
        if (!empty($user)) {
175
            $db->saveKeyValuePair('jira_user', $user);
176
        }
177
178
    }
179
180
    public function getUserString()
181
    {
182
        return hsc($this->authUser);
1 ignored issue
show
Bug introduced by
The function hsc was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

182
        return /** @scrutinizer ignore-call */ hsc($this->authUser);
Loading history...
183
    }
184
185
    public function retrieveIssue(Issue $issue)
186
    {
187
        // FIXME: somehow validate that we are allowed to retrieve that issue
188
189
        $projectKey = $issue->getProject();
190
191
        /** @var \helper_plugin_issuelinks_db $db */
192
        $db = plugin_load('helper', 'issuelinks_db');
1 ignored issue
show
Bug introduced by
The function plugin_load was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

192
        $db = /** @scrutinizer ignore-call */ plugin_load('helper', 'issuelinks_db');
Loading history...
193
        $webhooks = $db->getWebhooks('jira');
194
        $allowedRepos = explode(',', $webhooks[0]['repository_id']);
195
196
        if (!in_array($projectKey, $allowedRepos, true)) {
197
//            Jira Projects must be enabled as Webhook for on-demand fetching
198
            return;
199
        }
200
201
202
        $issueNumber = $issue->getKey();
203
        $endpoint = "/rest/api/2/issue/$projectKey-$issueNumber";
204
205
        $issueData = $this->makeJiraRequest($endpoint, [], 'GET');
206
        $this->setIssueData($issue, $issueData);
207
    }
208
209
    public function retrieveAllIssues($projectKey, &$startat = 0)
210
    {
211
        $jqlQuery = "project=$projectKey";
212
//        $jqlQuery = urlencode("project=$projectKey ORDER BY updated DESC");
213
        $endpoint = '/rest/api/2/search?jql=' . $jqlQuery. '&maxResults=50&startAt=' . $startat;
214
        $result = $this->makeJiraRequest($endpoint, [], 'GET');
215
216
        if (empty($result['issues'])) {
217
            return array();
218
        }
219
220
        $this->total = $result['total'];
221
222
        $startat += $result['maxResults'];
223
224
        $retrievedIssues = array();
225
        foreach ($result['issues'] as $issueData) {
226
            list(, $issueNumber) = explode('-', $issueData['key']);
227
            try {
228
                $issue = Issue::getInstance('jira', $projectKey, $issueNumber, false);
229
            } catch (\InvalidArgumentException $e) {
230
                continue;
231
            }
232
            $this->setIssueData($issue, $issueData);
233
            $issue->saveToDB();
234
            $retrievedIssues[] = $issue;
235
        }
236
        return $retrievedIssues;
237
    }
238
239
    /**
240
     * Get the total of issues currently imported by retrieveAllIssues()
241
     *
242
     * This may be an estimated number
243
     *
244
     * @return int
245
     */
246
    public function getTotalIssuesBeingImported()
247
    {
248
        return $this->total;
249
    }
250
251
    /**
252
     * Get a list of all organisations a user is member of
253
     *
254
     * @return string[] the identifiers of the organisations
255
     */
256
    public function getListOfAllUserOrganisations()
257
    {
258
        return ['All projects'];
259
    }
260
261
    /**
262
     * @param $organisation
263
     *
264
     * @return Repository[]
265
     */
266
    public function getListOfAllReposAndHooks($organisation)
267
    {
268
        /** @var \helper_plugin_issuelinks_db $db */
269
        $db = plugin_load('helper', 'issuelinks_db');
1 ignored issue
show
Bug introduced by
The function plugin_load was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

269
        $db = /** @scrutinizer ignore-call */ plugin_load('helper', 'issuelinks_db');
Loading history...
270
        $webhooks = $db->getWebhooks('jira');
271
        $subscribedProjects = [];
272
        if (!empty($webhooks)) {
273
            $subscribedProjects = explode(',', $webhooks[0]['repository_id']);
274
        }
275
276
        $projects = $this->makeJiraRequest('/rest/api/2/project', [], 'GET');
277
278
        $repositories = [];
279
        foreach ($projects as $project) {
280
            $repo = new Repository();
281
            $repo->displayName = $project['name'];
282
            $repo->full_name = $project['key'];
283
            if (in_array($project['key'], $subscribedProjects)) {
284
                $repo->hookID = 1;
285
            }
286
            $repositories[] = $repo;
287
        }
288
        return $repositories;
289
    }
290
291
    public function createWebhook($project)
292
    {
293
294
        // get old webhook id
295
        /** @var \helper_plugin_issuelinks_db $db */
296
        $db = plugin_load('helper', 'issuelinks_db');
1 ignored issue
show
Bug introduced by
The function plugin_load was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

296
        $db = /** @scrutinizer ignore-call */ plugin_load('helper', 'issuelinks_db');
Loading history...
297
        $webhooks = $db->getWebhooks('jira');
298
        $projects = [];
299
        if (!empty($webhooks)) {
300
            $oldID = $webhooks[0]['id'];
301
            // get current webhook projects
302
            $projects = explode(',', $webhooks[0]['repository_id']);
303
            // remove old webhook
304
            $this->makeJiraRequest('/rest/webhooks/1.0/webhook/' . $oldID, [], 'DELETE');
305
            // delete old webhook from database
306
            $db->deleteWebhook('jira', $webhooks[0]['repository_id'], $oldID);
307
        }
308
309
        // add new project
310
        $projects[] = $project;
311
        $projects = array_filter(array_unique($projects));
312
        $projectsString = implode(',', $projects);
313
314
        // add new webhooks
315
        global $conf;
316
        $payload = [
317
            'name' => 'dokuwiki plugin issuelinks for Wiki: ' . $conf['title'],
318
            'url' => self::WEBHOOK_URL,
319
            'events' => [
320
                'jira:issue_created',
321
                'jira:issue_updated',
322
            ],
323
            'description' => 'dokuwiki plugin issuelinks for Wiki: ' . $conf['title'],
324
            'jqlFilter' => "project in ($projectsString)",
325
            'excludeIssueDetails' => 'false'
326
        ];
327
        $response = $this->makeJiraRequest('/rest/webhooks/1.0/webhook', $payload, 'POST');
328
        $selfLink = $response['self'];
329
        $newWebhookID = substr($selfLink, strrpos($selfLink, '/') + 1);
330
331
332
        // store new webhook to database
333
        $db->saveWebhook('jira', $projectsString, $newWebhookID, 'jira rest webhooks have no secrets :/');
334
        return ['status' => 200, 'data' => ['id' => $newWebhookID]];
335
336
    }
337
338
    /**
339
     * Delete our webhook in a source repository
340
     *
341
     * @param     $project
342
     * @param int $hookid the numerical id of the hook to be deleted
343
     *
344
     * @return array
345
     */
346
    public function deleteWebhook($project, $hookid)
347
    {
348
        // get old webhook id
349
        /** @var \helper_plugin_issuelinks_db $db */
350
        $db = plugin_load('helper', 'issuelinks_db');
1 ignored issue
show
Bug introduced by
The function plugin_load was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

350
        $db = /** @scrutinizer ignore-call */ plugin_load('helper', 'issuelinks_db');
Loading history...
351
        $webhooks = $db->getWebhooks('jira');
352
        $projects = [];
353
        if (!empty($webhooks)) {
354
            $oldID = $webhooks[0]['id'];
355
            // get current webhook projects
356
            $projects = explode(',', $webhooks[0]['repository_id']);
357
            // remove old webhook
358
            $this->makeJiraRequest('/rest/webhooks/1.0/webhook/' . $oldID, [], 'DELETE');
359
            // delete old webhook from database
360
            $db->deleteWebhook('jira', $webhooks[0]['repository_id'], $oldID);
361
        }
362
363
        // remove project
364
        $projects = array_filter(array_diff($projects, [$project]));
365
        if (empty($projects)) {
366
            return ['status' => 204, 'data' => ''];
367
        }
368
369
        $projectsString = implode(',', $projects);
370
371
        // add new webhooks
372
        global $conf;
373
        $payload = [
374
            'name' => 'dokuwiki plugin issuelinks for Wiki: ' . $conf['title'],
375
            'url' => self::WEBHOOK_URL,
376
            'events' => [
377
                'jira:issue_created',
378
                'jira:issue_updated',
379
            ],
380
            'description' => 'dokuwiki plugin issuelinks for Wiki: ' . $conf['title'],
381
            'jqlFilter' => "project in ($projectsString)",
382
            'excludeIssueDetails' => 'false'
383
        ];
384
        $response = $this->makeJiraRequest('/rest/webhooks/1.0/webhook', $payload, 'POST');
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
391
        return ['status' => 204, 'data' => ''];
392
    }
393
394
    public static function isOurWebhook()
395
    {
396
        global $INPUT;
397
        $userAgent = $INPUT->server->str('HTTP_USER_AGENT');
398
        return strpos($userAgent, 'Atlassian HttpClient') === 0;
399
    }
400
401
    /**
402
     * Do all checks to verify that the webhook is expected and actually ours
403
     *
404
     * @param $webhookBody
405
     *
406
     * @return true|RequestResult true if the the webhook is our and should be processed RequestResult with explanation otherwise
407
     */
408
    public function validateWebhook($webhookBody)
409
    {
410
        $data = json_decode($webhookBody, true);
411
        /** @var \helper_plugin_issuelinks_db $db */
412
        $db = plugin_load('helper', 'issuelinks_db');
1 ignored issue
show
Bug introduced by
The function plugin_load was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

412
        $db = /** @scrutinizer ignore-call */ plugin_load('helper', 'issuelinks_db');
Loading history...
413
        $webhooks = $db->getWebhooks('jira');
414
        $projects = [];
415
        if (!empty($webhooks)) {
416
            // get current webhook projects
417
            $projects = explode(',', $webhooks[0]['repository_id']);
418
        }
419
420
        if (!$data['webhookEvent'] || !in_array($data['webhookEvent'], ['jira:issue_updated', 'jira:issue_created'])) {
421
            return new RequestResult(400, 'unknown webhook event');
422
        }
423
424
        list($projectKey, $issueId) = explode('-', $data['issue']['key']);
425
426
        if (!in_array($projectKey, $projects)) {
427
            return new RequestResult(202,'Project ' . $projectKey . ' is not handled by this wiki.');
428
        }
429
430
        return true;
431
    }
432
433
    /**
434
     * Handle the contents of the webhooks body
435
     *
436
     * @param $webhookBody
437
     *
438
     * @return RequestResult
439
     */
440
    public function handleWebhook($webhookBody)
441
    {
442
        $data = json_decode($webhookBody, true);
443
        $issueData = $data['issue'];
444
        list($projectKey, $issueId) = explode('-', $issueData['key']);
445
        $issue =Issue::getInstance('jira', $projectKey, $issueId, false);
446
        $this->setIssueData($issue, $issueData);
447
        $issue->saveToDB();
448
449
        return new RequestResult(200, 'OK');
450
    }
451
452
    protected function setIssueData(Issue $issue, $issueData)
453
    {
454
        $issue->setSummary($issueData['fields']['summary']);
455
        $issue->setStatus($issueData['fields']['status']['name']);
456
        $issue->setDescription($issueData['fields']['description']);
457
        $issue->setType($issueData['fields']['issuetype']['name']);
458
        $issue->setPriority($issueData['fields']['priority']['name']);
459
460
        $issue->setUpdated($issueData['fields']['updated']);
461
        $versions = array_column($issueData['fields']['fixVersions'], 'name');
462
        $issue->setVersions($versions);
463
        $components = array_column($issueData['fields']['components'], 'name');
464
        $issue->setComponents($components);
465
        $issue->setLabels($issueData['fields']['labels']);
466
467
        if ($issueData['fields']['assignee']) {
468
            $assignee = $issueData['fields']['assignee'];
469
            $issue->setAssignee($assignee['displayName'], $assignee['avatarUrls']['48x48']);
470
        }
471
472
        if ($issueData['fields']['duedate']) {
473
            $issue->setDuedate($issueData['fields']['duedate']);
474
        }
475
476
        // FIXME: check and handle these fields:
477
//        $issue->setParent($issueData['fields']['parent']['key']);
478
    }
479
480
    protected function makeJiraRequest($endpoint, array $data, $method, array $headers = array()) {
481
        $url = $this->jiraUrl . $endpoint;
482
        $dataToBeSend = json_encode($data);
483
        $defaultHeaders = [
484
            'Authorization' => 'Basic ' . base64_encode("$this->authUser:$this->token"),
485
            'Content-Type' => 'application/json',
486
        ];
487
488
        $this->dokuHTTPClient->headers = array_merge($this->dokuHTTPClient->headers, $defaultHeaders, $headers);
489
490
        try {
491
            $responseSuccess = $this->dokuHTTPClient->sendRequest($url, $dataToBeSend, $method);
492
        } catch (\HTTPClientException $e) {
1 ignored issue
show
Bug introduced by
The type HTTPClientException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
493
            throw new HTTPRequestException('request error', $this->dokuHTTPClient, $url, $method);
494
        }
495
496
        if (!$responseSuccess || $this->dokuHTTPClient->status < 200 || $this->dokuHTTPClient->status > 206) {
497
            if ($this->dokuHTTPClient->status >= 500) {
498
                throw new ExternalServerException('request error', $this->dokuHTTPClient, $url, $method);
499
            }
500
            throw new HTTPRequestException('request error', $this->dokuHTTPClient, $url, $method);
501
        }
502
        return json_decode($this->dokuHTTPClient->resp_body, true);
503
    }
504
505
}
506