Passed
Push — 1.x-dev ( de129f...a65d26 )
by Brian
02:51
created

JiraProject::getOpenBugs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * JiraProjectReader
4
 */
5
6
namespace Fr3nch13\Jira\Lib;
7
8
use Cake\Core\Configure;
9
use Fr3nch13\Jira\Exception\Exception;
10
use Fr3nch13\Jira\Exception\IssueSubmissionException;
11
use Fr3nch13\Jira\Exception\MissingAllowedTypeException;
12
use Fr3nch13\Jira\Exception\MissingConfigException;
13
use Fr3nch13\Jira\Exception\MissingIssueException;
14
use Fr3nch13\Jira\Exception\MissingIssueFieldException;
15
use Fr3nch13\Jira\Exception\MissingProjectException;
16
use JiraRestApi\Configuration\ArrayConfiguration;
17
use JiraRestApi\Issue\IssueField;
18
use JiraRestApi\Issue\IssueService;
19
use JiraRestApi\Issue\JqlQuery;
20
use JiraRestApi\Project\ProjectService;
21
22
/**
23
 * Jira Project class
24
 */
25
class JiraProject
26
{
27
    /**
28
     * Config Object.
29
     * @var \JiraRestApi\Configuration\ArrayConfiguration
30
     */
31
    public $ConfigObj;
32
33
    /**
34
     * The key for the project.
35
     * @var string|null
36
     */
37
    public $projectKey = null;
38
39
    /**
40
     * The project service object.
41
     * @var \JiraRestApi\Project\ProjectService
42
     */
43
    public $ProjectService;
44
45
    /**
46
     * The project object.
47
     * @var \JiraRestApi\Project\Project
48
     */
49
    protected $Project;
50
51
    /**
52
     * The list of a Project's Versions.
53
     * @var \ArrayObject|\JiraRestApi\Issue\Version[]
54
     */
55
    protected $Versions;
56
57
    /**
58
     * The project service object.
59
     * @var \JiraRestApi\Issue\IssueService
60
     */
61
    public $IssueService;
62
63
    /**
64
     * The Cached list of issues.
65
     * @var array
66
     */
67
    protected $Issues = [];
68
69
    /**
70
     * The cached list of returned issue info from the below getIssue() method.
71
     * @var array
72
     */
73
    protected $issuesCache = [];
74
75
    /**
76
     * Valid Types.
77
     * Used to ensure we're getting a valid type when filtering.
78
     * Currently only support Jira Core and Software.
79
     * @see https://confluence.atlassian.com/adminjiracloud/issue-types-844500742.html
80
     * @var array
81
     */
82
    protected $validTypes = [
83
        'Bug',
84
        'Epic',
85
        'Story',
86
        'Subtask',
87
        'Task',
88
    ];
89
90
    /**
91
     * Types of issues allowed to be submitted.
92
     * @var array
93
     */
94
    protected $allowedTypes = [
95
        'Bug' => [
96
            'jiraType' => 'Bug', // Must be one of the types in the $this->validTypes.
97
            'jiraLabels' => 'bug-submitted', // The label used to tag user submitted bugs.
98
            // The form's field information.
99
            'formData' => [
100
                'fields' => [
101
                    'summary' => [
102
                        'type' => 'text',
103
                        'required' => true,
104
                    ],
105
                    'details' => [
106
                        'type' => 'textarea',
107
                        'required' => true,
108
                    ]
109
                ]
110
            ]
111
        ],
112
        'FeatureRequest' => [
113
            'jiraType' => 'Story', // Must be one of the types in the $this->validTypes.
114
            'jiraLabels' => 'feature-request', // The label used to tag feature requests.
115
            // The form's field information.
116
            'formData' => [
117
                'fields' => [
118
                    'summary' => [
119
                        'type' => 'text',
120
                        'required' => true,
121
                    ],
122
                    'details' => [
123
                        'type' => 'textarea',
124
                        'required' => true,
125
                    ]
126
                ]
127
            ]
128
        ]
129
    ];
130
131
    /**
132
     * This is here for the Form object (or any other object) to use.
133
     * It tacks all errors, even if an exception is thrown.
134
     * @var array
135
     */
136
    protected $errors = [];
137
138
    /**
139
     * Constructor
140
     *
141
     * Reads the configuration, and crdate a config object to be passed to the other objects.
142
     *
143
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException When the project can't be found.
144
     * @return void
145
     */
146
    public function __construct()
147
    {
148
        $this->configure();
149
150
        // setup the objects
151
        $this->ProjectService = new ProjectService($this->ConfigObj);
152
        try {
153
            $this->Project = $this->ProjectService->get($this->projectKey);
154
        } catch (\JiraRestApi\JiraException $e) {
155
            $this->setError($this->projectKey, 'MissingProjectException');
156
            throw new MissingProjectException($this->projectKey);
157
        }
158
159
        $this->Versions = $this->ProjectService->getVersions($this->projectKey);
160
        $this->IssueService = new IssueService($this->ConfigObj);
161
    }
162
163
    /**
164
     * Configures the object.
165
     * Broken out of construct.
166
     *
167
     * @throws \Fr3nch13\Jira\Exception\MissingConfigException When a config setting isn't set.
168
     * @return void
169
     */
170
    public function configure()
171
    {
172
        $schema = Configure::read('Jira.schema');
173
        if (!$schema) {
174
            $this->setError('schema', 'MissingConfigException');
175
            throw new MissingConfigException('schema');
176
        }
177
        $host = Configure::read('Jira.host');
178
        if (!$host) {
179
            $this->setError('host', 'MissingConfigException');
180
            throw new MissingConfigException('host');
181
        }
182
        $username = Configure::read('Jira.username');
183
        if (!$username) {
184
            $this->setError('username', 'MissingConfigException');
185
            throw new MissingConfigException('username');
186
        }
187
        $apiKey = Configure::read('Jira.apiKey');
188
        if (!$apiKey) {
189
            $this->setError('apiKey', 'MissingConfigException');
190
            throw new MissingConfigException('apiKey');
191
        }
192
        $projectKey = Configure::read('Jira.projectKey');
193
        if (!$projectKey) {
194
            $this->setError('projectKey', 'MissingConfigException');
195
            throw new MissingConfigException('projectKey');
196
        }
197
        $this->ConfigObj = new ArrayConfiguration([
198
            'jiraHost' => $schema . '://' . $host,
199
            'jiraUser' => $username,
200
            'jiraPassword' => $apiKey,
201
        ]);
202
203
        $this->projectKey = $projectKey;
204
    }
205
206
    /**
207
     * Get the Project's Info.
208
     *
209
     * @return \JiraRestApi\Project\Project The information about the project.
210
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException If the project can't be found.
211
     */
212
    public function getInfo()
213
    {
214
        return $this->Project;
215
    }
216
217
    /**
218
     * Get the Project's Versions.
219
     *
220
     * @return \ArrayObject|\JiraRestApi\Issue\Version[] A list of version objects.
221
     */
222
    public function getVersions()
223
    {
224
        return $this->Versions;
225
    }
226
227
    /**
228
     * Get the Project's Issues.
229
     *
230
     * @param string|null $type Filter the Issues by type.
231
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
232
     */
233
    public function getIssues($type = null)
234
    {
235
        $cacheKey = 'all';
236
        if ($type) {
237
            $cacheKey .= '-' . $type;
238
        }
239
        if (!isset($this->Issues[$cacheKey])) {
240
            $jql = new JqlQuery();
241
242
            $jql->setProject($this->projectKey);
243
            if ($type && in_array($type, $this->validTypes)) {
244
                $jql->setType($type);
245
            }
246
            $jql->addAnyExpression('ORDER BY key DESC');
247
248
            $this->Issues[$cacheKey] = $this->IssueService->search($jql->getQuery(), 0, 1000);
249
        }
250
251
        return $this->Issues[$cacheKey];
252
    }
253
254
    /**
255
     * Get the Project's Open Issues.
256
     *
257
     * @param string|null $type Filter the Issues by type.
258
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
259
     */
260
    public function getOpenIssues($type = null)
261
    {
262
        $cacheKey = 'open';
263
        if ($type) {
264
            $cacheKey .= '-' . $type;
265
        }
266
        if (!isset($this->Issues[$cacheKey])) {
267
            $jql = new JqlQuery();
268
269
            $jql->setProject($this->projectKey);
270
            if ($type && in_array($type, $this->validTypes)) {
271
                $jql->setType($type);
272
            }
273
            $jql->addAnyExpression('AND resolution is EMPTY');
274
            $jql->addAnyExpression('ORDER BY key DESC');
275
276
            $this->Issues[$cacheKey] = $this->IssueService->search($jql->getQuery(), 0, 1000);
277
        }
278
279
        return $this->Issues[$cacheKey];
280
    }
281
282
    /**
283
     * Gets info on a particular issue within your project.
284
     *
285
     * @param int|null $id The issue id. The integer part without the project key.
286
     * @return \JiraRestApi\Issue\Issue|\JiraRestApi\Issue\IssueV3 the object that has the info of that issue.
287
     * @throws \Fr3nch13\Jira\Exception\Exception If the issue's id isn't given.
288
     * @throws \Fr3nch13\Jira\Exception\MissingIssueException If the project's issue can't be found.
289
     */
290
    public function getIssue($id = null)
291
    {
292
        if (!is_int($id)) {
293
            $this->setError(__('Missing the Issue\'s ID.'), 'Exception');
294
            throw new Exception(__('Missing the Issue\'s ID.'));
295
        }
296
        $key = $this->projectKey . '-' . $id;
297
        if (!isset($this->issuesCache[$key])) {
298
            if (!$this->issuesCache[$key] = $this->IssueService->get($key)) {
299
                $this->setError($key, 'MissingIssueException');
300
                throw new MissingIssueException($key);
301
            }
302
        }
303
304
        return $this->issuesCache[$key];
305
    }
306
307
    /**
308
     * Gets a list of issues that are considered bugs.
309
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
310
     */
311
    public function getBugs()
312
    {
313
        return $this->getIssues('Bug');
314
    }
315
316
    /**
317
     * Gets a list of open issues that are considered bugs.
318
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
319
     */
320
    public function getOpenBugs()
321
    {
322
        return $this->getOpenIssues('Bug');
323
    }
324
325
    /**
326
     * Methods used to submit an Issue to Jira.
327
     */
328
329
    /**
330
     * Returns the allowed types and their settings
331
     *
332
     * @param string|null $type The type of issue you want to get.
333
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If a type is given, and that type is not configured.
334
     * @return array the content of $this->allowedTypes.
335
     */
336
    public function getAllowedTypes($type = null)
337
    {
338
        if ($type) {
339
            if (!isset($this->allowedTypes[$type])) {
340
                $this->setError($type, 'MissingAllowedTypeException');
341
                throw new MissingAllowedTypeException($type);
342
            }
343
344
            return $this->allowedTypes[$type];
345
        }
346
347
        return $this->allowedTypes;
348
    }
349
350
    /**
351
     * Allows you to modify the form allowdTypes to fir your situation.
352
     *
353
     * @param string $type The type of issue you want to add/modify.
354
     * @param array $settings The settings for the type.
355
     * @throws \Fr3nch13\Jira\Exception\MissingIssueFieldException If we're adding a new issue type, and the summary field isn't defined.
356
     * @return void
357
     */
358
    public function modifyAllowedTypes($type, $settings = [])
359
    {
360
        if (!isset($this->allowedTypes[$type])) {
361
            $this->allowedTypes[$type] = [];
362
            if (!isset($settings['jiraType'])) {
363
                $this->setError('jiraType', 'MissingIssueFieldException');
364
                throw new MissingIssueFieldException('jiraType');
365
            }
366
            if (!isset($settings['formData'])) {
367
                $this->setError('formData', 'MissingIssueFieldException');
368
                throw new MissingIssueFieldException('formData');
369
            }
370
            if (!isset($settings['formData']['fields'])) {
371
                $this->setError('formData.fields', 'MissingIssueFieldException');
372
                throw new MissingIssueFieldException('formData.fields');
373
            }
374
            if (!isset($settings['formData']['fields']['summary'])) {
375
                $this->setError('formData.fields.summary', 'MissingIssueFieldException');
376
                throw new MissingIssueFieldException('formData.fields.summary');
377
            }
378
        }
379
380
        $this->allowedTypes[$type] += $settings;
381
    }
382
383
    /**
384
     * Checks to see if a type is allowed.
385
     *
386
     * @param string $type The type to check.
387
     * @return bool if it's allowed or not.
388
     */
389
    public function isAllowedType($type)
390
    {
391
        return (isset($this->allowedTypes[$type]) ? true : false);
392
    }
393
394
    /**
395
     * Gets the array for the forms when submitting an issue to Jira.
396
     *
397
     * @param string|null $type The type of issue we're submitting.
398
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that type is not configured.
399
     * @throws \Fr3nch13\Jira\Exception\Exception If the form data for that type is missing.
400
     * @return array The array of data to fill in the form with.
401
     */
402
    public function getFormData($type = null)
403
    {
404
        if (!$type) {
405
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
406
            throw new MissingAllowedTypeException('[$type is not set]');
407
        }
408
409
        if (!$this->isAllowedType($type)) {
410
            $this->setError($type, 'MissingAllowedTypeException');
411
            throw new MissingAllowedTypeException($type);
412
        }
413
414
        $allowedTypes = $this->getAllowedTypes();
415
416
        if (!isset($allowedTypes[$type]['formData'])) {
417
            $this->setError('No form data is set.', 'Exception');
418
            throw new Exception(__('No form data is set.'));
419
        }
420
421
        return $allowedTypes[$type]['formData'];
422
    }
423
424
    /**
425
     * Sets the formData variable if you want to modify the default/initial values.
426
     *
427
     * @param string $type The type you want to set the data for.
428
     *  - Needs to be in the allowedTypes already.
429
     * @param array $data The definition of the allowed types
430
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that type is not configured.
431
     * @return void
432
     */
433
    public function setFormData($type, $data = [])
434
    {
435
        if (!$type) {
436
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
437
            throw new MissingAllowedTypeException('[$type is not set]');
438
        }
439
440
        if (!$this->isAllowedType($type)) {
441
            $this->setError($type, 'MissingAllowedTypeException');
442
            throw new MissingAllowedTypeException($type);
443
        }
444
445
        $this->allowedTypes[$type]['formData'] = $data;
446
    }
447
448
    /**
449
     * Submits the Issue
450
     *
451
     * @param string $type The type you want to set the data for.
452
     *  - Needs to be in the allowedTypes already.
453
     * @param array $data The array of details about the issue.
454
     * @throws \Fr3nch13\Jira\Exception\IssueSubmissionException If submitting the issue fails.
455
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that issue type is not configured.
456
     * @throws \Fr3nch13\Jira\Exception\MissingIssueFieldException If we're adding a new issue, and required fields aren't defined.
457
     * @return int|bool If the request was successfully submitted.
458
     */
459
    public function submitIssue($type, array $data = [])
460
    {
461
        if (!$type) {
462
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
463
            throw new MissingAllowedTypeException('[$type is not set]');
464
        }
465
466
        if (!$this->isAllowedType($type)) {
467
            $this->setError($type, 'MissingAllowedTypeException');
468
            throw new MissingAllowedTypeException($type);
469
        }
470
471
        if (!isset($data['summary'])) {
472
            $this->setError('summary', 'MissingIssueFieldException');
473
            throw new MissingIssueFieldException('summary');
474
        }
475
476
        $typeInfo = $this->getAllowedTypes($type);
477
478
        // make sure we can get the project info first.
479
        // getInfo will throw an exception if it can't find the project.
480
        // putting a try/catch around it so scrutinizer stops complaining.
481
        try {
482
            $project = $this->getInfo();
483
        } catch (MissingProjectException $e) {
484
            $this->setError($this->projectKey, 'MissingProjectException');
485
            throw $e;
486
        }
487
488
        $issueField = new IssueField();
489
        $issueField->setProjectKey($this->projectKey)
490
            ->setIssueType($typeInfo['jiraType']);
491
492
        /*
493
         * Yes, I know I'm not validating the input here.
494
         * I'm relying on the underlying IssueField to validate the data.
495
         */
496
497
        // I know i'm checking above, so this really isn't needed, but keeping here for consistancy/readability.
498
        if (isset($data['summary'])) {
499
            $issueField->setSummary($data['summary']);
500
        }
501
        if (isset($data['description'])) {
502
            $issueField->setDescription($data['description']);
503
        }
504
        if (isset($data['priority'])) {
505
            $issueField->setPriorityName($data['priority']);
506
        }
507
        if (isset($data['assignee'])) {
508
            $issueField->setPriorityName($data['assignee']);
509
        }
510
        if (isset($data['version'])) {
511
            $issueField->addVersion($data['version']);
512
        }
513
        if (isset($data['components'])) {
514
            $issueField->addComponents($data['components']);
515
        }
516
        if (isset($data['duedate'])) {
517
            $issueField->setDueDate($data['duedate']);
518
        }
519
520
        // labels should be space seperated
521
        if (isset($typeInfo['jiraLabels'])) {
522
            if (is_string($typeInfo['jiraLabels'])) {
523
                $typeInfo['jiraLabels'] = preg_split('/\s+/', $typeInfo['jiraLabels']);
524
            }
525
            // track the type with a label
526
            $typeInfo['jiraLabels'][] = 'user-submitted-type-' . $type;
527
            foreach ($typeInfo['jiraLabels'] as $jiralabel) {
528
                $issueField->addLabel($jiralabel);
529
            }
530
        }
531
532
        $issueService = new IssueService($this->ConfigObj);
533
534
        try {
535
            $ret = $issueService->create($issueField);
536
        } catch (\JiraRestApi\JiraException $e) {
537
            //Sample return error with json in it.
538
            //Pasting here so I can mock this return message in the unit tests.
539
            //CURL HTTP Request Failed: Status Code : 400, URL:https://[hostname]/rest/api/2/issue
540
            //Error Message : {"errorMessages":[],"errors":{"user_type":"Field 'user_type' cannot be set. It is not on the appropriate screen, or unknown."}}             */
541
            $msg = $e->getMessage();
542
            if (strpos($msg, '{') !== false) {
543
                $msgArray = str_split($msg);
544
                // extract the json message.
545
                $json = '';
546
                $in = 0;
547
                foreach ($msgArray as $i => $char) {
548
                    if ($char == '{') {
549
                        $in++;
550
                    }
551
                    if ($in) {
552
                        $json .= $msg[$i];
553
                    }
554
                    if ($char == '}') {
555
                        $in--;
556
                    }
557
                }
558
                if ($json) {
559
                    $json = json_decode($json, true);
560
                }
561
                if ($json) {
562
                    $newMsg = [];
563
                    if (isset($json['errorMessages'])) {
564
                        foreach ($json['errorMessages'] as $jsonMsg) {
565
                            $newMsg[] = $jsonMsg;
566
                        }
567
                        foreach ($json['errors'] as $jsonMsg) {
568
                            $newMsg[] = $jsonMsg;
569
                        }
570
                        $msg = implode("\n", $newMsg);
571
                    }
572
                }
573
            }
574
            $this->setError($msg, 'IssueSubmissionException');
575
            throw new IssueSubmissionException($msg);
576
        }
577
578
        if ($ret instanceof \JiraRestApi\Issue\Issue && $ret->id) {
579
            return (int)$ret->id;
580
        }
581
582
        return true;
583
    }
584
585
    /**
586
     * Sets an error
587
     *
588
     * @param string $msg The error message.
589
     * @param string $key The key to use in the this->errors array.
590
     * @return bool If saved or not.
591
     */
592
    public function setError($msg = '', $key = '')
593
    {
594
        if (!trim($msg)) {
595
            return false;
596
        }
597
        if ($key) {
598
            $this->errors[$key] = $msg;
599
        } else {
600
            $this->errors[] = $msg;
601
        }
602
603
        return true;
604
    }
605
606
    /**
607
     * Gets the accumulated error messages.
608
     * If a key is given, return that specific message. If that key doesn't exist, return false.
609
     *
610
     * @param string|null $key The key to the specific message to get.
611
     * @return array|string|false
612
     */
613
    public function getErrors($key = null)
614
    {
615
        if ($key) {
616
            if (isset($this->errors[$key])) {
617
                return $this->errors[$key];
618
            } else {
619
                return false;
620
            }
621
        }
622
623
        return $this->errors;
624
    }
625
}
626