modules/comment/comment.module | 9 ++++ modules/file/file.api.php | 67 +++++++++++++++++++++++++++++++ modules/file/file.module | 85 +++++++++++++++++++++++++-------------- modules/file/tests/file.test | 64 +++++++++++++++++++++++++++++- modules/node/node.module | 9 ++++ modules/user/user.module | 9 ++++ 6 files changed, 211 insertions(+), 32 deletions(-) diff --git modules/comment/comment.module modules/comment/comment.module index e916b07..c40f2b6 100644 --- modules/comment/comment.module +++ modules/comment/comment.module @@ -2615,3 +2615,12 @@ function comment_rdf_mapping() { ), ); } + +/** + * Implements hook_file_download_access(). + */ +function comment_file_download_access($field, $entity_type, $entity) { + if ($entity_type == 'comment') { + return user_access('access comments') && $entity->status == COMMENT_PUBLISHED || user_access('administer comments'); + } +} diff --git modules/file/file.api.php modules/file/file.api.php new file mode 100644 index 0000000..b5cf793 --- /dev/null +++ modules/file/file.api.php @@ -0,0 +1,67 @@ + $grants['node']); + } +} diff --git modules/file/file.module modules/file/file.module index 1e6e80f..7f5d05a 100644 --- modules/file/file.module +++ modules/file/file.module @@ -141,46 +141,69 @@ function file_file_download($uri, $field_type = 'file') { // Find out which (if any) file fields contain this file. $references = file_get_file_references($file, NULL, FIELD_LOAD_REVISION, $field_type); - // TODO: Check field-level access if available here. - - $denied = $file->status ? NULL : FALSE; - // Check access to content containing the file fields. If access is allowed - // to any of this content, allow the download. + // Default to allow access. + $denied = FALSE; + // Loop through all references of this file. If a reference explicitly allows + // access to the field to which this file belongs, no further checks are done + // and download access is granted. If a reference denies access, eventually + // existing additional references are checked. If all references were checked + // and no reference denied access, access is granted as well. If at least one + // reference denied access, access is denied. foreach ($references as $field_name => $field_references) { foreach ($field_references as $entity_type => $type_references) { - foreach ($type_references as $reference) { - // If access is allowed to any object, immediately stop and grant - // access. If access is denied, continue through in case another object - // grants access. - // TODO: Switch this to a universal access check mechanism if available. - if ($entity_type == 'node' && ($node = node_load($reference->nid))) { - if (node_access('view', $node)) { - $denied = FALSE; - break 3; - } - else { - $denied = TRUE; + foreach ($type_references as $id => $reference) { + // Try to load $entity and $field. + $entity = reset(entity_load($entity_type, array($id))); + $field = NULL; + if ($entity) { + // Load all fields for that entity. + $field_items = field_get_items($entity_type, $entity, $field_name); + + // Find the field item with the matching URI. + foreach ($field_items as $field_item) { + if ($field_item['uri'] == $uri) { + $field = $field_item; + break; + } } } - if ($entity_type == 'user') { - if (user_access('access user profiles') || $user->uid == $reference->uid) { - $denied = FALSE; - break 3; - } - else { - $denied = TRUE; - } + + // Check that $entity and $field were loaded successfully and check if + // access to that field is not disallowed. If any of these checks fail, + // stop checking access for this reference. + if (empty($entity) || empty($field) || !field_access('view', $field, $entity_type, $entity)) { + $denied = TRUE; + break; + } + + // Invoke hook and collect grants/denies for download access. + // Default to FALSE and let entities overrule this ruling. + $grants = array('system' => FALSE); + foreach (module_implements('file_download_access') as $module) { + $grants = array_merge($grants, array($module => module_invoke($module, 'file_download_access', $field, $entity_type, $entity))); + } + // Allow other modules to alter the returned grants/denies. + drupal_alter('file_download_access', $grants, $field, $entity_type, $entity); + + if (in_array(TRUE, $grants)) { + // If TRUE is returned, access is granted and no further checks are + // necessary. + $denied = FALSE; + break 3; + } + + if (in_array(FALSE, $grants)) { + // If an implementation returns FALSE, access to this entity is denied + // but the file could belong to another entity to which the user might + // have access. Continue with these. + $denied = TRUE; } } } } - // No access was denied or granted. - if (!isset($denied)) { - return; - } - // Access specifically denied and not granted elsewhere. - elseif ($denied == TRUE) { + // Access specifically denied. + if ($denied) { return -1; } diff --git modules/file/tests/file.test modules/file/tests/file.test index 4ff5ace..4560b05 100644 --- modules/file/tests/file.test +++ modules/file/tests/file.test @@ -14,7 +14,7 @@ class FileFieldTestCase extends DrupalWebTestCase { function setUp() { parent::setUp('file'); - $this->admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer users', 'administer content types', 'administer nodes', 'bypass node access')); + $this->admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer users', 'administer permissions', 'administer content types', 'administer nodes', 'bypass node access')); $this->drupalLogin($this->admin_user); } @@ -301,6 +301,68 @@ class FileFieldWidgetTestCase extends FileFieldTestCase { $this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_name"); $this->assertFieldByXpath('//input[@id="edit-field-settings-uri-scheme-public" and not(@disabled)]', 'public', t('Upload destination setting enabled.')); } + + /** + * Tests that download restrictions on private files work on comments. + */ + function testPrivateFileComment() { + $user = $this->drupalCreateUser(array('access comments')); + + // Remove access comments permission from anon user. + $edit = array( + '1[access comments]' => FALSE, + ); + $this->drupalPost('admin/people/permissions', $edit, t('Save permissions')); + + // Create a new field. + $edit = array( + '_add_new_field[label]' => $label = $this->randomName(), + '_add_new_field[field_name]' => $name = strtolower($this->randomName()), + '_add_new_field[type]' => 'file', + '_add_new_field[widget_type]' => 'file_generic', + ); + $this->drupalPost('admin/structure/types/manage/article/comment/fields', $edit, t('Save')); + $edit = array('field[settings][uri_scheme]' => 'private'); + $this->drupalPost(NULL, $edit, t('Save field settings')); + $this->drupalPost(NULL, array(), t('Save settings')); + + // Create node. + $text_file = $this->getTestFile('text'); + $edit = array( + 'title' => $this->randomName(), + ); + $this->drupalPost('node/add/article', $edit, t('Save')); + + // Add a comment with a file. + $text_file = $this->getTestFile('text'); + $edit = array( + 'files[field_' . $name . '_' . LANGUAGE_NONE . '_' . 0 . ']' => realpath($text_file->uri), + 'comment_body[' . LANGUAGE_NONE . '][0][value]' => $comment_body = $this->randomName(), + ); + $this->drupalPost(NULL, $edit, t('Save')); + + // Get the comment ID. + preg_match('/comment-([0-9]+)/', $this->getUrl(), $matches); + $cid = $matches[1]; + + // Log in as normal user. + $this->drupalLogin($user); + + $comment = comment_load($cid); + $comment_file = (object) $comment->{'field_' . $name}[LANGUAGE_NONE][0]; + $this->assertFileExists($comment_file, t('New file saved to disk on node creation.')); + // Test authenticated file download. + $url = file_create_url($comment_file->uri); + $this->assertNotEqual($url, NULL, t('Confirmed that the URL is valid')); + $this->drupalGet(file_create_url($comment_file->uri)); + $this->assertResponse(200, t('Confirmed that the generated URL is correct by downloading the shipped file.')); + + // Test anonymous file download. + $this->drupalLogout(); + $this->drupalGet(file_create_url($comment_file->uri)); + $this->assertResponse(403, t('Confirmed that access is denied for the file without the needed permission.')); + } + } /** diff --git modules/node/node.module modules/node/node.module index 4c82202..52edf5d 100644 --- modules/node/node.module +++ modules/node/node.module @@ -3763,3 +3763,12 @@ class NodeController extends DrupalDefaultEntityController { return $query; } } + +/** + * Implements hook_file_download_access(). + */ +function node_file_download_access($field, $entity_type, $entity) { + if ($entity_type == 'node') { + return node_access('view', $entity); + } +} diff --git modules/user/user.module modules/user/user.module index d74d7c0..624ba7d 100644 --- modules/user/user.module +++ modules/user/user.module @@ -3684,3 +3684,12 @@ function user_rdf_mapping() { ), ); } + +/** + * Implements hook_file_download_access(). + */ +function user_file_download_access($field, $entity_type, $entity) { + if ($entity_type == 'user') { + return user_view_access($entity); + } +}