diff --git a/plugins/FeedsUserProcessor.inc b/plugins/FeedsUserProcessor.inc index 9001b9e899be85bb4d171fb1001d875e11be75f1..25ce1e6985bf7fcbb6e9db5f3ef7fb3e9245e7ca 100644 --- a/plugins/FeedsUserProcessor.inc +++ b/plugins/FeedsUserProcessor.inc @@ -26,6 +26,21 @@ class FeedsUserProcessor extends FeedsProcessor { */ const ROLE_SEARCH_RID = 'rid'; + /** + * Unencrypted password. + */ + const PASS_UNENCRYPTED = 'none'; + + /** + * MD5 encrypted password. + */ + const PASS_MD5 = 'md5'; + + /** + * SHA512 encrypted password. + */ + const PASS_SHA512 = 'sha512'; + /** * Define entity type. */ @@ -107,6 +122,15 @@ class FeedsUserProcessor extends FeedsProcessor { } user_save($account, $edit); + + // If an encrypted password was given, directly set this in the database. + if ($account->uid && !empty($account->pass_crypted)) { + db_update('users') + ->fields(array('pass' => $account->pass_crypted)) + ->condition('uid', $account->uid) + ->execute(); + } + if ($account->uid && !empty($account->openid)) { $authmap = array( 'uid' => $account->uid, @@ -191,10 +215,14 @@ class FeedsUserProcessor extends FeedsProcessor { } /** - * Override setTargetElement to operate on a target item that is a node. + * Overrides setTargetElement() to operate on a target item that is an user. */ public function setTargetElement(FeedsSource $source, $target_user, $target_element, $value, array $mapping = array()) { switch ($target_element) { + case 'pass': + $this->setPassTarget($source, $target_user, $target_element, $value, $mapping); + break; + case 'created': $target_user->created = feeds_to_unixtime($value, REQUEST_TIME); break; @@ -236,8 +264,10 @@ class FeedsUserProcessor extends FeedsProcessor { 'description' => t('The created (e. g. joined) data of the user.'), ), 'pass' => array( - 'name' => t('Unencrypted Password'), - 'description' => t('The unencrypted user password.'), + 'name' => t('Password'), + 'description' => t('The user password.'), + 'summary_callbacks' => array(array($this, 'passSummaryCallback')), + 'form_callbacks' => array(array($this, 'passFormCallback')), ), 'status' => array( 'name' => t('Account status'), @@ -495,4 +525,99 @@ class FeedsUserProcessor extends FeedsProcessor { } } } + + /** + * Mapping configuration summary callback for target "pass". + */ + public function passSummaryCallback($mapping, $target, $form, $form_state) { + $options = $this->passSummaryCallbackOptions(); + if (!isset($mapping['pass_encryption'])) { + $mapping['pass_encryption'] = self::PASS_UNENCRYPTED; + } + return t('Password encryption: <strong>@encryption</strong>', array('@encryption' => $options[$mapping['pass_encryption']])); + } + + /** + * Returns the list of available password encryption methods. + * + * Used by ::passSummaryCallback(). + * + * @return array + * An array of password encryption option titles. + * + * @see passSummaryCallback() + */ + protected function passSummaryCallbackOptions() { + return array( + self::PASS_UNENCRYPTED => t('None'), + self::PASS_MD5 => t('MD5'), + self::PASS_SHA512 => t('SHA512'), + ); + } + + /** + * Mapping configuration form callback for target "pass". + */ + public function passFormCallback($mapping, $target, $form, $form_state) { + return array( + 'pass_encryption' => array( + '#type' => 'select', + '#title' => t('Password encryption'), + '#options' => $this->passFormCallbackOptions(), + '#default_value' => !empty($mapping['pass_encryption']) ? $mapping['pass_encryption'] : self::PASS_UNENCRYPTED, + ), + ); + } + + /** + * Returns the list of available password encryption methods. + * + * Used by ::passFormCallback(). + * + * @return array + * An array of password encryption option titles. + * + * @see passFormCallback() + */ + protected function passFormCallbackOptions() { + return array( + self::PASS_UNENCRYPTED => t('Unencrypted'), + self::PASS_MD5 => t('MD5 (used in older versions of Drupal)'), + self::PASS_SHA512 => t('SHA512 (default in Drupal 7)'), + ); + } + + /** + * Sets the password on the user target. + * + * @see setTargetElement() + */ + protected function setPassTarget($source, $target_user, $target_element, $value, $mapping) { + if (empty($value)) { + return; + } + if (!isset($mapping['pass_encryption'])) { + $mapping['pass_encryption'] = self::PASS_UNENCRYPTED; + } + + switch ($mapping['pass_encryption']) { + case self::PASS_MD5: + require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); + $new_hash = user_hash_password($value); + if ($new_hash) { + // Indicate an updated password. + $new_hash = 'U' . $new_hash; + $target_user->pass = $target_user->pass_crypted = $new_hash; + } + break; + + case self::PASS_SHA512: + $target_user->pass = $target_user->pass_crypted = $value; + break; + + default: + $target_user->pass = $value; + break; + } + } } diff --git a/tests/feeds/users.csv b/tests/feeds/users.csv index aede81cabb8c729f8717f9b617296c46acea5073..eb13a1953a852c20256fad70e6f5162e909fd6e7 100644 --- a/tests/feeds/users.csv +++ b/tests/feeds/users.csv @@ -1,6 +1,6 @@ -name,mail,since,password -Morticia,morticia@example.com,1244347500,mort -Fester,fester@example.com,1241865600,fest -Gomez,gomez@example.com,1228572000,gome -Wednesday,wednesdayexample.com,1228347137,wedn -Pugsley,pugsley@example,1228260225,pugs +name,mail,since,password,password_md5,password_sha512 +Morticia,morticia@example.com,1244347500,mort,e0108a7eb91670308fff8179a4785453,$S$DfuNE4ur7Jq8xVoJURGm8oMIYunKd366KQUE6akc3EXW/ym9ghpq +Fester,fester@example.com,1241865600,fest,c8cce3815094f01f0ab774fd4f7a77d4,$S$DjJPqmjlWTIen0nQrG3a.vA71Vc0DqCpKuB.g9zmBMnGzIV6JxqH +Gomez,gomez@example.com,1228572000,gome,8a5346b9a510f1f698ab0062b71201ac,$S$Dv.EtHlTfnrxuWGLbe3cf31mD9MF6.4u2Z46M2o2dMGgQGzi7m/5 +Wednesday,wednesdayexample.com,1228347137,wedn,fefb673afaf531dbd78771976a150dc8,$S$DdPzksGh/c8UukipWagAhTzaqUp/eNHVPiC.x6URBQyA503Z41PI +Pugsley,pugsley@example,1228260225,pugs,09189568a8ee4d0addf53d2f6e4847cd,$S$D1oUihjrYXr.4iesN8Sfw1rVRLdo188v0NRGgcNR/V09oIyYPYmZ diff --git a/tests/feeds_processor_user.test b/tests/feeds_processor_user.test index 2384303573bad16d38093f2f44d236456b742ad4..2608bde5eaa705b96f72b8749d64db2be3e8fedd 100644 --- a/tests/feeds_processor_user.test +++ b/tests/feeds_processor_user.test @@ -124,10 +124,7 @@ class FeedsCSVtoUsersTest extends FeedsWebTestCase { $this->assertText('Failed importing 2 user'); // Attempt to log in as one of the imported users. - $account = user_load_by_name('Fester'); - $this->assertTrue($account, 'Imported user account loaded.'); - $account->pass_raw = 'fest'; - $this->drupalLogin($account); + $this->feedsLoginUser('Fester', 'fest'); // Login as admin. $this->drupalLogin($this->admin_user); @@ -668,4 +665,109 @@ class FeedsCSVtoUsersTest extends FeedsWebTestCase { $this->assertFalse(isset($account->roles[$manager_rid]), 'Morticia no longer has the manager role.'); $this->assertEqual(1, count($account->roles), 'Morticia has one role.'); } + + /** + * Test if users with md5 passwords can login after import. + */ + public function testMD5() { + // Set to update existing users. + $this->setSettings('user_import', 'FeedsUserProcessor', array('update_existing' => FEEDS_UPDATE_EXISTING)); + + // Replace password mapper. + $this->removeMappings('user_import', array( + 3 => array( + 'source' => 'password', + 'target' => 'pass', + ), + )); + $this->addMappings('user_import', array( + 3 => array( + 'source' => 'password_md5', + 'target' => 'pass', + 'pass_encryption' => 'md5', + ), + )); + + // Create an account for Gomez, to ensure passwords can also be imported for + // existing users. Give Gomez a password different from the one that gets + // imported to ensure that his password gets updated. + user_save(drupal_anonymous_user(), array( + 'name' => 'Gomez', + 'mail' => 'gomez@example.com', + 'pass' => 'temporary', + 'status' => 1, + )); + + // Import CSV file. + $this->importFile('user_import', $this->absolutePath() . '/tests/feeds/users.csv'); + + // Assert result. + $this->assertText('Created 2 users'); + $this->assertText('Updated 1 user'); + + // Try to login as each successful imported user. + $this->feedsLoginUser('Morticia', 'mort'); + $this->feedsLoginUser('Fester', 'fest'); + $this->feedsLoginUser('Gomez', 'gome'); + } + + /** + * Test if users with sha512 passwords can login after import. + */ + public function testSha512() { + // Set to update existing users. + $this->setSettings('user_import', 'FeedsUserProcessor', array('update_existing' => FEEDS_UPDATE_EXISTING)); + + // Replace password mapper. + $this->removeMappings('user_import', array( + 3 => array( + 'source' => 'password', + 'target' => 'pass', + ), + )); + $this->addMappings('user_import', array( + 3 => array( + 'source' => 'password_sha512', + 'target' => 'pass', + 'pass_encryption' => 'sha512', + ), + )); + + // Create an account for Gomez, to ensure passwords can also be imported for + // existing users. Give Gomez a password different from the one that gets + // imported to ensure that his password gets updated. + user_save(drupal_anonymous_user(), array( + 'name' => 'Gomez', + 'mail' => 'gomez@example.com', + 'pass' => 'temporary', + 'status' => 1, + )); + + // Import CSV file. + $this->importFile('user_import', $this->absolutePath() . '/tests/feeds/users.csv'); + + // Assert result. + $this->assertText('Created 2 users'); + $this->assertText('Updated 1 user'); + + // Try to login as each successful imported user. + $this->feedsLoginUser('Morticia', 'mort'); + $this->feedsLoginUser('Fester', 'fest'); + $this->feedsLoginUser('Gomez', 'gome'); + } + + /** + * Log in an imported user. + * + * @param string $username + * The user's username. + * @param string $password + * The user's password. + */ + protected function feedsLoginUser($username, $password) { + $account = user_load_by_name($username); + $this->assertTrue($account, 'Imported user account loaded.'); + $account->pass_raw = $password; + $this->drupalLogin($account); + } }