<?php

/**
 * @file
 * Extends Drupal file entities to be fieldable and viewable.
 */

/**
 * The default list of file extensions allowed by file_save_upload().
 *
 * Adds support for docx, xlsx, pptx and ppsx.
 */
define('FILE_ENTITY_DEFAULT_ALLOWED_EXTENSIONS', 'jpg jpeg gif png txt doc docx xls xlsx pdf ppt pptx pps ppsx odt ods odp');

/**
 * Modules should return this value from hook_file_entity_access() to allow
 * access to a file.
 */
define('FILE_ENTITY_ACCESS_ALLOW', 'allow');

/**
 * Modules should return this value from hook_file_entity_access() to deny
 * access to a file.
 */
define('FILE_ENTITY_ACCESS_DENY', 'deny');

/**
 * Modules should return this value from hook_file_entity_access() to not affect
 * file access.
 */
define('FILE_ENTITY_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.
 */
require_once dirname(__FILE__) . '/file_entity.file_api.inc';

// @todo Remove when http://drupal.org/node/977052 is fixed.
require_once dirname(__FILE__) . '/file_entity.field.inc';

/**
 * Implements hook_hook_info().
 */
function file_entity_hook_info() {
  $hooks = array(
    'file_operation_info',
    'file_operation_info_alter',
    'file_type_info',
    'file_type_info_alter',
    'file_formatter_info',
    'file_formatter_info_alter',
    'file_view',
    'file_view_alter',
    'file_displays_alter',
    'file_type',
    'file_type_alter',
  );

  return array_fill_keys($hooks, array('group' => 'file'));
}

/**
 * Implements hook_hook_info_alter().
 *
 * Add support for existing core hooks to be located in modulename.file.inc.
 */
function file_entity_hook_info_alter(&$info) {
  $hooks = array(
    // File API hooks
    'file_copy',
    'file_move',
    'file_validate',
    // File access
    'file_download',
    'file_download_access',
    'file_download_access_alter',
    // File entity hooks
    'file_load',
    'file_presave',
    'file_insert',
    'file_update',
    'file_delete',
    // Miscellanious hooks
    'file_mimetype_mapping_alter',
    'file_url_alter',
  );
  $info += array_fill_keys($hooks, array('group' => 'file'));
}

/**
 * Implements hook_help().
 */
function file_entity_help($path, $arg) {
  switch ($path) {
    case 'admin/structure/file-types':
      $output = '<p>' . t('When a file is uploaded to this website, it is assigned one of the following types, based on what kind of file it is.') . '</p>';
      return $output;
    case 'admin/structure/file-types/manage/%/display/preview':
    case 'admin/structure/file-types/manage/%/file-display/preview':
      drupal_set_message(t('Some modules rely on the Preview view mode to function correctly. Changing these settings may break parts of your site.'), 'warning');
      break;
  }
}

/**
 * Implements hook_menu().
 */
function file_entity_menu() {
  // File Configuration
  // @todo Move this back to admin/config/media/file-types in Drupal 8 if
  // MENU_MAX_DEPTH is increased to a value higher than 9.
  $items['admin/structure/file-types'] = array(
    '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 file types'),
    'file' => 'file_entity.admin.inc',
  );
  $items['admin/structure/file-types/manage/%file_type'] = array(
    'title' => 'Manage file types',
    'description' => 'Manage settings for the type of files used on your site.',
  );
  $items['admin/content/file'] = array(
    'title' => 'Files',
    'description' => 'Manage files used on your site.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('file_entity_admin_files'),
    'access arguments' => array('administer files'),
    'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM,
    'file' => 'file_entity.admin.inc',
  );
  $items['admin/content/file/list'] = array(
    'title' => 'List',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  // general view, edit, delete for files
  $items['file/add'] = array(
    'title' => 'Add file',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('file_entity_add_upload', array()),
    'access callback' => 'file_entity_access',
    'access arguments' => array('create'),
    'file' => 'file_entity.pages.inc',
  );
  if (module_exists('plupload') && module_exists('multiform')) {
    $items['file/add']['page arguments'] = array('file_entity_add_upload_multiple');
  }
  $items['file/add/upload'] = array(
    'title' => 'Upload',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['file/%file'] = array(
    'title callback' => 'entity_label',
    'title arguments' => array('file', 1),
    // The page callback also invokes drupal_set_title() in case
    // the menu router's title is overridden by a menu link.
    'page callback' => 'file_entity_view_page',
    'page arguments' => array(1),
    'access callback' => 'file_entity_access',
    'access arguments' => array('view', 1),
    'file' => 'file_entity.pages.inc',
  );
  $items['file/%file/view'] = array(
    'title' => 'View',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['file/%file/usage'] = array(
    'title' => 'Usage',
    'page callback' => 'file_entity_usage_page',
    'page arguments' => array(1),
    'access callback' => 'file_entity_access',
    'access arguments' => array('update', 1),
    'type' => MENU_LOCAL_TASK,
    'context' => MENU_CONTEXT_PAGE,
    'file' => 'file_entity.pages.inc',
  );
  $items['file/%file/edit'] = array(
    'title' => 'Edit',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('file_entity_edit', 1),
    'access callback' => 'file_entity_access',
    'access arguments' => array('update', 1),
    'weight' => 0,
    'type' => MENU_LOCAL_TASK,
    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
    'file' => 'file_entity.pages.inc',
  );
  $items['file/%file/delete'] = array(
    'title' => 'Delete',
    'page callback' => 'drupal_get_form',
    'page arguments'  => array('file_entity_delete_form', 1),
    'access callback' => 'file_entity_access',
    'access arguments' => array('delete', 1),
    'weight' => 1,
    'type' => MENU_LOCAL_TASK,
    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
    'file' => 'file_entity.pages.inc',
  );

  // Attach a "Manage file display" tab to each file type in the same way that
  // Field UI attaches "Manage fields" and "Manage display" tabs. Note that
  // Field UI does not have to be enabled; we're just using the same IA pattern
  // here for attaching the "Manage file display" page.
  $entity_info = entity_get_info('file');
  foreach ($entity_info['bundles'] as $file_type => $bundle_info) {
    if (isset($bundle_info['admin'])) {
      // Get the base path and access.
      $path = $bundle_info['admin']['path'];
      $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 file types'),
      );

      // The file type must be passed to the page callbacks. It might be
      // configured as a wildcard (multiple file types sharing the same menu
      // router path).
      $file_type_argument = isset($bundle_info['admin']['bundle argument']) ? $bundle_info['admin']['bundle argument'] : $file_type;

      $items[$path] = array(
        'title' => 'Edit file type',
        'title callback' => 'file_entity_type_get_name',
        'title arguments' => array(4),
        'page callback' => 'drupal_get_form',
        'page arguments' => array('file_entity_file_type_form', $file_type_argument),
        'file' => 'file_entity.admin.inc',
      ) + $access;

      // Add the 'File type settings' tab.
      $items["$path/edit"] = array(
        'title' => 'Edit',
        'type' => MENU_DEFAULT_LOCAL_TASK,
      );

      // Add the 'Manage file display' tab.
      $items["$path/file-display"] = array(
        'title' => 'Manage file display',
        'page callback' => 'drupal_get_form',
        'page arguments' => array('file_entity_file_display_form', $file_type_argument, 'default'),
        'type' => MENU_LOCAL_TASK,
        'weight' => 3,
        'file' => 'file_entity.admin.inc',
      ) + $access;

      // Add a secondary tab for each view mode.
      $weight = 0;
      $view_modes = array('default' => array('label' => t('Default'))) + $entity_info['view modes'];
      foreach ($view_modes as $view_mode => $view_mode_info) {
        $items["$path/file-display/$view_mode"] = array(
          'title' => $view_mode_info['label'],
          'page arguments' => array('file_entity_file_display_form', $file_type_argument, $view_mode),
          'type' => ($view_mode == 'default' ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK),
          'weight' => ($view_mode == 'default' ? -10 : $weight++),
          'file' => 'file_entity.admin.inc',
          // View modes for which the 'custom settings' flag isn't TRUE are
          // disabled via this access callback. This needs to extend, rather
          // than override normal $access rules.
          'access callback' => '_file_entity_view_mode_menu_access',
          'access arguments' => array_merge(array($file_type_argument, $view_mode, $access['access callback']), $access['access arguments']),
        );
      }
    }
  }

  // Optional devel module integration
  if (module_exists('devel')) {
    $items['file/%file/devel'] = array(
      'title' => 'Devel',
      'page callback' => 'devel_load_object',
      'page arguments' => array('file', 1),
      'access arguments' => array('access devel information'),
      'type' => MENU_LOCAL_TASK,
      'file' => 'devel.pages.inc',
      'file path' => drupal_get_path('module', 'devel'),
      'weight' => 100,
    );
    $items['file/%file/devel/load'] = array(
      'title' => 'Load',
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items['file/%file/devel/render'] = array(
      'title' => 'Render',
      'page callback' => 'devel_render_object',
      'page arguments' => array('file', 1),
      'access arguments' => array('access devel information'),
      'file' => 'devel.pages.inc',
      'file path' => drupal_get_path('module', 'devel'),
      'type' => MENU_LOCAL_TASK,
      'weight' => 100,
    );
    if (module_exists('token')) {
      $items['file/%file/devel/token'] = array(
        'title' => 'Tokens',
        'page callback' => 'token_devel_token_object',
        'page arguments' => array('file', 1),
        'access arguments' => array('access devel information'),
        'type' => MENU_LOCAL_TASK,
        'file' => 'token.pages.inc',
        'file path' => drupal_get_path('module', 'token'),
        'weight' => 5,
      );
    }
  }

  return $items;
}

/**
 * Implements hook_menu_local_tasks_alter().
 */
function file_entity_menu_local_tasks_alter(&$data, $router_item, $root_path) {
  // Add action link to 'file/add' on 'admin/content/file' page.
  if ($root_path == 'admin/content/file') {
    $item = menu_get_item('file/add');
    if (!empty($item['access'])) {
      $data['actions']['output'][] = array(
        '#theme' => 'menu_local_action',
        '#link' => $item,
        '#weight' => $item['weight'],
      );
    }
  }
}

/**
 * Implement hook_permission().
 */
function file_entity_permission() {
  $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 files' => array(
      'title' => t('View file details'),
      'description' => t('For viewing file details, not for downloading files.'),
    ),
    'view own files' => array(
      'title' => t('View own file details'),
      'description' => t('For viewing file details, not for downloading files.'),
    ),
    'delete own files' => array(
      'title' => t('Delete own 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'),
    ),
    'delete any files' => array(
      'title' => t('Delete any files'),
    ),
  );

  // Add description for the 'View file details' and 'View own private file
  // details' permissions to show which stream wrappers they apply to.
  $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 files']['description'] .= ' ' . t('Includes the following stream wrappers: %wrappers.', array('%wrappers' => implode(', ', $wrappers['public'])));
  $permissions['view own private files']['description'] .= ' ' . t('Includes the following stream wrappers: %wrappers.', array('%wrappers' => implode(', ', $wrappers['private'])));

  return $permissions;
}

/**
 * Implements hook_admin_paths().
 */
function file_entity_admin_paths() {
  $paths = array(
    'file/add' => TRUE,
    'file/add/*' => TRUE,
    'file/*/edit' => TRUE,
    'file/*/usage' => TRUE,
    'file/*/delete' => TRUE,
  );
  return $paths;
}

/**
 * Implements hook_theme().
 */
function file_entity_theme() {
  return array(
    'file_entity' => array(
      'render element' => 'elements',
      'template' => 'file_entity',
    ),
    'file_entity_file_type_overview' => array(
      'variables' => array('label' => NULL, 'description' => NULL),
      'file' => 'file_entity.admin.inc',
    ),
    'file_entity_file_display_order' => array(
      'render element' => 'element',
      'file' => 'file_entity.admin.inc',
    ),
    'file_entity_file_link' => array(
      'variables' => array('file' => NULL, 'icon_directory' => NULL),
      'file' => 'file_entity.theme.inc',
    ),
  );
}

/**
 * Implements hook_entity_info_alter().
 *
 * Extends the core file entity to be fieldable. The file type is used as the
 * bundle key. File types are implemented as CTools exportables, so modules can
 * define default file types via hook_file_default_types(), and the
 * administrator can override the default types or add custom ones via
 * admin/structure/file-types.
 */
function file_entity_entity_info_alter(&$entity_info) {
  $entity_info['file']['fieldable'] = TRUE;
  $entity_info['file']['entity keys']['bundle'] = 'type';
  $entity_info['file']['bundle keys']['bundle'] = 'type';
  $entity_info['file']['bundles'] = array();
  $entity_info['file']['uri callback'] = 'file_entity_uri';
  $entity_info['file']['view modes'] += array(
    'teaser' => array(
      'label' => t('Teaser'),
      'custom settings' => TRUE,
    ),
    'full' => array(
      'label' => t('Full content'),
      'custom settings' => FALSE,
    ),
    'preview' => array(
      'label' => t('Preview'),
      'custom settings' => TRUE,
    ),
  );

  foreach (file_type_get_enabled_types() as $type) {
    $entity_info['file']['bundles'][$type->type] = array(
      'label' => $type->label,
      'admin' => array(
        'path' => 'admin/structure/file-types/manage/%file_type',
        'real path' => 'admin/structure/file-types/manage/' . $type->type,
        'bundle argument' => 4,
      ),
    );
  }

  // Ensure some of the Entity API callbacks are supported.
  $entity_info['file']['creation callback'] = 'entity_metadata_create_object';
  $entity_info['file']['view callback'] = 'file_view_multiple';
  $entity_info['file']['edit callback'] = 'file_entity_metadata_form_file';
}

/**
 * Implements hook_entity_property_info().
 */
function file_entity_entity_property_info() {
  $info['file']['properties']['type'] = array(
    'label' => t('File type'),
    'type' => 'token',
    'description' => t('The type of the file.'),
    'setter callback' => 'entity_property_verbatim_set',
    'setter permission' => 'administer files',
    'options list' => 'file_entity_type_get_names',
    'required' => TRUE,
    'schema field' => 'type',
  );

  return $info;
}

/**
 * URI callback for file entities.
 */
function file_entity_uri($file) {
  $uri['path'] = 'file/' . $file->fid;
  return $uri;
}

/**
 * Entity API callback to get the form of a file entity.
 */
function file_entity_metadata_form_file($file) {
  // Pre-populate the form-state with the right form include.
  $form_state['build_info']['args'] = array($file);
  form_load_include($form_state, 'inc', 'file_entity', 'file_entity.pages');
  return drupal_build_form('file_entity_edit', $form_state);
}

/**
 * Implements hook_field_extra_fields().
 *
 * Adds 'file' as an extra field, so that its display and form component can be
 * weighted relative to the fields that are added to file entity bundles.
 */
function file_entity_field_extra_fields() {
  $info = array();

  if ($file_type_names = file_entity_type_get_names()) {
    foreach ($file_type_names as $type => $name) {
      $info['file'][$type]['form']['filename'] = array(
        'label' => t('File name'),
        'description' => t('File name'),
        'weight' => -10,
      );
      $info['file'][$type]['form']['preview'] = array(
        'label' => t('File'),
        'description' => t('File preview'),
        'weight' => -5,
      );
      $info['file'][$type]['display']['file'] = array(
        'label' => t('File'),
        'description' => t('File display'),
        'weight' => 0,
      );
    }
  }

  return $info;
}

/**
 * Implements hook_file_formatter_info().
 */
function file_entity_file_formatter_info() {
  $formatters = array();

  // Allow file field formatters to be reused for displaying the file entity's
  // file pseudo-field.
  foreach (field_info_formatter_types() as $field_formatter_type => $field_formatter_info) {
    if (in_array('file', $field_formatter_info['field types'])) {
      $formatters['file_field_' . $field_formatter_type] = array(
        'label' => $field_formatter_info['label'],
        'view callback' => 'file_entity_file_formatter_file_field_view',
      );
      if (!empty($field_formatter_info['settings'])) {
        $formatters['file_field_' . $field_formatter_type] += array(
          'default settings' => $field_formatter_info['settings'],
          'settings callback' => 'file_entity_file_formatter_file_field_settings',
        );
      }
    }
  }

  // Add a simple file formatter for displaying an image in a chosen style.
  if (module_exists('image')) {
    $formatters['file_image'] = array(
      'label' => t('Image'),
      'default settings' => array(
        'image_style' => '',
        'alt' => '',
        'title' => ''
      ),
      'view callback' => 'file_entity_file_formatter_file_image_view',
      'settings callback' => 'file_entity_file_formatter_file_image_settings',
    );
    // Provide default token values.
    if (module_exists('token')) {
      $formatters['file_image']['default settings']['alt'] = '[file:field_file_image_alt_text]';
      $formatters['file_image']['default settings']['title'] = '[file:field_file_image_title_text]';
    }
    elseif (module_exists('entity_token')) {
      $formatters['file_image']['default settings']['alt'] = '[file:field-file-image-alt-text]';
      $formatters['file_image']['default settings']['title'] = '[file:field-file-image-title-text]';
    }
  }

  return $formatters;
}

/**
 * Implements hook_file_formatter_FORMATTER_view().
 *
 * This function provides a bridge to the field formatter API, so that file
 * field formatters can be reused for displaying the file entity's file
 * pseudo-field.
 */
function file_entity_file_formatter_file_field_view($file, $display, $langcode) {
  if (strpos($display['type'], 'file_field_') === 0) {
    $field_formatter_type = substr($display['type'], strlen('file_field_'));
    $field_formatter_info = field_info_formatter_types($field_formatter_type);
    if (isset($field_formatter_info['module'])) {
      // Set $display['type'] to what hook_field_formatter_*() expects.
      $display['type'] = $field_formatter_type;

      // Set $items to what file field formatters expect. See file_field_load(),
      // and note that, here, $file is already a fully loaded entity.
      $items = array((array) $file);

      // Invoke hook_field_formatter_prepare_view() and
      // hook_field_formatter_view(). Note that we are reusing field formatter
      // functions, but we are not displaying a Field API field, so we set
      // $field and $instance accordingly, and do not invoke
      // hook_field_prepare_view(). This assumes that the formatter functions do
      // not rely on $field or $instance. A module that implements formatter
      // functions that rely on $field or $instance (and therefore, can only be
      // used for real fields) can prevent this formatter from being used on the
      // pseudo-field by removing it within hook_file_formatter_info_alter().
      $field = $instance = NULL;
      if (($function = ($field_formatter_info['module'] . '_field_formatter_prepare_view')) && function_exists($function)) {
        $fid = $file->fid;
        // hook_field_formatter_prepare_view() alters $items by reference.
        $grouped_items = array($fid => &$items);
        $function('file', array($fid => $file), $field, array($fid => $instance), $langcode, $grouped_items, array($fid => $display));
      }
      if (($function = ($field_formatter_info['module'] . '_field_formatter_view')) && function_exists($function)) {
        $element = $function('file', $file, $field, $instance, $langcode, $items, $display);
        // We passed the file as $items[0], so return the corresponding element.
        if (isset($element[0])) {
          return $element[0];
        }
      }
    }
  }
}

/**
 * Implements hook_file_formatter_FORMATTER_settings().
 *
 * This function provides a bridge to the field formatter API, so that file
 * field formatters can be reused for displaying the file entity's file
 * pseudo-field.
 */
function file_entity_file_formatter_file_field_settings($form, &$form_state, $settings, $formatter_type, $file_type, $view_mode) {
  if (strpos($formatter_type, 'file_field_') === 0) {
    $field_formatter_type = substr($formatter_type, strlen('file_field_'));
    $field_formatter_info = field_info_formatter_types($field_formatter_type);

    // Invoke hook_field_formatter_settings_form(). We are reusing field
    // formatter functions, but we are not working with a Field API field, so
    // set $field accordingly. Unfortunately, the API is for $settings to be
    // transfered via the $instance parameter, so we must mock it.
    if (isset($field_formatter_info['module']) && ($function = ($field_formatter_info['module'] . '_field_formatter_settings_form')) && function_exists($function)) {
      $field = NULL;
      $mock_instance['display'][$view_mode] = array(
        'type' => $field_formatter_type,
        'settings' => $settings,
      );
      return $function($field, $mock_instance, $view_mode, $form, $form_state);
    }
  }
}

/**
 * Implements hook_file_formatter_FORMATTER_view().
 *
 * Returns a drupal_render() array to display an image of the chosen style.
 *
 * This formatter is only capable of displaying local images. If the passed in
 * file is either not local or not an image, nothing is returned, so that
 * file_view_file() can try another formatter.
 */
function file_entity_file_formatter_file_image_view($file, $display, $langcode) {
  // Prevent PHP notices when trying to read empty files.
  // @see http://drupal.org/node/681042
  if (!$file->filesize) {
    return;
  }

  // Do not bother proceeding if this file does not have an image mime type.
  if (strpos($file->filemime, 'image/') !== 0) {
    return;
  }

  if (file_entity_file_is_readable($file) && isset($file->image_dimensions)) {
    // We don't sanitize here.
    // @see http://drupal.org/node/1553094#comment-6257382
    // Theme function will take care of escaping.
    $replace_options = array(
      'clear' => 1,
      'sanitize' => 0,
    );
    if (!empty($display['settings']['image_style'])) {
      $element = array(
        '#theme' => 'image_style',
        '#style_name' => $display['settings']['image_style'],
        '#path' => $file->uri,
        '#width' => $file->image_dimensions['width'],
        '#height' => $file->image_dimensions['height'],
        '#alt' => token_replace($display['settings']['alt'], array('file' => $file), $replace_options),
        '#title' => token_replace($display['settings']['title'], array('file' => $file), $replace_options),
      );
    }
    else {
      $element = array(
        '#theme' => 'image',
        '#path' => $file->uri,
        '#width' => $file->image_dimensions['width'],
        '#height' => $file->image_dimensions['height'],
        '#alt' => token_replace($display['settings']['alt'], array('file' => $file), $replace_options),
        '#title' => token_replace($display['settings']['title'], array('file' => $file), $replace_options),
      );
    }
    return $element;
  }
}

/**
 * Check if a file entity is readable or not.
 *
 * @param object $file
 *   A file entity object from file_load().
 *
 * @return boolean
 *   TRUE if the file is using a readable stream wrapper, or FALSE otherwise.
 */
function file_entity_file_is_readable($file) {
  $scheme = file_uri_scheme($file->uri);
  $wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_READ);
  return !empty($wrappers[$scheme]);
}

