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 / classes / siteguard-captcha.php
siteguard / classes Last commit date
siteguard-admin-filter.php 3 months ago siteguard-base.php 3 weeks ago siteguard-captcha.php 1 week ago siteguard-config.php 1 year ago siteguard-disable-author-query.php 2 months ago siteguard-disable-pingback.php 3 months ago siteguard-disable-xmlrpc.php 3 months ago siteguard-htaccess.php 3 weeks ago siteguard-login-alert.php 3 months ago siteguard-login-history.php 2 months ago siteguard-login-lock.php 2 months ago siteguard-rename-login.php 1 week ago siteguard-updates-notify.php 1 week ago siteguard-waf-exclude-rule.php 3 months ago
siteguard-captcha.php
381 lines
1 <?php
2
3 require_once SITEGUARD_PATH . 'really-simple-captcha/siteguard-really-simple-captcha.php';
4
5 class SiteGuard_CAPTCHA extends SiteGuard_Base {
6 protected $captcha;
7 protected $prefix;
8 protected $word;
9
10 private $last_check_result = null;
11 private $last_check_prefix = null;
12
13 /**
14 * Whether this PHP can render a CAPTCHA image at this moment.
15 *
16 * The requirements are checked on activation and on the settings page, but
17 * the environment can change afterwards: PHP is updated, the site is moved,
18 * GD is rebuilt without FreeType, the functions are listed in
19 * disable_functions -- or the plugin was activated through WP-CLI, whose SAPI
20 * has GD while php-fpm does not. The stored setting then says the feature is
21 * on while generate_image() would be a call to an undefined function, taking
22 * the login page down with a 500. So the capability is checked at run time,
23 * every request, right where the hooks are registered. The three functions
24 * are the ones generate_image() cannot do without.
25 */
26 public static function is_image_rendering_available() {
27 return function_exists( 'imagecreatetruecolor' )
28 && function_exists( 'imagettftext' )
29 && function_exists( 'imagepng' );
30 }
31
32 /**
33 * Whether the image and answer files can actually be written.
34 *
35 * Activating through WP-CLI as a different user than the web server (root is
36 * the usual case) creates wp-content/siteguard/ owned by that user, and the
37 * web server can then create nothing inside it. The image write fails, the
38 * answer file is never written, and every response is rejected -- the login
39 * page becomes impossible to pass. make_tmp_dir() used to report success in
40 * that state because the directory already existed, so neither activation nor
41 * the settings page noticed.
42 *
43 * The directory is created on first use, so until it exists the question is
44 * whether it can be created, which is a property of the parent. Memoized: the
45 * answer cannot change within a request, and this is on the front-end path.
46 */
47 public static function is_answer_dir_writable() {
48 static $writable = null;
49 if ( null !== $writable ) {
50 return $writable;
51 }
52 $dir = SiteGuardReallySimpleCaptcha::get_tmp_dir();
53 $writable = is_dir( $dir ) ? is_writable( $dir ) : is_writable( WP_CONTENT_DIR );
54 return $writable;
55 }
56
57 /**
58 * Whether a CAPTCHA can be produced and verified at all right now.
59 *
60 * Both halves have to hold together: presenting a CAPTCHA that cannot be
61 * checked, or demanding one that cannot be drawn, locks everyone out of the
62 * login page. This is the single answer used by the hook registration, by the
63 * login URL rescue form and by the requirement check on the settings page, so
64 * those three can never disagree with each other.
65 */
66 public static function is_captcha_available() {
67 return self::is_image_rendering_available() && self::is_answer_dir_writable();
68 }
69
70 function __construct() {
71 global $siteguard_config;
72 // The capability test belongs to this condition and not to any single
73 // handler: the form and its verification are registered together, so a
74 // server that cannot draw the image simply gets no CAPTCHA instead of a
75 // form that demands characters nobody can read. captcha_enable is left
76 // alone on purpose -- a temporary state of the environment is not a
77 // reason to rewrite a stored setting, and the feature comes back by
78 // itself once the server can render again.
79 if ( '1' == $siteguard_config->get( 'captcha_enable' ) && 'xmlrpc.php' != basename( $_SERVER['SCRIPT_NAME'] ) && ! is_admin() && self::is_captcha_available() ) {
80 $this->captcha = new SiteGuardReallySimpleCaptcha();
81
82 add_filter( 'shake_error_codes', array( $this, 'handler_shake_error_codes' ) );
83
84 // for login
85 if ( '0' !== $siteguard_config->get( 'captcha_login' ) ) {
86 add_filter( 'login_form', array( $this, 'handler_login_form' ) );
87 add_filter( 'wp_authenticate_user', array( $this, 'handler_wp_authenticate_user' ), 1, 2 );
88 }
89 // for lost password
90 if ( '0' !== $siteguard_config->get( 'captcha_lostpasswd' ) ) {
91 add_filter( 'lostpassword_form', array( $this, 'handler_lostpassword_form' ) );
92 add_filter( 'lostpassword_post', array( $this, 'handler_lostpassword_post' ), 1 );
93 }
94 // for register user
95 if ( '0' !== $siteguard_config->get( 'captcha_registuser' ) ) {
96 add_filter( 'register_form', array( $this, 'handler_register_form' ) );
97 add_action( 'registration_errors', array( $this, 'handler_registration_errors' ), 10, 3 );
98 }
99 // for comment
100 if ( '0' !== $siteguard_config->get( 'captcha_comment' ) ) {
101 add_action( 'comment_form_after_fields', array( $this, 'handler_comment_form' ), 1 );
102 add_action( 'comment_form_logged_in_after', array( $this, 'handler_comment_form' ), 1 );
103 add_action( 'comment_form', array( $this, 'handler_comment_form' ) );
104 add_filter( 'preprocess_comment', array( $this, 'handler_process_comment_post' ) );
105 add_action( 'wp_footer', array( $this, 'comment_captcha_reload_script' ) );
106 }
107 }
108 if ( '1' == $siteguard_config->get( 'same_login_error' ) ) {
109 add_filter( 'login_errors', array( $this, 'handler_login_errors' ) );
110 }
111 // A security feature must not switch itself off quietly.
112 if ( is_admin() && '1' == $siteguard_config->get( 'captcha_enable' ) && ! self::is_captcha_available() ) {
113 add_action( 'admin_notices', array( $this, 'handler_admin_notices_captcha_unavailable' ) );
114 }
115 }
116
117 function handler_admin_notices_captcha_unavailable() {
118 if ( ! current_user_can( 'manage_options' ) ) {
119 return;
120 }
121 // The two causes need different answers: one is for the hosting provider,
122 // the other the administrator can fix, so name the directory.
123 if ( ! self::is_image_rendering_available() ) {
124 $message = esc_html__( 'SiteGuard WP Plugin: CAPTCHA is turned on, but this server cannot render the CAPTCHA image, so CAPTCHA is not being applied. Please contact your hosting provider about the required image rendering support.', 'siteguard' );
125 } else {
126 $message = sprintf(
127 /* translators: %s: Filesystem path of the CAPTCHA working directory. */
128 esc_html__( 'SiteGuard WP Plugin: CAPTCHA is turned on, but the directory %s cannot be written to by the web server, so CAPTCHA is not being applied. Please change the owner of this directory to the user the web server runs as.', 'siteguard' ),
129 esc_html( SiteGuardReallySimpleCaptcha::get_tmp_dir() )
130 );
131 }
132 echo '<div class="notice notice-warning is-dismissible"><p>';
133 echo $message;
134 echo ' ';
135 echo esc_html__( 'The setting has been left as it is, and CAPTCHA resumes automatically once the problem is resolved.', 'siteguard' );
136 echo '</p></div>';
137 }
138 private function check_captcha_with_cache() {
139 $current_prefix = isset( $_POST['siteguard_captcha_prefix'] ) ? $_POST['siteguard_captcha_prefix'] : '';
140 if ( $this->last_check_prefix !== $current_prefix ) {
141 $this->last_check_result = null;
142 $this->last_check_prefix = $current_prefix;
143 }
144 if ( null !== $this->last_check_result ) {
145 return $this->last_check_result;
146 }
147
148 $is_ok = false;
149 if ( array_key_exists( 'siteguard_captcha', $_POST ) && array_key_exists( 'siteguard_captcha_prefix', $_POST ) ) {
150 $is_ok = $this->captcha->check( $_POST['siteguard_captcha_prefix'], $_POST['siteguard_captcha'], true );
151 }
152
153 $this->last_check_result = $is_ok;
154 return $is_ok;
155 }
156 function check_requirements() {
157 $error = siteguard_check_multisite();
158 if ( is_wp_error( $error ) ) {
159 return $error;
160 }
161 $error = $this->check_extensions();
162 if ( is_wp_error( $error ) ) {
163 return $error;
164 }
165 $error = $this->check_image_access();
166 if ( is_wp_error( $error ) ) {
167 return $error;
168 }
169 $error = $this->check_support_freetype();
170 if ( is_wp_error( $error ) ) {
171 return $error;
172 }
173 return true;
174 }
175
176 function check_extensions() {
177 $error_extensions = array();
178 $extensions = array(
179 'mbstring',
180 'gd',
181 );
182 foreach ( $extensions as $extension ) {
183 if ( ! extension_loaded( $extension ) ) {
184 $error_extensions[] = $extension;
185 }
186 }
187 if ( empty( $error_extensions ) ) {
188 return true;
189 }
190
191 $message = esc_html__( 'This feature requires additional server components. Please contact your hosting provider to enable it.', 'siteguard' );
192
193 $error = new WP_Error( 'siteguard_captcha', $message );
194 return $error;
195 }
196
197 function check_image_access() {
198 if ( is_object( $this->captcha ) ) {
199 $ret = $this->captcha->make_tmp_dir();
200 } else {
201 $captcha = new SiteGuardReallySimpleCaptcha();
202 $ret = $captcha->make_tmp_dir();
203 }
204 if ( false === $ret ) {
205 $message = esc_html__( 'Failed to write the CAPTCHA image file.', 'siteguard' );
206 $error = new WP_Error( 'siteguard_captcha', $message );
207 return $error;
208 }
209
210 return true;
211 }
212
213 function check_support_freetype() {
214 // The same three functions the run-time gate uses, so that the settings
215 // page cannot report the feature as usable while it is being skipped on
216 // the login page. imagettftext() alone missed a server that has GD and
217 // FreeType but hides imagepng() behind disable_functions.
218 if ( self::is_image_rendering_available() ) {
219 return true;
220 }
221 $message = esc_html__( 'Your server does not support the image rendering required for this feature. Please contact your hosting provider.', 'siteguard' );
222 $error = new WP_Error( 'siteguard_captcha', $message );
223 return $error;
224 }
225
226 function handler_login_errors( $error ) {
227 if ( strlen( $error ) > 0 && false === strpos( $error, esc_html__( 'ERROR: LOGIN LOCKED', 'siteguard' ) ) ) {
228 $error = esc_html__( 'ERROR: Please check your input and try again.', 'siteguard' );
229 }
230 return $error;
231 }
232
233 function handler_shake_error_codes( $shake_error_codes ) {
234 array_push( $shake_error_codes, 'siteguard-captcha-error' );
235 return $shake_error_codes;
236 }
237
238 function init() {
239 global $siteguard_config;
240 $errors = $this->check_requirements();
241 if ( ! is_wp_error( $errors ) ) {
242 $switch = '1';
243 } else {
244 $switch = '0';
245 }
246 $siteguard_config->set( 'captcha_enable', $switch );
247
248 $language = get_bloginfo( 'language' );
249 if ( 'ja' == $language ) {
250 $mode = '1'; // hiragana
251 } else {
252 $mode = '2'; // alphanumeric
253 }
254 $siteguard_config->set( 'captcha_login', $mode );
255 $siteguard_config->set( 'captcha_comment', $mode );
256 $siteguard_config->set( 'captcha_lostpasswd', $mode );
257 $siteguard_config->set( 'captcha_registuser', $mode );
258
259 if ( true === siteguard_check_multisite() ) {
260 $siteguard_config->set( 'same_login_error', '1' );
261 } else {
262 $siteguard_config->set( 'same_login_error', '0' );
263 }
264 $siteguard_config->update();
265 }
266
267 function get_captcha() {
268 $result = '<p>';
269 $result .= '<img src="' . WP_CONTENT_URL . '/siteguard/' . $this->prefix . '.png" alt="CAPTCHA">';
270 $result .= '</p><p>';
271 $result .= '<label for="siteguard_captcha">' . esc_html__( 'Please enter the characters shown above.', 'siteguard' ) . '</label><br />';
272 $result .= '<input type="text" name="siteguard_captcha" id="siteguard_captcha" class="input" value="" size="10" aria-required="true" />';
273 $result .= '<input type="hidden" name="siteguard_captcha_prefix" id="siteguard_captcha_prefix" value="' . $this->prefix . '" />';
274 $result .= '</p>';
275
276 return $result;
277 }
278
279 function put_captcha() {
280 $this->word = $this->captcha->generate_random_word();
281 $this->prefix = siteguard_rand();
282 $this->captcha->generate_image( $this->prefix, $this->word );
283 echo $this->get_captcha();
284 }
285
286 function handler_login_form() {
287 global $siteguard_config;
288 ( '2' === $siteguard_config->get( 'captcha_login' ) ) ? $this->captcha->set_lang_mode( 'en' ) : $this->captcha->set_lang_mode( 'jp' );
289 $this->put_captcha();
290 }
291
292 function handler_comment_form( $post_id ) {
293 global $siteguard_config;
294 if ( defined( 'SITEGUARD_PUT_COMMENT_FORM' ) ) {
295 return;
296 }
297 ( '2' === $siteguard_config->get( 'captcha_comment' ) ) ? $this->captcha->set_lang_mode( 'en' ) : $this->captcha->set_lang_mode( 'jp' );
298 $this->put_captcha();
299 define( 'SITEGUARD_PUT_COMMENT_FORM', '1' );
300 }
301
302 function handler_lostpassword_form() {
303 global $siteguard_config;
304 ( '2' === $siteguard_config->get( 'captcha_lostpasswd' ) ) ? $this->captcha->set_lang_mode( 'en' ) : $this->captcha->set_lang_mode( 'jp' );
305 $this->put_captcha();
306 }
307
308 function handler_register_form() {
309 global $siteguard_config;
310 ( '2' == $siteguard_config->get( 'captcha_registuser' ) ) ? $this->captcha->set_lang_mode( 'en' ) : $this->captcha->set_lang_mode( 'jp' );
311 $this->put_captcha();
312 }
313
314 function handler_wp_authenticate_user( $user, $password ) {
315 if ( array_key_exists( 'siteguard_captcha', $_POST ) && array_key_exists( 'siteguard_captcha_prefix', $_POST ) ) {
316 if ( $this->check_captcha_with_cache() ) {
317 return $user;
318 }
319 }
320 $error = new WP_Error();
321 $error->add( 'siteguard-captcha-error', esc_html__( 'ERROR: Invalid CAPTCHA.', 'siteguard' ) );
322 return $error;
323 }
324
325 function add_captcha_error() {
326 return new WP_Error( 'siteguard-captcha-error', esc_html__( 'ERROR: Invalid CAPTCHA.', 'siteguard' ) );
327 }
328
329 function handler_lostpassword_post() {
330 if ( array_key_exists( 'siteguard_captcha', $_POST ) && array_key_exists( 'siteguard_captcha_prefix', $_POST ) ) {
331 if ( $this->check_captcha_with_cache() ) {
332 return;
333 }
334 }
335 add_filter( 'allow_password_reset', array( $this, 'add_captcha_error' ) );
336 }
337
338 function handler_registration_errors( $errors, $sanitized_user_login, $user_email ) {
339 if ( array_key_exists( 'siteguard_captcha', $_POST ) && array_key_exists( 'siteguard_captcha_prefix', $_POST ) ) {
340 if ( $this->check_captcha_with_cache() ) {
341 return $errors;
342 }
343 }
344 $new_errors = new WP_Error();
345 $new_errors->add( 'siteguard-captcha-error', esc_html__( 'ERROR: Invalid CAPTCHA.', 'siteguard' ) );
346 return $new_errors;
347 }
348
349 function handler_process_comment_post( $comment ) {
350 if ( is_admin() ) {
351 return $comment;
352 }
353 if ( array_key_exists( 'siteguard_captcha', $_POST ) && array_key_exists( 'siteguard_captcha_prefix', $_POST ) ) {
354 if ( ! empty( $_POST['siteguard_captcha'] ) ) {
355 if ( $this->check_captcha_with_cache( ) ) {
356 return $comment;
357 }
358 }
359 }
360 wp_die( esc_html__( 'ERROR: Invalid CAPTCHA.', 'siteguard' ), esc_html( 'ERROR'), array( 'back_link' => true ) );
361 }
362 public function comment_captcha_reload_script( ) {
363 if ( is_singular() && comments_open() ) {
364 ?>
365 <script>
366 window.addEventListener('pageshow', function(event) {
367 var isBackForward = false;
368 if (window.performance && typeof performance.getEntriesByType === 'function') {
369 var perfEntries = performance.getEntriesByType('navigation');
370 isBackForward = perfEntries.length > 0 && perfEntries[0].type === 'back_forward';
371 }
372 if (event.persisted || isBackForward) {
373 window.location.reload();
374 }
375 });
376 </script>
377 <?php
378 }
379 }
380 }
381