PluginProbe ʕ •ᴥ•ʔ
SiteGuard WP Plugin / 1.8.8
SiteGuard WP Plugin v1.8.8
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 3 months ago
siteguard-really-simple-captcha.php
375 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 public function __construct() {
71 $this->lang_mode = 'jp';
72 $this->char_length = 4;
73 $this->tmp_dir = path_join( WP_CONTENT_DIR, 'siteguard' );
74 $this->ans_dir = $this->tmp_dir; // answer files live alongside images; see $ans_dir doc.
75 $this->img_size = array( 72, 24 );
76 $this->base = array( 6, 18 );
77 $this->font_size = 14;
78 $this->font_char_width = 15;
79 $this->img_type = 'png';
80 $this->file_mode = 0444;
81 $this->answer_file_mode = 0440;
82 }
83
84 /**
85 * Generate and return a random word.
86 */
87 public function generate_random_word() {
88 $chars_en = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
89 $chars_jp = 'あいうえおかきくけこさしすせそたちつてとなにのひふへまみむもやゆよらりん';
90
91 $chars = ( 'jp' === $this->lang_mode ) ? $chars_jp : $chars_en;
92 $word = '';
93
94 $chars_size = mb_strlen( $chars );
95 for ( $i = 0; $i < $this->char_length; $i++ ) {
96 $pos = siteguard_rand( 0, $chars_size - 1 );
97 $char = mb_substr( $chars, $pos, 1 );
98 $word .= $char;
99 }
100
101 return $word;
102 }
103
104 /**
105 * Generate CAPTCHA image and corresponding answer file.
106 *
107 * @return string|bool The image filename (e.g. 12345.png) or false on failure. //
108 */
109 public function generate_image( $prefix, $word ) {
110 if ( ! $this->make_tmp_dir() ) {
111 return false;
112 }
113
114 $this->cleanup();
115
116 /* Array of fonts. Randomly picked up per character */
117 if ( 'jp' == $this->lang_mode ) {
118 $fonts = array(
119 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-1c-hiragana-black.ttf',
120 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-1m-hiragana-bold.ttf',
121 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-1mn-hiragana-light.ttf',
122 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-1p-hiragana-medium.ttf',
123 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-2c-hiragana-thin.ttf',
124 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-2m-hiragana-thin.ttf',
125 __DIR__ . '/mplus-TESTFLIGHT-058/mplus-2p-hiragana-bold.ttf',
126 );
127 } else {
128 $fonts = array(
129 __DIR__ . '/gentium/GenBkBasR.ttf',
130 __DIR__ . '/gentium/GenBkBasI.ttf',
131 __DIR__ . '/gentium/GenBkBasBI.ttf',
132 __DIR__ . '/gentium/GenBkBasB.ttf',
133 );
134 }
135
136 $dir = trailingslashit( $this->tmp_dir );
137 $filename = null;
138
139 if ( $im = imagecreatetruecolor( $this->img_size[0], $this->img_size[1] ) ) {
140 $bg = imagecolorallocate( $im, 255, 255, 255 );
141 $fg = imagecolorallocate( $im, 0, 0, 0 );
142 imagefill( $im, 0, 0, $bg );
143
144 // random lines
145 for ( $i = 0; $i < 5; $i++ ) {
146 $color = imagecolorallocate( $im, 196, 196, 196 );
147 imageline(
148 $im,
149 siteguard_rand( 0, $this->img_size[0] - 1 ),
150 siteguard_rand( 0, $this->img_size[1] - 1 ),
151 siteguard_rand( 0, $this->img_size[0] - 1 ),
152 siteguard_rand( 0, $this->img_size[1] - 1 ),
153 $color
154 );
155 }
156
157 $x = $this->base[0] + siteguard_rand( -2, 2 );
158
159 $gd_info = gd_info();
160 $word_size = mb_strlen( $word );
161 for ( $i = 0; $i < $word_size; $i++ ) {
162 $font = $fonts[ array_rand( $fonts ) ];
163 $font = $this->normalize_path( $font );
164 if ( ! empty( $gd_info['JIS-mapped Japanese Font Support'] ) ) {
165 $char = mb_convert_encoding( mb_substr( $word, $i, 1 ), 'SJIS', 'UTF-8' );
166 } else {
167 $char = mb_substr( $word, $i, 1 );
168 }
169 imagettftext( $im, $this->font_size, siteguard_rand( -12, 12 ), $x, $this->base[1] + siteguard_rand( -2, 2 ), $fg, $font, $char );
170 $x += $this->font_char_width;
171 }
172
173 switch ( $this->img_type ) {
174 case 'jpeg':
175 $filename = sanitize_file_name( $prefix . '.jpeg' );
176 $file = $this->normalize_path( $dir . $filename );
177 imagejpeg( $im, $file );
178 break;
179 case 'gif':
180 $filename = sanitize_file_name( $prefix . '.gif' );
181 $file = $this->normalize_path( $dir . $filename );
182 imagegif( $im, $file );
183 break;
184 case 'png':
185 default:
186 $filename = sanitize_file_name( $prefix . '.png' );
187 $file = $this->normalize_path( $dir . $filename );
188 imagepng( $im, $file );
189 }
190
191 imagedestroy( $im );
192 @chmod( $file, $this->file_mode );
193 }
194
195 $this->generate_answer_file( $prefix, $word );
196
197 return $filename;
198 }
199
200 /**
201 * Generate answer file corresponding to CAPTCHA image.
202 * Written as a .php file with a "<?php exit; ?>" prefix so HTTP
203 * requests for the file return an empty response.
204 */
205 public function generate_answer_file( $prefix, $word ) {
206 $dir = trailingslashit( $this->ans_dir );
207 $answer_file = $this->normalize_path( $dir . sanitize_file_name( $prefix . '.php' ) );
208
209 if ( $fh = @fopen( $answer_file, 'w' ) ) {
210 $word = strtoupper( $word );
211 $salt = wp_generate_password( 64 );
212 $hash = hash_hmac( 'md5', $word, $salt );
213 fwrite( $fh, self::ANSWER_FILE_PREFIX . $salt . '|' . $hash );
214 fclose( $fh );
215 @chmod( $answer_file, $this->answer_file_mode );
216 } else {
217 siteguard_error_log( 'failed to open file (' . $answer_file . '). : ' . __FILE__ );
218 }
219 }
220
221 /**
222 * Check a response against the code kept in the (private) answer file.
223 */
224 public function check( $prefix, $response, $remove = false ) {
225 if ( 0 == strlen( $prefix ) ) {
226 return false;
227 }
228
229 $response = str_replace( array( ' ', "\t" ), '', $response );
230 $response = strtoupper( $response );
231
232 $dir = trailingslashit( $this->ans_dir );
233 $file = $this->normalize_path( $dir . sanitize_file_name( $prefix . '.php' ) );
234
235 if ( @is_readable( $file ) && ( $code = file_get_contents( $file ) ) ) {
236 if ( 0 === strpos( $code, self::ANSWER_FILE_PREFIX ) ) {
237 $code = substr( $code, strlen( self::ANSWER_FILE_PREFIX ) );
238 }
239 $code = explode( '|', $code, 2 );
240
241 if ( isset( $code[0], $code[1] ) ) {
242 $salt = $code[0];
243 $hash = $code[1];
244 if ( hash_hmac( 'md5', $response, $salt ) == $hash ) {
245 if ( $remove ) {
246 $this->remove( $prefix );
247 }
248 return true;
249 }
250 }
251 }
252
253 if ( $remove ) { //
254 $this->remove( $prefix );
255 }
256 return false;
257 }
258
259 /**
260 * Remove temporary files with given prefix.
261 * Images from public dir, answer file from private dir.
262 */
263 public function remove( $prefix ) {
264 // remove images
265 $img_suffixes = array( '.jpeg', '.gif', '.png' );
266 foreach ( $img_suffixes as $suffix ) {
267 $dir = trailingslashit( $this->tmp_dir );
268 $filename = sanitize_file_name( $prefix . $suffix );
269 $file = $this->normalize_path( $dir . $filename );
270 if ( @is_file( $file ) ) {
271 @unlink( $file );
272 }
273 }
274
275 // remove answer
276 $dir = trailingslashit( $this->ans_dir );
277 $file = $this->normalize_path( $dir . sanitize_file_name( $prefix . '.php' ) );
278 if ( @is_file( $file ) ) {
279 @unlink( $file );
280 }
281 }
282
283 /**
284 * Clean up dead files older than given minutes (images + answers).
285 */
286 public function cleanup( $minutes = 60 ) {
287 return $this->cleanup_dir(
288 $this->tmp_dir,
289 '/^[0-9]+\.(png|gif|jpeg|php)$/',
290 $minutes
291 );
292 }
293
294 private function cleanup_dir( $dir_base, $pattern, $minutes ) {
295 $dir = trailingslashit( $dir_base );
296 $dir = $this->normalize_path( $dir );
297
298 if ( ! @is_dir( $dir ) || ! @is_readable( $dir ) ) {
299 return 0;
300 }
301
302 $is_win = ( 'WIN' === strtoupper( substr( PHP_OS, 0, 3 ) ) );
303 if ( ! ( $is_win ? win_is_writable( $dir ) : @is_writable( $dir ) ) ) {
304 return 0;
305 }
306
307 $count = 0;
308 if ( $handle = @opendir( $dir ) ) {
309 while ( false !== ( $filename = readdir( $handle ) ) ) {
310 if ( ! preg_match( $pattern, $filename ) ) {
311 continue;
312 }
313 $file = $this->normalize_path( $dir . $filename );
314 $stat = @stat( $file );
315 if ( $stat && ( $stat['mtime'] + $minutes * 60 ) < time() ) {
316 if ( ! @unlink( $file ) ) {
317 @chmod( $file, 0644 );
318 @unlink( $file );
319 }
320 $count += 1;
321 }
322 }
323 closedir( $handle );
324 }
325 return $count;
326 }
327
328 /**
329 * Make the directory used for CAPTCHA images and answer files.
330 * Answer files protect themselves via a "<?php exit; ?>" prefix
331 * (see generate_answer_file), so this directory does not require
332 * .htaccess or restricted filesystem permissions.
333 */
334 public function make_tmp_dir() {
335 $dir = $this->normalize_path( trailingslashit( $this->tmp_dir ) );
336 if ( ! wp_mkdir_p( $dir ) ) {
337 siteguard_error_log( 'failed to make directory (' . $dir . '). :' . __FILE__ );
338 return false;
339 }
340 // minimal index to avoid directory listing (harmless on Nginx with autoindex off)
341 $index_file = $this->normalize_path( $dir . 'index.html' );
342 if ( ! file_exists( $index_file ) ) {
343 @file_put_contents( $index_file, '' );
344 @chmod( $index_file, 0444 );
345 }
346
347 // copy dummy image if missing
348 $dmy_src_file = SITEGUARD_PATH . 'images/dummy.png';
349 $dmy_dst_file = $dir . 'dummy.png';
350 if ( ! file_exists( $dmy_dst_file ) ) {
351 @copy( $dmy_src_file, $dmy_dst_file );
352 }
353
354 return true;
355 }
356
357 /**
358 * Normalize a filesystem path.
359 */
360 private function normalize_path( $path ) {
361 $path = str_replace( '\\', '/', $path );
362 $path = preg_replace( '|/+|', '/', $path );
363 return $path;
364 }
365
366 /**
367 * set $this->lang_mode
368 */
369 public function set_lang_mode( $mode ) {
370 if ( 'jp' === $mode || 'en' === $mode ) {
371 $this->lang_mode = $mode;
372 }
373 }
374 }
375