/**
 * Implements hook_file_formatter_FORMATTER_settings().
 *
 * Returns form elements for configuring the 'file_image' formatter.
 */
function file_entity_file_formatter_file_image_settings($form, &$form_state, $settings) {
  $element = array();
  $element['image_style'] = array(
    '#title' => t('Image style'),
    '#type' => 'select',
    '#options' => image_style_options(FALSE),
    '#default_value' => $settings['image_style'],
    '#empty_option' => t('None (original image)'),
  );

  // For image files we allow the alt attribute (required in HTML).
  $element['alt'] = array(
    '#title' => t('Alt attribute'),
    '#description' => t('The text to use as value for the <em>img</em> tag <em>alt</em> attribute.'),
    '#type' => 'textfield',
    '#default_value' => $settings['alt'],
  );

  // Allow the setting of the title attribute.
  $element['title'] = array(
    '#title' => t('Title attribute'),
    '#description' => t('The text to use as value for the <em>img</em> tag <em>title</em> attribute.'),
    '#type' => 'textfield',
    '#default_value' => $settings['title'],
  );

  if (module_exists('token')) {
    $element['alt']['#description'] .= t('This field supports tokens.');
    $element['title']['#description'] .= t('This field supports tokens.');
    $element['tokens'] = array(
      '#theme' => 'token_tree',
      '#token_types' => array('file'),
      '#dialog' => TRUE,
    );
  }

  return $element;
}

