1<?php
2/**
3 * DokuWiki Plugin Issuelinks (Action Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Michael Große <dokuwiki@cosmocode.de>
7 */
8
9use dokuwiki\plugin\issuelinks\classes\Issue;
10use dokuwiki\plugin\issuelinks\classes\ServiceProvider;
11
12class action_plugin_issuelinks_ajax extends DokuWiki_Action_Plugin
13{
14
15    /** @var helper_plugin_issuelinks_util util */
16    public $util;
17
18    public function __construct()
19    {
20        /** @var helper_plugin_issuelinks_util util */
21        $this->util = plugin_load('helper', 'issuelinks_util');
22    }
23
24    /**
25     * Registers a callback function for a given event
26     *
27     * @param Doku_Event_Handler $controller DokuWiki's event controller object
28     *
29     * @return void
30     */
31    public function register(Doku_Event_Handler $controller)
32    {
33        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjax');
34        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'repoAdminToggle');
35        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'repoAdminOrg');
36        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'asyncImportAllIssues');
37    }
38
39    /**
40     * Create/Delete the webhook of a repository
41     *
42     * @param Doku_Event $event  event object by reference
43     * @param mixed      $param  [the parameters passed as fifth argument to register_hook() when this
44     *                           handler was registered]
45     *
46     * @return void
47     */
48    public function repoAdminToggle(Doku_Event $event, $param)
49    {
50        if ($event->data !== 'issuelinks_repo_admin_toggle') {
51            return;
52        }
53        $event->preventDefault();
54        $event->stopPropagation();
55
56        if (empty($_SERVER['REMOTE_USER'])) {
57            $this->util->sendResponse(401, 'Not logged in!');
58            return;
59        }
60
61        global $INPUT, $INFO;
62        if (!auth_isadmin()) {
63            $this->util->sendResponse(403, 'Must be Admin');
64            return;
65        }
66
67        $serviceId = $INPUT->str('servicename');
68
69        $serviceProvider = ServiceProvider::getInstance();
70        $services = $serviceProvider->getServices();
71        $service = $services[$serviceId]::getInstance();
72
73        $project = $INPUT->str('project');
74
75        if ($INPUT->has('hookid')) {
76            $response = $service->deleteWebhook($project, $INPUT->str('hookid'));
77        } else {
78            $response = $service->createWebhook($project);
79        }
80
81        $this->util->sendResponse($response['status'], $response['data']);
82    }
83
84    /**
85     * Get the repos of an organisation and create HTML from them and return it to the request
86     *
87     * @param Doku_Event $event  event object by reference
88     * @param mixed      $param  [the parameters passed as fifth argument to register_hook() when this
89     *                           handler was registered]
90     *
91     * @return void
92     */
93    public function repoAdminOrg(Doku_Event $event, $param)
94    {
95        if ($event->data !== 'issuelinks_repo_admin_getorg') {
96            return;
97        }
98        $event->preventDefault();
99        $event->stopPropagation();
100
101        if (empty($_SERVER['REMOTE_USER'])) {
102            $this->util->sendResponse(401, 'Not logged in!');
103            return;
104        }
105
106        global $INPUT;
107        if (!auth_isadmin()) {
108            $this->util->sendResponse(403, 'Must be Admin');
109            return;
110        }
111
112        $serviceId = $INPUT->str('servicename');
113        $organisation = $INPUT->str('org');
114        try {
115            $html = $this->createOrgRepoHTML($serviceId, $organisation);
116        } catch (\Throwable $e) {
117            $this->util->sendResponse($e->getCode(), $e->getMessage());
118        }
119        $this->util->sendResponse(200, $html);
120    }
121
122    /**
123     * Create the HTML of the repositories of an organisation/group of a service.
124     *
125     * @param string $org the organisation from which to request the repositories
126     *
127     * @return string
128     */
129    public function createOrgRepoHTML($serviceId, $org)
130    {
131        $serviceProvider = ServiceProvider::getInstance();
132        $services = $serviceProvider->getServices();
133        $service = $services[$serviceId]::getInstance();
134
135        $repos = $service->getListOfAllReposAndHooks($org);
136        $html = '<div class="org_repos">';
137        $html .= '<p>' . $this->getLang('text:repo admin') . '</p>';
138        $orgAdvice = $service->getRepoPageText();
139        if ($orgAdvice) {
140            $html .= '<p>' . $orgAdvice . '</p>';
141        }
142        $html .= '<div><ul>';
143        usort($repos, function ($repo1, $repo2) {
144            return $repo1->displayName < $repo2->displayName ? -1 : 1;
145        });
146        $importSVG = inlineSVG(__DIR__ . '/../images/import.svg');
147        foreach ($repos as $repo) {
148            $stateIssue = empty($repo->hookID) ? 'inactive' : 'active';
149            if ($repo->error === 403) {
150                $stateIssue = 'forbidden';
151            } elseif (!empty($repo->error)) {
152                continue;
153            }
154            $repoDisplayName = $repo->displayName;
155            $project = $repo->full_name;
156            $hookTitle = $repo->error === 403 ? $this->getLang('title:forbidden') : $this->getLang('title:issue hook');
157            $html .= "<li><div class='li'>";
158            $spanAttributes = [
159                'title' => $hookTitle,
160                'data-project' => $project,
161                'data-id' => $repo->hookID,
162                'class' => "repohookstatus $stateIssue issue",
163            ];
164            $html .= '<span ' . buildAttributes($spanAttributes, true) . '></span>';
165            $buttonAttributes = [
166                'title' => 'Import all issues of this repository',
167                'data-project' => $project,
168                'class' => 'issueImport js-importIssues',
169            ];
170            $html .= '<button ' . buildAttributes($buttonAttributes, true) . ">$importSVG</button>";
171            $html .= "<span class='mm_reponame'>$repoDisplayName</span>";
172            $html .= '</div></li>';
173        }
174        $html .= '</ul></div></div>';
175        return $html;
176    }
177
178    public function asyncImportAllIssues(Doku_Event $event, $param)
179    {
180        if ($event->data !== 'issuelinks_import_all_issues_async') {
181            return;
182        }
183        $event->preventDefault();
184        $event->stopPropagation();
185
186        if (!auth_isadmin()) {
187            $this->util->sendResponse(403, 'Must be Admin');
188        }
189        global $INPUT;
190        $serviceName = $INPUT->str('servicename');
191        $projectKey = $INPUT->str('project');
192
193        // fixme check if $serviceName and $projectKey exist
194        if (empty($serviceName) || empty($projectKey)) {
195            $this->util->sendResponse(400, 'service or project is missing');
196        }
197
198
199        ignore_user_abort('true');
200        set_time_limit(60 * 20);
201        ob_start();
202        $this->util->sendResponse(202, 'Importing  issues...');
203        header('Connection: close');
204        header('Content-Length: ' . ob_get_length());
205        ob_end_flush();
206        ob_end_flush();
207        flush();
208
209        /** @var helper_plugin_issuelinks_data $data */
210        $data = plugin_load('helper', 'issuelinks_data');
211        $data->importAllIssues($serviceName, $projectKey);
212    }
213
214    /**
215     * Pass Ajax call to a type
216     *
217     * @param Doku_Event $event  event object by reference
218     * @param mixed      $param  [the parameters passed as fifth argument to register_hook() when this
219     *                           handler was registered]
220     *
221     * @return void
222     */
223    public function handleAjax(Doku_Event $event, $param)
224    {
225        if ($event->data !== 'plugin_issuelinks') {
226            return;
227        }
228        $event->preventDefault();
229        $event->stopPropagation();
230
231        if (empty($_SERVER['REMOTE_USER'])) {
232            $this->util->sendResponse(401, 'Not logged in!');
233            return;
234        }
235
236        global $INPUT;
237
238        $action = $INPUT->str('issuelinks-action');
239        $serviceName = $INPUT->str('issuelinks-service');
240        $projectKey = $INPUT->str('issuelinks-project');
241        $issueId = $INPUT->str('issuelinks-issueid');
242        $isMergeRequest = $INPUT->bool('issuelinks-ismergerequest');
243
244        switch ($action) {
245            case 'checkImportStatus':
246                list($code, $data) = $this->checkImportStatus($serviceName, $projectKey);
247                break;
248            case 'issueToolTip':
249                list($code, $data) = $this->getIssueTooltipHTML($serviceName, $projectKey, $issueId, $isMergeRequest);
250                break;
251            case 'getAdditionalIssueData':
252                list($code, $data) = $this->getAdditionalIssueData(
253                    $serviceName,
254                    $projectKey,
255                    $issueId,
256                    $isMergeRequest
257                );
258                break;
259            default:
260                $code = 400;
261                $data = 'malformed request';
262        }
263        $this->util->sendResponse($code, $data);
264    }
265
266    protected function checkImportStatus($serviceName, $projectKey)
267    {
268        if (!auth_isadmin()) {
269            return [403, 'Must be Admin'];
270        }
271
272        /** @var helper_plugin_issuelinks_data $data */
273        $data = plugin_load('helper', 'issuelinks_data');
274        $lockID = $data->getImportLockID($serviceName, $projectKey);
275        $lockData = $data->getLockContent($lockID);
276        if ($lockData === false) {
277            msg('Import not locked ' . $lockID, 2);
278            return [200, ['status' => 'done']];
279        }
280        if (!empty($lockData['status']) && $lockData['status'] === 'done') {
281            $data->removeLock($lockID);
282        }
283
284        return [200, $lockData];
285    }
286
287    /**
288     * @param $pmServiceName
289     * @param $projectKey
290     * @param $issueId
291     *
292     * @return array
293     */
294    private function getIssueTooltipHTML($pmServiceName, $projectKey, $issueId, $isMergeRequest)
295    {
296        try {
297            $issue = Issue::getInstance($pmServiceName, $projectKey, $issueId, $isMergeRequest);
298            $issue->getFromDB();
299        } catch (Exception $e) {
300            return [400, $e->getMessage()];
301        }
302        if (!$issue->isValid()) {
303            return [404, ''];
304        }
305
306        return [200, $issue->buildTooltipHTML()];
307    }
308
309    private function getAdditionalIssueData($pmServiceName, $projectKey, $issueId, $isMergeRequest)
310    {
311        try {
312            $issue = Issue::getInstance($pmServiceName, $projectKey, $issueId, $isMergeRequest);
313            $issue->getFromDB();
314        } catch (Exception $e) {
315            return [400, $e->getMessage()];
316        }
317        if (!$issue->isValid()) {
318            return [404, ''];
319        }
320
321        return [200, $issue->getAdditionalDataHTML()];
322    }
323}
324