PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / trunk
MainWP Dashboard: Self-hosted WordPress Management for Agencies vtrunk
6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 5.2.2 5.3 All 153 releases
mainwp / class / class-mainwp-keys-manager.php

class-mainwp-keys-manager.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies trunk, at class/class-mainwp-keys-manager.php

807 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 *
4 * Encrypts & Decrypts API Keys.
5 *
6 * @package MainWP/MainWP_Keys_Manager
7 */
8
9 namespace MainWP\Dashboard;
10
11 use phpseclib3\Crypt\AES;
12 use phpseclib3\Crypt\Random;
13
14 // Exit if accessed directly.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * Class MainWP_Keys_Manager
21 *
22 * @package MainWP/MainWP_Keys_Manager
23 */
24 class MainWP_Keys_Manager { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
25
26 /**
27 * Private static variable to hold the single instance of the class.
28 *
29 * @static
30 *
31 * @var mixed Default null
32 */
33 private static $instance = null;
34
35 /**
36 * Method instance()
37 *
38 * Create a public static instance.
39 *
40 * @static
41 * @return Instance class.
42 */
43 public static function instance() {
44 if ( null === static::$instance ) {
45 static::$instance = new self();
46 }
47 static::auto_load_files(); // to fix.
48 return static::$instance;
49 }
50
51 /**
52 * Method get_class_name()
53 *
54 * Get Class Name.
55 *
56 * @return object Class name.
57 */
58 public static function get_class_name() {
59 return __CLASS__;
60 }
61
62 /**
63 * Method auto_load_files()
64 *
65 * Handle autoload files.
66 */
67 public static function auto_load_files() {
68 require_once MAINWP_PLUGIN_DIR . 'libs' . DIRECTORY_SEPARATOR . 'phpseclib' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; // NOSONAR -- WP compatible.
69 }
70
71 /**
72 * Method get_keys_value()
73 *
74 * Get decrypt value.
75 *
76 * @param string $name Name of key.
77 * @param mixed $default_value Default value.
78 *
79 * @return string Decrypt value.
80 */
81 public function get_keys_value( $name, $default_value = false ) {
82 $opt = get_option( $name );
83 if ( ! empty( $opt ) && is_array( $opt ) && ! empty( $opt['file_key'] ) ) {
84 return $this->decrypt_keys_data( $opt, $default_value );
85 }
86 return $default_value;
87 }
88
89 /**
90 * Method update_key_value()
91 *
92 * Get decrypt value.
93 *
94 * @param mixed $option_name option name.
95 * @param mixed $value The option value.
96 * @param mixed $prefix The prefix value.
97 *
98 * @return string Decrypt value.
99 */
100 public function update_key_value( $option_name, $value = false, $prefix = 'dash_' ) {
101 static::init_keys_dir();
102
103 if ( false === $value || '' === $value ) {
104 $opt = get_option( $option_name );
105 if ( ! empty( $opt ) && is_array( $opt ) && ! empty( $opt['file_key'] ) ) {
106 $this->delete_key_file( $opt['file_key'] );
107 }
108 return delete_option( $option_name );
109 }
110
111 try {
112 $result = $this->encrypt_value( $value, $option_name, $prefix );
113 } catch ( \Exception $ex ) {
114 $err = $ex->getMessage();
115 if ( is_string( $err ) ) {
116 MainWP_Logger::instance()->debug( 'encrypt :: name[' . $option_name . '] :: error[' . $err . ']' );
117 }
118 return false;
119 }
120
121 if ( is_array( $result ) && ! empty( $result['encrypted_value'] ) ) {
122 $key = $result['key'];
123 $file = $result['file_key'];
124 $pw = $result['encrypted_value'];
125 if ( $this->save_key_file( $file, $key ) ) {
126 $update = array(
127 'encrypted_val' => $pw,
128 'file_key' => $file,
129 );
130 update_option( $option_name, $update );
131 return true;
132 }
133 }
134 return false;
135 }
136
137 /**
138 * Method delete_key_file()
139 *
140 * Delete key file.
141 *
142 * @param string $file_key Name of key file.
143 *
144 * @return string Deleted.
145 */
146 public function delete_key_file( $file_key ) {
147 $key_dir = static::get_keys_dir();
148 $file_path = $key_dir . $file_key;
149 MainWP_Utility::delete_file( $file_path ); // delete file content key.
150 return true;
151 }
152
153 /**
154 * Delete one canonical private key file with exact absence readback.
155 *
156 * @param string $file_key Name of key file.
157 *
158 * @return bool Whether exact absence was proved.
159 */
160 public function delete_key_file_with_result( $file_key ) {
161 if ( ! is_string( $file_key ) || 1 !== preg_match( '/^[A-Za-z0-9][A-Za-z0-9_-]{0,254}$/D', $file_key ) ) {
162 return false;
163 }
164
165 $key_dir = static::get_keys_dir();
166 if ( ! is_string( $key_dir ) || '' === $key_dir ) {
167 return false;
168 }
169
170 $file_path = trailingslashit( $key_dir ) . $file_key;
171 if ( ! $this->key_entry_present( $file_path ) ) {
172 return true;
173 }
174
175 MainWP_Utility::delete_file( $file_path );
176 clearstatcache( true, $file_path );
177
178 // A dangling symlink fails every exists() check, so the transport-aware helper skips it and leaves the entry behind.
179 if ( is_link( $file_path ) ) {
180 wp_delete_file( $file_path );
181 clearstatcache( true, $file_path );
182 }
183
184 return ! $this->key_entry_present( $file_path );
185 }
186
187 /**
188 * Whether any directory entry, file or symlink, still occupies the key path.
189 *
190 * @param string $file_path Absolute key path.
191 *
192 * @return bool Whether an entry is present.
193 */
194 private function key_entry_present( $file_path ) {
195 return $this->key_file_exists( $file_path ) || is_link( $file_path );
196 }
197
198 /**
199 * Check one exact private key path through the active filesystem boundary.
200 *
201 * @param string $file_path Absolute key path.
202 *
203 * @return bool Whether the path exists.
204 */
205 private function key_file_exists( $file_path ) {
206 global $wp_filesystem;
207
208 if ( is_object( $wp_filesystem ) && method_exists( $wp_filesystem, 'exists' ) ) {
209 return (bool) $wp_filesystem->exists( $file_path );
210 }
211
212 return file_exists( $file_path );
213 }
214
215 /**
216 * Method get_decrypt_values()
217 *
218 * Get decrypt value.
219 *
220 * @param mixed $encodedValue Encoded The value to decrypt.
221 * @param mixed $key_file The value key.
222 * @param mixed $default_value The default value.
223 *
224 * @return string Decrypt value.
225 */
226 private function get_decrypt_values( $encodedValue, $key_file, $default_value = '' ) {
227 // find the key file, and get saved key.
228 $key = $this->get_key_val( $key_file );
229 if ( ! empty( $key ) ) {
230 return $this->decrypt_value( $encodedValue, $key );
231 }
232 return $default_value;
233 }
234
235 /**
236 * Method encrypt_value()
237 *
238 * Handle encrypt value.
239 *
240 * @param mixed $keypass The value to encrypt.
241 * @param string $name Option name of encrypted data.
242 * @param string $prefix using for prefix key file name.
243 *
244 * @return string Encrypted value.
245 */
246 private function encrypt_value( $keypass, $name, $prefix ) {
247
248 if ( '_' !== substr( $prefix, -1 ) ) {
249 $prefix .= '_';
250 }
251
252 $opt = get_option( $name );
253
254 if ( ! empty( $opt ) && is_array( $opt ) && ! empty( $opt['file_key'] ) ) {
255 $file_name = $opt['file_key'];
256 } else {
257 $file_name = $prefix . sha1( sha1( $prefix . $name . time() ) . 'key_files' ); // NOSONAR - safe for salt file name.
258 }
259
260 MainWP_Logger::instance()->debug( 'encrypt :: option name[' . $name . '] :: K file[' . $file_name . ']' );
261
262 $key = Random::string( 32 ); // supported key length: 16, 24, 32.
263
264 $encrypted = $this->encrypt_with_key( $keypass, $key );
265
266 return array(
267 'key' => $key,
268 'file_key' => $file_name,
269 'encrypted_value' => $encrypted,
270 );
271 }
272
273 /**
274 * Method decrypt_value()
275 *
276 * Handle decrypt value.
277 *
278 * @param mixed $encodedValue The value to decrypt.
279 * @param mixed $key Key to decrypt.
280 *
281 * @return string Decrypt value.
282 */
283 private function decrypt_value( $encodedValue, $key ) {
284 return $this->decrypt_with_key( $encodedValue, $key );
285 }
286
287 /**
288 * Method save_key_file()
289 *
290 * Handle save key passwd.
291 *
292 * @param mixed $key_file The value key.
293 * @param mixed $key_val The value.
294 *
295 * @return mixed Result.
296 */
297 public function save_key_file( $key_file, $key_val ) {
298 static::init_keys_dir();
299 $key_dir = static::get_keys_dir();
300 $file_path = $key_dir . $key_file;
301 $saved = file_put_contents( $file_path, $key_val ); //phpcs:ignore
302 return false === $saved ? false : true;
303 }
304
305 /**
306 * Method get_key_val()
307 *
308 * Get decrypt value.
309 *
310 * @param mixed $key_file The value key.
311 *
312 * @return string Decrypt value.
313 */
314 public function get_key_val( $key_file ) {
315 $key_dir = static::get_keys_dir();
316 $path = $key_dir . $key_file;
317 if ( file_exists( $path ) ) {
318 return file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- private key files.
319 }
320 return '';
321 }
322
323
324 /**
325 * Method encrypt_with_key()
326 *
327 * Handle encrypt value.
328 *
329 * @param mixed $keypass The value to encrypt.
330 * @param mixed $key Key to encrypt.
331 *
332 * @return string Encrypted value.
333 */
334 private function encrypt_with_key( $keypass, $key ) {
335
336 // Generate a random IV (Initialization Vector).
337 $iv = Random::string( 16 );
338
339 // Create AES instance.
340 $aes = new AES( 'gcm' ); // MODE_GCM.
341 $aes->setKey( $key );
342
343 $aes->setNonce( $iv ); // Nonces are only used in GCM mode.
344 $aes->setAAD( 'authentication_data' );
345
346 // Encrypt the value.
347 $ciphertext = $aes->encrypt( $keypass );
348
349 // Get the authentication tag.
350 $tag = $aes->getTag();
351
352 // Combine IV, ciphertext, and tag.
353 $encryptedValue = $iv . $ciphertext . $tag;
354
355 // Encode the encrypted value using base64 for storage.
356 return base64_encode( $encryptedValue ); //phpcs:ignore
357 }
358
359 /**
360 * Method decrypt_with_key()
361 *
362 * Handle decrypt value.
363 *
364 * @param mixed $encodedValue The string to decrypt.
365 * @param mixed $key Key to decrypt.
366 *
367 * @return string Decrypt value.
368 */
369 private function decrypt_with_key( $encodedValue, $key ) {
370 if ( empty( $encodedValue ) ) {
371 return '';
372 }
373 try {
374 // Decode the base64 encoded value.
375 $encryptedValue = base64_decode( $encodedValue ); //phpcs:ignore
376
377 // Extract the IV, ciphertext, and tag.
378 $iv = substr( $encryptedValue, 0, 16 );
379 $ciphertext = substr( $encryptedValue, 16, -16 );
380 $tag = substr( $encryptedValue, -16 );
381
382 // Create AES instance.
383 $aes = new AES( 'gcm' ); // MODE_GCM.
384 $aes->setKey( $key );
385
386 $aes->setNonce( $iv ); // Nonces are only used in GCM mode.
387 $aes->setAAD( 'authentication_data' );
388
389 // Set the authentication tag.
390 $aes->setTag( $tag );
391
392 // Decrypt the value.
393 return $aes->decrypt( $ciphertext );
394 } catch ( \Exception $ex ) {
395 // error.
396 }
397 return '';
398 }
399
400 /**
401 * Method init_keys_dir()
402 *
403 * Check for keys directory and create it if it doesn't already exist,
404 * set the file permissions and update htaccess.
405 *
406 * @param mixed $keysDir Keys directory.
407 *
408 * @return void
409 */
410 public static function init_keys_dir( $keysDir = '' ) { //phpcs:ignore -- NOSONAR - complex.
411
412 if ( '' === $keysDir ) {
413 $keysDir = static::get_keys_dir();
414 }
415
416 if ( ! is_string( $keysDir ) || stristr( $keysDir, '..' ) ) {
417 return;
418 }
419
420 $hasWPFileSystem = MainWP_System_Utility::get_wp_file_system();
421
422 /**
423 * WordPress files system object.
424 *
425 * @global object
426 */
427 global $wp_filesystem;
428
429 if ( $hasWPFileSystem && ! empty( $wp_filesystem ) ) {
430
431 if ( ! $wp_filesystem->is_dir( $keysDir ) ) {
432 // MWP-1557: 0700 preferred (owner only), 0750 fallback for shared-hosting umask edge cases. Mirrors migrate_private_filenames().
433 if ( ! $wp_filesystem->mkdir( $keysDir, 0700 ) ) {
434 $wp_filesystem->mkdir( $keysDir, 0750 );
435 }
436 }
437
438 if ( ! file_exists( $keysDir . '.htaccess' ) ) {
439 $file_htaccess = $keysDir . '.htaccess';
440 $wp_filesystem->put_contents( $file_htaccess, 'deny from all' );
441 }
442
443 if ( ! file_exists( $keysDir . 'index.php' ) ) {
444 $file_index = $keysDir . 'index.php';
445 $wp_filesystem->touch( $file_index );
446 }
447 } else {
448
449 //phpcs:disable
450 if ( ! file_exists( $keysDir ) ) {
451 // MWP-1557: 0700 preferred (owner only), 0750 fallback for shared-hosting umask edge cases. Mirrors migrate_private_filenames().
452 if ( ! mkdir( $keysDir, 0700, true ) ) {
453 mkdir( $keysDir, 0750, true );
454 }
455 }
456
457 if ( ! file_exists( $keysDir . '.htaccess' ) ) {
458 $file_htaccess = @fopen( $keysDir . '.htaccess', 'w+' );
459 fwrite( $file_htaccess, 'deny from all' );
460 fclose( $file_htaccess );
461 }
462
463 if ( ! file_exists( $keysDir . 'index.php' ) ) {
464 $file_index = @fopen( $keysDir . 'index.php', 'w+' );
465 fclose( $file_index );
466 }
467 // phpcs:enable
468 }
469 }
470
471 /**
472 * Method get_keys_dir().
473 *
474 * Check for keys directory and create it if it doesn't already exist.
475 * set the file permissions and update htaccess.
476 *
477 * @return string Keys dir.
478 */
479 public static function get_keys_dir() {
480 $dirs = MainWP_System_Utility::get_mainwp_dir();
481 return $dirs[0] . 'pk' . DIRECTORY_SEPARATOR;
482 }
483
484 /**
485 * Method register_migration_hooks()
486 *
487 * Register the post-upgrade hook that triggers the one-time pk/ filename
488 * migration. Called from MainWP_System::activate_this_plugin() before
489 * MainWP_Install::install() runs, so the action handler is registered when
490 * `mainwp_db_after_update` fires.
491 *
492 * @return void
493 */
494 public static function register_migration_hooks() {
495 add_action( 'mainwp_db_after_update', array( static::class, 'migrate_private_filenames' ), 10, 2 );
496 add_action( 'mainwp_db_after_update', array( static::class, 'migrate_sibling_dir_perms' ), 10, 2 );
497 add_action( 'mainwp_db_after_update', array( static::class, 'fix_sibling_dir_perms_9023' ), 10, 2 );
498 }
499
500 /**
501 * Method migrate_private_filenames()
502 *
503 * One-time bulk migration for MWP-1557: rename legacy pk/ filenames
504 * (`mainwp_priv_encrypt_keys_<site_id>`) to the opaque HMAC-derived names
505 * computed by MainWP_System_Utility::get_private_filename(). Also tightens
506 * directory permissions from the legacy 0777 to 0700 (or 0750 fallback).
507 *
508 * Idempotent: only matches the legacy filename pattern, so re-running on
509 * already-migrated installs is a no-op. Lazy migration in
510 * MainWP_Encrypt_Data_Lib::get_key_file() handles any files this bulk
511 * pass might miss.
512 *
513 * @param string $from_version Pre-upgrade mainwp_db_version.
514 * @param string $to_version Post-upgrade mainwp_db_version.
515 *
516 * @return void
517 */
518 public static function migrate_private_filenames( $from_version, $to_version ) {
519 unset( $to_version );
520 if ( ! version_compare( $from_version, '9.0.2.0', '<' ) ) {
521 return;
522 }
523 static::init_keys_dir();
524 $key_dir = static::get_keys_dir();
525 if ( ! is_dir( $key_dir ) ) {
526 return;
527 }
528
529 // Tighten directory permissions; 0700 preferred, 0750 if a shared web group needs read access.
530 if ( ! @chmod( $key_dir, 0700 ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
531 @chmod( $key_dir, 0750 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
532 }
533
534 // Rename legacy pk files to opaque HMAC-derived names.
535 $entries = @scandir( $key_dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- best-effort directory walk.
536 if ( ! is_array( $entries ) ) {
537 return;
538 }
539 foreach ( $entries as $entry ) {
540 if ( ! preg_match( '/^mainwp_priv_encrypt_keys_(\d+)$/', $entry, $m ) ) {
541 continue;
542 }
543 $site_id = (int) $m[1];
544 $new_name = MainWP_System_Utility::get_private_filename( 'pk', $site_id, 'priv_encrypt_keys' );
545 $old_path = $key_dir . $entry;
546 $new_path = $key_dir . $new_name;
547 if ( file_exists( $new_path ) ) {
548 // New file already present (rare race); legacy is stale, remove it.
549 wp_delete_file( $old_path );
550 continue;
551 }
552 @rename( $old_path, $new_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors, WordPress.WP.AlternativeFunctions.rename_rename -- best-effort direct rename inside the keys dir, before WP_Filesystem is initialized; lazy migration in get_key_file() handles failures.
553 }
554 }
555
556 /**
557 * Method migrate_sibling_dir_perms()
558 *
559 * One-time chmod sweep for installs that created mainwp/ subdirs before
560 * MWP-1558's mkdir tightening landed in 9.0.2.0. mkdir() does not touch
561 * the mode of an existing directory, so pre-fix installs keep their
562 * legacy 0777 even after upgrading. Reported by Daan Kortenbach in the
563 * MWP-1557/1558 follow-up sweep (MWP-1566).
564 *
565 * Targets the known set of subdirs the plugin manages. Idempotent:
566 * chmodding an already-correct dir is a no-op. @chmod failures are
567 * swallowed (Windows hosts, shared-hosting suexec mismatches, dirs
568 * owned by a different system user -- all expected).
569 *
570 * @param string $from_version Pre-upgrade mainwp_db_version.
571 * @param string $to_version Post-upgrade mainwp_db_version.
572 *
573 * @return void
574 */
575 public static function migrate_sibling_dir_perms( $from_version, $to_version ) {
576 unset( $to_version );
577 if ( ! version_compare( $from_version, '9.0.2.1', '<' ) ) {
578 return;
579 }
580 $dirs = MainWP_System_Utility::get_mainwp_dir();
581 if ( empty( $dirs[0] ) || ! is_dir( $dirs[0] ) ) {
582 return;
583 }
584 $base = rtrim( $dirs[0], '/\\' ) . DIRECTORY_SEPARATOR;
585
586 // mainwp/ root: 0755 (public-asset convention, holds index.php + subdirs).
587 @chmod( rtrim( $base, '/\\' ), 0755 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
588
589 // Public-asset subdirs. cost-tracker-products-icons is module-specific (constant-gated)
590 // but uses the same get_mainwp_dir(..., true) public pattern, so legacy installs that ran
591 // Cost Tracker pre-9.0.2.0 need the same chmod.
592 foreach ( array( 'icons', 'plugin-icons', 'theme-icons', 'client-images', 'site-icons', 'themes', 'cost-tracker-products-icons' ) as $sub ) {
593 $p = $base . $sub;
594 if ( is_dir( $p ) ) {
595 @chmod( $p, 0755 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
596 }
597 }
598
599 // Private (htaccess-protected) subdirs.
600 foreach ( array( 'cookies', 'templates', 'templates' . DIRECTORY_SEPARATOR . 'emails', 'bulk' ) as $sub ) {
601 $p = $base . $sub;
602 if ( is_dir( $p ) ) {
603 @chmod( $p, 0750 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
604 }
605 }
606
607 // Per-user dirs and all descendants: mainwp/<userid>/, /<userid>/bulk/, per-site
608 // <siteid>/ dirs from get_mainwp_specific_dir($website->id), and any deeper paths
609 // that backup_download_file() may have materialized via dirname($pFile) on legacy
610 // installs with custom backup filenames. All private (0750).
611
612 $public_access_dirs = array( 'favorites' );
613
614 $chmod_recursive = function ( $dir ) use ( &$chmod_recursive, $public_access_dirs ) {
615 @chmod( $dir, 0750 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
616 $subs = @glob( $dir . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort directory walk.
617 if ( is_array( $subs ) ) {
618 foreach ( $subs as $s ) {
619 if ( in_array( basename( $s ), $public_access_dirs, true ) ) {
620 @chmod( $s, 0755 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
621 continue;
622 }
623 $chmod_recursive( $s );
624 }
625 }
626 };
627
628 $userdirs = @glob( $base . '[0-9]*', GLOB_ONLYDIR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- NOSONAR - best-effort directory walk.
629
630 if ( is_array( $userdirs ) ) {
631 foreach ( $userdirs as $udir ) {
632 @chmod( $udir, 0751 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
633 $subs = @glob( $udir . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort directory walk.
634 if ( is_array( $subs ) ) {
635 foreach ( $subs as $s ) {
636 if ( in_array( basename( $s ), $public_access_dirs, true ) ) {
637 @chmod( $s, 0755 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
638 continue;
639 }
640 $chmod_recursive( $s );
641 }
642 }
643 }
644 }
645 }
646
647 /**
648 * Method fix_sibling_dir_perms_9023()
649 *
650 * One-time chmod sweep for MWP-1566: fix a small number of installs that ran the MWP-1558/1566
651 * follow-up sweep (MWP-1566) on.
652 *
653 * @param string $from_version Pre-upgrade mainwp_db_version.
654 * @param string $to_version Post-upgrade mainwp_db_version.
655 *
656 * @return void
657 */
658 public static function fix_sibling_dir_perms_9023( $from_version, $to_version ) {
659 unset( $to_version );
660
661 if ( empty( $from_version ) || version_compare( $from_version, '9.0.2.1', '<' ) || version_compare( $from_version, '9.0.2.3', '>=' ) ) {
662 return;
663 }
664
665 $dirs = MainWP_System_Utility::get_mainwp_dir();
666 if ( empty( $dirs[0] ) || ! is_dir( $dirs[0] ) ) {
667 return;
668 }
669 $base = rtrim( $dirs[0], '/\\' ) . DIRECTORY_SEPARATOR;
670
671 // Per-user dirs and all descendants: mainwp/<userid>/, /<userid>/bulk/, per-site
672 // <siteid>/ dirs from get_mainwp_specific_dir($website->id), and any deeper paths
673 // that backup_download_file() may have materialized via dirname($pFile) on legacy
674 // installs with custom backup filenames. All private (0750).
675
676 $public_access_dirs = array( 'favorites' );
677
678 $chmod_recursive = function ( $dir ) use ( &$chmod_recursive, $public_access_dirs ) {
679 @chmod( $dir, 0750 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
680 $subs = @glob( $dir . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- NOSONAR - best-effort directory walk.
681 if ( is_array( $subs ) ) {
682 foreach ( $subs as $s ) {
683 if ( in_array( basename( $s ), $public_access_dirs, true ) ) {
684 @chmod( $s, 0755 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
685 continue;
686 }
687 $chmod_recursive( $s );
688 }
689 }
690 };
691
692 $userdirs = @glob( $base . '[0-9]*', GLOB_ONLYDIR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- NOSONAR - best-effort directory walk.
693
694 if ( is_array( $userdirs ) ) {
695 foreach ( $userdirs as $udir ) {
696 @chmod( $udir, 0751 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
697 $subs = @glob( $udir . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort directory walk.
698 if ( is_array( $subs ) ) {
699 foreach ( $subs as $s ) {
700 if ( in_array( basename( $s ), $public_access_dirs, true ) ) {
701 @chmod( $s, 0755 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
702 continue;
703 }
704 $chmod_recursive( $s );
705 }
706 }
707 }
708 }
709 }
710
711
712 /**
713 * Method encrypt_keys_data()
714 *
715 * Handle encrypt value.
716 *
717 * @param mixed $data The value to encrypt.
718 * @param string $prefix prefix key file name.
719 * @param string $key_file key file name.
720 *
721 * @return string Encrypted value.
722 */
723 public function encrypt_keys_data( $data, $prefix, $key_file = false ) { //phpcs:ignore -- NOSONAR - complex.
724
725 if ( empty( $data ) ) {
726 if ( ! empty( $key_file ) ) {
727 $this->delete_key_file( $key_file );
728 }
729 return $data;
730 }
731
732 if ( '_' !== substr( $prefix, -1 ) ) {
733 $prefix .= '_';
734 }
735
736 if ( ! function_exists( '\wp_rand' ) ) {
737 include_once ABSPATH . WPINC . '/pluggable.php'; // NOSONAR - WP compatible.
738 }
739
740 if ( ! empty( $key_file ) && is_string( $key_file ) ) {
741 $file_name = $key_file;
742 } elseif ( ! empty( $data ) && is_array( $data ) && ! empty( $data['file_key'] ) ) {
743 $file_name = $data['file_key'];
744 } else {
745 $ran = wp_rand( 0, 9990 ); // to fix repeat value.
746 $file_name = $prefix . sha1( sha1( $prefix . time() . $ran ) . 'key_files' ); // NOSONAR - safe for salt file name.
747 }
748
749 MainWP_Logger::instance()->debug( 'encrypt :: K file[' . $file_name . ']' );
750
751 try {
752 $key = Random::string( 32 ); // supported key length: 16, 24, 32.
753 $encrypted = $this->encrypt_with_key( $data, $key );
754 $result = array(
755 'key' => $key,
756 'file_key' => $file_name,
757 'encrypted_value' => $encrypted,
758 );
759 } catch ( \Exception $ex ) {
760 $err = $ex->getMessage();
761 if ( is_string( $err ) ) {
762 MainWP_Logger::instance()->debug( 'encrypt :: error[' . $err . ']' );
763 }
764 return false;
765 }
766
767 if ( is_array( $result ) && ! empty( $result['encrypted_value'] ) ) {
768 $key = $result['key'];
769 $file = $result['file_key'];
770 $pw = $result['encrypted_value'];
771 if ( $this->save_key_file( $file, $key ) ) {
772 return array(
773 'encrypted_val' => $pw,
774 'file_key' => $file,
775 );
776 }
777 }
778 return false;
779 }
780
781
782 /**
783 * Method decrypt_keys_data()
784 *
785 * Get decrypt value.
786 *
787 * @param string $encrypted Name of key.
788 * @param mixed $default_value Default value.
789 *
790 * @return string Decrypt value.
791 */
792 public function decrypt_keys_data( $encrypted, $default_value = false ) {
793 if ( is_array( $encrypted ) && ! empty( $encrypted['file_key'] ) && ! empty( $encrypted['encrypted_val'] ) ) {
794 try {
795 return $this->get_decrypt_values( $encrypted['encrypted_val'], $encrypted['file_key'], $default_value );
796 } catch ( \Exception $ex ) {
797 $err = $ex->getMessage();
798 if ( is_string( $err ) ) {
799 MainWP_Logger::instance()->debug( 'decrypt :: error[' . $err . ']' );
800 }
801 return false;
802 }
803 }
804 return $default_value;
805 }
806 }
807