/**
 * Menu access callback for the 'view mode file display settings' pages.
 *
 * Based on _field_ui_view_mode_menu_access(), but the Field UI module might not
 * be enabled.
 */
function _file_entity_view_mode_menu_access($file_type, $view_mode, $access_callback) {
  // Deny access if the view mode isn't configured to use custom display
  // settings.
  $view_mode_settings = field_view_mode_settings('file', $file_type->type);
  $visibility = ($view_mode == 'default') || !empty($view_mode_settings[$view_mode]['custom_settings']);
  if (!$visibility) {
    return FALSE;
  }

  // Otherwise, continue to an $access_callback check.
  $args = array_slice(func_get_args(), 3);
  $callback = empty($access_callback) ? 0 : trim($access_callback);
  if (is_numeric($callback)) {
    return (bool) $callback;
  }
  elseif (function_exists($access_callback)) {
    return call_user_func_array($access_callback, $args);
  }
}

/**
 * Implements hook_modules_enabled().
 */
function file_entity_modules_enabled($modules) {
  file_info_cache_clear();
}

/**
 * Implements hook_modules_disabled().
 */
function file_entity_modules_disabled($modules) {
  file_info_cache_clear();
}

/**
 * Implements hook_views_api().
 */
function file_entity_views_api() {
  return array(
    'api' => 3,
  );
}

