1<?php
2
3use dokuwiki\Extension\ActionPlugin;
4use dokuwiki\Extension\Event;
5use dokuwiki\Extension\EventHandler;
6
7class action_plugin_turnstile extends ActionPlugin
8{
9    private const TURNSTILE_CHALLENGE = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
10    private const TURNSTILE_SITEVERIFY = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
11
12    public function register(EventHandler $controller)
13    {
14        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'injectScript');
15
16        $controller->register_hook('FORM_REGISTER_OUTPUT', 'BEFORE', $this, 'handleFormOutput', []);
17
18        $controller->register_hook('FORM_LOGIN_OUTPUT', 'BEFORE', $this, 'handleFormOutput', []);
19
20        $controller->register_hook('AUTH_USER_CHANGE', 'BEFORE', $this, 'handleRegister', []);
21
22        $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'handleLogin', []);
23    }
24
25    private function getHTML() {
26        $siteKey = $this->getConf('sitekey');
27
28        $html = '<div style="margin-top: 10px; margin-bottom: 10px;">';
29        $html .= '<div class="cf-turnstile" data-sitekey="' . htmlspecialchars($siteKey) . '"></div>';
30        $html .= '</div>';
31        return $html;
32    }
33
34    public function injectScript(Event $event, $param)
35    {
36        global $ACT;
37
38        if ($ACT === 'register' || $ACT === 'login') {
39            $event->data['script'][] = [
40                'type'    => 'text/javascript',
41                'src'     => self::TURNSTILE_CHALLENGE,
42                'async'   => 'async',
43                'defer'   => 'defer',
44                '_data'   => ''
45            ];
46        }
47    }
48
49    public function handleFormOutput(Event $event, $param)
50    {
51        $form = $event->data;
52
53        $html = $this->getHTML();
54        if (empty($html)) return;
55
56        $pos = $form->findPositionByAttribute('type', 'submit');
57        if(!$pos) return;
58
59        $form->addHTML($html, $pos);
60    }
61
62    private function generateResponse(Event $event): ?array {
63        $private = $this->getConf('secretkey');
64        if (empty($private)) return null;
65
66        global $INPUT;
67
68        $ts_response = $INPUT->post->str('cf-turnstile-response');
69        if (empty($ts_response)) return null;
70
71        $ip = $INPUT->server->str('REMOTE_ADDR');
72
73        $data = [
74            'secret' => $private,
75            'response' => $ts_response,
76            'remoteip' => $ip
77        ];
78
79        $options = [
80            'http' => [
81                'header' => "Content-type: application/json\r\n",
82                'method' => 'POST',
83                'content' => json_encode($data)
84            ]
85        ];
86
87        $context = stream_context_create($options);
88        $result = @file_get_contents(self::TURNSTILE_SITEVERIFY, false, $context);
89
90        if ($result === false) return null;
91
92        $outcome = json_decode($result, true);
93        if (json_last_error() !== JSON_ERROR_NONE) return null;
94
95        return $outcome;
96    }
97
98    private function checkToken(Event $event) {
99
100        $public = $this->getConf('sitekey');
101        $private = $this->getConf('secretkey');
102        if (empty($public) || empty($private)) return;
103
104        $response = $this->generateResponse($event);
105
106        if (!is_null($response) && isset($response['success']) && $response['success'] === true) {
107            return;
108        }
109
110        // add msg here
111
112        global $INPUT;
113        global $ACT;
114
115        switch($ACT) {
116            case 'register':
117                $INPUT->post->set('save', false);
118                break;
119            case 'login':
120                $event->result = false;
121                $event->preventDefault();
122                $event->stopPropagation();
123                break;
124        }
125    }
126
127    public function handleRegister(Event $event, $param)
128    {
129        if ($event->data['type'] !== 'create') {
130            return;
131        }
132
133        $this->checkToken($event);
134    }
135
136    public function handleLogin(Event $event, $param)
137    {
138        global $INPUT;
139        if (!$INPUT->bool('u')) return;
140
141        $this->checkToken($event);
142    }
143}
144