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}