extended_file_field.module in Extended File Field 7
Extends the core File field widget and provides a new formatter.
File
extended_file_field.moduleView source
<?php
/**
* @file
* Extends the core File field widget and provides a new formatter.
*/
/**
* @defgroup extended_file_field_formatter Extended File Field Formatter
* @{
* Functions that implement the extended file table formatter.
*/
/**
* Defines the file metadata available for the extended_file_field formatter.
*
* @return array
* An array containing default file metadata as well as any metadata added by
* external modules. Each entry is an associative array, keyed by metadata
* type, with the following keys:
* - 'title': The human readable title describing this type.
* - 'sort': When this metadata type is selected as the sort criteria, this
* parameter determines whether to sort numerically ('sort' == 'numeric')
* or as a string (sort != 'numeric'). Setting 'sort' == FALSE prevents
* that particular field from appearing as an option in the 'sort by'
* field formatter settings field.
*/
function extended_file_field_metadata_types() {
$metadata =& drupal_static(__FUNCTION__);
if (!isset($metadata)) {
// Start with the default file field metadata.
$defaults = array(
'fid' => array(
'title' => t('File ID'),
'sort' => 'numeric',
),
'filename' => array(
'title' => t('File'),
'sort' => 'string',
),
'description' => array(
'title' => t('Description'),
'sort' => 'string',
),
'extension' => array(
'title' => t('Extension'),
'sort' => 'string',
),
'filesize' => array(
'title' => t('Size'),
'sort' => 'numeric',
),
'timestamp' => array(
'title' => t('Created'),
'sort' => 'numeric',
),
'uid' => array(
'title' => t('Author'),
'sort' => 'numeric',
),
'uri' => array(
'title' => t('URI'),
'sort' => 'string',
),
'filemime' => array(
'title' => t('Mime Type'),
'sort' => 'string',
),
'display' => array(
'title' => t('File Display'),
'sort' => FALSE,
),
'status' => array(
'title' => t('File Status'),
'sort' => FALSE,
),
);
// Allow modules to add their own file field metadata entries to the list.
$external = module_invoke_all('extended_file_field_metadata_types');
$metadata = array_merge($defaults, $external);
}
return $metadata;
}
/**
* Implements hook_menu().
*/
function extended_file_field_menu() {
$items = array();
$items['admin/extended-file-field/%node/%file/delete-contents'] = array(
'title' => 'Delete file contents',
'access callback' => 'extended_file_field_delete_contents_access',
'access arguments' => array(
3,
),
'page callback' => 'drupal_get_form',
'page arguments' => array(
'extended_file_field_delete_contents_confirm',
2,
3,
),
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Menu access callback.
*/
function extended_file_field_delete_contents_access($file) {
global $user;
return user_access('extended file field delete any file contents') || $user->uid && $file->uid == $user->uid && user_access('extended file field delete own file contents');
}
/**
* Form builder used as a menu callback.
*/
function extended_file_field_delete_contents_confirm($form, &$form_state, $node, $file) {
$dest = 'node/' . $node->nid . '/edit';
$form['#file'] = $file;
$form['#dest'] = $dest;
return confirm_form($form, 'Empty the contents of this file?', $dest, file_create_url($file->uri));
}
/*
* Form submit that empties file contents and replaces with text.
*/
function extended_file_field_delete_contents_confirm_submit($form, &$form_state) {
extended_file_field_delete_contents($form['#file']);
drupal_set_message(t('File %filename had its content deleted.', array(
'%filename' => $form['#file']->filename,
)));
$form_state['redirect'] = $form['#dest'];
}
function extended_file_field_delete_contents($file) {
$content = t("File content deleted on !date by user: !name\n", array(
'!date' => gmdate('c'),
'!name' => $GLOBALS['user']->name,
));
file_put_contents($file->uri, $content);
file_save($file);
}
/**
* Returns an array of available file metadata keys and titles.
*
* @return array
* An array of $type => $title metadata information representing the data
* from extended_file_field_metadata_types().
*/
function extended_file_field_metadata() {
$metadata =& drupal_static(__FUNCTION__);
if (!isset($metadata)) {
$metadata = array();
foreach (extended_file_field_metadata_types() as $type => $value) {
$metadata[$type] = $value['title'];
}
}
return $metadata;
}
/**
* Implements hook_field_formatter_info().
*
* Provides a formatter for file fields which renders a table of files with
* associated issue metadata.
*
* @return array
* An array of default values for the extended_file_field formatter
* configuration settings.
*/
function extended_file_field_field_formatter_info() {
return array(
'extended_file_field' => array(
'label' => t('Configurable table of files with metadata'),
'field types' => array(
'file',
),
'settings' => array(
// Restrict initial display to certain file types.
'extensionfilter' => 'all',
// Comma-separated list of extensions to display.
'extensions' => 'patch,diff',
// Unmatched extension behaviour
'extensionbehavior' => 'exclude',
// Metadata columns to display.
'columns' => array(
'filename',
'filesize',
'uid',
),
// Setting whether to include 'hidden' files in the display.
'showhidden' => 'exclude',
// Default property to use for sorting.
'sortby' => 'default',
// Default sort order ('asc' or 'desc').
'sortorder' => 'asc',
// Use the description as text for the filename, if present
'usedescriptionforfilename' => 1,
// Number of items to display
'restrictdisplay' => -1,
// Behaviour for items exceeding the configured number to display
'restrictbehavior' => 'exclude',
// Show a link for how to upload files?
'showuploadlink' => 0,
// Wrap second table in a collapsed fieldset.
'usefieldset' => FALSE,
),
),
);
}
/**
* Implements hook_field_formatter_settings_form().
*
* Specifies the form elements for the issue_files_summary_table formatter's
* configuration settings.
*
* @param $field
* The field structure being configured.
* @param $instance
* The instance structure being configured.
* @param $view_mode
* The view mode being configured.
* @param $form
* The (entire) configuration form array.
* @param $form_state
* The form state of the (entire) configuration form.
*
* @return array
* The form elements for the formatter's configuration settings.
*/
function extended_file_field_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
if ($instance['display'][$view_mode]['type'] == 'extended_file_field') {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$widget_settings = $field['settings'];
$metadata = extended_file_field_metadata();
$metadata_types = extended_file_field_metadata_types();
// "Columns to display" select box.
$form['columns'] = array(
'#type' => 'select',
'#title' => t('Display columns:'),
'#options' => $metadata,
'#default_value' => $settings['columns'],
'#size' => min(6, count($metadata)),
'#multiple' => TRUE,
);
// Sort settings.
// Add a default 'none' and a 'reverse' field order sort option.
$options = array(
'default' => t('Use the field item order (i.e. default sorting)'),
'reverse' => t('Reverse field order'),
);
foreach ($metadata_types as $data => $value) {
if (!empty($value['sort'])) {
$options[$data] = $metadata[$data];
}
}
$form['sortby'] = array(
'#type' => 'select',
'#title' => t('Sort by:'),
'#options' => $options,
'#default_value' => !empty($settings['sortby']) ? $settings['sortby'] : 'default',
);
$form['sortorder'] = array(
'#type' => 'radios',
'#title' => t('Sort order:'),
'#options' => array(
'asc' => t('Ascending'),
'desc' => t('Descending'),
),
'#default_value' => !empty($settings['sortorder']) ? $settings['sortorder'] : 'asc',
'#states' => array(
'invisible' => array(
array(
':input[name="fields[' . $field['field_name'] . '][settings_edit_form][settings][sortby]"]' => array(
'value' => 'default',
),
),
array(
':input[name="fields[' . $field['field_name'] . '][settings_edit_form][settings][sortby]"]' => array(
'value' => 'reverse',
),
),
),
),
);
$form['usedescriptionforfilename'] = array(
'#type' => 'checkbox',
'#title' => t('Use description as text for filename'),
'#default_value' => !empty($settings['usedescriptionforfilename']) ? $settings['usedescriptionforfilename'] : 1,
);
// Show a link to upload files.
if (extended_file_field_entity_type_supports_edit_link($instance['entity_type'])) {
$form['showuploadlink'] = array(
'#type' => 'checkbox',
'#title' => t('Show link to upload files'),
'#default_value' => $settings['showuploadlink'],
);
}
// Number of items to display
$options = array(
'-1' => t('Unlimited'),
) + array_combine(range(1, 10), range(1, 10));
$form['restrictdisplay'] = array(
'#type' => 'select',
'#title' => t('Number of items to display (per table)'),
'#options' => $options,
'#default_value' => !empty($settings['restrictdisplay']) ? $settings['restrictdisplay'] : '-1',
);
// Display behavior for files exceeding the number to display
$options = array(
'exclude' => t('Exclude from the table output'),
'hide' => t('Include in the table data but suppress display of the row'),
'toggle' => t('Include in the table and provide a javascript toggle to enable display'),
);
$form['restrictbehavior'] = array(
'#type' => 'radios',
'#title' => t('Display behavior for files not exceeding the "Number of items to display" count:'),
'#options' => $options,
'#default_value' => !empty($settings['restrictbehavior']) ? $settings['restrictbehavior'] : 'exclude',
'#states' => array(
'invisible' => array(
':input[name="fields[' . $field['field_name'] . '][settings_edit_form][settings][restrictdisplay]"]' => array(
'value' => '-1',
),
),
),
);
// File extension filter
$options = array(
'all' => t('Include all files'),
'filter' => t('Only include files matching certain file extensions'),
);
$form['extensionfilter'] = array(
'#type' => 'radios',
'#title' => t('File types to include:'),
'#options' => $options,
'#default_value' => !empty($settings['extensionfilter']) ? $settings['extensionfilter'] : 'all',
);
$form['extensions'] = array(
'#type' => 'textfield',
'#title' => t('Extension list:'),
'#description' => t('Comma separated list of file extensions to include.'),
'#default_value' => $settings['extensions'],
'#states' => array(
'invisible' => array(
':input[name="fields[' . $field['field_name'] . '][settings_edit_form][settings][extensionfilter]"]' => array(
'value' => 'all',
),
),
),
);
// Unmatched extension behaviour
$options = array(
'exclude' => t('Exclude from the table output'),
'hide' => t('Include in the table data but suppress display of the row'),
'table' => t('Display separately in a second table following the first'),
);
$form['extensionbehavior'] = array(
'#type' => 'radios',
'#title' => t('Display behavior for files not matching included extensions:'),
'#options' => $options,
'#default_value' => !empty($settings['extensionbehavior']) ? $settings['extensionbehavior'] : 'exclude',
'#states' => array(
'invisible' => array(
':input[name="fields[' . $field['field_name'] . '][settings_edit_form][settings][extensionfilter]"]' => array(
'value' => 'all',
),
),
),
);
// "Hidden" file treatment.
$options = array(
'exclude' => t('Exclude from the table output'),
'inline' => t('Display inline with other files'),
'hide' => t('Include in the table data but suppress display of the row'),
'table' => t('Display separately in a second table following the first'),
);
$form['showhidden'] = array(
'#type' => 'radios',
'#title' => t('Display behavior for files which have been marked with "display: FALSE":'),
'#options' => $options,
'#default_value' => !empty($settings['showhidden']) ? $settings['showhidden'] : 'exclude',
);
$form['usefieldset'] = array(
'#type' => 'checkbox',
'#title' => t('Enclose second table in a collapsed fieldset'),
'#default_value' => $settings['usefieldset'],
'#states' => array(
'visible' => array(
array(
':input[name="fields[' . $field['field_name'] . '][settings_edit_form][settings][extensionbehavior]"]' => array(
'value' => 'table',
),
),
array(
':input[name="fields[' . $field['field_name'] . '][settings_edit_form][settings][showhidden]"]' => array(
'value' => 'table',
),
),
),
),
);
}
return $form;
}
/**
* Implements hook_field_formatter_settings_summary().
*
* Contains the settings summary for the issue_files_summary_table formatter.
*
* @param array $field
* The field structure.
* @param array $instance
* The instance structure.
* @param string $view_mode
* The view mode for which the settings summary is being requested.
*
* @return string
* A string containing the short summary of the formatter settings.
*/
function extended_file_field_field_formatter_settings_summary($field, $instance, $view_mode) {
$summary = '';
if ($instance['display'][$view_mode]['type'] == 'extended_file_field') {
$metadata = extended_file_field_metadata();
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$summary = '';
if ($settings['extensionfilter'] === 'all') {
$summary = t('Listing all file types ');
}
else {
$summary = t('Listing files with extension (@extensions) ', array(
'@extensions' => $settings['extensions'],
));
}
if ($settings['sortby'] == 'reverse') {
$summary .= t('in reverse order');
}
elseif ($settings['sortby'] != 'default') {
$summary .= t('sorted by @sorttype ', array(
'@sorttype' => strtolower($metadata[$settings['sortby']]),
));
$summary .= $settings['sortorder'] === 'desc' ? t('descending') : t('ascending');
}
if ($settings['restrictdisplay'] != -1) {
$summary .= t(' (Limit @count)', array(
'@count' => $settings['restrictdisplay'],
));
}
$summary .= '<br />';
$summary .= t('Columns: @columns', array(
'@columns' => implode(',', array_intersect_key($metadata, array_flip($settings['columns']))),
));
}
return $summary;
}
/**
* Implements hook_entity_view_mode_alter().
*/
function extended_file_field_entity_view_mode_alter($entity_view_mode, $context) {
foreach (field_info_instances($context['entity_type'], entity_extract_ids($context['entity_type'], $context['entity'])[2]) as $instance) {
if ($instance['display']['default']['type'] === 'extended_file_field' && $instance['display']['default']['settings']['showhidden'] !== 'exclude') {
// Because core removes any files with display=FALSE from the $items
// array, we need to stash a full copy to display hidden files in the
// formatter. See https://drupal.org/node/993728 for fixing this in core.
$context['entity']->{'extended_file_field__' . $instance['field_name']} = $context['entity']->{$instance['field_name']};
}
}
}
/**
* Implements hook_field_formatter_prepare_view().
*
* @param $entity_type
* The type of $entity.
* @param $entities
* Array of entities being displayed, keyed by enitity ID.
* @param $field
* The field structure for the operation
* @param $instances
* Array of $field instance structures for each entity, keyed by entity_id.
* @param $langcode
* The language associated with $items.
* @param $items
* Array of file values for this field
* @param $displays
* Array of display settings to use for each entity, keyed by entity_id.
*/
function extended_file_field_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
if (!empty($field['settings']['display_field'])) {
$entity_ids = array();
foreach (array_keys($entities) as $id) {
// Any display formatter that is not configured to 'exclude' hidden files
// needs to have its entity reloaded.
if ($displays[$id]['settings']['showhidden'] != 'exclude') {
$entity_ids[] = $id;
}
}
if (!empty($entity_ids)) {
// Update the $items array
foreach ($entities as $entity_id => $entity) {
$items[$entity_id] = !empty($entity->{'extended_file_field__' . $field['field_name']}) ? $entity->{'extended_file_field__' . $field['field_name']}[$langcode] : array();
}
}
}
}
/**
* Implements hook_field_formatter_view().
*
* Generates the extended_file_field render array for the field's value. May
* also generate a second table for any 'hidden' files, optionally contained
* within a fieldset, depending on the value of $settings['showhidden'].
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity being displayed.
* @param $field
* The field structure.
* @param $instance
* The field instance.
* @param $langcode
* The language associated with $items.
* @param $items
* Array of file values for this field
* @param $display
* The display settings to use, as found in the 'display' entry of instance
* definitions. Contains keys of 'type' (name of the formatter to use) and
* 'settings' (the array of formatter settings).
*
* @return array
* A renderable array for the $items, as an array of child elements.
*/
function extended_file_field_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$metadata = extended_file_field_metadata();
$settings = $display['settings'];
$elements = array();
// Remove any NULL values from the $items array.
$items = array_filter($items);
if (!empty($items)) {
// Re-index array, keying by fid
$files = array();
foreach ($items as $item) {
$files[$item['fid']] = $item;
}
$items = $files;
unset($files);
// Populate the 'allowed extension types' array
$extension_filter = extended_file_field_extension_filter($settings);
// Add file extensions
extended_file_field_add_extensions($items);
// Apply "file extensions" treatment according to the formatter settings
if (!empty($extension_filter)) {
extended_file_field_filter_by_extension($items, $extension_filter, $settings['extensionbehavior']);
}
// Apply "display = 0" treatment according to the formatter settings
if (in_array($settings['showhidden'], array(
'exclude',
'hide',
'table',
))) {
extended_file_field_filter_by_display($items, $settings['showhidden']);
}
}
// Allow other modules to alter the $items array.
$context = array(
'field' => $field,
'instance' => $instance,
'entity' => $entity,
'entity_type' => $entity_type,
'langcode' => $langcode,
'display' => $display,
);
drupal_alter('extended_file_field_items', $items, $context);
if (!empty($items)) {
// Re-order the items array according to the desired sort order.
if ($settings['sortby'] == 'reverse') {
$items = array_reverse($items, TRUE);
}
elseif ($settings['sortby'] != 'default') {
extended_file_field_sort($items, $settings['sortby'], $settings['sortorder']);
}
// Apply "Number of items" treatment according to the formatter settings
if ($settings['restrictdisplay'] != '-1' && count($items) > $settings['restrictdisplay']) {
$counter1 = $counter2 = 0;
foreach ($items as $key => $item) {
$counter = empty($item['separate-table']) ? $counter1 : $counter2;
if ($counter < $settings['restrictdisplay'] && $item['display'] && empty($item['hide-extension']) && empty($item['hide-display'])) {
if (empty($item['separate-table'])) {
$counter1++;
}
else {
$counter2++;
}
}
elseif ($item['display'] == 1) {
if (!empty($settings['restrictbehavior']) && $settings['restrictbehavior'] == 'exclude') {
unset($items[$key]);
}
else {
$items[$key]['hide-count'] = TRUE;
}
}
}
}
}
$upload_link = array();
// Even though we'll want this to be the last thing in our $elements, since
// we'll bail early if there are no files, start by rendering the link to
// upload more files if we're configured to display it. We'll stuff this
// into $elements at the appropriate moment later in this function.
// We don't render the link in preview mode, due to the hook_entity_access()
// call causing PHP notices within entity_metadata_no_hook_node_access() as
// identified in http://drupal.org/node/2125091.
if (!empty($settings['showuploadlink']) && empty($entity->in_preview) && extended_file_field_entity_access('update', $entity_type, $entity) && field_access('update', $field, $entity_type, $entity) && extended_file_field_entity_type_supports_edit_link($entity_type)) {
// @todo: This is evil. Entities should have to tell us their edit URI.
// This hack here only works on certain entity_types.
// @see https://drupal.org/node/1970360
$uri = entity_uri($entity_type, $entity);
$uri['path'] .= '/edit';
// @todo: Files aren't translatable, so I think it's safe to hard-code
// '-und' in this fragment, but it'd be nice to confirm that. Also, is
// there a cleaner way to get an appropriate ID for a field?
$uri['options'] = array(
'fragment' => 'edit-' . str_replace('_', '-', $field['field_name']) . '-und',
);
$upload_link = array(
'#theme' => 'extended_file_field_upload_link',
'#uri' => $uri,
'#text' => t('Upload new files'),
// @todo: Maybe the theme function doesn't need any of this.
'#entity_type' => $entity_type,
'#entity' => $entity,
'#field' => $field,
);
}
// If the $items array is empty, bail now.
if (empty($items)) {
return $upload_link;
}
// Assemble the table header.
$header = extended_file_field_header($settings['columns']);
// Assemble the table rows. Returns rows sorted into an array with two keys,
// 'visible' and 'hidden'.
$rows = extended_file_field_generate_rows($items, $context);
// Generate the file formatter table
if (!empty($rows['visible'])) {
$table_id = check_plain("extended-file-field-table-" . str_replace('_', '-', $field['field_name']));
$table = extended_file_field_generate_table($header, $rows['visible'], $table_id);
// Add the javascript toggle if necessary
if (!empty($settings['restrictbehavior']) && $settings['restrictbehavior'] == 'toggle') {
$table['#attached']['js'][] = drupal_get_path('module', 'extended_file_field') . '/extended_file_field.formatter.js';
}
$elements[] = $table;
}
// If a second table for 'hidden' files is required, generate it
if (!empty($rows['hidden'])) {
$table_id = check_plain("extended-file-field-table-" . str_replace('_', '-', $field['field_name']) . "-hidden");
$table = extended_file_field_generate_table($header, $rows['hidden'], $table_id);
if ($settings['usefieldset'] == TRUE) {
// Wrap the table in a collapsed fieldset if desired
$num_hidden_files = count($rows['hidden']);
$elements[] = array(
'fieldset' => array(
'#type' => 'fieldset',
'#title' => format_plural($num_hidden_files, '1 more file', '@count more files'),
'#attributes' => array(
'class' => array(
'collapsible',
'collapsed',
),
),
'content' => array(
$table,
),
),
'#attached' => array(
'js' => array(
'misc/collapse.js',
'misc/form.js',
),
),
);
}
else {
// Create a field header and add the 'hidden' file table to $elements.
$elements[] = array(
'#markup' => t('Filtered files'),
);
$elements[] = $table;
}
}
// Add the upload link if it exists.
if (!empty($upload_link)) {
$elements[] = $upload_link;
}
// Add our $items array to the context variable
$context = array(
'items' => $items,
) + $context;
// Allow other modules to alter the table render arrays.
drupal_alter('extended_file_field_output', $elements, $context);
return $elements;
}
/**
* Returns HTML for a link to upload files to a file field.
*
* @param $variables
* An associative array containing:
* - element: A render element representing the link, with the following:
* - #uri: An array of information about the link to use
* - #text: The translated and HTML-safe text to use for the link.
* - #entity_type: The type of entity.
* - #entity: The entity this field is attached to.
* - #field: The file field to render the link for.
*
* @ingroup themeable
*/
function theme_extended_file_field_upload_link($variables) {
$text = $variables['element']['#text'];
$uri = $variables['element']['#uri'];
$url = url($uri['path'], $uri['options']);
return '<a class="upload-button" href="' . $url . '">' . $text . '</a>';
}
/**
* Does the given entity type have a known edit link we can use?
*
* @param string $entity_type
* The type of entity.
*
* @return bool
* TRUE if the entity type appends '/edit' to the view URI for the edit page.
*
* @todo It's stupid we have to do this at all, core should provide the edit
* URI as part of what's returned by entity_get_info().
*
* @see https://drupal.org/node/1970360
*/
function extended_file_field_entity_type_supports_edit_link($entity_type) {
return in_array($entity_type, array(
'node',
'user',
'comment',
'taxonomy_term',
));
}
/**
* Check access for an entity.
*
* Duplicated from Entity API since we don't want a dependency on that whole
* module just for this.
*
* @see entity_access()
*/
function extended_file_field_entity_access($op, $entity_type, $entity = NULL, $account = NULL) {
if (($info = entity_get_info()) && isset($info[$entity_type]['access callback'])) {
return $info[$entity_type]['access callback']($op, $entity, $account, $entity_type);
}
}
/**
* Generate an array of file extensions to filter against.
*
* @param $settings
* An array of settings associated with this field formatter instance.
* Relevant keys are 'extensionfilter', which determines the filter action,
* and 'extensions', which is a comma separated list of file extensions
*
* @return array
* An array of file extensions to be used in filtering, or array() if no
* extensions were defined
*/
function extended_file_field_extension_filter($settings) {
if ($settings['extensionfilter'] == 'filter') {
$filter = array_filter(array_map('trim', explode(',', $settings['extensions'])));
}
return isset($filter) ? $filter : array();
}
/**
* Adds a new 'extension' key to each file item, containing the file extension.
*
* @param $items
* The array of file items, passed by reference
*/
function extended_file_field_add_extensions(&$items) {
foreach ($items as $key => $item) {
$items[$key]['extension'] = pathinfo($item['filename'], PATHINFO_EXTENSION);
}
}
/**
* Filters the file $items array based on each item's 'extension' property.
*
* Depending on the formatter configuration, either removes items who's file
* extension does not match the provided filter criteria, or tags the item with
* a 'hide-extension' flag if they are to be included in the resulting table
* but hidden via display:none.
*
* @param array $items
* An array of file items, passed by reference.
* @param array $filter
* Extensions to use in filtering.
* @param string $action
* The action to apply for items not matching the filter, 'filter' or 'hide'
*
*/
function extended_file_field_filter_by_extension(&$items, $filter, $action = 'filter') {
foreach ($items as $key => $item) {
if (!in_array($item['extension'], $filter)) {
switch ($action) {
case 'exclude':
unset($items[$key]);
break;
case 'hide':
$items[$key]['hide-display'] = TRUE;
break;
case 'table':
$items[$key]['separate-table'] = TRUE;
break;
}
}
}
}
/**
* Filters the file $items array based on each item's 'display' property.
*
* Acts on any files with display = FALSE. Depending on the formatter
* configuration, these items are either removed from the file array or tagged
* with a flag indicating how it should be handled during display processing.
* These flags include 'hide-display' if hidden files should be included in
* the table but hidden via display:none; or 'separate-table' if the files
* should be displayed in a separate table.
*
* @param array $items
* An array of file items, passed by reference.
* @param string $action
* The action to apply against matched files, 'exclude', 'hide', or 'table'.
* The 'inline' action bypasses this function call.
*/
function extended_file_field_filter_by_display(&$items, $action = 'exclude') {
foreach ($items as $key => $item) {
if (empty($item['display'])) {
switch ($action) {
case 'exclude':
unset($items[$key]);
break;
case 'hide':
$items[$key]['hide-display'] = TRUE;
break;
case 'table':
$items[$key]['separate-table'] = TRUE;
break;
}
}
}
}
/**
* Sorts the field formatter's items array.
*/
function extended_file_field_sort(&$items, $on, $order = 'ASC') {
$metadata = extended_file_field_metadata_types();
if (!empty($metadata[$on]['sort']) && $metadata[$on]['sort'] == 'numeric') {
if ($order == 'asc') {
$function = function ($a, $b) use ($on) {
return $a[$on] - $b[$on];
};
}
else {
$function = function ($a, $b) use ($on) {
return $b[$on] - $a[$on];
};
}
}
elseif ($order == 'asc') {
$function = function ($a, $b) use ($on) {
return strcmp($a[$on], $b[$on]);
};
}
else {
$function = function ($a, $b) use ($on) {
return -strcmp($a[$on], $b[$on]);
};
}
uasort($items, $function);
}
/**
* Generate a table header based on the desired display columns.
*
* @param array $columns
* The array of columns desired in the table, as per $settings['columns'].
*
* @return array
* The generated table header array.
*/
function extended_file_field_header($columns) {
$metadata = extended_file_field_metadata();
$header = array();
foreach ($columns as $key => $column) {
$header[$column] = array(
'data' => $metadata[$column],
'name' => 'extended-file-field-table-header-' . $column,
'class' => array(
'extended-file-field-table-header',
),
);
}
return $header;
}
/**
* Generates individual table rows for each file item
*
* @param array $items
* An array of file items.
* @param array $context
* Associative array of contextual information which is passed down through
* each of the table generation functions (rows, row, & celldata), so that it
* can be made available to custom data formatters defined by external
* modules (via hook invocation in extended_file_field_generate_celldata()).
* Contains the following keys:
* - field: The field definition array.
* - instance: The field instance definition array.
* - entity: An object representing the entity the file field is attached to.
* - entity_type: String with the type of entity the field is attached to.
* - langcode: The language associated with $items.
* - display: The display settings to use, as found in the 'display' entry of
* the instance definition. Notable keys include the name of the formatter
* (in 'type') and the array of formatter settings (in 'settings')
*
* @return array
* A nested array of rows to be included in each table within the field
* formatter file listings, keyed by file display type ('visible' and
* 'hidden').
*
* @see extended_file_field_generate_celldata()
*/
function extended_file_field_generate_rows($items, $context) {
$rows = array(
'visible' => array(),
'hidden' => array(),
);
foreach ($items as $item) {
$row = [
'data' => [],
'class' => [
'extended-file-field-table-row',
],
'name' => 'issue-file-summary-table-row-' . $item['fid'],
];
foreach ($context['display']['settings']['columns'] as $column) {
$row['data'][$column] = [
'data' => extended_file_field_celldata($column, $item, $context),
'name' => 'extended-file-field-table-' . $column . '-' . $item['fid'],
'class' => 'extended-file-field-table-' . $column,
];
}
// Enforce the file's 'hidden field' handling.
// Display property.
if (!empty($item['hide-display'])) {
$row['class'][] = 'hidden-property';
}
// File extension filter.
if (!empty($item['hide-extension'])) {
$row['class'][] = 'hidden-extension';
}
// Limit to 'n' items.
if (!empty($item['hide-count'])) {
$row['class'][] = 'hidden-count';
}
// Add the row to the appropriate $rows array.
$rows[empty($item['separate-table']) ? 'visible' : 'hidden'][$item['fid']] = $row;
}
return $rows;
}
/**
* Generate the value for a given cell based on the column and $item displayed.
*
* @param string $column
* The name of the column (i.e. $items property key) being generated.
* @param array $item
* The file being rendered within this row.
* @param array $context
* Associative array of context for the table of files being altered, with
* the following keys:
* - field: The field definition array.
* - instance: The field instance definition array.
* - entity: The entity object the file field is attached to.
* - entity_type: String with the type of entity the field is attached to.
* - langcode: The language associated with $item.
* - display: The display settings to use, as found in the 'display' entry of
* the instance definition. Notable keys include the name of the formatter
* (in 'type') and the array of formatter settings (in 'settings').
*
* @return
* The render array or HTML value for inclusion in this cell.
*/
function extended_file_field_celldata($column, $item, $context) {
$metadata = extended_file_field_metadata_types();
if (!empty($metadata[$column]['formatter']) && function_exists($metadata[$column]['formatter'])) {
$data = $metadata[$column]['formatter']($item, $context);
}
else {
switch ($column) {
case 'filename':
// If description is set, the theme function will use it by default
if ($context['display']['settings']['usedescriptionforfilename'] == 0) {
// We want to use the actual filename for the filename column
unset($item['description']);
}
$data = theme('file_link', array(
'file' => (object) $item,
));
break;
case 'filesize':
$data = format_size($item['filesize']);
break;
case 'uid':
$data = theme('username', array(
'account' => user_load($item['uid']),
));
break;
case 'timestamp':
$data = format_date($item['timestamp'], 'short');
break;
default:
$data = check_plain($item[$column]);
break;
}
}
return $data;
}
/**
* Generate the final table output for a file listing table.
*
* @param array $header
* An array defining the table header.
* @param array $rows
* An array of $rows information for this table.
* @param string $table_id
* The (CSS) id for this table.
*
* @return array
* The render array for this table.
*/
function extended_file_field_generate_table($header, $rows, $table_id) {
$table = array(
'#theme' => 'table',
'#sticky' => FALSE,
'#header' => $header,
'#rows' => $rows,
'#attributes' => array(
'id' => $table_id,
),
);
return $table;
}
/**
* @} End of "defgroup extended_file_field_formatter".
*/
/**
* @defgroup extended_file_field_widget_enhancements File widget enhancements
* @{
* Functions that implement the file upload/edit widget enhancements.
*/
/**
* Implements hook_permission().
*/
function extended_file_field_permission() {
return array(
'extended file field delete any file contents' => array(
'title' => t('Delete contents of any file attachment'),
'description' => t('Allows users to delete the content of any file even if the file widget is configured to hide the "Remove" button.'),
),
'extended file field delete own file contents' => array(
'title' => t('Delete contents of own file attachment'),
'description' => t('Allows users to delete the content of a file they uploaded even if the file widget is configured to hide the "Remove" button.'),
),
);
}
/**
* Implements hook_theme().
*/
function extended_file_field_theme() {
return array(
'extended_file_field_widget_multiple' => array(
'render element' => 'element',
),
'extended_file_field_upload_link' => array(
'render element' => 'element',
),
);
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Modifies the field_ui_field_edit_form for the file_generic widget, to add a
* setting allowing users to disable access to the 'remove' button on file
* listings within the edit form.
*/
function extended_file_field_form_field_ui_field_edit_form_alter(&$form, &$form_state, $form_id) {
// Only operate on the file_generic widget settings form
if ($form['#instance']['widget']['type'] == 'file_generic') {
// Obtain the widget settings
$settings = $form['#instance']['widget']['settings'];
// Add a checkbox which can be used to toggle visibility of the 'Remove'
// button next to files on the widget.
$form['instance']['widget']['settings']['extended_file_field_show_remove'] = array(
'#type' => 'checkbox',
'#title' => t('Enable <em>Remove</em> button'),
'#description' => t('Note that a custom <em>Remove</em> link will be visible for users with the appropriate permission.'),
// We can't use empty() here since the default is to show and if a field
// hasn't resaved its widget settings, we won't have a value.
'#default_value' => isset($settings['extended_file_field_show_remove']) ? $settings['extended_file_field_show_remove'] : TRUE,
'#weight' => 12,
);
$form['instance']['widget']['settings']['extended_file_field_show_clear_contents'] = array(
'#type' => 'checkbox',
'#title' => t('Enable <em>Clear Contents</em> link'),
'#description' => t('Note that a custom <em>Clear Contents</em> link will be visible for users with the appropriate permission.'),
// We can't use empty() here since the default is to show and if a field
// hasn't resaved its widget settings, we won't have a value.
'#default_value' => isset($settings['extended_file_field_show_clear_contents']) ? $settings['extended_file_field_show_clear_contents'] : FALSE,
'#weight' => 13,
);
// Add a checkbox for displaying the widget file listing in reverse order
$form['instance']['widget']['settings']['extended_file_field_reverse_display'] = array(
'#type' => 'checkbox',
'#title' => t('Reverse display order'),
'#description' => t('Reverses the order of files displayed in the widget, with the upload widget at the top, and most recent files listed first.'),
'#default_value' => !empty($settings['extended_file_field_reverse_display']),
'#weight' => 14,
);
// Add a checkbox for restricting the number of files displayed in the
// widget. If checked, this will use the formatter setting to determine
// how many files to display.
$form['instance']['widget']['settings']['extended_file_field_restrict_display'] = array(
'#type' => 'checkbox',
'#title' => t('Restrict number of files displayed'),
'#description' => t('If checked, this will restrict the number of files displayed in the widget, according to the field formatter "Number of files to display" setting. Has no effect if the formatter is set to "Unlimited".'),
'#default_value' => !empty($settings['extended_file_field_restrict_display']),
'#weight' => 15,
);
// Add a checkbox which can be used to toggle the JavaScript functionality
// for showing/hiding hidden files
$form['instance']['widget']['settings']['extended_file_field_hidden_toggle'] = array(
'#type' => 'checkbox',
'#title' => t('Enable <em>Show hidden files</em> JavaScript toggle'),
'#description' => t('This setting enables a javascript show/hide toggle when restricting display of files exceeding the "number to display" count, and/or when the field\'s "Enable <em>Display</em> field" option is enabled and the node contains non-displayed files.'),
'#default_value' => !empty($settings['extended_file_field_hidden_toggle']),
'#weight' => 16,
);
// Add a checkbox for making the widget fieldset collapsible
$form['instance']['widget']['settings']['extended_file_field_collapsible'] = array(
'#type' => 'checkbox',
'#title' => t('Make the container fieldset collapsible'),
'#default_value' => !empty($settings['extended_file_field_collapsible']),
'#weight' => 17,
);
// Add a multi-select box which users can use to select additional metadata
// to be displayed with each file in the file widget.
$metadata = extended_file_field_metadata();
// Remove options for the fields already displayed by the core widget
$metadata = array_diff_key($metadata, array_flip(array(
'filename',
'filesize',
'description',
'display',
'status',
)));
$default = !empty($settings['extended_file_field_widget_metadata']) ? $settings['extended_file_field_widget_metadata'] : array();
$form['instance']['widget']['settings']['extended_file_field_widget_metadata'] = array(
'#type' => 'select',
'#title' => t('Additional metadata'),
'#description' => t('Optionally select other columns to add to the table of files while editing this field.'),
'#options' => $metadata,
'#default_value' => $default,
'#size' => min(6, count($metadata)),
'#multiple' => TRUE,
'#weight' => 18,
);
}
}
/**
* Implements hook_field_widget_WIDGET_TYPE_form_alter().
*
* Alters the 'file_generic' widget form. Used to add the 'Show hidden files'
* JavaScript to the widget, and override the theme used to render the
* file_widget_multiple widget.
*/
function extended_file_field_field_widget_file_generic_form_alter(&$element, &$form_state, $context) {
if ($context['field']['cardinality'] != 1) {
// Retrieve the widget settings
$settings = $context['instance']['widget']['settings'];
// We override the #theme function for the widget, so that we may display our
// additional metadata columns as needed, as well as hide the operations
// column when it is empty.
$element['#theme'] = 'extended_file_field_widget_multiple';
// Add JavaScript for the 'Show hidden files' functionality if desired.
if ((!empty($context['field']['settings']['display_field']) || !empty($settings['extended_file_field_restrict_display'])) && !empty($settings['extended_file_field_hidden_toggle'])) {
$element['#attached']['js'][] = drupal_get_path('module', 'extended_file_field') . '/extended_file_field.widget.js';
}
// If the setting exists, make the fieldset collapsible.
if (!empty($settings['extended_file_field_collapsible'])) {
$element['#collapsible'] = TRUE;
$element['#collapsed'] = empty($form_state['rebuild']) && empty($form_state['has_file_element']) ? TRUE : FALSE;
$element['#process'][] = 'form_process_fieldset';
}
}
}
/**
* Implements hook_preprocess_TEMPLATE().
*
* Disables the 'remove' buttons from file listings on the edit page, for
* theme_extended_file_field_widget_multiple() calls.
*/
function extended_file_field_preprocess_extended_file_field_widget_multiple(&$variables) {
// Even without files, the upload widgets should be present; so there will
// always be a child element we can use to retrieve the entity_type and
// bundle values, from which we can obtain the widget settings.
$element = $variables['element'];
$field_name = $element['#field_name'];
$entity = $element[0]['#entity'];
$entity_type = $element[0]['#entity_type'];
$bundle = $element[0]['#bundle'];
$field_info = field_info_field($field_name);
$instance_info = field_info_instance($entity_type, $field_name, $bundle);
$widget_settings = $instance_info['widget']['settings'];
$display_settings = $instance_info['display']['default']['settings'];
// Disable the 'Remove' button if the user does not have sufficient
// permission to access it.
// Note: We can't use empty() here since the default is to show and if a
// field hasn't resaved its widget settings, we won't have a value.
if (isset($widget_settings['extended_file_field_show_remove']) && $widget_settings['extended_file_field_show_remove'] == FALSE) {
foreach (element_children($element) as $key => $file) {
if (!empty($element[$key]['#file']->status)) {
$variables['element'][$key]['remove_button']['#access'] = FALSE;
}
}
}
// Show a 'clear contents' link if the user has sufficient permission to access it.
if (!empty($widget_settings['extended_file_field_show_clear_contents'])) {
// Add our clear contents link
foreach (element_children($element) as $key => $file) {
foreach (element_children($element) as $key => $file) {
if (!empty($element[$key]['#file']->status) && extended_file_field_delete_contents_access($element[$key]['#file'])) {
$dest = 'admin/extended-file-field/' . $element[$key]['#entity']->nid . '/' . $element[$key]['#file']->fid . '/delete-contents';
$variables['element'][$key]['deletecontents_link'] = array(
'#markup' => l(t('Delete contents'), $dest),
);
}
}
}
}
// Add per-file metadata elements to the table output.
if (!empty($widget_settings['extended_file_field_widget_metadata'])) {
// Generate contextual information, which will be provided to third party
// module metadata formatters (as defined within the 'formatter' property
// of hook_extended_file_field_metadata_types()) via the alter() hook.
$context = array(
'field' => $field_info,
'instance' => $instance_info,
'entity' => $entity,
'entity_type' => $entity_type,
'langcode' => $element['#language'],
'display' => $instance_info['display'],
);
// Retrieve information regarding all available file metadata.
$metadata = extended_file_field_metadata();
// Retrieve the file elements.
foreach (element_children($element) as $key) {
if (!empty($element[$key]['#file']->status)) {
$fids[$key] = $element[$key]['#value']['fid'];
}
}
$files = !empty($fids) ? file_load_multiple($fids) : array();
$element_keys = !empty($fids) ? array_flip($fids) : array();
// Cast file objects into arrays for compatibility with the field formatter
// functions.
$items = array();
foreach ($files as $key => $file) {
$items[$file->fid] = (array) $file;
}
// Add file extensions to each file item.
extended_file_field_add_extensions($items);
// Allow other modules to add metadata to each file item.
drupal_alter('extended_file_field_widget_items', $items, $context);
// Generate the markup for the additional metadata for each file.
foreach ($widget_settings['extended_file_field_widget_metadata'] as $column) {
foreach ($items as $key => $item) {
$variables['element'][$element_keys[$item['fid']]]['extended_file_field_widget_metadata_' . $column] = array(
'#type' => 'markup',
'#markup' => extended_file_field_celldata($column, (array) $item, $context),
);
}
// Add metadata column information to the parent form element.
$variables['element']['#extended_file_field_widget_metadata'][$column] = $metadata[$column];
}
}
// Restrict the number of displayed elements if necessary
if (!empty($widget_settings['extended_file_field_restrict_display']) && $display_settings['restrictdisplay'] != '-1') {
$variables['element']['#extended_file_field_restrict_display'] = $display_settings['restrictdisplay'];
drupal_add_js(array(
'extendedFileField' => array(
'restrictDisplay' => $display_settings['restrictbehavior'],
),
), 'setting');
}
// Reverse the file display order if necessary
if (!empty($widget_settings['extended_file_field_reverse_display'])) {
$variables['element']['#extended_file_field_reverse_display'] = TRUE;
drupal_add_js(array(
'extendedFileField' => array(
'reverseDisplay' => TRUE,
),
), 'setting');
}
}
/**
* Returns HTML for a group of file upload widgets.
*
* Mostly duplicated from theme_file_widget_multiple() in core. Enhanced to
* properly handle the Operations column (hide it completely if there are no
* visible operations).
*
* @param $variables
* An associative array containing:
* - element: A render element representing the widgets.
*
* @see theme_file_widget_multiple()
* @ingroup themeable
*/
function theme_extended_file_field_widget_multiple($variables) {
$element = $variables['element'];
$extra_columns = isset($element['#extended_file_field_widget_metadata']) ? $element['#extended_file_field_widget_metadata'] : array();
$have_operations = FALSE;
// Special ID and classes for draggable tables.
$weight_class = $element['#id'] . '-weight';
$table_id = $element['#id'] . '-table';
// Build up a table of applicable fields.
$headers = array();
$headers[] = t('File information');
foreach ($extra_columns as $column) {
$headers[] = $column;
}
if (isset($element['#display_field']) && $element['#display_field']) {
$headers[] = array(
'data' => t('Display'),
'class' => array(
'checkbox',
),
);
}
$headers[] = t('Weight');
// Get our list of widgets in order (needed when the form comes back after
// preview or failed validation).
$widgets = array();
foreach (element_children($element) as $key) {
// Screen out any empty file placeholders, but keep the file upload widget.
if ($element[$key]['#file'] == FALSE && $key != $element['#file_upload_delta']) {
unset($element[$key]);
continue;
}
$widgets[] =& $element[$key];
}
usort($widgets, '_field_sort_items_value_helper');
if (!empty($element['#extended_file_field_reverse_display'])) {
$widgets = array_reverse($widgets);
}
$rows = array();
foreach ($widgets as $key => &$widget) {
// Save the uploading row for last.
if ($widget['#file'] == FALSE) {
$widget['#title'] = $element['#file_upload_title'];
$widget['#description'] = $element['#file_upload_description'];
$upload_widget =& $widget;
continue;
}
// Delay rendering of the buttons, so that they can be rendered later in
// the "operations" column.
$operations_elements = array();
foreach (element_children($widget) as $sub_key) {
if (isset($widget[$sub_key]['#type']) && $widget[$sub_key]['#type'] == 'submit' || $sub_key == 'deletecontents_link') {
hide($widget[$sub_key]);
$operations_elements[] =& $widget[$sub_key];
}
}
// Delay rendering of the "Display" option and the weight selector, so that
// each can be rendered later in its own column.
if (isset($element['#display_field']) && $element['#display_field']) {
hide($widget['display']);
}
hide($widget['_weight']);
// Delay rendering of any added metadata types, so that each can be
// rendered later in it's own column.
foreach ($extra_columns as $column => $title) {
hide($widget['extended_file_field_widget_metadata_' . $column]);
}
// Render everything else together in a column, without the normal wrappers.
$widget['#theme_wrappers'] = array();
$information = drupal_render($widget);
// Render the previously hidden elements, using render() instead of
// drupal_render(), to undo the earlier hide().
$operations = '';
foreach ($operations_elements as $operation_element) {
$operations .= render($operation_element);
}
// Remember if *any* rows have any operations.
if (!empty($operations)) {
$have_operations = TRUE;
}
$display = '';
if (isset($element['#display_field']) && $element['#display_field']) {
unset($widget['display']['#title']);
$display = array(
'data' => render($widget['display']),
'class' => array(
'checkbox',
),
);
}
$widget['_weight']['#attributes']['class'] = array(
$weight_class,
);
$weight = render($widget['_weight']);
// Arrange the row with all of the rendered columns.
$row = array();
$row[] = $information;
foreach ($extra_columns as $column => $title) {
$row[] = render($widget['extended_file_field_widget_metadata_' . $column]);
}
if (isset($element['#display_field']) && $element['#display_field']) {
$row[] = $display;
}
$row[] = $weight;
// Always add an operations table cell. We'll rip all these out later if
// we don't see *any* operations in any rows.
$row['operations'] = $operations;
$rows[] = array(
'data' => $row,
'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], array(
'draggable',
)) : array(
'draggable',
),
);
}
drupal_add_tabledrag($table_id, 'order', 'sibling', $weight_class);
// If we saw any rows with operations, add the header for that column.
if ($have_operations) {
$headers[] = t('Operations');
}
else {
foreach ($rows as $key => &$row) {
unset($row['data']['operations']);
}
}
// Handle 'restrict display' behavior according to the formatter settings.
// We only 'hide' extra files, even if the formatter is set to 'exclude',
// since unsetting here would cause us to lose those files upon saving.
$counter = 0;
if (!empty($element['#extended_file_field_restrict_display'])) {
foreach ($rows as $key => &$row) {
$counter++;
if ($counter <= $element['#extended_file_field_restrict_display']) {
continue;
}
else {
$row['class'] = array_merge($row['class'], array(
'element-hidden',
'hidden-count',
));
}
}
}
$output = '';
if (!empty($element['#extended_file_field_reverse_display'])) {
$output = drupal_render($upload_widget);
}
$output .= empty($rows) ? '' : theme('table', array(
'header' => $headers,
'rows' => $rows,
'attributes' => array(
'id' => $table_id,
),
));
$output .= drupal_render_children($element);
return $output;
}
/**
* @} End of "defgroup extended_file_field_widget_enhancements".
*/