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); |
|
|
|
|
128
|
|
|
if ((time() - filemtime($lockFN)) > 120) { // todo: decide if we want this to be configurable? |
129
|
|
|
@unlink($lockFN); |
|
|
|
|
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
|
|
|
|