Passed
Push — master ( c7b2ee...da981b )
by Michael
03:00
created

helper_plugin_issuelinks_db::reset()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 6
nc 2
nop 0
dl 0
loc 9
ccs 0
cts 7
cp 0
crap 6
rs 9.6666
c 1
b 0
f 1
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
    public function getDB()
43
    {
44
        if (null === $this->db) {
45
            /** @var helper_plugin_sqlite $sqlite */
46
            $sqlite = plugin_load('helper', 'sqlite');
47
            if (!$sqlite) {
0 ignored issues
show
introduced by
$sqlite is of type helper_plugin_sqlite, thus it always evaluated to true.
Loading history...
48
                msg('This plugin requires the sqlite plugin. Please install it', -1);
49
                return null;
50
            }
51
52
            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
            $sqlite->getAdapter()->setUseNativeAlter(true);
59
60
            // initialize the database connection
61
            if (!$sqlite->init('issuelinks', DOKU_PLUGIN . 'issuelinks/db/')) {
62
                return null;
63
            }
64
65
            $this->db = $sqlite;
66
        }
67
        return $this->db;
68
    }
69
70
    /**
71
     * Removes ALL entries from the database
72
     *
73
     * Useful when resetting the project configuration
74
     */
75
    public function reset()
76
    {
77
        $db = $this->getDB();
78
        if (!$db) return;
79
80
        $db->query('DELETE FROM issues');
81
        $db->query('DELETE FROM issue_issues');
82
        $db->query('DELETE FROM pagerev_issues');
83
        $db->query('VACUUM');
84
    }
85
86
    /**
87
     * Get a value to a stored key from the database
88
     *
89
     * @param $key
90
     *
91
     * @return bool|string
92
     */
93
    public function getKeyValue($key)
94
    {
95
        $db = $this->getDB();
96
        if (!$db) {
97
            return false;
98
        }
99
        $sql = 'SELECT val FROM opts WHERE opt = ?';
100
        $res = $db->query($sql, [$key]);
101
        $value = $db->res2single($res);
102
        $db->res_close($res);
103
        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
    private function saveEntity($table, $entity)
133
    {
134
        $db = $this->getDB();
135
        if (!$db) {
136
            return false;
137
        }
138
139
        $keys = implode(', ', array_keys($entity));
140
        $vals = array_values($entity);
141
        $wlds = implode(', ', array_fill(0, count($vals), '?'));
142
143
        $sql = "REPLACE INTO $table ($keys) VALUES ($wlds)";
144
        $ok = $db->query($sql, $vals);
145
        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
        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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $secrets also could return the type boolean which is incompatible with the documented return type array.
Loading history...
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
    private function sqlArrayQuery($sql, $conditional)
180
    {
181
        if (substr(trim($sql), 0, strlen('SELECT')) !== 'SELECT') {
182
            throw new InvalidArgumentException("SQL-Statement must be a SELECT statement! \n" . $sql);
183
        }
184
        if (strpos(trim($sql, ';'), ';') !== false) {
185
            throw new InvalidArgumentException("SQL-Statement must be one single statement! \n" . $sql);
186
        }
187
        $db = $this->getDB();
188
        if (!$db) {
189
            return false;
190
        }
191
192
        $res = $db->query($sql, $conditional);
193
        $result = $db->res2arr($res);
194
        $db->res_close($res);
195
        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
    private function deleteEntity($table, $entity)
220
    {
221
        $db = $this->getDB();
222
        if (!$db) {
223
            return;
224
        }
225
226
        $where = implode(' = ? AND ', array_keys($entity)) . ' = ?';
227
        $vals = array_values($entity);
228
229
        $sql = "DELETE FROM $table WHERE $where";
230
        $db->query($sql, $vals);
231
    }
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
    public function loadIssue($serviceName, $projectKey, $issueId, $isMergeRequest)
289
    {
290
        $sql = 'SELECT * FROM issues WHERE service = ? AND project = ? AND id = ? AND is_mergerequest = ?';
291
        $issues = $this->sqlArrayQuery($sql, [$serviceName, $projectKey, $issueId, $isMergeRequest ? 1 : 0]);
292
        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]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->sqlArrayQu...sMergeRequest ? 1 : 0)) also could return the type boolean which is incompatible with the documented return type array.
Loading history...
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
    public function savePageRevIssues($page, $rev, $serviceName, $project, $issue_id, $isMergeRequest, $type)
402
    {
403
        /** @var helper_plugin_issuelinks_util $util */
404
        $util = plugin_load('helper', 'issuelinks_util');
405
        if (!$util->isValidTimeStamp($rev)) {
406
            throw new InvalidArgumentException("Second parameter must be a valid timestamp!");
407
        }
408
        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
        if (!is_numeric($issue_id) || (int)$issue_id != $issue_id) {
0 ignored issues
show
introduced by
The condition is_numeric($issue_id) is always true.
Loading history...
417
            throw new InvalidArgumentException("IssueId must be an integer!");
418
        }
419
        $ok = $this->saveEntity('pagerev_issues', [
420
            'page' => $page,
421
            'rev' => $rev,
422
            'service' => $serviceName,
423
            'project_id' => $project,
424
            'issue_id' => $issue_id,
425
            'is_mergerequest' => $isMergeRequest ? '1' : '0',
426
            'type' => $type,
427
        ]);
428
429
        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
    public function deleteAllIssuePageRevisions($page, $serviceName, $projectKey, $issueId, $isMergeRequest, $type)
472
    {
473
        // todo: validation
474
        $this->deleteEntity('pagerev_issues', [
475
            'page' => $page,
476
            'service' => $serviceName,
477
            'project_id' => $projectKey,
478
            'issue_id' => $issueId,
479
            'is_mergerequest' => $isMergeRequest ? 1 : 0,
480
            'type' => $type,
481
        ]);
482
    }
483
}
484