Completed
Push — patch_1-1-4 ( 3f780f...826343 )
by Emanuele
25:17 queued 11:40
created

Verification_Controls_Captcha   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 248
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 39.53%

Importance

Changes 0
Metric Value
dl 0
loc 248
rs 9.52
c 0
b 0
f 0
ccs 34
cts 86
cp 0.3953
wmc 36
lcom 1
cbo 1

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 2
B showVerification() 0 31 11
A createTest() 0 18 6
A prepareContext() 0 13 2
A doTest() 0 9 2
A hasVisibleTemplate() 0 4 1
B settings() 0 50 8
A _verifyCode() 0 4 4
1
<?php
2
3
/**
4
 * This file contains those functions specific to the various verification controls
5
 * used to challenge users, and hopefully robots as well.
6
 *
7
 * @name      ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
10
 *
11
 * This file contains code covered by:
12
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
13
 * license:  	BSD, See included LICENSE.TXT for terms and conditions.
14
 *
15
 * @version 1.1
16
 *
17
 */
18
19
/**
20
 * Simple function that loads and returns all the verification controls known to Elk
21
 */
22
function loadVerificationControls()
23
{
24
	$known_verifications = array(
25 1
		'captcha',
26 1
		'questions',
27
		'emptyfield'
28 1
	);
29
30
	// Let integration add some more controls
31 1
	call_integration_hook('integrate_control_verification', array(&$known_verifications));
32
33 1
	return $known_verifications;
34
}
35
36
/**
37
 * Create a anti-bot verification control?
38
 *
39
 * @param mixed[] $verificationOptions
40
 * @param bool    $do_test = false If we are validating the input to a verification control
41
 *
42
 * @return array|bool
43
 * @throws Elk_Exception no_access
44
 */
45
function create_control_verification(&$verificationOptions, $do_test = false)
46
{
47
	global $context;
48
49
	// We need to remember this because when failing the page is reloaded and the
50
	// code must remain the same (unless it has to change)
51
	static $all_instances = array();
52
53
	// Always have an ID.
54
	assert(isset($verificationOptions['id']));
55
	$isNew = !isset($context['controls']['verification'][$verificationOptions['id']]);
56
57
	if ($isNew)
58
	{
59
		$context['controls']['verification'][$verificationOptions['id']] = array(
60
			'id' => $verificationOptions['id'],
61
			'max_errors' => isset($verificationOptions['max_errors']) ? $verificationOptions['max_errors'] : 3,
62
			'render' => false,
63
		);
64
	}
65
	$thisVerification = &$context['controls']['verification'][$verificationOptions['id']];
66
67
	if (!isset($_SESSION[$verificationOptions['id'] . '_vv']))
68
		$_SESSION[$verificationOptions['id'] . '_vv'] = array();
69
70
	$force_refresh = ((!empty($_SESSION[$verificationOptions['id'] . '_vv']['did_pass']) || empty($_SESSION[$verificationOptions['id'] . '_vv']['count']) || $_SESSION[$verificationOptions['id'] . '_vv']['count'] > 3) && empty($verificationOptions['dont_refresh']));
71
	if (!isset($all_instances[$verificationOptions['id']]))
72
	{
73
		$known_verifications = loadVerificationControls();
74
		$all_instances[$verificationOptions['id']] = array();
75
76
		foreach ($known_verifications as $verification)
77
		{
78
			$class_name = 'Verification_Controls_' . ucfirst($verification);
79
			$current_instance = new $class_name($verificationOptions);
80
81
			// If there is anything to show, otherwise forget it
82
			if ($current_instance->showVerification($isNew, $force_refresh))
83
				$all_instances[$verificationOptions['id']][$verification] = $current_instance;
84
		}
85
	}
86
87
	$instances = &$all_instances[$verificationOptions['id']];
88
89
	// Is there actually going to be anything?
90
	if (empty($instances))
91
		return false;
92
	elseif (!$isNew && !$do_test)
93
		return true;
94
95
	$verification_errors = ElkArte\Errors\ErrorContext::context($verificationOptions['id']);
96
	$increase_error_count = false;
97
98
	// Start with any testing.
99
	if ($do_test)
100
	{
101
		// This cannot happen!
102
		if (!isset($_SESSION[$verificationOptions['id'] . '_vv']['count']))
103
			throw new Elk_Exception('no_access', false);
104
105
		foreach ($instances as $instance)
106
		{
107
			$outcome = $instance->doTest();
108
			if ($outcome !== true)
109
			{
110
				$increase_error_count = true;
111
				$verification_errors->addError($outcome);
112
			}
113
		}
114
	}
115
116
	// Any errors means we refresh potentially.
117
	if ($increase_error_count)
118
	{
119 View Code Duplication
		if (empty($_SESSION[$verificationOptions['id'] . '_vv']['errors']))
120
			$_SESSION[$verificationOptions['id'] . '_vv']['errors'] = 0;
121
		// Too many errors?
122
		elseif ($_SESSION[$verificationOptions['id'] . '_vv']['errors'] > $thisVerification['max_errors'])
123
			$force_refresh = true;
124
125
		// Keep a track of these.
126
		$_SESSION[$verificationOptions['id'] . '_vv']['errors']++;
127
	}
128
129
	// Are we refreshing then?
130 View Code Duplication
	if ($force_refresh)
131
	{
132
		// Assume nothing went before.
133
		$_SESSION[$verificationOptions['id'] . '_vv']['count'] = 0;
134
		$_SESSION[$verificationOptions['id'] . '_vv']['errors'] = 0;
135
		$_SESSION[$verificationOptions['id'] . '_vv']['did_pass'] = false;
136
	}
137
138
	foreach ($instances as $test => $instance)
139
	{
140
		$instance->createTest($force_refresh);
141
		$thisVerification['test'][$test] = $instance->prepareContext();
142
		if ($instance->hasVisibleTemplate())
143
			$thisVerification['render'] = true;
144
	}
145
146
	$_SESSION[$verificationOptions['id'] . '_vv']['count'] = empty($_SESSION[$verificationOptions['id'] . '_vv']['count']) ? 1 : $_SESSION[$verificationOptions['id'] . '_vv']['count'] + 1;
147
148
	// Return errors if we have them.
149
	if ($verification_errors->hasErrors())
150
	{
151
		// @todo temporary until the error class is implemented in register
152
		$error_codes = array();
153
		foreach ($verification_errors->getErrors() as $errors)
154
			foreach ($errors as $error)
155
				$error_codes[] = $error;
156
157
		return $error_codes;
158
	}
159
	// If we had a test that one, make a note.
160
	elseif ($do_test)
161
		$_SESSION[$verificationOptions['id'] . '_vv']['did_pass'] = true;
162
163
	// Say that everything went well chaps.
164
	return true;
165
}
166
167
/**
168
 * A simple interface that defines all the methods any "Control_Verification"
169
 * class MUST have because they are used in the process of creating the verification
170
 */
