PluginProbe ʕ •ᴥ•ʔ
CloudSecure WP Security / 1.4.8
CloudSecure WP Security v1.4.8
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 2 months ago
really-simple-captcha.php
379 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 = 6;
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( 145, 36 );
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, 28 );
72
73 /* Font size */
74 $this->font_size = 20;
75
76 /* Width of a character */
77 $this->font_char_width = 22;
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 = random_int( 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] + random_int( -2, 2 );
135
136 for ( $i = 0; $i < strlen( $word ); $i++ ) {
137 $font = $this->fonts[ random_int( 0, count( $this->fonts ) - 1 ) ];
138 $font = wp_normalize_path( $font );
139
140 imagettftext(
141 $im,
142 $this->font_size,
143 random_int( -25, 25 ),
144 $x,
145 $this->base[1] + random_int( -2, 2 ),
146 $fg,
147 $font,
148 $word[ $i ]
149 );
150
151 $x += $this->font_char_width;
152 }
153
154 /* ドットノイズの追加 */
155 for ( $i = 0; $i < 100; $i++ ) {
156 imagesetpixel( $im, random_int( 0, $this->img_size[0] - 1 ), random_int( 0, $this->img_size[1] - 1 ), $fg );
157 }
158
159 /* ラインノイズの追加 */
160 for ( $i = 0; $i < 2; $i++ ) {
161 imageline( $im, 0, random_int( 0, $this->img_size[1] - 1 ), $this->img_size[0] - 1, random_int( 0, $this->img_size[1] - 1 ), $fg );
162 }
163
164 switch ( $this->img_type ) {
165 case 'jpeg':
166 $filename = sanitize_file_name( $prefix . '.jpeg' );
167 $file = wp_normalize_path( path_join( $dir, $filename ) );
168 imagejpeg( $im, $file );
169 break;
170 case 'gif':
171 $filename = sanitize_file_name( $prefix . '.gif' );
172 $file = wp_normalize_path( path_join( $dir, $filename ) );
173 imagegif( $im, $file );
174 break;
175 case 'png':
176 default:
177 $filename = sanitize_file_name( $prefix . '.png' );
178 $file = wp_normalize_path( path_join( $dir, $filename ) );
179 imagepng( $im, $file );
180 }
181
182 imagedestroy( $im );
183 @chmod( $file, $this->file_mode );
184 }
185
186 $this->generate_answer_file( $prefix, $word );
187
188 return $filename;
189 }
190
191 /**
192 * Generate answer file corresponding to CAPTCHA image.
193 *
194 * @param string $prefix File prefix used for answer file
195 * @param string $word Random word generated by generate_random_word()
196 */
197 public function generate_answer_file( $prefix, $word ) {
198 $dir = trailingslashit( $this->tmp_dir );
199 $answer_file = path_join( $dir, sanitize_file_name( $prefix . '.txt' ) );
200 $answer_file = wp_normalize_path( $answer_file );
201
202 if ( $fh = @fopen( $answer_file, 'w' ) ) {
203 $word = strtoupper( $word );
204 $salt = wp_generate_password( 64 );
205 $hash = hash_hmac( 'sha256', $word, $salt );
206 $code = $salt . '|' . $hash;
207 fwrite( $fh, $code );
208 fclose( $fh );
209 }
210
211 @chmod( $answer_file, $this->answer_file_mode );
212 }
213
214 /**
215 * Check a response against the code kept in the temporary file.
216 *
217 * @param string $prefix File prefix used for both files
218 * @param string $response CAPTCHA response
219 * @param bool $remove_on_failure Whether to remove temporary files on failure. Default true.
220 *
221 * @return bool Return true if the two match, otherwise return false.
222 */
223 public function check( string $prefix, string $response, bool $remove_on_failure = true ): bool {
224 if ( 0 === strlen( $prefix ) ) {
225 return false;
226 }
227
228 $response = str_replace( array( " ", "\t" ), '', $response );
229 $response = strtoupper( $response );
230
231 $dir = trailingslashit( $this->tmp_dir );
232 $filename = sanitize_file_name( $prefix . '.txt' );
233 $file = wp_normalize_path( path_join( $dir, $filename ) );
234
235 if ( is_readable( $file )
236 and $code = file_get_contents( $file ) ) {
237 $code = explode( '|', $code, 2 );
238 $salt = $code[0];
239 $hash = $code[1];
240
241 if ( hash_equals( $hash, hash_hmac( 'sha256', $response, $salt ) ) ) {
242 $this->remove( $prefix );
243 return true;
244 }
245 }
246
247 if ( $remove_on_failure ) {
248 $this->remove( $prefix );
249 }
250 return false;
251 }
252
253 /**
254 * Remove temporary files with given prefix.
255 *
256 * @param string $prefix File prefix
257 */
258 public function remove( $prefix ) {
259 $dir = trailingslashit( $this->tmp_dir );
260 $suffixes = array( '.jpeg', '.gif', '.png', '.php', '.txt' );
261
262 foreach ( $suffixes as $suffix ) {
263 $filename = sanitize_file_name( $prefix . $suffix );
264 $file = wp_normalize_path( path_join( $dir, $filename ) );
265
266 if ( is_file( $file ) ) {
267 @unlink( $file );
268 }
269 }
270 }
271
272 /**
273 * Clean up dead files older than given length of time.
274 *
275 * @param int $minutes Consider older files than this time as dead files
276 * @return int|bool The number of removed files. Return false if error occurred.
277 */
278 public function cleanup( $minutes = 60, $max = 100 ) {
279 $dir = trailingslashit( $this->tmp_dir );
280 $dir = wp_normalize_path( $dir );
281
282 if ( ! is_dir( $dir )
283 or ! is_readable( $dir ) ) {
284 return false;
285 }
286
287 $is_win = ( 'WIN' === strtoupper( substr( PHP_OS, 0, 3 ) ) );
288
289 if ( ! ( $is_win ? win_is_writable( $dir ) : is_writable( $dir ) ) ) {
290 return false;
291 }
292
293 $count = 0;
294
295 if ( $handle = opendir( $dir ) ) {
296 while ( false !== ( $filename = readdir( $handle ) ) ) {
297 if ( ! preg_match( '/^[0-9]+\.(php|txt|png|gif|jpeg)$/', $filename ) ) {
298 continue;
299 }
300
301 $file = wp_normalize_path( path_join( $dir, $filename ) );
302
303 if ( ! file_exists( $file )
304 or ! $stat = stat( $file ) ) {
305 continue;
306 }
307
308 if ( ( $stat['mtime'] + $minutes * MINUTE_IN_SECONDS ) < time() ) {
309 if ( ! @unlink( $file ) ) {
310 @chmod( $file, 0644 );
311 @unlink( $file );
312 }
313
314 $count += 1;
315 }
316
317 if ( $max <= $count ) {
318 break;
319 }
320 }
321
322 closedir( $handle );
323 }
324
325 return $count;
326 }
327
328 /**
329 * Make a temporary directory and generate .htaccess file in it.
330 *
331 * @return bool True on successful create, false on failure.
332 */
333 public function make_tmp_dir() {
334 $dir = trailingslashit( $this->tmp_dir );
335 $dir = wp_normalize_path( $dir );
336
337 if ( ! wp_mkdir_p( $dir ) ) {
338 return false;
339 }
340
341 $htaccess_file = wp_normalize_path( path_join( $dir, '.htaccess' ) );
342
343 if ( file_exists( $htaccess_file ) ) {
344 list( $first_line_comment ) = (array) file(
345 $htaccess_file,
346 FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES
347 );
348
349 if ( '# Apache 2.4+' === $first_line_comment ) {
350 return true;
351 }
352 }
353
354 if ( $handle = @fopen( $htaccess_file, 'w' ) ) {
355 fwrite( $handle, "# Apache 2.4+\n" );
356 fwrite( $handle, "<IfModule authz_core_module>\n" );
357 fwrite( $handle, " Require all denied\n" );
358 fwrite( $handle, ' <FilesMatch "^\w+\.(jpe?g|gif|png)$">' . "\n" );
359 fwrite( $handle, " Require all granted\n" );
360 fwrite( $handle, " </FilesMatch>\n" );
361 fwrite( $handle, "</IfModule>\n" );
362 fwrite( $handle, "\n" );
363 fwrite( $handle, "# Apache 2.2\n" );
364 fwrite( $handle, "<IfModule !authz_core_module>\n" );
365 fwrite( $handle, " Order deny,allow\n" );
366 fwrite( $handle, " Deny from all\n" );
367 fwrite( $handle, ' <FilesMatch "^\w+\.(jpe?g|gif|png)$">' . "\n" );
368 fwrite( $handle, " Allow from all\n" );
369 fwrite( $handle, " </FilesMatch>\n" );
370 fwrite( $handle, "</IfModule>\n" );
371
372 fclose( $handle );
373 }
374
375 return true;
376 }
377
378 }
379