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 | // must be run within Dokuwiki |
||
10 | use dokuwiki\plugin\issuelinks\classes\Issue; |
||
11 | |||
12 | class helper_plugin_issuelinks_db extends DokuWiki_Plugin |
||
13 | { |
||
14 | private $db = null; |
||
15 | |||
16 | /** |
||
17 | * Save a key value pair to the database |
||
18 | * |
||
19 | * @param $key |
||
20 | * @param $value |
||
21 | * |
||
22 | * @return bool|null Returns false on error, nothing otherwise |
||
23 | */ |
||
24 | public function saveKeyValuePair($key, $value) |
||
25 | { |
||
26 | $db = $this->getDB(); |
||
27 | if (!$db) { |
||
28 | return false; |
||
29 | } |
||
30 | $sql = 'REPLACE INTO opts VALUES (?, ?)'; |
||
31 | $db->query($sql, [$key, $value]); |
||
32 | } |
||
33 | |||
34 | /** |
||
35 | * Gives access to the sqlite DB. |
||
36 | * |
||
37 | * Returns null on error |
||
38 | * |
||
39 | * @return helper_plugin_sqlite|null |
||
40 | * @throws Exception Only thrown in unittests |
||
41 | */ |
||
42 | 3 | public function getDB() |
|
43 | { |
||
44 | 3 | if (null === $this->db) { |
|
45 | /** @var helper_plugin_sqlite $sqlite */ |
||
46 | 3 | $sqlite = plugin_load('helper', 'sqlite'); |
|
47 | 3 | if (!$sqlite) { |
|
0 ignored issues
–
show
introduced
by
![]() |
|||
48 | msg('This plugin requires the sqlite plugin. Please install it', -1); |
||
49 | return null; |
||
50 | } |
||
51 | |||
52 | 3 | if ($sqlite->getAdapter()->getName() !== DOKU_EXT_PDO) { |
|
53 | if (defined('DOKU_UNITTEST')) { |
||
54 | throw new \Exception('Couldn\'t load PDO sqlite.'); |
||
55 | } |
||
56 | return null; |
||
57 | } |
||
58 | 3 | $sqlite->getAdapter()->setUseNativeAlter(true); |
|
59 | |||
60 | // initialize the database connection |
||
61 | 3 | if (!$sqlite->init('issuelinks', DOKU_PLUGIN . 'issuelinks/db/')) { |
|
62 | return null; |
||
63 | } |
||
64 | |||
65 | 3 | $this->db = $sqlite; |
|
66 | } |
||
67 | 3 | return $this->db; |
|
68 | } |
||
69 | |||
70 | /** |
||
71 | * Removes ALL entries from the database |
||
72 | * |
||
73 | * Useful when resetting the project configuration |
||
74 | */ |
||
75 | 3 | public function reset() |
|
76 | { |
||
77 | 3 | $db = $this->getDB(); |
|
78 | 3 | if (!$db) return; |
|
79 | |||
80 | 3 | $db->query('DELETE FROM issues'); |
|
81 | 3 | $db->query('DELETE FROM issue_issues'); |
|
82 | 3 | $db->query('DELETE FROM pagerev_issues'); |
|
83 | 3 | $db->query('VACUUM'); |
|
84 | 3 | } |
|
85 | |||
86 | /** |
||
87 | * Get a value to a stored key from the database |
||
88 | * |
||
89 | * @param $key |
||
90 | * |
||
91 | * @return bool|string |
||
92 | */ |
||
93 | 2 | public function getKeyValue($key) |
|
94 | { |
||
95 | 2 | $db = $this->getDB(); |
|
96 | 2 | if (!$db) { |
|
97 | return false; |
||
98 | } |
||
99 | 2 | $sql = 'SELECT val FROM opts WHERE opt = ?'; |
|
100 | 2 | $res = $db->query($sql, [$key]); |
|
101 | 2 | $value = $db->res2single($res); |
|
102 | 2 | $db->res_close($res); |
|
103 | 2 | return $value; |
|
104 | } |
||
105 | |||
106 | |||
107 | /** |
||
108 | * @param string $service The name of the repository management service |
||
109 | * @param string $repo The repository |
||
110 | * @param string $id The id of the webhook |
||
111 | * @param string $secret The secret to use when authenicationg incoming webhooks |
||
112 | */ |
||
113 | public function saveWebhook($service, $repo, $id, $secret) |
||
114 | { |
||
115 | $entity = [ |
||
116 | 'service' => $service, |
||
117 | 'repository_id' => $repo, |
||
118 | 'id' => $id, |
||
119 | 'secret' => $secret, |
||
120 | ]; |
||
121 | $this->saveEntity('webhooks', $entity); |
||
122 | } |
||
123 | |||
124 | /** |
||
125 | * Saves the given key-value array to the given table |
||
126 | * |
||
127 | * @param string $table |
||
128 | * @param array $entity associative array holding the key/value pairs |
||
129 | * |
||
130 | * @return bool|\SQLiteResult |
||
131 | */ |
||
132 | 2 | private function saveEntity($table, $entity) |
|
133 | { |
||
134 | 2 | $db = $this->getDB(); |
|
135 | 2 | if (!$db) { |
|
136 | return false; |
||
137 | } |
||
138 | |||
139 | 2 | $keys = implode(', ', array_keys($entity)); |
|
140 | 2 | $vals = array_values($entity); |
|
141 | 2 | $wlds = implode(', ', array_fill(0, count($vals), '?')); |
|
142 | |||
143 | 2 | $sql = "REPLACE INTO $table ($keys) VALUES ($wlds)"; |
|
144 | 2 | $ok = $db->query($sql, $vals); |
|
145 | 2 | if (empty($ok)) { |
|
146 | global $conf; |
||
147 | msg("Saving into table $table failed!", -1); |
||
148 | msg(print_r($entity, true), -1); |
||
149 | if ($conf['debug']) { |
||
150 | msg(dbg_backtrace(), -1); |
||
151 | } |
||
152 | } |
||
153 | 2 | return $ok; |
|
154 | } |
||
155 | |||
156 | /** |
||
157 | * Get the stored secret used to authenticate an incoming webhook |
||
158 | * |
||
159 | * @param string $rmservice |
||
160 | * @param string $repo |
||
161 | * |
||
162 | * @return array |
||
163 | */ |
||
164 | public function getWebhookSecrets($service, $repo) |
||
165 | { |
||
166 | $sql = "SELECT secret FROM webhooks WHERE service = ? AND repository_id = ?"; |
||
167 | $secrets = $this->sqlArrayQuery($sql, [$service, $repo]); |
||
168 | return $secrets; |
||
169 | } |
||
170 | |||
171 | /** |
||
172 | * make a provided sql query and return the resulting lines as an array of associative arrays |
||
173 | * |
||
174 | * @param string $sql the query |
||
175 | * @param string|array $conditional the parameters of the query |
||
176 | * |
||
177 | * @return array|bool |
||
178 | */ |
||
179 | 2 | private function sqlArrayQuery($sql, $conditional) |
|
180 | { |
||
181 | 2 | if (substr(trim($sql), 0, strlen('SELECT')) !== 'SELECT') { |
|
182 | throw new InvalidArgumentException("SQL-Statement must be a SELECT statement! \n" . $sql); |
||
183 | } |
||
184 | 2 | if (strpos(trim($sql, ';'), ';') !== false) { |
|
185 | throw new InvalidArgumentException("SQL-Statement must be one single statement! \n" . $sql); |
||
186 | } |
||
187 | 2 | $db = $this->getDB(); |
|
188 | 2 | if (!$db) { |
|
189 | return false; |
||
190 | } |
||
191 | |||
192 | 2 | $res = $db->query($sql, $conditional); |
|
193 | 2 | $result = $db->res2arr($res); |
|
194 | 2 | $db->res_close($res); |
|
195 | 2 | return $result; |
|
196 | } |
||
197 | |||
198 | /** |
||
199 | * @param string $service |
||
200 | * @param string $repo |
||
201 | * @param string $id |
||
202 | */ |
||
203 | public function deleteWebhook($service, $repo, $id) |
||
204 | { |
||
205 | $entity = [ |
||
206 | 'service' => $service, |
||
207 | 'repository_id' => $repo, |
||
208 | 'id' => $id, |
||
209 | ]; |
||
210 | $this->deleteEntity('webhooks', $entity); |
||
211 | } |
||
212 | |||
213 | /** |
||
214 | * Deletes the given key-value array to the given table |
||
215 | * |
||
216 | * @param string $table |
||
217 | * @param array $entity associative array holding the key/value pairs for the where clause |
||
218 | */ |
||
219 | 2 | private function deleteEntity($table, $entity) |
|
220 | { |
||
221 | 2 | $db = $this->getDB(); |
|
222 | 2 | if (!$db) { |
|
223 | return; |
||
224 | } |
||
225 | |||
226 | 2 | $where = implode(' = ? AND ', array_keys($entity)) . ' = ?'; |
|
227 | 2 | $vals = array_values($entity); |
|
228 | |||
229 | 2 | $sql = "DELETE FROM $table WHERE $where"; |
|
230 | 2 | $db->query($sql, $vals); |
|
231 | 2 | } |
|
232 | |||
233 | public function getWebhooks($service, $repo = null, $id = null) |
||
234 | { |
||
235 | $sql = 'SELECT * FROM webhooks WHERE service = ?'; |
||
236 | $params = [$service]; |
||
237 | if ($repo) { |
||
238 | $sql .= ' AND repository_id = ?'; |
||
239 | $params[] = $repo; |
||
240 | } |
||
241 | if ($id) { |
||
242 | $sql .= ' AND id = ?'; |
||
243 | $params[] = $id; |
||
244 | } |
||
245 | |||
246 | $webhooks = $this->sqlArrayQuery($sql, $params); |
||
247 | return $webhooks; |
||
248 | } |
||
249 | |||
250 | /** |
||
251 | * Save an issue into the database |
||
252 | * |
||
253 | * @param Issue $issue |
||
254 | * |
||
255 | * @return bool |
||
256 | */ |
||
257 | public function saveIssue(Issue $issue) |
||
258 | { |
||
259 | $ok = $this->saveEntity('issues', [ |
||
260 | 'service' => $issue->getServiceName(), |
||
261 | 'project' => $issue->getProject(), |
||
262 | 'id' => $issue->getKey(), |
||
263 | 'is_mergerequest' => $issue->isMergeRequest() ? '1' : '0', |
||
264 | 'summary' => $issue->getSummary(), |
||
265 | 'description' => $issue->getDescription(), |
||
266 | 'type' => $issue->getType(), |
||
267 | 'status' => $issue->getStatus(), |
||
268 | 'parent' => $issue->getParent(), |
||
269 | 'components' => implode(',', $issue->getComponents()), |
||
270 | 'labels' => implode(',', $issue->getLabels()), |
||
271 | 'priority' => $issue->getPriority(), |
||
272 | 'duedate' => $issue->getDuedate(), |
||
273 | 'versions' => implode(',', $issue->getVersions()), |
||
274 | 'updated' => $issue->getUpdated(), |
||
275 | ]); |
||
276 | return (bool)$ok; |
||
277 | } |
||
278 | |||
279 | /** |
||
280 | * Query the database for the issue corresponding to the given project and issueId |
||
281 | * |
||
282 | * @param string $serviceName The name of the project management service |
||
283 | * @param string $projectKey The short-key of a project, e.g. SPR |
||
284 | * @param int $issueId The id of an issue e.g. 42 |
||
285 | * |
||
286 | * @return bool|array |
||
287 | */ |
||
288 | 2 | public function loadIssue($serviceName, $projectKey, $issueId, $isMergeRequest) |
|
289 | { |
||
290 | 2 | $sql = 'SELECT * FROM issues WHERE service = ? AND project = ? AND id = ? AND is_mergerequest = ?'; |
|
291 | 2 | $issues = $this->sqlArrayQuery($sql, [$serviceName, $projectKey, $issueId, $isMergeRequest ? 1 : 0]); |
|
292 | 2 | return blank($issues[0]) ? false : $issues[0]; |
|
293 | } |
||
294 | |||
295 | public function saveIssueIssues(Issue $issue, array $issues) |
||
296 | { |
||
297 | $this->deleteEntity('issue_issues', [ |
||
298 | 'service' => $issue->getServiceName(), |
||
299 | 'project' => $issue->getProject(), |
||
300 | 'id' => $issue->getKey(), |
||
301 | 'is_mergerequest' => $issue->isMergeRequest() ? 1 : 0, |
||
302 | ]); |
||
303 | foreach ($issues as $issueData) { |
||
304 | $this->saveEntity('issue_issues', [ |
||
305 | 'service' => $issue->getServiceName(), |
||
306 | 'project' => $issue->getProject(), |
||
307 | 'id' => $issue->getKey(), |
||
308 | 'is_mergerequest' => $issue->isMergeRequest() ? 1 : 0, |
||
309 | 'referenced_service' => $issueData['service'], |
||
310 | 'referenced_project' => $issueData['project'], |
||
311 | 'referenced_id' => $issueData['issueId'], |
||
312 | 'referenced_is_mergerequest' => 0, |
||
313 | ]); |
||
314 | } |
||
315 | } |
||
316 | |||
317 | public function getMergeRequestsReferencingIssue($serviceName, $project, $issueId, $isMergeRequest) |
||
318 | { |
||
319 | $sql = ' |
||
320 | SELECT service, project as project_id, id as issue_id, is_mergerequest |
||
321 | FROM issue_issues |
||
322 | WHERE referenced_service = ? |
||
323 | AND referenced_project = ? |
||
324 | AND referenced_id = ? |
||
325 | AND referenced_is_mergerequest = ? |
||
326 | AND is_mergerequest = 1 |
||
327 | '; |
||
328 | return $this->sqlArrayQuery($sql, [$serviceName, $project, $issueId, $isMergeRequest ? 1 : 0]); |
||
329 | } |
||
330 | |||
331 | /** |
||
332 | * Query the database for pages with link-syntax to the given issue |
||
333 | * |
||
334 | * @param string $serviceName The name of the project management service |
||
335 | * @param string $projectKey The project short-key |
||
336 | * @param int $issue_id The ID of the issue, e.g. 42 |
||
337 | * |
||
338 | * @return array |
||
339 | */ |
||
340 | public function getAllPageLinkingToIssue($serviceName, $projectKey, $issue_id, $isMergeRequest) |
||
341 | { |
||
342 | $sql = "SELECT page, rev |
||
343 | FROM pagerev_issues |
||
344 | WHERE service = ? |
||
345 | AND project_id = ? |
||
346 | AND issue_id = ? |
||
347 | AND is_mergerequest = ? |
||
348 | AND type = 'link' |
||
349 | ORDER BY rev DESC "; |
||
350 | return $this->sqlArrayQuery($sql, [$serviceName, $projectKey, $issue_id, $isMergeRequest ? 1 : 0]); |
||
351 | } |
||
352 | |||
353 | /** |
||
354 | * Delete "Link"-references to old revisions from database |
||
355 | * |
||
356 | * @param string $serviceName The name of the project management service |
||
357 | * @param string $projectKey The short-key for the project, e.g. SPR |
||
358 | * @param int $issue_id The id of the issue, e.g. 42 |
||
359 | * @param array $pages |
||
360 | * |
||
361 | * @return array |
||
362 | */ |
||
363 | public function removeOldLinks($serviceName, $projectKey, $issue_id, $isMergeRequest, $pages) |
||
364 | { |
||
365 | $activeLinks = []; |
||
366 | |||
367 | foreach ($pages as $linkingPage) { |
||
368 | $changelog = new PageChangelog($linkingPage['page']); |
||
369 | $currentRev = $changelog->getRelativeRevision(time(), -1); |
||
370 | if ($linkingPage['rev'] < $currentRev) { |
||
371 | $entity = [ |
||
372 | 'page' => $linkingPage['page'], |
||
373 | 'issue_id' => $issue_id, |
||
374 | 'project_id' => $projectKey, |
||
375 | 'service' => $serviceName, |
||
376 | 'is_mergerequest' => $isMergeRequest ? '1' : '0', |
||
377 | 'type' => 'link', |
||
378 | ]; |
||
379 | $this->deleteEntity('pagerev_issues', $entity); |
||
380 | } else { |
||
381 | $activeLinks[] = $linkingPage; |
||
382 | } |
||
383 | } |
||
384 | return $activeLinks; |
||
385 | } |
||
386 | |||
387 | /** |
||
388 | * Save the connection between a Jira issue and a revision of a page. |
||
389 | * |
||
390 | * @param string $page |
||
391 | * @param int $rev |
||
392 | * @param string $serviceName The name of the project management service |
||
393 | * @param string $project |
||
394 | * @param int $issue_id |
||
395 | * @param string $type |
||
396 | * |
||
397 | * @return bool |
||
398 | * |
||
399 | * @throws \InvalidArgumentException |
||
400 | */ |
||
401 | 2 | public function savePageRevIssues($page, $rev, $serviceName, $project, $issue_id, $isMergeRequest, $type) |
|
402 | { |
||
403 | /** @var helper_plugin_issuelinks_util $util */ |
||
404 | 2 | $util = plugin_load('helper', 'issuelinks_util'); |
|
405 | 2 | if (!$util->isValidTimeStamp($rev)) { |
|
406 | throw new InvalidArgumentException("Second parameter must be a valid timestamp!"); |
||
407 | } |
||
408 | 2 | if ((int)$rev === 0) { |
|
409 | $rev = filemtime(wikiFN($page)); |
||
410 | $changelog = new PageChangelog($page); |
||
411 | $rev_info = $changelog->getRevisionInfo($rev); |
||
412 | $user = $rev_info['user'] ? $rev_info['user'] : $rev_info['ip']; |
||
413 | $this->savePageRev($page, $rev, $rev_info['sum'], $user); |
||
414 | } |
||
415 | /** @noinspection TypeUnsafeComparisonInspection this is done to ensure $issue_id is a natural number */ |
||
416 | 2 | if (!is_numeric($issue_id) || (int)$issue_id != $issue_id) { |
|
0 ignored issues
–
show
|
|||
417 | throw new InvalidArgumentException("IssueId must be an integer!"); |
||
418 | } |
||
419 | 2 | $ok = $this->saveEntity('pagerev_issues', [ |
|
420 | 2 | 'page' => $page, |
|
421 | 2 | 'rev' => $rev, |
|
422 | 2 | 'service' => $serviceName, |
|
423 | 2 | 'project_id' => $project, |
|
424 | 2 | 'issue_id' => $issue_id, |
|
425 | 2 | 'is_mergerequest' => $isMergeRequest ? '1' : '0', |
|
426 | 2 | 'type' => $type, |
|
427 | ]); |
||
428 | |||
429 | 2 | return (bool)$ok; |
|
430 | } |
||
431 | |||
432 | /** |
||
433 | * Save the data about a pagerevision |
||
434 | * |
||
435 | * @param string $page |
||
436 | * @param int $rev |
||
437 | * @param string $summary |
||
438 | * @param string $user |
||
439 | * |
||
440 | * @return bool |
||
441 | */ |
||
442 | public function savePageRev($page, $rev, $summary, $user) |
||
443 | { |
||
444 | if (blank($page) || blank($rev) || blank($user)) { |
||
445 | throw new InvalidArgumentException("No empty values allowed!"); |
||
446 | } |
||
447 | /** @var helper_plugin_issuelinks_util $util */ |
||
448 | $util = plugin_load('helper', 'issuelinks_util'); |
||
449 | if (!$util->isValidTimeStamp($rev)) { |
||
450 | throw new InvalidArgumentException("Second parameter must be a valid timestamp!"); |
||
451 | } |
||
452 | $ok = $this->saveEntity('pagerevs', [ |
||
453 | 'page' => $page, |
||
454 | 'rev' => $rev, |
||
455 | 'summary' => $summary, |
||
456 | 'user' => $user, |
||
457 | ]); |
||
458 | return (bool)$ok; |
||
459 | } |
||
460 | |||
461 | /** |
||
462 | * Delete ALL entries from the database that correspond to the given page, issue and type. |
||
463 | * |
||
464 | * @param string $page the wikipage |
||
465 | * @param string $serviceName The name of the project management service |
||
466 | * @param string $projectKey the key of the project, e.g. SPR |
||
467 | * @param int $issueId the id of the issue, e.g. 42 |
||
468 | * @param bool $isMergeRequest |
||
469 | * @param string $type either 'context' or 'link' |
||
470 | */ |
||
471 | 2 | public function deleteAllIssuePageRevisions($page, $serviceName, $projectKey, $issueId, $isMergeRequest, $type) |
|
472 | { |
||
473 | // todo: validation |
||
474 | 2 | $this->deleteEntity('pagerev_issues', [ |
|
475 | 2 | 'page' => $page, |
|
476 | 2 | 'service' => $serviceName, |
|
477 | 2 | 'project_id' => $projectKey, |
|
478 | 2 | 'issue_id' => $issueId, |
|
479 | 2 | 'is_mergerequest' => $isMergeRequest ? 1 : 0, |
|
480 | 2 | 'type' => $type, |
|
481 | ]); |
||
482 | 2 | } |
|
483 | } |
||
484 |