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