/**
 * Returns whether the current page is the full page view of the passed-in file.
 *
 * @param $file
 *   A file object.
 */
function file_entity_is_page($file) {
  $page_file = menu_get_object('file', 1);
  return (!empty($page_file) ? $page_file->fid == $file->fid : FALSE);
}

/**
 * Process variables for file_entity.tpl.php
 *
 * The $variables array contains the following arguments:
 * - $file
 * - $view_mode
 *
 * @see file_entity.tpl.php
 */
function template_preprocess_file_entity(&$variables) {
  $view_mode = $variables['view_mode'] = $variables['elements']['#view_mode'];
  $variables['file'] = $variables['elements']['#file'];
  $file = $variables['file'];

  $variables['date']      = format_date($file->timestamp);
  $account = user_load($file->uid);
  $variables['name']      = theme('username', array('account' => $account));

  // @todo Use entity_uri once http://drupal.org/node/1057242 is fixed.
  //$uri = entity_uri('file', $file);
  //$variables['file_url']  = url($uri['path'], $uri['options']);
  $variables['file_url'] = file_create_url($file->uri);
  $label = entity_label('file', $file);
  $variables['label']     = check_plain($label);
  $variables['page']      = $view_mode == 'full' && file_entity_is_page($file);

  // Hide the file name from being displayed until we can figure out a better
  // way to control this. We cannot simply not output the title since
  // contextual links require $title_suffix to be output in the template.
  // @see http://drupal.org/node/1245266
  if (!$variables['page']) {
    $variables['title_attributes_array']['class'][] = 'element-invisible';
  }

  // Flatten the file object's member fields.
  $variables = array_merge((array) $file, $variables);

  // Helpful $content variable for templates.
  $variables += array('content' => array());
  foreach (element_children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }

  // Make the field variables available with the appropriate language.
  field_attach_preprocess('file', $file, $variables['content'], $variables);

  // Attach the file object to the content element.
  $variables['content']['file']['#file'] = $file;

  // Display post information only on certain file types.
  if (variable_get('file_submitted_' . $file->type, FALSE)) {
    $variables['display_submitted'] = TRUE;
    $variables['submitted'] = t('Uploaded by !username on !datetime', array('!username' => $variables['name'], '!datetime' => $variables['date']));
    $variables['user_picture'] = theme_get_setting('toggle_file_user_picture') ? theme('user_picture', array('account' => $account)) : '';
  }
  else {
    $variables['display_submitted'] = FALSE;
    $variables['submitted'] = '';
    $variables['user_picture'] = '';
  }

  // Gather file classes.
  $variables['classes_array'][] = drupal_html_class('file-' . $file->type);
  $variables['classes_array'][] = drupal_html_class('file-' . $file->filemime);
  if ($file->status != FILE_STATUS_PERMANENT) {
    $variables['classes_array'][] = 'file-temporary';
  }

  // Change the 'file-entity' class into 'file'
  if ($variables['classes_array'][0] == 'file-entity') {
    $variables['classes_array'][0] = 'file';
  }

  // Clean up name so there are no underscores.
  $variables['theme_hook_suggestions'][] = 'file__' . $file->type;
  $variables['theme_hook_suggestions'][] = 'file__' . $file->type . '__' . $view_mode;
  $variables['theme_hook_suggestions'][] = 'file__' . str_replace(array('/', '-'), array('__', '_'), $file->filemime);
  $variables['theme_hook_suggestions'][] = 'file__' . str_replace(array('/', '-'), array('__', '_'), $file->filemime) . '__' . $view_mode;
  $variables['theme_hook_suggestions'][] = 'file__' . $file->fid;
  $variables['theme_hook_suggestions'][] = 'file__' . $file->fid . '__' . $view_mode;
}

