PluginProbe ʕ •ᴥ•ʔ
SiteGuard WP Plugin / 1.8.7
SiteGuard WP Plugin v1.8.7
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-htaccess.php
siteguard / classes Last commit date
siteguard-admin-filter.php 1 month ago siteguard-base.php 1 month ago siteguard-captcha.php 1 month ago siteguard-config.php 11 months ago siteguard-disable-author-query.php 2 weeks ago siteguard-disable-pingback.php 1 month ago siteguard-disable-xmlrpc.php 1 month ago siteguard-htaccess.php 2 weeks ago siteguard-login-alert.php 1 month ago siteguard-login-history.php 1 month ago siteguard-login-lock.php 1 month ago siteguard-rename-login.php 1 week ago siteguard-updates-notify.php 1 month ago siteguard-waf-exclude-rule.php 1 month ago
siteguard-htaccess.php
444 lines
1 <?php
2
3 class SiteGuard_Htaccess extends SiteGuard_Base {
4 const HTACCESS_PERMISSION = 0604;
5 const HTACCESS_MARK_START = '#SITEGUARD_PLUGIN_SETTINGS_START';
6 const HTACCESS_MARK_END = '#SITEGUARD_PLUGIN_SETTINGS_END';
7
8 function __construct() {
9 }
10 static function get_htaccess_file() {
11 return ABSPATH . '.htaccess';
12 }
13 static function get_tmp_dir() {
14 return SITEGUARD_PATH . 'tmp/';
15 }
16 static function is_writable_htaccess() {
17 if ( isset( $_SERVER['SERVER_SOFTWARE'] ) && false !== stripos( $_SERVER['SERVER_SOFTWARE'], 'nginx' ) ) {
18 return false;
19 }
20 if ( file_exists( self::get_htaccess_file() ) ) {
21 return is_writable( self::get_htaccess_file() );
22 }
23 return is_writable( ABSPATH );
24 }
25
26 // Diagnostic reason for the most recent test_htaccess() failure, as an array
27 // like array( 'code' => 'http_status', 'url' => ..., 'status' => 403 ). An empty
28 // array means success (or not yet run). SiteGuard_RenameLogin reads this to
29 // record why it fell back to stub (.php) mode, so administrators can see the
30 // cause on the settings screen.
31 public static $last_reason = array();
32
33 static function test_htaccess() {
34 self::$last_reason = array();
35 if ( ! self::is_writable_htaccess() ) {
36 $is_nginx = isset( $_SERVER['SERVER_SOFTWARE'] ) && false !== stripos( $_SERVER['SERVER_SOFTWARE'], 'nginx' );
37 self::$last_reason = array( 'code' => $is_nginx ? 'nginx' : 'not_writable' );
38 return false;
39 }
40
41 // Sweep orphaned test directories left behind by previous runs whose
42 // wp_remote_get timed out before cleanup could execute.
43 self::cleanup_orphaned_test_dirs();
44
45
46 $test_dir_name = 'siteguard-test-' . uniqid();
47 $test_dir_path = ABSPATH . $test_dir_name;
48 $htaccess_path = $test_dir_path . '/.htaccess';
49 $php_file_path = $test_dir_path . '/test.php';
50 // The test directory is created under ABSPATH, which is served at the
51 // WordPress Address (siteurl), NOT necessarily the Site Address (home).
52 // On "WordPress in its own directory" installs (e.g. core in /wp, site at
53 // root) these differ, so home_url() would build a URL that does not map to
54 // the test directory and the self-test would always 404. Use the raw
55 // siteurl (get_option avoids the rename-login site_url filter) so the URL
56 // matches ABSPATH. For ordinary installs siteurl == home, so no change.
57 $base_url = rtrim( get_option( 'siteurl' ), '/' );
58 $test_url = $base_url . '/' . $test_dir_name . '/test.html';
59 $php_content = '<?php echo "SUCCESS";';
60 $htaccess_content = "RewriteEngine On\nRewriteRule ^test\\.html$ test.php [L]";
61
62 $cleanup = function () use ( $htaccess_path, $php_file_path, $test_dir_path ) {
63 if ( file_exists( $htaccess_path ) ) {
64 @unlink( $htaccess_path );
65 }
66 if ( file_exists( $php_file_path ) ) {
67 @unlink( $php_file_path );
68 }
69 if ( is_dir( $test_dir_path ) ) {
70 @rmdir( $test_dir_path );
71 }
72 };
73
74 if ( ! @mkdir( $test_dir_path, 0755 ) ) {
75 self::$last_reason = array( 'code' => 'mkdir' );
76 return false;
77 }
78
79 if ( false === @file_put_contents( $php_file_path, $php_content ) || false === @file_put_contents( $htaccess_path, $htaccess_content ) ) {
80 $cleanup();
81 self::$last_reason = array( 'code' => 'write' );
82 return false;
83 }
84
85 $args = array(
86 'timeout' => 10,
87 'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
88 );
89 $response = wp_remote_get( $test_url, $args );
90
91 // On success the .htaccess rewrite turned test.html into test.php (SUCCESS).
92 if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) && 'SUCCESS' === wp_remote_retrieve_body( $response ) ) {
93 $cleanup();
94 return true;
95 }
96
97 // The rewrite test failed. Probe test.php directly (before cleanup) to tell
98 // apart "the .htaccess was ignored" from "the test files were unreachable":
99 // if test.php itself returns SUCCESS, the directory and PHP are reachable
100 // and only the RewriteRule had no effect (AllowOverride None / mod_rewrite
101 // off). If test.php is also unreachable, the URL did not map to the test
102 // directory at all (subdirectory install, routing, or access restriction).
103 $php_url = $base_url . '/' . $test_dir_name . '/test.php';
104 $php_probe = wp_remote_get( $php_url, $args );
105 $probe_ok = ! is_wp_error( $php_probe ) && 200 === wp_remote_retrieve_response_code( $php_probe ) && 'SUCCESS' === wp_remote_retrieve_body( $php_probe );
106
107 $cleanup();
108
109 if ( is_wp_error( $response ) ) {
110 self::$last_reason = array(
111 'code' => 'wp_error',
112 'url' => $test_url,
113 'detail' => $response->get_error_message(),
114 );
115 return false;
116 }
117 if ( $probe_ok ) {
118 self::$last_reason = array(
119 'code' => 'htaccess_ignored',
120 'url' => $test_url,
121 );
122 return false;
123 }
124 $status = wp_remote_retrieve_response_code( $response );
125 if ( 200 === $status ) {
126 self::$last_reason = array(
127 'code' => 'bad_body',
128 'url' => $test_url,
129 );
130 return false;
131 }
132 self::$last_reason = array(
133 'code' => 'http_status',
134 'url' => $test_url,
135 'status' => $status,
136 );
137 return false;
138 }
139 private static function cleanup_orphaned_test_dirs() {
140 $orphans = glob( ABSPATH . 'siteguard-test-*', GLOB_ONLYDIR );
141 if ( empty( $orphans ) ) {
142 return;
143 }
144 foreach ( $orphans as $dir ) {
145 $entries = @scandir( $dir );
146 if ( is_array( $entries ) ) {
147 foreach ( $entries as $entry ) {
148 if ( '.' === $entry || '..' === $entry ) {
149 continue;
150 }
151 $path = $dir . DIRECTORY_SEPARATOR . $entry;
152 if ( is_file( $path ) ) {
153 @unlink( $path );
154 }
155 }
156 }
157 @rmdir( $dir );
158 }
159 }
160 static function get_htaccess_new_file() {
161 return tempnam( self::get_tmp_dir(), 'htaccess_' );
162 }
163 static function make_tmp_dir() {
164 $dir = self::get_tmp_dir();
165 if ( ! wp_mkdir_p( $dir ) ) {
166 siteguard_error_log( "make tempdir failed: $dir" );
167 return false;
168 }
169 // Defense-in-depth against directory listing if .htaccess is ignored
170 // (e.g. Apache configured with AllowOverride None). Do not chmod the
171 // file — making it read-only blocks WordPress plugin overwrite/upgrade.
172 $index_file = $dir . 'index.html';
173 if ( ! file_exists( $index_file ) ) {
174 @file_put_contents( $index_file, '' );
175 }
176 $htaccess_file = $dir . '.htaccess';
177
178 if ( file_exists( $htaccess_file ) ) {
179 $lines = file( $htaccess_file );
180 $res = preg_grep( '/IfModule authz_core_module/', $lines );
181 if ( ! empty( $res ) ) {
182 return true;
183 }
184 }
185
186 if ( $handle = @fopen( $htaccess_file, 'w' ) ) {
187 fwrite( $handle, '<IfModule authz_core_module>' . "\n" );
188 fwrite( $handle, ' Require all denied' . "\n" );
189 fwrite( $handle, '</IfModule>' . "\n" );
190 fwrite( $handle, '<IfModule !authz_core_module>' . "\n" );
191 fwrite( $handle, ' Order deny,allow' . "\n" );
192 fwrite( $handle, ' Deny from all' . "\n" );
193 fwrite( $handle, '</IfModule>' . "\n" );
194 fclose( $handle );
195 }
196
197 return true;
198 }
199 static function is_exists_setting( $mark ) {
200 $result = false;
201 if ( '' === $mark ) {
202 $mark_start = self::HTACCESS_MARK_START;
203 $mark_end = self::HTACCESS_MARK_END;
204 } else {
205 $mark_start = $mark . '_START';
206 $mark_end = $mark . '_END';
207 }
208 $current_file = self::get_htaccess_file();
209 if ( ! file_exists( $current_file ) ) {
210 return $result;
211 }
212 $fr = @fopen( $current_file, 'r' );
213 if ( null === $fr ) {
214 return $result;
215 }
216 $line_num = 0;
217 $start_line = 0;
218 $end_line = 0;
219 while ( ! feof( $fr ) ) {
220 $line = fgets( $fr, 4096 );
221 ++$line_num;
222 if ( false !== strpos( $line, $mark_start ) ) {
223 $start_line = $line_num;
224 }
225 if ( false !== strpos( $line, $mark_end ) ) {
226 $end_line = $line_num;
227 if ( $start_line > 0 && ( $end_line - $start_line ) > 1 ) {
228 $result = true;
229 }
230 break;
231 }
232 }
233 @fclose( $fr );
234
235 return $result;
236 }
237 static function check_permission( $flag_create = true ) {
238 $file = self::get_htaccess_file();
239 if ( true === $flag_create ) {
240 self::get_apply_permission( $file );
241 }
242 if ( ! is_readable( $file ) ) {
243 siteguard_error_log( "file not readable: $file" );
244 return false;
245 }
246 if ( ! is_writable( $file ) ) {
247 siteguard_error_log( "file not writable: $file" );
248 return false;
249 }
250 $path = pathinfo( $file, PATHINFO_DIRNAME );
251 if ( ! is_writable( $path ) ) {
252 siteguard_error_log( 'directory not writable: ' . $path );
253 return false;
254 }
255 return true;
256 }
257 static function get_apply_permission_itr( $file ) {
258 clearstatcache();
259 $perm = intval( substr( sprintf( '%o', fileperms( $file ) ), -4 ), 8 );
260 return $perm;
261 }
262 static function get_apply_permission( $file ) {
263 $perm = self::HTACCESS_PERMISSION;
264 if ( file_exists( $file ) ) {
265 $perm = self::get_apply_permission_itr( $file );
266 } else {
267 @touch( $file );
268 }
269 @chmod( $file, $perm );
270 return $perm;
271 }
272 static function clear_settings( $mark ) {
273 // On Nginx (or any environment where .htaccess is not in use), the
274 // rebuild is a no-op. Skipping here avoids creating the in-plugin
275 // tmp/ directory and any tempnam fragments that would not be
276 // protected from web access.
277 if ( ! self::is_writable_htaccess() ) {
278 return true;
279 }
280 if ( ! self::make_tmp_dir() ) {
281 return false;
282 }
283 if ( '' === $mark ) {
284 $mark_start = self::HTACCESS_MARK_START;
285 $mark_end = self::HTACCESS_MARK_END;
286 } else {
287 $mark_start = $mark . '_START';
288 $mark_end = $mark . '_END';
289 }
290 $flag_settings = false;
291 $current_file = self::get_htaccess_file();
292 if ( ! file_exists( $current_file ) ) {
293 return false;
294 }
295 $perm = self::get_apply_permission( $current_file );
296
297 if ( ! self::check_permission( false ) ) {
298 return false;
299 }
300 $fr = @fopen( $current_file, 'r' );
301 if ( null === $fr ) {
302 siteguard_error_log( "fopen failed: $current_file" );
303 return false;
304 }
305 $new_file = self::get_htaccess_new_file();
306 $fw = @fopen( $new_file, 'w' );
307 if ( null === $fw ) {
308 siteguard_error_log( "fopen failed: $new_file" );
309 @unlink( $new_file );
310 fclose( $fr );
311 return false;
312 }
313 while ( ! feof( $fr ) ) {
314 $line = fgets( $fr, 4096 );
315 if ( false !== strpos( $line, $mark_start ) ) {
316 $flag_settings = true;
317 }
318 if ( false === $flag_settings ) {
319 fputs( $fw, $line, 4096 );
320 }
321 if ( true == $flag_settings && false !== strpos( $line, $mark_end ) ) {
322 $flag_settings = false;
323 }
324 }
325 fclose( $fr );
326 fclose( $fw );
327 @chmod( $new_file, $perm );
328 if ( ! rename( $new_file, $current_file ) ) {
329 siteguard_error_log( "rename failed: $new_file $current_file" );
330 @unlink( $new_file );
331 return false;
332 }
333 return true;
334 }
335 function update_settings( $mark, $data ) {
336 // See note in clear_settings(): skip on Nginx where .htaccess is unused.
337 if ( ! self::is_writable_htaccess() ) {
338 return true;
339 }
340 if ( ! self::make_tmp_dir() ) {
341 return false;
342 }
343 $flag_write = false;
344 $flag_through = true;
345 $flag_wp = false;
346 $flag_wp_set = false;
347 $wp_settings = '';
348 $mark_start = $mark . '_START';
349 $mark_end = $mark . '_END';
350 $mark_wp_start = '# BEGIN WordPress';
351 $mark_wp_end = '# END WordPress';
352 $current_file = self::get_htaccess_file();
353 $perm = self::get_apply_permission( $current_file );
354 if ( ! self::check_permission( false ) ) {
355 return false;
356 }
357 $fr = @fopen( $current_file, 'r' );
358 if ( null === $fr ) {
359 siteguard_error_log( "fopen failed: $current_file" );
360 return false;
361 }
362 $new_file = self::get_htaccess_new_file();
363 if ( ! is_writable( $new_file ) ) {
364 siteguard_error_log( "file not writable: $new_file" );
365 @unlink( $new_file );
366 fclose( $fr );
367 return false;
368 }
369 $fw = @fopen( $new_file, 'w' );
370 if ( null === $fw ) {
371 siteguard_error_log( "fopen failed: $new_file" );
372 @unlink( $new_file );
373 fclose( $fr );
374 return false;
375 }
376 while ( ! feof( $fr ) ) {
377 $line = fgets( $fr, 4096 );
378
379 // Save WordPress settings.
380 // WordPress settings has to be written after SiteGuard settings.
381 if ( false === $flag_write && false == $flag_wp_set && false !== strpos( $line, $mark_wp_start ) ) {
382 $flag_wp = true;
383 $flag_wp_set = true;
384 }
385 if ( $flag_wp_set ) {
386 $wp_settings .= $line;
387 if ( false !== strpos( $line, $mark_wp_end ) ) {
388 $flag_wp_set = false;
389 }
390 continue;
391 }
392
393 if ( false === $flag_write && false !== strpos( $line, $mark_start ) ) {
394 fwrite( $fw, $line, strlen( $line ) );
395 fwrite( $fw, $data, strlen( $data ) );
396 $flag_write = true;
397 $flag_through = false;
398 // continue;
399 }
400 if ( false === $flag_write && false !== strpos( $line, self::HTACCESS_MARK_END ) ) {
401 fwrite( $fw, $mark_start . "\n", strlen( $mark_start ) + 1 );
402 fwrite( $fw, $data, strlen( $data ) );
403 fwrite( $fw, $mark_end . "\n", strlen( $mark_end ) + 1 );
404 $flag_write = true;
405 }
406 if ( false === $flag_through && false !== strpos( $line, $mark_end ) ) {
407 $flag_through = true;
408 }
409 if ( $flag_through ) {
410 fwrite( $fw, $line, strlen( $line ) );
411 if ( false === $flag_wp && false !== strpos( $line, $mark_wp_start ) ) {
412 $flag_wp = true;
413 }
414 }
415 }
416 if ( false === $flag_write ) {
417 fwrite( $fw, "\n" . self::HTACCESS_MARK_START . "\n", strlen( self::HTACCESS_MARK_START ) + 2 );
418 fwrite( $fw, $mark_start . "\n", strlen( $mark_start ) + 1 );
419 fwrite( $fw, $data, strlen( $data ) );
420 fwrite( $fw, $mark_end . "\n", strlen( $mark_end ) + 1 );
421 fwrite( $fw, self::HTACCESS_MARK_END . "\n", strlen( self::HTACCESS_MARK_END ) + 1 );
422 }
423 if ( '' != $wp_settings ) { // Write saved WordPress Settings
424 fwrite( $fw, "\n", 1 );
425 fwrite( $fw, $wp_settings, strlen( $wp_settings ) );
426 fwrite( $fw, "\n", 1 );
427 } elseif ( false === $flag_wp ) { // Write empty WordPress Settings
428 fwrite( $fw, "\n", 1 );
429 fwrite( $fw, $mark_wp_start . "\n", strlen( $mark_wp_start ) + 1 );
430 fwrite( $fw, $mark_wp_end . "\n", strlen( $mark_wp_end ) + 1 );
431 fwrite( $fw, "\n", 1 );
432 }
433 fclose( $fr );
434 fclose( $fw );
435 @chmod( $new_file, $perm );
436 if ( ! rename( $new_file, $current_file ) ) {
437 siteguard_error_log( "rename failed: $new_file $current_file" );
438 @unlink( $new_file );
439 return false;
440 }
441 return true;
442 }
443 }
444