PluginProbe ʕ •ᴥ•ʔ
SiteGuard WP Plugin / 1.8.9
SiteGuard WP Plugin v1.8.9
1.8.9 1.8.8 1.8.7 1.8.6 1.8.6-beta1 1.8.6-beta2 1.8.4 1.8.5 1.8.3 1.8.2 1.8.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.2.3 1.4.3 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.7.0 1.7.1 1.7.10 1.7.11 1.7.12 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.0-beta1 1.8.0-beta2 1.8.0-beta3 1.8.0-beta4
siteguard / really-simple-captcha / siteguard-really-simple-captcha.php
siteguard / really-simple-captcha Last commit date
gentium 11 years ago mplus-TESTFLIGHT-058 11 years ago siteguard-really-simple-captcha.php 1 week ago
siteguard-really-simple-captcha.php
410 lines
1 <?php
2 /*
3 This function based on Really Simple CAPTCHA 1.8.
4 modify matters
5 * add Hiragana ( Japanese ) CAPTCHA
6 * add random line
7 * store answer files in a non-public directory
8
9 Base-Plugin Name: Really Simple CAPTCHA
10 Base-Plugin URI: http://contactform7.com/captcha/
11 Base-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.
12 Base-Author: Takayuki Miyoshi
13 Base-Version: 1.8
14 Base-Author URI: http://ideasilo.wordpress.com/
15 */
16
17 /*
18 Copyright 2007-2014 Takayuki Miyoshi
19
20 This program is free software; you can redistribute it and/or modify
21 it under the terms of the GNU General Public License as published by
22 the Free Software Foundation; either version 2 of the License, or
23 (at your option) any later version.
24 */
25
26 class SiteGuardReallySimpleCaptcha extends SiteGuard_Base {
27 /* Mode of character set alphabet(en) or hiragana(jp) */
28 protected $lang_mode;
29
30 /* Length of a word in an image */
31 protected $char_length;
32
33 /* Directory for public CAPTCHA images (also stores answer files; see $ans_dir) */
34 protected $tmp_dir;
35
36 /* Directory for answer files. Same path as $tmp_dir; the files use the
37 * .php extension with a "<?php exit; ?>" prefix so HTTP requests return
38 * an empty response while PHP can still read the contents via the
39 * filesystem. This avoids depending on .htaccess (Apache-only) or
40 * sys_get_temp_dir() (unreliable on some shared/Nginx hosts). */
41 protected $ans_dir;
42
43 /* Array of CAPTCHA image size. Width and height */
44 protected $img_size;
45
46 /* Coordinates for a text in an image */
47 protected $base;
48
49 /* Font size */
50 protected $font_size;
51
52 /* Width of a character */
53 protected $font_char_width;
54
55 /* Image type. 'png', 'gif' or 'jpeg' */
56 protected $img_type;
57
58 /* Mode of temporary image files */
59 protected $file_mode;
60
61 /* Mode of temporary answer text files */
62 protected $answer_file_mode;
63
64 /* PHP stub file prefix written to answer files. When the file is
65 * requested over HTTP the PHP runtime processes the file, exits, and
66 * returns an empty response — preventing the hash from leaking. The
67 * plugin strips this prefix when reading the file via the filesystem. */
68 const ANSWER_FILE_PREFIX = '<?php exit; ?>';
69
70 /**
71 * Directory holding the CAPTCHA images and answer files.
72 *
73 * Static so that the capability check can ask for the path without building
74 * an instance, and so the path is written down in exactly one place.
75 */
76 public static function get_tmp_dir() {
77 return path_join( WP_CONTENT_DIR, 'siteguard' );
78 }
79
80 public function __construct() {
81 $this->lang_mode = 'jp';
82 $this->char_length = 4;
83 $this->tmp_dir = self::get_tmp_dir();
84 $this->ans_dir = $this->tmp_dir; // answer files live alongside images; see $ans_dir doc.
85 $this->img_size = array( 72, 24 );
86 $this->base = array( 6, 18 );
87 $this->font_size = 14;
88 $this->font_char_width = 15;
89 $this->img_type = 'png';
90 $this->file_mode = 0444;
91 $this->answer_file_mode = 0440;
92 }
93
94 /**
95 * Generate and return a random word.
96 */
97 public function generate_random_word() {
98 $chars_en = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
99 $chars_jp = 'あいうえおかきくけこさしすせそたちつてとなにのひふへまみむもやゆよらりん';
100
101 $chars = ( 'jp' === $this->lang_mode ) ? $chars_jp : $chars_en;
102 $word = '';
103
104 $chars_size = mb_strlen( $chars );
105 for ( $i = 0; $i < $this->char_length; $i++ ) {
106 $pos = siteguard_rand( 0, $chars_size - 1 );
107 $char = mb_substr( $chars, $pos, 1 );
108 $word .= $char;
109 }
110
111 return $word;
112 }
113
114 /**
115 * Generate CAPTCHA image and corresponding answer file.
116 *
117 * @return string|bool The image filename (e.g. 12345.png) or false on failure. //
118 */
119 public function generate_image( $prefix, $word ) {
120 // Same test as SiteGuard_CAPTCHA::is_image_rendering_available(), repeated
121 // here so this file stays independent of the plugin classes. Callers are
122 // expected to skip the CAPTCHA when the server cannot draw it; this makes
123 // sure a caller that forgets gets `false` and a missing image rather than
124 // a call to an undefined function and a 500 on the login page.
125 if ( ! function_exists( 'imagecreatetruecolor' ) || ! function_exists( 'imagettftext' ) || ! function_exists( 'imagepng' ) ) {
126 return false;
127 }
128 if ( ! $this->make_tmp_dir() ) {
129 return false;
130 }
131
132 $this->cleanup();
133
134 /* Array of fonts. Randomly picked up per character */
135 if ( 'jp' == $this->lang_mode ) {
136 $fonts = array(
137 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-1c-hiragana-black.ttf',
138 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-1m-hiragana-bold.ttf',
139 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-1mn-hiragana-light.ttf',
140 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-1p-hiragana-medium.ttf',
141 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-2c-hiragana-thin.ttf',
142 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-2m-hiragana-thin.ttf',
143 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-2p-hiragana-bold.ttf',
144 );
145 } else {
146 $fonts = array(
147 __DIR__ . '/gentium/GenBkBasR.ttf',
148 __DIR__ . '/gentium/GenBkBasI.ttf',
149 __DIR__ . '/gentium/GenBkBasBI.ttf',
150 __DIR__ . '/gentium/GenBkBasB.ttf',
151 );
152 }
153
154 $dir = trailingslashit( $this->tmp_dir );
155 $filename = null;
156
157 if ( $im = imagecreatetruecolor( $this->img_size[0], $this->img_size[1] ) ) {
158 $bg = imagecolorallocate( $im, 255, 255, 255 );
159 $fg = imagecolorallocate( $im, 0, 0, 0 );
160 imagefill( $im, 0, 0, $bg );
161
162 // random lines
163 for ( $i = 0; $i < 5; $i++ ) {
164 $color = imagecolorallocate( $im, 196, 196, 196 );
165 imageline(
166 $im,
167 siteguard_rand( 0, $this->img_size[0] - 1 ),
168 siteguard_rand( 0, $this->img_size[1] - 1 ),
169 siteguard_rand( 0, $this->img_size[0] - 1 ),
170 siteguard_rand( 0, $this->img_size[1] - 1 ),
171 $color
172 );
173 }
174
175 $x = $this->base[0] + siteguard_rand( -2, 2 );
176
177 $gd_info = gd_info();
178 $word_size = mb_strlen( $word );
179 for ( $i = 0; $i < $word_size; $i++ ) {
180 $font = $fonts[ array_rand( $fonts ) ];
181 $font = $this->normalize_path( $font );
182 if ( ! empty( $gd_info['JIS-mapped Japanese Font Support'] ) ) {
183 $char = mb_convert_encoding( mb_substr( $word, $i, 1 ), 'SJIS', 'UTF-8' );
184 } else {
185 $char = mb_substr( $word, $i, 1 );
186 }
187 imagettftext( $im, $this->font_size, siteguard_rand( -12, 12 ), $x, $this->base[1] + siteguard_rand( -2, 2 ), $fg, $font, $char );
188 $x += $this->font_char_width;
189 }
190
191 $written = false;
192 switch ( $this->img_type ) {
193 case 'jpeg':
194 $filename = sanitize_file_name( $prefix . '.jpeg' );
195 $file = $this->normalize_path( $dir . $filename );
196 $written = imagejpeg( $im, $file );
197 break;
198 case 'gif':
199 $filename = sanitize_file_name( $prefix . '.gif' );
200 $file = $this->normalize_path( $dir . $filename );
201 $written = imagegif( $im, $file );
202 break;
203 case 'png':
204 default:
205 $filename = sanitize_file_name( $prefix . '.png' );
206 $file = $this->normalize_path( $dir . $filename );
207 $written = imagepng( $im, $file );
208 }
209
210 imagedestroy( $im );
211 // The return value used to be ignored, so a failed write still handed
212 // the caller a filename and the form pointed at an image that was
213 // never created.
214 if ( ! $written ) {
215 siteguard_error_log( 'failed to write image file (' . $file . '). :' . __FILE__ );
216 return false;
217 }
218 @chmod( $file, $this->file_mode );
219 }
220
221 $this->generate_answer_file( $prefix, $word );
222
223 return $filename;
224 }
225
226 /**
227 * Generate answer file corresponding to CAPTCHA image.
228 * Written as a .php file with a "<?php exit; ?>" prefix so HTTP
229 * requests for the file return an empty response.
230 */
231 public function generate_answer_file( $prefix, $word ) {
232 $dir = trailingslashit( $this->ans_dir );
233 $answer_file = $this->normalize_path( $dir . sanitize_file_name( $prefix . '.php' ) );
234
235 if ( $fh = @fopen( $answer_file, 'w' ) ) {
236 $word = strtoupper( $word );
237 $salt = wp_generate_password( 64 );
238 $hash = hash_hmac( 'md5', $word, $salt );
239 fwrite( $fh, self::ANSWER_FILE_PREFIX . $salt . '|' . $hash );
240 fclose( $fh );
241 @chmod( $answer_file, $this->answer_file_mode );
242 } else {
243 siteguard_error_log( 'failed to open file (' . $answer_file . '). : ' . __FILE__ );
244 }
245 }
246
247 /**
248 * Check a response against the code kept in the (private) answer file.
249 */
250 public function check( $prefix, $response, $remove = false ) {
251 if ( 0 == strlen( $prefix ) ) {
252 return false;
253 }
254
255 $response = str_replace( array( ' ', "\t" ), '', $response );
256 $response = strtoupper( $response );
257
258 $dir = trailingslashit( $this->ans_dir );
259 $file = $this->normalize_path( $dir . sanitize_file_name( $prefix . '.php' ) );
260
261 if ( @is_readable( $file ) && ( $code = file_get_contents( $file ) ) ) {
262 if ( 0 === strpos( $code, self::ANSWER_FILE_PREFIX ) ) {
263 $code = substr( $code, strlen( self::ANSWER_FILE_PREFIX ) );
264 }
265 $code = explode( '|', $code, 2 );
266
267 if ( isset( $code[0], $code[1] ) ) {
268 $salt = $code[0];
269 $hash = $code[1];
270 if ( hash_hmac( 'md5', $response, $salt ) == $hash ) {
271 if ( $remove ) {
272 $this->remove( $prefix );
273 }
274 return true;
275 }
276 }
277 }
278
279 if ( $remove ) { //
280 $this->remove( $prefix );
281 }
282 return false;
283 }
284
285 /**
286 * Remove temporary files with given prefix.
287 * Images from public dir, answer file from private dir.
288 */
289 public function remove( $prefix ) {
290 // remove images
291 $img_suffixes = array( '.jpeg', '.gif', '.png' );
292 foreach ( $img_suffixes as $suffix ) {
293 $dir = trailingslashit( $this->tmp_dir );
294 $filename = sanitize_file_name( $prefix . $suffix );
295 $file = $this->normalize_path( $dir . $filename );
296 if ( @is_file( $file ) ) {
297 @unlink( $file );
298 }
299 }
300
301 // remove answer
302 $dir = trailingslashit( $this->ans_dir );
303 $file = $this->normalize_path( $dir . sanitize_file_name( $prefix . '.php' ) );
304 if ( @is_file( $file ) ) {
305 @unlink( $file );
306 }
307 }
308
309 /**
310 * Clean up dead files older than given minutes (images + answers).
311 */
312 public function cleanup( $minutes = 60 ) {
313 return $this->cleanup_dir(
314 $this->tmp_dir,
315 '/^[0-9]+\.(png|gif|jpeg|php)$/',
316 $minutes
317 );
318 }
319
320 private function cleanup_dir( $dir_base, $pattern, $minutes ) {
321 $dir = trailingslashit( $dir_base );
322 $dir = $this->normalize_path( $dir );
323
324 if ( ! @is_dir( $dir ) || ! @is_readable( $dir ) ) {
325 return 0;
326 }
327
328 $is_win = ( 'WIN' === strtoupper( substr( PHP_OS, 0, 3 ) ) );
329 if ( ! ( $is_win ? win_is_writable( $dir ) : @is_writable( $dir ) ) ) {
330 return 0;
331 }
332
333 $count = 0;
334 if ( $handle = @opendir( $dir ) ) {
335 while ( false !== ( $filename = readdir( $handle ) ) ) {
336 if ( ! preg_match( $pattern, $filename ) ) {
337 continue;
338 }
339 $file = $this->normalize_path( $dir . $filename );
340 $stat = @stat( $file );
341 if ( $stat && ( $stat['mtime'] + $minutes * 60 ) < time() ) {
342 if ( ! @unlink( $file ) ) {
343 @chmod( $file, 0644 );
344 @unlink( $file );
345 }
346 $count += 1;
347 }
348 }
349 closedir( $handle );
350 }
351 return $count;
352 }
353
354 /**
355 * Make the directory used for CAPTCHA images and answer files.
356 * Answer files protect themselves via a "<?php exit; ?>" prefix
357 * (see generate_answer_file), so this directory does not require
358 * .htaccess or restricted filesystem permissions.
359 */
360 public function make_tmp_dir() {
361 $dir = $this->normalize_path( trailingslashit( $this->tmp_dir ) );
362 if ( ! wp_mkdir_p( $dir ) ) {
363 siteguard_error_log( 'failed to make directory (' . $dir . '). :' . __FILE__ );
364 return false;
365 }
366 // wp_mkdir_p() reports success for a directory that already exists, whoever
367 // owns it. Without this, a directory created by WP-CLI under another user
368 // passed every check while the web server could not write a single file
369 // into it -- the image and the answer file both silently failed and no
370 // response could ever be accepted.
371 if ( ! is_writable( $dir ) ) {
372 siteguard_error_log( 'directory is not writable (' . $dir . '). :' . __FILE__ );
373 return false;
374 }
375 // minimal index to avoid directory listing (harmless on Nginx with autoindex off)
376 $index_file = $this->normalize_path( $dir . 'index.html' );
377 if ( ! file_exists( $index_file ) ) {
378 @file_put_contents( $index_file, '' );
379 @chmod( $index_file, 0444 );
380 }
381
382 // copy dummy image if missing
383 $dmy_src_file = SITEGUARD_PATH . 'images/dummy.png';
384 $dmy_dst_file = $dir . 'dummy.png';
385 if ( ! file_exists( $dmy_dst_file ) ) {
386 @copy( $dmy_src_file, $dmy_dst_file );
387 }
388
389 return true;
390 }
391
392 /**
393 * Normalize a filesystem path.
394 */
395 private function normalize_path( $path ) {
396 $path = str_replace( '\\', '/', $path );
397 $path = preg_replace( '|/+|', '/', $path );
398 return $path;
399 }
400
401 /**
402 * set $this->lang_mode
403 */
404 public function set_lang_mode( $mode ) {
405 if ( 'jp' === $mode || 'en' === $mode ) {
406 $this->lang_mode = $mode;
407 }
408 }
409 }
410