PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.5
Patchstack – WordPress & Plugins Security v2.3.5
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.5, at includes/activation.php

854 lines 29.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 // 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', '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 = 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', true );
399 $this->auto_prepend_removal();
400
401 return [
402 'result' => 'success',
403 'message' => 'License deactivated!',
404 ];
405 }
406 }
407
408 /**
409 * Send a request to our API for the IP address header.
410 *
411 * @return void
412 */
413 public function header()
414 {
415 $header = get_option( 'patchstack_firewall_ip_header', '' );
416 $computed = get_option( 'patchstack_ip_header_computed', 0 );
417 $force = get_option( 'patchstack_ip_header_force_compute', 0 );
418
419 if ( ( $header == '' && ! $computed ) || $force ) {
420 // Create an OTT token.
421 $ott = md5( wp_generate_password( 32, true, true ) );
422 update_option( 'patchstack_ott_action', $ott );
423
424 // Tell our API.
425 wp_remote_request(
426 $this->plugin->api_url . '/api/header',
427 [
428 'method' => 'POST',
429 'timeout' => 60,
430 'redirection' => 5,
431 'httpversion' => '1.0',
432 'blocking' => true,
433 'headers' => [
434 'Source-Host' => get_site_url(),
435 ],
436 'body' => [
437 'token' => $ott,
438 'url' => get_site_url()
439 ],
440 'cookies' => [],
441 ]
442 );
443 }
444 }
445
446 /**
447 * Create the environment needed for the auto prepend firewall functionality.
448 * 1. First we check if an auto_prepend_file already exists somewhere.
449 * 2. Then we write to the .htaccess file and check its status code.
450 * 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.
451 *
452 * @param boolean $refresh
453 * @return boolean
454 */
455 public function auto_prepend_injection($refresh = false)
456 {
457 // Determine if AP firewall is enabled.
458 if ( ! get_option( 'patchstack_firewall_ap_enabled', false ) ) {
459 return;
460 }
461
462 // Determine if we received an error that hasn't been cleared yet.
463 if ( get_option( 'patchstack_firewall_ap_error', '' ) != '' ) {
464 return;
465 }
466
467 // No need to display this error if the .htaccess functionality has been disabled.
468 if ( get_option( 'patchstack_disable_htaccess', 0 ) || ( defined( 'PS_DISABLE_HTACCESS' ) && PS_DISABLE_HTACCESS ) || ( defined( 'PS_DISABLE_MU' ) && PS_DISABLE_MU ) ) {
469 return;
470 }
471
472 // Get filesystem.
473 global $wp_filesystem;
474 if ( ! $this->get_filesystem() ) {
475 update_option( 'patchstack_firewall_ap_error', 'Could not establish filesystem.' );
476 return false;
477 }
478
479 // First ensure a .htaccess file exists, otherwise no point.
480 $htaccess_file = ABSPATH . '.htaccess';
481 if ( ! $wp_filesystem->exists( $htaccess_file ) && ! $wp_filesystem->touch( $htaccess_file ) ) {
482 update_option( 'patchstack_firewall_ap_error', 'The .htaccess file could be found nor created.' );
483 return false;
484 }
485
486 // Completely halt if there is already an auto_prepend_file present in .htaccess and not of Patchstack.
487 $htaccess_content = $wp_filesystem->get_contents( $htaccess_file );
488 if ( stripos( $htaccess_content, 'auto_prepend_file' ) !== false && stripos( $htaccess_content, 'mu-plugin-ap.php' ) === false ) {
489 update_option( 'patchstack_firewall_ap_error', 'A different auto_prepend_file value is already present in the .htaccess file.' );
490 return false;
491 }
492
493 // Completely halt if there is already an auto_prepend_file present in .user.ini and not of Patchstack.
494 $user_ini = ini_get( 'user_ini.filename' );
495 if ( $user_ini && $wp_filesystem->exists( ABSPATH . $user_ini ) ) {
496 $ini_content = $wp_filesystem->get_contents( ABSPATH . $user_ini );
497 if ( stripos( $ini_content, 'auto_prepend_file' ) !== false && stripos( $ini_content, 'mu-plugin-ap.php' ) === false ) {
498 update_option( 'patchstack_firewall_ap_error', 'A different auto_prepend_file value is already present in the .user.ini file.' );
499 return false;
500 }
501 }
502
503 // Determine if we can write the /wp-content/pslogs/ folder.
504 $logs_dir = WP_CONTENT_DIR . '/pslogs/';
505 if ( ! $wp_filesystem->exists( $logs_dir ) && ! $wp_filesystem->mkdir( $logs_dir ) ) {
506 update_option( 'patchstack_firewall_ap_error', 'The path ' . $logs_dir . ' could not be created.' );
507 return false;
508 }
509
510 // Create the blank index.php file.
511 if ( ! $wp_filesystem->exists( $logs_dir . 'index.php' ) && ! $wp_filesystem->put_contents( $logs_dir . 'index.php', '' ) ) {
512 update_option( 'patchstack_firewall_ap_error', 'The file ' . $logs_dir . 'index.php could not be created.' );
513 return false;
514 }
515
516 // Create the logs.php file.
517 if ( ! $wp_filesystem->exists( $logs_dir . 'logs.php' ) && ! $wp_filesystem->put_contents( $logs_dir . 'logs.php', '<?php exit; ?>' . PHP_EOL ) ) {
518 update_option( 'patchstack_firewall_ap_error', 'The file ' . $logs_dir . 'logs.php could not be created.' );
519 return false;
520 }
521
522 // Save current site id.
523 $current_id = get_current_blog_id();
524
525 // Pull data to save into the config.php file.
526 $sites = $this->get_sites();
527 $data = [];
528 foreach ($sites as $site) {
529 $this->switch_to_blog( $site->id );
530 $data[] = [
531 'site_id' => $site->id,
532 'site_url' => preg_replace( '/^https?:\/\//i', '', $site->siteurl ),
533 'home_url' => preg_replace( '/^https?:\/\//i', '', get_option( 'home' ) ),
534 'patchstack_basic_firewall' => get_option( 'patchstack_basic_firewall', 1 ),
535 'patchstack_license_activated' => get_option( 'patchstack_license_activated', 0 ),
536 'patchstack_license_free' => get_option( 'patchstack_license_free', 0 ),
537 'patchstack_firewall_ip_header' => get_option( 'patchstack_firewall_ip_header', '' ),
538 'patchstack_firewall_rules_v3_ap' => base64_encode( get_option( 'patchstack_firewall_rules_v3_ap', '[]' ) )
539 ];
540 }
541
542 // Switch back to current site.
543 $this->switch_to_blog( $current_id );
544
545 // Save into the config.php file.
546 if ( ! $wp_filesystem->put_contents( $logs_dir . 'config.php', '<?php return ' . var_export( $data, true ) . ';' ) ) {
547 update_option( 'patchstack_firewall_ap_error', 'The file ' . $logs_dir . 'config.php could not be created.' );
548 return false;
549 }
550
551 // In case we only want to refresh the auto prepend rules, we stop here.
552 if ( $refresh ) {
553 return true;
554 }
555
556 // Prepare the rules to inject into .htaccess.
557 $prepend_rules = $this->get_auto_prepend_rules();
558 if ( ! $prepend_rules ) {
559 return false;
560 }
561
562 // Determine if the rules already exist and overwrite them in case of path change.
563 $original_htaccess = $htaccess_content;
564 $re = '/# BEGIN AP Patchstack.*?# END AP Patchstack/is';
565 if ( preg_match( $re, $htaccess_content ) ) {
566 $htaccess_content = preg_replace( $re, rtrim($prepend_rules['htaccess']), $htaccess_content );
567 } else {
568 $htaccess_content .= "\n" . $prepend_rules['htaccess'];
569 }
570
571 // Attempt to write to the .htaccess file.
572 if ( ! $wp_filesystem->put_contents( $htaccess_file, $htaccess_content ) ) {
573 update_option( 'patchstack_firewall_ap_error', 'Could not inject into the .htaccess file.' );
574 return false;
575 }
576
577 // Determine if the site still works as expected with the injected htaccess rules.
578 if ( $this->get_site_status_code() >= 400 ) {
579 $wp_filesystem->put_contents( $htaccess_file, $original_htaccess );
580 update_option( 'patchstack_firewall_ap_error', 'The .htaccess rules caused a fatal internal server error.' );
581 return false;
582 }
583
584 // Ensure a .user.ini is present.
585 $user_ini = ini_get( 'user_ini.filename' );
586 if ( ! $user_ini ) {
587 update_option( 'patchstack_firewall_ap_error', '' );
588 return true;
589 }
590
591 // Define full path to the .user.ini file.
592 $user_ini = ABSPATH . $user_ini;
593
594 // Create the file if it does not exist.
595 if ( ! $wp_filesystem->exists( $user_ini ) && ! $wp_filesystem->touch( $user_ini ) ) {
596 update_option( 'patchstack_firewall_ap_error', 'The .user.ini file could not be created.' );
597 return true;
598 }
599
600 // Get the contents of the current .user.ini file.
601 $ini_content = $wp_filesystem->get_contents( $user_ini );
602
603 // Determine if the rules already exist and overwrite them in case of path change.
604 $original_ini = $ini_content;
605 $re = '/; BEGIN AP Patchstack.*?; END AP Patchstack/is';
606 if ( preg_match( $re, $ini_content ) ) {
607 $ini_content = preg_replace( $re, rtrim($prepend_rules['ini']), $ini_content );
608 } else {
609 $ini_content .= "\n" . $prepend_rules['ini'];
610 }
611
612 // Attempt to write to the .user.ini file.
613 if ( ! $wp_filesystem->put_contents( $user_ini, $ini_content ) ) {
614 update_option( 'patchstack_firewall_ap_error', 'Could not inject into the .user.ini file.' );
615 return true;
616 }
617
618 // Determine if the site still works as expected with the injected .user.ini rules.
619 if ( $this->get_site_status_code() == 500 ) {
620 $wp_filesystem->put_contents( $user_ini, $original_ini );
621 update_option( 'patchstack_firewall_ap_error', 'The .user.ini rules caused a fatal internal server error.' );
622 return false;
623 }
624
625 update_option( 'patchstack_firewall_ap_error', '' );
626 return true;
627 }
628
629 /**
630 * Remove everything related to the auto prepend functionality.
631 *
632 * @return boolean
633 */
634 public function auto_prepend_removal()
635 {
636 global $wp_filesystem;
637 $this->get_filesystem();
638
639 // Define our paths to access.
640 $logs_dir = WP_CONTENT_DIR . '/pslogs/';
641 $htaccess_file = ABSPATH . '.htaccess';
642 $ini_file = ABSPATH . '.user.ini';
643
644 // Remove the entire /pslogs/ directory.
645 if ( $wp_filesystem->is_dir( $logs_dir ) ) {
646 $wp_filesystem->delete( $logs_dir, true );
647 }
648
649 // Remove the .htaccess injected rules.
650 if ( $wp_filesystem->is_file( $htaccess_file ) ) {
651 $htaccess_content = $wp_filesystem->get_contents( $htaccess_file );
652 $re = '/# BEGIN AP Patchstack.*?# END AP Patchstack/is';
653 if ( preg_match( $re, $htaccess_content ) ) {
654 $htaccess_content = preg_replace( $re, '', $htaccess_content );
655 $wp_filesystem->put_contents( $htaccess_file, $htaccess_content );
656 }
657 }
658
659 // Remove the .user.ini injected rules.
660 if ( $wp_filesystem->is_file( $ini_file ) ) {
661 $ini_content = $wp_filesystem->get_contents( $ini_file );
662 $re = '/; BEGIN AP Patchstack.*?; END AP Patchstack/is';
663 if ( preg_match( $re, $ini_content ) ) {
664 $ini_content = preg_replace( $re, '', $ini_content );
665 $wp_filesystem->put_contents( $ini_file, $ini_content );
666 }
667 }
668 }
669
670 /**
671 * Attempt to establish the proper WP_FileSystem.
672 *
673 * @return boolean
674 */
675 private function get_filesystem()
676 {
677 // Seems to be the only native way to obtain FTP credentials, if defined.
678 include_once( ABSPATH . 'wp-admin/includes/file.php' );
679 ob_start();
680 $creds = request_filesystem_credentials( admin_url( 'admin-ajax.php' ), '', false, ABSPATH, null, true );
681 ob_end_clean();
682
683 // Returns false if no filesystem connection could be determined.
684 if ( $creds === false ) {
685 update_option( 'patchstack_firewall_ap_error', 'Unable to establish filesystem connection.' );
686 return false;
687 }
688
689 // Attempt to initialize it.
690 $fs = WP_Filesystem( $creds, ABSPATH, true );
691 if ( ! $fs ) {
692 update_option( 'patchstack_firewall_ap_error', 'Unable to establish filesystem connection through acquired creds.' );
693 return false;
694 }
695
696 return true;
697 }
698
699 /**
700 * Get sites as part of the environment.
701 *
702 * @return array
703 */
704 private function get_sites()
705 {
706 if ( ! function_exists( 'get_sites' ) ) {
707 return [
708 (object) [
709 'id' => 0,
710 'siteurl' => get_site_url()
711 ]
712 ];
713 }
714
715 return get_sites();
716 }
717
718 /**
719 * Switch to a different site.
720 *
721 * @param integer $site_id
722 * @return void
723 */
724 private function switch_to_blog($site_id)
725 {
726 if ( ! function_exists( 'switch_to_blog' ) ) {
727 return;
728 }
729
730 switch_to_blog( $site_id );
731 }
732
733 /**
734 * Determine the web-server software and make sure we support it before we generate the .htaccess rules for it.
735 *
736 * @return array|boolean
737 */
738 private function get_auto_prepend_rules()
739 {
740 // Establish location of the auto prepend file.
741 $mu_file = __DIR__ . '/mu-plugin-ap.php';
742 if ( ! file_exists( $mu_file ) ) {
743 return false;
744 }
745
746 // Ensure that the SERVER_SOFTWARE value is set.
747 $software = isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : '';
748 if ( ! $software ) {
749 update_option( 'patchstack_firewall_ap_error', 'Unsupported SERVER_SOFTWARE, found: ' . $_SERVER['SERVER_SOFTWARE'] );
750 return false;
751 }
752
753 // At this time, reject non-Apache environments.
754 $sapi = function_exists( 'php_sapi_name' ) ? php_sapi_name() : false;
755 if ( ! $sapi || stripos( $_SERVER['SERVER_SOFTWARE'], 'litespeed' ) === false && $sapi != 'litespeed' && stripos($_SERVER['SERVER_SOFTWARE'], 'apache' ) === false) {
756 update_option( 'patchstack_firewall_ap_error', 'Unsupported SERVER_SOFTWARE, found: ' . $_SERVER['SERVER_SOFTWARE'] . ' and ' . $sapi );
757 return false;
758 }
759
760 // Seperate flag for LiteSpeed.
761 $is_litespeed = stripos( $_SERVER['SERVER_SOFTWARE'], 'litespeed' ) !== false || $sapi == 'litespeed';
762
763 // Attempt to find the Apache version, < 2.4 does not support <If>.
764 // This depends on ServerTokens value, so only stop execution if we can't find the specific unsupported versions.
765 $version = function_exists( 'apache_get_version' ) ? apache_get_version() : $_SERVER['SERVER_SOFTWARE'];
766 if ( stripos( $version, 'Apache/2.4' ) === false ) {
767 update_option( 'patchstack_firewall_ap_error', 'Unsupported SERVER_SOFTWARE, found: ' . $_SERVER['SERVER_SOFTWARE'] );
768 return false;
769 }
770
771 // Add c-style slashes.
772 $mu_file_as = wp_normalize_path(addcslashes($mu_file, "'"));
773
774 // Bit different rules for LiteSpeed.
775 if ( ! $is_litespeed ) {
776 $rules = "<IfModule mod_php.c>
777 php_value auto_prepend_file '" . $mu_file_as . "'
778 </IfModule>
779 <IfModule mod_php5.c>
780 php_value auto_prepend_file '" . $mu_file_as . "'
781 </IfModule>
782 <IfModule mod_php7.c>
783 php_value auto_prepend_file '" . $mu_file_as . "'
784 </IfModule>";
785 } else {
786 $rules = "<IfModule LiteSpeed>
787 php_value auto_prepend_file '" . $mu_file_as . "'
788 </IfModule>
789 <IfModule lsapi_module>
790 php_value auto_prepend_file '" . $mu_file_as . "'
791 </IfModule>";
792 }
793
794 return [
795 'htaccess' => "# BEGIN AP Patchstack
796 <IfModule mod_authz_core.c>
797 <If \"-f '" . $mu_file_as . "'\">
798 " . $rules . "
799
800 <Files \".user.ini\">
801 <IfModule mod_authz_core.c>
802 Require all denied
803 </IfModule>
804 <IfModule !mod_authz_core.c>
805 Order deny,allow
806 Deny from all
807 </IfModule>
808 </Files>
809 </If>
810 </IfModule>
811 # END AP Patchstack
812 ",
813 'ini' => "; BEGIN AP Patchstack
814 auto_prepend_file = '" . $mu_file_as . "'
815 ; END AP Patchstack
816 "
817 ];
818 }
819
820 /**
821 * Retrieve the status code of the site.
822 * This is done to determine if the .htaccess rules do not trigger an error.
823 *
824 * @return integer
825 */
826 public function get_site_status_code() {
827 $response = wp_remote_get( get_site_url() );
828 $http_code = wp_remote_retrieve_response_code( $response );
829 return $http_code;
830 }
831
832 /**
833 * If option is updated, refresh AP config file.
834 *
835 * @param string $option_name
836 * @param string $option_name
837 * @param mixed $value
838 * @return void
839 */
840 public function updated_option( $option_name, $old_value, $value ) {
841 // Only allow to run for our options.
842 if ( !in_array( $option_name, [ 'patchstack_basic_firewall', 'patchstack_license_free', 'patchstack_firewall_rules_v3_ap' ] ) ) {
843 return;
844 }
845
846 // Not strict type matching.
847 if ( $old_value == $value ) {
848 return;
849 }
850
851 $this->auto_prepend_injection(true);
852 }
853 }
854