PluginProbe
Block wp-login / trunk
Block wp-login vtrunk
1.5.8 1.5.7 1.3.2 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 1.4.9 1.5 1.5.1 1.5.2 1.5.3 1.5.4 All 40 releases
block-wp-login / block-wp-login.php

block-wp-login.php in Block wp-login trunk, at block-wp-login.php

1,396 lines 42.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 * Plugin Name: Block wp-login
4 * Version: 1.5.8
5 * Requires at least: 5.0
6 * Requires PHP: 7.0
7 * Plugin URI: https://webd.uk/support/
8 * Description: Blocks direct access to wp-login.php and replaces it with a secret login URL to reduce brute-force traffic on Apache servers.
9 * Author: Webd Ltd
10 * Author URI: https://webd.uk
11 * License: GPLv2 or later
12 * License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
13 * Text Domain: block-wp-login
14 */
15
16 if (!defined('ABSPATH')) {
17 exit(esc_html(__('This isn\'t the page you\'re looking for. Move along, move along.', 'block-wp-login')));
18 }
19
20 if (!class_exists('bwpl_class')) {
21
22 class bwpl_class {
23
24 const VERSION = '1.5.8';
25
26 private $bwpl_new_slug = '';
27
28 public function bwpl_load() {
29
30 add_action('admin_menu', array($this, 'bwpl_add_settings_page'));
31 add_action('admin_post_bwpl_save_settings', array($this, 'bwpl_configure_slug'));
32 register_activation_hook(__FILE__, array(__CLASS__, 'bwpl_activate'));
33 register_deactivation_hook(__FILE__, array($this, 'bwpl_uninstall'));
34
35 if (get_option('bwpl_slug')) {
36
37 if (is_admin() && self::installed_wordpress_version() !== get_bloginfo('version')) {
38
39 add_action('admin_init', array($this, 'bwpl_new_wordpress_version'));
40
41 }
42
43 add_filter('login_url', array($this, 'bwpl_change_login_url'), 10, 3);
44 add_filter('logout_url', array($this, 'bwpl_change_logout_url'), 10, 2);
45 add_filter('wp_redirect', array($this, 'bwpl_change_login_redirect'), 10, 2);
46 add_filter('logout_redirect', array($this, 'bwpl_change_logout_redirect'), 10, 3);
47 add_filter('lostpassword_url', array($this, 'bwpl_change_lostpassword_url'), 10, 2);
48 add_filter('register_url', array($this, 'bwpl_change_register_url'));
49 add_filter('site_url', array($this, 'bwpl_change_password_reset_url'), 10, 3);
50 add_filter('network_site_url', array($this, 'bwpl_change_password_reset_url'), 10, 3);
51
52 } else {
53
54 add_action('admin_notices', array($this, 'bwpl_setup_admin_notice'));
55
56 }
57
58 if (is_admin()) {
59
60 add_filter('plugin_action_links_' . plugin_basename(__FILE__), array($this, 'bwpl_add_plugin_action_links'));
61 add_action('admin_notices', 'bwplCommon::admin_notices');
62 add_action('admin_notices', array($this, 'bwpl_daf_upgrade_notice'));
63 add_action('wp_ajax_dismiss_bwpl_notice_handler', 'bwplCommon::ajax_notice_handler');
64 add_filter('bwpl_admin_notice_pages', array($this, 'bwpl_admin_notice_pages'));
65
66 }
67
68 add_action('wp_login', 'bwpl_class::wp_login', 10, 2);
69
70 }
71
72 function bwpl_add_plugin_action_links($links) {
73
74 $settings_links = bwplCommon::plugin_action_links(admin_url('options-general.php?page=block-wp-login'));
75
76 return array_merge($settings_links, $links);
77
78 }
79
80 public function bwpl_add_settings_page() {
81
82 add_options_page(
83 _x('Block wp-login', 'UI Strings', 'block-wp-login'),
84 _x('Block wp-login', 'UI Strings', 'block-wp-login'),
85 'manage_options',
86 'block-wp-login',
87 array($this, 'bwpl_settings_page')
88 );
89
90 }
91
92 public function bwpl_settings_page() {
93
94 if (!current_user_can('manage_options')) {
95
96 return;
97
98 }
99
100 ?>
101 <div class="wrap">
102 <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
103 <?php if (isset($_GET['settings-updated']) && 'true' === sanitize_text_field(wp_unslash($_GET['settings-updated']))) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only status flag. ?>
104 <div class="notice notice-success is-dismissible"><p><?php esc_html_e('Settings saved.', 'block-wp-login'); ?></p></div>
105 <?php } ?>
106 <?php if (isset($_GET['bwpl-error'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only error flag. ?>
107 <div class="notice notice-error"><?php
108 $error = sanitize_key(wp_unslash($_GET['bwpl-error'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only error flag.
109
110 if ('requirements' === $error) {
111
112 $requirements = self::bwpl_requirements_check();
113 echo is_wp_error($requirements) ? wp_kses_post(self::bwpl_requirements_message($requirements)) : '<p>' . esc_html__('The server requirements could not be confirmed. Please try again.', 'block-wp-login') . '</p>';
114
115 } elseif ('locked' === $error) {
116
117 echo '<p>' . esc_html__('The settings could not be saved because another Block wp-login update is in progress. Please try again.', 'block-wp-login') . '</p>';
118
119 }
120 ?></div>
121 <?php } ?>
122 <?php $this->bwpl_settings_title(); ?>
123 <form action="<?php echo esc_url(admin_url('admin-post.php')); ?>" method="post">
124 <input type="hidden" name="action" value="bwpl_save_settings" />
125 <table class="form-table" role="presentation">
126 <tr>
127 <th scope="row"><label for="bwpl_slug"><?php esc_html_e('Private login address', 'block-wp-login'); ?></label></th>
128 <td><?php $this->bwpl_settings_html(); ?></td>
129 </tr>
130 </table>
131 <?php submit_button(); ?>
132 </form>
133 <?php if (!class_exists('Deny_All_Firewall') && !class_exists('daf_class')) { ?>
134 <h2><?php esc_html_e('Protect more than your login page', 'block-wp-login'); ?></h2>
135 <p><?php
136 echo wp_kses(
137 sprintf(
138 /* translators: link to the Deny All Firewall plugin installation search */
139 __('Block wp-login helps reduce unwanted login attempts. For broader protection against unwanted requests, take a look at %s.', 'block-wp-login'),
140 '<a href="' . esc_url(add_query_arg(array('s' => 'domainsupport%20deny%20all%20firewall', 'tab' => 'search', 'type' => 'term'), self_admin_url('plugin-install.php'))) . '">' . esc_html__('Deny All Firewall', 'block-wp-login') . '</a>'
141 ),
142 array('a' => array('href' => array()))
143 );
144 ?></p>
145 <p>
146 <a href="<?php echo esc_url(add_query_arg(array('s' => 'domainsupport%20deny%20all%20firewall', 'tab' => 'search', 'type' => 'term'), self_admin_url('plugin-install.php'))); ?>" title="<?php esc_attr_e('For broader protection against unwanted requests, take a look at Deny All Firewall.', 'block-wp-login'); ?>">
147 <img src="<?php echo esc_url(plugin_dir_url(__FILE__) . 'assets/deny-all-firewall-banner.jpg'); ?>" alt="<?php esc_attr_e('For broader protection against unwanted requests, take a look at Deny All Firewall.', 'block-wp-login'); ?>" title="<?php esc_attr_e('For broader protection against unwanted requests, take a look at Deny All Firewall.', 'block-wp-login'); ?>" style="width: 100%; max-width: 772px; height: auto;" />
148 </a>
149 </p>
150 <?php } ?>
151 </div>
152 <?php
153
154 }
155
156 function bwpl_configure_slug() {
157
158 if (!current_user_can('manage_options')) {
159
160 wp_die(esc_html__('You are not allowed to manage these settings.', 'block-wp-login'));
161
162 }
163
164 check_admin_referer('bwpl_slug_change', 'bwpl_nonce');
165
166 $requirements = self::bwpl_requirements_check();
167
168 if (is_wp_error($requirements)) {
169
170 wp_safe_redirect(add_query_arg('bwpl-error', 'requirements', admin_url('options-general.php?page=block-wp-login')));
171 exit;
172
173 }
174
175 if (isset($_POST['bwpl_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['bwpl_nonce'])), 'bwpl_slug_change') && isset($_POST['bwpl_slug']) && current_user_can('manage_options')) {
176
177 $version_lock = self::acquire_version_lock();
178
179 if (!$version_lock) {
180
181 wp_safe_redirect(add_query_arg('bwpl-error', 'locked', admin_url('options-general.php?page=block-wp-login')));
182 exit;
183
184 }
185
186 try {
187
188 $this->bwpl_new_slug = trim(sanitize_key(wp_strip_all_tags(wp_unslash($_POST['bwpl_slug']))));
189
190 if ($this->bwpl_new_slug) {
191
192 $notify = (isset($_POST['bwpl_notify']) && 'true' === $_POST['bwpl_notify']);
193 $this->bwpl_uninstall();
194 $this->bwpl_install(false, $notify, $version_lock);
195
196 } else {
197
198 if (isset($_POST['bwpl_notify']) && 'true' === $_POST['bwpl_notify']) {
199
200 $this->bwpl_send_emails(false);
201
202 }
203
204 $this->bwpl_uninstall();
205
206 }
207
208 update_option('bwpl_slug', $this->bwpl_new_slug);
209 add_filter('login_url', array($this, 'bwpl_change_login_url'), 10, 3);
210 add_filter('logout_url', array($this, 'bwpl_change_logout_url'), 10, 2);
211 add_filter('logout_redirect', array($this, 'bwpl_change_logout_redirect'), 10, 3);
212 add_filter('lostpassword_url', array($this, 'bwpl_change_lostpassword_url'), 10, 2);
213 add_filter('register_url', array($this, 'bwpl_change_register_url'));
214 add_filter('site_url', array($this, 'bwpl_change_password_reset_url'), 10, 3);
215 add_filter('network_site_url', array($this, 'bwpl_change_password_reset_url'), 10, 3);
216
217 $this->bwpl_refresh_daf_rules();
218
219 if (
220 isset($_POST['bwpl_unknown_admin']) &&
221 sanitize_text_field(wp_unslash($_POST['bwpl_unknown_admin'])) &&
222 isset($_POST['bwpl_known_ips']) &&
223 sanitize_textarea_field(wp_unslash($_POST['bwpl_known_ips']))
224 ) {
225
226 $known_ips = preg_split('/\r\n|[\r\n]/', sanitize_textarea_field(wp_unslash($_POST['bwpl_known_ips'])));
227
228 foreach ($known_ips AS $key => $known_ip) {
229
230 if (!(
231 filter_var($known_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ||
232 filter_var($known_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
233 )) {
234
235 unset($known_ips[$key]);
236
237 }
238
239 }
240
241 if ($known_ips) {
242
243 update_option('bwpl_known_ips', implode(PHP_EOL, $known_ips));
244
245 } else {
246
247 delete_option('bwpl_known_ips');
248
249 }
250
251 } else {
252
253 delete_option('bwpl_known_ips');
254
255 }
256
257 } finally {
258
259 self::release_version_lock($version_lock);
260
261 }
262
263 }
264
265
266 wp_safe_redirect(add_query_arg('settings-updated', 'true', admin_url('options-general.php?page=block-wp-login')));
267 exit;
268
269 }
270
271 public static function bwpl_activate() {
272
273 $requirements = self::bwpl_requirements_check();
274
275 if (is_wp_error($requirements)) {
276
277 wp_die(
278 wp_kses_post(self::bwpl_requirements_message($requirements)),
279 esc_html__('Block wp-login could not be activated', 'block-wp-login'),
280 array('back_link' => true)
281 );
282
283 }
284
285 }
286
287 private static function bwpl_requirements_check() {
288
289 if (!function_exists('got_mod_rewrite')) {
290
291 require_once ABSPATH . 'wp-admin/includes/misc.php';
292
293 }
294
295 $server_software = isset($_SERVER['SERVER_SOFTWARE']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_SOFTWARE'])) : '';
296 $supports_htaccess = got_mod_rewrite() || false !== stripos($server_software, 'litespeed');
297
298 /**
299 * Filters whether the web server supports Apache-style .htaccess rewrite rules.
300 *
301 * @param bool $supports_htaccess Whether compatible rewrite support was detected.
302 */
303 $supports_htaccess = (bool) apply_filters('bwpl_server_supports_htaccess', $supports_htaccess);
304 $errors = new WP_Error();
305
306 if (!$supports_htaccess) {
307
308 $errors->add('rewrite_support', __('An Apache-compatible web server with .htaccess and mod_rewrite support is required.', 'block-wp-login'));
309
310 }
311
312 $home_path = bwplCommon::get_home_path();
313 $htaccess_file = $home_path . '.htaccess';
314 $login_file = $home_path . 'wp-login.php';
315
316 if (!is_dir($home_path) || !wp_is_writable($home_path)) {
317
318 $errors->add('home_not_writable', __('The WordPress home directory must be writable so the private login file can be created.', 'block-wp-login'));
319
320 }
321
322 if (!is_file($login_file) || !is_readable($login_file)) {
323
324 $errors->add('login_not_readable', __('The standard WordPress wp-login.php file could not be found or read.', 'block-wp-login'));
325
326 }
327
328 if (file_exists($htaccess_file) && (!is_file($htaccess_file) || !wp_is_writable($htaccess_file))) {
329
330 $errors->add('htaccess_not_writable', __('The WordPress .htaccess file must be writable.', 'block-wp-login'));
331
332 }
333
334 return $errors->has_errors() ? $errors : true;
335
336 }
337
338 private static function bwpl_requirements_message($requirements) {
339
340 $message = '<p>' . esc_html__('Block wp-login cannot run on this website for the following reason(s):', 'block-wp-login') . '</p><ul>';
341
342 foreach ($requirements->get_error_messages() as $error_message) {
343
344 $message .= '<li>' . esc_html($error_message) . '</li>';
345
346 }
347
348 return $message . '</ul>';
349
350 }
351
352 function bwpl_settings_title() {
353
354 ?>
355 <p><?php esc_html_e('Choose a private login address to block direct access to wp-login.php and reduce unwanted login attempts.', 'block-wp-login'); ?></p>
356 <?php
357
358 }
359
360 function bwpl_settings_html() {
361
362 wp_nonce_field('bwpl_slug_change', 'bwpl_nonce');
363
364 $characters = 'abcdefghijklmnopqrstuvwxyz';
365 $randomString = '';
366
367 for ($i = 0; $i < 8; $i++) {
368
369 $randomString .= $characters[wp_rand(0, strlen($characters) - 1)];
370
371 }
372
373 ?>
374 <input id="bwpl_slug" name="bwpl_slug" type="text" class="regular-text code" value="<?php echo esc_attr(get_option('bwpl_slug')); ?>" />
375
376 <script type="text/javascript">
377 jQuery('#bwpl_slug').change(function() {
378 jQuery(this).val(jQuery(this).val().toLowerCase().replace(/[^a-z0-9]+/g,''));
379 if (jQuery(this).val().length !== 0) {
380 alert(<?php echo wp_json_encode(__('Your new login address will be:', 'block-wp-login') . "\n\n" . get_site_url() . '/'); ?> + jQuery(this).val() + <?php echo wp_json_encode('/' . "\n\n" . __('Save your changes, then bookmark the new address so you can find it again.', 'block-wp-login')); ?>);
381 }
382 jQuery('#bwpl_notify').prop('checked', true);
383 });
384 </script>
385
386 <p class="description"><?php esc_html_e('Enter the final part of the address using letters and numbers only. Leave this blank to use the standard wp-login.php address.', 'block-wp-login'); ?></p>
387
388 <p><a href="javascript:void(0)" class="randomlogin"><?php esc_html_e('Generate a random login address', 'block-wp-login'); ?></a></p>
389
390 <script type="text/javascript">
391 jQuery('.randomlogin').on('click',function() {
392 var result = '',
393 characters = 'abcdefghijklmnopqrstuvwxyz';
394 for (var i = 0; i < 8; i++) {
395 result += characters.charAt(Math.floor(Math.random() * characters.length));
396 }
397 jQuery('#bwpl_slug').val(result);
398 alert(<?php echo wp_json_encode(__('Your new login address will be:', 'block-wp-login') . "\n\n" . get_site_url() . '/'); ?> + result + <?php echo wp_json_encode('/' . "\n\n" . __('Save your changes, then bookmark the new address so you can find it again.', 'block-wp-login')); ?>);
399 jQuery('#bwpl_notify').prop('checked', true);
400 });
401 </script>
402
403 <p><strong><?php esc_html_e('Current login address: ', 'block-wp-login'); ?></strong><?php
404
405 if (get_option('bwpl_slug')) {
406
407 echo esc_url(get_site_url(null, (get_option('bwpl_slug') . '/')));
408
409 } else {
410
411 echo esc_url(get_site_url(null, 'wp-login.php'));
412
413 } ?></p>
414
415 <p><input id="bwpl_notify" name="bwpl_notify" type="checkbox" value="true"> <?php esc_html_e('Email all administrators when the login address changes.', 'block-wp-login'); ?></p>
416
417 <?php
418
419 $known_ips = get_option('bwpl_known_ips');
420
421 if (!$known_ips) { $known_ips = false; }
422
423 ?>
424 <p><input id="bwpl_unknown_admin" name="bwpl_unknown_admin" type="checkbox" value="true"<?php if ($known_ips) { echo ' checked'; } ?>> <?php esc_html_e('Email the site owner when an administrator signs in from an unrecognised IP address.', 'block-wp-login'); ?></p>
425 <p id="bwpl_known_ips_wrapper"<?php if (!$known_ips) { echo ' style="display: none;"'; } ?>><label for="bwpl_known_ips"><strong><?php esc_html_e('Recognised IP addresses', 'block-wp-login'); ?></strong></label><br>
426 <textarea name="bwpl_known_ips" id="bwpl_known_ips" class="large-text code" rows="5"><?php echo ($known_ips ? esc_html($known_ips) : esc_html(self::get_current_ip())); ?></textarea><br>
427 <span class="description"><?php esc_html_e('Enter one IP address per line. The site owner will not be notified when an administrator signs in from one of these addresses.', 'block-wp-login'); ?></span></p>
428 <script type="text/javascript">
429 jQuery('#bwpl_unknown_admin').on('change',function() {
430 jQuery('#bwpl_known_ips_wrapper').toggle();
431 });
432 </script>
433 <?php
434
435 }
436
437 function bwpl_send_emails($new_version) {
438
439 $blogusers = get_users('role=Administrator');
440 $admin_emails = array();
441
442 foreach ($blogusers as $user) {
443 if ($user->user_email) {
444 $this->bwpl_send_email($user->user_email, $new_version);
445 array_push($admin_emails, $user->user_email);
446 }
447 }
448
449 if (get_bloginfo('admin_email') && !in_array(get_bloginfo('admin_email'),$admin_emails)) {
450 $this->bwpl_send_email(get_bloginfo('admin_email'), $new_version);
451 }
452
453 }
454
455 function bwpl_install($new_version = false, $notify = true, $version_lock = false) {
456
457 global $wp_filesystem;
458
459 if (!$wp_filesystem) {
460
461 require_once ABSPATH . 'wp-admin/includes/file.php';
462
463 if (!WP_Filesystem()) {
464
465 return false;
466
467 }
468
469 }
470
471 if (!is_object($wp_filesystem)) {
472
473 return false;
474
475 }
476
477 $home_path = bwplCommon::get_home_path();
478 $login_file = $home_path . 'wp-login.php';
479
480 if ($wp_filesystem->exists($login_file)) {
481
482 $content = $wp_filesystem->get_contents($login_file);
483
484 if (false === $content) {
485
486 return false;
487
488 }
489
490 $content_chunks = explode('wp-login.php', $content);
491 $content = implode($this->bwpl_new_slug . '-wp-login.php', $content_chunks);
492 $new_login_file = $home_path . $this->bwpl_new_slug . '-wp-login.php';
493
494 if ((!$wp_filesystem->exists($new_login_file) && $wp_filesystem->is_writable($home_path)) || $wp_filesystem->is_writable($new_login_file)) {
495
496 if (!$wp_filesystem->put_contents($new_login_file, $content, FS_CHMOD_FILE)) {
497
498 return false;
499
500 }
501
502 } else {
503
504 return false;
505
506 }
507
508 } else {
509
510 return false;
511
512 }
513
514 $htaccess_file = $home_path . '.htaccess';
515
516 if ((!$wp_filesystem->exists($htaccess_file) && $wp_filesystem->is_writable($home_path)) || $wp_filesystem->is_writable($htaccess_file)) {
517
518 $markerdata = $wp_filesystem->exists($htaccess_file) ? $wp_filesystem->get_contents($htaccess_file) : '';
519
520 if (false !== $markerdata) {
521
522 $markerdata = explode("\n", $markerdata);
523 $found = false;
524 $newdata = '';
525
526 foreach ($markerdata as $line) {
527
528 if (!$found) {
529
530 $newdata .= "# BEGIN BlockWPLogin\n";
531 $newdata .= "<IfModule mod_rewrite.c>\n";
532 $newdata .= "RewriteEngine On\n";
533
534 $newdata .= "RewriteCond %{QUERY_STRING} \"^action=postpass$\"
535 RewriteRule \"^" . str_replace('.', '\.', substr(site_url('wp-login.php', 'relative'), 1)) . "$\" " . str_replace('.', '\.', site_url($this->bwpl_new_slug . '-wp-login.php', 'relative')) . " [QSA,L]
536 ";
537
538 $newdata .= "RewriteRule \"^" . str_replace('.', '\.', substr(site_url('wp-login.php', 'relative'), 1)) . "\" - [F]\n";
539 $newdata .= "RewriteRule \"^$this->bwpl_new_slug\\/?$\" " . str_replace('.', '\.', site_url($this->bwpl_new_slug . '-wp-login.php', 'relative')) . " [R=301,QSA,L]\n";
540 $newdata .= "</IfModule>\n";
541 $newdata .= "# END BlockWPLogin\n\n";
542 $newdata .= "$line\n";
543 $found = true;
544
545 } else {
546
547 $newdata .= "$line\n";
548
549 }
550
551 }
552
553 if (!$wp_filesystem->put_contents($htaccess_file, $newdata, FS_CHMOD_FILE)) {
554
555 return false;
556
557 }
558
559 } else {
560
561 return false;
562
563 }
564
565 } else {
566
567 return false;
568
569 }
570
571 if (!self::record_installed_wordpress_version(get_bloginfo('version'), $version_lock)) {
572
573 return false;
574
575 }
576
577 if ($new_version || $notify) {
578
579 $this->bwpl_send_emails($new_version);
580
581 }
582
583 return true;
584
585 }
586
587 function bwpl_setup_admin_notice() {
588
589 $screen = get_current_screen();
590
591 if ($screen && 'settings_page_block-wp-login' === $screen->id) {
592
593 return;
594
595 }
596
597 ?>
598 <div class="notice notice-success">
599 <p><?php
600 /* translators: <strong> HTML tags */
601 echo wp_kses(sprintf(__('%1$sBlock wp-login%2$s activated. ', 'block-wp-login'),'<strong>','</strong>'), 'post'); ?><a href="<?php echo esc_url(admin_url('options-general.php?page=block-wp-login')); ?>"><?php esc_html_e('Configure the plugin here.', 'block-wp-login'); ?></a></p>
602 </div>
603 <?php
604
605 }
606
607 /**
608 * Allow common plugin notices on this plugin's settings screen.
609 */
610 public function bwpl_admin_notice_pages($allowed_pages) {
611
612 $screen = get_current_screen();
613
614 if ($screen && 'settings_page_block-wp-login' === $screen->id) {
615
616 $allowed_pages[] = 'options-general.php';
617
618 }
619
620 return array_unique($allowed_pages);
621
622 }
623
624 function bwpl_uninstall() {
625
626 global $wp_filesystem;
627
628 if (!$wp_filesystem) {
629
630 require_once ABSPATH . 'wp-admin/includes/file.php';
631
632 if (!WP_Filesystem()) {
633
634 return false;
635
636 }
637
638 }
639
640 if (!is_object($wp_filesystem)) {
641
642 return false;
643
644 }
645
646 $home_path = bwplCommon::get_home_path();
647 $htaccess_file = $home_path . '.htaccess';
648
649 if ($wp_filesystem->exists($htaccess_file)) {
650
651 if (!$wp_filesystem->is_writable($htaccess_file)) {
652
653 return false;
654
655 }
656
657 $markerdata = $wp_filesystem->get_contents($htaccess_file);
658
659 if (false === $markerdata) {
660
661 return false;
662
663 }
664
665 $markerdata = explode("\n", $markerdata);
666 $found = false;
667 $blank_line = false;
668 $newdata = '';
669
670 foreach ($markerdata as $line) {
671
672 if ($blank_line && !$line) {
673
674 $found = true;
675
676 }
677
678 if ($blank_line && $line) {
679
680 $found = false;
681
682 }
683
684 if ($line) {
685
686 $blank_line = false;
687
688 } else {
689
690 $blank_line = true;
691
692 }
693
694 if ('# BEGIN BlockWPLogin' === $line) {
695
696 $found = true;
697
698 }
699
700 if (!$found) {
701
702 $newdata .= "$line\n";
703
704 }
705
706 if ('# END BlockWPLogin' === $line) {
707
708 $found = false;
709
710 }
711
712 }
713
714 if (!$wp_filesystem->put_contents($htaccess_file, $newdata, FS_CHMOD_FILE)) {
715
716 return false;
717
718 }
719
720 }
721
722 add_filter('logout_url', array($this, 'bwpl_reset_logout_url'));
723 add_filter('logout_redirect', array($this, 'bwpl_reset_logout_url'));
724 add_filter('lostpassword_url', array($this, 'bwpl_reset_logout_url'));
725
726 $slug = get_option('bwpl_slug');
727 $login_file = $home_path . $slug . '-wp-login.php';
728
729 if ($slug && $wp_filesystem->exists($login_file)) {
730
731 if (!$wp_filesystem->is_writable($login_file) || !$wp_filesystem->delete($login_file)) {
732
733 return false;
734
735 }
736
737 }
738
739 update_option('bwpl_slug', '');
740
741 $this->bwpl_refresh_daf_rules();
742
743 return true;
744
745 }
746
747 function bwpl_change_login_url($login_url, $redirect, $force_reauth) {
748
749 if (
750 function_exists('is_user_logged_in') &&
751 (
752 is_user_logged_in() ||
753 did_action('password_reset') ||
754 $this->bwpl_is_secret_login_request()
755 )
756 ) {
757
758 $login_url = str_replace('/wp-login.php', '/' . get_option('bwpl_slug') . '-wp-login.php', $login_url);
759
760 }
761
762 return $login_url;
763
764 }
765
766 function bwpl_change_logout_url($logout_url, $redirect) {
767
768 if (
769 (function_exists('is_user_logged_in') && is_user_logged_in()) ||
770 $this->bwpl_is_secret_login_request()
771 ) {
772
773 $logout_url = str_replace('/wp-login.php', '/' . get_option('bwpl_slug') . '-wp-login.php', $logout_url);
774
775 }
776
777 return $logout_url;
778
779 }
780
781 function bwpl_change_lostpassword_url($lostpassword_url, $redirect) {
782
783 if ($this->bwpl_is_secret_login_request()) {
784
785 $lostpassword_url = str_replace('/wp-login.php', '/' . get_option('bwpl_slug') . '-wp-login.php', $lostpassword_url);
786
787 }
788
789 return $lostpassword_url;
790
791 }
792
793 function bwpl_change_register_url($register_url) {
794
795 if ($this->bwpl_is_secret_login_request()) {
796
797 $register_url = str_replace('/wp-login.php', '/' . get_option('bwpl_slug') . '-wp-login.php', $register_url);
798
799 }
800
801 return $register_url;
802
803 }
804
805 function bwpl_change_password_reset_url($url, $path, $scheme) {
806
807 if (
808 !is_string($path) ||
809 1 !== preg_match('/\Awp-login\.php\?login=[^&]+&key=[^&]+&action=rp\z/D', $path)
810 ) {
811
812 return $url;
813
814 }
815
816 return preg_replace(
817 '/\/wp-login\.php(?=\?|$)/',
818 '/' . get_option('bwpl_slug') . '-wp-login.php',
819 $url,
820 1
821 );
822
823 }
824
825 function bwpl_change_login_redirect($location, $status) {
826
827 $path = wp_parse_url($location, PHP_URL_PATH);
828 $query = wp_parse_url($location, PHP_URL_QUERY);
829
830 if (
831 !did_action('wp_login') ||
832 !is_string($path) ||
833 '/wp-login.php' !== substr($path, -13) ||
834 !is_string($query)
835 ) {
836
837 return $location;
838
839 }
840
841 preg_match_all('/(?:^|&)action=([^&]*)/', $query, $action_matches);
842
843 if (
844 1 === count($action_matches[1]) &&
845 'confirm_admin_email' === rawurldecode($action_matches[1][0])
846 ) {
847
848 $location = preg_replace(
849 '/\/wp-login\.php(?=\?|$)/',
850 '/' . get_option('bwpl_slug') . '-wp-login.php',
851 $location,
852 1
853 );
854
855 }
856
857 return $location;
858
859 }
860
861 function bwpl_change_logout_redirect($redirect_to, $requested_redirect_to, $user) {
862
863 if ($user->ID) {
864
865 $redirect_to = str_replace('/wp-login.php', '/' . get_option('bwpl_slug') . '-wp-login.php', $redirect_to);
866
867 }
868
869 return $redirect_to;
870
871 }
872
873 private function bwpl_is_secret_login_request() {
874
875 if (empty($_SERVER['SCRIPT_FILENAME']) || !is_string($_SERVER['SCRIPT_FILENAME'])) {
876
877 return false;
878
879 }
880
881 $script_filename = sanitize_text_field(wp_unslash($_SERVER['SCRIPT_FILENAME']));
882 $slug = get_option('bwpl_slug');
883
884 return $slug && $slug . '-wp-login.php' === basename($script_filename);
885
886 }
887
888 function bwpl_reset_logout_url($logout_url) {
889
890 $logout_url = str_replace('/' . get_option('bwpl_slug') . '-wp-login.php', '/wp-login.php', $logout_url);
891
892 return $logout_url;
893
894 }
895
896 function bwpl_send_email($recipient, $new_version) {
897
898 if ($new_version) {
899
900 $message = __('A recent WordPress core update has been detected and “Block wp-login” has been re-installed. Here is a reminder of your login address:', 'block-wp-login');
901
902 } else {
903
904 $message = __('Your WordPress login address has been changed:', 'block-wp-login');
905
906 }
907
908 $message .= "\r\n\r\n";
909
910 if ($this->bwpl_new_slug) {
911
912 $message .= get_site_url(null, ($this->bwpl_new_slug . '/')) . "\r\n\r\n";
913
914 } else {
915
916 $message .= get_site_url(null, 'wp-login.php') . "\r\n\r\n";
917
918 }
919
920 $message .= __('Keep this link handy! Bookmarking it is the best way to ensure you never get locked out.', 'block-wp-login');
921 $message .= "\r\n\r\n---\r\n\r\n";
922 $message .= __('Does your site need a glow-up?', 'block-wp-login');
923 $message .= "\r\n";
924 $message .= __('Running slowly?', 'block-wp-login');
925 $message .= "\r\n";
926 $message .= __('Want new features?', 'block-wp-login');
927 $message .= "\r\n\r\n";
928 $message .= __('See how we can help', 'block-wp-login');
929 $message .= ' https://webd.uk';
930
931 if (is_multisite()) {
932
933 $blogname = get_network()->site_name;
934
935 } else {
936
937 $blogname = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES);
938
939 }
940
941 if ($new_version) {
942
943 /* translators: website title */
944 $title = sprintf(__('[%s] WordPress Login Reminder', 'block-wp-login'), $blogname);
945
946 } else {
947
948 /* translators: website title */
949 $title = sprintf(__('[%s] WordPress Login Changed', 'block-wp-login'), $blogname);
950
951 }
952
953 if (!wp_mail($recipient, wp_specialchars_decode($title), $message)) {
954
955 add_action('admin_notices', array($this, 'bwpl_admin_notice_email_html'));
956
957 }
958
959 }
960
961 function bwpl_admin_notice_email_html() {
962
963 ?>
964 <div class="notice notice-error">
965 <p><?php
966 /* translators: <strong> HTML tags */
967 echo wp_kses(sprintf(__('%1$sBlock wp-login%2$s activated email could not be sent.', 'block-wp-login'),'<strong>','</strong>'), 'post'); ?></p>
968 </div>
969 <?php
970
971 }
972
973 function bwpl_new_wordpress_version() {
974
975 if (self::is_server_overloaded()) {
976
977 return;
978
979 }
980
981 $version_lock = self::acquire_version_lock();
982
983 if ($version_lock) {
984
985 try {
986
987 $installed_version = self::read_locked_wordpress_version($version_lock);
988
989 if (!$installed_version) {
990
991 $installed_version = self::legacy_wordpress_version();
992
993 if ($installed_version === get_bloginfo('version')) {
994
995 self::record_installed_wordpress_version($installed_version, $version_lock);
996
997 }
998
999 }
1000
1001 if ($installed_version !== get_bloginfo('version')) {
1002
1003 $bwpl_old_slug = get_option('bwpl_slug');
1004
1005 if ($this->bwpl_uninstall()) {
1006
1007 $this->bwpl_new_slug = $bwpl_old_slug;
1008 if ($this->bwpl_install(true, true, $version_lock)) {
1009
1010 update_option('bwpl_slug', $bwpl_old_slug);
1011
1012 }
1013
1014 $this->bwpl_refresh_daf_rules();
1015
1016 }
1017
1018 }
1019
1020 } finally {
1021
1022 self::release_version_lock($version_lock);
1023
1024 }
1025
1026 }
1027
1028 }
1029
1030 public static function wp_login($user_login, $user) {
1031
1032 if (user_can($user->ID, 'manage_options')) {
1033
1034 $ip = self::get_current_ip();
1035
1036 if ($ip) {
1037
1038 $known_ips = get_option('bwpl_known_ips');
1039
1040 if ($known_ips) {
1041
1042 $known_ips = preg_split('/\r\n|[\r\n]/', $known_ips);
1043
1044 if (!in_array($ip, $known_ips, true)) {
1045
1046 if (is_multisite()) {
1047
1048 $blogname = get_network()->site_name;
1049
1050 } else {
1051
1052 $blogname = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES);
1053
1054 }
1055
1056 /* translators: website title */
1057 $title = sprintf(__('[%s] WordPress Login Alert', 'block-wp-login'), $blogname);
1058
1059 $message = __('An administrator with an un-recognised IP address has signed in:', 'block-wp-login') . "\r\n\r\n";
1060 $message .= __('User: ', 'block-wp-login') . $user_login . "\r\n\r\n";
1061 $message .= __('IP: ', 'block-wp-login') . $ip . "\r\n\r\n";
1062 $message .= __('Contact us if you are having trouble with WordPress https://webd.uk', 'block-wp-login') . "\r\n\r\n";
1063 $message .= __('If you like our plugin please leave a short review: https://wordpress.org/support/plugin/block-wp-login/reviews/#new-post', 'block-wp-login') . "\r\n\r\n";
1064
1065 wp_mail(get_bloginfo('admin_email'), wp_specialchars_decode($title), $message);
1066
1067 }
1068
1069 }
1070
1071 }
1072
1073 }
1074
1075 }
1076
1077 /**
1078 * Ask Deny All Firewall to rebuild its rules.
1079 *
1080 * Deny All Firewall 2.0 provides a public action for integrations. Keep
1081 * the legacy calls as a compatibility fallback for older releases.
1082 */
1083 private function bwpl_refresh_daf_rules() {
1084
1085 $daf_options = get_option('daf_options');
1086
1087 if (empty($daf_options['enable_firewall'])) {
1088
1089 return;
1090
1091 }
1092
1093 if (has_action('deny_all_firewall_refresh_rules')) {
1094
1095 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
1096 do_action('deny_all_firewall_refresh_rules');
1097 return;
1098
1099 }
1100
1101 global $daf;
1102
1103 if (
1104 is_object($daf) &&
1105 is_callable(array($daf, 'daf_create_htaccess')) &&
1106 is_callable(array($daf, 'daf_remove_rules')) &&
1107 is_callable(array($daf, 'daf_inject_rules'))
1108 ) {
1109
1110 $daf_htaccess = $daf->daf_create_htaccess();
1111
1112 if ($daf_htaccess && $daf->daf_remove_rules()) {
1113
1114 $daf->daf_inject_rules($daf_htaccess);
1115
1116 }
1117
1118 }
1119
1120 }
1121
1122 /**
1123 * Prompt administrators to install the available Deny All Firewall 2.0 update.
1124 */
1125 public function bwpl_daf_upgrade_notice() {
1126
1127 $screen = get_current_screen();
1128 $notice_screens = array('plugins', 'update-core', 'settings_page_block-wp-login');
1129
1130 if (
1131 !$screen ||
1132 !in_array($screen->id, $notice_screens, true) ||
1133 !current_user_can('update_plugins') ||
1134 !class_exists('daf_class') ||
1135 has_action('deny_all_firewall_refresh_rules')
1136 ) {
1137
1138 return;
1139
1140 }
1141
1142 $plugin_file = 'deny-all-firewall/deny-all-firewall.php';
1143 $updates = get_site_transient('update_plugins');
1144
1145 if (
1146 !is_object($updates) ||
1147 empty($updates->response[$plugin_file]) ||
1148 empty($updates->response[$plugin_file]->new_version) ||
1149 version_compare($updates->response[$plugin_file]->new_version, '2.0', '<')
1150 ) {
1151
1152 return;
1153
1154 }
1155
1156 $update_url = wp_nonce_url(
1157 self_admin_url('update.php?action=upgrade-plugin&plugin=' . rawurlencode($plugin_file)),
1158 'upgrade-plugin_' . $plugin_file
1159 );
1160
1161 ?>
1162 <div class="notice notice-warning">
1163 <p><strong><?php esc_html_e('Block wp-login', 'block-wp-login'); ?></strong></p>
1164 <p><?php esc_html_e('Deny All Firewall 2.0 or newer is available. Please update Deny All Firewall so Block wp-login can refresh its firewall rules through the supported integration.', 'block-wp-login'); ?></p>
1165 <p><a class="button button-primary" href="<?php echo esc_url($update_url); ?>"><?php esc_html_e('Update Deny All Firewall', 'block-wp-login'); ?></a></p>
1166 </div>
1167 <?php
1168
1169 }
1170
1171 private static function get_current_ip() {
1172
1173 $ip = false;
1174
1175 if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
1176
1177 $ip = filter_var(wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP']), FILTER_VALIDATE_IP);
1178
1179 } elseif (isset($_SERVER['REMOTE_ADDR'])) {
1180
1181 $ip = filter_var(wp_unslash($_SERVER['REMOTE_ADDR']), FILTER_VALIDATE_IP);
1182
1183 }
1184
1185
1186 if (
1187 $ip &&
1188 !(
1189 filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ||
1190 filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
1191 )
1192 ) {
1193
1194 $ip = false;
1195
1196 }
1197
1198 return $ip;
1199
1200 }
1201
1202 /**
1203 * Determine whether server load is too high for filesystem maintenance.
1204 *
1205 * Failure to detect either load or CPU capacity is treated as normal
1206 * load so the reinstall can proceed as before.
1207 */
1208 private static function is_server_overloaded() {
1209
1210 if (!function_exists('sys_getloadavg')) {
1211
1212 return false;
1213
1214 }
1215
1216 $load_average = sys_getloadavg();
1217
1218 if (!is_array($load_average) || !isset($load_average[0])) {
1219
1220 return false;
1221
1222 }
1223
1224 $cores_available = 0;
1225
1226 $cpu_directories = glob('/sys/devices/system/cpu/cpu[0-9]*', GLOB_ONLYDIR);
1227
1228 if (is_array($cpu_directories) && $cpu_directories) {
1229
1230 $cores_available = count($cpu_directories);
1231
1232 } elseif (false !== getenv('NUMBER_OF_PROCESSORS')) {
1233
1234 $cores_available = absint(getenv('NUMBER_OF_PROCESSORS'));
1235
1236 }
1237
1238 if (!$cores_available) {
1239
1240 return false;
1241
1242 }
1243
1244 return ((float) $load_average[0] / $cores_available) > 1;
1245
1246 }
1247
1248 private static function installed_wordpress_version() {
1249
1250 $state_file = ABSPATH . '.block-wp-login-version';
1251
1252 if (is_readable($state_file) && wp_is_writable($state_file)) {
1253
1254 $version = file_get_contents($state_file);
1255
1256 return false === $version ? false : trim($version);
1257
1258 }
1259
1260 if (file_exists($state_file)) {
1261
1262 return self::legacy_wordpress_version();
1263
1264 }
1265
1266 if (wp_is_writable(ABSPATH)) {
1267
1268 return false;
1269
1270 }
1271
1272 return self::legacy_wordpress_version();
1273
1274 }
1275
1276 private static function acquire_version_lock() {
1277
1278 // WP_Filesystem does not expose the stream handle required for an atomic flock().
1279 $handle = @fopen(ABSPATH . '.block-wp-login-version', 'c+'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
1280
1281 if (is_resource($handle)) {
1282
1283 if (flock($handle, LOCK_EX | LOCK_NB)) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_flock
1284
1285 return array('type' => 'file', 'handle' => $handle);
1286
1287 }
1288
1289 fclose($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
1290 return false;
1291
1292 }
1293
1294 global $wpdb;
1295
1296 // Fall back to a database advisory lock when the state file cannot be opened.
1297 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1298 if ($wpdb->get_var($wpdb->prepare("SELECT GET_LOCK(%s, %d)", 'bwpl_lock', 0))) {
1299
1300 return array('type' => 'database');
1301
1302 }
1303
1304 return false;
1305
1306 }
1307
1308 private static function release_version_lock($version_lock) {
1309
1310 if ('file' === $version_lock['type']) {
1311
1312 flock($version_lock['handle'], LOCK_UN); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_flock
1313 fclose($version_lock['handle']); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
1314 return;
1315
1316 }
1317
1318 global $wpdb;
1319
1320 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1321 $wpdb->get_var($wpdb->prepare("SELECT RELEASE_LOCK(%s)", 'bwpl_lock'));
1322
1323 }
1324
1325 private static function read_locked_wordpress_version($version_lock) {
1326
1327 if ('file' === $version_lock['type']) {
1328
1329 rewind($version_lock['handle']);
1330 $version = stream_get_contents($version_lock['handle']);
1331
1332 return false === $version ? false : trim($version);
1333
1334 }
1335
1336 return self::legacy_wordpress_version();
1337
1338 }
1339
1340 private static function legacy_wordpress_version() {
1341
1342 global $wpdb;
1343
1344 // Read the row directly because a persistent object cache may hold a stale option value.
1345 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1346 $version = $wpdb->get_var($wpdb->prepare("SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", 'bwpl_wp_version'));
1347
1348 return is_string($version) ? $version : false;
1349
1350 }
1351
1352 private static function record_installed_wordpress_version($version, $version_lock) {
1353
1354 if ($version_lock && 'file' === $version_lock['type']) {
1355
1356 rewind($version_lock['handle']);
1357
1358 if (
1359 ftruncate($version_lock['handle'], 0) &&
1360 strlen($version) === fwrite($version_lock['handle'], $version) && // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
1361 fflush($version_lock['handle'])
1362 ) {
1363
1364 self::record_installed_wordpress_version($version, false);
1365 return true;
1366
1367 }
1368
1369 return false;
1370
1371 }
1372
1373 global $wpdb;
1374
1375 // Keep the fallback current without routing the write through a persistent object cache.
1376 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1377 $result = $wpdb->query($wpdb->prepare("INSERT INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, 'yes') ON DUPLICATE KEY UPDATE option_value = VALUES(option_value)", 'bwpl_wp_version', $version));
1378 wp_cache_delete('bwpl_wp_version', 'options');
1379 wp_cache_delete('alloptions', 'options');
1380
1381 return false !== $result;
1382
1383 }
1384
1385 }
1386
1387 if (!class_exists('bwplCommon')) {
1388
1389 require_once(dirname(__FILE__) . '/includes/class-bwpl-common.php');
1390
1391 }
1392
1393 (new bwpl_class())->bwpl_load();
1394
1395 }
1396