| @@ -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 | |