/**
 * Returns the file type name of the passed file or file type string.
 *
 * @param $file
 *   A file object or string that indicates the file type to return.
 *
 * @return
 *   The file type name or FALSE if the file type is not found.
 */
function file_entity_type_get_name($file) {
  $type = is_object($file) ? $file->type : $file;
  $info = entity_get_info('file');
  return isset($info['bundles'][$type]['label']) ? $info['bundles'][$type]['label'] : FALSE;
}

/**
 * Returns a list of available file type names.
 *
 * @return
 *   An array of file type names, keyed by the type.
 */
function file_entity_type_get_names() {
  $names = &drupal_static(__FUNCTION__);

  if (!isset($names)) {
    $info = entity_get_info('file');
    foreach ($info['bundles'] as $bundle => $bundle_info) {
      $names[$bundle] = $bundle_info['label'];
    }
  }

  return $names;
}

/**
 * Return an array of available view modes for file entities.
 */
function file_entity_view_mode_labels() {
  $labels = &drupal_static(__FUNCTION__);

  if (!isset($options)) {
    $entity_info = entity_get_info('file');
    $labels = array('default' => t('Default'));
    foreach ($entity_info['view modes'] as $machine_name => $mode) {
      $labels[$machine_name] = $mode['label'];
    }
  }

  return $labels;
}

