Passed
Push — master ( 2fc7fc...cf401d )
by Michael
02:09
created

helper_plugin_issuelinks_data   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 310
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 310
rs 8.2857
wmc 39

15 Methods

Rating   Name   Duplication   Size   Complexity  
B isImportLocked() 0 21 5
A removeLock() 0 5 1
B importAllIssues() 0 60 8
A getImportLockID() 0 3 1
A getLockContent() 0 8 2
A isImportLockedByMe() 0 15 3
A __construct() 0 3 1
A unlockImport() 0 8 1
A lockImport() 0 7 1
A getIssue() 0 12 3
A keepNewest() 0 9 4
A getMergeRequestsForIssue() 0 16 2
A addUserToPages() 0 8 2
A getLinkingPages() 0 13 2
A filterPagesForACL() 0 9 3
1
<?php
2
/**
3
 * DokuWiki Plugin issuelinks (Helper Component)
4
 *
5
 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6
 * @author  Andreas Gohr <[email protected]>
7
 */
8
9
use dokuwiki\plugin\issuelinks\classes\Issue;
10
use dokuwiki\plugin\issuelinks\classes\ServiceProvider;
11
12
// must be run within Dokuwiki
13
if (!defined('DOKU_INC')) {
14
    die();
15
}
16
17
class helper_plugin_issuelinks_data extends DokuWiki_Plugin
18
{
19
20
    /** @var helper_plugin_issuelinks_db */
21
    private $db = null;
22
23
    /**
24
     * constructor. loads helpers
25
     */
26
    public function __construct()
27
    {
28
        $this->db = $this->loadHelper('issuelinks_db');
29
    }
30
31
32
    /**
33
     * Import all Jira issues starting at given paging offset
34
     *
35
     * @param string $serviceName The name of the project management service
36
     * @param string $projectKey  The short-key of the project to be imported
37
     *
38
     * @throws Exception
39
     */
40
    public function importAllIssues($serviceName, $projectKey)
41
    {
42
        $lockfileKey = $this->getImportLockID($serviceName, $projectKey);
43
        if ($this->isImportLocked($lockfileKey)) {
44
            throw new RuntimeException('Import of Issues is already locked!');
45
        }
46
        dbglog('start import. $lockfileKey: ' . $lockfileKey);
47
        $this->lockImport($lockfileKey, json_encode(['user' => $_SERVER['REMOTE_USER'], 'status' => 'started']));
48
49
        $serviceProvider = ServiceProvider::getInstance();
50
        $services = $serviceProvider->getServices();
51
        dbglog($services);
52
        dbglog($serviceName);
53
        $serviceClass = $services[$serviceName];
54
        dbglog($serviceClass);
55
        $service = $serviceClass::getInstance();
56
57
        $total = 0;
58
        $counter = 0;
59
        $startAt = 0;
60
61
        try {
62
            while ($issues = $service->retrieveAllIssues($projectKey, $startAt)) {
63
                if (!$total) {
64
                    $total = $service->getTotalIssuesBeingImported();
65
                }
66
67
                if ($counter > $total) {
68
                    break;
69
                }
70
71
                if (!$this->isImportLockedByMe($lockfileKey)) {
72
                    throw new RuntimeException('Import of Issues aborted because lock removed');
73
                }
74
75
                $counter += count($issues);
76
                $this->lockImport($lockfileKey, json_encode([
77
                    'user' => $_SERVER['REMOTE_USER'],
78
                    'total' => $total,
79
                    'count' => $counter,
80
                    'status' => 'running',
81
                ]));
82
83
84
            }
85
        } catch (\Throwable $e) {
86
            dbglog("Downloading all issues from $serviceName fpr project $projectKey failed ",
87
                __FILE__ . ': ' . __LINE__);
88
            if (is_a($e, \dokuwiki\plugin\issuelinks\classes\HTTPRequestException::class)) {
89
                /** @var \dokuwiki\plugin\issuelinks\classes\HTTPRequestException $e */
90
                dbglog($e->getUrl());
91
                dbglog($e->getHttpError());
92
                dbglog($e->getMessage());
93
                dbglog($e->getCode());
94
                dbglog($e->getResponseBody());
95
            }
96
            $this->lockImport($lockfileKey, json_encode(['status' => 'failed']));
97
            throw $e;
98
        }
99
        $this->unlockImport($lockfileKey);
100
    }
101
102
103
    public function getImportLockID($serviceName, $projectKey)
104
    {
105
        return "_plugin__issuelinks_import_$serviceName-$projectKey";
106
    }
107
108
    /**
109
     * This checks the lock for the import process it behaves differently from the dokuwiki-core checklock() function!
110
     *
111
     * It returns false if the lock does not exist. It returns **boolean true** if the lock exists and is mine.
112
     * It returns the username/ip if the lock exists and is not mine.
113
     * It is therefore important to use strict (===) checking for true!
114
     *
115
     * @param $id
116
     *
117
     * @return bool|string
118
     */
119
    public function isImportLocked($id)
120
    {
121
        global $conf;
122
        $lockFN = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock';
123
        if (!file_exists($lockFN)) {
124
            return false;
125
        }
126
127
        clearstatcache($lockFN);
0 ignored issues
show
Bug introduced by
$lockFN of type string is incompatible with the type boolean expected by parameter $clear_realpath_cache of clearstatcache(). ( Ignorable by Annotation )

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

127
        clearstatcache(/** @scrutinizer ignore-type */ $lockFN);
Loading history...
128
        if ((time() - filemtime($lockFN)) > 120) { // todo: decide if we want this to be configurable?
129
            @unlink($lockFN);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

129
            /** @scrutinizer ignore-unhandled */ @unlink($lockFN);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
130
            dbglog('issuelinks: stale lock timeout');
131
            return false;
132
        }
133
134
        $lockData = json_decode(io_readFile($lockFN), true);
135
        if (!empty($lockData['status']) && $lockData['status'] === 'done') {
136
            return false;
137
        }
138
139
        return true;
140
    }
141
142
    /**
143
     * Generate lock file for import of issues/commits
144
     *
145
     * This is mostly a reimplementation of @see lock()
146
     * However we do not clean the id and prepent a underscore to avoid conflicts with locks of existing pages.
147
     *
148
     * @param $id
149
     * @param $jsonData
150
     */
151
    public function lockImport($id, $jsonData)
152
    {
153
        global $conf;
154
155
        $lock = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock';
156
        dbglog('lock import: ' . $jsonData, __FILE__ . ': ' . __LINE__);
157
        io_saveFile($lock, $jsonData);
158
    }
159
160
    public function isImportLockedByMe($id)
161
    {
162
        if (!$this->isImportLocked($id)) {
163
            return false;
164
        }
165
166
        global $conf, $INPUT;
167
        $lockFN = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock';
168
        $lockData = json_decode(io_readFile($lockFN), true);
169
        if ($lockData['user'] !== $INPUT->server->str('REMOTE_USER')) {
170
            return false;
171
        }
172
173
        touch($lockFN);
174
        return true;
175
    }
176
177
    /**
178
     * Marks the import as unlocked / done
179
     *
180
     * @param $id
181
     */
182
    public function unlockImport($id)
183
    {
184
        global $conf;
185
        $lockFN = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock';
186
        $lockData = json_decode(io_readFile($lockFN), true);
187
        $lockData['status'] = 'done';
188
        $lockData['total'] = $lockData['count'];
189
        io_saveFile($lockFN, json_encode($lockData));
190
    }
191
192
    public function getLockContent($id)
193
    {
194
        global $conf;
195
        $lockFN = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock';
196
        if (!file_exists($lockFN)) {
197
            return false;
198
        }
199
        return json_decode(io_readFile($lockFN), true);
200
    }
201
202
    public function removeLock($lockID)
203
    {
204
        global $conf;
205
        $lockFN = $conf['lockdir'] . '/' . md5('_' . $lockID) . '.lock';
206
        unlink($lockFN);
207
    }
208
209
    /**
210
     * Get an issue either from local DB or attempt to import it
211
     *
212
     * @param string $pmServiceName The name of the project management service
213
     * @param string $project
214
     * @param int    $issueid
215
     * @param bool   $isMergeRequest
216
     *
217
     * @return bool|Issue
218
     */
219
    public function getIssue($pmServiceName, $project, $issueid, $isMergeRequest)
220
    {
221
        $issue = Issue::getInstance($pmServiceName, $project, $issueid, $isMergeRequest);
222
        if (!$issue->isValid()) {
223
            try {
224
                $issue->getFromService();
225
                $issue->saveToDB();
226
            } catch (Exception $e) {
227
                // that's fine
228
            }
229
        }
230
        return $issue;
231
    }
232
233
    public function getMergeRequestsForIssue($serviceName, $projectKey, $issueId, $isMergeRequest)
234
    {
235
        /** @var helper_plugin_issuelinks_db $db */
236
        $db = plugin_load('helper', 'issuelinks_db');
237
        $issues = $db->getMergeRequestsReferencingIssue($serviceName, $projectKey, $issueId, $isMergeRequest);
238
        foreach ($issues as &$issueData) {
239
            $issue = Issue::getInstance($issueData['service'], $issueData['project_id'], $issueData['issue_id'],
240
                $issueData['is_mergerequest']);
241
            $issue->getFromDB();
242
            $issueData['summary'] = $issue->getSummary();
243
            $issueData['status'] = $issue->getStatus();
244
            $issueData['url'] = $issue->getIssueURL();
245
        }
246
        unset($issueData);
247
248
        return $issues;
249
    }
250
251
    /**
252
     * Get Pages with links to issues
253
     *
254
     * @param string $pmServiceName The name of the project management service
255
     * @param string $projectKey
256
     * @param int    $issueId       the issue id
257
     * @param bool   $isMergeRequest
258
     *
259
     * @return array
260
     */
261
    public function getLinkingPages($pmServiceName, $projectKey, $issueId, $isMergeRequest)
262
    {
263
        $pages = $this->db->getAllPageLinkingToIssue($pmServiceName, $projectKey, $issueId, $isMergeRequest);
264
        $pages = $this->db->removeOldLinks($pmServiceName, $projectKey, $issueId, $isMergeRequest, $pages);
265
266
        if (empty($pages)) {
267
            return [];
268
        }
269
270
        $pages = $this->keepNewest($pages);
271
        $pages = $this->filterPagesForACL($pages);
272
        $pages = $this->addUserToPages($pages);
273
        return $pages;
274
    }
275
276
    /**
277
     * remove duplicate revisions of a page and keep only the newest
278
     *
279
     * @param array $pages Array of pages sorted(!) from newest to oldest
280
     *
281
     * @return array
282
     */
283
    public function keepNewest($pages)
284
    {
285
        $uniquePages = [];
286
        foreach ($pages as $page) {
287
            if (!array_key_exists($page['page'], $uniquePages) || $uniquePages[$page['page']]['rev'] < $page['rev']) {
288
                $uniquePages[$page['page']] = $page;
289
            }
290
        }
291
        return array_values($uniquePages);
292
    }
293
294
    /**
295
     * Filter the given pages for at least AUTH_READ
296
     *
297
     * @param array $pages
298
     *
299
     * @return array
300
     */
301
    private function filterPagesForACL($pages)
302
    {
303
        $allowedPagegs = [];
304
        foreach ($pages as $page) {
305
            if (auth_quickaclcheck($page['page']) >= AUTH_READ) {
306
                $allowedPagegs[] = $page;
307
            }
308
        }
309
        return $allowedPagegs;
310
    }
311
312
    /**
313
     * add the corresponding user to each revision
314
     *
315
     * @param array $pages
316
     *
317
     * @return array
318
     */
319
    public function addUserToPages($pages)
320
    {
321
        foreach ($pages as &$page) {
322
            $changelog = new PageChangelog($page['page']);
323
            $revision = $changelog->getRevisionInfo($page['rev']);
324
            $page['user'] = $revision['user'];
325
        }
326
        return $pages;
327
    }
328
}
329
330
// vim:ts=4:sw=4:et:
331