diff --git a/plugins/FeedsUserProcessor.inc b/plugins/FeedsUserProcessor.inc index 47d26e5..5c65bf1 100644 --- a/plugins/FeedsUserProcessor.inc +++ b/plugins/FeedsUserProcessor.inc @@ -16,6 +16,15 @@ define('FEEDS_BLOCK_NON_EXISTENT', 'block'); * Feeds processor plugin. Create users from feed items. */ class FeedsUserProcessor extends FeedsProcessor { + /** + * Search by role name. + */ + const ROLE_SEARCH_NAME = 'name'; + + /** + * Search by role id. + */ + const ROLE_SEARCH_RID = 'rid'; /** * Define entity type. @@ -150,14 +159,22 @@ class FeedsUserProcessor extends FeedsProcessor { /** * Override setTargetElement to operate on a target item that is a node. */ - public function setTargetElement(FeedsSource $source, $target_user, $target_element, $value) { + public function setTargetElement(FeedsSource $source, $target_user, $target_element, $value, array $mapping = array()) { switch ($target_element) { case 'created': $target_user->created = feeds_to_unixtime($value, REQUEST_TIME); break; + case 'language': $target_user->language = strtolower($value); break; + + case 'roles_list': + // Ensure that the role list is an array. + $value = (array) $value; + $this->rolesListSetTarget($source, $target_user, $target_element, $value, $mapping); + break; + default: parent::setTargetElement($source, $target_user, $target_element, $value); break; @@ -196,6 +213,12 @@ class FeedsUserProcessor extends FeedsProcessor { 'name' => t('User language'), 'description' => t('Default language for the user.'), ), + 'roles_list' => array( + 'name' => t('User roles'), + 'description' => t('User roles provided as role names in comma separated list.'), + 'summary_callbacks' => array(array($this, 'rolesListSummaryCallback')), + 'form_callbacks' => array(array($this, 'rolesListFormCallback')), + ), ); if (module_exists('openid')) { $targets['openid'] = array( @@ -270,4 +293,170 @@ class FeedsUserProcessor extends FeedsProcessor { } } + /** + * Returns default values for mapper "roles_list". + */ + public function rolesListDefaults() { + $roles = user_roles(TRUE); + unset($roles[DRUPAL_AUTHENTICATED_RID]); + $rids = array_keys($roles); + $rids = array_combine($rids, $rids); + return array( + 'role_search' => self::ROLE_SEARCH_NAME, + 'allowed_roles' => $rids, + 'autocreate' => 0, + 'revoke_roles' => 1, + ); + } + + /** + * Mapping configuration summary for mapper "roles_list". + */ + public function rolesListSummaryCallback(array $mapping, $target, array $form, array $form_state) { + $options = array(); + + // Add in defaults. + $defaults = $this->rolesListDefaults(); + $mapping += $defaults; + $mapping['allowed_roles'] += $defaults['allowed_roles']; + + // Role search. + $role_search_options = $this->rolesListRoleSearchOptions(); + $options[] = t('Search roles by: @search', array('@search' => $role_search_options[$mapping['role_search']])); + + // Allowed roles. + $role_names = array(); + $roles = user_roles(TRUE); + foreach (array_filter($mapping['allowed_roles']) as $rid => $enabled) { + $role_names[] = $roles[$rid]; + } + + if (empty($role_names)) { + $role_names = array('<' . t('none') . '>'); + } + $options[] = t('Allowed roles: %roles', array('%roles' => implode(', ', $role_names))); + + // Autocreate. + if ($mapping['autocreate']) { + $options[] = t('Automatically create roles'); + } + else { + $options[] = t('Only assign existing roles'); + } + + // Revoke roles. + if ($mapping['revoke_roles']) { + $options[] = t('Revoke roles: yes'); + } + else { + $options[] = t('Revoke roles: no'); + } + + return implode('
', $options); + } + + /** + * Mapping configuration form callback for mapper "roles_list". + */ + public function rolesListFormCallback(array $mapping, $target, array $form, array $form_state) { + // Add in defaults. + $defaults = $this->rolesListDefaults(); + $mapping += $defaults; + $mapping['allowed_roles'] += $defaults['allowed_roles']; + + $allowed_roles_options = user_roles(TRUE); + unset($allowed_roles_options[DRUPAL_AUTHENTICATED_RID]); + + return array( + 'role_search' => array( + '#type' => 'select', + '#title' => t('Search roles by'), + '#options' => $this->rolesListRoleSearchOptions(), + '#default_value' => $mapping['role_search'], + ), + 'allowed_roles' => array( + '#type' => 'checkboxes', + '#title' => t('Allowed roles'), + '#options' => $allowed_roles_options, + '#default_value' => $mapping['allowed_roles'], + '#description' => t('Select the roles to accept from the feed.
Any other roles will be ignored.') + ), + 'autocreate' => array( + '#type' => 'checkbox', + '#title' => t('Auto create'), + '#description' => t("Create the role if it doesn't exist."), + '#default_value' => $mapping['autocreate'], + ), + 'revoke_roles' => array( + '#type' => 'checkbox', + '#title' => t('Revoke roles'), + '#description' => t('If enabled, roles that are not provided by the feed will be revoked for the user. This affects only the "Allowed roles" as configured above.'), + '#default_value' => $mapping['revoke_roles'], + ), + ); + } + + /** + * Returns role list options. + */ + public function rolesListRoleSearchOptions() { + return array( + self::ROLE_SEARCH_NAME => t('Role name'), + self::ROLE_SEARCH_RID => t('Role ID'), + ); + } + + /** + * Sets role target on the user entity. + */ + public function rolesListSetTarget(FeedsSource $source, $entity, $target, array $values, array $mapping) { + // Add in defaults. + $defaults = $this->rolesListDefaults(); + $mapping += $defaults; + $mapping['allowed_roles'] += $defaults['allowed_roles']; + + // Eventually revoke roles. Do not touch roles that are not allowed to set + // by the source. + if ($mapping['revoke_roles']) { + foreach ($mapping['allowed_roles'] as $rid) { + unset($entity->roles[$rid]); + } + } + + foreach ($values as $value) { + $role = NULL; + + $value = trim($value); + if (strlen($value) < 1) { + // No role provided. Continue to the next role. + continue; + } + + switch ($mapping['role_search']) { + case self::ROLE_SEARCH_NAME: + $role = user_role_load_by_name($value); + if (!$role && !empty($mapping['autocreate'])) { + // Create new role if role doesn't exist. + $role = new stdClass(); + $role->name = $value; + user_role_save($role); + $role = user_role_load_by_name($role->name); + } + break; + + case self::ROLE_SEARCH_RID: + $role = user_role_load($value); + break; + } + + if ($role) { + // Check if the role may be assigned. + if (isset($mapping['allowed_roles'][$role->rid]) && !$mapping['allowed_roles'][$role->rid]) { + // This role may *not* be assiged. + continue; + } + $entity->roles[$role->rid] = $role->name; + } + } + } } diff --git a/tests/feeds.test b/tests/feeds.test index b3ce145..e803525 100644 --- a/tests/feeds.test +++ b/tests/feeds.test @@ -468,7 +468,14 @@ class FeedsWebTestCase extends DrupalWebTestCase { // Set some settings. $edit = array(); foreach ($config as $key => $value) { - $edit["config[$i][settings][$key]"] = $value; + if (is_array($value)) { + foreach ($value as $subkey => $subvalue) { + $edit["config[$i][settings][$key][$subkey]"] = $subvalue; + } + } + else { + $edit["config[$i][settings][$key]"] = $value; + } } $this->drupalPostAJAX(NULL, $edit, 'mapping_settings_update_' . $i); $this->drupalPost(NULL, array(), t('Save')); diff --git a/tests/feeds/users_roles.csv b/tests/feeds/users_roles.csv new file mode 100644 index 0000000..414cfef --- /dev/null +++ b/tests/feeds/users_roles.csv @@ -0,0 +1,5 @@ +name,mail,since,password,roles,rids +Morticia,morticia@example.com,1244347500,mort,editor,6 +Fester,fester@example.com,1241865600,fest,manager,4 +Gomez,gomez@example.com,1228572000,gome,tester| |manager,5| |4 +Pugsley,pugsley@example.com,1228260225,pugs,, \ No newline at end of file diff --git a/tests/feeds_processor_user.test b/tests/feeds_processor_user.test index 4bc82d5..81372a7 100644 --- a/tests/feeds_processor_user.test +++ b/tests/feeds_processor_user.test @@ -18,9 +18,11 @@ class FeedsCSVtoUsersTest extends FeedsWebTestCase { } /** - * Test node creation, refreshing/deleting feeds and feed items. + * {@inheritdoc} */ - public function test() { + public function setUp() { + parent::setUp(); + // Create an importer. $this->createImporterConfiguration('User import', 'user_import'); @@ -57,7 +59,12 @@ class FeedsCSVtoUsersTest extends FeedsWebTestCase { 'content_type' => '', ); $this->drupalPost('admin/structure/feeds/user_import/settings', $edit, 'Save'); + } + /** + * Test user creation, refreshing/deleting feeds and feed items. + */ + public function test() { // Create roles and assign one of them to the users to be imported. $manager_rid = $this->drupalCreateRole(array('access content'), 'manager'); $admin_rid = $this->drupalCreateRole(array('access content'), 'administrator'); @@ -134,4 +141,337 @@ class FeedsCSVtoUsersTest extends FeedsWebTestCase { $this->assertText('Failed importing 2 user'); } + /** + * Tests mapping to role without automatically creating new roles. + */ + public function testRoleTargetWithoutRoleCreation() { + // Add mapping to role. + $this->addMappings('user_import', array( + 4 => array( + 'source' => 'roles', + 'target' => 'roles_list', + ), + )); + + // Create manager role. + $manager_rid = $this->drupalCreateRole(array('access content'), 'manager'); + + // Import CSV file. + $this->importFile('user_import', $this->absolutePath() . '/tests/feeds/users_roles.csv'); + + // Assert that Mortica did not get the editor role and has one role in + // total. + $account = user_load_by_name('Morticia'); + $this->assertFalse(in_array('editor', $account->roles), 'Morticia does not have the editor role.'); + $this->assertEqual(1, count($account->roles), 'Morticia has one role.'); + + // Assert that Fester got the manager role and two roles in total. + $account = user_load_by_name('Fester'); + $this->assertTrue(isset($account->roles[$manager_rid]), 'Fester has the manager role.'); + $this->assertEqual(2, count($account->roles), 'Fester has two roles.'); + + // Assert that Gomez got the manager role but not the tester role, since + // that role doesn't exist on the system. + $account = user_load_by_name('Gomez'); + $this->assertTrue(isset($account->roles[$manager_rid]), 'Gomez has the manager role.'); + $this->assertFalse(in_array('tester', $account->roles), 'Gomez does not have the tester role.'); + $this->assertEqual(2, count($account->roles), 'Gomez has two roles.'); + + // Assert that Pugsley only has one role. + $account = user_load_by_name('Pugsley'); + $this->assertEqual(1, count($account->roles), 'Pugsley has one role.'); + + // Assert that only three roles exist: + // - authenticated user + // - role from the admin user + // - manager + $roles = user_roles(TRUE); + $this->assertEqual(3, count($roles), 'Only three roles exist.'); + } + + /** + * Tests mapping to role with automatically creating new roles. + */ + public function testRoleTargetWithRoleCreation() { + // Add mapping to role. + $this->addMappings('user_import', array( + 4 => array( + 'source' => 'roles', + 'target' => 'roles_list', + 'autocreate' => TRUE, + ), + )); + + // Create manager role. + $manager_rid = $this->drupalCreateRole(array('access content'), 'manager'); + + // Import CSV file. + $this->importFile('user_import', $this->absolutePath() . '/tests/feeds/users_roles.csv'); + + // Assert that Mortica got the editor role and two roles in total. + $account = user_load_by_name('Morticia'); + $this->assertTrue(in_array('editor', $account->roles), 'Morticia has the editor role.'); + $this->assertEqual(2, count($account->roles), 'Morticia has two roles.'); + + // Assert that Fester got the manager role and two roles in total. + $account = user_load_by_name('Fester'); + $this->assertTrue(isset($account->roles[$manager_rid]), 'Fester has the manager role.'); + $this->assertEqual(2, count($account->roles), 'Fester has two roles.'); + + // Assert that Gomez got the manager, the editor role and three roles in + // total. + $account = user_load_by_name('Gomez'); + $this->assertTrue(isset($account->roles[$manager_rid]), 'Gomez has the manager role.'); + $this->assertTrue(in_array('tester', $account->roles), 'Gomez has the tester role.'); + $this->assertEqual(3, count($account->roles), 'Gomez has three roles.'); + + // Assert that Pugsley only has one role. + $account = user_load_by_name('Pugsley'); + $this->assertEqual(1, count($account->roles), 'Pugsley has two roles.'); + + // Assert that five roles exist: + // - authenticated user + // - role from the admin user + // - manager + // - editor + // - tester + $roles = user_roles(TRUE); + $this->assertEqual(5, count($roles), 'Five roles exist.'); + } + + /** + * Tests mapping to role using role ID's. + */ + public function testRoleTargetRids() { + // Add mapping to role. + $this->addMappings('user_import', array( + 4 => array( + 'source' => 'rids', + 'target' => 'roles_list', + 'role_search' => FeedsUserProcessor::ROLE_SEARCH_RID, + ), + )); + + // Create manager and tester roles. + $manager_rid = $this->drupalCreateRole(array('access content'), 'manager'); + $tester_rid = $this->drupalCreateRole(array('access content'), 'tester'); + // Ensure expected ID's of these roles. + $this->assertEqual(4, $manager_rid); + $this->assertEqual(5, $tester_rid); + + // Import CSV file. + $this->importFile('user_import', $this->absolutePath() . '/tests/feeds/users_roles.csv'); + + // Assert that Mortica did not get the editor role and has one role in + // total. + $account = user_load_by_name('Morticia'); + $this->assertFalse(in_array('editor', $account->roles), 'Morticia does not have the editor role.'); + $this->assertEqual(1, count($account->roles), 'Morticia has one role.'); + + // Assert that Fester got the manager role and two roles in total. + $account = user_load_by_name('Fester'); + $this->assertTrue(isset($account->roles[$manager_rid]), 'Fester has the manager role.'); + $this->assertEqual(2, count($account->roles), 'Fester has two roles.'); + + // Assert that Gomez got the manager and tester roles. + $account = user_load_by_name('Gomez'); + $this->assertTrue(isset($account->roles[$manager_rid]), 'Gomez has the manager role.'); + $this->assertTrue(isset($account->roles[$manager_rid]), 'Gomez has the tester role.'); + $this->assertEqual(3, count($account->roles), 'Gomez has two roles.'); + + // Assert that Pugsley only has one role. + $account = user_load_by_name('Pugsley'); + $this->assertEqual(1, count($account->roles), 'Pugsley has one role.'); + + // Assert that four roles exist: + // - authenticated user + // - role from the admin user + // - manager + // - tester + $roles = user_roles(TRUE); + $this->assertEqual(4, count($roles), 'Four roles exist.'); + } + + /** + * Tests mapping to role using only allowed roles. + */ + public function testRoleTargetWithAllowedRoles() { + // Create manager role. + $manager_rid = $this->drupalCreateRole(array('access content'), 'manager'); + // Create editor role. + $editor_rid = $this->drupalCreateRole(array('access content'), 'editor'); + + // Add mapping to role. + // The manager role may not be assigned to the user by the feed. + $this->addMappings('user_import', array( + 4 => array( + 'source' => 'roles', + 'target' => 'roles_list', + 'allowed_roles' => array( + $manager_rid => FALSE, + $editor_rid => $editor_rid, + ), + 'autocreate' => TRUE, + ), + )); + + // Import CSV file. + $this->importFile('user_import', $this->absolutePath() . '/tests/feeds/users_roles.csv'); + + // Assert that Mortica got the editor role and two roles in total. + $account = user_load_by_name('Morticia'); + $this->assertTrue(in_array('editor', $account->roles), 'Morticia has the editor role.'); + $this->assertEqual(2, count($account->roles), 'Morticia has two roles.'); + + // Assert that Fester did not got the manager role, because that role was + // not an allowed value. + $account = user_load_by_name('Fester'); + $this->assertFalse(isset($account->roles[$manager_rid]), 'Fester does not have the manager role.'); + $this->assertEqual(1, count($account->roles), 'Fester has one role.'); + + // Assert that Gomez only got the tester role and not the manager role. + $account = user_load_by_name('Gomez'); + $this->assertFalse(isset($account->roles[$manager_rid]), 'Gomez does not have the manager role.'); + $this->assertTrue(in_array('tester', $account->roles), 'Gomez has the tester role.'); + $this->assertEqual(2, count($account->roles), 'Gomez has two roles.'); + } + + /** + * Tests that roles can be revoked and that only allowed roles are revoked. + */ + public function testRoleTargetRevokeRoles() { + // Create manager role. + $manager_rid = $this->drupalCreateRole(array('access content'), 'manager'); + // Create editor role. + $editor_rid = $this->drupalCreateRole(array('access content'), 'editor'); + // Create tester role. + $tester_rid = $this->drupalCreateRole(array('access content'), 'tester'); + + // Set to update existing users. + $this->setSettings('user_import', 'FeedsUserProcessor', array('update_existing' => 2)); + + // Add mapping to role. + // The manager role may not be revoked, but the editor role may. + $this->addMappings('user_import', array( + 4 => array( + 'source' => 'roles', + 'target' => 'roles_list', + 'allowed_roles' => array( + $manager_rid => FALSE, + $editor_rid => $editor_rid, + $tester_rid => $tester_rid, + ), + ), + )); + + // Create account for Morticia with roles "manager" and "editor". In the + // source only "editor" is specified. Morticia should keep both roles. + user_save(drupal_anonymous_user(), array( + 'name' => 'Morticia', + 'mail' => 'morticia@example.com', + 'pass' => 'mort', + 'status' => 1, + 'roles' => array( + $manager_rid => $manager_rid, + $editor_rid => $editor_rid, + ), + )); + // Create account for Pugsley with roles "manager", "editor" and "tester". + // Pugsley has no roles in the source so should only keep the "manager" + // role. + user_save(drupal_anonymous_user(), array( + 'name' => 'Pugsley', + 'mail' => 'pugsley@example.com', + 'pass' => 'pugs', + 'status' => 1, + 'roles' => array( + $manager_rid => $manager_rid, + $editor_rid => $editor_rid, + $tester_rid => $tester_rid, + ), + )); + // Create account for Gomez and give it the "editor" role. Gomez has roles + // "tester" and "manager" in the source, so it should lose the "editor" role + // and gain the "tester" role. + user_save(drupal_anonymous_user(), array( + 'name' => 'Gomez', + 'mail' => 'gomez@example.com', + 'pass' => 'gome', + 'status' => 1, + 'roles' => array( + $editor_rid => $editor_rid, + ), + )); + + // Import CSV file. + $this->importFile('user_import', $this->absolutePath() . '/tests/feeds/users_roles.csv'); + + // Assert that Mortica kept the manager and editor roles. + $account = user_load_by_name('Morticia'); + $this->assertTrue(isset($account->roles[$manager_rid]), 'Morticia still has the manager role.'); + $this->assertTrue(isset($account->roles[$editor_rid]), 'Morticia has the editor role.'); + $this->assertEqual(3, count($account->roles), 'Morticia has three roles.'); + + // Assert that Pugsley only kept the manager role. + $account = user_load_by_name('Pugsley'); + $this->assertTrue(isset($account->roles[$manager_rid]), 'Pugsley still has the manager role.'); + $this->assertFalse(isset($account->roles[$editor_rid]), 'Pugsley no longer has the editor role.'); + $this->assertFalse(isset($account->roles[$tester_rid]), 'Pugsley no longer has the tester role.'); + $this->assertEqual(2, count($account->roles), 'Pugsley has two roles.'); + + // Assert that Gomez lost the editor role, and gained the tester role. + $account = user_load_by_name('Gomez'); + $this->assertFalse(isset($account->roles[$editor_rid]), 'Gomez no longer has the editor role.'); + $this->assertTrue(isset($account->roles[$tester_rid]), 'Gomez has the tester role.'); + $this->assertEqual(2, count($account->roles), 'Gomez has two roles.'); + } + + /** + * Tests if no roles are revoked if the option "Revoke roles" is disabled. + */ + public function testRoleTargetNoRevokeRoles() { + // Create manager role. + $manager_rid = $this->drupalCreateRole(array('access content'), 'manager'); + // Create editor role. + $editor_rid = $this->drupalCreateRole(array('access content'), 'editor'); + + // Set to update existing users. + $this->setSettings('user_import', 'FeedsUserProcessor', array('update_existing' => 2)); + + // Add mapping to role. Set option to not revoke roles. + $this->addMappings('user_import', array( + 4 => array( + 'source' => 'roles', + 'target' => 'roles_list', + 'allowed_roles' => array( + $manager_rid => FALSE, + $editor_rid => $editor_rid, + ), + 'revoke_roles' => FALSE, + ), + )); + + // Create account for Pugsley with roles "manager" and "editor". Pugsley has + // no roles, but roles should not be revoked, so Pugsley should keep all + // roles. + user_save(drupal_anonymous_user(), array( + 'name' => 'Pugsley', + 'mail' => 'pugsley@example.com', + 'pass' => 'pugs', + 'status' => 1, + 'roles' => array( + $manager_rid => $manager_rid, + $editor_rid => $editor_rid, + ), + )); + + // Import CSV file. + $this->importFile('user_import', $this->absolutePath() . '/tests/feeds/users_roles.csv'); + + // Assert that Pugsley kept all roles. + $account = user_load_by_name('Pugsley'); + $this->assertTrue(isset($account->roles[$manager_rid]), 'Pugsley still has the manager role.'); + $this->assertTrue(isset($account->roles[$editor_rid]), 'Pugsley still has the editor role.'); + $this->assertEqual(3, count($account->roles), 'Pugsley has three roles.'); + } } diff --git a/tests/feeds_tests.module b/tests/feeds_tests.module index 79df587..5688b66 100644 --- a/tests/feeds_tests.module +++ b/tests/feeds_tests.module @@ -352,20 +352,35 @@ function feeds_tests_mapper_unique(FeedsSource $source, $entity_type, $bundle, $ /** * Implements hook_feeds_after_parse(). - * - * Empties the list of items to import in case the test says that there are - * items in there with encoding issues. These items can not be processed during - * tests without having a test failure because in < PHP 5.4 that would produce - * the following warning: - * htmlspecialchars(): Invalid multibyte sequence in argument - * - * @see FeedsCSVParserTestCase::testMbstringExtensionDisabled() */ function feeds_tests_feeds_after_parse(FeedsSource $source, FeedsParserResult $result) { + // Empties the list of items to import in case the test says that there are + // items in there with encoding issues. These items can not be processed + // during tests without having a test failure because in < PHP 5.4 that would + // produce the following warning: + // htmlspecialchars(): Invalid multibyte sequence in argument + // @see FeedsCSVParserTestCase::testMbstringExtensionDisabled() if (variable_get('feeds_tests_feeds_after_parse_empty_items', FALSE)) { // Remove all items. No items will be processed. $result->items = array(); } + + if ($source->id == 'user_import') { + foreach ($result->items as &$item) { + if (!empty($item['roles']) && strpos($item['roles'], '|')) { + // Convert roles value to multiple values. + // @see FeedsCSVtoUsersTest::testRoleTargetWithoutRoleCreation() + // @see FeedsCSVtoUsersTest::testRoleTargetWithRoleCreation() + // @see FeedsCSVtoUsersTest::testRoleTargetWithAllowedRoles() + $item['roles'] = explode('|', $item['roles']); + } + if (!empty($item['rids']) && strpos($item['rids'], '|')) { + // Convert roles value to multiple values. + // @see FeedsCSVtoUsersTest::testRoleTargetRids() + $item['rids'] = explode('|', $item['rids']); + } + } + } } /**