/**
 * Return the label for a specific file entity view mode.
 */
function file_entity_view_mode_label($view_mode, $default = FALSE) {
  $labels = file_entity_view_mode_labels();
  return isset($labels[$view_mode]) ? $labels[$view_mode] : $default;
}

/**
 * Return information about all bulk file operations.
 *
 * @see hook_file_operation_info()
 * @see hook_file_operation_info_alter()
 *
 * @todo Add real caching to this function?
 */
function file_entity_get_file_operation_info() {
  $info = &drupal_static(__FUNCTION__);

  if (!isset($info)) {
    $info = module_invoke_all('file_operation_info');

    // Support the deprecated hooks.
    // @todo Remove after 2.0 release.
    $info = drupal_array_merge_deep(module_invoke_all('file_operations_info'), $info);
    drupal_alter('file_entity_file_operations', $old_info);

    drupal_alter('file_operation_info_alter', $info);
  }

  return $info;
}

/**
 * File operation to show a confirm form for file deletion.
 *
 * @param array $files
 *   An array of file_ids to delete.
 * @return type
 */
function file_entity_multiple_delete_confirm_operation($fids) {
  // This function is a form generation function.
  $form = array();
  $form_state = array();
  // Set the submit handler explicitly because this form is being built
  // under a different form_id.
  $form['#submit'] = array();
  $form['#submit'][] = 'file_entity_multiple_delete_form_submit';
  $files = file_load_multiple($fids);
  return file_entity_multiple_delete_form($form, $form_state, $files);
}

