PluginProbe
Patchstack – WordPress & Plugins Security / trunk
Patchstack – WordPress & Plugins Security vtrunk
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 / core.php

core.php in Patchstack – WordPress & Plugins Security trunk, at includes/core.php

443 lines 11.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 * The core class is used as a base class for all the other classes.
10 * This will allow us to declare certain global methods/variables.
11 */
12 class P_Core {
13
14 /**
15 * This will allow us to communicate between classes.
16 *
17 * @var Patchstack
18 */
19 public $plugin;
20
21 /**
22 * Whether or not the site is a multisite.
23 *
24 * @var boolean
25 */
26 public $is_multi_site = false;
27
28 /**
29 * Allowed HTML for the wp_kses function used to render certain paragraphs of texts.
30 *
31 * @var array
32 */
33 public $allowed_html = [
34 'a' => [
35 'href' => [],
36 'title' => [],
37 'target' => []
38 ],
39 'p' => [
40 'style' => []
41 ],
42 'span' => [
43 'style' => []
44 ],
45 'br' => [],
46 'strong' => [],
47 'b' => [],
48 'i' => [
49 'style' => []
50 ],
51 'label' => [
52 'for' => [],
53 'style' => []
54 ],
55 'input' => [
56 'type' => [],
57 'class' => [],
58 'name' => [],
59 'id' => [],
60 'value' => [],
61 'checked' => [],
62 'style' => []
63 ],
64 'textarea' => [
65 'rows' => [],
66 'id' => [],
67 'name' => []
68 ],
69 'select' => [
70 'name' => [],
71 'id' => [],
72 'data-selected' => []
73 ],
74 'option' => [
75 'value' => [],
76 'selected' => []
77 ],
78 'table' => [
79 'class' => [],
80 'style' => []
81 ],
82 'thead' => [],
83 'th' => [
84 'style' => []
85 ],
86 'tr' => [],
87 'td' => [],
88 'div' => [
89 'class' => [],
90 'style' => []
91 ]
92 ];
93
94 /**
95 * @param Patchstack $plugin
96 * @return void
97 */
98 public function __construct( $plugin ) {
99 $this->plugin = $plugin;
100 $this->is_multi_site = is_multisite();
101 }
102
103 /**
104 * In case of multisite we want to determine if there's a difference between the
105 * network setting and site setting and if so, use the site setting.
106 *
107 * @param string $name
108 * @param mixed $default
109 * @return mixed
110 */
111 public function get_option( $name, $default = false ) {
112 // We always want to return the site option on the default settings management page.
113 if ( isset( $_GET['page'] ) && $_GET['page'] == 'patchstack-multisite-settings' && function_exists( 'wp_get_current_user' ) && is_super_admin() ) {
114 return get_site_option( $name, $default );
115 }
116
117 // Get the setting of the current site.
118 $secondary = get_option( $name, $default );
119
120 // On single-site installs there is no network option to reconcile, so
121 // avoid the extra get_site_option() lookup on every read.
122 if ( ! is_multisite() ) {
123 return $secondary;
124 }
125
126 // Get the setting of the network and in case there's a difference,
127 // return the value of site.
128 $main = get_site_option( $name, $default );
129 return $main != $secondary ? $secondary : $main;
130 }
131
132 /**
133 * In case we need to retrieve the option of a specific site, we can use this.
134 * It will determine if it's on a multisite environment and if so, use get_blog_option.
135 *
136 * @param int $site_id
137 * @param string $name
138 * @param mixed $default
139 * @return mixed
140 */
141 public function get_blog_option( $site_id, $name, $default = false ) {
142 if ( $this->is_multi_site ) {
143 return get_blog_option( $site_id, $name, $default );
144 }
145
146 return get_option( $name, $default );
147 }
148
149 /**
150 * In case we need to update the option of a specific site, we can use this.
151 * It will determine if it's on a multisite environment and if so, use update_blog_option.
152 *
153 * @param int $site_id
154 * @param string $name
155 * @param mixed $value
156 * @return mixed
157 */
158 public function update_blog_option( $site_id, $name, $value ) {
159 if ( $this->is_multi_site ) {
160 return update_blog_option( $site_id, $name, $value );
161 }
162
163 return update_option( $name, $value );
164 }
165
166 /**
167 * Determine if the license is active and not expired.
168 *
169 * @return boolean
170 */
171 public function license_is_active() {
172 if ( get_option( 'patchstack_license_activated', 0 ) ) {
173 return true;
174 }
175
176 $expiry = get_option( 'patchstack_license_expiry', '' );
177 if ( $expiry != '' && ( strtotime( $expiry ) > ( time() - ( 3600 * 24 ) ) ) ) {
178 return true;
179 }
180
181 return false;
182 }
183
184 /**
185 * Determine if the plugin is connected to the API.
186 *
187 * @return boolean
188 */
189 public function is_connected() {
190 // Determine if the API client id is set.
191 if ( $this->plugin->client_id == 'PATCHSTACK_CLIENT_ID' && ! get_option( 'patchstack_clientid' ) ) {
192 return false;
193 }
194
195 // Determine if we have an API token.
196 if ( get_option( 'patchstack_api_token', '' ) == '' ) {
197 return false;
198 }
199
200 // Determine if we have a last license check set.
201 $last_license_check = get_option( 'patchstack_last_license_check', 0 );
202 if ( !empty( $last_license_check ) && time() - $last_license_check >= 604800 ) {
203 return false;
204 }
205
206 return true;
207 }
208
209 /**
210 * Determine if the plugin provides protection.
211 *
212 * @return boolean
213 */
214 public function is_protected() {
215 return (int) get_option( 'patchstack_license_free', 0 ) == 0;
216 }
217
218 /**
219 * Format a UNIX timestamp as a short relative-time string for the connection card.
220 * Returns "Never" for empty/zero, otherwise "Just now" / "Xm ago" / "Xh ago" / "Xd ago".
221 *
222 * The returned string is the raw translated value — escape it at the call site.
223 *
224 * @param int $timestamp UNIX timestamp.
225 * @return string Translated relative-time label (not escaped).
226 */
227 public function format_relative_time( $timestamp ) {
228 $timestamp = (int) $timestamp;
229 if ( $timestamp <= 0 ) {
230 return __( 'Never', 'patchstack' );
231 }
232
233 $diff = time() - $timestamp;
234 if ( $diff < 60 ) {
235 return __( 'Just now', 'patchstack' );
236 }
237 if ( $diff < 3600 ) {
238 /* translators: %d: number of minutes since the last sync. */
239 return sprintf( __( '%dm ago', 'patchstack' ), (int) floor( $diff / 60 ) );
240 }
241 if ( $diff < 86400 ) {
242 /* translators: %d: number of hours since the last sync. */
243 return sprintf( __( '%dh ago', 'patchstack' ), (int) floor( $diff / 3600 ) );
244 }
245 /* translators: %d: number of days since the last sync. */
246 return sprintf( __( '%dd ago', 'patchstack' ), (int) floor( $diff / 86400 ) );
247 }
248
249 /**
250 * Get the timestamp of the last successful API sync.
251 *
252 * Prefers patchstack_last_sync, which is stamped on every successful (200 OK)
253 * API request (log/software uploads, rule pulls, license verify, ping, etc.),
254 * so it reflects real sync activity rather than only license verification.
255 * Falls back to patchstack_last_license_check for sites that have not synced
256 * yet since this option was introduced.
257 *
258 * @return int UNIX timestamp, or 0 if never synced.
259 */
260 public function get_last_sync_time() {
261 $last_sync = (int) get_option( 'patchstack_last_sync', 0 );
262 if ( $last_sync > 0 ) {
263 return $last_sync;
264 }
265
266 return (int) get_option( 'patchstack_last_license_check', 0 );
267 }
268
269 /**
270 * Grab the IP address of the user. Give the override IP header priority.
271 * If this does not exist, we should always default to REMOTE_ADDR.
272 *
273 * @return string
274 */
275 public function get_ip() {
276 $override = get_option( 'patchstack_firewall_ip_header', '' );
277 if ( $override != '' && isset( $_SERVER[ $override ] ) ) {
278 return $_SERVER[ $override ];
279 }
280
281 return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
282 }
283
284 /**
285 * Grab the secret key used for API communication.
286 *
287 * @param string $custom
288 * @return string
289 */
290 public function get_secret_key( $custom = '' ) {
291 if ( $custom != '' ) {
292 return $this->encrypt( $custom );
293 }
294
295 $secret = get_option( 'patchstack_secretkey', '' );
296 if ( ! $secret ) {
297 return '';
298 }
299
300 if ( strlen( $secret ) === 40 ) {
301 $enc = $this->encrypt( $secret );
302
303 update_option( 'patchstack_secretkey', $enc['cipher'] );
304 update_option( 'patchstack_secretkey_nonce', $enc['nonce'] );
305
306 return $secret;
307 }
308
309 $nonce = get_option( 'patchstack_secretkey_nonce' );
310 return $this->decrypt( $secret, $nonce );
311 }
312
313 /**
314 * Set the secret key used for API communication.
315 *
316 * @param string $secret
317 * @return void
318 */
319 public function set_secret_key( $secret ) {
320 $enc = $this->encrypt( $secret );
321
322 update_option( 'patchstack_secretkey', $enc['cipher'] );
323 update_option( 'patchstack_secretkey_nonce', $enc['nonce'] );
324 }
325
326 /**
327 * Determine which encryption dependency we can use.
328 *
329 * @return string
330 */
331 public function get_enc_type() {
332 if ( function_exists('sodium_crypto_generichash') ) {
333 return 'native';
334 }
335
336 return 'compat';
337 }
338
339 /**
340 * Get the unique nonce that is used for the secretbox.
341 *
342 * @return string
343 */
344 public function get_enc_nonce() {
345 if ( function_exists('random_bytes') ) {
346 return random_bytes( 24 );
347 }
348
349 require_once dirname( __FILE__ ) . '/2fa/polyfill/lib/random.php';
350 return random_bytes( 24 );
351 }
352
353 /**
354 * Encrypt a string.
355 *
356 * @param string $message
357 * @return array
358 */
359 public function encrypt( $message ) {
360 if ( is_null( $message ) || ! defined( 'AUTH_KEY' ) ) {
361 return [
362 'cipher' => $message,
363 'nonce' => ''
364 ];
365 }
366
367 $enc_type = $this->get_enc_type();
368 $nonce = $this->get_enc_nonce();
369
370 try {
371 // Use the PHP native encryption functions.
372 if ( $enc_type == 'native' ) {
373 $key = sodium_crypto_generichash( AUTH_KEY );
374
375 return [
376 'cipher' => sodium_bin2hex( sodium_crypto_secretbox( $message, $nonce, $key ) ),
377 'nonce' => sodium_bin2hex( $nonce )
378 ];
379 }
380
381 // Use the Sodium polyfill library part of WordPress core.
382 if ( ! file_exists( ABSPATH . WPINC . '/sodium_compat/autoload.php' ) ) {
383 return [
384 'cipher' => $message,
385 'nonce' => ''
386 ];
387 }
388 require_once ABSPATH . WPINC . '/sodium_compat/autoload.php';
389 $key = \Sodium\crypto_generichash( AUTH_KEY );
390
391 return [
392 'cipher' => \Sodium\bin2hex( \Sodium\crypto_secretbox( $message, $nonce, $key ) ),
393 'nonce' => \Sodium\bin2hex( $nonce )
394 ];
395 } catch ( Exception $e ) {
396 return [
397 'cipher' => $message,
398 'nonce' => ''
399 ];
400 }
401 }
402
403 /**
404 * Decrypt a cipher to plain-text.
405 *
406 * @param string $cipher
407 * @param string $nonce
408 * @return string
409 */
410 public function decrypt( $cipher, $nonce ) {
411 $enc_type = $this->get_enc_type();
412
413 // If we received an empty nonce, we assume it was never properly encrypted to begin with.
414 if ( $nonce == '' || ! defined( 'AUTH_KEY' ) ) {
415 return $cipher;
416 }
417
418 try {
419 // Determine if we should use native or polyfill functions.
420 if ( $enc_type == 'native' ) {
421 $key = sodium_crypto_generichash( AUTH_KEY );
422 $dec = sodium_crypto_secretbox_open( sodium_hex2bin( $cipher ), sodium_hex2bin( $nonce ), $key );
423 } else {
424 if ( ! file_exists( ABSPATH . WPINC . '/sodium_compat/autoload.php' ) ) {
425 return $cipher;
426 }
427 require_once ABSPATH . WPINC . '/sodium_compat/autoload.php';
428 $key = \Sodium\crypto_generichash( AUTH_KEY );
429 $dec = \Sodium\crypto_secretbox_open( sodium_hex2bin( $cipher ), sodium_hex2bin( $nonce ), $key );
430 }
431 } catch ( Exception $e ) {
432 return $cipher;
433 }
434
435 // In case decryption failed, return null.
436 if ( ! $dec ) {
437 return null;
438 }
439
440 return $dec;
441 }
442 }
443