From f568036c9700f1d5f09779bdf248398f6b41d85f Mon Sep 17 00:00:00 2001
From: Dave Reid <dave@davereid.net>
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: <em>@wrappers</em>.', array('@wrappers' => implode(', ', $wrappers['public'])));
+  $permissions['view own private files']['description'] .= ' ' . t('Includes the following types of files: <em>@wrappers</em>.', array('@wrappers' => implode(', ', $wrappers['private'])));
+
+  return $permissions;
 }
 /**
  * Implements hook_admin_paths().
@@ -731,3 +779,197 @@ function theme_file_entity_file_link($variables) {
 
   return '<span class="file">' . $icon . ' ' . l($link_text, $url, $options) . '</span>';
 }
+
+/**
+ * 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

