Passed
Push — master ( 0ddf87...7b0474 )
by Michael
02:03
created

Issue::getMaterialDesignTypeIcon()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 12
nc 2
nop 0
dl 0
loc 18
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace dokuwiki\plugin\issuelinks\classes;
4
5
/**
6
 *
7
 * Class Issue
8
 */
9
class Issue extends \DokuWiki_Plugin implements \JsonSerializable
10
{
11
12
    /** @var Issue[] */
13
    private static $instances = [];
14
    protected $issueId = null;
15
    protected $projectId = null;
16
    protected $isMergeRequest = null;
17
    protected $files = [];
18
    protected $serviceID = '';
19
    private $summary = '';
20
    private $description = '';
21
    private $status = '';
22
    private $type = 'unknown';
23
    private $components = [];
24
    private $labels = [];
25
    private $priority = '';
26
    private $assignee = [];
27
    private $labelData = [];
28
    private $versions = [];
29
    private $duedate = null;
30
    private $updated = null;
31
    /** @var int may be set if this issue is created in specific relation to another issue */
32
    private $relatedWeight = 0;
0 ignored issues
show
introduced by
The private property $relatedWeight is not used, and could be removed.
Loading history...
33
    private $parent;
34
    private $errors = [];
35
    private $isValid = null;
36
37
    /**
38
     * @param        $serviceName
39
     * @param string $projectKey The shortkey of the project, e.g. SPR
40
     * @param int    $issueId    The id of the issue, e.g. 42
41
     *
42
     * @param        $isMergeRequest
43
     */
44
    private function __construct($serviceName, $projectKey, $issueId, $isMergeRequest)
45
    {
46
        if (empty($serviceName) || empty($projectKey) || empty($issueId) || !is_numeric($issueId)) {
47
            throw new \InvalidArgumentException('Empty value passed to Issue constructor');
48
        }
49
50
        $this->issueId = $issueId;
51
        $this->projectId = $projectKey;
52
        $this->isMergeRequest = $isMergeRequest;
53
        $this->serviceID = $serviceName;
54
55
//        $this->getFromDB();
56
    }
57
58
    /**
59
     * Get the singleton instance of a issue
60
     *
61
     * @param      $serviceName
62
     * @param      $projectKey
63
     * @param      $issueId
64
     * @param bool $isMergeRequest
65
     * @param bool $forcereload create a new instace
66
     *
67
     * @return Issue
68
     */
69
    public static function getInstance(
70
        $serviceName,
71
        $projectKey,
72
        $issueId,
73
        $isMergeRequest = false,
74
        $forcereload = false
75
    ) {
76
        $issueHash = $serviceName . $projectKey . $issueId . '!' . $isMergeRequest;
77
        if (empty(self::$instances[$issueHash]) || $forcereload) {
78
            self::$instances[$issueHash] = new Issue($serviceName, $projectKey, $issueId, $isMergeRequest);
79
        }
80
        return self::$instances[$issueHash];
81
    }
82
83
    /**
84
     * @return bool true if issue was found in database, false otherwise
85
     */
86
    public function getFromDB()
87
    {
88
        /** @var \helper_plugin_issuelinks_db $db */
89
        $db = plugin_load('helper', 'issuelinks_db');
90
        $issue = $db->loadIssue($this->serviceID, $this->projectId, $this->issueId, $this->isMergeRequest);
91
        if (empty($issue)) {
92
            return false;
93
        }
94
        $this->summary = $issue['summary'] ?: '';
95
        $this->status = $issue['status'];
96
        $this->type = $issue['type'];
97
        $this->description = $issue['description'];
98
        $this->setComponents($issue['components']);
99
        $this->setLabels($issue['labels']);
100
        $this->priority = $issue['priority'];
101
        $this->duedate = $issue['duedate'];
102
        $this->setVersions($issue['versions']);
103
        $this->setUpdated($issue['updated']);
104
        return true;
105
    }
106
107
    public function __toString()
108
    {
109
        $sep = $this->pmService->getProjectIssueSeparator($this->isMergeRequest);
110
        return $this->projectId . $sep . $this->issueId;
111
    }
112
113
    /**
114
     * @return \Exception|null
115
     */
116
    public function getLastError()
117
    {
118
        if (!end($this->errors)) {
119
            return null;
120
        }
121
        return end($this->errors);
122
    }
123
124
    /**
125
     * @return bool|self
126
     */
127
    public function isMergeRequest($isMergeRequest = null)
128
    {
129
        if ($isMergeRequest === null) {
130
            return $this->isMergeRequest;
131
        }
132
133
        $this->isMergeRequest = $isMergeRequest;
134
        return $this;
135
    }
136
137
    /**
138
     * Specify data which should be serialized to JSON
139
     *
140
     * @link  http://php.net/manual/en/jsonserializable.jsonserialize.php
141
     * @return mixed data which can be serialized by <b>json_encode</b>,
142
     * which is a value of any type other than a resource.
143
     * @since 5.4.0
144
     *
145
     * @link  http://stackoverflow.com/a/4697671/3293343
146
     */
147
    public function jsonSerialize()
148
    {
149
        return [
150
            'service' => $this->serviceID,
151
            'project' => $this->getProject(),
152
            'id' => $this->getKey(),
153
            'isMergeRequest' => $this->isMergeRequest ? '1' : '0',
154
            'summary' => $this->getSummary(),
155
            'description' => $this->getDescription(),
156
            'type' => $this->getType(),
157
            'status' => $this->getStatus(),
158
            'parent' => $this->getParent(),
159
            'components' => $this->getComponents(),
160
            'labels' => $this->getLabels(),
161
            'priority' => $this->getPriority(),
162
            'duedate' => $this->getDuedate(),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getDuedate() targeting dokuwiki\plugin\issuelin...ses\Issue::getDuedate() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
163
            'versions' => $this->getVersions(),
164
            'updated' => $this->getUpdated(),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getUpdated() targeting dokuwiki\plugin\issuelin...ses\Issue::getUpdated() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
165
        ];
166
    }
167
168
    /**
169
     * @return string
170
     */
171
    public function getProject()
172
    {
173
        return $this->projectId;
174
    }
175
176
    /**
177
     * Returns the key, i.e. number, of the issue
178
     *
179
     * @param bool $annotateMergeRequest If true, prepends a `!` to the key of a merge requests
180
     *
181
     * @return int|string
182
     */
183
    public function getKey($annotateMergeRequest = false)
184
    {
185
        if ($annotateMergeRequest && $this->isMergeRequest) {
186
            return '!' . $this->issueId;
187
        }
188
        return $this->issueId;
189
    }
190
191
    public function getSummary()
192
    {
193
        return $this->summary;
194
    }
195
196
    /**
197
     * @param string $summary
198
     *
199
     * @return Issue
200
     *
201
     * todo: decide if we should test for non-empty string here
202
     */
203
    public function setSummary($summary)
204
    {
205
        $this->summary = $summary;
206
        return $this;
207
    }
208
209
    /**
210
     * @return string
211
     */
212
    public function getDescription()
213
    {
214
        return $this->description;
215
    }
216
217
    /**
218
     * @param string $description
219
     *
220
     * @return Issue
221
     */
222
    public function setDescription($description)
223
    {
224
        $this->description = $description;
225
        return $this;
226
    }
227
228
    /**
229
     * @return string
230
     */
231
    public function getType()
232
    {
233
        return $this->type;
234
    }
235
236
    /**
237
     * @param string $type
238
     *
239
     * @return Issue
240
     */
241
    public function setType($type)
242
    {
243
        $this->type = $type;
244
        return $this;
245
    }
246
247
    /**
248
     * @return string
249
     */
250
    public function getStatus()
251
    {
252
        return $this->status;
253
    }
254
255
    /**
256
     * @param string $status
257
     *
258
     * @return Issue
259
     */
260
    public function setStatus($status)
261
    {
262
        $this->status = $status;
263
        return $this;
264
    }
265
266
    /**
267
     * @return mixed
268
     */
269
    public function getParent()
270
    {
271
        return $this->parent;
272
    }
273
274
    public function setParent($key)
275
    {
276
        $this->parent = $key;
277
        return $this;
278
    }
279
280
    /**
281
     * @return array
282
     */
283
    public function getComponents()
284
    {
285
        return $this->components;
286
    }
287
288
    /**
289
     * @param array $components
290
     *
291
     * @return Issue
292
     */
293
    public function setComponents($components)
294
    {
295
        if (!is_array($components)) {
0 ignored issues
show
introduced by
The condition is_array($components) is always true.
Loading history...
296
            $components = array_filter(array_map('trim', explode(',', $components)));
297
        }
298
        if (!empty($components[0]['name'])) {
299
            $components = array_map(function ($component) {
300
                return $component['name'];
301
            }, $components);
302
        }
303
        $this->components = $components;
304
        return $this;
305
    }
306
307
    /**
308
     * @return array
309
     */
310
    public function getLabels()
311
    {
312
        return $this->labels;
313
    }
314
315
    /**
316
     * @param array $labels
317
     *
318
     * @return Issue
319
     */
320
    public function setLabels($labels)
321
    {
322
        if (!is_array($labels)) {
0 ignored issues
show
introduced by
The condition is_array($labels) is always true.
Loading history...
323
            $labels = array_filter(array_map('trim', explode(',', $labels)));
324
        }
325
        $this->labels = $labels;
326
        return $this;
327
    }
328
329
    /**
330
     * @return string
331
     */
332
    public function getPriority()
333
    {
334
        return $this->priority;
335
    }
336
337
    /**
338
     * @param string $priority
339
     *
340
     * @return Issue
341
     */
342
    public function setPriority($priority)
343
    {
344
        $this->priority = $priority;
345
        return $this;
346
    }
347
348
    /**
349
     * @return null
350
     */
351
    public function getDuedate()
352
    {
353
        return $this->duedate;
354
    }
355
356
    /**
357
     * @param null $duedate
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $duedate is correct as it would always require null to be passed?
Loading history...
358
     *
359
     * @return Issue
360
     */
361
    public function setDuedate($duedate)
362
    {
363
        $this->duedate = $duedate;
364
        return $this;
365
    }
366
367
    /**
368
     * @return array
369
     */
370
    public function getVersions()
371
    {
372
        return $this->versions;
373
    }
374
375
    /**
376
     * @param array $versions
377
     *
378
     * @return Issue
379
     */
380
    public function setVersions($versions)
381
    {
382
        if (!is_array($versions)) {
0 ignored issues
show
introduced by
The condition is_array($versions) is always true.
Loading history...
383
            $versions = array_map('trim', explode(',', $versions));
384
        }
385
        if (!empty($versions[0]['name'])) {
386
            $versions = array_map(function ($version) {
387
                return $version['name'];
388
            }, $versions);
389
        }
390
        $this->versions = $versions;
391
        return $this;
392
    }
393
394
    /**
395
     * @return null
396
     */
397
    public function getUpdated()
398
    {
399
        return $this->updated;
400
    }
401
402
    /**
403
     * @param string|int $updated
404
     *
405
     * @return Issue
406
     */
407
    public function setUpdated($updated)
408
    {
409
        /** @var \helper_plugin_issuelinks_util $util */
410
        $util = plugin_load('helper', 'issuelinks_util');
411
        if (!$util->isValidTimeStamp($updated)) {
412
            $updated = strtotime($updated);
413
        }
414
        $this->updated = (int)$updated;
415
        return $this;
416
    }
417
418
    /**
419
     * Get a fancy HTML-link to issue
420
     *
421
     * @param bool $addSummary add the issue's summary after it status
422
     *
423
     * @return string
424
     */
425
    public function getIssueLinkHTML($addSummary = false)
426
    {
427
        $serviceProvider = ServiceProvider::getInstance();
428
        $service = $serviceProvider->getServices()[$this->serviceID];
429
        $name = $this->projectId . $service::getProjectIssueSeparator($this->isMergeRequest) . $this->issueId;
430
        $url = $this->getIssueURL();
431
432
        $status = cleanID($this->getStatus());
433
        if ($status) {
434
            $name .= $this->getformattedIssueStatus();
435
        }
436
        if ($addSummary) {
437
            $name .= ' ' . $this->getSummary();
438
        }
439
440
        $target = 'target="_blank" rel="noopener"';
441
        $classes = 'issuelink ' . cleanID($this->getType()) . ($this->isMergeRequest ? ' mergerequest' : '');
442
        $dataAttributes = "data-service=\"$this->serviceID\" data-project=\"$this->projectId\" data-issueid=\"$this->issueId\"";
443
        $dataAttributes .= ' data-ismergerequest="' . ($this->isMergeRequest ? '1' : '0') . '"';
444
        return "<a href=\"$url\" class=\"$classes\" $dataAttributes $target>" . $this->getTypeHTML() . "$name</a>";
445
    }
446
447
    /**
448
     * @return string
449
     */
450
    public function getIssueURL()
451
    {
452
        $serviceProvider = ServiceProvider::getInstance();
453
        $service = $serviceProvider->getServices()[$this->serviceID]::getInstance();
454
        return $service->getIssueURL($this->projectId, $this->issueId, $this->isMergeRequest);
455
    }
456
457
    /**
458
     * get the status of the issue as HTML string
459
     *
460
     * @param string|null $status
461
     *
462
     * @return string
463
     */
464
    public function getformattedIssueStatus($status = null)
465
    {
466
        if ($status === null) {
467
            $status = $this->getStatus();
468
        }
469
        $status = strtolower($status);
470
        return "<span class='mm__status " . cleanID($status) . "'>$status</span>";
471
    }
472
473
    /**
474
     * @return string
475
     */
476
    public function getTypeHTML()
477
    {
478
        if ($this->isMergeRequest) {
479
            return inlineSVG(__DIR__ . '/../images/mdi-source-pull.svg');
480
        }
481
        $image = $this->getMaterialDesignTypeIcon();
482
        return "<img src='$image' alt='$this->type' />";
483
    }
484
485
    /**
486
     * ToDo: replace all with SVG
487
     *
488
     * @return string the path to the icon / base64 image if type unknown
489
     */
490
    protected function getMaterialDesignTypeIcon()
491
    {
492
        $typeIcon = [
493
            'bug' => 'mdi-bug.png',
494
            'story' => 'mdi-bookmark.png',
495
            'epic' => 'mdi-flash.png',
496
            'change_request' => 'mdi-plus.png',
497
            'improvement' => 'mdi-arrow-up-thick.png',
498
            'organisation_task' => 'mdi-calendar-text.png',
499
            'technical_task' => 'mdi-source-branch.png',
500
            'task' => 'mdi-check.png',
501
        ];
502
503
        if (!isset($typeIcon[cleanID($this->type)])) {
504
            return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAABDUlEQVQYGQXBLWuVcQDA0fM8272OIYLCmi+IOBBWhWEZohiHn0AQi/H3CQxaLVptgmmIacUwWLthsDiQBaOmIaYF+XsOgHb61N9Glx30qAkAtOigVbttttZGO31t1VUArXfeCwCg3S66Buhzr6Blb/rVeS+b6WEnTehuZ0206Gej0Wh0CH3pCXrXM2ijVW+bW3bS6Bbd6xiddQNogpadNrpDa40mXbYBQI+7bPS9CRotdN51gOZGo9dN0Nxo1vv2AFpr1RFAtztBD1oBtOhffwD62D7osH2gZaN/QNv9aAZd6XdPgZYtoPtdtAWgzY771nbrNHezD523BQCa2uuo0Wh02vNmAADQ1KIZAPgPQZt8UVJ7VXIAAAAASUVORK5CYII=';
505
        }
506
507
        return DOKU_URL . '/lib/plugins/issuelinks/images/' . $typeIcon[cleanID($this->type)];
508
    }
509
510
    public function setAssignee($name, $avatar_url)
511
    {
512
        $this->assignee['name'] = $name;
513
        $this->assignee['avatarURL'] = $avatar_url;
514
    }
515
516
    public function getAdditionalDataHTML()
517
    {
518
        $this->getFromService();
519
        $data = [];
520
        if (!empty($this->assignee)) {
521
            $data['avatarHTML'] = "<img src=\"{$this->assignee['avatarURL']}\" alt=\"{$this->assignee['name']}\">";
522
        }
523
        if (!empty($this->labelData)) {
524
            $labels = $this->getLabels();
525
            $data['fancyLabelsHTML'] = '';
526
            foreach ($labels as $label) {
527
                $colors = '';
528
                $classes = 'label';
529
                if (isset($this->labelData[$label])) {
530
                    $colors = "style=\"background-color: {$this->labelData[$label]['background-color']};";
531
                    $colors .= " color: {$this->labelData[$label]['color']};\"";
532
                    $classes .= ' color';
533
                }
534
                $data['fancyLabelsHTML'] .= "<span class=\"$classes\" $colors>$label</span>";
535
            }
536
        }
537
        return $data;
538
    }
539
540
    public function getFromService()
541
    {
542
        $serviceProvider = ServiceProvider::getInstance();
543
        $service = $serviceProvider->getServices()[$this->serviceID]::getInstance();
544
545
        try {
546
            $service->retrieveIssue($this);
547
            if ($this->isValid(true)) {
548
                $this->saveToDB();
549
            }
550
        } catch (IssueLinksException $e) {
551
            $this->errors[] = $e;
552
            $this->isValid = false;
553
            return false;
554
        }
555
        return true;
556
    }
557
558
    /**
559
     * Check if an issue is valid.
560
     *
561
     * The specific rules depend on the service and the cached value may also be set by other functions.
562
     *
563
     * @param bool $recheck force a validity check instead of using cached value if available
564
     *
565
     * @return bool
566
     */
567
    public function isValid($recheck = false)
568
    {
569
        if ($recheck || $this->isValid === null) {
570
            $serviceProvider = ServiceProvider::getInstance();
571
            $service = $serviceProvider->getServices()[$this->serviceID];
572
            $this->isValid = $service::isIssueValid($this);
573
        }
574
        return $this->isValid;
575
    }
576
577
    public function saveToDB()
578
    {
579
        /** @var \helper_plugin_issuelinks_db $db */
580
        $db = plugin_load('helper', 'issuelinks_db');
581
        return $db->saveIssue($this);
582
    }
583
584
    public function buildTooltipHTML()
585
    {
586
        $html = '<aside class="issueTooltip">';
587
        $html .= "<h1 class=\"issueTitle\">{$this->getSummary()}</h1>";
588
        $html .= "<div class='assigneeAvatar waiting'></div>";
589
590
        /** @var \helper_plugin_issuelinks_util $util */
591
        $util = plugin_load('helper', 'issuelinks_util');
592
593
        $components = $this->getComponents();
594
        if (!empty($components)) {
595
            $html .= '<p class="components">';
596
            foreach ($components as $component) {
597
                $html .= "<span class=\"component\">$component</span>";
598
            }
599
            $html .= '</p>';
600
        }
601
602
        $labels = $this->getLabels();
603
        if (!empty($labels)) {
604
            $html .= '<p class="labels">';
605
            foreach ($labels as $label) {
606
                $html .= "<span class=\"label\">$label</span>";
607
            }
608
            $html .= '</p>';
609
        }
610
611
        $html .= '<p class="descriptionTeaser">';
612
        $description = $this->getDescription();
613
        if ($description) {
614
            $lines = explode("\n", $description);
615
            $cnt = min(count($lines), 5);
616
            for ($i = 0; $i < $cnt; $i += 1) {
617
                $html .= hsc($lines[$i]) . "\n";
618
            }
619
        } else {
620
            $html .= $util->getLang('no issue description');
621
        }
622
        $html .= '</p>';
623
624
        /** @var \helper_plugin_issuelinks_data $data */
625
        $data = $this->loadHelper('issuelinks_data');
626
627
        if (!$this->isMergeRequest) {
628
            // show merge requests referencing this Issues
629
            $mrs = $data->getMergeRequestsForIssue($this->getServiceName(), $this->getProject(), $this->issueId,
630
                $this->isMergeRequest);
631
            if (!empty($mrs)) {
632
                $html .= '<div class="mergeRequests">';
633
                $html .= '<h2>Merge Requests</h2>';
634
                $html .= '<ul>';
635
                foreach ($mrs as $mr) {
636
                    $html .= '<li>';
637
                    $a = "<a href=\"$mr[url]\">$mr[summary]</a>";
638
                    $html .= $this->getformattedIssueStatus($mr['status']) . ' ' . $a;
639
                    $html .= '</li>';
640
                }
641
                $html .= '</ul>';
642
                $html .= '</div>';
643
            }
644
        }
645
646
        $linkingPages = $data->getLinkingPages($this->getServiceName(), $this->getProject(), $this->issueId,
647
            $this->isMergeRequest);
648
        if (count($linkingPages)) {
649
            $html .= '<div class="relatedPages📄">';
650
            $html .= '<h2>' . $util->getLang('linking pages') . '</h2>';
651
            $html .= '<ul>';
652
            foreach ($linkingPages as $linkingPage) {
653
                $html .= '<li>';
654
                $html .= html_wikilink($linkingPage['page']);
655
                $html .= '</li>';
656
            }
657
            $html .= '</ul>';
658
            $html .= '</div>';
659
        }
660
661
        $html .= '</aside>';
662
        return $html;
663
    }
664
665
    public function getServiceName()
666
    {
667
        return $this->serviceID;
668
    }
669
670
    /**
671
     * @param string $labelName the background color without the leading #
672
     * @param string $color
673
     */
674
    public function setLabelData($labelName, $color)
675
    {
676
        $this->labelData[$labelName] = [
677
            'background-color' => $color,
678
            'color' => $this->calculateColor($color),
679
        ];
680
    }
681
682
    /**
683
     * Calculate if a white or black font-color should be with the given background color
684
     *
685
     * https://www.w3.org/TR/WCAG20/#relativeluminancedef
686
     * http://stackoverflow.com/a/3943023/3293343
687
     *
688
     * @param string $color the background-color, without leading #
689
     *
690
     * @return string
691
     */
692
    private function calculateColor($color)
693
    {
694
        /** @noinspection PrintfScanfArgumentsInspection */
695
        list($r, $g, $b) = array_map(function ($color8bit) {
696
            $c = $color8bit / 255;
697
            if ($c <= 0.03928) {
698
                $cl = $c / 12.92;
699
            } else {
700
                $cl = pow(($c + 0.055) / 1.055, 2.4);
701
            }
702
            return $cl;
703
        }, sscanf($color, "%02x%02x%02x"));
704
        if ($r * 0.2126 + $g * 0.7152 + $b * 0.0722 > 0.179) {
705
            return '#000000';
706
        }
707
        return '#FFFFFF';
708
    }
709
}
710