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