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 <dokuwiki@cosmocode.de>
7 */
8
9use dokuwiki\plugin\issuelinks\classes\Issue;
10use dokuwiki\plugin\issuelinks\classes\ServiceProvider;
11
12class helper_plugin_issuelinks_data extends DokuWiki_Plugin
13{
14
15    /** @var helper_plugin_issuelinks_db */
16    private $db = null;
17
18    /**
19     * constructor. loads helpers
20     */
21    public function __construct()
22    {
23        $this->db = $this->loadHelper('issuelinks_db');
24    }
25
26
27    /**
28     * Import all Jira issues starting at given paging offset
29     *
30     * @param string $serviceName The name of the project management service
31     * @param string $projectKey  The short-key of the project to be imported
32     *
33     * @throws Exception
34     */
35    public function importAllIssues($serviceName, $projectKey)
36    {
37        $lockfileKey = $this->getImportLockID($serviceName, $projectKey);
38        if ($this->isImportLocked($lockfileKey)) {
39            throw new RuntimeException('Import of Issues is already locked!');
40        }
41        dbglog('start import. $lockfileKey: ' . $lockfileKey);
42        $this->lockImport($lockfileKey, json_encode(['user' => $_SERVER['REMOTE_USER'], 'status' => 'started']));
43
44        $serviceProvider = ServiceProvider::getInstance();
45        $services = $serviceProvider->getServices();
46        dbglog($services);
47        dbglog($serviceName);
48        $serviceClass = $services[$serviceName];
49        dbglog($serviceClass);
50        $service = $serviceClass::getInstance();
51
52        $total = 0;
53        $counter = 0;
54        $startAt = 0;
55
56        try {
57            while ($issues = $service->retrieveAllIssues($projectKey, $startAt)) {
58                if (!$total) {
59                    $total = $service->getTotalIssuesBeingImported();
60                }
61
62                if ($counter > $total) {
63                    break;
64                }
65
66                if (!$this->isImportLockedByMe($lockfileKey)) {
67                    throw new RuntimeException('Import of Issues aborted because lock removed');
68                }
69
70                $counter += count($issues);
71                $this->lockImport($lockfileKey, json_encode([
72                    'user' => $_SERVER['REMOTE_USER'],
73                    'total' => $total,
74                    'count' => $counter,
75                    'status' => 'running',
76                ]));
77            }
78        } catch (\Throwable $e) {
79            dbglog(
80                "Downloading all issues from $serviceName fpr project $projectKey failed ",
81                __FILE__ . ': ' . __LINE__
82            );
83            if (is_a($e, \dokuwiki\plugin\issuelinks\classes\HTTPRequestException::class)) {
84                /** @var \dokuwiki\plugin\issuelinks\classes\HTTPRequestException $e */
85                dbglog($e->getUrl());
86                dbglog($e->getHttpError());
87                dbglog($e->getMessage());
88                dbglog($e->getCode());
89                dbglog($e->getResponseBody());
90            }
91            $this->lockImport($lockfileKey, json_encode(['status' => 'failed']));
92            throw $e;
93        }
94        $this->unlockImport($lockfileKey);
95    }
96
97
98    public function getImportLockID($serviceName, $projectKey)
99    {
100        return "_plugin__issuelinks_import_$serviceName-$projectKey";
101    }
102
103    /**
104     * This checks the lock for the import process it behaves differently from the dokuwiki-core checklock() function!
105     *
106     * It returns false if the lock does not exist. It returns **boolean true** if the lock exists and is mine.
107     * It returns the username/ip if the lock exists and is not mine.
108     * It is therefore important to use strict (===) checking for true!
109     *
110     * @param $id
111     *
112     * @return bool|string
113     */
114    public function isImportLocked($id)
115    {
116        global $conf;
117        $lockFN = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock';
118        if (!file_exists($lockFN)) {
119            return false;
120        }
121
122        clearstatcache(true, $lockFN);
123        if ((time() - filemtime($lockFN)) > 120) {
124            unlink($lockFN);
125            dbglog('issuelinks: stale lock timeout');
126            return false;
127        }
128
129        $lockData = json_decode(io_readFile($lockFN), true);
130        if (!empty($lockData['status']) && $lockData['status'] === 'done') {
131            return false;
132        }
133
134        return true;
135    }
136
137    /**
138     * Generate lock file for import of issues/commits
139     *
140     * This is mostly a reimplementation of @see lock()
141     * However we do not clean the id and prepent a underscore to avoid conflicts with locks of existing pages.
142     *
143     * @param $id
144     * @param $jsonData
145     */
146    public function lockImport($id, $jsonData)
147    {
148        global $conf;
149
150        $lock = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock';
151        dbglog('lock import: ' . $jsonData, __FILE__ . ': ' . __LINE__);
152        io_saveFile($lock, $jsonData);
153    }
154
155    public function isImportLockedByMe($id)
156    {
157        if (!$this->isImportLocked($id)) {
158            return false;
159        }
160
161        global $conf, $INPUT;
162        $lockFN = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock';
163        $lockData = json_decode(io_readFile($lockFN), true);
164        if ($lockData['user'] !== $INPUT->server->str('REMOTE_USER')) {
165            return false;
166        }
167
168        touch($lockFN);
169        return true;
170    }
171
172    /**
173     * Marks the import as unlocked / done
174     *
175     * @param $id
176     */
177    public function unlockImport($id)
178    {
179        global $conf;
180        $lockFN = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock';
181        $lockData = json_decode(io_readFile($lockFN), true);
182        $lockData['status'] = 'done';
183        $lockData['total'] = $lockData['count'];
184        io_saveFile($lockFN, json_encode($lockData));
185    }
186
187    public function getLockContent($id)
188    {
189        global $conf;
190        $lockFN = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock';
191        if (!file_exists($lockFN)) {
192            return false;
193        }
194        return json_decode(io_readFile($lockFN), true);
195    }
196
197    public function removeLock($lockID)
198    {
199        global $conf;
200        $lockFN = $conf['lockdir'] . '/' . md5('_' . $lockID) . '.lock';
201        unlink($lockFN);
202    }
203
204    /**
205     * Get an issue either from local DB or attempt to import it
206     *
207     * @param string $pmServiceName The name of the project management service
208     * @param string $project
209     * @param int    $issueid
210     * @param bool   $isMergeRequest
211     *
212     * @return bool|Issue
213     */
214    public function getIssue($pmServiceName, $project, $issueid, $isMergeRequest)
215    {
216        $issue = Issue::getInstance($pmServiceName, $project, $issueid, $isMergeRequest);
217        if (!$issue->isValid()) {
218            try {
219                $issue->getFromService();
220                $issue->saveToDB();
221            } catch (Exception $e) {
222                // that's fine
223            }
224        }
225        return $issue;
226    }
227
228    public function getMergeRequestsForIssue($serviceName, $projectKey, $issueId, $isMergeRequest)
229    {
230        /** @var helper_plugin_issuelinks_db $db */
231        $db = plugin_load('helper', 'issuelinks_db');
232        $issues = $db->getMergeRequestsReferencingIssue($serviceName, $projectKey, $issueId, $isMergeRequest);
233        foreach ($issues as &$issueData) {
234            $issue = Issue::getInstance(
235                $issueData['service'],
236                $issueData['project_id'],
237                $issueData['issue_id'],
238                $issueData['is_mergerequest']
239            );
240            $issue->getFromDB();
241            $issueData['summary'] = $issue->getSummary();
242            $issueData['status'] = $issue->getStatus();
243            $issueData['url'] = $issue->getIssueURL();
244        }
245        unset($issueData);
246
247        return $issues;
248    }
249
250    /**
251     * Get Pages with links to issues
252     *
253     * @param string $pmServiceName The name of the project management service
254     * @param string $projectKey
255     * @param int    $issueId       the issue id
256     * @param bool   $isMergeRequest
257     *
258     * @return array
259     */
260    public function getLinkingPages($pmServiceName, $projectKey, $issueId, $isMergeRequest)
261    {
262        $pages = $this->db->getAllPageLinkingToIssue($pmServiceName, $projectKey, $issueId, $isMergeRequest);
263        $pages = $this->db->removeOldLinks($pmServiceName, $projectKey, $issueId, $isMergeRequest, $pages);
264
265        if (empty($pages)) {
266            return [];
267        }
268
269        $pages = $this->keepNewest($pages);
270        $pages = $this->filterPagesForACL($pages);
271        $pages = $this->addUserToPages($pages);
272        return $pages;
273    }
274
275    /**
276     * remove duplicate revisions of a page and keep only the newest
277     *
278     * @param array $pages Array of pages sorted(!) from newest to oldest
279     *
280     * @return array
281     */
282    public function keepNewest($pages)
283    {
284        $uniquePages = [];
285        foreach ($pages as $page) {
286            if (!array_key_exists($page['page'], $uniquePages) || $uniquePages[$page['page']]['rev'] < $page['rev']) {
287                $uniquePages[$page['page']] = $page;
288            }
289        }
290        return array_values($uniquePages);
291    }
292
293    /**
294     * Filter the given pages for at least AUTH_READ
295     *
296     * @param array $pages
297     *
298     * @return array
299     */
300    private function filterPagesForACL($pages)
301    {
302        $allowedPagegs = [];
303        foreach ($pages as $page) {
304            if (auth_quickaclcheck($page['page']) >= AUTH_READ) {
305                $allowedPagegs[] = $page;
306            }
307        }
308        return $allowedPagegs;
309    }
310
311    /**
312     * add the corresponding user to each revision
313     *
314     * @param array $pages
315     *
316     * @return array
317     */
318    public function addUserToPages($pages)
319    {
320        foreach ($pages as &$page) {
321            $changelog = new PageChangelog($page['page']);
322            $revision = $changelog->getRevisionInfo($page['rev']);
323            $page['user'] = $revision['user'];
324        }
325        return $pages;
326    }
327}
328
329// vim:ts=4:sw=4:et:
330