PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.6
Patchstack – WordPress & Plugins Security v2.3.6
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 / patchstack.php

patchstack.php in Patchstack – WordPress & Plugins Security 2.3.6, at patchstack.php

450 lines 12.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Patchstack Security
4 * Plugin URI: https://patchstack.com/?utm_medium=wp&utm_source=dashboard&utm_campaign=patchstack%20plugin
5 * Author URI: https://patchstack.com/?utm_medium=wp&utm_source=dashboard&utm_campaign=patchstack%20plugin
6 * Description: Patchstack identifies security vulnerabilities in WordPress plugins, themes, and core.
7 * Version: 2.3.6
8 * Author: Patchstack
9 * License: GPLv3
10 * Text Domain: patchstack
11 * Domain Path: /languages
12 * Requires at least: 4.4
13 * Requires PHP: 5.6
14 */
15
16 // Do not allow the file to be called directly.
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 if ( ! function_exists( 'patchstack_autoload_classes' ) ) {
22 /**
23 * Autoloads the Patchstack classes when called.
24 *
25 * @param string $class_name The class name to autoload.
26 * @return void
27 */
28 function patchstack_autoload_classes( $class_name ) {
29 // If the requested class doesn't have our prefix, don't load it.
30 if ( strpos( $class_name, 'P_' ) !== 0 ) {
31 return;
32 }
33
34 // Set up our filename.
35 $file_name = strtolower( str_replace( '_', '-', substr( $class_name, strlen( 'P_' ) ) ) );
36 $dir = trailingslashit( dirname( __FILE__ ) ) . 'includes/';
37 $target = [ $dir . $file_name . '.php', $dir . 'admin/' . str_replace( 'admin-', '', $file_name ) . '.php' ];
38
39 // Attempt each target and load if it exists.
40 foreach ( $target as $file ) {
41 if ( file_exists( $file ) ) {
42 include_once $file;
43 }
44 }
45 }
46 }
47 spl_autoload_register( 'patchstack_autoload_classes' );
48
49 if ( ! class_exists( 'patchstack' ) ) {
50
51 /**
52 * This is the main Patchstack class used for all Patchstack related features and to launch
53 * the Patchstack plugin.
54 */
55 class Patchstack {
56
57 /**
58 * The plugin version.
59 *
60 * @var string
61 */
62 const VERSION = '2.3.6';
63
64 /**
65 * API URL of Patchstack to communicate with.
66 *
67 * @var string
68 */
69 const API_URL = 'https://api.patchstack.com';
70
71 /**
72 * API Auth URL of Patchstack to communicate with.
73 *
74 * @var string
75 */
76 const AUTH_URL = 'https://auth.patchstack.com';
77
78 /**
79 * Client ID, this is only set when freshly downloaded from the app.
80 *
81 * @var string
82 */
83 const CLIENT_ID = 'PATCHSTACK_CLIENT_ID';
84
85 /**
86 * Client private key, this is only set when freshly downloaded from the app.
87 *
88 * @var string
89 */
90 const PRIVATE_KEY = 'PATCHSTACK_PRIVATE_KEY';
91
92 /**
93 * URL of the plugin directory.
94 *
95 * @var string
96 */
97 protected $url = '';
98
99 /**
100 * Plugin basename.
101 *
102 * @var string
103 */
104 protected $basename = '';
105
106 /**
107 * Plugin name.
108 *
109 * @var string
110 */
111 protected $name = '';
112
113 /**
114 * Detailed activation error messages.
115 *
116 * @var array
117 */
118 protected $activation_errors = [];
119
120 /**
121 * Singleton instance of plugin.
122 *
123 * @var Patchstack
124 */
125 protected static $single_instance = null;
126
127 /**
128 * Define all the variables that will hold the Patchstack classes.
129 * These must be defined because it allows us to communicate from one class to the other.
130 */
131 protected $firewall;
132 protected $firewall_base;
133 protected $activation;
134 protected $cron;
135 protected $api;
136 protected $login;
137 protected $ban;
138 protected $hardening;
139 protected $htaccess;
140 protected $hacker_log;
141 protected $upload;
142 protected $rules;
143 protected $hide_login;
144 protected $listener;
145 protected $event_log;
146 protected $multisite;
147 protected $admin_ajax;
148 protected $admin_general;
149 protected $admin_menu;
150 protected $admin_options;
151
152 /**
153 * Setup a few base variables for the plugin.
154 * Also make sure certain constants are defined.
155 *
156 * @return void
157 */
158 protected function __construct() {
159 // Set the permission constants if not already set.
160 if ( ! defined( 'FS_CHMOD_DIR' ) ) {
161 define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 | 0755 ) );
162 }
163
164 if ( ! defined( 'FS_CHMOD_FILE' ) ) {
165 define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ) );
166 }
167
168 // Define local variables.
169 $this->basename = plugin_basename( __FILE__ );
170 $this->url = plugin_dir_url( __FILE__ );
171 $names = explode( '/', $this->basename );
172 $this->name = $names[0];
173
174 // Define WP_CLI command.
175 if ( defined( 'WP_CLI' ) && WP_CLI && method_exists('\WP_CLI', 'add_command')) {
176 \WP_CLI::add_command( 'patchstack activate', [ $this, 'cli_activate' ] );
177 \WP_CLI::add_command( 'patchstack deactivate', [ $this, 'cli_deactivate' ] );
178 \WP_CLI::add_command( 'patchstack status', [ $this, 'cli_status' ] );
179 }
180 }
181
182 /**
183 * Call the constructor of all the Patchstack related classes.
184 *
185 * @return void
186 */
187 public function plugin_classes() {
188 // Define the array of the classes.
189 foreach ( [
190 'admin_options' => 'P_Admin_Options',
191 'cron' => 'P_Cron',
192 'api' => 'P_Api',
193 'login' => 'P_Login',
194 'ban' => 'P_Ban',
195 'hardening' => 'P_Hardening',
196 'htaccess' => 'P_Htaccess',
197 'hacker_log' => 'P_Hacker_Log',
198 'upload' => 'P_Upload',
199 'rules' => 'P_Rules',
200 'hide_login' => 'P_Hide_Login',
201 'event_log' => 'P_Event_Log',
202 'activation' => 'P_Activation',
203 'listener' => 'P_Listener',
204 'multisite' => 'P_Multisite',
205 'admin_ajax' => 'P_Admin_Ajax',
206 'admin_general' => 'P_Admin_General',
207 'admin_menu' => 'P_Admin_Menu',
208 ] as $var => $class ) {
209 $this->$var = new $class( $this );
210 }
211
212 // Load firewall base functionality.
213 $this->firewall_base = new P_Firewall( true, $this, true );
214 }
215
216 /**
217 * Activate the plugin.
218 *
219 * @return void
220 */
221 public function activate() {
222 $this->plugin_classes();
223 $this->activation->activate( $this );
224 }
225
226 /**
227 * Connects the Patchstack plugin to the API with the license id and secret key.
228 *
229 * Returns an error if the connection was not successful.
230 *
231 * ## OPTIONS
232 *
233 * [<id>]
234 * : The API client id.
235 *
236 * [<secret>]
237 * : The API secret key.
238 *
239 * <secret-id>
240 * : The API client id and secret key merged together, found in the App. E.g. 2b072e8b60402e30d481df351fc08183906254e0-123456
241 *
242 * ## EXAMPLES
243 *
244 * $ wp patchstack activate 123456 2b072e8b60402e30d481df351fc08183906254e0
245 * Success: The Patchstack plugin has been successfully connected.
246 *
247 * or
248 *
249 * $ wp patchstack activate 2b072e8b60402e30d481df351fc08183906254e0-123456
250 * Success: The Patchstack plugin has been successfully connected.
251 */
252 public function cli_activate( $args ) {
253 // Handle both ways to activate the plugin.
254 if ( count( $args ) === 1 && strpos( $args[0], '-' ) !== false ) {
255 list( $secret, $id ) = explode( '-', $args[0] );
256 } else {
257 $id = isset( $args[0] ) ? trim( $args[0] ) : '';
258 $secret = isset( $args[1] ) ? trim( $args[1] ) : '';
259 }
260
261 $result = $this->activation->alter_license( $id, $secret, 'activate' );
262 if ( $result['result'] == 'error' ) {
263 \WP_CLI::error( "The Patchstack plugin could not be connected. Make sure the id and secret key are valid and that api.patchstack.com is not blocked. Additional information:\n" . $result['body'] );
264 return;
265 }
266
267 \WP_CLI::success( 'The Patchstack plugin has been successfully connected.' );
268 }
269
270 /**
271 * Disconnects the Patchstack plugin from the API and removes the API key.
272 *
273 * ## EXAMPLES
274 *
275 * $ wp patchstack deactivate
276 * Success: The Patchstack plugin has been successfully disconnected.
277 */
278 public function cli_deactivate() {
279 $this->activation->deactivate();
280 $this->activation->alter_license( '', '', 'deactivate' );
281
282 \WP_CLI::success( 'The Patchstack plugin has been successfully disconnected.' );
283 }
284
285 /**
286 * Gets the current API connection status from the Patchstack plugin.
287 *
288 * ## EXAMPLES
289 *
290 * $ wp patchstack status
291 * Success: The Patchstack plugin is currently connected to the API.
292 *
293 * $ wp patchstack status
294 * Warning: The Patchstack plugin is not connected to the API.
295 */
296 public function cli_status() {
297 if ( $this->api->is_connected() ) {
298 \WP_CLI::success( __( 'The Patchstack plugin is currently connected to the API.', 'patchstack' ) );
299 } else {
300 \WP_CLI::warning( __( 'The Patchstack plugin is not connected to the API.', 'patchstack' ) );
301 }
302 }
303
304 /**
305 * Deactivate the plugin.
306 *
307 * @return void
308 */
309 public function deactivate() {
310 $this->plugin_classes();
311 $this->activation->deactivate();
312 }
313
314 /**
315 * Load translated strings for the plugin.
316 *
317 * @return void
318 */
319 public function load_textdomain () {
320 load_plugin_textdomain( 'patchstack', false, dirname( $this->basename ) . '/languages/' );
321 }
322
323 /**
324 * Boot Patchstack.
325 *
326 * @return void
327 */
328 public function init() {
329 add_action( 'init', [ $this, 'load_textdomain' ] );
330
331 // Initialize plugin classes.
332 $this->plugin_classes();
333
334 // Perform migrations if necessary.
335 $this->activation->migrate_check();
336
337 // If license expiration has not been fetched yet while the plugin is active, update it.
338 if ( get_option( 'patchstack_api_token', '' ) == '' && get_option( 'patchstack_license_expiry', '' ) == '' ) {
339 $this->api->update_license_status();
340 }
341
342 // Run firewall if not disabled and license activated.
343 if ( get_option( 'patchstack_license_activated', 0 ) == 1 && get_option( 'patchstack_basic_firewall', 0 ) == 1 && get_option( 'patchstack_license_free', 0 ) == 0 ) {
344 $this->firewall = new P_Firewall( true, $this );
345 }
346 }
347
348 /**
349 * Creates or returns an instance of this class.
350 *
351 * @return Patchstack
352 */
353 public static function get_instance() {
354 if ( null === self::$single_instance ) {
355 self::$single_instance = new self();
356 }
357
358 return self::$single_instance;
359 }
360
361 /**
362 * Magic getter.
363 *
364 * @param string $field The field to magically get.
365 * @return mixed
366 */
367 public function __get( $field ) {
368 switch ( $field ) {
369 case 'version':
370 return self::VERSION;
371 case 'api_url':
372 return self::API_URL;
373 case 'auth_url':
374 return self::AUTH_URL;
375 case 'client_id':
376 return self::CLIENT_ID;
377 case 'private_key':
378 return self::PRIVATE_KEY;
379 default:
380 try {
381 return $this->$field;
382 } catch ( \Exception $e ) {
383 return null;
384 }
385 }
386 }
387 }
388 }
389
390 if ( ! function_exists( 'patchstack_uninstall' ) ) {
391 /**
392 * Called when the plugin is uninstalled/removed from the site.
393 * This is not the same as deactivation, where the plugin still resides on the site.
394 *
395 * @return void
396 */
397 function patchstack_uninstall() {
398 // Delete most of the Patchstack options.
399 global $wpdb;
400 $options = $wpdb->get_results( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE 'patchstack_%'" );
401
402 // Few options we want to keep.
403 $keep = ['patchstack_hits_last_30', 'patchstack_hits_all_time', 'patchstack_clientid', 'patchstack_secretkey', 'patchstack_secretkey_nonce', 'patchstack_api_token'];
404
405 // Delete everything else.
406 foreach( $options as $option ) {
407 if ( in_array( $option->option_name, $keep ) || stripos( $option->option_name, 'patchstack_captcha_' ) !== false ) {
408 continue;
409 }
410
411 delete_option( $option->option_name );
412
413 if ( is_multisite() ) {
414 delete_site_option( $option->option_name );
415 }
416 }
417
418 // Drop all tables.
419 global $wpdb;
420 $prefixes = ['patchstack_', 'webarx_'];
421 foreach ( $prefixes as $prefix ) {
422 $tables = [ 'user_log', 'visitor_log', 'firewall_log', 'file_hashes', 'logic', 'ip', 'event_log' ];
423 foreach ( $tables as $table ) {
424 $wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . $prefix . $table );
425 }
426 }
427 }
428 }
429
430 if ( ! function_exists( 'patchstack' ) ) {
431 /**
432 * Grab the Patchstack object and return it.
433 *
434 * @return Patchstack
435 */
436 function patchstack() {
437 return patchstack::get_instance();
438 }
439 }
440
441 if ( ! has_action( 'plugins_loaded', [ patchstack(), 'init' ] ) ) {
442 // Kick it off.
443 add_action( 'plugins_loaded', [ patchstack(), 'init' ] );
444
445 // Activation and deactivation hooks.
446 register_activation_hook( __FILE__, [ patchstack(), 'activate' ] );
447 register_deactivation_hook( __FILE__, [ patchstack(), 'deactivate' ] );
448 register_uninstall_hook( __FILE__, 'patchstack_uninstall' );
449 }
450