/**
 * Helper function to get a list of hidden stream wrappers.
 *
 * This is used in several places to filter queries for media so that files in
 * temporary:// don't show up.
 */
function file_entity_get_hidden_stream_wrappers() {
  return array_diff_key(file_get_stream_wrappers(STREAM_WRAPPERS_ALL), file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE));
}

/**
 * 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_entity_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) {
  if (isset($wrappers['private'])) {
    $wrappers['private']['private'] = TRUE;
  }
  if (isset($wrappers['temporary'])) {
    $wrappers['temporary']['private'] = TRUE;
  }
}

/**
 * Implements hook_ctools_plugin_api().
 */
function file_entity_ctools_plugin_api($owner, $api) {
  if ($owner == 'file_entity' && $api == 'file_type') {
    return array('version' => 1);
  }
}

/**
 * @defgroup file_entity_access File access rights
 * @{
 * The file access system determines who can do what to which files.
 *
 * In determining access rights for a file, file_entity_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_entity_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_entity_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_entity_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_ENTITY_ACCESS_DENY from
 * hook_file_entity_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_ENTITY_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_entity_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_entity_access($op, $file = NULL, $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'];
  }

  if (!$file && $op == 'create') {
    return user_access('create files', $account) || user_access('bypass file access', $account);
  }

  // $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.
  $cache_id = 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][$cache_id][$op])) {
    return $rights[$account->uid][$cache_id][$op];
  }

  if (user_access('bypass file access', $account)) {
    return $rights[$account->uid][$cache_id][$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_entity_access', $op, $file, $account);
  if (in_array(FILE_ENTITY_ACCESS_DENY, $access, TRUE)) {
    return $rights[$account->uid][$cache_id][$op] = FALSE;
  }
  elseif (in_array(FILE_ENTITY_ACCESS_ALLOW, $access, TRUE)) {
    return $rights[$account->uid][$cache_id][$op] = TRUE;
  }


  // Fall back to default behaviors on view.
  if ($op == 'view' && is_object($file)) {
    $scheme = file_uri_scheme($file->uri);
    $wrapper = file_entity_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 (!empty($account->uid) && $file->uid == $account->uid && user_access('view own private files', $account)) {
        return $rights[$account->uid][$cache_id][$op] = TRUE;
      }
    }
    elseif ($file->status == FILE_STATUS_PERMANENT && $file->uid == $account->uid && user_access('view own files', $account)) {
      // For non-private files, allow to see if user owns the file.
      return $rights[$account->uid][$cache_id][$op] = TRUE;
    }
    elseif ($file->status == FILE_STATUS_PERMANENT && user_access('view files', $account)) {
      // For non-private files, users can view if they have the 'view files'
      // permission.
      return $rights[$account->uid][$cache_id][$op] = TRUE;
    }
  }

  return FALSE;
}

/**
 * Implements hook_file_entity_access().
 */
