1: <?php
2:
3: use Xmf\Request;
4:
5: /**
6: * CAPTCHA configurations for Image mode
7: *
8: * Based on DuGris' SecurityImage
9: *
10: * You may not change or alter any portion of this comment or credits
11: * of supporting developers from this source code or any supporting source code
12: * which is considered copyrighted (c) material of the original comment or credit authors.
13: * This program is distributed in the hope that it will be useful,
14: * but WITHOUT ANY WARRANTY; without even the implied warranty of
15: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16: *
17: * @copyright (c) 2000-2021 XOOPS Project (https://xoops.org)
18: * @license GNU GPL 2 (https://www.gnu.org/licenses/gpl-2.0.html)
19: * @package class
20: * @subpackage CAPTCHA
21: * @since 2.3.0
22: * @author Taiwen Jiang <phppp@users.sourceforge.net>
23: */
24: defined('XOOPS_ROOT_PATH') || exit('Restricted access');
25:
26: /**
27: * Class XoopsCaptcha
28: */
29: class XoopsCaptcha
30: {
31: public $active;
32: public $handler;
33: public $path_basic;
34: public $path_config;
35: public $path_plugin;
36: public $name;
37: public $config = array();
38: public $message = array(); // Logging error messages
39:
40: /**
41: * construct
42: */
43: protected function __construct()
44: {
45: xoops_loadLanguage('captcha');
46: // Load static configurations
47: $this->path_basic = XOOPS_ROOT_PATH . '/class/captcha';
48: $this->path_config = XOOPS_VAR_PATH . '/configs/captcha';
49: $this->path_plugin = XOOPS_ROOT_PATH . '/Frameworks/captcha';
50: $this->config = $this->loadConfig();
51: $this->name = $this->config['name'];
52: }
53:
54: /**
55: * Get Instance
56: *
57: * @return XoopsCaptcha Instance
58: */
59: public static function getInstance()
60: {
61: static $instance;
62: if (null === $instance) {
63: $instance = new static();
64: }
65:
66: return $instance;
67: }
68:
69: /**
70: * XoopsCaptcha::loadConfig()
71: *
72: * @param string|null $methodname the captcha method, i.e. text, image, recaptcha2, etc
73: *
74: * @return array
75: */
76: public function loadConfig($methodname = null)
77: {
78: $basic_config = array();
79: $plugin_config = array();
80: $filename = empty($methodname) ? 'config.php' : 'config.' . $methodname . '.php';
81: $distfilename = empty($methodname) ? 'config.dist.php' : 'config.' . $methodname . '.dist.php';
82: if (file_exists($file = $this->path_config . '/' . $filename)) {
83: $basic_config = include $file;
84: } elseif (file_exists($distfile = $this->path_basic . '/' . $distfilename)) {
85: $basic_config = include $distfile;
86: if (false===copy($distfile, $file)) {
87: trigger_error('Could not create captcha config file ' . $filename);
88: }
89: }
90: // use of the path_plugin
91: if (file_exists($file = $this->path_plugin . '/' . $filename)) {
92: $plugin_config = include $file;
93: }
94:
95: $config = array_merge($basic_config, $plugin_config);
96: foreach ($config as $key => $val) {
97: $this->config[$key] = $val;
98: }
99:
100: return $config;
101: }
102:
103: /**
104: * XoopsCaptcha::isActive()
105: *
106: * @return bool
107: */
108: public function isActive()
109: {
110: if (null !== $this->active) {
111: return $this->active;
112: }
113: if (!empty($this->config['disabled'])) {
114: $this->active = false;
115:
116: return $this->active;
117: }
118: if (!empty($this->config['skipmember']) && is_object($GLOBALS['xoopsUser'])) {
119: $this->active = false;
120:
121: return $this->active;
122: }
123: if (null === $this->handler) {
124: $this->loadHandler();
125: }
126: $this->active = isset($this->handler);
127:
128: return $this->active;
129: }
130:
131: /**
132: * XoopsCaptcha::loadHandler()
133: *
134: * @param mixed $name
135: * @return
136: */
137: public function loadHandler($name = null)
138: {
139: $name = !empty($name) ? $name : (empty($this->config['mode']) ? 'text' : $this->config['mode']);
140: $class = 'XoopsCaptcha' . ucfirst($name);
141: if (!empty($this->handler) && get_class($this->handler) == $class) {
142: return $this->handler;
143: }
144: $this->handler = null;
145: if (file_exists($file = $this->path_basic . '/' . $name . '.php')) {
146: require_once $file;
147: } else {
148: if (file_exists($file = $this->path_plugin . '/' . $name . '.php')) {
149: require_once $file;
150: }
151: }
152:
153: if (!class_exists($class)) {
154: $class = 'XoopsCaptchaText';
155: require_once $this->path_basic . '/text.php';
156: }
157: $handler = new $class($this);
158: if ($handler->isActive()) {
159: $this->handler = $handler;
160: $this->handler->loadConfig($name);
161: }
162:
163: return $this->handler;
164: }
165:
166: /**
167: * XoopsCaptcha::setConfigs()
168: *
169: * @param mixed $configs
170: * @return bool
171: */
172: public function setConfigs($configs)
173: {
174: foreach ($configs as $key => $val) {
175: $this->setConfig($key, $val);
176: }
177:
178: return true;
179: }
180:
181: /**
182: * XoopsCaptcha::setConfig()
183: *
184: * @param mixed $name
185: * @param mixed $val
186: * @return bool
187: */
188: public function setConfig($name, $val)
189: {
190: if (isset($this->$name)) {
191: $this->$name = $val;
192: } else {
193: $this->config[$name] = $val;
194: }
195:
196: return true;
197: }
198:
199: /**
200: * Verify user submission
201: */
202: /**
203: * XoopsCaptcha::verify()
204: *
205: * @param mixed $skipMember
206: * @param mixed $name
207: * @return bool
208: */
209: public function verify($skipMember = null, $name = null)
210: {
211: $sessionName = empty($name) ? $this->name : $name;
212: $skipMember = ($skipMember === null) && isset($_SESSION["{$sessionName}_skipmember"]) ? $_SESSION["{$sessionName}_skipmember"] : $skipMember;
213: $maxAttempts = isset($_SESSION["{$sessionName}_maxattempts"]) ? $_SESSION["{$sessionName}_maxattempts"] : $this->config['maxattempts'];
214: $attempt = isset($_SESSION["{$sessionName}_attempt"]) ? $_SESSION["{$sessionName}_attempt"] : 0;
215: $is_valid = false;
216: // Skip CAPTCHA verification if disabled
217: if (!$this->isActive()) {
218: $is_valid = true;
219: // Skip CAPTCHA for member if set
220: } elseif (!empty($skipMember) && is_object($GLOBALS['xoopsUser'])) {
221: $is_valid = true;
222: // Kill too many attempts
223: } elseif (!empty($maxAttempts) && $attempt > $maxAttempts) {
224: $this->message[] = _CAPTCHA_TOOMANYATTEMPTS;
225: // Verify the code
226: } else {
227: $is_valid = $this->handler->verify($sessionName);
228: $xoopsPreload = XoopsPreload::getInstance();
229: $xoopsPreload->triggerEvent('core.behavior.captcha.result', $is_valid);
230: }
231:
232: if (!$is_valid) {
233: // Increase the attempt records on failure
234: $_SESSION["{$sessionName}_attempt"]++;
235: // Log the error message
236: $this->message[] = _CAPTCHA_INVALID_CODE;
237: } else {
238: // reset attempt records on success
239: $_SESSION["{$sessionName}_attempt"] = null;
240: }
241: $this->destroyGarbage(true);
242:
243: return $is_valid;
244: }
245:
246: /**
247: * XoopsCaptcha::getCaption()
248: *
249: * @return mixed|string
250: */
251: public function getCaption()
252: {
253: return defined('_CAPTCHA_CAPTION') ? constant('_CAPTCHA_CAPTION') : '';
254: }
255:
256: /**
257: * XoopsCaptcha::getMessage()
258: *
259: * @return string
260: */
261: public function getMessage()
262: {
263: return implode('<br>', $this->message);
264: }
265:
266: /**
267: * Destroy historical stuff
268: * @param bool $clearSession
269: * @return bool
270: */
271: public function destroyGarbage($clearSession = false)
272: {
273: $this->loadHandler();
274: if (is_callable($this->handler, 'destroyGarbage')) {
275: $this->handler->destroyGarbage();
276: }
277: if ($clearSession) {
278: $_SESSION[$this->name . '_name'] = null;
279: $_SESSION[$this->name . '_skipmember'] = null;
280: $_SESSION[$this->name . '_code'] = null;
281: $_SESSION[$this->name . '_maxattempts'] = null;
282: }
283:
284: return true;
285: }
286:
287: /**
288: * XoopsCaptcha::render()
289: *
290: * @return string
291: */
292: public function render()
293: {
294: $_SESSION[$this->name . '_name'] = $this->name;
295: $_SESSION[$this->name . '_skipmember'] = $this->config['skipmember'];
296: $form = '';
297: if (!$this->active || empty($this->config['name'])) {
298: return $form;
299: }
300:
301: $maxAttempts = $this->config['maxattempts'];
302: $_SESSION[$this->name . '_maxattempts'] = $maxAttempts;
303: $attempt = isset($_SESSION[$this->name . '_attempt']) ? $_SESSION[$this->name . '_attempt'] : 0;
304: $_SESSION[$this->name . '_attempt'] = $attempt;
305:
306: // Failure on too many attempts
307: if (!empty($maxAttempts) && $attempt > $maxAttempts) {
308: $form = _CAPTCHA_TOOMANYATTEMPTS;
309: // Load the form element
310: } else {
311: $form = $this->loadForm();
312: }
313:
314: return $form;
315: }
316:
317: /**
318: * XoopsCaptcha::renderValidationJS()
319: *
320: * @return string
321: */
322: public function renderValidationJS()
323: {
324: if (!$this->active || empty($this->config['name'])) {
325: return '';
326: }
327:
328: return $this->handler->renderValidationJS();
329: }
330:
331: /**
332: * XoopsCaptcha::setCode()
333: *
334: * @param mixed $code
335: * @return bool
336: */
337: public function setCode($code = null)
338: {
339: $code = ($code === null) ? $this->handler->getCode() : $code;
340: if (!empty($code)) {
341: $_SESSION[$this->name . '_code'] = $code;
342:
343: return true;
344: }
345:
346: return false;
347: }
348:
349: /**
350: * XoopsCaptcha::loadForm()
351: *
352: * @return
353: */
354: public function loadForm()
355: {
356: $form = $this->handler->render();
357: $this->setCode();
358:
359: return $form;
360: }
361: }
362:
363: /**
364: * Abstract class for CAPTCHA method
365: *
366: * Currently, there are two types of CAPTCHA forms, text and image
367: * The default mode is "text", it can be changed in the priority:
368: * 1 If mode is set through XoopsFormCaptcha::setConfig("mode", $mode), take it
369: * 2 Elseif mode is set though captcha/config.php, take it
370: * 3 Else, take "text"
371: */
372: class XoopsCaptchaMethod
373: {
374: public $handler;
375: public $config;
376: public $code;
377:
378: /**
379: * XoopsCaptchaMethod::__construct()
380: *
381: * @param mixed $handler
382: */
383: public function __construct($handler = null)
384: {
385: $this->handler = $handler;
386: }
387:
388: /**
389: * XoopsCaptchaMethod::isActive()
390: *
391: * @return bool
392: */
393: public function isActive()
394: {
395: return true;
396: }
397:
398: /**
399: * XoopsCaptchaMethod::loadConfig()
400: *
401: * @param string $name
402: * @return void
403: */
404: public function loadConfig($name = '')
405: {
406: $this->config = empty($name) ? $this->handler->config : array_merge($this->handler->config, $this->handler->loadConfig($name));
407: }
408:
409: /**
410: * XoopsCaptchaMethod::getCode()
411: *
412: * @return string
413: */
414: public function getCode()
415: {
416: return (string)$this->code;
417: }
418:
419: /**
420: * XoopsCaptchaMethod::render()
421: *
422: * @return void
423: */
424: public function render()
425: {
426: }
427:
428: /**
429: * @return string
430: */
431: public function renderValidationJS()
432: {
433: return '';
434: }
435:
436: /**
437: * XoopsCaptchaMethod::verify()
438: *
439: * @param mixed $sessionName
440: * @return bool
441: */
442: public function verify($sessionName = null)
443: {
444: $is_valid = false;
445: if (!empty($_SESSION["{$sessionName}_code"])) {
446: $func = !empty($this->config['casesensitive']) ? 'strcmp' : 'strcasecmp';
447: $is_valid = !$func(trim(Request::getString($sessionName, '', 'POST')), $_SESSION["{$sessionName}_code"]);
448: }
449:
450: return $is_valid;
451: }
452: }
453: