1<?php
2
3use dokuwiki\Extension\ActionPlugin;
4use dokuwiki\Extension\Event;
5use dokuwiki\Extension\EventHandler;
6
7class action_plugin_validator extends ActionPlugin
8{
9    private const CAPTCHA_PROVIDERS = [
10        'cloudflare' => [
11            'challenge_script'  => 'https://challenges.cloudflare.com/turnstile/v0/api.js',
12            'verify_endpoint'   => 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
13            'sitekey_name'      => 'cf-sitekey',
14            'secretkey_name'    => 'cf-secretkey',
15            'class_name'        => 'cf-turnstile',
16            'response_name'     => 'cf-turnstile-response',
17            'function_name'     => 'getTurnstile',
18        ],
19        'google' => [
20            'challenge_script'  => 'https://www.google.com/recaptcha/api.js',
21            'verify_endpoint'   => 'https://www.google.com/recaptcha/api/siteverify',
22            'sitekey_name'      => 'g-sitekey',
23            'secretkey_name'    => 'g-secretkey',
24            'class_name'        => 'g-recaptcha',
25            'response_name'     => 'g-recaptcha-response',
26            'function_name'     => 'getCaptcha',
27        ],
28    ];
29
30    private const DEFAULT = 'cloudflare';
31
32    public function register(EventHandler $controller)
33    {
34        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'injectScript');
35        $controller->register_hook('FORM_REGISTER_OUTPUT', 'BEFORE', $this, 'handleFormOutput');
36        $controller->register_hook('FORM_LOGIN_OUTPUT', 'BEFORE', $this, 'handleFormOutput');
37        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handleRegister');
38        $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'handleLogin');
39    }
40
41    private function getMode(): string
42    {
43        return $this->getConf('mode') ?: self::DEFAULT;
44    }
45
46    private function getHTML() {
47        $mode = $this->getMode();
48        $sitekey = $this->getConf(self::CAPTCHA_PROVIDERS[$mode]['sitekey_name']);
49        $class = self::CAPTCHA_PROVIDERS[$mode]['class_name'];
50        return '<div style="margin-top: 10px; margin-bottom: 10px;" class="' . htmlspecialchars($class) . '" data-sitekey="' . htmlspecialchars($sitekey) . '"></div>';
51    }
52
53    public function injectScript(Event $event, $param)
54    {
55        global $ACT;
56
57        if ($ACT === 'register' || $ACT === 'login') {
58            $src = self::CAPTCHA_PROVIDERS[$this->getMode()]['challenge_script'];
59            $event->data['script'][] = [
60                'type'    => 'text/javascript',
61                'src'     => $src,
62                'async'   => 'async',
63                'defer'   => 'defer',
64                '_data'   => ''
65            ];
66        }
67    }
68
69    public function handleFormOutput(Event $event, $param)
70    {
71        if(!$html = $this->getHTML()) return;
72
73        $form = $event->data;
74
75        $pos = $form->findPositionByAttribute('type', 'submit');
76        $form->addHTML($html, $pos);
77    }
78
79    private function generateResponse(Event $event): ?array {
80        $mode = $this->getMode();
81        $secretkey = $this->getConf(self::CAPTCHA_PROVIDERS[$mode]['secretkey_name']);
82
83        global $INPUT;
84
85        $response = $INPUT->post->str(self::CAPTCHA_PROVIDERS[$mode]['response_name']);
86        if (empty($response)) return null;
87
88        $data = [
89            'secret' => $secretkey,
90            'response' => $response,
91        ];
92
93        $ip = $INPUT->server->str('REMOTE_ADDR');
94        if (!empty($ip)) $data['remoteip'] = $ip;
95
96        $function = self::CAPTCHA_PROVIDERS[$mode]['function_name'];
97        $helper = plugin_load('helper', 'validator');
98        $curl = $helper->$function(self::CAPTCHA_PROVIDERS[$mode]['verify_endpoint'], $data);
99
100        $responseBody = curl_exec($curl);
101
102        if (curl_errno($curl) || curl_getinfo($curl, CURLINFO_HTTP_CODE) !== 200) {
103            curl_close($curl);
104            return null;
105        }
106
107        curl_close($curl);
108
109        $outcome = json_decode($responseBody, true);
110        if (json_last_error() !== JSON_ERROR_NONE) return null;
111
112        return $outcome;
113    }
114
115    private function checkToken(Event $event)
116    {
117        $mode = $this->getMode();
118
119        $sitekey = $this->getConf(self::CAPTCHA_PROVIDERS[$mode]['sitekey_name']);
120        $secretkey = $this->getConf(self::CAPTCHA_PROVIDERS[$mode]['secretkey_name']);
121        if (empty($sitekey) || empty($secretkey)) return;
122
123        $response = $this->generateResponse($event);
124
125
126        if (!is_null($response) && isset($response['success']) && $response['success'] === true) {
127            return;
128        }
129
130        // add msg here
131
132        global $INPUT;
133        global $ACT;
134
135        switch ($ACT) {
136            case 'register':
137                $INPUT->post->set('save', false);
138                break;
139            case 'login':
140                $event->result = false;
141                $event->preventDefault();
142                $event->stopPropagation();
143                break;
144        }
145    }
146
147    public function handleRegister(Event $event, $param)
148    {
149        global $INPUT;
150        $act = act_clean($event->data);
151        if ($act !== 'register' || !$INPUT->bool('save')) return;
152
153        $this->checkToken($event);
154    }
155
156    public function handleLogin(Event $event, $param)
157    {
158        global $INPUT;
159        if (!$INPUT->bool('u')) return;
160
161        $this->checkToken($event);
162    }
163}