1<?php 2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project 3// 4// All Rights Reserved. See copyright.txt for details and a complete list of authors. 5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. 6// $Id$ 7 8//this script may only be included - so its better to die if called directly. 9if (strpos($_SERVER['SCRIPT_NAME'], basename(__FILE__)) != false) { 10 header('location: index.php'); 11 exit; 12} 13 14/** 15 * A simple class to switch between Zend\Captcha\Image and 16 * Zend\Captcha\ReCaptcha based on admin preference 17 */ 18class Captcha 19{ 20 21 /** 22 * The type of the captch ('default' when using Zend\Captcha\Image 23 * or 'recaptcha' when using Zend\Captcha\ReCaptcha) 24 * 25 * @var string 26 */ 27 public $type = ''; 28 29 /** 30 * An instance of Zend\Captcha\Image or Zend\Captcha\ReCaptcha 31 * depending on the value of $this->type 32 * 33 * @var object 34 */ 35 public $captcha = ''; 36 37 /** 38 * Class constructor: decides whether to create an instance of 39 * Zend\Captcha\Image or Zend\Captcha\ReCaptcha or Captcha_Question 40 * 41 * @param string $type recaptcha|questions|default|dumb 42 */ 43 function __construct($type = '') 44 { 45 global $prefs; 46 47 if (empty($type)) { 48 if ($prefs['recaptcha_enabled'] == 'y' && ! empty($prefs['recaptcha_privkey']) && ! empty($prefs['recaptcha_pubkey'])) { 49 if ($prefs['recaptcha_version'] == '2') { 50 $type = 'recaptcha20'; 51 } elseif ( $prefs['recaptcha_version'] == '3') { 52 $type = 'recaptcha30'; 53 } else { 54 $type = 'recaptcha'; 55 } 56 } elseif ($prefs['captcha_questions_active'] == 'y' && ! empty($prefs['captcha_questions'])) { 57 $type = 'questions'; 58 } elseif (extension_loaded('gd') && function_exists('imagepng') && function_exists('imageftbbox')) { 59 $type = 'default'; 60 } else { 61 $type = 'dumb'; 62 } 63 } 64 65 if ($type === 'recaptcha') { 66 $this->captcha = new Zend\Captcha\ReCaptcha( 67 [ 68 'private_key' => $prefs['recaptcha_privkey'], 69 'public_key' => $prefs['recaptcha_pubkey'], 70 ] 71 ); 72 $httpClient = TikiLib::lib('tiki')->get_http_client(); 73 $this->captcha->getService()->setHttpClient($httpClient); 74 75 $this->captcha->getService()->setOption('theme', isset($prefs['recaptcha_theme']) ? $prefs['recaptcha_theme'] : 'clean'); 76 77 $this->captcha->setOption('ssl', true); 78 79 $this->type = $type; 80 81 $this->recaptchaCustomTranslations(); 82 } elseif (in_array($type, ['recaptcha20', 'recaptcha30'])) { 83 $params = [ 84 'privkey' => $prefs['recaptcha_privkey'], 85 'pubkey' => $prefs['recaptcha_pubkey'], 86 'theme' => isset($prefs['recaptcha_theme']) ? $prefs['recaptcha_theme'] : 'clean', 87 ]; 88 89 if ($type === 'recaptcha20') { 90 include_once('lib/captcha/Captcha_ReCaptcha20.php'); 91 $this->captcha = new Captcha_ReCaptcha20($params); 92 } else { 93 include_once('lib/captcha/Captcha_ReCaptcha30.php'); 94 $this->captcha = new Captcha_ReCaptcha30($params); 95 } 96 97 $httpClient = TikiLib::lib('tiki')->get_http_client(); 98 $this->captcha->getService()->setHttpClient($httpClient); 99 100 $this->captcha->setOption('ssl', true); 101 102 $this->type = $type; 103 104 $this->recaptchaCustomTranslations(); 105 } elseif ($type === 'default') { 106 $this->captcha = new Zend\Captcha\Image( 107 [ 108 'wordLen' => $prefs['captcha_wordLen'], 109 'timeout' => 600, 110 'font' => __DIR__ . '/DejaVuSansMono.ttf', 111 'imgdir' => 'temp/public/', 112 'suffix' => '.captcha.png', 113 'width' => $prefs['captcha_width'], 114 'dotNoiseLevel' => $prefs['captcha_noise'], 115 ] 116 ); 117 $this->type = 'default'; 118 } elseif ($type === 'questions') { 119 $this->type = 'questions'; 120 121 $questions = []; 122 $lines = explode("\n", $prefs['captcha_questions']); 123 124 foreach ($lines as $line) { 125 $line = explode(':', $line, 2); 126 if (count($line) === 2) { 127 $questions[] = [trim($line[0]), trim($line[1])]; 128 } 129 } 130 131 include_once('lib/captcha/Captcha_Questions.php'); 132 $this->captcha = new Captcha_Questions($questions); 133 } else { // implied $type==='dumb' 134 $this->captcha = new Zend\Captcha\Dumb; 135 $this->captcha->setWordlen($prefs['captcha_wordLen']); 136 $this->captcha->setLabel(tra('Please type this word backwards')); 137 $this->type = 'dumb'; 138 } 139 140 $this->setErrorMessages(); 141 } 142 143 /** 144 * Create the default captcha 145 * 146 * @return string 147 */ 148 function generate() 149 { 150 $key = ''; 151 try { 152 $key = $this->captcha->generate(); 153 if ($this->type == 'default' || $this->type == 'questions') { 154 // the following needed to keep session active for ajax checking 155 $session = $this->captcha->getSession(); 156 $session->setExpirationHops(2, null, true); 157 $this->captcha->setSession($session); 158 $this->captcha->setKeepSession(false); 159 } 160 } catch (Exception $e) { 161 Feedback::error($e->getMessage()); 162 } 163 return $key; 164 } 165 166 /** Return captcha ID 167 * 168 * @return string captcha ID 169 */ 170 function getId() 171 { 172 return $this->captcha->getId(); 173 } 174 175 /** 176 * HTML code for the captcha 177 * 178 * @return string 179 */ 180 function render() 181 { 182 $access = TikiLib::lib('access'); 183 if ($access->is_xml_http_request()) { 184 if (in_array($this->type, ['recaptcha20', 'recaptcha30'])) { 185 return $this->captcha->renderAjax(); 186 } elseif ($this->type == 'recaptcha') { 187 $params = json_encode($this->captcha->getService()->getOptions()); 188 $id = 1; 189 TikiLib::lib('header')->add_js(' 190Recaptcha.create("' . $this->captcha->getPubKey() . '", 191 "captcha' . $id . '",' . $params . ' 192 ); 193', 100); 194 return '<div id="captcha' . $id . '"></div>'; 195 } else { 196 return $this->captcha->render(); 197 } 198 } else { 199 if (in_array($this->type, ['recaptcha20', 'recaptcha30'])) { 200 return $this->captcha->render(); 201 } elseif ($this->captcha instanceof Zend\Captcha\ReCaptcha) { 202 return $this->captcha->getService()->getHtml(); 203 } elseif ($this->captcha instanceof Zend\Captcha\Dumb) { 204 return $this->captcha->getLabel() . ': <b>' 205 . strrev($this->captcha->getWord()) 206 . '</b>'; 207 } 208 return $this->captcha->render(); 209 } 210 } 211 212 /** 213 * Validate user input for the captcha 214 * 215 * @param array $input 216 * @return bool true or false 217 */ 218 function validate($input = null) 219 { 220 if (is_null($input)) { 221 $input = $_REQUEST; 222 } 223 if (in_array($this->type, ['recaptcha', 'recaptcha20', 'recaptcha30'])) { 224 // Temporary workaround of zend/http client uses arg_separator.output for making POST request body 225 // which fails with Google recaptcha services if used with '&' value 226 // should be fixed in zend/http (pull request submitted) 227 // or remove ini_get('arg_separator.output', '&') we have in tiki code tiki-setup_base.php:31 228 $oldVal = ini_get('arg_separator.output'); 229 ini_set('arg_separator.output', '&'); 230 $result = $this->captcha->isValid($input); 231 ini_set('arg_separator.output', $oldVal); 232 return $result; 233 } else { 234 return $this->captcha->isValid($input['captcha']); 235 } 236 } 237 238 /** 239 * Return the full path to the captcha image when using default captcha 240 * 241 * @return string full path to default captcha image 242 */ 243 function getPath() 244 { 245 try { 246 return $this->captcha->getImgDir() . $this->captcha->getId() . $this->captcha->getSuffix(); 247 } catch (Exception $e) { 248 Feedback::error($e->getMessage()); 249 } 250 } 251 252 /** 253 * Translate Zend\Captcha\Image, Zend\Captcha\Dumb and Zend\Captcha\ReCaptcha 254 * default error messages 255 * 256 * @return void 257 */ 258 function setErrorMessages() 259 { 260 $errors = [ 261 'missingValue' => tra('Empty CAPTCHA value'), 262 'badCaptcha' => tra('You have mistyped the anti-bot verification code. Please try again.') 263 ]; 264 265 if (in_array($this->type, ['recaptcha', 'recaptcha20', 'recaptcha30'])) { 266 $errors['errCaptcha'] = tra('Failed to validate CAPTCHA'); 267 } else { 268 $errors['missingID'] = tra('CAPTCHA ID field is missing'); 269 } 270 271 $this->captcha->setMessages($errors); 272 } 273 274 /** 275 * Convert the errors array into a string and return it 276 * 277 * @return string error messages 278 */ 279 function getErrors() 280 { 281 return implode('<br />', $this->captcha->getMessages()); 282 } 283 284 /** 285 * Custom translation for ReCaptcha interface 286 * 287 * @return void 288 */ 289 function recaptchaCustomTranslations() 290 { 291 $recaptchaService = $this->captcha->getService(); 292 $recaptchaService->setOption( 293 'custom_translations', 294 [ 295 'visual_challenge' => tra('Get a visual challenge'), 296 'audio_challenge' => tra('Get an audio challenge'), 297 'refresh_btn' => tra('Get a new challenge'), 298 'instructions_visual' => tra('Type the two words'), 299 'instructions_audio' => tra('Type what you hear'), 300 'help_btn' => tra('Help'), 301 'play_again' => tra('Play sound again'), 302 'cant_hear_this' => tra('Download audio as an MP3 file'), 303 'incorrect_try_again' => tra('Incorrect. Try again.') 304 ] 305 ); 306 } 307}