function file_entity_file_entity_access($op, $file, $account) {
  $grants = array();

  // If the file URI is invalid, deny access.
  if (is_object($file) && !file_valid_uri($file->uri)) {
    return FILE_ENTITY_ACCESS_DENY;
  }

  if ($op == 'create') {
    if (user_access('create files')) {
      return FILE_ENTITY_ACCESS_ALLOW;
    }
  }

  if ($op == 'update') {
    if (user_access('edit any files', $account) || (is_object($file) && user_access('edit own files', $account) && ($account->uid == $file->uid))) {
      return FILE_ENTITY_ACCESS_ALLOW;
    }
  }

  if ($op == 'delete') {
    if (user_access('delete any files', $account) || (is_object($file) && user_access('delete own files', $account) && ($account->uid == $file->uid))) {
      return FILE_ENTITY_ACCESS_ALLOW;
    }
  }

  if ($op == 'view' && is_object($file) && 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) {
        $access = module_invoke($module, 'file_download', $file->uri);
        if ($access === -1) {
          return FILE_ENTITY_ACCESS_DENY;
        }
        elseif (!empty($access)) {
          $grants[] = $access;
        }
      }
    }
  }

  return !empty($grants) ? FILE_ENTITY_ACCESS_ALLOW : FILE_ENTITY_ACCESS_IGNORE;
}

/**
 * @} End of "defgroup file_entity_access".
 *
 * Implements hook_file_default_types().
 */
function file_entity_file_default_types() {
  $types = array();

  // Image.
  $types['image'] = (object) array(
    'api_version' => 1,
    'type' => 'image',
    'label' => t('Image'),
    'description' => t('An <em>Image</em> file is a still visual.'),
    'mimetypes' => array(
      'image/jpeg',
      'image/gif',
      'image/png',
    ),
    'streams' => array(
      'public',
    ),
  );

  // Video.
  $types['video'] = (object) array(
    'api_version' => 1,
    'type' => 'video',
    'label' => t('Video'),
    'description' => t('A <em>Video</em> file is a moving visual recording.'),
    'mimetypes' => array(
      'video/quicktime',
      'video/mp4',
      'video/x-msvideo',
      'video/ogg',
    ),
    'streams' => array(
      'public',
    ),
  );

  // Audio.
  $types['audio'] = (object) array(
    'api_version' => 1,
    'type' => 'audio',
    'label' => t('Audio'),
    'description' => t('An <em>Audio</em> file is a sound recording.'),
    'mimetypes' => array(
      'audio/mpeg',
      'audio/x-ms-wma',
      'audio/x-wav',
      'audio/ogg',
    ),
    'streams' => array(
      'public',
    ),
  );

  // Document.
  $types['document'] = (object) array(
    'api_version' => 1,
    'type' => 'document',
    'label' => t('Document'),
    'description' => t('A <em>Document</em> file is written information.'),
    'mimetypes' => array(
      'text/plain',
      'application/msword',
      'application/vnd.ms-excel',
      'application/pdf',
      'application/vnd.ms-powerpoint',
      'application/vnd.oasis.opendocument.text',
      'application/vnd.oasis.opendocument.spreadsheet',
      'application/vnd.oasis.opendocument.presentation',
    ),
    'streams' => array(
      'public',
    ),
  );

  return $types;
}

/**
 * Clear the field cache for any entities referencing a specific file.
 *
 * @param object $file
 *   A file object.
 */
function file_entity_invalidate_field_caches($file) {
  $entity_types = &drupal_static(__FUNCTION__);

  // Gather the list of entity types which support field caching.
  if (!isset($entity_types)) {
    $entity_types = array();
    foreach (entity_get_info() as $entity_type => $entity_info) {
      if (!empty($entity_info['fieldable']) && !empty($entity_info['field cache'])) {
        $entity_types[] = $entity_type;
      }
    }
  }

  // If no entity types support field caching, then there is no work to be done.
  if (empty($entity_types)) {
    return;
  }

  $records = db_query("SELECT DISTINCT type, id FROM {file_usage} WHERE fid = :fid AND type IN (:types) AND id > 0", array(':fid' => $file->fid, ':types' => $entity_types))->fetchAll();
  if (!empty($records)) {
    $cids = array();
    foreach ($records as $record) {
      $cids[] = 'field:' . $record->type . ':' . $record->id;
    }
    cache_clear_all($cids, 'cache_field');
  }
}

/**
 * Check if a file entity is considered local or not.
 *
 * @param object $file
 *   A file entity object from file_load().
 *
 * @return
 *   TRUE if the file is using a local stream wrapper, or FALSE otherwise.
 */
function file_entity_file_is_local($file) {
  $scheme = file_uri_scheme($file->uri);
  $wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
  return !empty($wrappers[$scheme]) && empty($wrappers[$scheme]['remote']);
}

/**
 * Pre-render callback for adding validation descriptions to file upload fields.
 */
function file_entity_upload_validators_pre_render($element) {
  if (!empty($element['#upload_validators'])) {
    if (!isset($element['#description'])) {
      $element['#description'] = '';
    }
    if ($element['#description'] !== FALSE) {
      $element['#description'] = theme('file_upload_help', array('description' => $element['#description'], 'upload_validators' => $element['#upload_validators']));
    }
  }
  return $element;
}
