From f568036c9700f1d5f09779bdf248398f6b41d85f Mon Sep 17 00:00:00 2001 From: Dave Reid Date: Sat, 30 Jul 2011 09:56:05 -0500 Subject: [PATCH] Issue #1227706: Added a file entity access API. --- file_entity.api.php | 62 +++++++++++ file_entity.module | 288 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 327 insertions(+), 23 deletions(-) diff --git a/file_entity.api.php b/file_entity.api.php index a7d9e8a..1de667d 100644 --- a/file_entity.api.php +++ b/file_entity.api.php @@ -126,3 +126,65 @@ function hook_file_view($file, $view_mode, $langcode) { */ function hook_file_view_alter($build, $type) { } + +/** + * Control access to a file. + * + * Modules may implement this hook if they want to have a say in whether or not + * a given user has access to perform a given operation on a file. + * + * The administrative account (user ID #1) always passes any access check, + * so this hook is not called in that case. Users with the "bypass file access" + * permission may always view and edit content through the administrative + * interface. + * + * Note that not all modules will want to influence access on all + * file types. If your module does not want to actively grant or + * block access, return FILE_ACCESS_IGNORE or simply return nothing. + * Blindly returning FALSE will break other file access modules. + * + * @param $op + * The operation to be performed. Possible values: + * - "create" + * - "delete" + * - "update" + * - "view" + * @param $file + * The file on which the operation is to be performed, or, if it does + * not yet exist, the type of file to be created. + * @param $account + * A user object representing the user for whom the operation is to be + * performed. + * + * @return + * FILE_ACCESS_ALLOW if the operation is to be allowed; + * FILE_ACCESS_DENY if the operation is to be denied; + * FILE_ACCESS_IGNORE to not affect this operation at all. + * + * @ingroup file_access + */ +function hook_file_access($op, $file, $account) { + $type = is_string($file) ? $file : $node->type; + + if ($op !== 'create' && (REQUEST_TIME - $file->timestamp) < 3600) { + // Deny access to files in the first hour after uplaod so that they can be moderated. + return FILE_ACCESS_DENY; + } + + // Returning nothing from this function would have the same effect. + return FILE_ACCESS_IGNORE; +} + +/** + * Control access to listings of files. + * + * @param $query + * A query object describing the composite parts of a SQL query related to + * listing files. + * + * @see hook_query_TAG_alter() + */ +function hook_query_file_access_alter(QueryAlterableInterface $query) { + // Only show files that have been uploaded more than an hour ago. + $query->condition('timestamp', REQUEST_TIME - 3600, '<='); +} diff --git a/file_entity.module b/file_entity.module index ec68932..ec08968 100644 --- a/file_entity.module +++ b/file_entity.module @@ -6,6 +6,21 @@ */ /** + * Modules should return this value from hook_file_access() to allow access to a file. + */ +define('FILE_ACCESS_ALLOW', 'allow'); + +/** + * Modules should return this value from hook_file_access() to deny access to a file. + */ +define('FILE_ACCESS_DENY', 'deny'); + +/** + * Modules should return this value from hook_file_access() to not affect file access. + */ +define('FILE_ACCESS_IGNORE', NULL); + +/** * As part of extending Drupal core's file entity API, this module adds some * functions to the 'file' namespace. For organization, those are kept in the * 'file_entity.file_api.inc' file. @@ -27,13 +42,6 @@ function file_entity_help($path, $arg) { } /** - * Access callback for files. - */ -function file_entity_access($op) { - return (user_access('administer files') || user_access($op . ' file')); -} - -/** * Implements hook_menu(). */ function file_entity_menu() { @@ -42,7 +50,7 @@ function file_entity_menu() { 'title' => 'File types', 'description' => 'Manage settings for the type of files used on your site.', 'page callback' => 'file_entity_list_types_page', - 'access arguments' => array('administer site configuration'), + 'access arguments' => array('administer file types'), 'file' => 'file_entity.admin.inc', ); $items['admin/config/media/file-types/manage/%'] = array( @@ -66,8 +74,8 @@ function file_entity_menu() { $items['file/%file'] = array( 'page callback' => 'file_entity_view_page', 'page arguments' => array(1), - 'access callback' => 'file_entity_access', - 'access arguments' => array('view'), + 'access callback' => 'file_access', + 'access arguments' => array('view', 1), 'file' => 'file_entity.pages.inc', ); $items['file/%file/view'] = array( @@ -79,8 +87,8 @@ function file_entity_menu() { 'title' => 'Edit', 'page callback' => 'file_entity_page_edit', 'page arguments' => array(1), - 'access callback' => 'file_entity_access', - 'access arguments' => array('edit'), + 'access callback' => 'file_access', + 'access arguments' => array('update', 1), 'weight' => 0, 'type' => MENU_LOCAL_TASK, 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, @@ -90,8 +98,8 @@ function file_entity_menu() { 'title' => 'Delete', 'page callback' => 'file_entity_page_delete', 'page arguments' => array(1), - 'access callback' => 'file_entity_access', - 'access arguments' => array('edit'), + 'access callback' => 'file_access', + 'access arguments' => array('delete', 1), 'weight' => 1, 'type' => MENU_LOCAL_TASK, 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, @@ -110,7 +118,7 @@ function file_entity_menu() { $access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments'))); $access += array( 'access callback' => 'user_access', - 'access arguments' => array('administer site configuration'), + 'access arguments' => array('administer file types'), ); // The file type must be passed to the page callbacks. It might be @@ -154,20 +162,60 @@ function file_entity_menu() { * Implement hook_permission(). */ function file_entity_permission() { - return array( + $permissions = array( + 'bypass file access' => array( + 'title' => t('Bypass file access control'), + 'description' => t('View, edit and delete all files regardless of permission restrictions.'), + 'restrict access' => TRUE, + ), 'administer files' => array( 'title' => t('Administer files'), - 'description' => t('Add, edit or delete files and administer settings.'), + 'restrict access' => TRUE, + ), + 'administer file types' => array( + 'title' => t('Administer file types'), + 'restrict access' => TRUE, + ), + 'view public files' => array( + 'title' => t('View file details'), + 'description' => t('For viewing file details, not for downloading files.'), + ), + 'view own private files' => array( + 'title' => t('View own private file details'), + 'description' => t('For viewing file details, not for downloading files.'), + ), + 'create files' => array( + 'title' => t('Add and upload files'), + ), + 'edit own files' => array( + 'title' => t('Edit own files'), + ), + 'edit any files' => array( + 'title' => t('Edit any files'), ), - 'view file' => array( - 'title' => t('View file'), - 'description' => t('View all files.'), + 'delete own files' => array( + 'title' => t('Delete own files'), ), - 'edit file' => array( - 'title' => t('Edit file'), - 'description' => t('Edit all files.'), + 'delete any files' => array( + 'title' => t('Delete any files'), ), ); + + $wrappers = array(); + foreach (file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE) as $key => $wrapper) { + if (empty($wrapper['private'])) { + $wrappers['public'][$key] = $wrapper['name']; + } + else { + $wrappers['private'][$key] = $wrapper['name']; + } + } + $wrappers += array('public' => array(t('None')), 'private' => array(t('None'))); + + $permissions['view public files']['description'] .= ' ' . t('Includes the following types of files: @wrappers.', array('@wrappers' => implode(', ', $wrappers['public']))); + $permissions['view own private files']['description'] .= ' ' . t('Includes the following types of files: @wrappers.', array('@wrappers' => implode(', ', $wrappers['private']))); + + return $permissions; } /** * Implements hook_admin_paths(). @@ -731,3 +779,197 @@ function theme_file_entity_file_link($variables) { return '' . $icon . ' ' . l($link_text, $url, $options) . ''; } + +/** + * Return a specific stream wrapper's registry information. + * + * @param $scheme + * A URI scheme, a stream is referenced as "scheme://target". + * + * @see file_get_stream_wrappers() + */ +function file_get_stream_wrapper($scheme) { + $wrappers = file_get_stream_wrappers(); + return isset($wrappers[$scheme]) ? $wrappers[$scheme] : FALSE; +} + +/** + * Implements hook_stream_wrappers_alter(). + */ +function file_entity_stream_wrappers_alter(&$wrappers) { + $wrappers['private']['private'] = TRUE; + $wrappers['temporary']['private'] = TRUE; +} + +/** + * @defgroup file_access File access rights + * @{ + * The file access system determines who can do what to which files. + * + * In determining access rights for a file, file_access() first checks + * whether the user has the "bypass file access" permission. Such users have + * unrestricted access to all files. user 1 will always pass this check. + * + * Next, all implementations of hook_file_access() will be called. Each + * implementation may explicitly allow, explicitly deny, or ignore the access + * request. If at least one module says to deny the request, it will be rejected. + * If no modules deny the request and at least one says to allow it, the request + * will be permitted. + * + * There is no access grant system for files. + * + * In file listings, the process above is followed except that + * hook_file_access() is not called on each file for performance reasons and for + * proper functioning of the pager system. When adding a filelisting to your + * module, be sure to use a dynamic query created by db_select() and add a tag + * of "file_access". This will allow modules dealing with file access to ensure + * only files to which the user has access are retrieved, through the use of + * hook_query_TAG_alter(). + * + * Note: Even a single module returning FILE_ACCESS_DENY from hook_file_access() + * will block access to the file. Therefore, implementers should take care to + * not deny access unless they really intend to. Unless a module wishes to + * actively deny access it should return FILE_ACCESS_IGNORE (or simply return + * nothing) to allow other modules to control access. + * + * Stream wrappers that are considered private should implement a 'private' + * flag equal to TRUE in hook_stream_wrappers(). + * + * @todo Unify core's hook_file_download() as a 'download' op of file_access(). + */ + +/** + * Determine if a user may perform the given operation on the specified file. + * + * @param $op + * The operation to be performed on the file. Possible values are: + * - "view" + * - "update" + * - "delete" + * - "create" + * @param $file + * The file object on which the operation is to be performed, or file type + * (e.g. 'image') for "create" operation. + * @param $account + * Optional, a user object representing the user for whom the operation is to + * be performed. Determines access for a user other than the current user. + * + * @return + * TRUE if the operation may be performed, FALSE otherwise. + */ +function file_access($op, $file, $account = NULL) { + $rights = &drupal_static(__FUNCTION__, array()); + + if (!$file || !in_array($op, array('view', 'update', 'delete', 'create'), TRUE)) { + // If there was no file to check against, or the $op was not one of the + // supported ones, we return access denied. + return FALSE; + } + + // If no user object is supplied, the access check is for the current user. + if (empty($account)) { + $account = $GLOBALS['user']; + } + + // $file may be either an object or a file type. Since file types cannot be + // an integer, use either fid or type as the static cache id. + $cid = is_object($file) ? $file->fid : $file; + + // If we've already checked access for this file, user and op, return from + // cache. + if (isset($rights[$account->uid][$cid][$op])) { + return $rights[$account->uid][$cid][$op]; + } + + if (user_access('bypass file access', $account)) { + return $rights[$account->uid][$cid][$op] = TRUE; + } + + // We grant access to the file if both of the following conditions are met: + // - No modules say to deny access. + // - At least one module says to grant access. + $access = module_invoke_all('file_access', $file, $op, $account); + if (in_array(FILE_ACCESS_DENY, $access, TRUE)) { + return $rights[$account->uid][$cid][$op] = FALSE; + } + elseif (in_array(FILE_ACCESS_ALLOW, $access, TRUE)) { + return $rights[$account->uid][$cid][$op] = TRUE; + } + + + // Fall back to default behaviors on view. + if ($op == 'view' && is_object($file)) { + $scheme = file_uri_scheme($file->uri); + $wrapper = file_get_stream_wrapper($scheme); + + if (!empty($wrapper['private'])) { + // For private files, users can view their own private files if the + // user is not anonymous, and has the 'view own private files' permission. + if ($file->uid == $account->uid && !empty($account->uid) && user_access('view own private files', $account)) { + return $rights[$account->uid][$cid][$op] = TRUE; + } + } + elseif ($file->status == FILE_STATUS_PERMANENT && user_access('view files')) { + // For non-private files, users can view if they have the 'view files' + // permission. + return $rights[$account->uid][$cid][$op] = TRUE; + } + } + + return FALSE; +} + +/** + * Implements hook_file_access(). + */ +function file_entity_file_access($op, $file, $account) { + $type = is_string($node) ? $node : $node->type; + + // If the file URI is invalid, deny access. + if (!file_valid_uri($file->uri)) { + return FILE_ACCESS_DENY; + } + + if ($op == 'create') { + if (user_access('create files')) { + return FILE_ACCESS_ALLOW; + } + } + + if ($op == 'update') { + if (user_access('edit any files', $account) || (user_access('edit own files', $account) && ($account->uid == $file->uid))) { + return FILE_ACCESS_ALLOW; + } + } + + if ($op == 'delete') { + if (user_access('delete any files', $account) || (user_access('delete own files', $account) && ($account->uid == $file->uid))) { + return FILE_ACCESS_ALLOW; + } + } + + return FILE_ACCESS_IGNORE; +} + +/** + * Implements hook_file_access() on behalf of system.module and private files. + */ +function system_file_access($op, $file, $account) { + if ($op == 'view' && file_uri_scheme($file->uri) == 'private') { + // When viewing private files, we can only invoke hook_file_download() + // if the $account user objet matches the current user. + if ($GLOBALS['user']->uid == $account->uid) { + foreach (module_implements('file_download') as $module) { + if (module_invoke($module, 'file_download', $file->uri) === -1) { + return FILE_ACCESS_DENY; + } + } + } + } + + return FILE_ACCESS_IGNORE; +} + +/** + * @} End of "defgroup file_access". + */ -- 1.7.3.1