PluginProbe
Patchstack – WordPress & Plugins Security / 2.2.10
Patchstack – WordPress & Plugins Security v2.2.10
2.3.7 trunk 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.17 2.1.18 2.1.19 2.1.2 2.1.20 2.1.21 2.1.22 2.1.23 2.1.24 2.1.25 2.1.3 2.1.4 2.1.5 2.1.6 All 49 releases
← All changes | includes/activation.php +67 -457 trunk2.2.10 View file →
@@ -25,11 +25,9 @@
25 25 * @return void
26 26 */
27 27 public function __construct( $core ) {
28 28 parent::__construct( $core );
29 -
30 29 add_action( 'activated_plugin', [ $this, 'redirect_activation' ], 10, 2 );
31 - add_action( 'updated_option', [ $this, 'updated_option' ], 10, 3 );
32 30 }
33 31
34 32 /**
35 33 * Redirect the user to our settings page after plugin activation.
@@ -42,21 +40,10 @@
42 40 if ( defined( 'WP_CLI' ) && WP_CLI ) {
43 41 return;
44 42 }
45 43
46 - // Only if it's the current plugin.
47 - if ( $plugin == $this->plugin->basename && ! isset( $_REQUEST['_ajax_nonce'] ) ) {
44 + if ( $plugin == $this->plugin->basename ) {
48 45
49 - // If plugin bulk activate through wp-admin, we ignore the redirect if it's more than 1 plugin.
50 - if ( isset( $_POST['checked'] ) && is_array( $_POST['checked'] ) && count( $_POST['checked'] ) > 1 ) {
51 - return;
52 - }
53 -
54 - // If the plugin is already connected or API activated, no need to redirect again.
55 - if ( $this->license_is_active() || $this->is_connected() ) {
56 - return;
57 - }
58 -
59 46 // Determine if secret token was set, if so, sync with API.
60 47 $attemptAuto = false;
61 48 $secretToken = get_option( 'patchstack_activation_secret', '' );
62 49 if ( ! empty( $secretToken ) ) {
@@ -64,11 +51,11 @@
64 51 }
65 52
66 53 // In case of multisite, we want to redirect the user to a different page.
67 54 if ( $network_activation ) {
68 - wp_safe_redirect( network_admin_url( 'admin.php?page=patchstack-multisite-settings&tab=multisite&ps_activated=1' . ($attemptAuto ? '&ps_autoa=1' : '' ) ) );
55 + wp_safe_redirect( network_admin_url( 'admin.php?page=patchstack-multisite-settings&tab=multisite&ps_activated=1' . ($attemptAuto ? '&ps_autoa=1' : '') ) );
69 56 } else {
70 - wp_safe_redirect( admin_url( 'admin.php?page=' . $this->plugin->name . '&ps_activated=1' . ($attemptAuto ? '&ps_autoa=1' : '' ) ) );
57 + wp_safe_redirect( admin_url( 'admin.php?page=' . $this->plugin->name . '&ps_activated=1' . ($attemptAuto ? '&ps_autoa=1' : '') ) );
71 58 }
72 59 exit;
73 60 }
74 61 }
@@ -105,16 +92,16 @@
105 92 );
106 93
107 94 // Check if we can access the API.
108 95 if ( is_wp_error( $response ) ) {
109 - $this->activation_errors[] = 'We were unable to contact our API server. Please contact your host and ask them to make sure that outgoing connections to api.patchstack.com are not blocked.<br />Additional error message to give to your host: ' . $response->get_error_message();
96 + $this->activation_errors[] = 'We were unable to contact our API server. Please contact your host and ask them to make sure that outgoing connections to api.webarxsecurity.com and api.patchstack.com are not blocked.<br />Additional error message to give to your host: ' . $response->get_error_message();
110 97 return false;
111 98 }
112 99
113 100 // Do checks for required classes / functions or similar.
114 101 // Add detailed messages to $this->activation_errors array.
115 - if ( version_compare( phpversion(), '5.6.0', '<' ) ) {
116 - $this->activation_errors[] = 'Please update the PHP version on your host to at least 5.6.0. Ask your host if you do not know what this means.';
102 + if ( version_compare( phpversion(), '5.3.0', '<' ) ) {
103 + $this->activation_errors[] = 'Please update the PHP version on your host to at least 5.3.0. Ask your host if you do not know what this means.';
117 104 return false;
118 105 }
119 106
120 107 global $wp_version;
@@ -171,9 +158,9 @@
171 158
172 159 // Deactivate the plugin.
173 160 include_once ABSPATH . 'wp-admin/includes/plugin.php';
174 161 deactivate_plugins( [ 'webarx/webarx.php' ] );
175 - update_option( 'patchstack_license_free', '0', true );
162 + update_option( 'patchstack_license_free', '0' );
176 163 }
177 164
178 165 // Make sure any rewrite functionality has been loaded.
179 166 $this->migrate();
@@ -188,9 +175,9 @@
188 175 } elseif ( get_option( 'patchstack_clientid', false ) != false && get_option( 'patchstack_secretkey', false ) != false ) {
189 176 $this->alter_license( get_option( 'patchstack_clientid' ), $this->get_secret_key(), 'activate' );
190 177 } else {
191 178 $sendSecret = true;
192 - update_option( 'patchstack_license_free', '1', true );
179 + update_option( 'patchstack_license_free', '1' );
193 180 }
194 181
195 182 // Update firewall status after activating plugin
196 183 $api = new P_Api( $core );
@@ -211,11 +198,14 @@
211 198 do_action( 'patchstack_post_firewall_rules' );
212 199 do_action( 'patchstack_post_dynamic_firewall_rules' );
213 200 }
214 201
202 + // One time actions should be placed here.
203 + $this->plugin->hardening->delete_readme();
204 +
215 205 // Try to create the mu-plugins folder/file.
216 206 // No need to do this if it already exists.
217 - if ( file_exists( WPMU_PLUGIN_DIR . '/patchstack.php' ) || file_exists( WPMU_PLUGIN_DIR . '/_patchstack.php' ) || ( defined( 'PS_DISABLE_MU' ) && PS_DISABLE_MU ) ) {
207 + if ( file_exists( WPMU_PLUGIN_DIR . '/patchstack.php' ) || file_exists( WPMU_PLUGIN_DIR . '/_patchstack.php' )) {
218 208 return;
219 209 }
220 210
221 211 // The mu-plugin does not exist, try to create it.
@@ -243,8 +233,51 @@
243 233 }
244 234 }
245 235
246 236 /**
237 + * Used to activate an individual license on multisite/network.
238 + *
239 + * @param object $site
240 + * @param array $license
241 + * @return void
242 + */
243 + public function activate_multisite_license( $site, $license ) {
244 + // Build the Patchstack tables on the site.
245 + $this->migrate( null, $site->id );
246 +
247 + // Add the options to given site.
248 + foreach ( $this->plugin->admin_options->options as $name => $value ) {
249 + add_blog_option( $site->id, $name, $value );
250 + }
251 +
252 + // Set the client id and secret key.
253 + update_blog_option( $site->id, 'patchstack_clientid', $license['id'] );
254 + $enc = $this->get_secret_key( $license['secret'] );
255 + update_blog_option( $site->id, 'patchstack_secretkey', $enc['cipher'] );
256 + update_blog_option( $site->id, 'patchstack_secretkey_nonce', $enc['nonce'] );
257 +
258 + $this->plugin->api->blog_id = $site->id;
259 +
260 + // Activate the license and update firewall status after activating the plugin.
261 + $token = $this->plugin->api->get_access_token( $license['id'], $license['secret'], true );
262 + if ( ! empty( $token ) ) {
263 + $this->plugin->api->update_firewall_status( [ 'status' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ] );
264 + $this->plugin->api->update_url( [ 'plugin_url' => get_blog_option( $site->id, 'siteurl' ) ] );
265 +
266 + // If we have an access token, tell our API that the firewall is activated
267 + // and the current URL of the site.
268 + update_blog_option( $site->id, 'patchstack_license_activated', '1' );
269 + $this->plugin->api->update_license_status();
270 +
271 + // This will trigger the software synchronization action.
272 + wp_remote_get( get_site_url( $site->id ), [ 'sslverify' => false ] );
273 + }
274 +
275 + // Make sure to switch back to the current blog id.
276 + $this->plugin->api->blog_id = get_current_blog_id();
277 + }
278 +
279 + /**
247 280 * Build the required Patchstack tables.
248 281 *
249 282 * @param null|string $ver The version to upgrade to.
250 283 * @param null|integer $site_id The blog id to perform the upgrades on.
@@ -305,9 +338,9 @@
305 338 //
306 339 }
307 340
308 341 // Clear all Patchstack scheduled tasks.
309 - $tasks = [ 'patchstack_zip_backup', 'patchstack_send_software_data', 'patchstack_send_hacker_logs', 'patchstack_send_visitor_logs', 'patchstack_send_event_logs', 'patchstack_reset_blocked_attacks', 'patchstack_post_firewall_rules', 'patchstack_post_firewall_htaccess_rules', 'patchstack_post_dynamic_firewall_rules', 'patchstack_update_license_status', 'patchstack_update_plugins', 'patchstack_send_ping', 'patchstack_check_env', 'puc_cron_check_updates-webarx' ];
342 + $tasks = [ 'patchstack_zip_backup', 'patchstack_send_software_data', 'patchstack_send_hacker_logs', 'patchstack_send_visitor_logs', 'patchstack_send_event_logs', 'patchstack_reset_blocked_attacks', 'patchstack_post_firewall_rules', 'patchstack_post_firewall_htaccess_rules', 'patchstack_post_dynamic_firewall_rules', 'patchstack_update_license_status', 'patchstack_update_plugins', 'patchstack_send_ping', 'puc_cron_check_updates-webarx' ];
310 343 foreach ( $tasks as $task ) {
311 344 wp_clear_scheduled_hook( $task );
312 345 }
313 346
@@ -312,9 +345,8 @@
312 345 }
313 346
314 347 // Cleanup the .htaccess file.
315 348 $this->plugin->htaccess->cleanup_htaccess_file();
316 - $this->auto_prepend_removal();
317 349
318 350 // Remove the mu-plugin file if it exists.
319 351 foreach (['patchstack.php', '_patchstack.php'] as $file) {
320 352 if ( file_exists( WPMU_PLUGIN_DIR . '/' . $file )) {
@@ -331,10 +363,12 @@
331 363 * @param string $action
332 364 * @return array
333 365 */
334 366 public function alter_license( $id, $secret, $action ) {
335 - // Set default options in case they have not been set yet.
336 - $this->plugin->admin_options->settings_init();
367 + // Set the default option values if calling through CLI.
368 + if ( defined( 'WP_CLI' ) && WP_CLI) {
369 + $this->plugin->admin_options->settings_init();
370 + }
337 371
338 372 // Store current keys in tmp variable so in case it fails, we can set it back.
339 373 $tmp_id = get_option( 'patchstack_clientid' );
340 374 $tmp_key = $this->get_secret_key();
@@ -353,38 +387,29 @@
353 387 $this->set_secret_key( $tmp_key );
354 388
355 389 return [
356 390 'result' => 'error',
357 - 'body' => json_encode($this->plugin->api->message),
358 391 'message' => 'Cannot activate license!',
359 392 ];
360 393 }
361 394
362 - // Successfully activated.
363 - update_option( 'patchstack_license_activated', '1', true );
364 -
365 - // Update license status and fetch policy settings.
366 - $fetchPolicy = (int) get_option( 'patchstack_last_license_check', 0 ) == 0;
367 - $this->plugin->api->update_license_status( $fetchPolicy );
368 -
369 - // Perform post-activation actions, incl. access token retrieval.
395 + // If we have an access token, tell our API that the firewall is activated
396 + // and the current URL of the site.
397 + update_option( 'patchstack_license_activated', '1' );
398 + $this->plugin->api->update_license_status();
370 399 $token = $this->plugin->api->get_access_token();
371 400 if ( ! empty( $token ) ) {
372 -
373 - // Immediately send software data to our server to set firewall as enabled.
374 - // Also immediately download the whitelist file and the firewall rules.
375 401 do_action( 'patchstack_send_software_data' );
376 402 if ( get_option( 'patchstack_license_free', 0 ) != 1 ) {
377 - update_option( 'patchstack_basic_firewall', 1, true );
403 + update_option( 'patchstack_basic_firewall', 1 );
378 404 do_action( 'patchstack_post_firewall_rules' );
379 405 do_action( 'patchstack_post_dynamic_firewall_rules' );
380 406 $this->header();
381 407 }
382 408
383 - // Update firewall status, URL and ping API.
384 409 $this->plugin->api->update_firewall_status( [ 'status' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ] );
385 410 $this->plugin->api->update_url( [ 'plugin_url' => get_option( 'siteurl' ) ] );
386 - $this->auto_prepend_injection();
411 + $this->plugin->api->ping();
387 412 }
388 413
389 414 return [
390 415 'result' => 'success',
@@ -395,13 +420,8 @@
395 420 // Deactivate the license.
396 421 if ( $action == 'deactivate' ) {
397 422 update_option( 'patchstack_api_token', '' );
398 423 update_option( 'patchstack_license_activated', '0' );
399 - update_option( 'patchstack_clientid', '' );
400 - update_option( 'patchstack_secretkey', '' );
401 - update_option( 'patchstack_secretkey_nonce', '' );
402 -
403 - $this->auto_prepend_removal();
404 424
405 425 return [
406 426 'result' => 'success',
407 427 'message' => 'License deactivated!',
@@ -417,11 +437,10 @@
417 437 public function header()
418 438 {
419 439 $header = get_option( 'patchstack_firewall_ip_header', '' );
420 440 $computed = get_option( 'patchstack_ip_header_computed', 0 );
421 - $force = get_option( 'patchstack_ip_header_force_compute', 0 );
422 441
423 - if ( ( $header == '' && ! $computed ) || $force ) {
442 + if ( $header == '' && ! $computed ) {
424 443 // Create an OTT token.
425 444 $ott = md5( wp_generate_password( 32, true, true ) );
426 445 update_option( 'patchstack_ott_action', $ott );
427 446
@@ -444,415 +463,6 @@
444 463 'cookies' => [],
445 464 ]
446 465 );
447 466 }
448 - }
449 -
450 - /**
451 - * Create the environment needed for the auto prepend firewall functionality.
452 - * 1. First we check if an auto_prepend_file already exists somewhere.
453 - * 2. Then we write to the .htaccess file and check its status code.
454 - * 3. Then we write to the .user.ini file and check its status code, .user.ini is optional if there are any errors with it.
455 - *
456 - * @param boolean $refresh
457 - * @return boolean
458 - */
459 - public function auto_prepend_injection($refresh = false)
460 - {
461 - // Determine if AP firewall is enabled.
462 - if ( ! get_option( 'patchstack_firewall_ap_enabled', false ) ) {
463 - return;
464 - }
465 -
466 - // Determine if we received an error that hasn't been cleared yet.
467 - if ( get_option( 'patchstack_firewall_ap_error', '' ) != '' ) {
468 - return;
469 - }
470 -
471 - // No need to display this error if the .htaccess functionality has been disabled.
472 - if ( get_option( 'patchstack_disable_htaccess', 0 ) || ( defined( 'PS_DISABLE_HTACCESS' ) && PS_DISABLE_HTACCESS ) || ( defined( 'PS_DISABLE_MU' ) && PS_DISABLE_MU ) ) {
473 - return;
474 - }
475 -
476 - // Get filesystem.
477 - global $wp_filesystem;
478 - if ( ! $this->get_filesystem() ) {
479 - update_option( 'patchstack_firewall_ap_error', 'Could not establish filesystem.' );
480 - return false;
481 - }
482 -
483 - // First ensure a .htaccess file exists, otherwise no point.
484 - $htaccess_file = ABSPATH . '.htaccess';
485 - if ( ! $wp_filesystem->exists( $htaccess_file ) && ! $wp_filesystem->touch( $htaccess_file ) ) {
486 - update_option( 'patchstack_firewall_ap_error', 'The .htaccess file could be found nor created.' );
487 - return false;
488 - }
489 -
490 - // Completely halt if there is already an auto_prepend_file present in .htaccess and not of Patchstack.
491 - $htaccess_content = $wp_filesystem->get_contents( $htaccess_file );
492 - if ( stripos( $htaccess_content, 'auto_prepend_file' ) !== false && stripos( $htaccess_content, 'mu-plugin-ap.php' ) === false ) {
493 - update_option( 'patchstack_firewall_ap_error', 'A different auto_prepend_file value is already present in the .htaccess file.' );
494 - return false;
495 - }
496 -
497 - // Completely halt if there is already an auto_prepend_file present in .user.ini and not of Patchstack.
498 - $user_ini = ini_get( 'user_ini.filename' );
499 - if ( $user_ini && $wp_filesystem->exists( ABSPATH . $user_ini ) ) {
500 - $ini_content = $wp_filesystem->get_contents( ABSPATH . $user_ini );
501 - if ( stripos( $ini_content, 'auto_prepend_file' ) !== false && stripos( $ini_content, 'mu-plugin-ap.php' ) === false ) {
502 - update_option( 'patchstack_firewall_ap_error', 'A different auto_prepend_file value is already present in the .user.ini file.' );
503 - return false;
504 - }
505 - }
506 -
507 - // Determine if we can write the /wp-content/pslogs/ folder.
508 - $logs_dir = WP_CONTENT_DIR . '/pslogs/';
509 - if ( ! $wp_filesystem->exists( $logs_dir ) && ! $wp_filesystem->mkdir( $logs_dir ) ) {
510 - update_option( 'patchstack_firewall_ap_error', 'The path ' . $logs_dir . ' could not be created.' );
511 - return false;
512 - }
513 -
514 - // Create the blank index.php file.
515 - if ( ! $wp_filesystem->exists( $logs_dir . 'index.php' ) && ! $wp_filesystem->put_contents( $logs_dir . 'index.php', '' ) ) {
516 - update_option( 'patchstack_firewall_ap_error', 'The file ' . $logs_dir . 'index.php could not be created.' );
517 - return false;
518 - }
519 -
520 - // Create the logs.php file.
521 - if ( ! $wp_filesystem->exists( $logs_dir . 'logs.php' ) && ! $wp_filesystem->put_contents( $logs_dir . 'logs.php', '<?php exit; ?>' . PHP_EOL ) ) {
522 - update_option( 'patchstack_firewall_ap_error', 'The file ' . $logs_dir . 'logs.php could not be created.' );
523 - return false;
524 - }
525 -
526 - // Save current site id.
527 - $current_id = get_current_blog_id();
528 -
529 - // Pull data to save into the config.php file.
530 - $sites = $this->get_sites();
531 - $data = [];
532 - foreach ($sites as $site) {
533 - $this->switch_to_blog( $site->id );
534 - $data[] = [
535 - 'site_id' => $site->id,
536 - 'site_url' => preg_replace( '/^https?:\/\//i', '', $site->siteurl ),
537 - 'home_url' => preg_replace( '/^https?:\/\//i', '', get_option( 'home' ) ),
538 - 'patchstack_basic_firewall' => get_option( 'patchstack_basic_firewall', 1 ),
539 - 'patchstack_license_activated' => get_option( 'patchstack_license_activated', 0 ),
540 - 'patchstack_license_free' => get_option( 'patchstack_license_free', 0 ),
541 - 'patchstack_firewall_ip_header' => get_option( 'patchstack_firewall_ip_header', '' ),
542 - 'patchstack_firewall_rules_v3_ap' => base64_encode( get_option( 'patchstack_firewall_rules_v3_ap', '[]' ) )
543 - ];
544 - }
545 -
546 - // Switch back to current site.
547 - $this->switch_to_blog( $current_id );
548 -
549 - // Save into the config.php file.
550 - if ( ! $wp_filesystem->put_contents( $logs_dir . 'config.php', '<?php return ' . var_export( $data, true ) . ';' ) ) {
551 - update_option( 'patchstack_firewall_ap_error', 'The file ' . $logs_dir . 'config.php could not be created.' );
552 - return false;
553 - }
554 -
555 - // In case we only want to refresh the auto prepend rules, we stop here.
556 - if ( $refresh ) {
557 - return true;
558 - }
559 -
560 - // Prepare the rules to inject into .htaccess.
561 - $prepend_rules = $this->get_auto_prepend_rules();
562 - if ( ! $prepend_rules ) {
563 - return false;
564 - }
565 -
566 - // Determine if the rules already exist and overwrite them in case of path change.
567 - $original_htaccess = $htaccess_content;
568 - $re = '/# BEGIN AP Patchstack.*?# END AP Patchstack/is';
569 - if ( preg_match( $re, $htaccess_content ) ) {
570 - $htaccess_content = preg_replace( $re, rtrim($prepend_rules['htaccess']), $htaccess_content );
571 - } else {
572 - $htaccess_content .= "\n" . $prepend_rules['htaccess'];
573 - }
574 -
575 - // Attempt to write to the .htaccess file.
576 - if ( ! $wp_filesystem->put_contents( $htaccess_file, $htaccess_content ) ) {
577 - update_option( 'patchstack_firewall_ap_error', 'Could not inject into the .htaccess file.' );
578 - return false;
579 - }
580 -
581 - // Determine if the site still works as expected with the injected htaccess rules.
582 - if ( $this->get_site_status_code() >= 400 ) {
583 - $wp_filesystem->put_contents( $htaccess_file, $original_htaccess );
584 - update_option( 'patchstack_firewall_ap_error', 'The .htaccess rules caused a fatal internal server error.' );
585 - return false;
586 - }
587 -
588 - // Ensure a .user.ini is present.
589 - $user_ini = ini_get( 'user_ini.filename' );
590 - if ( ! $user_ini ) {
591 - update_option( 'patchstack_firewall_ap_error', '' );
592 - return true;
593 - }
594 -
595 - // Define full path to the .user.ini file.
596 - $user_ini = ABSPATH . $user_ini;
597 -
598 - // Create the file if it does not exist.
599 - if ( ! $wp_filesystem->exists( $user_ini ) && ! $wp_filesystem->touch( $user_ini ) ) {
600 - update_option( 'patchstack_firewall_ap_error', 'The .user.ini file could not be created.' );
601 - return true;
602 - }
603 -
604 - // Get the contents of the current .user.ini file.
605 - $ini_content = $wp_filesystem->get_contents( $user_ini );
606 -
607 - // Determine if the rules already exist and overwrite them in case of path change.
608 - $original_ini = $ini_content;
609 - $re = '/; BEGIN AP Patchstack.*?; END AP Patchstack/is';
610 - if ( preg_match( $re, $ini_content ) ) {
611 - $ini_content = preg_replace( $re, rtrim($prepend_rules['ini']), $ini_content );
612 - } else {
613 - $ini_content .= "\n" . $prepend_rules['ini'];
614 - }
615 -
616 - // Attempt to write to the .user.ini file.
617 - if ( ! $wp_filesystem->put_contents( $user_ini, $ini_content ) ) {
618 - update_option( 'patchstack_firewall_ap_error', 'Could not inject into the .user.ini file.' );
619 - return true;
620 - }
621 -
622 - // Determine if the site still works as expected with the injected .user.ini rules.
623 - if ( $this->get_site_status_code() == 500 ) {
624 - $wp_filesystem->put_contents( $user_ini, $original_ini );
625 - update_option( 'patchstack_firewall_ap_error', 'The .user.ini rules caused a fatal internal server error.' );
626 - return false;
627 - }
628 -
629 - update_option( 'patchstack_firewall_ap_error', '' );
630 - return true;
631 - }
632 -
633 - /**
634 - * Remove everything related to the auto prepend functionality.
635 - *
636 - * @return boolean
637 - */
638 - public function auto_prepend_removal()
639 - {
640 - global $wp_filesystem;
641 - $this->get_filesystem();
642 -
643 - // Define our paths to access.
644 - $logs_dir = WP_CONTENT_DIR . '/pslogs/';
645 - $htaccess_file = ABSPATH . '.htaccess';
646 - $ini_file = ABSPATH . '.user.ini';
647 -
648 - // Remove the entire /pslogs/ directory.
649 - if ( $wp_filesystem->is_dir( $logs_dir ) ) {
650 - $wp_filesystem->delete( $logs_dir, true );
651 - }
652 -
653 - // Remove the .htaccess injected rules.
654 - if ( $wp_filesystem->is_file( $htaccess_file ) ) {
655 - $htaccess_content = $wp_filesystem->get_contents( $htaccess_file );
656 - $re = '/# BEGIN AP Patchstack.*?# END AP Patchstack/is';
657 - if ( preg_match( $re, $htaccess_content ) ) {
658 - $htaccess_content = preg_replace( $re, '', $htaccess_content );
659 - $wp_filesystem->put_contents( $htaccess_file, $htaccess_content );
660 - }
661 - }
662 -
663 - // Remove the .user.ini injected rules.
664 - if ( $wp_filesystem->is_file( $ini_file ) ) {
665 - $ini_content = $wp_filesystem->get_contents( $ini_file );
666 - $re = '/; BEGIN AP Patchstack.*?; END AP Patchstack/is';
667 - if ( preg_match( $re, $ini_content ) ) {
668 - $ini_content = preg_replace( $re, '', $ini_content );
669 - $wp_filesystem->put_contents( $ini_file, $ini_content );
670 - }
671 - }
672 - }
673 -
674 - /**
675 - * Attempt to establish the proper WP_FileSystem.
676 - *
677 - * @return boolean
678 - */
679 - private function get_filesystem()
680 - {
681 - // Seems to be the only native way to obtain FTP credentials, if defined.
682 - include_once( ABSPATH . 'wp-admin/includes/file.php' );
683 - ob_start();
684 - $creds = request_filesystem_credentials( admin_url( 'admin-ajax.php' ), '', false, ABSPATH, null, true );
685 - ob_end_clean();
686 -
687 - // Returns false if no filesystem connection could be determined.
688 - if ( $creds === false ) {
689 - update_option( 'patchstack_firewall_ap_error', 'Unable to establish filesystem connection.' );
690 - return false;
691 - }
692 -
693 - // Attempt to initialize it.
694 - $fs = WP_Filesystem( $creds, ABSPATH, true );
695 - if ( ! $fs ) {
696 - update_option( 'patchstack_firewall_ap_error', 'Unable to establish filesystem connection through acquired creds.' );
697 - return false;
698 - }
699 -
700 - return true;
701 - }
702 -
703 - /**
704 - * Get sites as part of the environment.
705 - *
706 - * @return array
707 - */
708 - private function get_sites()
709 - {
710 - if ( ! function_exists( 'get_sites' ) ) {
711 - return [
712 - (object) [
713 - 'id' => 0,
714 - 'siteurl' => get_site_url()
715 - ]
716 - ];
717 - }
718 -
719 - return get_sites();
720 - }
721 -
722 - /**
723 - * Switch to a different site.
724 - *
725 - * @param integer $site_id
726 - * @return void
727 - */
728 - private function switch_to_blog($site_id)
729 - {
730 - if ( ! function_exists( 'switch_to_blog' ) ) {
731 - return;
732 - }
733 -
734 - switch_to_blog( $site_id );
735 - }
736 -
737 - /**
738 - * Determine the web-server software and make sure we support it before we generate the .htaccess rules for it.
739 - *
740 - * @return array|boolean
741 - */
742 - private function get_auto_prepend_rules()
743 - {
744 - // Establish location of the auto prepend file.
745 - $mu_file = __DIR__ . '/mu-plugin-ap.php';
746 - if ( ! file_exists( $mu_file ) ) {
747 - return false;
748 - }
749 -
750 - // Ensure that the SERVER_SOFTWARE value is set.
751 - $software = isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : '';
752 - if ( ! $software ) {
753 - update_option( 'patchstack_firewall_ap_error', 'Unsupported SERVER_SOFTWARE, found: ' . $software );
754 - return false;
755 - }
756 -
757 - // At this time, reject non-Apache environments.
758 - $sapi = function_exists( 'php_sapi_name' ) ? php_sapi_name() : false;
759 - if ( ! $sapi || stripos( $_SERVER['SERVER_SOFTWARE'], 'litespeed' ) === false && $sapi != 'litespeed' && stripos($_SERVER['SERVER_SOFTWARE'], 'apache' ) === false) {
760 - update_option( 'patchstack_firewall_ap_error', 'Unsupported SERVER_SOFTWARE, found: ' . $_SERVER['SERVER_SOFTWARE'] . ' and ' . $sapi );
761 - return false;
762 - }
763 -
764 - // Seperate flag for LiteSpeed.
765 - $is_litespeed = stripos( $_SERVER['SERVER_SOFTWARE'], 'litespeed' ) !== false || $sapi == 'litespeed';
766 -
767 - // Attempt to find the Apache version, < 2.4 does not support <If>.
768 - // This depends on ServerTokens value, so only stop execution if we can't find the specific unsupported versions.
769 - $version = function_exists( 'apache_get_version' ) ? apache_get_version() : $software;
770 - if ( ! $is_litespeed && stripos( $version, 'Apache/2.4' ) === false ) {
771 - update_option( 'patchstack_firewall_ap_error', 'Unsupported SERVER_SOFTWARE, found: ' . $software );
772 - return false;
773 - }
774 -
775 - // Add c-style slashes.
776 - $mu_file_as = wp_normalize_path(addcslashes($mu_file, "'"));
777 -
778 - // Bit different rules for LiteSpeed.
779 - if ( ! $is_litespeed ) {
780 - $rules = "<IfModule mod_php.c>
781 - php_value auto_prepend_file '" . $mu_file_as . "'
782 - </IfModule>
783 - <IfModule mod_php5.c>
784 - php_value auto_prepend_file '" . $mu_file_as . "'
785 - </IfModule>
786 - <IfModule mod_php7.c>
787 - php_value auto_prepend_file '" . $mu_file_as . "'
788 - </IfModule>";
789 - } else {
790 - $rules = "<IfModule LiteSpeed>
791 - php_value auto_prepend_file '" . $mu_file_as . "'
792 - </IfModule>
793 - <IfModule lsapi_module>
794 - php_value auto_prepend_file '" . $mu_file_as . "'
795 - </IfModule>";
796 - }
797 -
798 - return [
799 - 'htaccess' => "# BEGIN AP Patchstack
800 -<IfModule mod_authz_core.c>
801 - <If \"-f '" . $mu_file_as . "'\">
802 - " . $rules . "
803 -
804 - <Files \".user.ini\">
805 - <IfModule mod_authz_core.c>
806 - Require all denied
807 - </IfModule>
808 - <IfModule !mod_authz_core.c>
809 - Order deny,allow
810 - Deny from all
811 - </IfModule>
812 - </Files>
813 - </If>
814 -</IfModule>
815 -# END AP Patchstack
816 -",
817 - 'ini' => "; BEGIN AP Patchstack
818 -auto_prepend_file = '" . $mu_file_as . "'
819 -; END AP Patchstack
820 -"
821 - ];
822 - }
823 -
824 - /**
825 - * Retrieve the status code of the site.
826 - * This is done to determine if the .htaccess rules do not trigger an error.
827 - *
828 - * @return integer
829 - */
830 - public function get_site_status_code() {
831 - $response = wp_remote_get( get_site_url() );
832 - $http_code = wp_remote_retrieve_response_code( $response );
833 - return $http_code;
834 - }
835 -
836 - /**
837 - * If option is updated, refresh AP config file.
838 - *
839 - * @param string $option_name
840 - * @param string $option_name
841 - * @param mixed $value
842 - * @return void
843 - */
844 - public function updated_option( $option_name, $old_value, $value ) {
845 - // Only allow to run for our options. The IP header is embedded in the AP config
846 - // file too, so a change there must also regenerate it.
847 - if ( !in_array( $option_name, [ 'patchstack_basic_firewall', 'patchstack_license_free', 'patchstack_firewall_rules_v3_ap', 'patchstack_firewall_ip_header' ] ) ) {
848 - return;
849 - }
850 -
851 - // Not strict type matching.
852 - if ( $old_value == $value ) {
853 - return;
854 - }
855 -
856 - $this->auto_prepend_injection(true);
857 467 }
858 468 }