171
interface Verification_Controls
172
{
173
	/**
174
	 * Used to build the control and return if it should be shown or not
175
	 *
176
	 * @param boolean $isNew
177
	 * @param boolean $force_refresh
178
	 *
179
	 * @return boolean
180
	 */
181
	public function showVerification($isNew, $force_refresh = true);
182
183
	/**
184
	 * Create the actual test that will be used
185
	 *
186
	 * @param boolean $refresh
187
	 *
188
	 * @return void
189
	 */
190
	public function createTest($refresh = true);
191
192
	/**
193
	 * Prepare the context for use in the template
194
	 *
195
	 * @return void
196
	 */
197
	public function prepareContext();
198
199
	/**
200
	 * Run the test, return if it passed or not
201
	 *
202
	 * @return string|boolean
203
	 */
204
	public function doTest();
205
206
	/**
207
	 * If the control has a visible location on the template or if its hidden
208
	 *
209
	 * @return boolean
210
	 */
211
	public function hasVisibleTemplate();
212
213
	/**
214
	 * Handles the ACP for the control
215
	 *
216
	 * @return void
217
	 */
218
	public function settings();
219
}
220
221
/**
222
 * Class to manage, create, show and validate captcha images
223
 */
224
class Verification_Controls_Captcha implements Verification_Controls
225
{
226
	/**
227
	 * Holds the $verificationOptions passed to the constructor
228
	 *
229
	 * @var array
230
	 */
231
	private $_options = null;
232
233
	/**
234
	 * If we are actually displaying the captcha image
235
	 *
236
	 * @var boolean
237
	 */
238
	private $_show_captcha = false;
239
240
	/**
241
	 * The string of text that will be used in the image and verification
242
	 *
243
	 * @var string
244
	 */
245
	private $_text_value = null;
246
247
	/**
248
	 * The number of characters to generate
249
	 *
250
	 * @var int
251
	 */
252
	private $_num_chars = null;
253
254
	/**
255
	 * The url to the created image
256
	 *
257
	 * @var string
258
	 */
259
	private $_image_href = null;
260
261
	/**
262
	 * If the response has been tested or not
263
	 *
264
	 * @var boolean
265
	 */
266
	private $_tested = false;
267
268
	/**
269
	 * If the GD library is available for use
270
	 *
271
	 * @var boolean
272
	 */
273
	private $_use_graphic_library = false;
274
275
	/**
276
	 * array of allowable characters that can be used in the image
277
	 *
278
	 * @var array
279
	 */
280
	private $_standard_captcha_range = array();
281
282
	/**
283
	 * Get things started,
284
	 * set the letters we will use to avoid confusion
285
	 * set graphics capability
286
	 *
287
	 * @param mixed[]|null $verificationOptions override_range, override_visual, id
288
	 */
289 1
	public function __construct($verificationOptions = null)
290
	{
291 1
		global $modSettings;
292
293 1
		$this->_use_graphic_library = in_array('gd', get_loaded_extensions());
294 1
		$this->_num_chars = $modSettings['visual_verification_num_chars'];
295
296
		// Skip I, J, L, O, Q, S and Z.
297 1
		$this->_standard_captcha_range = array_merge(range('A', 'H'), array('K', 'M', 'N', 'P', 'R'), range('T', 'Y'));
298
299 1
		if (!empty($verificationOptions))
300 1
			$this->_options = $verificationOptions;
301 1
	}
302
303
	/**
304
	 * Show a verification captcha
305
	 *
306
	 * @param boolean $isNew
307
	 * @param boolean $force_refresh
308
	 * @throws Elk_Exception
309
	 */
310
	public function showVerification($isNew, $force_refresh = true)
311
	{
312
		global $context, $modSettings, $scripturl;
313
314
		// Some javascript ma'am? (But load it only once)
315
		if (!empty($this->_options['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($this->_options['override_visual'])) && empty($context['captcha_js_loaded']))
316
		{
317
			loadTemplate('VerificationControls');
318
			loadJavascriptFile('jquery.captcha.js');
319
			$context['captcha_js_loaded'] = true;
320
		}
321
322
		$this->_tested = false;
323
324
		// Requesting a new challenge, build the image link, seed the JS
325
		if ($isNew)
326
		{
327
			$this->_show_captcha = !empty($this->_options['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($this->_options['override_visual']));
328
329
			if ($this->_show_captcha)
330
			{
331
				$this->_text_value = '';
332
				$this->_image_href = $scripturl . '?action=register;sa=verificationcode;vid=' . $this->_options['id'] . ';rand=' . md5(mt_rand());
333
			}
334
		}
335
336
		if ($isNew || $force_refresh)
337
			$this->createTest($force_refresh);
338
339
		return $this->_show_captcha;
340
	}
341
342
	/**
343
	 * Build the string that will be used to build the captcha
344
	 *
345
	 * @param boolean $refresh
346
	 */
347
	public function createTest($refresh = true)
348
	{
349
		if (!$this->_show_captcha)
350
			return;
351
352
		if ($refresh)
353
		{
354
			$_SESSION[$this->_options['id'] . '_vv']['code'] = '';
355
356
			// Are we overriding the range?
357
			$character_range = !empty($this->_options['override_range']) ? $this->_options['override_range'] : $this->_standard_captcha_range;
358
359
			for ($i = 0; $i < $this->_num_chars; $i++)
360
				$_SESSION[$this->_options['id'] . '_vv']['code'] .= $character_range[array_rand($character_range)];
361
		}
362
		else
363
			$this->_text_value = !empty($_REQUEST[$this->_options['id'] . '_vv']['code']) ? Util::htmlspecialchars($_REQUEST[$this->_options['id'] . '_vv']['code']) : '';
364
	}
365
366
	/**
367
	 * Prepare the captcha for the template
368
	 */
369
	public function prepareContext()
370
	{
371
		return array(
372
			'template' => 'captcha',
373
			'values' => array(
374
				'image_href' => $this->_image_href,
375
				'text_value' => $this->_text_value,
376
				'use_graphic_library' => $this->_use_graphic_library,
377
				'chars_number' => $this->_num_chars,
378
				'is_error' => $this->_tested && !$this->_verifyCode(),
379
			)
380
		);
381
	}
382
383
	/**
384
	 * Perform the test, make people do it again and robots pass :P
385
	 * @return string|boolean
386
	 */
387
	public function doTest()
388
	{
389
		$this->_tested = true;
390
391
		if (!$this->_verifyCode())
392
			return 'wrong_verification_code';
393
394
		return true;
395
	}
396
397
	/**
398
	 * Required by the interface, returns true for Captcha display
399
	 *
400
	 * @return bool
401
	 */
402
	public function hasVisibleTemplate()
403
	{
404
		return true;
405
	}
406
407
	/**
408
	 * Configuration settings for the admin template
409
	 *
410
	 * @return string
411
	 */
412 1
	public function settings()
413
	{
414 1
		global $txt, $scripturl, $modSettings;
415
416
		// Generate a sample registration image.
417 1
		$verification_image = $scripturl . '?action=register;sa=verificationcode;rand=' . md5(mt_rand());
418
419
		// Visual verification.
420
		$config_vars = array(
421 1
			array('title', 'configure_verification_means'),
422 1
			array('desc', 'configure_verification_means_desc'),
423 1
			array('int', 'visual_verification_num_chars'),
424 1
			'vv' => array('select', 'visual_verification_type',
425 1
				array($txt['setting_image_verification_off'], $txt['setting_image_verification_vsimple'], $txt['setting_image_verification_simple'], $txt['setting_image_verification_medium'], $txt['setting_image_verification_high'], $txt['setting_image_verification_extreme']),
426 1
				'subtext' => $txt['setting_visual_verification_type_desc']),
427 1
		);
428
429
		// Save it
430 1
		if (isset($_GET['save']))
431 1
		{
432
			if (isset($_POST['visual_verification_num_chars']) && $_POST['visual_verification_num_chars'] < 6)
433
				$_POST['visual_verification_num_chars'] = 5;
434
		}
435
436 1
		$_SESSION['visual_verification_code'] = '';
437 1
		for ($i = 0; $i < $this->_num_chars; $i++)
438 1
			$_SESSION['visual_verification_code'] .= $this->_standard_captcha_range[array_rand($this->_standard_captcha_range)];
439
440
		// Some javascript for CAPTCHA.
441 1
		if ($this->_use_graphic_library)
442 1
		{
443 1
			loadJavascriptFile('jquery.captcha.js');
444 1
			addInlineJavascript('
445
		$(\'#visual_verification_type\').Elk_Captcha({
446 1
			\'imageURL\': ' . JavaScriptEscape($verification_image) . ',
447
			\'useLibrary\': true,
448 1
			\'letterCount\': ' . $this->_num_chars . ',
449
			\'refreshevent\': \'change\',
450
			\'admin\': true
451 1
		});', true);
452 1
		}
453
454
		// Show the image itself, or text saying we can't.
455 1
		if ($this->_use_graphic_library)
456 1
			$config_vars['vv']['postinput'] = '<br /><img src="' . $verification_image . ';type=' . (empty($modSettings['visual_verification_type']) ? 0 : $modSettings['visual_verification_type']) . '" alt="' . $txt['setting_image_verification_sample'] . '" id="verification_image" /><br />';
457
		else
458
			$config_vars['vv']['postinput'] = '<br /><span class="smalltext">' . $txt['setting_image_verification_nogd'] . '</span>';
459
460 1
		return $config_vars;
461
	}
462
463
	/**
464
	 * Does what they typed = what was supplied in the image
465
	 * @return boolean
466
	 */
467
	private function _verifyCode()
468
	{
469
		return !$this->_show_captcha || (!empty($_REQUEST[$this->_options['id'] . '_vv']['code']) && !empty($_SESSION[$this->_options['id'] . '_vv']['code']) && strtoupper($_REQUEST[$this->_options['id'] . '_vv']['code']) === $_SESSION[$this->_options['id'] . '_vv']['code']);
470
	}
471
}
472
473
/**
474
 * Class to manage, prepare, show, and validate question -> answer verifications
475
 */
476
class Verification_Controls_Questions implements Verification_Controls
477
{
478
	/**
479
	 * Holds any options passed to the class
480
	 *
481
	 * @var array
482
	 */
483
	private $_options = null;
484
485
	/**
486
	 * array holding all of the available question id
487
	 * @var int[]
488
	 */
489
	private $_questionIDs = null;
490
491
	/**
492
	 * Number of challenge questions to use
493
	 *
494
	 * @var int
495
	 */
496
	private $_number_questions = null;
497
498
	/**
499
	 * Language the question is in
500
	 *
501
	 * @var string
502
	 */
503
	private $_questions_language = null;
504
505
	/**
506
	 * Questions that can be used given what available (try's to account for languages)
507
	 *
508
	 * @var int[]
509
	 */
510
	private $_possible_questions = null;
511
512
	/**
513
	 * Array of question id's that they provided a wrong answer to
514
	 *
515
	 * @var int[]
516
	 */
517
	private $_incorrectQuestions = null;
518
519
	/**
520
	 * On your mark
521
	 *
522
	 * @param mixed[]|null $verificationOptions override_qs,
523
	 */
524 1
	public function __construct($verificationOptions = null)
525
	{
526 1
		if (!empty($verificationOptions))
527 1
			$this->_options = $verificationOptions;
528 1
	}
529
530
	/**
531
	 * Show the question to the user
532
	 * Try's to account for languages
533
	 *
534
	 * @param boolean $isNew
535
	 * @param boolean $force_refresh
536
	 *
537
	 * @return boolean
538
	 */
539
	public function showVerification($isNew, $force_refresh = true)
540
	{
541
		global $modSettings, $user_info, $language;
542
543
		if ($isNew)
544
		{
545
			$this->_number_questions = isset($this->_options['override_qs']) ? $this->_options['override_qs'] : (!empty($modSettings['qa_verification_number']) ? $modSettings['qa_verification_number'] : 0);
546
547
			// If we want questions do we have a cache of all the IDs?
548
			if (!empty($this->_number_questions) && empty($modSettings['question_id_cache']))
549
				$this->_refreshQuestionsCache();
550
551
			// Let's deal with languages
552
			// First thing we need to know what language the user wants and if there is at least one question
553
			$this->_questions_language = !empty($_SESSION[$this->_options['id'] . '_vv']['language']) ? $_SESSION[$this->_options['id'] . '_vv']['language'] : (!empty($user_info['language']) ? $user_info['language'] : $language);
554
555
			// No questions in the selected language?
556
			if (empty($modSettings['question_id_cache'][$this->_questions_language]))
557
			{
558
				// Not even in the forum default? What the heck are you doing?!
559
				if (empty($modSettings['question_id_cache'][$language]))
560
					$this->_number_questions = 0;
561
				// Fall back to the default
562
				else
563
					$this->_questions_language = $language;
564
			}
565
566
			// Do we have enough questions?
567
			if (!empty($this->_number_questions) && $this->_number_questions <= count($modSettings['question_id_cache'][$this->_questions_language]))
568
			{
569
				$this->_possible_questions = $modSettings['question_id_cache'][$this->_questions_language];
570
				$this->_number_questions = min($this->_number_questions, count($this->_possible_questions));
571
				$this->_questionIDs = array();
572
573
				if ($isNew || $force_refresh)
574
					$this->createTest($force_refresh);
575
			}
576
		}
577
578
		return !empty($this->_number_questions);
579
	}
580
581
	/**
582
	 * Prepare the Q&A test/list for this request
583
	 *
584
	 * @param boolean $refresh
585
	 */
586
	public function createTest($refresh = true)
587
	{
588
		if (empty($this->_number_questions))
589
			return;
590
591
		// Getting some new questions?
592
		if ($refresh)
593
		{
594
			$this->_questionIDs = array();
595
596
			// Pick some random IDs
597
			if ($this->_number_questions == 1)
598
				$this->_questionIDs[] = $this->_possible_questions[array_rand($this->_possible_questions, $this->_number_questions)];
599
			else
600
				foreach (array_rand($this->_possible_questions, $this->_number_questions) as $index)
601
					$this->_questionIDs[] = $this->_possible_questions[$index];
602
		}
603
		// Same questions as before.
604
		else
605
			$this->_questionIDs = !empty($_SESSION[$this->_options['id'] . '_vv']['q']) ? $_SESSION[$this->_options['id'] . '_vv']['q'] : array();
606
607
		if (empty($this->_questionIDs) && !$refresh)
608
			$this->createTest(true);
609
	}
610
611
	/**
612
	 * Get things ready for the template
613
	 *
614
	 * @return mixed[]
615
	 * @throws Elk_Exception
616
	 */
617
	public function prepareContext()
618
	{
619
		loadTemplate('VerificationControls');
620
621
		$_SESSION[$this->_options['id'] . '_vv']['q'] = array();
622
623
		$questions = $this->_loadAntispamQuestions(array('type' => 'id_question', 'value' => $this->_questionIDs));
624
		$asked_questions = array();
625
626
		$parser = \BBC\ParserWrapper::instance();
627
628
		foreach ($questions as $row)
629
		{
630
			$asked_questions[] = array(
631
				'id' => $row['id_question'],
632
				'q' => $parser->parseVerificationControls($row['question']),
633
				'is_error' => !empty($this->_incorrectQuestions) && in_array($row['id_question'], $this->_incorrectQuestions),
634
				// Remember a previous submission?
635
				'a' => isset($_REQUEST[$this->_options['id'] . '_vv'], $_REQUEST[$this->_options['id'] . '_vv']['q'], $_REQUEST[$this->_options['id'] . '_vv']['q'][$row['id_question']]) ? Util::htmlspecialchars($_REQUEST[$this->_options['id'] . '_vv']['q'][$row['id_question']]) : '',
636
			);
637
			$_SESSION[$this->_options['id'] . '_vv']['q'][] = $row['id_question'];
638
		}
639
640
		return array(
641
			'template' => 'questions',
642
			'values' => $asked_questions,
643
		);
644
	}
645
646
	/**
647
	 * Performs the test to see if the answer is correct
648
	 *
649
	 * @return bool|string
650
	 * @throws Elk_Exception no_access
651
	 */
652
	public function doTest()
653
	{
654
		if ($this->_number_questions && (!isset($_SESSION[$this->_options['id'] . '_vv']['q']) || !isset($_REQUEST[$this->_options['id'] . '_vv']['q'])))
655
			throw new Elk_Exception('no_access', false);
656
657
		if (!$this->_verifyAnswers())
658
			return 'wrong_verification_answer';
659
660
		return true;
661
	}
662
663
	/**
664
	 * Required by the interface, returns true for question challenges
665
	 *
666
	 * @return boolean
667
	 */
668
	public function hasVisibleTemplate()
669
	{
670
		return true;
671
	}
672
673
	/**
674
	 * Admin panel interface to manage the anti spam question area
675
	 *
676
	 * @return mixed[]
677
	 */
678 1
	public function settings()
679
	{
680 1
		global $txt, $context, $language;
681
682
		// Load any question and answers!
683 1
		$filter = null;
684 1
		if (isset($_GET['language']))
685 1
		{
686
			$filter = array(
687
				'type' => 'language',
688
				'value' => $_GET['language'],
689
			);
690
		}
691 1
		$context['question_answers'] = $this->_loadAntispamQuestions($filter);
692 1
		$languages = getLanguages();
693
694
		// Languages dropdown only if we have more than a lang installed, otherwise is plain useless
695 1
		if (count($languages) > 1)
696 1
		{
697
			$context['languages'] = $languages;
698
			foreach ($context['languages'] as &$lang)
699
				if ($lang['filename'] === $language)
700
					$lang['selected'] = true;
701
		}
702
703
		// Saving them?
704 1
		if (isset($_GET['save']))
705 1
		{
706
			// Handle verification questions.
707
			$questionInserts = array();
708
			$count_questions = 0;
709
710
			foreach ($_POST['question'] as $id => $question)
711
			{
712
				$question = trim(Util::htmlspecialchars($question, ENT_COMPAT));
713
				$answers = array();
714
				$question_lang = isset($_POST['language'][$id]) && isset($languages[$_POST['language'][$id]]) ? $_POST['language'][$id] : $language;
715
				if (!empty($_POST['answer'][$id]))
716
					foreach ($_POST['answer'][$id] as $answer)
717
					{
718
						$answer = trim(Util::strtolower(Util::htmlspecialchars($answer, ENT_COMPAT)));
719
						if ($answer != '')
720
							$answers[] = $answer;
721
					}
722
723
				// Already existed?
724
				if (isset($context['question_answers'][$id]))
725
				{
726
					$count_questions++;
727
728
					// Changed?
729
					if ($question == '' || empty($answers))
730
					{
731
						$this->_delete($id);
732
						$count_questions--;
733
					}
734
					else
735
						$this->_update($id, $question, $answers, $question_lang);
736
				}
737
				// It's so shiney and new!
738
				elseif ($question != '' && !empty($answers))
739
				{
740
					$questionInserts[] = array(
741
						'question' => $question,
742
						// @todo: remotely possible that the serialized value is longer than 65535 chars breaking the update/insertion
743
						'answer' => serialize($answers),
744
						'language' => $question_lang,
745
					);
746
					$count_questions++;
747
				}
748
			}
749
750
			// Any questions to insert?
751
			if (!empty($questionInserts))
752
				$this->_insert($questionInserts);
753
754
			if (empty($count_questions) || $_POST['qa_verification_number'] > $count_questions)
755
				$_POST['qa_verification_number'] = $count_questions;
756
757
		}
758
759
		return array(
760
			// Clever Thomas, who is looking sheepy now? Not I, the mighty sword swinger did say.
761 1
			array('title', 'setup_verification_questions'),
762 1
				array('desc', 'setup_verification_questions_desc'),
763 1
				array('int', 'qa_verification_number', 'postinput' => $txt['setting_qa_verification_number_desc']),
764 1
				array('callback', 'question_answer_list'),
765 1
		);
766
	}
767
768
	/**
769
	 * Checks if an the answers to anti-spam questions are correct
770
	 *
771
	 * @return boolean
772
	 */
773
	private function _verifyAnswers()
774
	{
775
		// Get the answers and see if they are all right!
776
		$questions = $this->_loadAntispamQuestions(array('type' => 'id_question', 'value' => $_SESSION[$this->_options['id'] . '_vv']['q']));
777
		$this->_incorrectQuestions = array();
778
		foreach ($questions as $row)
779
		{
780
			// Everything lowercase
781
			$answers = array();
782
			foreach ($row['answer'] as $answer)
783
				$answers[] = Util::strtolower($answer);
784
785
			if (!isset($_REQUEST[$this->_options['id'] . '_vv']['q'][$row['id_question']]) || trim($_REQUEST[$this->_options['id'] . '_vv']['q'][$row['id_question']]) == '' || !in_array(trim(Util::htmlspecialchars(Util::strtolower($_REQUEST[$this->_options['id'] . '_vv']['q'][$row['id_question']]))), $answers))
786
				$this->_incorrectQuestions[] = $row['id_question'];
787
		}
788
789
		return empty($this->_incorrectQuestions);
790
	}
791
792
	/**
793
	 * Updates the cache of questions IDs
794
	 */
795
	private function _refreshQuestionsCache()
796
	{
797
		global $modSettings;
798
799
		$db = database();
800
		$cache = Cache::instance();
801
802
		if (!$cache->getVar($modSettings['question_id_cache'], 'verificationQuestionIds', 300) || !$modSettings['question_id_cache'])
803
		{
804
			$request = $db->query('', '
805
				SELECT 
806
					id_question, language
807
				FROM {db_prefix}antispam_questions',
808
				array()
809
			);
810
			$modSettings['question_id_cache'] = array();
811
			while ($row = $db->fetch_assoc($request))
812
				$modSettings['question_id_cache'][$row['language']][] = $row['id_question'];
813
			$db->free_result($request);
814
815
			$cache->put('verificationQuestionIds', $modSettings['question_id_cache'], 300);
816
		}
817
	}
818
819
	/**
820
	 * Loads all the available antispam questions, or a subset based on a filter
821
	 *
822
	 * @param array|null $filter if specified it myst be an array with two indexes:
823
	 *              - 'type' => a valid filter, it can be 'language' or 'id_question'
824
	 *              - 'value' => the value of the filter (i.e. the language)
825
	 */
826 1
	private function _loadAntispamQuestions($filter = null)
827
	{
828 1
		$db = database();
829
830
		$available_filters = array(
831 1
			'language' => 'language = {string:current_filter}',
832 1
			'id_question' => 'id_question IN ({array_int:current_filter})',
833 1
		);
834
835
		// Load any question and answers!
836 1
		$question_answers = array();
837 1
		$request = $db->query('', '
838
			SELECT 
839
				id_question, question, answer, language
840 1
			FROM {db_prefix}antispam_questions' . ($filter === null || !isset($available_filters[$filter['type']]) ? '' : '
841 1
			WHERE ' . $available_filters[$filter['type']]),
842
			array(
843 1
				'current_filter' => $filter['value'],
844
			)
845 1
		);
846 1
		while ($row = $db->fetch_assoc($request))
847
		{
848
			$question_answers[$row['id_question']] = array(
849
				'id_question' => $row['id_question'],
850
				'question' => $row['question'],
851
				'answer' => Util::unserialize($row['answer']),
852
				'language' => $row['language'],
853
			);
854
		}
855 1
		$db->free_result($request);
856
857 1
		return $question_answers;
858
	}
859
860
	/**
861
	 * Remove a question by id
862
	 *
863
	 * @param int $id
864
	 */
865
	private function _delete($id)
866
	{
867
		$db = database();
868
869
		$db->query('', '
870
			DELETE FROM {db_prefix}antispam_questions
871
			WHERE id_question = {int:id}',
872
			array(
873
				'id' => $id,
874
			)
875
		);
876
	}
877
878
	/**
879
	 * Update an existing question
880
	 *
881
	 * @param int $id
882
	 * @param string $question
883
	 * @param string[] $answers
884
	 * @param string $language
885
	 */
886
	private function _update($id, $question, $answers, $language)
887
	{
888
		$db = database();
889
890
		$db->query('', '
891
			UPDATE {db_prefix}antispam_questions
892
			SET
893
				question = {string:question},
894
				answer = {string:answer},
895
				language = {string:language}
896
			WHERE id_question = {int:id}',
897
			array(
898
				'id' => $id,
899
				'question' => $question,
900
				// @todo: remotely possible that the serialized value is longer than 65535 chars breaking the update/insertion
901
				'answer' => serialize($answers),
902
				'language' => $language,
903
			)
904
		);
905
	}
906
907
	/**
908
	 * Adds the questions to the db
909
	 *
910
	 * @param mixed[] $questions
911
	 */
912
	private function _insert($questions)
913
	{
914
		$db = database();
915
916
		$db->insert('',
917
			'{db_prefix}antispam_questions',
918
			array('question' => 'string-65535', 'answer' => 'string-65535', 'language' => 'string-50'),
919
			$questions,
920
			array('id_question')
921
		);
922
	}
923
}
924
925
/**
926
 * This class shows an anti spam bot box in the form
927
 * The proper response is to leave the field empty, bots however will see this
928
 * much like a session field and populate it with a value.
929
 *
930
 * Adding additional catch terms is recommended to keep bots from learning
931
 */
932
class Verification_Controls_EmptyField implements Verification_Controls
933
{
934
	/**
935
	 * Hold the options passed to the class
936
	 *
937
	 * @var array
938
	 */
939
	private $_options = null;
940
941
	/**
942
	 * If its going to be used or not on a form
943
	 *
944
	 * @var boolean
945
	 */
946
	private $_empty_field = null;
947
948
	/**
949
	 * Holds a randomly generated field name
950
	 *
951
	 * @var string
952
	 */
953
	private $_field_name = null;
954
955
	/**
956
	 * If the validation test has been run
957
	 *
958
	 * @var boolean
959
	 */
960
	private $_tested = false;
961
962
	/**
963
	 * What the user may have entered in the field
964
	 *
965
	 * @var string
966
	 */
967
	private $_user_value = null;
968
969
	/**
970
	 * Hash value used to generate the field name
971
	 *
972
	 * @var string
973
	 */
974
	private $_hash = null;
975
976
	/**
977
	 * Array of terms used in building the field name
978
	 * @var string[]
979
	 */
980
	private $_terms = array('gadget', 'device', 'uid', 'gid', 'guid', 'uuid', 'unique', 'identifier', 'bb2');
981
982
	/**
983
	 * Secondary array used to build out the field name
984
	 * @var string[]
985
	 */
986
	private $_second_terms = array('hash', 'cipher', 'code', 'key', 'unlock', 'bit', 'value', 'screener');
987
988
	/**
989
	 * Get things rolling
990
	 *
991
	 * @param mixed[]|null $verificationOptions no_empty_field,
992
	 */
993 1
	public function __construct($verificationOptions = null)
994
	{
995 1
		if (!empty($verificationOptions))
996 1
			$this->_options = $verificationOptions;
997 1
	}
998
999
	/**
1000
	 * Returns if we are showing this verification control or not
1001
	 * Build the control if we are
1002
	 *
1003
	 * @param boolean $isNew
1004
	 * @param boolean $force_refresh
1005
	 */
1006
	public function showVerification($isNew, $force_refresh = true)
1007
	{
1008
		global $modSettings;
1009
1010
		$this->_tested = false;
1011
1012
		if ($isNew)
1013
		{
1014
			$this->_empty_field = !empty($this->_options['no_empty_field']) || (!empty($modSettings['enable_emptyfield']) && !isset($this->_options['no_empty_field']));
1015
			$this->_user_value = '';
1016
		}
1017
1018
		if ($isNew || $force_refresh)
1019
			$this->createTest($force_refresh);
1020
1021
		return $this->_empty_field;
1022
	}
1023
1024
	/**
1025
	 * Create the name data for the empty field that will be added to the template
1026
	 *
1027
	 * @param boolean $refresh
1028
	 */
1029
	public function createTest($refresh = true)
1030
	{
1031
		if (!$this->_empty_field)
1032
			return;
1033
1034
		// Building a field with a believable name that will be inserted lives in the template.
1035
		if ($refresh || !isset($_SESSION[$this->_options['id'] . '_vv']['empty_field']))
1036
		{
1037
			$start = mt_rand(0, 27);
1038
			$this->_hash = substr(md5(time()), $start, 6);
1039
			$this->_field_name = $this->_terms[array_rand($this->_terms)] . '-' . $this->_second_terms[array_rand($this->_second_terms)] . '-' . $this->_hash;
1040
			$_SESSION[$this->_options['id'] . '_vv']['empty_field'] = '';
1041
			$_SESSION[$this->_options['id'] . '_vv']['empty_field'] = $this->_field_name;
1042
		}
1043
		else
1044
		{
1045
			$this->_field_name = $_SESSION[$this->_options['id'] . '_vv']['empty_field'];
1046
			$this->_user_value = !empty($_REQUEST[$_SESSION[$this->_options['id'] . '_vv']['empty_field']]) ? $_REQUEST[$_SESSION[$this->_options['id'] . '_vv']['empty_field']] : '';
1047
		}
1048
	}
1049
1050
	/**
1051
	 * Values passed to the template inside of GenericControls
1052
	 * Use the values to adjust how the control does or does not appear
1053
	 */
1054
	public function prepareContext()
1055
	{
1056
		loadTemplate('VerificationControls');
1057
1058
		return array(
1059
			'template' => 'emptyfield',
1060
			'values' => array(
1061
				'is_error' => $this->_tested && !$this->_verifyField(),
1062
				// Can be used in the template to show the normally hidden field to add some spice to things
1063
				'show' => !empty($_SESSION[$this->_options['id'] . '_vv']['empty_field']) && (mt_rand(1, 100) > 60),
1064
				'user_value' => $this->_user_value,
1065
				'field_name' => $this->_field_name,
1066
				// Can be used in the template to randomly add a value to the empty field that needs to be removed when show is on
1067
				'clear' => (mt_rand(1, 100) > 60),
1068
			)
1069
		);
1070
	}
1071
1072
	/**
1073
	 * Run the test on the returned value and return pass or fail
1074
	 */
1075
	public function doTest()
1076
	{
1077
		$this->_tested = true;
1078
1079
		if (!$this->_verifyField())
1080
			return 'wrong_verification_answer';
1081
1082
		return true;
1083
	}
1084
1085
	/**
1086
	 * Not used, just returns false for empty field verifications
1087
	 *
1088
	 * @return false
1089
	 */
1090
	public function hasVisibleTemplate()
1091
	{
1092
		return false;
1093
	}
1094
1095
	/**
1096
	 * Test the field, easy, its on, its is set and it is empty
1097
	 */
1098
	private function _verifyField()
1099
	{
1100
		return $this->_empty_field && !empty($_SESSION[$this->_options['id'] . '_vv']['empty_field']) && empty($_REQUEST[$_SESSION[$this->_options['id'] . '_vv']['empty_field']]);
1101
	}
1102
1103
	/**
1104
	 * Callback for this verification control options, which is on or off
1105
	 */
1106 1
	public function settings()
1107
	{
1108
		// Empty field verification.
1109
		$config_vars = array(
1110 1
			array('title', 'configure_emptyfield'),
1111 1
			array('desc', 'configure_emptyfield_desc'),
1112 1
			array('check', 'enable_emptyfield'),
1113 1
		);
1114
1115 1
		return $config_vars;
1116
	}
1117
}
1118