PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 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 All 155 releases
← All changes | class/class-mainwp-keys-manager.php +158 -11 6.1 → 6.2.2 View file →
@@ -150,8 +150,70 @@
150 150 return true;
151 151 }
152 152
153 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 + /**
154 216 * Method get_decrypt_values()
155 217 *
156 218 * Get decrypt value.
157 219 *
@@ -431,8 +493,9 @@
431 493 */
432 494 public static function register_migration_hooks() {
433 495 add_action( 'mainwp_db_after_update', array( static::class, 'migrate_private_filenames' ), 10, 2 );
434 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 );
435 498 }
436 499
437 500 /**
438 501 * Method migrate_private_filenames()
@@ -452,8 +515,9 @@
452 515 *
453 516 * @return void
454 517 */
455 518 public static function migrate_private_filenames( $from_version, $to_version ) {
519 + unset( $to_version );
456 520 if ( ! version_compare( $from_version, '9.0.2.0', '<' ) ) {
457 521 return;
458 522 }
459 523 static::init_keys_dir();
@@ -462,10 +526,10 @@
462 526 return;
463 527 }
464 528
465 529 // Tighten directory permissions; 0700 preferred, 0750 if a shared web group needs read access.
466 - if ( ! @chmod( $key_dir, 0700 ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors -- best-effort hardening.
467 - @chmod( $key_dir, 0750 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- best-effort hardening.
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.
468 532 }
469 533
470 534 // Rename legacy pk files to opaque HMAC-derived names.
471 535 $entries = @scandir( $key_dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- best-effort directory walk.
@@ -481,12 +545,12 @@
481 545 $old_path = $key_dir . $entry;
482 546 $new_path = $key_dir . $new_name;
483 547 if ( file_exists( $new_path ) ) {
484 548 // New file already present (rare race); legacy is stale, remove it.
485 - @unlink( $old_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- cleanup of stale legacy.
549 + wp_delete_file( $old_path );
486 550 continue;
487 551 }
488 - @rename( $old_path, $new_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- best-effort; lazy migration in get_key_file() handles failures.
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.
489 553 }
490 554 }
491 555
492 556 /**
@@ -508,8 +572,9 @@
508 572 *
509 573 * @return void
510 574 */
511 575 public static function migrate_sibling_dir_perms( $from_version, $to_version ) {
576 + unset( $to_version );
512 577 if ( ! version_compare( $from_version, '9.0.2.1', '<' ) ) {
513 578 return;
514 579 }
515 580 $dirs = MainWP_System_Utility::get_mainwp_dir();
@@ -518,9 +583,9 @@
518 583 }
519 584 $base = rtrim( $dirs[0], '/\\' ) . DIRECTORY_SEPARATOR;
520 585
521 586 // mainwp/ root: 0755 (public-asset convention, holds index.php + subdirs).
522 - @chmod( rtrim( $base, '/\\' ), 0755 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- NOSONAR - best-effort hardening.
587 + @chmod( rtrim( $base, '/\\' ), 0755 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
523 588
524 589 // Public-asset subdirs. cost-tracker-products-icons is module-specific (constant-gated)
525 590 // but uses the same get_mainwp_dir(..., true) public pattern, so legacy installs that ran
526 591 // Cost Tracker pre-9.0.2.0 need the same chmod.
@@ -526,9 +591,9 @@
526 591 // Cost Tracker pre-9.0.2.0 need the same chmod.
527 592 foreach ( array( 'icons', 'plugin-icons', 'theme-icons', 'client-images', 'site-icons', 'themes', 'cost-tracker-products-icons' ) as $sub ) {
528 593 $p = $base . $sub;
529 594 if ( is_dir( $p ) ) {
530 - @chmod( $p, 0755 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- NOSONAR - best-effort hardening.
595 + @chmod( $p, 0755 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
531 596 }
532 597 }
533 598
534 599 // Private (htaccess-protected) subdirs.
@@ -534,9 +599,9 @@
534 599 // Private (htaccess-protected) subdirs.
535 600 foreach ( array( 'cookies', 'templates', 'templates' . DIRECTORY_SEPARATOR . 'emails', 'bulk' ) as $sub ) {
536 601 $p = $base . $sub;
537 602 if ( is_dir( $p ) ) {
538 - @chmod( $p, 0750 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- NOSONAR - best-effort hardening.
603 + @chmod( $p, 0750 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- NOSONAR - best-effort hardening.
539 604 }
540 605 }
541 606
542 607 // Per-user dirs and all descendants: mainwp/<userid>/, /<userid>/bulk/, per-site
@@ -542,22 +607,104 @@
542 607 // Per-user dirs and all descendants: mainwp/<userid>/, /<userid>/bulk/, per-site
543 608 // <siteid>/ dirs from get_mainwp_specific_dir($website->id), and any deeper paths
544 609 // that backup_download_file() may have materialized via dirname($pFile) on legacy
545 610 // installs with custom backup filenames. All private (0750).
546 - $chmod_recursive = function ( $dir ) use ( &$chmod_recursive ) {
547 - @chmod( $dir, 0750 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- NOSONAR - best-effort hardening.
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.
548 680 $subs = @glob( $dir . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- NOSONAR - best-effort directory walk.
549 681 if ( is_array( $subs ) ) {
550 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 + }
551 687 $chmod_recursive( $s );
552 688 }
553 689 }
554 690 };
555 691
556 - $userdirs = @glob( $base . '[0-9]*', GLOB_ONLYDIR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- NOSONAR - best-effort directory walk.
692 + $userdirs = @glob( $base . '[0-9]*', GLOB_ONLYDIR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- NOSONAR - best-effort directory walk.
693 +
557 694 if ( is_array( $userdirs ) ) {
558 695 foreach ( $userdirs as $udir ) {
559 - $chmod_recursive( $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 + }
560 707 }
561 708 }
562 709 }
563 710