PluginProbe
Patchstack – WordPress & Plugins Security / 2.2.8
Patchstack – WordPress & Plugins Security v2.2.8
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 +68 -474 trunk2.2.8 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.
@@ -41,34 +39,16 @@
41 39 public function redirect_activation( $plugin, $network_activation ) {
42 40 if ( defined( 'WP_CLI' ) && WP_CLI ) {
43 41 return;
44 42 }
43 +
44 + if ( $plugin == $this->plugin->basename ) {
45 45
46 - // Only if it's the current plugin.
47 - if ( $plugin == $this->plugin->basename && ! isset( $_REQUEST['_ajax_nonce'] ) ) {
48 -
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 - // Determine if secret token was set, if so, sync with API.
60 - $attemptAuto = false;
61 - $secretToken = get_option( 'patchstack_activation_secret', '' );
62 - if ( ! empty( $secretToken ) ) {
63 - $attemptAuto = true;
64 - }
65 -
66 46 // In case of multisite, we want to redirect the user to a different page.
67 47 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' : '' ) ) );
48 + wp_safe_redirect( network_admin_url( 'admin.php?page=patchstack-multisite-settings&tab=multisite&ps_activated=1' ) );
69 49 } else {
70 - wp_safe_redirect( admin_url( 'admin.php?page=' . $this->plugin->name . '&ps_activated=1' . ($attemptAuto ? '&ps_autoa=1' : '' ) ) );
50 + wp_safe_redirect( admin_url( 'admin.php?page=' . $this->plugin->name . '&ps_activated=1' ) );
71 51 }
72 52 exit;
73 53 }
74 54 }
@@ -105,16 +85,16 @@
105 85 );
106 86
107 87 // Check if we can access the API.
108 88 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();
89 + $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 90 return false;
111 91 }
112 92
113 93 // Do checks for required classes / functions or similar.
114 94 // 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.';
95 + if ( version_compare( phpversion(), '5.3.0', '<' ) ) {
96 + $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 97 return false;
118 98 }
119 99
120 100 global $wp_version;
@@ -135,9 +115,9 @@
135 115 // Deactivate the plugin.
136 116 deactivate_plugins( $this->plugin->basename );
137 117
138 118 // Compile default message.
139 - $default_message = esc_attr__( 'Patchstack could not be activated due to a conflict. See below for information regarding the conflict.<br />', 'patchstack' );
119 + $default_message = __( 'Patchstack could not be activated due to a conflict. See below for information regarding the conflict.<br />', 'patchstack' );
140 120
141 121 // Print the errors on the screen.
142 122 echo wp_kses_post( $default_message );
143 123 echo wp_kses_post( implode( '<br />', $this->activation_errors ) );
@@ -171,9 +151,9 @@
171 151
172 152 // Deactivate the plugin.
173 153 include_once ABSPATH . 'wp-admin/includes/plugin.php';
174 154 deactivate_plugins( [ 'webarx/webarx.php' ] );
175 - update_option( 'patchstack_license_free', '0', true );
155 + update_option( 'patchstack_license_free', '0' );
176 156 }
177 157
178 158 // Make sure any rewrite functionality has been loaded.
179 159 $this->migrate();
@@ -178,11 +158,8 @@
178 158 // Make sure any rewrite functionality has been loaded.
179 159 $this->migrate();
180 160 add_option( 'patchstack_first_activated', '1' );
181 161
182 - // Whether or not we should send a secret key to our API.
183 - $sendSecret = false;
184 -
185 162 // Activate the license.
186 163 if ( $this->plugin->client_id != 'PATCHSTACK_CLIENT_ID' && $this->plugin->private_key != 'PATCHSTACK_PRIVATE_KEY' ) {
187 164 $this->alter_license( $this->plugin->client_id, $this->plugin->private_key, 'activate' );
188 165 } elseif ( get_option( 'patchstack_clientid', false ) != false && get_option( 'patchstack_secretkey', false ) != false ) {
@@ -187,10 +164,9 @@
187 164 $this->alter_license( $this->plugin->client_id, $this->plugin->private_key, 'activate' );
188 165 } elseif ( get_option( 'patchstack_clientid', false ) != false && get_option( 'patchstack_secretkey', false ) != false ) {
189 166 $this->alter_license( get_option( 'patchstack_clientid' ), $this->get_secret_key(), 'activate' );
190 167 } else {
191 - $sendSecret = true;
192 - update_option( 'patchstack_license_free', '1', true );
168 + update_option( 'patchstack_license_free', '1' );
193 169 }
194 170
195 171 // Update firewall status after activating plugin
196 172 $api = new P_Api( $core );
@@ -197,12 +173,8 @@
197 173 $token = $api->get_access_token();
198 174 if ( ! empty( $token ) ) {
199 175 $api->update_firewall_status( [ 'status' => 1 ] );
200 176 $api->update_url( [ 'plugin_url' => get_option( 'siteurl' ) ] );
201 - } elseif ( $sendSecret ) {
202 - $secretToken = wp_generate_password( 36, true );
203 - update_option( 'patchstack_activation_secret', $secretToken );
204 - update_option( 'patchstack_activation_time', time() + 59 ) ;
205 177 }
206 178
207 179 // Immediately send software data to our server to set firewall as enabled.
208 180 // Also immediately download the whitelist file and the firewall rules.
@@ -211,11 +183,14 @@
211 183 do_action( 'patchstack_post_firewall_rules' );
212 184 do_action( 'patchstack_post_dynamic_firewall_rules' );
213 185 }
214 186
187 + // One time actions should be placed here.
188 + $this->plugin->hardening->delete_readme();
189 +
215 190 // Try to create the mu-plugins folder/file.
216 191 // 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 ) ) {
192 + if ( file_exists( WPMU_PLUGIN_DIR . '/patchstack.php' ) || file_exists( WPMU_PLUGIN_DIR . '/_patchstack.php' )) {
218 193 return;
219 194 }
220 195
221 196 // The mu-plugin does not exist, try to create it.
@@ -243,8 +218,51 @@
243 218 }
244 219 }
245 220
246 221 /**
222 + * Used to activate an individual license on multisite/network.
223 + *
224 + * @param object $site
225 + * @param array $license
226 + * @return void
227 + */
228 + public function activate_multisite_license( $site, $license ) {
229 + // Build the Patchstack tables on the site.
230 + $this->migrate( null, $site->id );
231 +
232 + // Add the options to given site.
233 + foreach ( $this->plugin->admin_options->options as $name => $value ) {
234 + add_blog_option( $site->id, $name, $value );
235 + }
236 +
237 + // Set the client id and secret key.
238 + update_blog_option( $site->id, 'patchstack_clientid', $license['id'] );
239 + $enc = $this->get_secret_key( $license['secret'] );
240 + update_blog_option( $site->id, 'patchstack_secretkey', $enc['cipher'] );
241 + update_blog_option( $site->id, 'patchstack_secretkey_nonce', $enc['nonce'] );
242 +
243 + $this->plugin->api->blog_id = $site->id;
244 +
245 + // Activate the license and update firewall status after activating the plugin.
246 + $token = $this->plugin->api->get_access_token( $license['id'], $license['secret'], true );
247 + if ( ! empty( $token ) ) {
248 + $this->plugin->api->update_firewall_status( [ 'status' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ] );
249 + $this->plugin->api->update_url( [ 'plugin_url' => get_blog_option( $site->id, 'siteurl' ) ] );
250 +
251 + // If we have an access token, tell our API that the firewall is activated
252 + // and the current URL of the site.
253 + update_blog_option( $site->id, 'patchstack_license_activated', '1' );
254 + $this->plugin->api->update_license_status();
255 +
256 + // This will trigger the software synchronization action.
257 + wp_remote_get( get_site_url( $site->id ), [ 'sslverify' => false ] );
258 + }
259 +
260 + // Make sure to switch back to the current blog id.
261 + $this->plugin->api->blog_id = get_current_blog_id();
262 + }
263 +
264 + /**
247 265 * Build the required Patchstack tables.
248 266 *
249 267 * @param null|string $ver The version to upgrade to.
250 268 * @param null|integer $site_id The blog id to perform the upgrades on.
@@ -305,9 +323,9 @@
305 323 //
306 324 }
307 325
308 326 // 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' ];
327 + $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 328 foreach ( $tasks as $task ) {
311 329 wp_clear_scheduled_hook( $task );
312 330 }
313 331
@@ -312,9 +330,8 @@
312 330 }
313 331
314 332 // Cleanup the .htaccess file.
315 333 $this->plugin->htaccess->cleanup_htaccess_file();
316 - $this->auto_prepend_removal();
317 334
318 335 // Remove the mu-plugin file if it exists.
319 336 foreach (['patchstack.php', '_patchstack.php'] as $file) {
320 337 if ( file_exists( WPMU_PLUGIN_DIR . '/' . $file )) {
@@ -331,10 +348,12 @@
331 348 * @param string $action
332 349 * @return array
333 350 */
334 351 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();
352 + // Set the default option values if calling through CLI.
353 + if ( defined( 'WP_CLI' ) && WP_CLI) {
354 + $this->plugin->admin_options->settings_init();
355 + }
337 356
338 357 // Store current keys in tmp variable so in case it fails, we can set it back.
339 358 $tmp_id = get_option( 'patchstack_clientid' );
340 359 $tmp_key = $this->get_secret_key();
@@ -353,38 +372,28 @@
353 372 $this->set_secret_key( $tmp_key );
354 373
355 374 return [
356 375 'result' => 'error',
357 - 'body' => json_encode($this->plugin->api->message),
358 376 'message' => 'Cannot activate license!',
359 377 ];
360 378 }
361 379
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.
380 + // If we have an access token, tell our API that the firewall is activated
381 + // and the current URL of the site.
382 + update_option( 'patchstack_license_activated', '1' );
383 + $this->plugin->api->update_license_status();
370 384 $token = $this->plugin->api->get_access_token();
371 385 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 386 do_action( 'patchstack_send_software_data' );
376 387 if ( get_option( 'patchstack_license_free', 0 ) != 1 ) {
377 - update_option( 'patchstack_basic_firewall', 1, true );
378 388 do_action( 'patchstack_post_firewall_rules' );
379 389 do_action( 'patchstack_post_dynamic_firewall_rules' );
380 390 $this->header();
381 391 }
382 392
383 - // Update firewall status, URL and ping API.
384 393 $this->plugin->api->update_firewall_status( [ 'status' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ] );
385 394 $this->plugin->api->update_url( [ 'plugin_url' => get_option( 'siteurl' ) ] );
386 - $this->auto_prepend_injection();
395 + $this->plugin->api->ping();
387 396 }
388 397
389 398 return [
390 399 'result' => 'success',
@@ -395,13 +404,8 @@
395 404 // Deactivate the license.
396 405 if ( $action == 'deactivate' ) {
397 406 update_option( 'patchstack_api_token', '' );
398 407 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 408
405 409 return [
406 410 'result' => 'success',
407 411 'message' => 'License deactivated!',
@@ -417,11 +421,10 @@
417 421 public function header()
418 422 {
419 423 $header = get_option( 'patchstack_firewall_ip_header', '' );
420 424 $computed = get_option( 'patchstack_ip_header_computed', 0 );
421 - $force = get_option( 'patchstack_ip_header_force_compute', 0 );
422 425
423 - if ( ( $header == '' && ! $computed ) || $force ) {
426 + if ( $header == '' && ! $computed ) {
424 427 // Create an OTT token.
425 428 $ott = md5( wp_generate_password( 32, true, true ) );
426 429 update_option( 'patchstack_ott_action', $ott );
427 430
@@ -444,415 +447,6 @@
444 447 'cookies' => [],
445 448 ]
446 449 );
447 450 }
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 451 }
858 452 }