| @@ -493,28 +493,20 @@ | ||
| 493 | 493 | 'to_type' => 'user', |
| 494 | 494 | 'to_fieldname' => '_bbp_old_user_id' |
| 495 | 495 | ); |
| 496 | 496 | |
| 497 | - // Store old user password (Stored in usermeta serialized with salt) | |
| 497 | + // Store old user password (Stored in usermeta as a serialized array) | |
| 498 | 498 | $this->field_map[] = array( |
| 499 | 499 | 'from_tablename' => 'users', |
| 500 | 500 | 'from_fieldname' => 'pass', |
| 501 | 501 | 'to_type' => 'user', |
| 502 | - 'to_fieldname' => '_bbp_password' | |
| 503 | - // 'callback_method' => 'callback_savepass' | |
| 502 | + 'to_fieldname' => '_bbp_password', | |
| 503 | + 'callback_method' => 'callback_savepass' | |
| 504 | 504 | ); |
| 505 | 505 | |
| 506 | - // Store old user salt (This is only used for the SELECT row info for the above password save) | |
| 507 | - $this->field_map[] = array( | |
| 508 | - 'from_tablename' => 'users', | |
| 509 | - 'from_fieldname' => 'pass', | |
| 510 | - 'to_type' => 'user', | |
| 511 | - 'to_fieldname' => '' | |
| 512 | - ); | |
| 513 | - | |
| 514 | 506 | // User password verify class (Stored in usermeta for verifying password) |
| 515 | 507 | $this->field_map[] = array( |
| 516 | - 'to_type' => 'users', | |
| 508 | + 'to_type' => 'user', | |
| 517 | 509 | 'to_fieldname' => '_bbp_class', |
| 518 | 510 | 'default' => 'Drupal7' |
| 519 | 511 | ); |
| 520 | 512 | |
| @@ -569,22 +561,66 @@ | ||
| 569 | 561 | return ''; |
| 570 | 562 | } |
| 571 | 563 | |
| 572 | 564 | /** |
| 573 | - * This method is to save the salt and password together. That | |
| 574 | - * way when we authenticate it we can get it out of the database | |
| 575 | - * as one value. Array values are auto sanitized by WordPress. | |
| 565 | + * Store the Drupal password hash in a serialized array. | |
| 566 | + * | |
| 567 | + * Array values are automatically serialized by WordPress. | |
| 576 | 568 | */ |
| 577 | 569 | public function callback_savepass( $field, $row ) { |
| 578 | - $pass_array = array( | |
| 579 | - 'hash' => $field, | |
| 580 | - 'salt' => $row['salt'] | |
| 581 | - ); | |
| 570 | + return array( 'hash' => $field ); | |
| 571 | + } | |
| 582 | 572 | |
| 583 | - return $pass_array; | |
| 573 | + /** | |
| 574 | + * Upgrade password metadata written by earlier Drupal 7 imports. | |
| 575 | + * | |
| 576 | + * Earlier imports stored the hash without serializing it. New imports use | |
| 577 | + * the parent callback and the serialized format from callback_savepass(). | |
| 578 | + * | |
| 579 | + * @param string $username WordPress user login. | |
| 580 | + * @param string $password Unslashed password for Drupal. | |
| 581 | + * @param string|null $wp_password Optional slashed password for WordPress. | |
| 582 | + */ | |
| 583 | + public function callback_pass( $username = '', $password = '', $wp_password = null ) { | |
| 584 | + $user = get_user_by( 'login', $username ); | |
| 585 | + | |
| 586 | + if ( ! empty( $user ) && '' === $user->user_pass ) { | |
| 587 | + $stored_hash = get_user_meta( $user->ID, '_bbp_password', true ); | |
| 588 | + | |
| 589 | + if ( is_string( $stored_hash ) && $this->is_drupal_hash( $stored_hash ) ) { | |
| 590 | + if ( $this->authenticate_pass( $password, serialize( array( 'hash' => $stored_hash ) ) ) ) { | |
| 591 | + $this->upgrade_user_pass( $user->ID, is_null( $wp_password ) ? $password : $wp_password ); | |
| 592 | + } | |
| 593 | + | |
| 594 | + return; | |
| 595 | + } | |
| 596 | + } | |
| 597 | + | |
| 598 | + parent::callback_pass( $username, $password, $wp_password ); | |
| 584 | 599 | } |
| 585 | 600 | |
| 586 | 601 | /** |
| 602 | + * Upgrade a Drupal hash copied directly into wp_users by an old import. | |
| 603 | + * | |
| 604 | + * @param WP_User $user WordPress user. | |
| 605 | + * @param string $password Unslashed password for Drupal. | |
| 606 | + * @param string|null $wp_password Optional slashed password for WordPress. | |
| 607 | + */ | |
| 608 | + public function callback_user_pass( $user, $password, $wp_password = null ) { | |
| 609 | + $wp_password = is_null( $wp_password ) ? $password : $wp_password; | |
| 610 | + | |
| 611 | + // Do not replace a password that WordPress or a password plugin already | |
| 612 | + // recognizes, regardless of the stored hash prefix. | |
| 613 | + if ( ! ( $user instanceof WP_User ) || wp_check_password( $wp_password, $user->user_pass, $user->ID ) || ! $this->is_drupal_hash( $user->user_pass, false ) ) { | |
| 614 | + return; | |
| 615 | + } | |
| 616 | + | |
| 617 | + if ( $this->authenticate_pass( $password, serialize( array( 'hash' => $user->user_pass ) ) ) ) { | |
| 618 | + $this->upgrade_user_pass( $user->ID, $wp_password ); | |
| 619 | + } | |
| 620 | + } | |
| 621 | + | |
| 622 | + /** | |
| 587 | 623 | * This method is to take the pass out of the database and compare |
| 588 | 624 | * to a pass the user has typed in. |
| 589 | 625 | */ |
| 590 | 626 | public function authenticate_pass( $password, $serialized_pass ) { |
| @@ -589,26 +625,153 @@ | ||
| 589 | 625 | */ |
| 590 | 626 | public function authenticate_pass( $password, $serialized_pass ) { |
| 591 | 627 | |
| 592 | 628 | // Unserialize the password, with safeguards |
| 593 | - $pass_array = unserialize( | |
| 594 | - $serialized_pass, | |
| 595 | - array( | |
| 596 | - 'allowed_classes' => false, | |
| 597 | - 'max_depth' => 1 | |
| 598 | - ) | |
| 599 | - ); | |
| 629 | + $pass_array = $this->unserialize_pass( $serialized_pass ); | |
| 600 | 630 | |
| 601 | - // Bail if missing values | |
| 602 | - if ( ! is_array( $pass_array ) || ! isset( $pass_array['hash'], $pass_array['salt'] ) ) { | |
| 631 | + // Bail if missing or invalid values | |
| 632 | + if ( ! is_string( $password ) || ! is_array( $pass_array ) || ! isset( $pass_array['hash'] ) || ! is_string( $pass_array['hash'] ) ) { | |
| 603 | 633 | return false; |
| 604 | 634 | } |
| 605 | 635 | |
| 606 | - // Return comparison | |
| 607 | - return hash_equals( | |
| 608 | - $pass_array['hash'], | |
| 609 | - md5( md5( $password ) . $pass_array['salt'] ) | |
| 610 | - ); | |
| 636 | + $stored_hash = $pass_array['hash']; | |
| 637 | + | |
| 638 | + // Drupal 6 passwords upgraded to Drupal 7 use an MD5 pre-hash | |
| 639 | + if ( 0 === strpos( $stored_hash, 'U$' ) ) { | |
| 640 | + $stored_hash = substr( $stored_hash, 1 ); | |
| 641 | + $password = md5( $password ); | |
| 642 | + } | |
| 643 | + | |
| 644 | + switch ( substr( $stored_hash, 0, 3 ) ) { | |
| 645 | + case '$S$' : | |
| 646 | + $hash = $this->hash_password( 'sha512', $password, $stored_hash ); | |
| 647 | + break; | |
| 648 | + | |
| 649 | + case '$H$' : | |
| 650 | + case '$P$' : | |
| 651 | + $hash = $this->hash_password( 'md5', $password, $stored_hash ); | |
| 652 | + break; | |
| 653 | + | |
| 654 | + default : | |
| 655 | + return false; | |
| 656 | + } | |
| 657 | + | |
| 658 | + return is_string( $hash ) && hash_equals( $stored_hash, $hash ); | |
| 659 | + } | |
| 660 | + | |
| 661 | + /** | |
| 662 | + * Hash a password using Drupal 7's portable password algorithm. | |
| 663 | + * | |
| 664 | + * @param string $algorithm Hash algorithm. | |
| 665 | + * @param string $password Plain-text password. | |
| 666 | + * @param string $setting Stored hash or hash setting. | |
| 667 | + * @return string|bool Password hash on success, false on failure. | |
| 668 | + */ | |
| 669 | + private function hash_password( $algorithm, $password, $setting ) { | |
| 670 | + if ( strlen( $password ) > 512 ) { | |
| 671 | + return false; | |
| 672 | + } | |
| 673 | + | |
| 674 | + $setting = substr( $setting, 0, 12 ); | |
| 675 | + if ( 12 !== strlen( $setting ) || '$' !== $setting[0] || '$' !== $setting[2] ) { | |
| 676 | + return false; | |
| 677 | + } | |
| 678 | + | |
| 679 | + $characters = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; | |
| 680 | + $count_log2 = strpos( $characters, $setting[3] ); | |
| 681 | + $salt = substr( $setting, 4, 8 ); | |
| 682 | + | |
| 683 | + if ( false === $count_log2 || $count_log2 < 7 || $count_log2 > 30 || 8 !== strlen( $salt ) ) { | |
| 684 | + return false; | |
| 685 | + } | |
| 686 | + | |
| 687 | + $count = 1 << $count_log2; | |
| 688 | + $hash = hash( $algorithm, $salt . $password, true ); | |
| 689 | + | |
| 690 | + do { | |
| 691 | + $hash = hash( $algorithm, $hash . $password, true ); | |
| 692 | + } while ( --$count ); | |
| 693 | + | |
| 694 | + $output = $setting . $this->base64_encode_password( $hash, strlen( $hash ) ); | |
| 695 | + $expected = 12 + ceil( ( 8 * strlen( $hash ) ) / 6 ); | |
| 696 | + | |
| 697 | + return ( strlen( $output ) === (int) $expected ) | |
| 698 | + ? substr( $output, 0, 55 ) | |
| 699 | + : false; | |
| 700 | + } | |
| 701 | + | |
| 702 | + /** | |
| 703 | + * Encode bytes using Drupal 7's password base64 alphabet. | |
| 704 | + * | |
| 705 | + * @param string $input Bytes to encode. | |
| 706 | + * @param int $count Number of bytes to encode. | |
| 707 | + * @return string Encoded bytes. | |
| 708 | + */ | |
| 709 | + private function base64_encode_password( $input, $count ) { | |
| 710 | + $output = ''; | |
| 711 | + $index = 0; | |
| 712 | + $characters = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; | |
| 713 | + | |
| 714 | + do { | |
| 715 | + $value = ord( $input[ $index++ ] ); | |
| 716 | + $output .= $characters[ $value & 0x3f ]; | |
| 717 | + if ( $index < $count ) { | |
| 718 | + $value |= ord( $input[ $index ] ) << 8; | |
| 719 | + } | |
| 720 | + $output .= $characters[ ( $value >> 6 ) & 0x3f ]; | |
| 721 | + if ( $index++ >= $count ) { | |
| 722 | + break; | |
| 723 | + } | |
| 724 | + if ( $index < $count ) { | |
| 725 | + $value |= ord( $input[ $index ] ) << 16; | |
| 726 | + } | |
| 727 | + $output .= $characters[ ( $value >> 12 ) & 0x3f ]; | |
| 728 | + if ( $index++ >= $count ) { | |
| 729 | + break; | |
| 730 | + } | |
| 731 | + $output .= $characters[ ( $value >> 18 ) & 0x3f ]; | |
| 732 | + } while ( $index < $count ); | |
| 733 | + | |
| 734 | + return $output; | |
| 735 | + } | |
| 736 | + | |
| 737 | + /** | |
| 738 | + * Determine whether a stored value is a Drupal password hash. | |
| 739 | + * | |
| 740 | + * @param string $hash Stored password hash. | |
| 741 | + * @param bool $allow_phpass Whether to include WordPress-compatible $P$. | |
| 742 | + * @return bool True when the value has a supported Drupal hash prefix. | |
| 743 | + */ | |
| 744 | + private function is_drupal_hash( $hash, $allow_phpass = true ) { | |
| 745 | + if ( ! is_string( $hash ) ) { | |
| 746 | + return false; | |
| 747 | + } | |
| 748 | + | |
| 749 | + // A $P$ hash in metadata is unambiguously from Drupal, while one copied | |
| 750 | + // into user_pass is also a WordPress-compatible password hash. | |
| 751 | + $prefixes = $allow_phpass | |
| 752 | + ? array( '$S$', '$H$', '$P$', 'U$' ) | |
| 753 | + : array( '$S$', '$H$', 'U$' ); | |
| 754 | + | |
| 755 | + foreach ( $prefixes as $prefix ) { | |
| 756 | + if ( 0 === strpos( $hash, $prefix ) ) { | |
| 757 | + return true; | |
| 758 | + } | |
| 759 | + } | |
| 760 | + | |
| 761 | + return false; | |
| 762 | + } | |
| 763 | + | |
| 764 | + /** | |
| 765 | + * Replace a verified Drupal hash with a WordPress password. | |
| 766 | + * | |
| 767 | + * @param int $user_id WordPress user ID. | |
| 768 | + * @param string $password Password to hash for WordPress. | |
| 769 | + */ | |
| 770 | + private function upgrade_user_pass( $user_id, $password ) { | |
| 771 | + wp_set_password( $password, $user_id ); | |
| 772 | + delete_user_meta( $user_id, '_bbp_password' ); | |
| 773 | + delete_user_meta( $user_id, '_bbp_class' ); | |
| 611 | 774 | } |
| 612 | 775 | |
| 613 | 776 | /** |
| 614 | 777 | * Translate the post status from Drupal v7.x numerics to WordPress's |