PluginProbe ʕ •ᴥ•ʔ
CloudSecure WP Security / 1.4.4
CloudSecure WP Security v1.4.4
1.4.10 1.4.9 trunk 0.9.0 1.0.2 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.14 1.3.15 1.3.16 1.3.17 1.3.18 1.3.19 1.3.2 1.3.20 1.3.21 1.3.22 1.3.23 1.3.24 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8
cloudsecure-wp-security / really-simple-captcha / really-simple-captcha.php
cloudsecure-wp-security / really-simple-captcha Last commit date
gentium 2 years ago tmp 2 years ago license.txt 2 years ago readme.txt 2 years ago really-simple-captcha.php 3 months ago
really-simple-captcha.php
369 lines
1 <?php
2 /*
3 Class names are changed to avoid duplication.
4 The class name has been changed from ReallySimpleCaptcha to CloudSecureWP_ReallySimpleCaptcha.
5 */
6
7 /*
8 * Plugin Name: Really Simple CAPTCHA
9 * Plugin URI: https://contactform7.com/captcha/
10 * Description: Really Simple CAPTCHA is a CAPTCHA module intended to be called from other plugins. It is originally created for my Contact Form 7 plugin.
11 * Author: Takayuki Miyoshi
12 * Author URI: https://ideasilo.wordpress.com/
13 * License: GPL v2 or later
14 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
15 * Version: 2.2
16 * Requires at least: 6.1
17 * Requires PHP: 7.4
18 */
19
20
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit;
23 }
24
25 define( 'CLOUDSECUREWP_REALLYSIMPLECAPTCHA_VERSION', '2.2' );
26
27 class CloudSecureWP_ReallySimpleCaptcha {
28
29 public $chars;
30 public $char_length;
31 public $fonts;
32 public $tmp_dir;
33 public $img_size;
34 public $bg;
35 public $fg;
36 public $base;
37 public $font_size;
38 public $font_char_width;
39 public $img_type;
40 public $file_mode;
41 public $answer_file_mode;
42
43 public function __construct() {
44 /* Characters available in images */
45 $this->chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
46
47 /* Length of a word in an image */
48 $this->char_length = 4;
49
50 /* Array of fonts. Randomly picked up per character */
51 $this->fonts = array(
52 path_join( __DIR__, 'gentium/GenBkBasR.ttf' ),
53 path_join( __DIR__, 'gentium/GenBkBasI.ttf' ),
54 path_join( __DIR__, 'gentium/GenBkBasBI.ttf' ),
55 path_join( __DIR__, 'gentium/GenBkBasB.ttf' ),
56 );
57
58 /* Directory temporary keeping CAPTCHA images and corresponding text files */
59 $this->tmp_dir = path_join( __DIR__, 'tmp' );
60
61 /* Array of CAPTCHA image size. Width and height */
62 $this->img_size = array( 72, 24 );
63
64 /* Background color of CAPTCHA image. RGB color 0-255 */
65 $this->bg = array( 255, 255, 255 );
66
67 /* Foreground (character) color of CAPTCHA image. RGB color 0-255 */
68 $this->fg = array( 0, 0, 0 );
69
70 /* Coordinates for a text in an image. I don't know the meaning. Just adjust. */
71 $this->base = array( 6, 18 );
72
73 /* Font size */
74 $this->font_size = 14;
75
76 /* Width of a character */
77 $this->font_char_width = 15;
78
79 /* Image type. 'png', 'gif' or 'jpeg' */
80 $this->img_type = 'png';
81
82 /* Mode of temporary image files */
83 $this->file_mode = 0644;
84
85 /* Mode of temporary answer text files */
86 $this->answer_file_mode = 0640;
87 }
88
89 /**
90 * Generate and return a random word.
91 *
92 * @return string Random word with $chars characters x $char_length length
93 */
94 public function generate_random_word() {
95 $word = '';
96
97 for ( $i = 0; $i < $this->char_length; $i++ ) {
98 $pos = mt_rand( 0, strlen( $this->chars ) - 1 );
99 $char = $this->chars[ $pos ];
100 $word .= $char;
101 }
102
103 return $word;
104 }
105
106 /**
107 * Generate CAPTCHA image and corresponding answer file.
108 *
109 * @param string $prefix File prefix used for both files
110 * @param string $word Random word generated by generate_random_word()
111 * @return string|bool The file name of the CAPTCHA image. Return false if temp directory is not available.
112 */
113 public function generate_image( $prefix, $word ) {
114 if ( ! $this->make_tmp_dir() ) {
115 return false;
116 }
117
118 $this->cleanup();
119
120 $dir = trailingslashit( $this->tmp_dir );
121 $filename = null;
122
123 $im = imagecreatetruecolor(
124 $this->img_size[0],
125 $this->img_size[1]
126 );
127
128 if ( $im ) {
129 $bg = imagecolorallocate( $im, $this->bg[0], $this->bg[1], $this->bg[2] );
130 $fg = imagecolorallocate( $im, $this->fg[0], $this->fg[1], $this->fg[2] );
131
132 imagefill( $im, 0, 0, $bg );
133
134 $x = $this->base[0] + mt_rand( -2, 2 );
135
136 for ( $i = 0; $i < strlen( $word ); $i++ ) {
137 $font = $this->fonts[ array_rand( $this->fonts ) ];
138 $font = wp_normalize_path( $font );
139
140 imagettftext(
141 $im,
142 $this->font_size,
143 mt_rand( -12, 12 ),
144 $x,
145 $this->base[1] + mt_rand( -2, 2 ),
146 $fg,
147 $font,
148 $word[ $i ]
149 );
150
151 $x += $this->font_char_width;
152 }
153
154 switch ( $this->img_type ) {
155 case 'jpeg':
156 $filename = sanitize_file_name( $prefix . '.jpeg' );
157 $file = wp_normalize_path( path_join( $dir, $filename ) );
158 imagejpeg( $im, $file );
159 break;
160 case 'gif':
161 $filename = sanitize_file_name( $prefix . '.gif' );
162 $file = wp_normalize_path( path_join( $dir, $filename ) );
163 imagegif( $im, $file );
164 break;
165 case 'png':
166 default:
167 $filename = sanitize_file_name( $prefix . '.png' );
168 $file = wp_normalize_path( path_join( $dir, $filename ) );
169 imagepng( $im, $file );
170 }
171
172 imagedestroy( $im );
173 @chmod( $file, $this->file_mode );
174 }
175
176 $this->generate_answer_file( $prefix, $word );
177
178 return $filename;
179 }
180
181 /**
182 * Generate answer file corresponding to CAPTCHA image.
183 *
184 * @param string $prefix File prefix used for answer file
185 * @param string $word Random word generated by generate_random_word()
186 */
187 public function generate_answer_file( $prefix, $word ) {
188 $dir = trailingslashit( $this->tmp_dir );
189 $answer_file = path_join( $dir, sanitize_file_name( $prefix . '.txt' ) );
190 $answer_file = wp_normalize_path( $answer_file );
191
192 if ( $fh = @fopen( $answer_file, 'w' ) ) {
193 $word = strtoupper( $word );
194 $salt = wp_generate_password( 64 );
195 $hash = hash_hmac( 'md5', $word, $salt );
196 $code = $salt . '|' . $hash;
197 fwrite( $fh, $code );
198 fclose( $fh );
199 }
200
201 @chmod( $answer_file, $this->answer_file_mode );
202 }
203
204 /**
205 * Check a response against the code kept in the temporary file.
206 *
207 * @param string $prefix File prefix used for both files
208 * @param string $response CAPTCHA response
209 * @param bool $remove_on_failure Whether to remove temporary files on failure. Default true.
210 *
211 * @return bool Return true if the two match, otherwise return false.
212 */
213 public function check( string $prefix, string $response, bool $remove_on_failure = true ): bool {
214 if ( 0 === strlen( $prefix ) ) {
215 return false;
216 }
217
218 $response = str_replace( array( " ", "\t" ), '', $response );
219 $response = strtoupper( $response );
220
221 $dir = trailingslashit( $this->tmp_dir );
222 $filename = sanitize_file_name( $prefix . '.txt' );
223 $file = wp_normalize_path( path_join( $dir, $filename ) );
224
225 if ( is_readable( $file )
226 and $code = file_get_contents( $file ) ) {
227 $code = explode( '|', $code, 2 );
228 $salt = $code[0];
229 $hash = $code[1];
230
231 if ( hash_equals( $hash, hash_hmac( 'md5', $response, $salt ) ) ) {
232 $this->remove( $prefix );
233 return true;
234 }
235 }
236
237 if ( $remove_on_failure ) {
238 $this->remove( $prefix );
239 }
240 return false;
241 }
242
243 /**
244 * Remove temporary files with given prefix.
245 *
246 * @param string $prefix File prefix
247 */
248 public function remove( $prefix ) {
249 $dir = trailingslashit( $this->tmp_dir );
250 $suffixes = array( '.jpeg', '.gif', '.png', '.php', '.txt' );
251
252 foreach ( $suffixes as $suffix ) {
253 $filename = sanitize_file_name( $prefix . $suffix );
254 $file = wp_normalize_path( path_join( $dir, $filename ) );
255
256 if ( is_file( $file ) ) {
257 @unlink( $file );
258 }
259 }
260 }
261
262 /**
263 * Clean up dead files older than given length of time.
264 *
265 * @param int $minutes Consider older files than this time as dead files
266 * @return int|bool The number of removed files. Return false if error occurred.
267 */
268 public function cleanup( $minutes = 60, $max = 100 ) {
269 $dir = trailingslashit( $this->tmp_dir );
270 $dir = wp_normalize_path( $dir );
271
272 if ( ! is_dir( $dir )
273 or ! is_readable( $dir ) ) {
274 return false;
275 }
276
277 $is_win = ( 'WIN' === strtoupper( substr( PHP_OS, 0, 3 ) ) );
278
279 if ( ! ( $is_win ? win_is_writable( $dir ) : is_writable( $dir ) ) ) {
280 return false;
281 }
282
283 $count = 0;
284
285 if ( $handle = opendir( $dir ) ) {
286 while ( false !== ( $filename = readdir( $handle ) ) ) {
287 if ( ! preg_match( '/^[0-9]+\.(php|txt|png|gif|jpeg)$/', $filename ) ) {
288 continue;
289 }
290
291 $file = wp_normalize_path( path_join( $dir, $filename ) );
292
293 if ( ! file_exists( $file )
294 or ! $stat = stat( $file ) ) {
295 continue;
296 }
297
298 if ( ( $stat['mtime'] + $minutes * MINUTE_IN_SECONDS ) < time() ) {
299 if ( ! @unlink( $file ) ) {
300 @chmod( $file, 0644 );
301 @unlink( $file );
302 }
303
304 $count += 1;
305 }
306
307 if ( $max <= $count ) {
308 break;
309 }
310 }
311
312 closedir( $handle );
313 }
314
315 return $count;
316 }
317
318 /**
319 * Make a temporary directory and generate .htaccess file in it.
320 *
321 * @return bool True on successful create, false on failure.
322 */
323 public function make_tmp_dir() {
324 $dir = trailingslashit( $this->tmp_dir );
325 $dir = wp_normalize_path( $dir );
326
327 if ( ! wp_mkdir_p( $dir ) ) {
328 return false;
329 }
330
331 $htaccess_file = wp_normalize_path( path_join( $dir, '.htaccess' ) );
332
333 if ( file_exists( $htaccess_file ) ) {
334 list( $first_line_comment ) = (array) file(
335 $htaccess_file,
336 FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES
337 );
338
339 if ( '# Apache 2.4+' === $first_line_comment ) {
340 return true;
341 }
342 }
343
344 if ( $handle = @fopen( $htaccess_file, 'w' ) ) {
345 fwrite( $handle, "# Apache 2.4+\n" );
346 fwrite( $handle, "<IfModule authz_core_module>\n" );
347 fwrite( $handle, " Require all denied\n" );
348 fwrite( $handle, ' <FilesMatch "^\w+\.(jpe?g|gif|png)$">' . "\n" );
349 fwrite( $handle, " Require all granted\n" );
350 fwrite( $handle, " </FilesMatch>\n" );
351 fwrite( $handle, "</IfModule>\n" );
352 fwrite( $handle, "\n" );
353 fwrite( $handle, "# Apache 2.2\n" );
354 fwrite( $handle, "<IfModule !authz_core_module>\n" );
355 fwrite( $handle, " Order deny,allow\n" );
356 fwrite( $handle, " Deny from all\n" );
357 fwrite( $handle, ' <FilesMatch "^\w+\.(jpe?g|gif|png)$">' . "\n" );
358 fwrite( $handle, " Allow from all\n" );
359 fwrite( $handle, " </FilesMatch>\n" );
360 fwrite( $handle, "</IfModule>\n" );
361
362 fclose( $handle );
363 }
364
365 return true;
366 }
367
368 }
369