PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.7
Patchstack – WordPress & Plugins Security v2.3.7
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
patchstack / includes / activation.php

activation.php in Patchstack – WordPress & Plugins Security 2.3.7, at includes/activation.php

859 lines 29.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // Do not allow the file to be called directly.
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * This class is used to activate and deactivate the plugin.
10 * Additionally, we use it to run migrations.
11 */
12 class P_Activation extends P_Core {
13
14 /**
15 * Holds any activation errors.
16 *
17 * @var array
18 */
19 private $activation_errors = [];
20
21 /**
22 * Add the actions required for the activation.
23 *
24 * @param Patchstack $core
25 * @return void
26 */
27 public function __construct( $core ) {
28 parent::__construct( $core );
29
30 add_action( 'activated_plugin', [ $this, 'redirect_activation' ], 10, 2 );
31 add_action( 'updated_option', [ $this, 'updated_option' ], 10, 3 );
32 }
33
34 /**
35 * Redirect the user to our settings page after plugin activation.
36 *
37 * @param string $plugin The plugin that is activated.
38 * @param boolean $network_activation If a network wide activation. (multisite)
39 * @return void
40 */
41 public function redirect_activation( $plugin, $network_activation ) {
42 if ( defined( 'WP_CLI' ) && WP_CLI ) {
43 return;
44 }
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 // In case of multisite, we want to redirect the user to a different page.
67 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' : '' ) ) );
69 } else {
70 wp_safe_redirect( admin_url( 'admin.php?page=' . $this->plugin->name . '&ps_activated=1' . ($attemptAuto ? '&ps_autoa=1' : '' ) ) );
71 }
72 exit;
73 }
74 }
75
76 /**
77 * Check if the plugin meets requirements and disable it if they are not present.
78 *
79 * @return boolean
80 */
81 public function check_requirements() {
82 if ( $this->meets_requirements() ) {
83 return true;
84 }
85
86 // Add a dashboard notice.
87 add_action( 'all_admin_notices', [ $this, 'requirements_not_met_notice' ] );
88 return false;
89 }
90
91 /**
92 * Check that all plugin requirements are met.
93 *
94 * @return boolean
95 */
96 public function meets_requirements() {
97 // Check to see if we can access the API.
98 $response = wp_remote_request(
99 $this->plugin->api_url,
100 [
101 'method' => 'GET',
102 'timeout' => 10,
103 'redirection' => 5,
104 ]
105 );
106
107 // Check if we can access the API.
108 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();
110 return false;
111 }
112
113 // Do checks for required classes / functions or similar.
114 // 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.';
117 return false;
118 }
119
120 global $wp_version;
121 if ( version_compare( $wp_version, '4.3.0', '<' ) ) {
122 $this->activation_errors[] = 'Please upgrade your WordPress site to at least 4.3.0.';
123 return false;
124 }
125
126 return true;
127 }
128
129 /**
130 * Adds a notice to the dashboard if the plugin requirements are not met.
131 *
132 * @return void
133 */
134 public function requirements_not_met_notice() {
135 // Deactivate the plugin.
136 deactivate_plugins( $this->plugin->basename );
137
138 // 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' );
140
141 // Print the errors on the screen.
142 echo wp_kses_post( $default_message );
143 echo wp_kses_post( implode( '<br />', $this->activation_errors ) );
144 }
145
146 /**
147 * Activate the plugin.
148 *
149 * @param Patchstack $core
150 * @return void
151 */
152 public function activate( $core ) {
153 // Bail early if requirements are not met.
154 if ( ! $this->check_requirements() ) {
155 $this->requirements_not_met_notice();
156 exit;
157 }
158
159 // Check if the webarx/webarx.php plugin is present, if so, remove it.
160 if ( is_dir( WP_PLUGIN_DIR . '/webarx' ) ) {
161
162 // Migrate all current options to the new prefix.
163 global $wpdb;
164 $exists = $wpdb->get_var( "SELECT COUNT(*) FROM " . $wpdb->prefix . "options WHERE option_name = 'webarx_api_token'" );
165
166 // Move over the options.
167 if ( !is_null( $exists ) && $exists >= 1 ) {
168 $wpdb->query( 'INSERT IGNORE INTO ' . $wpdb->prefix . "options (option_name, option_value, autoload) SELECT REPLACE(option_name, 'webarx_', 'patchstack_') as option_name, option_value, autoload FROM " . $wpdb->prefix . "options WHERE option_name like 'webarx_%'" );
169 $wpdb->query( 'UPDATE ' . $wpdb->prefix . 'options AS a SET option_value = (SELECT option_value FROM ' . $wpdb->prefix . "options WHERE option_name = REPLACE(a.option_name, 'patchstack_', 'webarx_')) WHERE option_name LIKE 'patchstack_%'" );
170 }
171
172 // Deactivate the plugin.
173 include_once ABSPATH . 'wp-admin/includes/plugin.php';
174 deactivate_plugins( [ 'webarx/webarx.php' ] );
175 update_option( 'patchstack_license_free', '0', true );
176 }
177
178 // Make sure any rewrite functionality has been loaded.
179 $this->migrate();
180 add_option( 'patchstack_first_activated', '1' );
181
182 // Whether or not we should send a secret key to our API.
183 $sendSecret = false;
184
185 // Activate the license.
186 if ( $this->plugin->client_id != 'PATCHSTACK_CLIENT_ID' && $this->plugin->private_key != 'PATCHSTACK_PRIVATE_KEY' ) {
187 $this->alter_license( $this->plugin->client_id, $this->plugin->private_key, 'activate' );
188 } elseif ( get_option( 'patchstack_clientid', false ) != false && get_option( 'patchstack_secretkey', false ) != false ) {
189 $this->alter_license( get_option( 'patchstack_clientid' ), $this->get_secret_key(), 'activate' );
190 } else {
191 $sendSecret = true;
192 update_option( 'patchstack_license_free', '1', true );
193 }
194
195 // Update firewall status after activating plugin
196 $api = new P_Api( $core );
197 $token = $api->get_access_token();
198 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 ) ;
205 }
206
207 // Immediately send software data to our server to set firewall as enabled.
208 // Also immediately download the whitelist file and the firewall rules.
209 do_action( 'patchstack_send_software_data' );
210 if ( get_option( 'patchstack_license_free', 0 ) != 1 ) {
211 do_action( 'patchstack_post_firewall_rules' );
212 do_action( 'patchstack_post_dynamic_firewall_rules' );
213 }
214
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;
219 }
220
221 // The mu-plugin does not exist, try to create it.
222 @include_once ABSPATH . 'wp-admin/includes/file.php';
223 $wpfs = WP_Filesystem();
224
225 // Failed to initialize WP_Filesystem.
226 if ( ! $wpfs ) {
227 return;
228 }
229
230 if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
231 wp_mkdir_p( WPMU_PLUGIN_DIR );
232 }
233
234 // Failed to create the mu-plugin folder.
235 if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
236 return;
237 }
238
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 }
244 }
245
246 /**
247 * Build the required Patchstack tables.
248 *
249 * @param null|string $ver The version to upgrade to.
250 * @param null|integer $site_id The blog id to perform the upgrades on.
251 * @return void
252 */
253 public function migrate( $ver = null, $site_id = null ) {
254 global $wpdb;
255 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
256 $charset_collate = $wpdb->get_charset_collate();
257 $prefix = $site_id != null ? $wpdb->get_blog_prefix( $site_id ) : $wpdb->prefix;
258
259 // The following conditions will only execute if Patchstack is installed because of an update
260 // and if we need to perform migrations.
261 if ( $ver !== null && file_exists( dirname( __FILE__ ) . '/migrations/v' . str_replace( '.', '', $ver ) . '.php' ) ) {
262 require_once dirname( __FILE__ ) . '/migrations/v' . str_replace( '.', '', $ver ) . '.php';
263 return;
264 }
265
266 // Require the base migration.
267 require_once dirname( __FILE__ ) . '/migrations/base.php';
268 }
269
270 /**
271 * Check if the database version of the plugin is running behind.
272 * If so, run the migrations up until the latest version.
273 *
274 * @return void
275 */
276 public function migrate_check() {
277 // 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'];
279 if ( count( $versions ) == 0 ) {
280 return;
281 }
282
283 // Get current database version and run the migrations.
284 $db_version = get_option( 'patchstack_db_version', false );
285 foreach ( $versions as $version ) {
286 if ( version_compare( $db_version, $version, '<' ) ) {
287 $this->migrate( $version );
288 }
289 }
290 }
291
292 /**
293 * Perform cleanup when the plugin is deactivated.
294 *
295 * @return void
296 */
297 public function deactivate() {
298 // 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 //
306 }
307
308 // 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' ];
310 foreach ( $tasks as $task ) {
311 wp_clear_scheduled_hook( $task );
312 }
313
314 // Cleanup the .htaccess file.
315 $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 }
325
326 /**
327 * Activate or deactivate a license on the current site.
328 *
329 * @param integer $id
330 * @param string $secret
331 * @param string $action
332 * @return array
333 */
334 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 // Store current keys in tmp variable so in case it fails, we can set it back.
339 $tmp_id = get_option( 'patchstack_clientid' );
340 $tmp_key = $this->get_secret_key();
341
342 // Set the new values.
343 update_option( 'patchstack_clientid', $id );
344 $this->set_secret_key( $secret );
345
346 // Activate the license.
347 if ( $action == 'activate' ) {
348 $api_result = $this->plugin->api->get_access_token( $id, $secret, true );
349
350 // Valid result?
351 if ( ! $api_result ) {
352 update_option( 'patchstack_clientid', $tmp_id );
353 $this->set_secret_key( $tmp_key );
354
355 return [
356 'result' => 'error',
357 'body' => json_encode($this->plugin->api->message),
358 'message' => 'Cannot activate license!',
359 ];
360 }
361
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.
370 $token = $this->plugin->api->get_access_token();
371 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 do_action( 'patchstack_send_software_data' );
376 if ( get_option( 'patchstack_license_free', 0 ) != 1 ) {
377 update_option( 'patchstack_basic_firewall', 1, true );
378 do_action( 'patchstack_post_firewall_rules' );
379 do_action( 'patchstack_post_dynamic_firewall_rules' );
380 $this->header();
381 }
382
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();
387 }
388
389 return [
390 'result' => 'success',
391 'message' => 'License activated!',
392 ];
393 }
394
395 // Deactivate the license.
396 if ( $action == 'deactivate' ) {
397 update_option( 'patchstack_api_token', '' );
398 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 [
406 'result' => 'success',
407 'message' => 'License deactivated!',
408 ];
409 }
410 }
411
412 /**
413 * Send a request to our API for the IP address header.
414 *
415 * @return void
416 */
417 public function header()
418 {
419 $header = get_option( 'patchstack_firewall_ip_header', '' );
420 $computed = get_option( 'patchstack_ip_header_computed', 0 );
421 $force = get_option( 'patchstack_ip_header_force_compute', 0 );
422
423 if ( ( $header == '' && ! $computed ) || $force ) {
424 // Create an OTT token.
425 $ott = md5( wp_generate_password( 32, true, true ) );
426 update_option( 'patchstack_ott_action', $ott );
427
428 // Tell our API.
429 wp_remote_request(
430 $this->plugin->api_url . '/api/header',
431 [
432 'method' => 'POST',
433 'timeout' => 60,
434 'redirection' => 5,
435 'httpversion' => '1.0',
436 'blocking' => true,
437 'headers' => [
438 'Source-Host' => get_site_url(),
439 ],
440 'body' => [
441 'token' => $ott,
442 'url' => get_site_url()
443 ],
444 'cookies' => [],
445 ]
446 );
447 }
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 }
858 }
859