PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.1
Patchstack – WordPress & Plugins Security v2.3.1
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.1, at includes/activation.php

879 lines 30.1 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 if ( $plugin == $this->plugin->basename && ! isset( $_REQUEST['_ajax_nonce'] ) ) {
47
48 // Determine if secret token was set, if so, sync with API.
49 $attemptAuto = false;
50 $secretToken = get_option( 'patchstack_activation_secret', '' );
51 if ( ! empty( $secretToken ) ) {
52 $attemptAuto = true;
53 }
54
55 // In case of multisite, we want to redirect the user to a different page.
56 if ( $network_activation ) {
57 wp_safe_redirect( network_admin_url( 'admin.php?page=patchstack-multisite-settings&tab=multisite&ps_activated=1' . ($attemptAuto ? '&ps_autoa=1' : '' ) ) );
58 } else {
59 wp_safe_redirect( admin_url( 'admin.php?page=' . $this->plugin->name . '&ps_activated=1' . ($attemptAuto ? '&ps_autoa=1' : '' ) ) );
60 }
61 exit;
62 }
63 }
64
65 /**
66 * Check if the plugin meets requirements and disable it if they are not present.
67 *
68 * @return boolean
69 */
70 public function check_requirements() {
71 if ( $this->meets_requirements() ) {
72 return true;
73 }
74
75 // Add a dashboard notice.
76 add_action( 'all_admin_notices', [ $this, 'requirements_not_met_notice' ] );
77 return false;
78 }
79
80 /**
81 * Check that all plugin requirements are met.
82 *
83 * @return boolean
84 */
85 public function meets_requirements() {
86 // Check to see if we can access the API.
87 $response = wp_remote_request(
88 $this->plugin->api_url,
89 [
90 'method' => 'GET',
91 'timeout' => 10,
92 'redirection' => 5,
93 ]
94 );
95
96 // Check if we can access the API.
97 if ( is_wp_error( $response ) ) {
98 $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();
99 return false;
100 }
101
102 // Do checks for required classes / functions or similar.
103 // Add detailed messages to $this->activation_errors array.
104 if ( version_compare( phpversion(), '5.6.0', '<' ) ) {
105 $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.';
106 return false;
107 }
108
109 global $wp_version;
110 if ( version_compare( $wp_version, '4.3.0', '<' ) ) {
111 $this->activation_errors[] = 'Please upgrade your WordPress site to at least 4.3.0.';
112 return false;
113 }
114
115 return true;
116 }
117
118 /**
119 * Adds a notice to the dashboard if the plugin requirements are not met.
120 *
121 * @return void
122 */
123 public function requirements_not_met_notice() {
124 // Deactivate the plugin.
125 deactivate_plugins( $this->plugin->basename );
126
127 // Compile default message.
128 $default_message = esc_attr__( 'Patchstack could not be activated due to a conflict. See below for information regarding the conflict.<br />', 'patchstack' );
129
130 // Print the errors on the screen.
131 echo wp_kses_post( $default_message );
132 echo wp_kses_post( implode( '<br />', $this->activation_errors ) );
133 }
134
135 /**
136 * Activate the plugin.
137 *
138 * @param Patchstack $core
139 * @return void
140 */
141 public function activate( $core ) {
142 // Bail early if requirements are not met.
143 if ( ! $this->check_requirements() ) {
144 $this->requirements_not_met_notice();
145 exit;
146 }
147
148 // Check if the webarx/webarx.php plugin is present, if so, remove it.
149 if ( is_dir( WP_PLUGIN_DIR . '/webarx' ) ) {
150
151 // Migrate all current options to the new prefix.
152 global $wpdb;
153 $exists = $wpdb->get_var( "SELECT COUNT(*) FROM " . $wpdb->prefix . "options WHERE option_name = 'webarx_api_token'" );
154
155 // Move over the options.
156 if ( !is_null( $exists ) && $exists >= 1 ) {
157 $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_%'" );
158 $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_%'" );
159 }
160
161 // Deactivate the plugin.
162 include_once ABSPATH . 'wp-admin/includes/plugin.php';
163 deactivate_plugins( [ 'webarx/webarx.php' ] );
164 update_option( 'patchstack_license_free', '0', true );
165 }
166
167 // Make sure any rewrite functionality has been loaded.
168 $this->migrate();
169 add_option( 'patchstack_first_activated', '1' );
170
171 // Whether or not we should send a secret key to our API.
172 $sendSecret = false;
173
174 // Activate the license.
175 if ( $this->plugin->client_id != 'PATCHSTACK_CLIENT_ID' && $this->plugin->private_key != 'PATCHSTACK_PRIVATE_KEY' ) {
176 $this->alter_license( $this->plugin->client_id, $this->plugin->private_key, 'activate' );
177 } elseif ( get_option( 'patchstack_clientid', false ) != false && get_option( 'patchstack_secretkey', false ) != false ) {
178 $this->alter_license( get_option( 'patchstack_clientid' ), $this->get_secret_key(), 'activate' );
179 } else {
180 $sendSecret = true;
181 update_option( 'patchstack_license_free', '1', true );
182 }
183
184 // Update firewall status after activating plugin
185 $api = new P_Api( $core );
186 $token = $api->get_access_token();
187 if ( ! empty( $token ) ) {
188 $api->update_firewall_status( [ 'status' => 1 ] );
189 $api->update_url( [ 'plugin_url' => get_option( 'siteurl' ) ] );
190 } elseif ( $sendSecret ) {
191 $secretToken = wp_generate_password( 36, true );
192 update_option( 'patchstack_activation_secret', $secretToken );
193 update_option( 'patchstack_activation_time', time() + 59 ) ;
194 }
195
196 // Immediately send software data to our server to set firewall as enabled.
197 // Also immediately download the whitelist file and the firewall rules.
198 do_action( 'patchstack_send_software_data' );
199 if ( get_option( 'patchstack_license_free', 0 ) != 1 ) {
200 do_action( 'patchstack_post_firewall_rules' );
201 do_action( 'patchstack_post_dynamic_firewall_rules' );
202 }
203
204 // Try to create the mu-plugins folder/file.
205 // No need to do this if it already exists.
206 if ( file_exists( WPMU_PLUGIN_DIR . '/patchstack.php' ) || file_exists( WPMU_PLUGIN_DIR . '/_patchstack.php' ) || ( defined( 'PS_DISABLE_MU' ) && PS_DISABLE_MU ) ) {
207 return;
208 }
209
210 // The mu-plugin does not exist, try to create it.
211 @include_once ABSPATH . 'wp-admin/includes/file.php';
212 $wpfs = WP_Filesystem();
213
214 // Failed to initialize WP_Filesystem.
215 if ( ! $wpfs ) {
216 return;
217 }
218
219 if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
220 wp_mkdir_p( WPMU_PLUGIN_DIR );
221 }
222
223 // Failed to create the mu-plugin folder.
224 if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
225 return;
226 }
227
228 // Create the mu-plugin file in the folder.
229 if ( is_writable( WPMU_PLUGIN_DIR ) ) {
230 $php = @file_get_contents( trailingslashit( plugin_dir_path( __FILE__ ) ) . 'mu-plugin.php' );
231 @file_put_contents( trailingslashit( WPMU_PLUGIN_DIR ) . '_patchstack.php', $php );
232 }
233 }
234
235 /**
236 * Used to activate an individual license on multisite/network.
237 *
238 * @param object $site
239 * @param array $license
240 * @return void
241 */
242 public function activate_multisite_license( $site, $license ) {
243 // Build the Patchstack tables on the site.
244 $this->migrate( null, $site->id );
245
246 // Add the options to given site.
247 foreach ( $this->plugin->admin_options->options as $name => $value ) {
248 add_blog_option( $site->id, $name, $value['default'] );
249 }
250
251 // Set the client id and secret key.
252 update_blog_option( $site->id, 'patchstack_clientid', $license['id'] );
253 $enc = $this->get_secret_key( $license['secret'] );
254 update_blog_option( $site->id, 'patchstack_secretkey', $enc['cipher'] );
255 update_blog_option( $site->id, 'patchstack_secretkey_nonce', $enc['nonce'] );
256
257 $this->plugin->api->blog_id = $site->id;
258
259 // Activate the license and update firewall status after activating the plugin.
260 $token = $this->plugin->api->get_access_token( $license['id'], $license['secret'], true );
261 if ( ! empty( $token ) ) {
262 $this->plugin->api->update_firewall_status( [ 'status' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ] );
263 $this->plugin->api->update_url( [ 'plugin_url' => get_blog_option( $site->id, 'siteurl' ) ] );
264
265 // If we have an access token, tell our API that the firewall is activated
266 // and the current URL of the site.
267 update_blog_option( $site->id, 'patchstack_license_activated', '1' );
268 $this->plugin->api->update_license_status();
269
270 // This will trigger the software synchronization action.
271 wp_remote_get( get_site_url( $site->id ), [ 'sslverify' => false ] );
272 }
273
274 // Make sure to switch back to the current blog id.
275 $this->plugin->api->blog_id = get_current_blog_id();
276 }
277
278 /**
279 * Build the required Patchstack tables.
280 *
281 * @param null|string $ver The version to upgrade to.
282 * @param null|integer $site_id The blog id to perform the upgrades on.
283 * @return void
284 */
285 public function migrate( $ver = null, $site_id = null ) {
286 global $wpdb;
287 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
288 $charset_collate = $wpdb->get_charset_collate();
289 $prefix = $site_id != null ? $wpdb->get_blog_prefix( $site_id ) : $wpdb->prefix;
290
291 // The following conditions will only execute if Patchstack is installed because of an update
292 // and if we need to perform migrations.
293 if ( $ver !== null && file_exists( dirname( __FILE__ ) . '/migrations/v' . str_replace( '.', '', $ver ) . '.php' ) ) {
294 require_once dirname( __FILE__ ) . '/migrations/v' . str_replace( '.', '', $ver ) . '.php';
295 return;
296 }
297
298 // Require the base migration.
299 require_once dirname( __FILE__ ) . '/migrations/base.php';
300 }
301
302 /**
303 * Check if the database version of the plugin is running behind.
304 * If so, run the migrations up until the latest version.
305 *
306 * @return void
307 */
308 public function migrate_check() {
309 // Only perform migrations if we have any to execute.
310 $versions = ['3.0.0', '3.0.1', '3.0.2', '3.0.3', '3.0.4'];
311 if ( count( $versions ) == 0 ) {
312 return;
313 }
314
315 // Get current database version and run the migrations.
316 $db_version = get_option( 'patchstack_db_version', false );
317 foreach ( $versions as $version ) {
318 if ( version_compare( $db_version, $version, '<' ) ) {
319 $this->migrate( $version );
320 }
321 }
322 }
323
324 /**
325 * Perform cleanup when the plugin is deactivated.
326 *
327 * @return void
328 */
329 public function deactivate() {
330 // Update firewall status after de-activating plugin
331 try {
332 $token = $this->plugin->api->get_access_token();
333 if ( ! empty( $token ) ) {
334 $this->plugin->api->update_firewall_status( [ 'status' => 0 ] );
335 }
336 } catch (\Exception $e) {
337 //
338 }
339
340 // Clear all Patchstack scheduled tasks.
341 $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' ];
342 foreach ( $tasks as $task ) {
343 wp_clear_scheduled_hook( $task );
344 }
345
346 // Cleanup the .htaccess file.
347 $this->plugin->htaccess->cleanup_htaccess_file();
348 $this->auto_prepend_removal();
349
350 // Remove the mu-plugin file if it exists.
351 foreach (['patchstack.php', '_patchstack.php'] as $file) {
352 if ( file_exists( WPMU_PLUGIN_DIR . '/' . $file )) {
353 wp_delete_file( WPMU_PLUGIN_DIR . '/' . $file );
354 }
355 }
356 }
357
358 /**
359 * Activate or deactivate a license on the current site.
360 *
361 * @param integer $id
362 * @param string $secret
363 * @param string $action
364 * @return array
365 */
366 public function alter_license( $id, $secret, $action ) {
367 // Set default options in case they have not been set yet.
368 $this->plugin->admin_options->settings_init();
369
370 // Store current keys in tmp variable so in case it fails, we can set it back.
371 $tmp_id = get_option( 'patchstack_clientid' );
372 $tmp_key = $this->get_secret_key();
373
374 // Set the new values.
375 update_option( 'patchstack_clientid', $id );
376 $this->set_secret_key( $secret );
377
378 // Activate the license.
379 if ( $action == 'activate' ) {
380 $api_result = $this->plugin->api->get_access_token( $id, $secret, true );
381
382 // Valid result?
383 if ( ! $api_result ) {
384 update_option( 'patchstack_clientid', $tmp_id );
385 $this->set_secret_key( $tmp_key );
386
387 return [
388 'result' => 'error',
389 'body' => json_encode($this->plugin->api->message),
390 'message' => 'Cannot activate license!',
391 ];
392 }
393
394 // If we have an access token, tell our API that the firewall is activated
395 // and the current URL of the site.
396 update_option( 'patchstack_license_activated', '1', true );
397 $this->plugin->api->update_license_status();
398 $token = $this->plugin->api->get_access_token();
399 if ( ! empty( $token ) ) {
400 do_action( 'patchstack_send_software_data' );
401 if ( get_option( 'patchstack_license_free', 0 ) != 1 ) {
402 update_option( 'patchstack_basic_firewall', 1, true );
403 do_action( 'patchstack_post_firewall_rules' );
404 do_action( 'patchstack_post_dynamic_firewall_rules' );
405 $this->header();
406 }
407
408 $this->plugin->api->update_firewall_status( [ 'status' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ] );
409 $this->plugin->api->update_url( [ 'plugin_url' => get_option( 'siteurl' ) ] );
410 $this->plugin->api->ping();
411 $this->auto_prepend_injection();
412 }
413
414 return [
415 'result' => 'success',
416 'message' => 'License activated!',
417 ];
418 }
419
420 // Deactivate the license.
421 if ( $action == 'deactivate' ) {
422 update_option( 'patchstack_api_token', '' );
423 update_option( 'patchstack_license_activated', '0', true );
424 $this->auto_prepend_removal();
425
426 return [
427 'result' => 'success',
428 'message' => 'License deactivated!',
429 ];
430 }
431 }
432
433 /**
434 * Send a request to our API for the IP address header.
435 *
436 * @return void
437 */
438 public function header()
439 {
440 $header = get_option( 'patchstack_firewall_ip_header', '' );
441 $computed = get_option( 'patchstack_ip_header_computed', 0 );
442 $force = get_option( 'patchstack_ip_header_force_compute', 0 );
443
444 if ( ( $header == '' && ! $computed ) || $force ) {
445 // Create an OTT token.
446 $ott = md5( wp_generate_password( 32, true, true ) );
447 update_option( 'patchstack_ott_action', $ott );
448
449 // Tell our API.
450 wp_remote_request(
451 $this->plugin->api_url . '/api/header',
452 [
453 'method' => 'POST',
454 'timeout' => 60,
455 'redirection' => 5,
456 'httpversion' => '1.0',
457 'blocking' => true,
458 'headers' => [
459 'Source-Host' => get_site_url(),
460 ],
461 'body' => [
462 'token' => $ott,
463 'url' => get_site_url()
464 ],
465 'cookies' => [],
466 ]
467 );
468 }
469 }
470
471 /**
472 * Create the environment needed for the auto prepend firewall functionality.
473 * 1. First we check if an auto_prepend_file already exists somewhere.
474 * 2. Then we write to the .htaccess file and check its status code.
475 * 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.
476 *
477 * @param boolean $refresh
478 * @return boolean
479 */
480 public function auto_prepend_injection($refresh = false)
481 {
482 // Determine if AP firewall is enabled.
483 if ( ! get_option( 'patchstack_firewall_ap_enabled', false ) ) {
484 return;
485 }
486
487 // Determine if we received an error that hasn't been cleared yet.
488 if ( get_option( 'patchstack_firewall_ap_error', '' ) != '' ) {
489 return;
490 }
491
492 // No need to display this error if the .htaccess functionality has been disabled.
493 if ( get_option( 'patchstack_disable_htaccess', 0 ) || ( defined( 'PS_DISABLE_HTACCESS' ) && PS_DISABLE_HTACCESS ) || ( defined( 'PS_DISABLE_MU' ) && PS_DISABLE_MU ) ) {
494 return;
495 }
496
497 // Get filesystem.
498 global $wp_filesystem;
499 if ( ! $this->get_filesystem() ) {
500 update_option( 'patchstack_firewall_ap_error', 'Could not establish filesystem.' );
501 return false;
502 }
503
504 // First ensure a .htaccess file exists, otherwise no point.
505 $htaccess_file = ABSPATH . '.htaccess';
506 if ( ! $wp_filesystem->exists( $htaccess_file ) && ! $wp_filesystem->touch( $htaccess_file ) ) {
507 update_option( 'patchstack_firewall_ap_error', 'The .htaccess file could be found nor created.' );
508 return false;
509 }
510
511 // Completely halt if there is already an auto_prepend_file present in .htaccess and not of Patchstack.
512 $htaccess_content = $wp_filesystem->get_contents( $htaccess_file );
513 if ( stripos( $htaccess_content, 'auto_prepend_file' ) !== false && stripos( $htaccess_content, 'mu-plugin-ap.php' ) === false ) {
514 update_option( 'patchstack_firewall_ap_error', 'A different auto_prepend_file value is already present in the .htaccess file.' );
515 return false;
516 }
517
518 // Completely halt if there is already an auto_prepend_file present in .user.ini and not of Patchstack.
519 $user_ini = ini_get( 'user_ini.filename' );
520 if ( $user_ini && $wp_filesystem->exists( ABSPATH . $user_ini ) ) {
521 $ini_content = $wp_filesystem->get_contents( ABSPATH . $user_ini );
522 if ( stripos( $ini_content, 'auto_prepend_file' ) !== false && stripos( $ini_content, 'mu-plugin-ap.php' ) === false ) {
523 update_option( 'patchstack_firewall_ap_error', 'A different auto_prepend_file value is already present in the .user.ini file.' );
524 return false;
525 }
526 }
527
528 // Determine if we can write the /wp-content/pslogs/ folder.
529 $logs_dir = WP_CONTENT_DIR . '/pslogs/';
530 if ( ! $wp_filesystem->exists( $logs_dir ) && ! $wp_filesystem->mkdir( $logs_dir ) ) {
531 update_option( 'patchstack_firewall_ap_error', 'The path ' . $logs_dir . ' could not be created.' );
532 return false;
533 }
534
535 // Create the blank index.php file.
536 if ( ! $wp_filesystem->exists( $logs_dir . 'index.php' ) && ! $wp_filesystem->put_contents( $logs_dir . 'index.php', '' ) ) {
537 update_option( 'patchstack_firewall_ap_error', 'The file ' . $logs_dir . 'index.php could not be created.' );
538 return false;
539 }
540
541 // Create the logs.php file.
542 if ( ! $wp_filesystem->exists( $logs_dir . 'logs.php' ) && ! $wp_filesystem->put_contents( $logs_dir . 'logs.php', '<?php exit; ?>' . PHP_EOL ) ) {
543 update_option( 'patchstack_firewall_ap_error', 'The file ' . $logs_dir . 'logs.php could not be created.' );
544 return false;
545 }
546
547 // Save current site id.
548 $current_id = get_current_blog_id();
549
550 // Pull data to save into the config.php file.
551 $sites = $this->get_sites();
552 $data = [];
553 foreach ($sites as $site) {
554 $this->switch_to_blog( $site->id );
555 $data[] = [
556 'site_id' => $site->id,
557 'site_url' => preg_replace( '/^https?:\/\//i', '', $site->siteurl ),
558 'home_url' => preg_replace( '/^https?:\/\//i', '', get_option( 'home' ) ),
559 'patchstack_basic_firewall' => get_option( 'patchstack_basic_firewall', 1 ),
560 'patchstack_license_activated' => get_option( 'patchstack_license_activated', 0 ),
561 'patchstack_license_free' => get_option( 'patchstack_license_free', 0 ),
562 'patchstack_firewall_ip_header' => get_option( 'patchstack_firewall_ip_header', '' ),
563 'patchstack_firewall_rules_v3_ap' => base64_encode( get_option( 'patchstack_firewall_rules_v3_ap', '[]' ) )
564 ];
565 }
566
567 // Switch back to current site.
568 $this->switch_to_blog( $current_id );
569
570 // Save into the config.php file.
571 if ( ! $wp_filesystem->put_contents( $logs_dir . 'config.php', '<?php return ' . var_export( $data, true ) . ';' ) ) {
572 update_option( 'patchstack_firewall_ap_error', 'The file ' . $logs_dir . 'config.php could not be created.' );
573 return false;
574 }
575
576 // In case we only want to refresh the auto prepend rules, we stop here.
577 if ( $refresh ) {
578 return true;
579 }
580
581 // Prepare the rules to inject into .htaccess.
582 $prepend_rules = $this->get_auto_prepend_rules();
583 if ( ! $prepend_rules ) {
584 return false;
585 }
586
587 // Determine if the rules already exist and overwrite them in case of path change.
588 $original_htaccess = $htaccess_content;
589 $re = '/# BEGIN AP Patchstack.*?# END AP Patchstack/is';
590 if ( preg_match( $re, $htaccess_content ) ) {
591 $htaccess_content = preg_replace( $re, rtrim($prepend_rules['htaccess']), $htaccess_content );
592 } else {
593 $htaccess_content .= "\n" . $prepend_rules['htaccess'];
594 }
595
596 // Attempt to write to the .htaccess file.
597 if ( ! $wp_filesystem->put_contents( $htaccess_file, $htaccess_content ) ) {
598 update_option( 'patchstack_firewall_ap_error', 'Could not inject into the .htaccess file.' );
599 return false;
600 }
601
602 // Determine if the site still works as expected with the injected htaccess rules.
603 if ( $this->get_site_status_code() >= 400 ) {
604 $wp_filesystem->put_contents( $htaccess_file, $original_htaccess );
605 update_option( 'patchstack_firewall_ap_error', 'The .htaccess rules caused a fatal internal server error.' );
606 return false;
607 }
608
609 // Ensure a .user.ini is present.
610 $user_ini = ini_get( 'user_ini.filename' );
611 if ( ! $user_ini ) {
612 update_option( 'patchstack_firewall_ap_error', '' );
613 return true;
614 }
615
616 // Define full path to the .user.ini file.
617 $user_ini = ABSPATH . $user_ini;
618
619 // Create the file if it does not exist.
620 if ( ! $wp_filesystem->exists( $user_ini ) && ! $wp_filesystem->touch( $user_ini ) ) {
621 update_option( 'patchstack_firewall_ap_error', 'The .user.ini file could not be created.' );
622 return true;
623 }
624
625 // Get the contents of the current .user.ini file.
626 $ini_content = $wp_filesystem->get_contents( $user_ini );
627
628 // Determine if the rules already exist and overwrite them in case of path change.
629 $original_ini = $ini_content;
630 $re = '/; BEGIN AP Patchstack.*?; END AP Patchstack/is';
631 if ( preg_match( $re, $ini_content ) ) {
632 $ini_content = preg_replace( $re, rtrim($prepend_rules['ini']), $ini_content );
633 } else {
634 $ini_content .= "\n" . $prepend_rules['ini'];
635 }
636
637 // Attempt to write to the .user.ini file.
638 if ( ! $wp_filesystem->put_contents( $user_ini, $ini_content ) ) {
639 update_option( 'patchstack_firewall_ap_error', 'Could not inject into the .user.ini file.' );
640 return true;
641 }
642
643 // Determine if the site still works as expected with the injected .user.ini rules.
644 if ( $this->get_site_status_code() == 500 ) {
645 $wp_filesystem->put_contents( $user_ini, $original_ini );
646 update_option( 'patchstack_firewall_ap_error', 'The .user.ini rules caused a fatal internal server error.' );
647 return false;
648 }
649
650 update_option( 'patchstack_firewall_ap_error', '' );
651 return true;
652 }
653
654 /**
655 * Remove everything related to the auto prepend functionality.
656 *
657 * @return boolean
658 */
659 public function auto_prepend_removal()
660 {
661 global $wp_filesystem;
662 $this->get_filesystem();
663
664 // Define our paths to access.
665 $logs_dir = WP_CONTENT_DIR . '/pslogs/';
666 $htaccess_file = ABSPATH . '.htaccess';
667 $ini_file = ABSPATH . '.user.ini';
668
669 // Remove the entire /pslogs/ directory.
670 if ( $wp_filesystem->is_dir( $logs_dir ) ) {
671 $wp_filesystem->delete( $logs_dir, true );
672 }
673
674 // Remove the .htaccess injected rules.
675 if ( $wp_filesystem->is_file( $htaccess_file ) ) {
676 $htaccess_content = $wp_filesystem->get_contents( $htaccess_file );
677 $re = '/# BEGIN AP Patchstack.*?# END AP Patchstack/is';
678 if ( preg_match( $re, $htaccess_content ) ) {
679 $htaccess_content = preg_replace( $re, '', $htaccess_content );
680 $wp_filesystem->put_contents( $htaccess_file, $htaccess_content );
681 }
682 }
683
684 // Remove the .user.ini injected rules.
685 if ( $wp_filesystem->is_file( $ini_file ) ) {
686 $ini_content = $wp_filesystem->get_contents( $ini_file );
687 $re = '/; BEGIN AP Patchstack.*?; END AP Patchstack/is';
688 if ( preg_match( $re, $ini_content ) ) {
689 $ini_content = preg_replace( $re, '', $ini_content );
690 $wp_filesystem->put_contents( $ini_file, $ini_content );
691 }
692 }
693 }
694
695 /**
696 * Attempt to establish the proper WP_FileSystem.
697 *
698 * @return boolean
699 */
700 private function get_filesystem()
701 {
702 // Seems to be the only native way to obtain FTP credentials, if defined.
703 include_once( ABSPATH . 'wp-admin/includes/file.php' );
704 ob_start();
705 $creds = request_filesystem_credentials( admin_url( 'admin-ajax.php' ), '', false, ABSPATH, null, true );
706 ob_end_clean();
707
708 // Returns false if no filesystem connection could be determined.
709 if ( $creds === false ) {
710 update_option( 'patchstack_firewall_ap_error', 'Unable to establish filesystem connection.' );
711 return false;
712 }
713
714 // Attempt to initialize it.
715 $fs = WP_Filesystem( $creds, ABSPATH, true );
716 if ( ! $fs ) {
717 update_option( 'patchstack_firewall_ap_error', 'Unable to establish filesystem connection through acquired creds.' );
718 return false;
719 }
720
721 return true;
722 }
723
724 /**
725 * Get sites as part of the environment.
726 *
727 * @return array
728 */
729 private function get_sites()
730 {
731 if ( ! function_exists( 'get_sites' ) ) {
732 return [
733 (object) [
734 'id' => 0,
735 'siteurl' => get_site_url()
736 ]
737 ];
738 }
739
740 return get_sites();
741 }
742
743 /**
744 * Switch to a different site.
745 *
746 * @param integer $site_id
747 * @return void
748 */
749 private function switch_to_blog($site_id)
750 {
751 if ( ! function_exists( 'switch_to_blog' ) ) {
752 return;
753 }
754
755 switch_to_blog( $site_id );
756 }
757
758 /**
759 * Determine the web-server software and make sure we support it before we generate the .htaccess rules for it.
760 *
761 * @return array|boolean
762 */
763 private function get_auto_prepend_rules()
764 {
765 // Establish location of the auto prepend file.
766 $mu_file = __DIR__ . '/mu-plugin-ap.php';
767 if ( ! file_exists( $mu_file ) ) {
768 return false;
769 }
770
771 // Ensure that the SERVER_SOFTWARE value is set.
772 $software = isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : '';
773 if ( ! $software ) {
774 update_option( 'patchstack_firewall_ap_error', 'Unsupported SERVER_SOFTWARE, found: ' . $_SERVER['SERVER_SOFTWARE'] );
775 return false;
776 }
777
778 // At this time, reject non-Apache environments.
779 $sapi = function_exists( 'php_sapi_name' ) ? php_sapi_name() : false;
780 if ( ! $sapi || stripos( $_SERVER['SERVER_SOFTWARE'], 'litespeed' ) === false && $sapi != 'litespeed' && stripos($_SERVER['SERVER_SOFTWARE'], 'apache' ) === false) {
781 update_option( 'patchstack_firewall_ap_error', 'Unsupported SERVER_SOFTWARE, found: ' . $_SERVER['SERVER_SOFTWARE'] . ' and ' . $sapi );
782 return false;
783 }
784
785 // Seperate flag for LiteSpeed.
786 $is_litespeed = stripos( $_SERVER['SERVER_SOFTWARE'], 'litespeed' ) !== false || $sapi == 'litespeed';
787
788 // Attempt to find the Apache version, < 2.4 does not support <If>.
789 // This depends on ServerTokens value, so only stop execution if we can't find the specific unsupported versions.
790 $version = function_exists( 'apache_get_version' ) ? apache_get_version() : $_SERVER['SERVER_SOFTWARE'];
791 if ( stripos( $version, 'Apache/2.4' ) === false ) {
792 update_option( 'patchstack_firewall_ap_error', 'Unsupported SERVER_SOFTWARE, found: ' . $_SERVER['SERVER_SOFTWARE'] );
793 return false;
794 }
795
796 // Add c-style slashes.
797 $mu_file_as = wp_normalize_path(addcslashes($mu_file, "'"));
798
799 // Bit different rules for LiteSpeed.
800 if ( ! $is_litespeed ) {
801 $rules = "<IfModule mod_php.c>
802 php_value auto_prepend_file '" . $mu_file_as . "'
803 </IfModule>
804 <IfModule mod_php5.c>
805 php_value auto_prepend_file '" . $mu_file_as . "'
806 </IfModule>
807 <IfModule mod_php7.c>
808 php_value auto_prepend_file '" . $mu_file_as . "'
809 </IfModule>";
810 } else {
811 $rules = "<IfModule LiteSpeed>
812 php_value auto_prepend_file '" . $mu_file_as . "'
813 </IfModule>
814 <IfModule lsapi_module>
815 php_value auto_prepend_file '" . $mu_file_as . "'
816 </IfModule>";
817 }
818
819 return [
820 'htaccess' => "# BEGIN AP Patchstack
821 <IfModule mod_authz_core.c>
822 <If \"-f '" . $mu_file_as . "'\">
823 " . $rules . "
824
825 <Files \".user.ini\">
826 <IfModule mod_authz_core.c>
827 Require all denied
828 </IfModule>
829 <IfModule !mod_authz_core.c>
830 Order deny,allow
831 Deny from all
832 </IfModule>
833 </Files>
834 </If>
835 </IfModule>
836 # END AP Patchstack
837 ",
838 'ini' => "; BEGIN AP Patchstack
839 auto_prepend_file = '" . $mu_file_as . "'
840 ; END AP Patchstack
841 "
842 ];
843 }
844
845 /**
846 * Retrieve the status code of the site.
847 * This is done to determine if the .htaccess rules do not trigger an error.
848 *
849 * @return integer
850 */
851 public function get_site_status_code() {
852 $response = wp_remote_get( get_site_url() );
853 $http_code = wp_remote_retrieve_response_code( $response );
854 return $http_code;
855 }
856
857 /**
858 * If option is updated, refresh AP config file.
859 *
860 * @param string $option_name
861 * @param string $option_name
862 * @param mixed $value
863 * @return void
864 */
865 public function updated_option( $option_name, $old_value, $value ) {
866 // Only allow to run for our options.
867 if ( !in_array( $option_name, [ 'patchstack_basic_firewall', 'patchstack_license_free', 'patchstack_firewall_rules_v3_ap' ] ) ) {
868 return;
869 }
870
871 // Not strict type matching.
872 if ( $old_value == $value ) {
873 return;
874 }
875
876 $this->auto_prepend_injection(true);
877 }
878 }
879