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