PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.25
Patchstack – WordPress & Plugins Security v2.1.25
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 +81 -543 trunk2.1.25 View file →
@@ -11,15 +11,8 @@
11 11 */
12 12 class P_Activation extends P_Core {
13 13
14 14 /**
15 - * Holds any activation errors.
16 - *
17 - * @var array
18 - */
19 - private $activation_errors = [];
20 -
21 - /**
22 15 * Add the actions required for the activation.
23 16 *
24 17 * @param Patchstack $core
25 18 * @return void
@@ -25,11 +18,9 @@
25 18 * @return void
26 19 */
27 20 public function __construct( $core ) {
28 21 parent::__construct( $core );
29 -
30 - add_action( 'activated_plugin', [ $this, 'redirect_activation' ], 10, 2 );
31 - add_action( 'updated_option', [ $this, 'updated_option' ], 10, 3 );
22 + add_action( 'activated_plugin', array( $this, 'redirect_activation' ), 10, 2 );
32 23 }
33 24
34 25 /**
35 26 * Redirect the user to our settings page after plugin activation.
@@ -41,34 +32,16 @@
41 32 public function redirect_activation( $plugin, $network_activation ) {
42 33 if ( defined( 'WP_CLI' ) && WP_CLI ) {
43 34 return;
44 35 }
36 +
37 + if ( $plugin == $this->plugin->basename ) {
45 38
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 39 // In case of multisite, we want to redirect the user to a different page.
67 40 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' : '' ) ) );
41 + wp_safe_redirect( network_admin_url( 'admin.php?page=patchstack-multisite-settings&tab=multisite&activated=1' ) );
69 42 } else {
70 - wp_safe_redirect( admin_url( 'admin.php?page=' . $this->plugin->name . '&ps_activated=1' . ($attemptAuto ? '&ps_autoa=1' : '' ) ) );
43 + wp_safe_redirect( admin_url( 'admin.php?page=' . $this->plugin->name . '&activated=1' ) );
71 44 }
72 45 exit;
73 46 }
74 47 }
@@ -83,9 +56,9 @@
83 56 return true;
84 57 }
85 58
86 59 // Add a dashboard notice.
87 - add_action( 'all_admin_notices', [ $this, 'requirements_not_met_notice' ] );
60 + add_action( 'all_admin_notices', array( $this, 'requirements_not_met_notice' ) );
88 61 return false;
89 62 }
90 63
91 64 /**
@@ -96,25 +69,25 @@
96 69 public function meets_requirements() {
97 70 // Check to see if we can access the API.
98 71 $response = wp_remote_request(
99 72 $this->plugin->api_url,
100 - [
73 + array(
101 74 'method' => 'GET',
102 75 'timeout' => 10,
103 76 'redirection' => 5,
104 - ]
77 + )
105 78 );
106 79
107 80 // Check if we can access the API.
108 81 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();
82 + $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 83 return false;
111 84 }
112 85
113 86 // Do checks for required classes / functions or similar.
114 87 // 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.';
88 + if ( version_compare( phpversion(), '5.3.0', '<' ) ) {
89 + $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 90 return false;
118 91 }
119 92
120 93 global $wp_version;
@@ -135,9 +108,9 @@
135 108 // Deactivate the plugin.
136 109 deactivate_plugins( $this->plugin->basename );
137 110
138 111 // 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' );
112 + $default_message = __( 'Patchstack could not be activated due to a conflict. See below for information regarding the conflict.<br />', 'patchstack' );
140 113
141 114 // Print the errors on the screen.
142 115 echo wp_kses_post( $default_message );
143 116 echo wp_kses_post( implode( '<br />', $this->activation_errors ) );
@@ -170,10 +143,10 @@
170 143 }
171 144
172 145 // Deactivate the plugin.
173 146 include_once ABSPATH . 'wp-admin/includes/plugin.php';
174 - deactivate_plugins( [ 'webarx/webarx.php' ] );
175 - update_option( 'patchstack_license_free', '0', true );
147 + deactivate_plugins( array( 'webarx/webarx.php' ) );
148 + update_option( 'patchstack_license_free', '0' );
176 149 }
177 150
178 151 // Make sure any rewrite functionality has been loaded.
179 152 $this->migrate();
@@ -178,11 +151,8 @@
178 151 // Make sure any rewrite functionality has been loaded.
179 152 $this->migrate();
180 153 add_option( 'patchstack_first_activated', '1' );
181 154
182 - // Whether or not we should send a secret key to our API.
183 - $sendSecret = false;
184 -
185 155 // Activate the license.
186 156 if ( $this->plugin->client_id != 'PATCHSTACK_CLIENT_ID' && $this->plugin->private_key != 'PATCHSTACK_PRIVATE_KEY' ) {
187 157 $this->alter_license( $this->plugin->client_id, $this->plugin->private_key, 'activate' );
188 158 } elseif ( get_option( 'patchstack_clientid', false ) != false && get_option( 'patchstack_secretkey', false ) != false ) {
@@ -187,10 +157,9 @@
187 157 $this->alter_license( $this->plugin->client_id, $this->plugin->private_key, 'activate' );
188 158 } elseif ( get_option( 'patchstack_clientid', false ) != false && get_option( 'patchstack_secretkey', false ) != false ) {
189 159 $this->alter_license( get_option( 'patchstack_clientid' ), $this->get_secret_key(), 'activate' );
190 160 } else {
191 - $sendSecret = true;
192 - update_option( 'patchstack_license_free', '1', true );
161 + update_option( 'patchstack_license_free', '1' );
193 162 }
194 163
195 164 // Update firewall status after activating plugin
196 165 $api = new P_Api( $core );
@@ -195,14 +164,10 @@
195 164 // Update firewall status after activating plugin
196 165 $api = new P_Api( $core );
197 166 $token = $api->get_access_token();
198 167 if ( ! empty( $token ) ) {
199 - $api->update_firewall_status( [ 'status' => 1 ] );
200 - $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 ) ;
168 + $api->update_firewall_status( array( 'status' => 1 ) );
169 + $api->update_url( array( 'plugin_url' => get_option( 'siteurl' ) ) );
205 170 }
206 171
207 172 // Immediately send software data to our server to set firewall as enabled.
208 173 // Also immediately download the whitelist file and the firewall rules.
@@ -211,37 +176,53 @@
211 176 do_action( 'patchstack_post_firewall_rules' );
212 177 do_action( 'patchstack_post_dynamic_firewall_rules' );
213 178 }
214 179
215 - // Try to create the mu-plugins folder/file.
216 - // 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 ) ) {
218 - return;
180 + // One time actions should be placed here.
181 + $this->plugin->hardening->delete_readme();
182 + }
183 +
184 + /**
185 + * Used to activate an individual license on multisite/network.
186 + *
187 + * @param object $site
188 + * @param array $license
189 + * @return void
190 + */
191 + public function activate_multisite_license( $site, $license ) {
192 + // Build the Patchstack tables on the site.
193 + $this->migrate( null, $site->id );
194 +
195 + // Add the options to given site.
196 + foreach ( $this->plugin->admin_options->options as $name => $value ) {
197 + add_blog_option( $site->id, $name, $value );
219 198 }
220 199
221 - // The mu-plugin does not exist, try to create it.
222 - @include_once ABSPATH . 'wp-admin/includes/file.php';
223 - $wpfs = WP_Filesystem();
200 + // Set the client id and secret key.
201 + update_blog_option( $site->id, 'patchstack_clientid', $license['id'] );
202 + $enc = $this->get_secret_key( $license['secret'] );
203 + update_blog_option( $site->id, 'patchstack_secretkey', $enc['cipher'] );
204 + update_blog_option( $site->id, 'patchstack_secretkey_nonce', $enc['nonce'] );
224 205
225 - // Failed to initialize WP_Filesystem.
226 - if ( ! $wpfs ) {
227 - return;
228 - }
206 + $this->plugin->api->blog_id = $site->id;
229 207
230 - if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
231 - wp_mkdir_p( WPMU_PLUGIN_DIR );
232 - }
208 + // Activate the license and update firewall status after activating the plugin.
209 + $token = $this->plugin->api->get_access_token( $license['id'], $license['secret'], true );
210 + if ( ! empty( $token ) ) {
211 + $this->plugin->api->update_firewall_status( array( 'status' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ) );
212 + $this->plugin->api->update_url( array( 'plugin_url' => get_blog_option( $site->id, 'siteurl' ) ) );
233 213
234 - // Failed to create the mu-plugin folder.
235 - if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
236 - return;
214 + // If we have an access token, tell our API that the firewall is activated
215 + // and the current URL of the site.
216 + update_blog_option( $site->id, 'patchstack_license_activated', '1' );
217 + $this->plugin->api->update_license_status();
218 +
219 + // This will trigger the software synchronization action.
220 + wp_remote_get( get_site_url( $site->id ), array( 'sslverify' => false ) );
237 221 }
238 222
239 - // Create the mu-plugin file in the folder.
240 - if ( is_writable( WPMU_PLUGIN_DIR ) ) {
241 - $php = @file_get_contents( trailingslashit( plugin_dir_path( __FILE__ ) ) . 'mu-plugin.php' );
242 - @file_put_contents( trailingslashit( WPMU_PLUGIN_DIR ) . '_patchstack.php', $php );
243 - }
223 + // Make sure to switch back to the current blog id.
224 + $this->plugin->api->blog_id = get_current_blog_id();
244 225 }
245 226
246 227 /**
247 228 * Build the required Patchstack tables.
@@ -274,9 +255,9 @@
274 255 * @return void
275 256 */
276 257 public function migrate_check() {
277 258 // Only perform migrations if we have any to execute.
278 - $versions = ['3.0.0', '3.0.1', '3.0.2', '3.0.3', '3.0.4'];
259 + $versions = array('3.0.0', '3.0.1', '3.0.2');
279 260 if ( count( $versions ) == 0 ) {
280 261 return;
281 262 }
282 263
@@ -295,19 +276,16 @@
295 276 * @return void
296 277 */
297 278 public function deactivate() {
298 279 // Update firewall status after de-activating plugin
299 - try {
300 - $token = $this->plugin->api->get_access_token();
301 - if ( ! empty( $token ) ) {
302 - $this->plugin->api->update_firewall_status( [ 'status' => 0 ] );
303 - }
304 - } catch (\Exception $e) {
305 - //
280 + $api = new P_Api( $this );
281 + $token = $api->get_access_token();
282 + if ( ! empty( $token ) ) {
283 + $api->update_firewall_status( array( 'status' => 0 ) );
306 284 }
307 285
308 286 // 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' ];
287 + $tasks = array( '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 288 foreach ( $tasks as $task ) {
311 289 wp_clear_scheduled_hook( $task );
312 290 }
313 291
@@ -312,16 +290,8 @@
312 290 }
313 291
314 292 // Cleanup the .htaccess file.
315 293 $this->plugin->htaccess->cleanup_htaccess_file();
316 - $this->auto_prepend_removal();
317 -
318 - // Remove the mu-plugin file if it exists.
319 - foreach (['patchstack.php', '_patchstack.php'] as $file) {
320 - if ( file_exists( WPMU_PLUGIN_DIR . '/' . $file )) {
321 - wp_delete_file( WPMU_PLUGIN_DIR . '/' . $file );
322 - }
323 - }
324 294 }
325 295
326 296 /**
327 297 * Activate or deactivate a license on the current site.
@@ -331,11 +301,8 @@
331 301 * @param string $action
332 302 * @return array
333 303 */
334 304 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();
337 -
338 305 // Store current keys in tmp variable so in case it fails, we can set it back.
339 306 $tmp_id = get_option( 'patchstack_clientid' );
340 307 $tmp_key = $this->get_secret_key();
341 308
@@ -350,47 +317,34 @@
350 317 // Valid result?
351 318 if ( ! $api_result ) {
352 319 update_option( 'patchstack_clientid', $tmp_id );
353 320 $this->set_secret_key( $tmp_key );
354 -
355 - return [
321 + return array(
356 322 'result' => 'error',
357 - 'body' => json_encode($this->plugin->api->message),
358 323 'message' => 'Cannot activate license!',
359 - ];
324 + );
360 325 }
361 326
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.
327 + // If we have an access token, tell our API that the firewall is activated
328 + // and the current URL of the site.
329 + update_option( 'patchstack_license_activated', '1' );
330 + $this->plugin->api->update_license_status();
370 331 $token = $this->plugin->api->get_access_token();
371 332 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 333 do_action( 'patchstack_send_software_data' );
376 334 if ( get_option( 'patchstack_license_free', 0 ) != 1 ) {
377 - update_option( 'patchstack_basic_firewall', 1, true );
378 335 do_action( 'patchstack_post_firewall_rules' );
379 336 do_action( 'patchstack_post_dynamic_firewall_rules' );
380 337 $this->header();
381 338 }
382 339
383 - // Update firewall status, URL and ping API.
384 - $this->plugin->api->update_firewall_status( [ 'status' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ] );
385 - $this->plugin->api->update_url( [ 'plugin_url' => get_option( 'siteurl' ) ] );
386 - $this->auto_prepend_injection();
340 + $this->plugin->api->update_firewall_status( array( 'status' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ) );
341 + $this->plugin->api->update_url( array( 'plugin_url' => get_option( 'siteurl' ) ) );
387 342 }
388 -
389 - return [
343 + return array(
390 344 'result' => 'success',
391 345 'message' => 'License activated!',
392 - ];
346 + );
393 347 }
394 348
395 349 // Deactivate the license.
396 350 if ( $action == 'deactivate' ) {
@@ -395,18 +349,12 @@
395 349 // Deactivate the license.
396 350 if ( $action == 'deactivate' ) {
397 351 update_option( 'patchstack_api_token', '' );
398 352 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 -
405 - return [
353 + return array(
406 354 'result' => 'success',
407 355 'message' => 'License deactivated!',
408 - ];
356 + );
409 357 }
410 358 }
411 359
412 360 /**
@@ -417,11 +365,10 @@
417 365 public function header()
418 366 {
419 367 $header = get_option( 'patchstack_firewall_ip_header', '' );
420 368 $computed = get_option( 'patchstack_ip_header_computed', 0 );
421 - $force = get_option( 'patchstack_ip_header_force_compute', 0 );
422 369
423 - if ( ( $header == '' && ! $computed ) || $force ) {
370 + if ( $header == '' && ! $computed ) {
424 371 // Create an OTT token.
425 372 $ott = md5( wp_generate_password( 32, true, true ) );
426 373 update_option( 'patchstack_ott_action', $ott );
427 374
@@ -427,432 +374,23 @@
427 374
428 375 // Tell our API.
429 376 wp_remote_request(
430 377 $this->plugin->api_url . '/api/header',
431 - [
378 + array(
432 379 'method' => 'POST',
433 380 'timeout' => 60,
434 381 'redirection' => 5,
435 382 'httpversion' => '1.0',
436 383 'blocking' => true,
437 - 'headers' => [
384 + 'headers' => array(
438 385 'Source-Host' => get_site_url(),
439 - ],
440 - 'body' => [
386 + ),
387 + 'body' => array(
441 388 'token' => $ott,
442 389 'url' => get_site_url()
443 - ],
444 - 'cookies' => [],
445 - ]
390 + ),
391 + 'cookies' => array(),
392 + )
446 393 );
447 394 }
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 395 }
858 396 }