diff --git a/i18n_access.info b/i18n_access.info index e19050a..074cec2 100644 --- a/i18n_access.info +++ b/i18n_access.info @@ -2,6 +2,9 @@ name = Translation Access description = Control access to creating content in different languages. package = Multilanguage core = 7.x +configure = admin/config/regional/language/access dependencies[] = locale dependencies[] = translation +dependencies[] = i18n_node +files[] = i18n_access.test diff --git a/i18n_access.install b/i18n_access.install index 24aefb4..95c935c 100644 --- a/i18n_access.install +++ b/i18n_access.install @@ -42,4 +42,5 @@ function i18n_access_install() { * Implements hook_uninstall(). */ function i18n_access_uninstall() { + variable_del('i18n_access_languages'); } diff --git a/i18n_access.module b/i18n_access.module index 5f4aa56..3b68458 100644 --- a/i18n_access.module +++ b/i18n_access.module @@ -2,32 +2,14 @@ /** * @file - * file_description + * i18n_access.module */ -define('I18N_ACCESS_LANGUAGE_NEUTRAL', 'NEUTRAL'); - /** * Implements hook_user_insert(). */ function i18n_access_user_insert(&$edit, &$account, $category = NULL) { - if ($category == 'account') { - // see user_admin_perm_submit() - if (isset($edit['i18n_access'])) { - db_delete('i18n_access') - ->condition('uid', $account->uid) - ->execute(); - $edit['i18n_access'] = array_filter($edit['i18n_access']); - if (count($edit['i18n_access'])) { - db_insert('i18n_access') - ->fields(array( - 'uid' => $account->uid, - 'perm' => implode(', ', array_keys($edit['i18n_access'])), - ))->execute(); - } - unset($edit['i18n_access']); - } - } + i18n_access_user_update($edit, $account, $category); } /** @@ -54,10 +36,19 @@ function i18n_access_user_update(&$edit, &$account, $category = NULL) { } /** + * Implements hook_user_delete(). + */ +function i18n_access_user_delete($account) { + db_delete('i18n_access') + ->condition('uid', $account->uid) + ->execute(); +} + +/** * Load the language permissions for a given user */ function i18n_access_load_permissions($uid = NULL) { - static $perms = array(); + $perms = &drupal_static(__FUNCTION__); // use the global user id if none is passed if (!isset($uid)) { @@ -94,7 +85,8 @@ function i18n_access_permission() { return array( 'access selected languages' => array( 'title' => t('Access selected languages'), - 'description' => t('access selected languages.'), + 'description' => t('This permission gives this role edit/delete access to all content which are in the selected language. View/create access needs a different access level.', array('!url' => url('admin/config/regional/language/access'))), + 'restrict access' => TRUE, ), ); } @@ -102,34 +94,39 @@ function i18n_access_permission() { /** * Implements hook_form_node_form_alter(). */ -function i18n_access_form_node_form_alter(&$form, &$form_state, $form_id) { - - if (isset($form['language']['#options'])) { - // Remove inaccessible languages from the select box - // don't do it for admininstrators - if (!user_access('administer nodes')) { - $perms = i18n_access_load_permissions(); - foreach ($form['language']['#options'] as $key => $value) { - $perm_key = ($key == '') ? I18N_ACCESS_LANGUAGE_NEUTRAL : $key; - if ($key!='en' && empty($perms[$perm_key])) { - unset($form['language']['#options']["$key"]); - } +function i18n_access_form_node_form_alter(&$form) { + $form['#after_build'][] = '_i18n_access_form_node_form_alter'; +} + +/** + * Unset's languages from language options if user does not have permission to + * use. + * + * @param $form + * @param $form_state + * @return mixed + */ +function _i18n_access_form_node_form_alter($form, &$form_state) { + if (isset($form['language']['#options']) && !user_access('bypass node access')) { + $perms = i18n_access_load_permissions(); + foreach ($form['language']['#options'] as $key => $value) { + if (empty($perms[$key])) { + unset($form['language']['#options'][$key]); } } - unset($form['#after_build']['0']); } + + return $form; } /** * Implements hook_form_alter(). */ function i18n_access_form_alter(&$form, &$form_state, $form_id) { - //Configuring translation edit form to limit it to allowed language - if ($form_id == 'i18n_node_select_translation' && !user_access('administer nodes')) { + if ($form_id == 'i18n_node_select_translation' && !user_access('bypass node access')) { $perms = i18n_access_load_permissions(); - foreach ($form['translations']['nid'] as $language => $translation) { if (!isset($perms[$language]) && $language != '#tree') { unset($form['translations']['nid'][$language]); @@ -159,17 +156,15 @@ function i18n_access_form_alter(&$form, &$form_state, $form_id) { ); $form['i18n_access']['i18n_access'] = array( '#type' => 'checkboxes', - '#options' => array(I18N_ACCESS_LANGUAGE_NEUTRAL => t('Language neutral')) + locale_language_list('name'), + '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'), '#default_value' => i18n_access_load_permissions($form['#user']->uid), - '#description' => t('Select the languages that this user should have permission to create and edit content for.'), + '#description' => t('The user get edit, delete access to all content which are in this enabled languages. Create, view access needs a different access level.'), ); } } /** - * Wrapper around node_access() with additional checks for language permissions. - * - * @see node_access() + * Implements hook_node_access(). */ function i18n_access_node_access($node, $op, $account = NULL) { if (is_object($node)) { @@ -181,29 +176,16 @@ function i18n_access_node_access($node, $op, $account = NULL) { $account = $user; } - // Bypass completely if node_access returns false. - //TODO $access = node_access($node, $op, $account); - - /* TODO if (!$access) { - return FALSE; - } */ - // This module doesn't deal with view permissions if ($op == 'view') { return NODE_ACCESS_IGNORE; } - // make sure that administrators always have access - if (user_access('administer nodes', $account)) { - return TRUE; - } - $perms = i18n_access_load_permissions($account->uid); // Make sure to use the language neutral constant if node language is empty - $langcode = $node->language ? $node->language : I18N_ACCESS_LANGUAGE_NEUTRAL; + $langcode = $node->language ? $node->language : LANGUAGE_NONE; - //return isset($perms[$langcode]) ? (bool) $perms[$langcode] : NODE_ACCESS_DENY; return isset($perms[$langcode]) ? NODE_ACCESS_ALLOW : NODE_ACCESS_DENY; } } @@ -212,14 +194,26 @@ function i18n_access_node_access($node, $op, $account = NULL) { * Implements hook_menu_alter(). */ function i18n_access_menu_alter(&$items) { - // Replace the translation overview page since we can't hook it. - $items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview'; + if (isset($items['node/%node/translate'])) { + $items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview'; + } } +/** + * Most logic comes from translation/i18n_node module. + * + * We removes here only the "add translation" links for languages which are not your selected language. + * + * @see translation_node_overview + * @see i18n_node_translation_overview + * + * @param object $node + * + * @return array. + */ function i18n_access_translation_node_overview($node) { include_once DRUPAL_ROOT . '/includes/language.inc'; - if (!empty($node->tnid)) { // Already part of a set, grab that set. $tnid = $node->tnid; @@ -231,16 +225,12 @@ function i18n_access_translation_node_overview($node) { $translations = array($node->language => $node); } - $type = variable_get('translation_language_type', LANGUAGE_TYPE_INTERFACE); $header = array(t('Language'), t('Title'), t('Status'), t('Operations')); - - //added from i18n/i18n_node/i18n_node.pages.inc function + $rows = array(); global $user; - $account = $user; - $perms = i18n_access_load_permissions($account->uid); + $perms = i18n_access_load_permissions($user->uid); //end - // Modes have different allowed languages foreach (i18n_node_language_list($node) as $langcode => $language_name) { if ($langcode == LANGUAGE_NONE) { @@ -268,15 +258,11 @@ function i18n_access_translation_node_overview($node) { else { // No such translation in the set yet: help user to create it. $title = t('n/a'); - if (node_access('create', $node)) { + if (node_access('create', $node->type) && (!empty($perms[$langcode]) || user_access('bypass node access'))) { $text = t('add translation'); $path = 'node/add/' . str_replace('_', '-', $node->type); $query = array('query' => array('translation' => $node->nid, 'target' => $langcode)); - - //condition added from i18n/i18n_node/i18n_node.pages.inc - if (in_array($langcode, $perms)) { - $options[] = i18n_node_translation_link($text, $path, $langcode, $query); - } + $options[] = i18n_node_translation_link($text, $path, $langcode, $query); } $status = t('Not translated'); } @@ -301,9 +287,7 @@ function i18n_access_translation_node_overview($node) { * Implements hook_menu(). */ function i18n_access_menu() { - $items = array(); - - $items['admin/settings/language/access'] = array( + $items['admin/config/regional/language/access'] = array( 'title' => 'Access', 'page callback' => 'drupal_get_form', 'page arguments' => array('i18n_access_admin_settings'), @@ -311,23 +295,20 @@ function i18n_access_menu() { 'type' => MENU_LOCAL_TASK, 'weight' => 10, ); - return $items; } /** - * Admin settings form + * Admin settings form. */ -function i18n_access_admin_settings() { - +function i18n_access_admin_settings($form) { $form['i18n_access_languages'] = array( '#title' => t('Select the default access languages'), '#type' => 'select', - '#multiple' => 'true', - '#options' => array(I18N_ACCESS_LANGUAGE_NEUTRAL => t('Language neutral')) + locale_language_list('name'), + '#multiple' => TRUE, + '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'), '#default_value' => variable_get('i18n_access_languages', array()), '#description' => t("This selection of languages will be connected with the 'access selected languages' permission which you can use to grant a role access to these languages at once.") ); - return system_settings_form($form); -} \ No newline at end of file +} diff --git a/i18n_access.test b/i18n_access.test index d32dbf4..5b2f443 100644 --- a/i18n_access.test +++ b/i18n_access.test @@ -6,10 +6,17 @@ */ class i18nAccessTestCase extends DrupalWebTestCase { + + protected $admin_user; + + protected $translator; + + protected $visitor; + /** * Implementation of getInfo(). */ - function getInfo() { + public static function getInfo() { return array( 'name' => t('Translation Access'), 'description' => t('Test suite for the i18n_access module.'), @@ -20,22 +27,21 @@ class i18nAccessTestCase extends DrupalWebTestCase { /** * Implementation of setUp(). */ - function setUp() { - parent::setUp('locale', 'translation', 'i18n_access'); + public function setUp() { + parent::setUp(array('locale', 'translation', 'i18n_access', 'i18n_node')); - $this->admin_user = $this->drupalCreateUser(array('administer languages', 'administer site configuration', 'access administration pages', 'administer content types', 'administer nodes', 'administer users')); - $this->translator = $this->drupalCreateUser(array('create story content', 'edit own story content', 'translate content')); + $this->admin_user = $this->drupalCreateUser(array('administer languages', 'administer site configuration', 'access administration pages', 'administer content types', 'administer users', 'bypass node access', 'translate content')); + $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content')); $this->visitor = $this->drupalCreateUser(array('access content')); $this->drupalLogin($this->admin_user); + $this->addLanguage('fr'); $this->addLanguage('de'); - $this->setLanguagePermissions($this->translator, array('en', 'fr')); // Set Story content type to use multilingual support with translation. - $edit = array(); $edit['language_content_type'] = 2; - $this->drupalPost('admin/content/node-type/story', $edit, t('Save content type')); - $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Story')), t('Story content type has been updated.')); + $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type')); + $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Article')), 'Story content type has been updated.'); } @@ -47,26 +53,27 @@ class i18nAccessTestCase extends DrupalWebTestCase { */ function addLanguage($language_code) { // Check to make sure that language has not already been installed. - $this->drupalGet('admin/settings/language'); + $this->drupalGet('admin/config/regional/language'); if (strpos($this->drupalGetContent(), 'enabled[' . $language_code . ']') === FALSE) { // Doesn't have language installed so add it. $edit = array(); $edit['langcode'] = $language_code; - $this->drupalPost('admin/settings/language/add', $edit, t('Add language')); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - $languages = language_list('language', TRUE); // Make sure not using cached version. - $this->assertTrue(array_key_exists($language_code, $languages), t('Language was installed successfully.')); + drupal_static_reset('language_list'); // Make sure not using cached version. + $languages = language_list('language'); + $this->assertTrue(array_key_exists($language_code, $languages), 'Language was installed successfully.'); if (array_key_exists($language_code, $languages)) { - $this->assertRaw(t('The language %language has been created and can now be used.', array('%language' => $languages[$language_code]->name)), t('Language has been created.')); + $this->assertRaw(t('The language %language has been created and can now be used.', array('%language' => $languages[$language_code]->name)), 'Language has been created.'); } } else { // Ensure that it is enabled. $this->drupalPost(NULL, array('enabled[' . $language_code . ']' => TRUE), t('Save configuration')); - $this->assertRaw(t('Configuration saved.'), t('Language successfully enabled.')); + $this->assertRaw(t('Configuration saved.'), 'Language successfully enabled.'); } } @@ -80,8 +87,9 @@ class i18nAccessTestCase extends DrupalWebTestCase { * An array of language codes to give permission for */ function setLanguagePermissions($account, $languages = array()) { - $this->assertTrue(user_access('administer users'), t('User has permission to administer users')); - + $this->assertTrue(user_access('administer users'), 'User has permission to administer users'); + $expected = array(); + $edit = array(); foreach ($languages as $langcode) { $key = 'i18n_access[' . $langcode . ']'; $edit[$key] = $langcode; @@ -90,7 +98,31 @@ class i18nAccessTestCase extends DrupalWebTestCase { $this->drupalPost('user/' . $account->uid . '/edit', $edit, t('Save')); $actual = i18n_access_load_permissions($account->uid); - $this->assertEqual($expected, $actual, t('Language permissions set correctly.'), 'i18n_access'); + $this->assertEqual($expected, $actual, 'Language permissions set correctly.', 'i18n_access'); + } + + /** + * Unsets the language permission for the specified user. Must be logged in as + * an 'administer users' privileged user before calling this. + * + * @param $account + * The user account to modify + * @param $languages + * An array of language codes to remove permission for + */ + function unsetLanguagePermissions($account, $languages = array()) { + $this->assertTrue(user_access('administer users'), 'User has permission to administer users'); + $expected = array(); + $edit = array(); + foreach ($languages as $langcode) { + $key = 'i18n_access[' . $langcode . ']'; + $edit[$key] = FALSE; + } + $this->drupalPost('user/' . $account->uid . '/edit', $edit, t('Save')); + drupal_static_reset('i18n_access_load_permissions'); + drupal_static_reset('node_access'); + $actual = i18n_access_load_permissions($account->uid); + $this->assertEqual($expected, $actual, 'Language permissions unset correctly.', 'i18n_access'); } /** @@ -109,7 +141,6 @@ class i18nAccessTestCase extends DrupalWebTestCase { function assertLanguageOption($langcode, $message, $group = 'Other') { $xpath = '//select[@name="language"]/option'; $fields = $this->xpath($xpath); - // If value specified then check array for match. $found = TRUE; if (isset($langcode)) { @@ -157,52 +188,145 @@ class i18nAccessTestCase extends DrupalWebTestCase { return $this->assertFalse($fields && $found, $message, $group); } - function dsm($object) { - $this->error('
' . check_plain(print_r($object, 1)) . ''); - } - /** - * Test translator user. User with 'create story content' and 'edit own story - * content' permissions should be able to create and edit story nodes only in + * Test translator user. User with 'create article content' permission + * should be able to create and edit article nodes only in/for * the languages that they have permissions for. */ function testTranslatorUser() { + $this->_testTranslatorNodeAccess(); + $this->_testTranslatorNodeAccess(TRUE); + } + + + function _testTranslatorNodeAccess($via_role = FALSE) { + $this->drupalLogin($this->admin_user); + if (!$via_role) { + $this->setLanguagePermissions($this->translator, array('en', 'fr')); + } + else{ + $edit = array( + 'i18n_access_languages[]' => array('en', 'fr'), + ); + $this->drupalPost('admin/config/regional/language/access', $edit, t('Save configuration')); + + $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content', 'access selected languages')); + } + $this->drupalLogin($this->translator); - $this->drupalGet('node/add/story'); - $this->assertField('language', t('Found language selector.')); + $this->drupalGet('node/add/article'); + $this->assertField('language', 'Found language selector.'); $perms = i18n_access_load_permissions($this->translator->uid); $languages = language_list(); - $languages[I18N_ACCESS_LANGUAGE_NEUTRAL] = (object)array('language' => '', 'name' => 'Language Neutral'); + $languages[LANGUAGE_NONE] = (object)array('language' => LANGUAGE_NONE, 'name' => 'Language Neutral'); foreach ($languages as $key => $language) { // TODO: Add in check for language neutral if (isset($perms[$key]) && $perms[$key]) { - $this->assertLanguageOption($language->language, t('Option found for %language in language selector.', array('%language' => $language->name))); + $this->assertLanguageOption($language->language, format_string('Option found for %language in language selector.', array('%language' => $language->name))); } else { - $this->assertNoLanguageOption($language->language, t('Option not found for %language in language selector.', array('%language' => $language->name))); + $this->assertNoLanguageOption($language->language, format_string('Option not found for %language in language selector.', array('%language' => $language->name))); } } - } + $this->drupalLogin($this->admin_user); + $node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'de', 'body' => array('de' => array(array())))); + + $this->drupalLogin($this->translator); + $this->assertFalse(node_access('update', $node, $this->loggedInUser)); + $this->drupalGet('node/' . $node->nid . '/edit'); + $this->assertResponse(403); + + $this->assertFalse(node_access('delete', $node, $this->loggedInUser)); + $this->drupalGet('node/' . $node->nid . '/delete'); + $this->assertResponse(403); + + $this->drupalLogin($this->admin_user); + $node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'fr', 'body' => array('fr' => array(array())))); + + $this->drupalLogin($this->translator); + $this->assertTrue(node_access('update', $node, $this->loggedInUser)); + $this->drupalGet('node/' . $node->nid . '/edit'); + $this->assertResponse(200); + + $this->assertTrue(node_access('delete', $node, $this->loggedInUser)); + $this->drupalGet('node/' . $node->nid . '/delete'); + $this->assertResponse(200); + + $this->drupalGet('node/' . $node->nid . '/translate'); + $query = array('query' => array('translation' => $node->nid, 'target' => 'de')); + $this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'de', $query)); + $query = array('query' => array('translation' => $node->nid, 'target' => 'en')); + $this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query)); + $this->assertRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'fr')); + + $this->drupalLogin($this->admin_user); + if (!$via_role) { + $this->unsetLanguagePermissions($this->translator, array('fr', 'en')); + } + else{ + $edit = array( + 'i18n_access_languages[]' => array(), + ); + $this->drupalPost('admin/config/regional/language/access', $edit, t('Save configuration')); + $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content', 'access selected languages')); + drupal_static_reset('i18n_access_load_permissions'); + drupal_static_reset('node_access'); + } + + $this->drupalLogin($this->translator); + $this->assertFalse(node_access('update', $node, $this->loggedInUser)); + $this->drupalGet('node/' . $node->nid . '/edit'); + $this->assertResponse(403); + + $this->assertFalse(node_access('delete', $node, $this->loggedInUser)); + $this->drupalGet('node/' . $node->nid . '/delete'); + $this->assertResponse(403); + + $this->drupalGet('node/' . $node->nid . '/translate'); + $query = array('query' => array('translation' => $node->nid, 'target' => 'de')); + $this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'de', $query)); + $query = array('query' => array('translation' => $node->nid, 'target' => 'en')); + $this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query)); + $this->assertNoRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'fr')); + + } /** - * Test admin user. User with 'administer nodes' permission should be able to - * create and edit nodes regardless of the language + * Test admin user. User with 'bypass node access' permission should be able to + * update, delete nodes regardless of the language. */ function testAdminUser() { $this->drupalLogin($this->admin_user); + $this->drupalGet('node/add/article'); + $this->assertField('language', 'Found language selector.'); - $this->drupalGet('node/add/story'); - $this->assertField('language', t('Found language selector.')); - - $perms = i18n_access_load_permissions($this->admin_user->uid); $languages = language_list(); - $languages[I18N_ACCESS_LANGUAGE_NEUTRAL] = (object)array('language' => '', 'name' => 'Language Neutral'); + $languages[LANGUAGE_NONE] = (object)array('language' => LANGUAGE_NONE, 'name' => 'Language Neutral'); foreach ($languages as $language) { - // TODO: Add in check for language neutral - $this->assertLanguageOption($language->language, t('Option found for %language, regardless of permission, for administrator.', array('%language' => $language->name))); + $this->assertLanguageOption($language->language, format_string('Option found for %language, regardless of permission, for administrator.', array('%language' => $language->name))); } + $this->drupalLogin($this->translator); + $node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'de', 'body' => array('de' => array(array())))); + + $this->drupalLogin($this->admin_user); + + $this->assertTrue(node_access('update', $node, $this->loggedInUser)); + $this->drupalGet('node/' . $node->nid . '/edit'); + $this->assertResponse(200); + + $this->assertTrue(node_access('delete', $node, $this->loggedInUser)); + $this->drupalGet('node/' . $node->nid . '/delete'); + $this->assertResponse(200); + + $this->drupalGet('node/' . $node->nid . '/translate'); + + $query = array('query' => array('translation' => $node->nid, 'target' => 'fr')); + $this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'fr', $query)); + $query = array('query' => array('translation' => $node->nid, 'target' => 'en')); + $this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query)); + $this->assertRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'de')); } -} \ No newline at end of file +}