Move all files to 2017/

This commit is contained in:
Oliver Davies 2025-09-29 22:25:17 +01:00
parent ac7370f67f
commit 2875863330
15717 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,6 @@
description:
type: 'textfield'
length: 128
icon:
directory: 'core/modules/file/icons'
make_unused_managed_files_temporary: false

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,165 @@
# Schema for the configuration files of the File module.
file.settings:
type: config_object
label: 'File settings'
mapping:
description:
type: mapping
label: 'Description'
mapping:
type:
type: string
label: 'Type'
length:
type: integer
label: 'Length'
icon:
type: mapping
label: 'Icon'
mapping:
directory:
type: path
label: 'Directory'
make_unused_managed_files_temporary:
type: boolean
label: 'Controls if unused files should be marked temporary'
field.storage_settings.file:
type: base_entity_reference_field_settings
label: 'File settings'
mapping:
display_field:
type: boolean
label: 'Enable Display field'
display_default:
type: boolean
label: 'Files displayed by default'
uri_scheme:
type: string
label: 'Upload destination'
field.value.file:
type: mapping
label: 'Default value'
base_file_field_field_settings:
type: mapping
mapping:
handler:
type: string
label: 'Reference method'
handler_settings:
type: entity_reference_selection.[%parent.handler]
label: 'File selection handler settings'
file_directory:
type: string
label: 'File directory'
file_extensions:
type: string
label: 'Allowed file extensions'
max_filesize:
type: string
label: 'Maximum upload size'
field.field_settings.file:
type: base_file_field_field_settings
label: 'File settings'
mapping:
description_field:
type: boolean
label: 'Enable Description field'
file.formatter.media:
type: mapping
label: 'Media display format settings'
mapping:
controls:
type: boolean
label: 'Show playback controls'
autoplay:
type: boolean
label: 'Autoplay'
loop:
type: boolean
label: 'Loop'
multiple_file_display_type:
type: string
label: 'Display of multiple files'
field.formatter.settings.file_audio:
type: file.formatter.media
label: 'Audio file display format settings'
field.formatter.settings.file_video:
type: file.formatter.media
label: 'Video file display format settings'
mapping:
muted:
type: boolean
label: 'Muted'
width:
type: integer
label: 'Width'
height:
type: integer
label: 'Height'
field.formatter.settings.file_default:
type: mapping
label: 'Generic file format settings'
mapping:
use_description_as_link_text:
type: boolean
label: 'Replace the file name by its description when available'
field.formatter.settings.file_rss_enclosure:
type: mapping
label: 'RSS enclosure format settings'
field.formatter.settings.file_table:
type: field.formatter.settings.file_default
label: 'Table of files format settings'
field.formatter.settings.file_url_plain:
type: mapping
label: 'URL to file format settings'
field.widget.settings.file_generic:
type: mapping
label: 'File format settings'
mapping:
progress_indicator:
type: string
label: 'Progress indicator'
field_formatter_settings_base_file:
type: mapping
mapping:
link_to_file:
type: boolean
label: 'Link to file'
field.formatter.settings.file_link:
type: field_formatter_settings_base_file
field.formatter.settings.file_uri:
type: field_formatter_settings_base_file
mapping:
file_download_path:
type: boolean
label: 'Display download path'
field.formatter.settings.file_filemime:
type: field_formatter_settings_base_file
mapping:
filemime_image:
type: boolean
label: 'Display the filemime as icon'
field.formatter.settings.file_extension:
type: field_formatter_settings_base_file
mapping:
extension_detect_tar:
type: boolean
label: 'Detect tar'

View file

@ -0,0 +1,42 @@
# Schema for the views plugins of the File module.
views.argument.file_fid:
type: views.argument.numeric
label: 'File ID'
views.field.file_extension:
type: views_field
label: 'File extension'
mapping:
extension_detect_tar:
type: boolean
label: 'Detect if tar is part of the extension'
views.field.file:
type: views_field
label: 'File'
mapping:
link_to_file:
type: boolean
label: 'Link this field to download the file'
views.field.file_filemime:
type: views.field.file
label: 'File MIME'
mapping:
filemime_image:
type: boolean
label: 'Display an icon representing the file type, instead of the MIME text (such as "image/jpeg")'
views.field.file_uri:
type: views.field.file
label: 'File URI'
mapping:
file_download_path:
type: boolean
label: 'Display download path instead of file storage URI'
views.filter.file_status:
type: views.filter.in_operator
label: 'File status'

View file

@ -0,0 +1,82 @@
<?php
/**
* @file
* Hooks for file module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Check that files meet a given criteria.
*
* This hook lets modules perform additional validation on files. They're able
* to report a failure by returning one or more error messages.
*
* @param \Drupal\file\FileInterface $file
* The file entity being validated.
* @return array
* An array of error messages. If there are no problems with the file return
* an empty array.
*
* @see file_validate()
*/
function hook_file_validate(Drupal\file\FileInterface $file) {
$errors = [];
if (!$file->getFilename()) {
$errors[] = t("The file's name is empty. Please give a name to the file.");
}
if (strlen($file->getFilename()) > 255) {
$errors[] = t("The file's name exceeds the 255 characters limit. Please rename the file and try again.");
}
return $errors;
}
/**
* Respond to a file that has been copied.
*
* @param \Drupal\file\FileInterface $file
* The newly copied file entity.
* @param \Drupal\file\FileInterface $source
* The original file before the copy.
*
* @see file_copy()
*/
function hook_file_copy(Drupal\file\FileInterface $file, Drupal\file\FileInterface $source) {
// Make sure that the file name starts with the owner's user name.
if (strpos($file->getFilename(), $file->getOwner()->name) !== 0) {
$file->setFilename($file->getOwner()->name . '_' . $file->getFilename());
$file->save();
\Drupal::logger('file')->notice('Copied file %source has been renamed to %destination', ['%source' => $source->filename, '%destination' => $file->getFilename()]);
}
}
/**
* Respond to a file that has been moved.
*
* @param \Drupal\file\FileInterface $file
* The updated file entity after the move.
* @param \Drupal\file\FileInterface $source
* The original file entity before the move.
*
* @see file_move()
*/
function hook_file_move(Drupal\file\FileInterface $file, Drupal\file\FileInterface $source) {
// Make sure that the file name starts with the owner's user name.
if (strpos($file->getFilename(), $file->getOwner()->name) !== 0) {
$file->setFilename($file->getOwner()->name . '_' . $file->getFilename());
$file->save();
\Drupal::logger('file')->notice('Moved file %source has been renamed to %destination', ['%source' => $source->filename, '%destination' => $file->getFilename()]);
}
}
/**
* @} End of "addtogroup hooks".
*/

View file

@ -0,0 +1,303 @@
/**
* @file
* Provides JavaScript additions to the managed file field type.
*
* This file provides progress bar support (if available), popup windows for
* file previews, and disabling of other file fields during Ajax uploads (which
* prevents separate file fields from accidentally uploading files).
*/
(function($, Drupal) {
/**
* Attach behaviors to the file fields passed in the settings.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches validation for file extensions.
* @prop {Drupal~behaviorDetach} detach
* Detaches validation for file extensions.
*/
Drupal.behaviors.fileValidateAutoAttach = {
attach(context, settings) {
const $context = $(context);
let elements;
function initFileValidation(selector) {
$context
.find(selector)
.once('fileValidate')
.on(
'change.fileValidate',
{ extensions: elements[selector] },
Drupal.file.validateExtension,
);
}
if (settings.file && settings.file.elements) {
elements = settings.file.elements;
Object.keys(elements).forEach(initFileValidation);
}
},
detach(context, settings, trigger) {
const $context = $(context);
let elements;
function removeFileValidation(selector) {
$context
.find(selector)
.removeOnce('fileValidate')
.off('change.fileValidate', Drupal.file.validateExtension);
}
if (trigger === 'unload' && settings.file && settings.file.elements) {
elements = settings.file.elements;
Object.keys(elements).forEach(removeFileValidation);
}
},
};
/**
* Attach behaviors to file element auto upload.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches triggers for the upload button.
* @prop {Drupal~behaviorDetach} detach
* Detaches auto file upload trigger.
*/
Drupal.behaviors.fileAutoUpload = {
attach(context) {
$(context)
.find('input[type="file"]')
.once('auto-file-upload')
.on('change.autoFileUpload', Drupal.file.triggerUploadButton);
},
detach(context, settings, trigger) {
if (trigger === 'unload') {
$(context)
.find('input[type="file"]')
.removeOnce('auto-file-upload')
.off('.autoFileUpload');
}
},
};
/**
* Attach behaviors to the file upload and remove buttons.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches form submit events.
* @prop {Drupal~behaviorDetach} detach
* Detaches form submit events.
*/
Drupal.behaviors.fileButtons = {
attach(context) {
const $context = $(context);
$context
.find('.js-form-submit')
.on('mousedown', Drupal.file.disableFields);
$context
.find('.js-form-managed-file .js-form-submit')
.on('mousedown', Drupal.file.progressBar);
},
detach(context, settings, trigger) {
if (trigger === 'unload') {
const $context = $(context);
$context
.find('.js-form-submit')
.off('mousedown', Drupal.file.disableFields);
$context
.find('.js-form-managed-file .js-form-submit')
.off('mousedown', Drupal.file.progressBar);
}
},
};
/**
* Attach behaviors to links within managed file elements for preview windows.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches triggers.
* @prop {Drupal~behaviorDetach} detach
* Detaches triggers.
*/
Drupal.behaviors.filePreviewLinks = {
attach(context) {
$(context)
.find('div.js-form-managed-file .file a')
.on('click', Drupal.file.openInNewWindow);
},
detach(context) {
$(context)
.find('div.js-form-managed-file .file a')
.off('click', Drupal.file.openInNewWindow);
},
};
/**
* File upload utility functions.
*
* @namespace
*/
Drupal.file = Drupal.file || {
/**
* Client-side file input validation of file extensions.
*
* @name Drupal.file.validateExtension
*
* @param {jQuery.Event} event
* The event triggered. For example `change.fileValidate`.
*/
validateExtension(event) {
event.preventDefault();
// Remove any previous errors.
$('.file-upload-js-error').remove();
// Add client side validation for the input[type=file].
const extensionPattern = event.data.extensions.replace(/,\s*/g, '|');
if (extensionPattern.length > 1 && this.value.length > 0) {
const acceptableMatch = new RegExp(`\\.(${extensionPattern})$`, 'gi');
if (!acceptableMatch.test(this.value)) {
const error = Drupal.t(
'The selected file %filename cannot be uploaded. Only files with the following extensions are allowed: %extensions.',
{
// According to the specifications of HTML5, a file upload control
// should not reveal the real local path to the file that a user
// has selected. Some web browsers implement this restriction by
// replacing the local path with "C:\fakepath\", which can cause
// confusion by leaving the user thinking perhaps Drupal could not
// find the file because it messed up the file path. To avoid this
// confusion, therefore, we strip out the bogus fakepath string.
'%filename': this.value.replace('C:\\fakepath\\', ''),
'%extensions': extensionPattern.replace(/\|/g, ', '),
},
);
$(this)
.closest('div.js-form-managed-file')
.prepend(
`<div class="messages messages--error file-upload-js-error" aria-live="polite">${error}</div>`,
);
this.value = '';
// Cancel all other change event handlers.
event.stopImmediatePropagation();
}
}
},
/**
* Trigger the upload_button mouse event to auto-upload as a managed file.
*
* @name Drupal.file.triggerUploadButton
*
* @param {jQuery.Event} event
* The event triggered. For example `change.autoFileUpload`.
*/
triggerUploadButton(event) {
$(event.target)
.closest('.js-form-managed-file')
.find('.js-form-submit')
.trigger('mousedown');
},
/**
* Prevent file uploads when using buttons not intended to upload.
*
* @name Drupal.file.disableFields
*
* @param {jQuery.Event} event
* The event triggered, most likely a `mousedown` event.
*/
disableFields(event) {
const $clickedButton = $(this);
$clickedButton.trigger('formUpdated');
// Check if we're working with an "Upload" button.
let $enabledFields = [];
if ($clickedButton.closest('div.js-form-managed-file').length > 0) {
$enabledFields = $clickedButton
.closest('div.js-form-managed-file')
.find('input.js-form-file');
}
// Temporarily disable upload fields other than the one we're currently
// working with. Filter out fields that are already disabled so that they
// do not get enabled when we re-enable these fields at the end of
// behavior processing. Re-enable in a setTimeout set to a relatively
// short amount of time (1 second). All the other mousedown handlers
// (like Drupal's Ajax behaviors) are executed before any timeout
// functions are called, so we don't have to worry about the fields being
// re-enabled too soon. @todo If the previous sentence is true, why not
// set the timeout to 0?
const $fieldsToTemporarilyDisable = $(
'div.js-form-managed-file input.js-form-file',
)
.not($enabledFields)
.not(':disabled');
$fieldsToTemporarilyDisable.prop('disabled', true);
setTimeout(() => {
$fieldsToTemporarilyDisable.prop('disabled', false);
}, 1000);
},
/**
* Add progress bar support if possible.
*
* @name Drupal.file.progressBar
*
* @param {jQuery.Event} event
* The event triggered, most likely a `mousedown` event.
*/
progressBar(event) {
const $clickedButton = $(this);
const $progressId = $clickedButton
.closest('div.js-form-managed-file')
.find('input.file-progress');
if ($progressId.length) {
const originalName = $progressId.attr('name');
// Replace the name with the required identifier.
$progressId.attr(
'name',
originalName.match(/APC_UPLOAD_PROGRESS|UPLOAD_IDENTIFIER/)[0],
);
// Restore the original name after the upload begins.
setTimeout(() => {
$progressId.attr('name', originalName);
}, 1000);
}
// Show the progress bar if the upload takes longer than half a second.
setTimeout(() => {
$clickedButton
.closest('div.js-form-managed-file')
.find('div.ajax-progress-bar')
.slideDown();
}, 500);
$clickedButton.trigger('fileUpload');
},
/**
* Open links to files within forms in a new window.
*
* @name Drupal.file.openInNewWindow
*
* @param {jQuery.Event} event
* The event triggered, most likely a `click` event.
*/
openInNewWindow(event) {
event.preventDefault();
$(this).attr('target', '_blank');
window.open(
this.href,
'filePreview',
'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=500,height=550',
);
},
};
})(jQuery, Drupal);

View file

@ -0,0 +1,209 @@
<?php
/**
* @file
* Field module functionality for the File module.
*/
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Render\Element;
/**
* Prepares variables for multi file form widget templates.
*
* Default template: file-widget-multiple.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: A render element representing the widgets.
*/
function template_preprocess_file_widget_multiple(&$variables) {
$element = $variables['element'];
// 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 = [];
$headers[] = t('File information');
if ($element['#display_field']) {
$headers[] = [
'data' => t('Display'),
'class' => ['checkbox'],
];
}
$headers[] = t('Weight');
$headers[] = t('Operations');
// Get our list of widgets in order (needed when the form comes back after
// preview or failed validation).
$widgets = [];
foreach (Element::children($element) as $key) {
$widgets[] = &$element[$key];
}
usort($widgets, '_field_multiple_value_form_sort_helper');
$rows = [];
foreach ($widgets as $key => &$widget) {
// Save the uploading row for last.
if (empty($widget['#files'])) {
$widget['#title'] = $element['#file_upload_title'];
$widget['#description'] = \Drupal::service('renderer')->renderPlain($element['#file_upload_description']);
continue;
}
// Delay rendering of the buttons, so that they can be rendered later in the
// "operations" column.
$operations_elements = [];
foreach (Element::children($widget) as $sub_key) {
if (isset($widget[$sub_key]['#type']) && $widget[$sub_key]['#type'] == 'submit') {
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 ($element['#display_field']) {
hide($widget['display']);
}
hide($widget['_weight']);
// Render everything else together in a column, without the normal wrappers.
$widget['#theme_wrappers'] = [];
$information = \Drupal::service('renderer')->render($widget);
$display = '';
if ($element['#display_field']) {
unset($widget['display']['#title']);
$display = [
'data' => render($widget['display']),
'class' => ['checkbox'],
];
}
$widget['_weight']['#attributes']['class'] = [$weight_class];
$weight = render($widget['_weight']);
// Arrange the row with all of the rendered columns.
$row = [];
$row[] = $information;
if ($element['#display_field']) {
$row[] = $display;
}
$row[] = $weight;
// Show the buttons that had previously been marked as hidden in this
// preprocess function. We use show() to undo the earlier hide().
foreach (Element::children($operations_elements) as $key) {
show($operations_elements[$key]);
}
$row[] = [
'data' => $operations_elements,
];
$rows[] = [
'data' => $row,
'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], ['draggable']) : ['draggable'],
];
}
$variables['table'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#attributes' => [
'id' => $table_id,
],
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => $weight_class,
],
],
'#access' => !empty($rows),
];
$variables['element'] = $element;
}
/**
* Prepares variables for file upload help text templates.
*
* Default template: file-upload-help.html.twig.
*
* @param array $variables
* An associative array containing:
* - description: The normal description for this field, specified by the
* user.
* - upload_validators: An array of upload validators as used in
* $element['#upload_validators'].
*/
function template_preprocess_file_upload_help(&$variables) {
$description = $variables['description'];
$upload_validators = $variables['upload_validators'];
$cardinality = $variables['cardinality'];
$descriptions = [];
if (!empty($description)) {
$descriptions[] = FieldFilteredMarkup::create($description);
}
if (isset($cardinality)) {
if ($cardinality == -1) {
$descriptions[] = t('Unlimited number of files can be uploaded to this field.');
}
else {
$descriptions[] = \Drupal::translation()->formatPlural($cardinality, 'One file only.', 'Maximum @count files.');
}
}
if (isset($upload_validators['file_validate_size'])) {
$descriptions[] = t('@size limit.', ['@size' => format_size($upload_validators['file_validate_size'][0])]);
}
if (isset($upload_validators['file_validate_extensions'])) {
$descriptions[] = t('Allowed types: @extensions.', ['@extensions' => $upload_validators['file_validate_extensions'][0]]);
}
if (isset($upload_validators['file_validate_image_resolution'])) {
$max = $upload_validators['file_validate_image_resolution'][0];
$min = $upload_validators['file_validate_image_resolution'][1];
if ($min && $max && $min == $max) {
$descriptions[] = t('Images must be exactly <strong>@size</strong> pixels.', ['@size' => $max]);
}
elseif ($min && $max) {
$descriptions[] = t('Images must be larger than <strong>@min</strong> pixels. Images larger than <strong>@max</strong> pixels will be resized.', ['@min' => $min, '@max' => $max]);
}
elseif ($min) {
$descriptions[] = t('Images must be larger than <strong>@min</strong> pixels.', ['@min' => $min]);
}
elseif ($max) {
$descriptions[] = t('Images larger than <strong>@max</strong> pixels will be resized.', ['@max' => $max]);
}
}
$variables['descriptions'] = $descriptions;
}
/**
* Determine whether a field references files stored in {file_managed}.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field
* A field definition.
*
* @return bool
* The field column if the field references {file_managed}.fid, typically
* fid, FALSE if it does not.
*/
function file_field_find_file_reference_column(FieldDefinitionInterface $field) {
$schema = $field->getFieldStorageDefinition()->getSchema();
foreach ($schema['foreign keys'] as $data) {
if ($data['table'] == 'file_managed') {
foreach ($data['columns'] as $field_column => $column) {
if ($column == 'fid') {
return $field_column;
}
}
}
}
return FALSE;
}

View file

@ -0,0 +1,8 @@
name: File
type: module
description: 'Defines a file field type.'
package: Field types
version: VERSION
core: 8.x
dependencies:
- drupal:field

View file

@ -0,0 +1,166 @@
<?php
/**
* @file
* Install, update and uninstall functions for File module.
*/
use Drupal\Core\Entity\Entity\EntityViewDisplay;
/**
* Implements hook_schema().
*/
function file_schema() {
$schema['file_usage'] = [
'description' => 'Track where a file is used.',
'fields' => [
'fid' => [
'description' => 'File ID.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
],
'module' => [
'description' => 'The name of the module that is using the file.',
'type' => 'varchar_ascii',
'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
'not null' => TRUE,
'default' => '',
],
'type' => [
'description' => 'The name of the object type in which the file is used.',
'type' => 'varchar_ascii',
'length' => 64,
'not null' => TRUE,
'default' => '',
],
'id' => [
'description' => 'The primary key of the object using the file.',
'type' => 'varchar_ascii',
'length' => 64,
'not null' => TRUE,
'default' => 0,
],
'count' => [
'description' => 'The number of times this file is used by this object.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
],
'primary key' => ['fid', 'type', 'id', 'module'],
'indexes' => [
'type_id' => ['type', 'id'],
'fid_count' => ['fid', 'count'],
'fid_module' => ['fid', 'module'],
],
];
return $schema;
}
/**
* Implements hook_requirements().
*
* Display information about getting upload progress bars working.
*/
function file_requirements($phase) {
$requirements = [];
// Check the server's ability to indicate upload progress.
if ($phase == 'runtime') {
$description = NULL;
$implementation = file_progress_implementation();
$server_software = \Drupal::request()->server->get('SERVER_SOFTWARE');
// Test the web server identity.
if (preg_match("/Nginx/i", $server_software)) {
$is_nginx = TRUE;
$is_apache = FALSE;
$fastcgi = FALSE;
}
elseif (preg_match("/Apache/i", $server_software)) {
$is_nginx = FALSE;
$is_apache = TRUE;
$fastcgi = strpos($server_software, 'mod_fastcgi') !== FALSE || strpos($server_software, 'mod_fcgi') !== FALSE;
}
else {
$is_nginx = FALSE;
$is_apache = FALSE;
$fastcgi = FALSE;
}
if (!$is_apache && !$is_nginx) {
$value = t('Not enabled');
$description = t('Your server is not capable of displaying file upload progress. File upload progress requires an Apache server running PHP with mod_php or Nginx with PHP-FPM.');
}
elseif ($fastcgi) {
$value = t('Not enabled');
$description = t('Your server is not capable of displaying file upload progress. File upload progress requires PHP be run with mod_php or PHP-FPM and not as FastCGI.');
}
elseif (!$implementation) {
$value = t('Not enabled');
$description = t('Your server is capable of displaying file upload progress, but does not have the required libraries. It is recommended to install the <a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress library</a>.');
}
elseif ($implementation == 'apc') {
$value = t('Enabled (<a href="http://php.net/manual/apcu.configuration.php#ini.apcu.rfc1867">APC RFC1867</a>)');
$description = t('Your server is capable of displaying file upload progress using APC RFC1867. Note that only one upload at a time is supported. It is recommended to use the <a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress library</a> if possible.');
}
elseif ($implementation == 'uploadprogress') {
$value = t('Enabled (<a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress</a>)');
}
$requirements['file_progress'] = [
'title' => t('Upload progress'),
'value' => $value,
'description' => $description,
];
}
return $requirements;
}
/**
* Prevent unused files from being deleted.
*/
function file_update_8300() {
// Disable deletion of unused permanent files.
\Drupal::configFactory()->getEditable('file.settings')
->set('make_unused_managed_files_temporary', FALSE)
->save();
return t('Files that have no remaining usages are no longer deleted by default.');
}
/**
* Add 'use_description_as_link_text' setting to file field formatters.
*/
function file_update_8001() {
$displays = EntityViewDisplay::loadMultiple();
foreach ($displays as $display) {
/** @var \Drupal\Core\Entity\Entity\EntityViewDisplay $display */
$fields_settings = $display->get('content');
$changed = FALSE;
foreach ($fields_settings as $field_name => $settings) {
if (!empty($settings['type'])) {
switch ($settings['type']) {
// The file_table formatter never displayed available descriptions
// before, so we disable this option to ensure backward compatibility.
case 'file_table':
$fields_settings[$field_name]['settings']['use_description_as_link_text'] = FALSE;
$changed = TRUE;
break;
// The file_default formatter always displayed available descriptions
// before, so we enable this option to ensure backward compatibility.
case 'file_default':
$fields_settings[$field_name]['settings']['use_description_as_link_text'] = TRUE;
$changed = TRUE;
break;
}
}
}
if ($changed === TRUE) {
$display->set('content', $fields_settings)->save();
}
}
}

View file

@ -0,0 +1,136 @@
/**
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(function ($, Drupal) {
Drupal.behaviors.fileValidateAutoAttach = {
attach: function attach(context, settings) {
var $context = $(context);
var elements = void 0;
function initFileValidation(selector) {
$context.find(selector).once('fileValidate').on('change.fileValidate', { extensions: elements[selector] }, Drupal.file.validateExtension);
}
if (settings.file && settings.file.elements) {
elements = settings.file.elements;
Object.keys(elements).forEach(initFileValidation);
}
},
detach: function detach(context, settings, trigger) {
var $context = $(context);
var elements = void 0;
function removeFileValidation(selector) {
$context.find(selector).removeOnce('fileValidate').off('change.fileValidate', Drupal.file.validateExtension);
}
if (trigger === 'unload' && settings.file && settings.file.elements) {
elements = settings.file.elements;
Object.keys(elements).forEach(removeFileValidation);
}
}
};
Drupal.behaviors.fileAutoUpload = {
attach: function attach(context) {
$(context).find('input[type="file"]').once('auto-file-upload').on('change.autoFileUpload', Drupal.file.triggerUploadButton);
},
detach: function detach(context, settings, trigger) {
if (trigger === 'unload') {
$(context).find('input[type="file"]').removeOnce('auto-file-upload').off('.autoFileUpload');
}
}
};
Drupal.behaviors.fileButtons = {
attach: function attach(context) {
var $context = $(context);
$context.find('.js-form-submit').on('mousedown', Drupal.file.disableFields);
$context.find('.js-form-managed-file .js-form-submit').on('mousedown', Drupal.file.progressBar);
},
detach: function detach(context, settings, trigger) {
if (trigger === 'unload') {
var $context = $(context);
$context.find('.js-form-submit').off('mousedown', Drupal.file.disableFields);
$context.find('.js-form-managed-file .js-form-submit').off('mousedown', Drupal.file.progressBar);
}
}
};
Drupal.behaviors.filePreviewLinks = {
attach: function attach(context) {
$(context).find('div.js-form-managed-file .file a').on('click', Drupal.file.openInNewWindow);
},
detach: function detach(context) {
$(context).find('div.js-form-managed-file .file a').off('click', Drupal.file.openInNewWindow);
}
};
Drupal.file = Drupal.file || {
validateExtension: function validateExtension(event) {
event.preventDefault();
$('.file-upload-js-error').remove();
var extensionPattern = event.data.extensions.replace(/,\s*/g, '|');
if (extensionPattern.length > 1 && this.value.length > 0) {
var acceptableMatch = new RegExp('\\.(' + extensionPattern + ')$', 'gi');
if (!acceptableMatch.test(this.value)) {
var error = Drupal.t('The selected file %filename cannot be uploaded. Only files with the following extensions are allowed: %extensions.', {
'%filename': this.value.replace('C:\\fakepath\\', ''),
'%extensions': extensionPattern.replace(/\|/g, ', ')
});
$(this).closest('div.js-form-managed-file').prepend('<div class="messages messages--error file-upload-js-error" aria-live="polite">' + error + '</div>');
this.value = '';
event.stopImmediatePropagation();
}
}
},
triggerUploadButton: function triggerUploadButton(event) {
$(event.target).closest('.js-form-managed-file').find('.js-form-submit').trigger('mousedown');
},
disableFields: function disableFields(event) {
var $clickedButton = $(this);
$clickedButton.trigger('formUpdated');
var $enabledFields = [];
if ($clickedButton.closest('div.js-form-managed-file').length > 0) {
$enabledFields = $clickedButton.closest('div.js-form-managed-file').find('input.js-form-file');
}
var $fieldsToTemporarilyDisable = $('div.js-form-managed-file input.js-form-file').not($enabledFields).not(':disabled');
$fieldsToTemporarilyDisable.prop('disabled', true);
setTimeout(function () {
$fieldsToTemporarilyDisable.prop('disabled', false);
}, 1000);
},
progressBar: function progressBar(event) {
var $clickedButton = $(this);
var $progressId = $clickedButton.closest('div.js-form-managed-file').find('input.file-progress');
if ($progressId.length) {
var originalName = $progressId.attr('name');
$progressId.attr('name', originalName.match(/APC_UPLOAD_PROGRESS|UPLOAD_IDENTIFIER/)[0]);
setTimeout(function () {
$progressId.attr('name', originalName);
}, 1000);
}
setTimeout(function () {
$clickedButton.closest('div.js-form-managed-file').find('div.ajax-progress-bar').slideDown();
}, 500);
$clickedButton.trigger('fileUpload');
},
openInNewWindow: function openInNewWindow(event) {
event.preventDefault();
$(this).attr('target', '_blank');
window.open(this.href, 'filePreview', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=500,height=550');
}
};
})(jQuery, Drupal);

View file

@ -0,0 +1,9 @@
drupal.file:
version: VERSION
js:
file.js: {}
dependencies:
- core/jquery
- core/jquery.once
- core/drupal
- core/drupalSettings

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
access files overview:
title: 'Access the Files overview page'

View file

@ -0,0 +1,6 @@
file.ajax_progress:
path: '/file/progress/{key}'
defaults:
_controller: '\Drupal\file\Controller\FileWidgetAjaxController::progress'
requirements:
_permission: 'access content'

View file

@ -0,0 +1,6 @@
services:
file.usage:
class: Drupal\file\FileUsage\DatabaseFileUsageBackend
arguments: ['@database', 'file_usage', '@config.factory']
tags:
- { name: backend_overridable }

View file

@ -0,0 +1,70 @@
<?php
/**
* @file
* Provide views data for file.module.
*/
use Drupal\field\FieldStorageConfigInterface;
/**
* Implements hook_field_views_data().
*
* Views integration for file fields. Adds a file relationship to the default
* field data.
*
* @see views_field_default_views_data()
*/
function file_field_views_data(FieldStorageConfigInterface $field_storage) {
$data = views_field_default_views_data($field_storage);
foreach ($data as $table_name => $table_data) {
// Add the relationship only on the fid field.
$data[$table_name][$field_storage->getName() . '_target_id']['relationship'] = [
'id' => 'standard',
'base' => 'file_managed',
'entity type' => 'file',
'base field' => 'fid',
'label' => t('file from @field_name', ['@field_name' => $field_storage->getName()]),
];
}
return $data;
}
/**
* Implements hook_field_views_data_views_data_alter().
*
* Views integration to provide reverse relationships on file fields.
*/
function file_field_views_data_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
$entity_type_id = $field_storage->getTargetEntityTypeId();
$entity_manager = \Drupal::entityManager();
$entity_type = $entity_manager->getDefinition($entity_type_id);
$field_name = $field_storage->getName();
$pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping();
list($label) = views_entity_field_label($entity_type_id, $field_name);
$data['file_managed'][$pseudo_field_name]['relationship'] = [
'title' => t('@entity using @field', ['@entity' => $entity_type->getLabel(), '@field' => $label]),
'label' => t('@field_name', ['@field_name' => $field_name]),
'group' => $entity_type->getLabel(),
'help' => t('Relate each @entity with a @field set to the file.', ['@entity' => $entity_type->getLabel(), '@field' => $label]),
'id' => 'entity_reverse',
'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
'entity_type' => $entity_type_id,
'base field' => $entity_type->getKey('id'),
'field_name' => $field_name,
'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
'field field' => $field_name . '_target_id',
'join_extra' => [
0 => [
'field' => 'deleted',
'value' => 0,
'numeric' => TRUE,
],
],
];
}

View file

@ -0,0 +1,52 @@
# Every migration that references a file by Drupal 6 fid should specify this
# migration as an optional dependency.
id: d6_file
label: Public files
audit: true
migration_tags:
- Drupal 6
- Content
source:
plugin: d6_file
constants:
# The tool configuring this migration must set source_base_path. It
# represents the fully qualified path relative to which URIs in the files
# table are specified, and must end with a /. See source_full_path
# configuration in this migration's process pipeline as an example.
source_base_path: ''
process:
# If you are using both this migration and d6_user_picture_file in a custom
# migration and executing migrations incrementally, it is strongly
# recommended that you remove the fid mapping to avoid potential ID
# conflicts. For that reason, this mapping is commented out by default.
# fid: fid
filename: filename
source_full_path:
-
plugin: concat
delimiter: /
source:
- constants/source_base_path
- filepath
-
plugin: urlencode
destination_full_path:
plugin: file_uri
source:
- filepath
- file_directory_path
- temp_directory_path
- is_public
uri:
plugin: file_copy
source:
- '@source_full_path'
- '@destination_full_path'
filemime: filemime
# No need to migrate filesize, it is computed when file entities are saved.
# filesize: filesize
status: status
changed: timestamp
uid: uid
destination:
plugin: entity:file

View file

@ -0,0 +1,32 @@
id: d6_upload
label: File uploads
migration_tags:
- Drupal 6
- Content
source:
plugin: d6_upload
process:
nid: nid
vid: vid
langcode:
plugin: user_langcode
source: language
fallback_to_site_default: true
type: type
upload:
plugin: sub_process
source: upload
process:
target_id:
plugin: migration_lookup
migration: d6_file
source: fid
display: list
description: description
destination:
plugin: entity:node
migration_dependencies:
required:
- d6_file
- d6_node
- d6_upload_field_instance

View file

@ -0,0 +1,35 @@
id: d6_upload_entity_display
label: Upload display configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_upload_instance
constants:
entity_type: node
view_mode: default
name: upload
type: file_default
options:
label: hidden
settings: {}
process:
entity_type: 'constants/entity_type'
bundle:
-
plugin: migration_lookup
migration: d6_node_type
source: node_type
-
plugin: skip_on_empty
method: row
view_mode: 'constants/view_mode'
field_name: 'constants/name'
type: 'constants/type'
options: 'constants/options'
'options/type': '@type'
destination:
plugin: component_entity_display
migration_dependencies:
required:
- d6_upload_field_instance

View file

@ -0,0 +1,36 @@
id: d6_upload_entity_form_display
label: Upload form display configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_upload_instance
constants:
empty: {}
entity_type: node
form_mode: default
name: upload
type: file_generic
options:
settings:
progress_indicator: throbber
process:
entity_type: 'constants/entity_type'
bundle:
-
plugin: migration_lookup
migration: d6_node_type
source: node_type
-
plugin: skip_on_empty
method: row
field_name: 'constants/name'
form_mode: 'constants/form_mode'
type: 'constants/type'
options: 'constants/options'
'options/type': '@type'
destination:
plugin: component_entity_form_display
migration_dependencies:
required:
- d6_upload_field_instance

View file

@ -0,0 +1,27 @@
id: d6_upload_field
label: Upload field configuration
migration_tags:
- Drupal 6
- Configuration
source:
# We do an empty source and a proper destination to have an idmap for
# migration_dependencies.
plugin: md_empty
source_module: upload
constants:
entity_type: node
type: file
name: upload
cardinality: -1
display_field: true
process:
entity_type: 'constants/entity_type'
field_name: 'constants/name'
type: 'constants/type'
cardinality: 'constants/cardinality'
'settings/display_field': 'constants/display_field'
destination:
plugin: entity:field_storage_config
dependencies:
module:
- file

View file

@ -0,0 +1,32 @@
id: d6_upload_field_instance
label: Upload field instance configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_upload_instance
constants:
entity_type: node
name: upload
settings:
description_field: 1
process:
entity_type: 'constants/entity_type'
bundle:
-
plugin: migration_lookup
migration: d6_node_type
source: node_type
-
plugin: skip_on_empty
method: row
field_name: 'constants/name'
settings: 'constants/settings'
'settings/file_extensions': file_extensions
'settings/max_filesize': max_filesize
destination:
plugin: entity:field_config
migration_dependencies:
required:
- d6_upload_field
- d6_node_type

View file

@ -0,0 +1,47 @@
# Every migration that references a file by Drupal 7 fid should specify this
# migration as an optional dependency.
id: d7_file
label: Public files
audit: true
migration_tags:
- Drupal 7
- Content
source:
plugin: d7_file
scheme: public
constants:
# The tool configuring this migration must set source_base_path. It
# represents the fully qualified path relative to which URIs in the files
# table are specified, and must end with a /. See source_full_path
# configuration in this migration's process pipeline as an example.
source_base_path: ''
process:
# If you are using this file to build a custom migration consider removing
# the fid field to allow incremental migrations.
fid: fid
filename: filename
source_full_path:
-
plugin: concat
delimiter: /
source:
- constants/source_base_path
- filepath
-
plugin: urlencode
uri:
plugin: file_copy
source:
- '@source_full_path'
- uri
filemime: filemime
# No need to migrate filesize, it is computed when file entities are saved.
# filesize: filesize
status: status
# Drupal 7 didn't keep track of the file's creation or update time -- all it
# had was the vague "timestamp" column. So we'll use it for both.
created: timestamp
changed: timestamp
uid: uid
destination:
plugin: entity:file

View file

@ -0,0 +1,41 @@
id: d7_file_private
label: Private files
audit: true
migration_tags:
- Drupal 7
- Content
source:
plugin: d7_file
scheme: private
constants:
# source_base_path must be set by the tool configuring this migration.
# It represents the fully qualified path relative to which uris in the files
# table are specified, and must end with a /. See source_full_path
# configuration in this migration's process pipeline as an example.
source_base_path: ''
process:
# If you are using this file to build a custom migration consider removing
# the fid field to allow incremental migrations.
fid: fid
filename: filename
source_full_path:
-
plugin: concat
delimiter: /
source:
- constants/source_base_path
- filepath
uri:
plugin: file_copy
source:
- '@source_full_path'
- uri
filemime: filemime
status: status
# Drupal 7 didn't keep track of the file's creation or update time -- all it
# had was the vague "timestamp" column. So we'll use it for both.
created: timestamp
changed: timestamp
uid: uid
destination:
plugin: entity:file

View file

@ -0,0 +1,20 @@
id: file_settings
label: File configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- file_description_type
- file_description_length
- file_icon_directory
source_module: system
process:
'description/type': file_description_type
'description/length': file_description_length
'icon/directory': file_icon_directory
destination:
plugin: config
config_name: file.settings

View file

@ -0,0 +1,47 @@
<?php
namespace Drupal\file;
use Drupal\Core\TypedData\TypedData;
/**
* Computed file URL property class.
*/
class ComputedFileUrl extends TypedData {
/**
* Computed root-relative file URL.
*
* @var string
*/
protected $url = NULL;
/**
* {@inheritdoc}
*/
public function getValue() {
if ($this->url !== NULL) {
return $this->url;
}
assert($this->getParent()->getEntity() instanceof FileInterface);
$uri = $this->getParent()->getEntity()->getFileUri();
$this->url = file_url_transform_relative(file_create_url($uri));
return $this->url;
}
/**
* {@inheritdoc}
*/
public function setValue($value, $notify = TRUE) {
$this->url = $value;
// Notify the parent of any changes.
if ($notify && isset($this->parent)) {
$this->parent->onChange($this->name);
}
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Drupal\file\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* Defines a controller to respond to file widget AJAX requests.
*/
class FileWidgetAjaxController {
/**
* Returns the progress status for a file upload process.
*
* @param string $key
* The unique key for this upload process.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JsonResponse object.
*/
public function progress($key) {
$progress = [
'message' => t('Starting upload...'),
'percentage' => -1,
];
$implementation = file_progress_implementation();
if ($implementation == 'uploadprogress') {
$status = uploadprogress_get_info($key);
if (isset($status['bytes_uploaded']) && !empty($status['bytes_total'])) {
$progress['message'] = t('Uploading... (@current of @total)', ['@current' => format_size($status['bytes_uploaded']), '@total' => format_size($status['bytes_total'])]);
$progress['percentage'] = round(100 * $status['bytes_uploaded'] / $status['bytes_total']);
}
}
elseif ($implementation == 'apc') {
$status = apcu_fetch('upload_' . $key);
if (isset($status['current']) && !empty($status['total'])) {
$progress['message'] = t('Uploading... (@current of @total)', ['@current' => format_size($status['current']), '@total' => format_size($status['total'])]);
$progress['percentage'] = round(100 * $status['current'] / $status['total']);
}
}
return new JsonResponse($progress);
}
}

View file

@ -0,0 +1,464 @@
<?php
namespace Drupal\file\Element;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\FormElement;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\file\Entity\File;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides an AJAX/progress aware widget for uploading and saving a file.
*
* @FormElement("managed_file")
*/
class ManagedFile extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#process' => [
[$class, 'processManagedFile'],
],
'#element_validate' => [
[$class, 'validateManagedFile'],
],
'#pre_render' => [
[$class, 'preRenderManagedFile'],
],
'#theme' => 'file_managed_file',
'#theme_wrappers' => ['form_element'],
'#progress_indicator' => 'throbber',
'#progress_message' => NULL,
'#upload_validators' => [],
'#upload_location' => NULL,
'#size' => 22,
'#multiple' => FALSE,
'#extended' => FALSE,
'#attached' => [
'library' => ['file/drupal.file'],
],
'#accept' => NULL,
];
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
// Find the current value of this field.
$fids = !empty($input['fids']) ? explode(' ', $input['fids']) : [];
foreach ($fids as $key => $fid) {
$fids[$key] = (int) $fid;
}
$force_default = FALSE;
// Process any input and save new uploads.
if ($input !== FALSE) {
$input['fids'] = $fids;
$return = $input;
// Uploads take priority over all other values.
if ($files = file_managed_file_save_upload($element, $form_state)) {
if ($element['#multiple']) {
$fids = array_merge($fids, array_keys($files));
}
else {
$fids = array_keys($files);
}
}
else {
// Check for #filefield_value_callback values.
// Because FAPI does not allow multiple #value_callback values like it
// does for #element_validate and #process, this fills the missing
// functionality to allow File fields to be extended through FAPI.
if (isset($element['#file_value_callbacks'])) {
foreach ($element['#file_value_callbacks'] as $callback) {
$callback($element, $input, $form_state);
}
}
// Load files if the FIDs have changed to confirm they exist.
if (!empty($input['fids'])) {
$fids = [];
foreach ($input['fids'] as $fid) {
if ($file = File::load($fid)) {
$fids[] = $file->id();
// Temporary files that belong to other users should never be
// allowed.
if ($file->isTemporary()) {
if ($file->getOwnerId() != \Drupal::currentUser()->id()) {
$force_default = TRUE;
break;
}
// Since file ownership can't be determined for anonymous users,
// they are not allowed to reuse temporary files at all. But
// they do need to be able to reuse their own files from earlier
// submissions of the same form, so to allow that, check for the
// token added by $this->processManagedFile().
elseif (\Drupal::currentUser()->isAnonymous()) {
$token = NestedArray::getValue($form_state->getUserInput(), array_merge($element['#parents'], ['file_' . $file->id(), 'fid_token']));
if ($token !== Crypt::hmacBase64('file-' . $file->id(), \Drupal::service('private_key')->get() . Settings::getHashSalt())) {
$force_default = TRUE;
break;
}
}
}
}
}
if ($force_default) {
$fids = [];
}
}
}
}
// If there is no input or if the default value was requested above, use the
// default value.
if ($input === FALSE || $force_default) {
if ($element['#extended']) {
$default_fids = isset($element['#default_value']['fids']) ? $element['#default_value']['fids'] : [];
$return = isset($element['#default_value']) ? $element['#default_value'] : ['fids' => []];
}
else {
$default_fids = isset($element['#default_value']) ? $element['#default_value'] : [];
$return = ['fids' => []];
}
// Confirm that the file exists when used as a default value.
if (!empty($default_fids)) {
$fids = [];
foreach ($default_fids as $fid) {
if ($file = File::load($fid)) {
$fids[] = $file->id();
}
}
}
}
$return['fids'] = $fids;
return $return;
}
/**
* #ajax callback for managed_file upload forms.
*
* This ajax callback takes care of the following things:
* - Ensures that broken requests due to too big files are caught.
* - Adds a class to the response to be able to highlight in the UI, that a
* new file got uploaded.
*
* @param array $form
* The build form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The ajax response of the ajax upload.
*/
public static function uploadAjaxCallback(&$form, FormStateInterface &$form_state, Request $request) {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$form_parents = explode('/', $request->query->get('element_parents'));
// Sanitize form parents before using them.
$form_parents = array_filter($form_parents, [Element::class, 'child']);
// Retrieve the element to be rendered.
$form = NestedArray::getValue($form, $form_parents);
// Add the special AJAX class if a new file was added.
$current_file_count = $form_state->get('file_upload_delta_initial');
if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
$form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
}
// Otherwise just add the new content class on a placeholder.
else {
$form['#suffix'] .= '<span class="ajax-new-content"></span>';
}
$status_messages = ['#type' => 'status_messages'];
$form['#prefix'] .= $renderer->renderRoot($status_messages);
$output = $renderer->renderRoot($form);
$response = new AjaxResponse();
$response->setAttachments($form['#attached']);
return $response->addCommand(new ReplaceCommand(NULL, $output));
}
/**
* Render API callback: Expands the managed_file element type.
*
* Expands the file type to include Upload and Remove buttons, as well as
* support for a default value.
*/
public static function processManagedFile(&$element, FormStateInterface $form_state, &$complete_form) {
// This is used sometimes so let's implode it just once.
$parents_prefix = implode('_', $element['#parents']);
$fids = isset($element['#value']['fids']) ? $element['#value']['fids'] : [];
// Set some default element properties.
$element['#progress_indicator'] = empty($element['#progress_indicator']) ? 'none' : $element['#progress_indicator'];
$element['#files'] = !empty($fids) ? File::loadMultiple($fids) : FALSE;
$element['#tree'] = TRUE;
// Generate a unique wrapper HTML ID.
$ajax_wrapper_id = Html::getUniqueId('ajax-wrapper');
$ajax_settings = [
'callback' => [get_called_class(), 'uploadAjaxCallback'],
'options' => [
'query' => [
'element_parents' => implode('/', $element['#array_parents']),
],
],
'wrapper' => $ajax_wrapper_id,
'effect' => 'fade',
'progress' => [
'type' => $element['#progress_indicator'],
'message' => $element['#progress_message'],
],
];
// Set up the buttons first since we need to check if they were clicked.
$element['upload_button'] = [
'#name' => $parents_prefix . '_upload_button',
'#type' => 'submit',
'#value' => t('Upload'),
'#attributes' => ['class' => ['js-hide']],
'#validate' => [],
'#submit' => ['file_managed_file_submit'],
'#limit_validation_errors' => [$element['#parents']],
'#ajax' => $ajax_settings,
'#weight' => -5,
];
// Force the progress indicator for the remove button to be either 'none' or
// 'throbber', even if the upload button is using something else.
$ajax_settings['progress']['type'] = ($element['#progress_indicator'] == 'none') ? 'none' : 'throbber';
$ajax_settings['progress']['message'] = NULL;
$ajax_settings['effect'] = 'none';
$element['remove_button'] = [
'#name' => $parents_prefix . '_remove_button',
'#type' => 'submit',
'#value' => $element['#multiple'] ? t('Remove selected') : t('Remove'),
'#validate' => [],
'#submit' => ['file_managed_file_submit'],
'#limit_validation_errors' => [$element['#parents']],
'#ajax' => $ajax_settings,
'#weight' => 1,
];
$element['fids'] = [
'#type' => 'hidden',
'#value' => $fids,
];
// Add progress bar support to the upload if possible.
if ($element['#progress_indicator'] == 'bar' && $implementation = file_progress_implementation()) {
$upload_progress_key = mt_rand();
if ($implementation == 'uploadprogress') {
$element['UPLOAD_IDENTIFIER'] = [
'#type' => 'hidden',
'#value' => $upload_progress_key,
'#attributes' => ['class' => ['file-progress']],
// Uploadprogress extension requires this field to be at the top of
// the form.
'#weight' => -20,
];
}
elseif ($implementation == 'apc') {
$element['APC_UPLOAD_PROGRESS'] = [
'#type' => 'hidden',
'#value' => $upload_progress_key,
'#attributes' => ['class' => ['file-progress']],
// Uploadprogress extension requires this field to be at the top of
// the form.
'#weight' => -20,
];
}
// Add the upload progress callback.
$element['upload_button']['#ajax']['progress']['url'] = Url::fromRoute('file.ajax_progress', ['key' => $upload_progress_key]);
// Set a custom submit event so we can modify the upload progress
// identifier element before the form gets submitted.
$element['upload_button']['#ajax']['event'] = 'fileUpload';
}
// The file upload field itself.
$element['upload'] = [
'#name' => 'files[' . $parents_prefix . ']',
'#type' => 'file',
'#title' => t('Choose a file'),
'#title_display' => 'invisible',
'#size' => $element['#size'],
'#multiple' => $element['#multiple'],
'#theme_wrappers' => [],
'#weight' => -10,
'#error_no_message' => TRUE,
];
if (!empty($element['#accept'])) {
$element['upload']['#attributes'] = ['accept' => $element['#accept']];
}
if (!empty($fids) && $element['#files']) {
foreach ($element['#files'] as $delta => $file) {
$file_link = [
'#theme' => 'file_link',
'#file' => $file,
];
if ($element['#multiple']) {
$element['file_' . $delta]['selected'] = [
'#type' => 'checkbox',
'#title' => \Drupal::service('renderer')->renderPlain($file_link),
];
}
else {
$element['file_' . $delta]['filename'] = $file_link + ['#weight' => -10];
}
// Anonymous users who have uploaded a temporary file need a
// non-session-based token added so $this->valueCallback() can check
// that they have permission to use this file on subsequent submissions
// of the same form (for example, after an Ajax upload or form
// validation error).
if ($file->isTemporary() && \Drupal::currentUser()->isAnonymous()) {
$element['file_' . $delta]['fid_token'] = [
'#type' => 'hidden',
'#value' => Crypt::hmacBase64('file-' . $delta, \Drupal::service('private_key')->get() . Settings::getHashSalt()),
];
}
}
}
// Add the extension list to the page as JavaScript settings.
if (isset($element['#upload_validators']['file_validate_extensions'][0])) {
$extension_list = implode(',', array_filter(explode(' ', $element['#upload_validators']['file_validate_extensions'][0])));
$element['upload']['#attached']['drupalSettings']['file']['elements']['#' . $element['#id']] = $extension_list;
}
// Let #id point to the file element, so the field label's 'for' corresponds
// with it.
$element['#id'] = &$element['upload']['#id'];
// Prefix and suffix used for Ajax replacement.
$element['#prefix'] = '<div id="' . $ajax_wrapper_id . '">';
$element['#suffix'] = '</div>';
return $element;
}
/**
* Render API callback: Hides display of the upload or remove controls.
*
* Upload controls are hidden when a file is already uploaded. Remove controls
* are hidden when there is no file attached. Controls are hidden here instead
* of in \Drupal\file\Element\ManagedFile::processManagedFile(), because
* #access for these buttons depends on the managed_file element's #value. See
* the documentation of \Drupal\Core\Form\FormBuilderInterface::doBuildForm()
* for more detailed information about the relationship between #process,
* #value, and #access.
*
* Because #access is set here, it affects display only and does not prevent
* JavaScript or other untrusted code from submitting the form as though
* access were enabled. The form processing functions for these elements
* should not assume that the buttons can't be "clicked" just because they are
* not displayed.
*
* @see \Drupal\file\Element\ManagedFile::processManagedFile()
* @see \Drupal\Core\Form\FormBuilderInterface::doBuildForm()
*/
public static function preRenderManagedFile($element) {
// If we already have a file, we don't want to show the upload controls.
if (!empty($element['#value']['fids'])) {
if (!$element['#multiple']) {
$element['upload']['#access'] = FALSE;
$element['upload_button']['#access'] = FALSE;
}
}
// If we don't already have a file, there is nothing to remove.
else {
$element['remove_button']['#access'] = FALSE;
}
return $element;
}
/**
* Render API callback: Validates the managed_file element.
*/
public static function validateManagedFile(&$element, FormStateInterface $form_state, &$complete_form) {
$clicked_button = end($form_state->getTriggeringElement()['#parents']);
if ($clicked_button != 'remove_button' && !empty($element['fids']['#value'])) {
$fids = $element['fids']['#value'];
foreach ($fids as $fid) {
if ($file = File::load($fid)) {
// If referencing an existing file, only allow if there are existing
// references. This prevents unmanaged files from being deleted if
// this item were to be deleted. When files that are no longer in use
// are automatically marked as temporary (now disabled by default),
// it is not safe to reference a permanent file without usage. Adding
// a usage and then later on removing it again would delete the file,
// but it is unknown if and where it is currently referenced. However,
// when files are not marked temporary (and then removed)
// automatically, it is safe to add and remove usages, as it would
// simply return to the current state.
// @see https://www.drupal.org/node/2891902
if ($file->isPermanent() && \Drupal::config('file.settings')->get('make_unused_managed_files_temporary')) {
$references = static::fileUsage()->listUsage($file);
if (empty($references)) {
// We expect the field name placeholder value to be wrapped in t()
// here, so it won't be escaped again as it's already marked safe.
$form_state->setError($element, t('The file used in the @name field may not be referenced.', ['@name' => $element['#title']]));
}
}
}
else {
// We expect the field name placeholder value to be wrapped in t()
// here, so it won't be escaped again as it's already marked safe.
$form_state->setError($element, t('The file referenced by the @name field does not exist.', ['@name' => $element['#title']]));
}
}
}
// Check required property based on the FID.
if ($element['#required'] && empty($element['fids']['#value']) && !in_array($clicked_button, ['upload_button', 'remove_button'])) {
// We expect the field name placeholder value to be wrapped in t()
// here, so it won't be escaped again as it's already marked safe.
$form_state->setError($element, t('@name field is required.', ['@name' => $element['#title']]));
}
// Consolidate the array value of this field to array of FIDs.
if (!$element['#extended']) {
$form_state->setValueForElement($element, $element['fids']['#value']);
}
}
/**
* Wraps the file usage service.
*
* @return \Drupal\file\FileUsage\FileUsageInterface
*/
protected static function fileUsage() {
return \Drupal::service('file.usage');
}
}

View file

@ -0,0 +1,287 @@
<?php
namespace Drupal\file\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\file\FileInterface;
use Drupal\user\UserInterface;
/**
* Defines the file entity class.
*
* @ingroup file
*
* @ContentEntityType(
* id = "file",
* label = @Translation("File"),
* label_collection = @Translation("Files"),
* label_singular = @Translation("file"),
* label_plural = @Translation("files"),
* label_count = @PluralTranslation(
* singular = "@count file",
* plural = "@count files",
* ),
* handlers = {
* "storage" = "Drupal\file\FileStorage",
* "storage_schema" = "Drupal\file\FileStorageSchema",
* "access" = "Drupal\file\FileAccessControlHandler",
* "views_data" = "Drupal\file\FileViewsData",
* },
* base_table = "file_managed",
* entity_keys = {
* "id" = "fid",
* "label" = "filename",
* "langcode" = "langcode",
* "uuid" = "uuid"
* }
* )
*/
class File extends ContentEntityBase implements FileInterface {
use EntityChangedTrait;
/**
* {@inheritdoc}
*/
public function getFilename() {
return $this->get('filename')->value;
}
/**
* {@inheritdoc}
*/
public function setFilename($filename) {
$this->get('filename')->value = $filename;
}
/**
* {@inheritdoc}
*/
public function getFileUri() {
return $this->get('uri')->value;
}
/**
* {@inheritdoc}
*/
public function setFileUri($uri) {
$this->get('uri')->value = $uri;
}
/**
* {@inheritdoc}
*
* @see file_url_transform_relative()
*/
public function url($rel = 'canonical', $options = []) {
return file_create_url($this->getFileUri());
}
/**
* {@inheritdoc}
*/
public function getMimeType() {
return $this->get('filemime')->value;
}
/**
* {@inheritdoc}
*/
public function setMimeType($mime) {
$this->get('filemime')->value = $mime;
}
/**
* {@inheritdoc}
*/
public function getSize() {
return $this->get('filesize')->value;
}
/**
* {@inheritdoc}
*/
public function setSize($size) {
$this->get('filesize')->value = $size;
}
/**
* {@inheritdoc}
*/
public function getCreatedTime() {
return $this->get('created')->value;
}
/**
* {@inheritdoc}
*/
public function getOwner() {
return $this->get('uid')->entity;
}
/**
* {@inheritdoc}
*/
public function getOwnerId() {
return $this->get('uid')->target_id;
}
/**
* {@inheritdoc}
*/
public function setOwnerId($uid) {
$this->set('uid', $uid);
return $this;
}
/**
* {@inheritdoc}
*/
public function setOwner(UserInterface $account) {
$this->set('uid', $account->id());
return $this;
}
/**
* {@inheritdoc}
*/
public function isPermanent() {
return $this->get('status')->value == FILE_STATUS_PERMANENT;
}
/**
* {@inheritdoc}
*/
public function isTemporary() {
return $this->get('status')->value == 0;
}
/**
* {@inheritdoc}
*/
public function setPermanent() {
$this->get('status')->value = FILE_STATUS_PERMANENT;
}
/**
* {@inheritdoc}
*/
public function setTemporary() {
$this->get('status')->value = 0;
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageInterface $storage, array &$values) {
// Automatically detect filename if not set.
if (!isset($values['filename']) && isset($values['uri'])) {
$values['filename'] = drupal_basename($values['uri']);
}
// Automatically detect filemime if not set.
if (!isset($values['filemime']) && isset($values['uri'])) {
$values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['uri']);
}
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// The file itself might not exist or be available right now.
$uri = $this->getFileUri();
$size = @filesize($uri);
// Set size unless there was an error.
if ($size !== FALSE) {
$this->setSize($size);
}
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
foreach ($entities as $entity) {
// Delete all remaining references to this file.
$file_usage = \Drupal::service('file.usage')->listUsage($entity);
if (!empty($file_usage)) {
foreach ($file_usage as $module => $usage) {
\Drupal::service('file.usage')->delete($entity, $module);
}
}
// Delete the actual file. Failures due to invalid files and files that
// were already deleted are logged to watchdog but ignored, the
// corresponding file entity will be deleted.
file_unmanaged_delete($entity->getFileUri());
}
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
$fields = parent::baseFieldDefinitions($entity_type);
$fields['fid']->setLabel(t('File ID'))
->setDescription(t('The file ID.'));
$fields['uuid']->setDescription(t('The file UUID.'));
$fields['langcode']->setLabel(t('Language code'))
->setDescription(t('The file language code.'));
$fields['uid'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('User ID'))
->setDescription(t('The user ID of the file.'))
->setSetting('target_type', 'user');
$fields['filename'] = BaseFieldDefinition::create('string')
->setLabel(t('Filename'))
->setDescription(t('Name of the file with no path components.'));
$fields['uri'] = BaseFieldDefinition::create('file_uri')
->setLabel(t('URI'))
->setDescription(t('The URI to access the file (either local or remote).'))
->setSetting('max_length', 255)
->setSetting('case_sensitive', TRUE)
->addConstraint('FileUriUnique');
$fields['filemime'] = BaseFieldDefinition::create('string')
->setLabel(t('File MIME type'))
->setSetting('is_ascii', TRUE)
->setDescription(t("The file's MIME type."));
$fields['filesize'] = BaseFieldDefinition::create('integer')
->setLabel(t('File size'))
->setDescription(t('The size of the file in bytes.'))
->setSetting('unsigned', TRUE)
->setSetting('size', 'big');
$fields['status'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Status'))
->setDescription(t('The status of the file, temporary (FALSE) and permanent (TRUE).'))
->setDefaultValue(FALSE);
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Created'))
->setDescription(t('The timestamp that the file was created.'));
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The timestamp that the file was last changed.'));
return $fields;
}
}

View file

@ -0,0 +1,133 @@
<?php
namespace Drupal\file;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Provides a File access control handler.
*/
class FileAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\file\FileInterface $entity */
if ($operation == 'download' || $operation == 'view') {
if (\Drupal::service('file_system')->uriScheme($entity->getFileUri()) === 'public') {
if ($operation === 'download') {
return AccessResult::allowed();
}
else {
return AccessResult::allowedIfHasPermission($account, 'access content');
}
}
elseif ($references = $this->getFileReferences($entity)) {
foreach ($references as $field_name => $entity_map) {
foreach ($entity_map as $referencing_entity_type => $referencing_entities) {
/** @var \Drupal\Core\Entity\EntityInterface $referencing_entity */
foreach ($referencing_entities as $referencing_entity) {
$entity_and_field_access = $referencing_entity->access('view', $account, TRUE)->andIf($referencing_entity->$field_name->access('view', $account, TRUE));
if ($entity_and_field_access->isAllowed()) {
return $entity_and_field_access;
}
}
}
}
}
elseif ($entity->getOwnerId() == $account->id()) {
// This case handles new nodes, or detached files. The user who uploaded
// the file can access it even if it's not yet used.
if ($account->isAnonymous()) {
// For anonymous users, only the browser session that uploaded the
// file is positively allowed access to it. See file_save_upload().
// @todo Implement \Drupal\Core\Entity\EntityHandlerInterface so that
// services can be more properly injected.
$allowed_fids = \Drupal::service('session')->get('anonymous_allowed_file_ids', []);
if (!empty($allowed_fids[$entity->id()])) {
return AccessResult::allowed()->addCacheContexts(['session', 'user']);
}
}
else {
return AccessResult::allowed()->addCacheContexts(['user']);
}
}
}
if ($operation == 'delete' || $operation == 'update') {
$account = $this->prepareUser($account);
$file_uid = $entity->get('uid')->getValue();
// Only the file owner can update or delete the file entity.
if ($account->id() == $file_uid[0]['target_id']) {
return AccessResult::allowed();
}
return AccessResult::forbidden('Only the file owner can update or delete the file entity.');
}
// No opinion.
return AccessResult::neutral();
}
/**
* Wrapper for file_get_file_references().
*
* @param \Drupal\file\FileInterface $file
* The file object for which to get references.
*
* @return array
* A multidimensional array. The keys are field_name, entity_type,
* entity_id and the value is an entity referencing this file.
*
* @see file_get_file_references()
*/
protected function getFileReferences(FileInterface $file) {
return file_get_file_references($file, NULL, EntityStorageInterface::FIELD_LOAD_REVISION, NULL);
}
/**
* {@inheritdoc}
*/
protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
// Deny access to fields that should only be set on file creation, and
// "status" which should only be changed based on a file's usage.
$create_only_fields = [
'uri',
'filemime',
'filesize',
];
// The operation is 'edit' when the entity is being created or updated.
// Determine if the entity is being updated by checking if it is new.
$field_name = $field_definition->getName();
if ($operation === 'edit' && $items && ($entity = $items->getEntity()) && !$entity->isNew() && in_array($field_name, $create_only_fields, TRUE)) {
return AccessResult::forbidden();
}
// Regardless of whether the entity exists access should be denied to the
// status field as this is managed via other APIs, for example:
// - \Drupal\file\FileUsage\FileUsageBase::add()
// - \Drupal\file\Plugin\EntityReferenceSelection\FileSelection::createNewEntity()
if ($operation === 'edit' && $field_name === 'status') {
return AccessResult::forbidden();
}
return parent::checkFieldAccess($operation, $field_definition, $account, $items);
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
// The file entity has no "create" permission because by default Drupal core
// does not allow creating file entities independently. It allows you to
// create file entities that are referenced from another entity
// (e.g. an image for a article). A contributed module is free to alter
// this to allow file entities to be created directly.
return AccessResult::neutral();
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Drupal\file;
use Drupal\Core\Entity\EntityAccessControlHandlerInterface;
/**
* Defines an interface for file access handlers that need to run on file formatters.
*
* \Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase,
* which file and image formatters extend, checks 'view' access on the
* referenced files before displaying them. That check would be useless and
* costly with Core's default access control implementation for files
* (\Drupal\file\FileAccessControlHandler grants access based on whether
* there are existing entities with granted access that reference the file). But
* it might be needed if a different access control handler with different logic
* is swapped in.
*
* \Drupal\file\Plugin\Field\FieldFormatter\FileFormatterBase thus adjusts that
* behavior, and only checks access if the access control handler in use for
* files opts in by implementing this interface.
*
* @see \Drupal\file\Plugin\Field\FieldFormatter\FileFormatterBase::needsAccessCheck()
*/
interface FileAccessFormatterControlHandlerInterface extends EntityAccessControlHandlerInterface {}

View file

@ -0,0 +1,119 @@
<?php
namespace Drupal\file;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\user\EntityOwnerInterface;
use Drupal\Core\Entity\EntityChangedInterface;
/**
* Defines getter and setter methods for file entity base fields.
*
* @ingroup file
*/
interface FileInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface {
/**
* Returns the name of the file.
*
* This may differ from the basename of the URI if the file is renamed to
* avoid overwriting an existing file.
*
* @return string
* Name of the file.
*/
public function getFilename();
/**
* Sets the name of the file.
*
* @param string $filename
* The file name that corresponds to this file. May differ from the basename
* of the URI and changing the filename does not change the URI.
*/
public function setFilename($filename);
/**
* Returns the URI of the file.
*
* @return string
* The URI of the file, e.g. public://directory/file.jpg.
*/
public function getFileUri();
/**
* Sets the URI of the file.
*
* @param string $uri
* The URI of the file, e.g. public://directory/file.jpg. Does not change
* the location of the file.
*/
public function setFileUri($uri);
/**
* Returns the MIME type of the file.
*
* @return string
* The MIME type of the file, e.g. image/jpeg or text/xml.
*/
public function getMimeType();
/**
* Sets the MIME type of the file.
*
* @param string $mime
* The MIME type of the file, e.g. image/jpeg or text/xml.
*/
public function setMimeType($mime);
/**
* Returns the size of the file.
*
* @return string
* The size of the file in bytes.
*/
public function getSize();
/**
* Sets the size of the file.
*
* @param int $size
* The size of the file in bytes.
*/
public function setSize($size);
/**
* Returns TRUE if the file is permanent.
*
* @return bool
* TRUE if the file status is permanent.
*/
public function isPermanent();
/**
* Returns TRUE if the file is temporary.
*
* @return bool
* TRUE if the file status is temporary.
*/
public function isTemporary();
/**
* Sets the file status to permanent.
*/
public function setPermanent();
/**
* Sets the file status to temporary.
*/
public function setTemporary();
/**
* Returns the file entity creation timestamp.
*
* @return int
* Creation timestamp of the file entity.
*/
public function getCreatedTime();
}

View file

@ -0,0 +1,23 @@
<?php
namespace Drupal\file;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
use Drupal\Core\StackMiddleware\NegotiationMiddleware;
/**
* Adds 'application/octet-stream' as a known (bin) format.
*/
class FileServiceProvider implements ServiceModifierInterface {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
if ($container->has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')->getClass(), NegotiationMiddleware::class, TRUE)) {
$container->getDefinition('http_middleware.negotiation')->addMethodCall('registerFormat', ['bin', ['application/octet-stream']]);
}
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Drupal\file;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
/**
* File storage for files.
*/
class FileStorage extends SqlContentEntityStorage implements FileStorageInterface {
/**
* {@inheritdoc}
*/
public function spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT) {
$query = $this->database->select($this->entityType->getBaseTable(), 'f')
->condition('f.status', $status);
$query->addExpression('SUM(f.filesize)', 'filesize');
if (isset($uid)) {
$query->condition('f.uid', $uid);
}
return $query->execute()->fetchField();
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\file;
use Drupal\Core\Entity\ContentEntityStorageInterface;
/**
* Defines an interface for file entity storage classes.
*/
interface FileStorageInterface extends ContentEntityStorageInterface {
/**
* Determines total disk space used by a single user or the whole filesystem.
*
* @param int $uid
* Optional. A user id, specifying NULL returns the total space used by all
* non-temporary files.
* @param int $status
* (Optional) The file status to consider. The default is to only
* consider files in status FILE_STATUS_PERMANENT.
*
* @return int
* An integer containing the number of bytes used.
*/
public function spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT);
}

View file

@ -0,0 +1,33 @@
<?php
namespace Drupal\file;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Defines the file schema handler.
*/
class FileStorageSchema extends SqlContentEntityStorageSchema {
/**
* {@inheritdoc}
*/
protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
$schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
$field_name = $storage_definition->getName();
if ($table_name == $this->storage->getBaseTable()) {
switch ($field_name) {
case 'status':
case 'changed':
case 'uri':
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
break;
}
}
return $schema;
}
}

View file

@ -0,0 +1,115 @@
<?php
namespace Drupal\file\FileUsage;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\file\FileInterface;
/**
* Defines the database file usage backend. This is the default Drupal backend.
*/
class DatabaseFileUsageBackend extends FileUsageBase {
/**
* The database connection used to store file usage information.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The name of the SQL table used to store file usage information.
*
* @var string
*/
protected $tableName;
/**
* Construct the DatabaseFileUsageBackend.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection which will be used to store the file usage
* information.
* @param string $table
* (optional) The table to store file usage info. Defaults to 'file_usage'.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* (optional) The config factory.
*/
public function __construct(Connection $connection, $table = 'file_usage', ConfigFactoryInterface $config_factory = NULL) {
parent::__construct($config_factory);
$this->connection = $connection;
$this->tableName = $table;
}
/**
* {@inheritdoc}
*/
public function add(FileInterface $file, $module, $type, $id, $count = 1) {
$this->connection->merge($this->tableName)
->keys([
'fid' => $file->id(),
'module' => $module,
'type' => $type,
'id' => $id,
])
->fields(['count' => $count])
->expression('count', 'count + :count', [':count' => $count])
->execute();
parent::add($file, $module, $type, $id, $count);
}
/**
* {@inheritdoc}
*/
public function delete(FileInterface $file, $module, $type = NULL, $id = NULL, $count = 1) {
// Delete rows that have a exact or less value to prevent empty rows.
$query = $this->connection->delete($this->tableName)
->condition('module', $module)
->condition('fid', $file->id());
if ($type && $id) {
$query
->condition('type', $type)
->condition('id', $id);
}
if ($count) {
$query->condition('count', $count, '<=');
}
$result = $query->execute();
// If the row has more than the specified count decrement it by that number.
if (!$result && $count > 0) {
$query = $this->connection->update($this->tableName)
->condition('module', $module)
->condition('fid', $file->id());
if ($type && $id) {
$query
->condition('type', $type)
->condition('id', $id);
}
$query->expression('count', 'count - :count', [':count' => $count]);
$query->execute();
}
parent::delete($file, $module, $type, $id, $count);
}
/**
* {@inheritdoc}
*/
public function listUsage(FileInterface $file) {
$result = $this->connection->select($this->tableName, 'f')
->fields('f', ['module', 'type', 'id', 'count'])
->condition('fid', $file->id())
->condition('count', 0, '>')
->execute();
$references = [];
foreach ($result as $usage) {
$references[$usage->module][$usage->type][$usage->id] = $usage->count;
}
return $references;
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Drupal\file\FileUsage;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\file\FileInterface;
/**
* Defines the base class for database file usage backend.
*/
abstract class FileUsageBase implements FileUsageInterface {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Creates a FileUsageBase object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* (optional) The config factory. Defaults to NULL and will use
* \Drupal::configFactory() instead.
*
* @deprecated The $config_factory parameter will become required in Drupal
* 9.0.0.
*/
public function __construct(ConfigFactoryInterface $config_factory = NULL) {
$this->configFactory = $config_factory ?: \Drupal::configFactory();
}
/**
* {@inheritdoc}
*/
public function add(FileInterface $file, $module, $type, $id, $count = 1) {
// Make sure that a used file is permanent.
if (!$file->isPermanent()) {
$file->setPermanent();
$file->save();
}
}
/**
* {@inheritdoc}
*/
public function delete(FileInterface $file, $module, $type = NULL, $id = NULL, $count = 1) {
// Do not actually mark files as temporary when the behavior is disabled.
if (!$this->configFactory->get('file.settings')->get('make_unused_managed_files_temporary')) {
return;
}
// If there are no more remaining usages of this file, mark it as temporary,
// which result in a delete through system_cron().
$usage = \Drupal::service('file.usage')->listUsage($file);
if (empty($usage)) {
$file->setTemporary();
$file->save();
}
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace Drupal\file\FileUsage;
use Drupal\file\FileInterface;
/**
* File usage backend interface.
*/
interface FileUsageInterface {
/**
* Records that a module is using a file.
*
* Examples:
* - A module that associates files with nodes, so $type would be
* 'node' and $id would be the node's nid. Files for all revisions are
* stored within a single nid.
* - The User module associates an image with a user, so $type would be 'user'
* and the $id would be the user's uid.
*
* @param \Drupal\file\FileInterface $file
* A file entity.
* @param string $module
* The name of the module using the file.
* @param string $type
* The type of the object that contains the referenced file.
* @param string $id
* The unique ID of the object containing the referenced file.
* @param int $count
* (optional) The number of references to add to the object. Defaults to 1.
*/
public function add(FileInterface $file, $module, $type, $id, $count = 1);
/**
* Removes a record to indicate that a module is no longer using a file.
*
* @param \Drupal\file\FileInterface $file
* A file entity.
* @param string $module
* The name of the module using the file.
* @param string $type
* (optional) The type of the object that contains the referenced file. May
* be omitted if all module references to a file are being deleted. Defaults
* to NULL.
* @param string $id
* (optional) The unique ID of the object containing the referenced file.
* May be omitted if all module references to a file are being deleted.
* Defaults to NULL.
* @param int $count
* (optional) The number of references to delete from the object. Defaults
* to 1. Zero may be specified to delete all references to the file within a
* specific object.
*/
public function delete(FileInterface $file, $module, $type = NULL, $id = NULL, $count = 1);
/**
* Determines where a file is used.
*
* @param \Drupal\file\FileInterface $file
* A file entity.
*
* @return array
* A nested array with usage data. The first level is keyed by module name,
* the second by object type and the third by the object id. The value of
* the third level contains the usage count.
*/
public function listUsage(FileInterface $file);
}

View file

@ -0,0 +1,324 @@
<?php
namespace Drupal\file;
use Drupal\views\EntityViewsData;
/**
* Provides views data for the file entity type.
*/
class FileViewsData extends EntityViewsData {
/**
* {@inheritdoc}
*/
public function getViewsData() {
$data = parent::getViewsData();
// @TODO There is no corresponding information in entity metadata.
$data['file_managed']['table']['base']['help'] = $this->t('Files maintained by Drupal and various modules.');
$data['file_managed']['table']['base']['defaults']['field'] = 'filename';
$data['file_managed']['table']['wizard_id'] = 'file_managed';
$data['file_managed']['fid']['argument'] = [
'id' => 'file_fid',
// The field to display in the summary.
'name field' => 'filename',
'numeric' => TRUE,
];
$data['file_managed']['fid']['relationship'] = [
'title' => $this->t('File usage'),
'help' => $this->t('Relate file entities to their usage.'),
'id' => 'standard',
'base' => 'file_usage',
'base field' => 'fid',
'field' => 'fid',
'label' => $this->t('File usage'),
];
$data['file_managed']['uri']['field']['default_formatter'] = 'file_uri';
$data['file_managed']['filemime']['field']['default_formatter'] = 'file_filemime';
$data['file_managed']['extension'] = [
'title' => $this->t('Extension'),
'help' => $this->t('The extension of the file.'),
'real field' => 'filename',
'field' => [
'entity_type' => 'file',
'field_name' => 'filename',
'default_formatter' => 'file_extension',
'id' => 'field',
'click sortable' => FALSE,
],
];
$data['file_managed']['filesize']['field']['default_formatter'] = 'file_size';
$data['file_managed']['status']['field']['default_formatter_settings'] = [
'format' => 'custom',
'format_custom_false' => $this->t('Temporary'),
'format_custom_true' => $this->t('Permanent'),
];
$data['file_managed']['status']['filter']['id'] = 'file_status';
$data['file_managed']['uid']['relationship']['title'] = $this->t('User who uploaded');
$data['file_managed']['uid']['relationship']['label'] = $this->t('User who uploaded');
$data['file_usage']['table']['group'] = $this->t('File Usage');
// Provide field-type-things to several base tables; on the core files table
// ("file_managed") so that we can create relationships from files to
// entities, and then on each core entity type base table so that we can
// provide general relationships between entities and files.
$data['file_usage']['table']['join'] = [
'file_managed' => [
'field' => 'fid',
'left_field' => 'fid',
],
// Link ourselves to the {node_field_data} table
// so we can provide node->file relationships.
'node_field_data' => [
'field' => 'id',
'left_field' => 'nid',
'extra' => [['field' => 'type', 'value' => 'node']],
],
// Link ourselves to the {users_field_data} table
// so we can provide user->file relationships.
'users_field_data' => [
'field' => 'id',
'left_field' => 'uid',
'extra' => [['field' => 'type', 'value' => 'user']],
],
// Link ourselves to the {comment_field_data} table
// so we can provide comment->file relationships.
'comment' => [
'field' => 'id',
'left_field' => 'cid',
'extra' => [['field' => 'type', 'value' => 'comment']],
],
// Link ourselves to the {taxonomy_term_field_data} table
// so we can provide taxonomy_term->file relationships.
'taxonomy_term_data' => [
'field' => 'id',
'left_field' => 'tid',
'extra' => [['field' => 'type', 'value' => 'taxonomy_term']],
],
];
// Provide a relationship between the files table and each entity type,
// and between each entity type and the files table. Entity->file
// relationships are type-restricted in the joins declared above, and
// file->entity relationships are type-restricted in the relationship
// declarations below.
// Describes relationships between files and nodes.
$data['file_usage']['file_to_node'] = [
'title' => $this->t('Content'),
'help' => $this->t('Content that is associated with this file, usually because this file is in a field on the content.'),
// Only provide this field/relationship/etc.,
// when the 'file_managed' base table is present.
'skip base' => ['node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'],
'real field' => 'id',
'relationship' => [
'title' => $this->t('Content'),
'label' => $this->t('Content'),
'base' => 'node_field_data',
'base field' => 'nid',
'relationship field' => 'id',
'extra' => [['table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'node']],
],
];
$data['file_usage']['node_to_file'] = [
'title' => $this->t('File'),
'help' => $this->t('A file that is associated with this node, usually because it is in a field on the node.'),
// Only provide this field/relationship/etc.,
// when the 'node' base table is present.
'skip base' => ['file_managed', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'],
'real field' => 'fid',
'relationship' => [
'title' => $this->t('File'),
'label' => $this->t('File'),
'base' => 'file_managed',
'base field' => 'fid',
'relationship field' => 'fid',
],
];
// Describes relationships between files and users.
$data['file_usage']['file_to_user'] = [
'title' => $this->t('User'),
'help' => $this->t('A user that is associated with this file, usually because this file is in a field on the user.'),
// Only provide this field/relationship/etc.,
// when the 'file_managed' base table is present.
'skip base' => ['node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'],
'real field' => 'id',
'relationship' => [
'title' => $this->t('User'),
'label' => $this->t('User'),
'base' => 'users',
'base field' => 'uid',
'relationship field' => 'id',
'extra' => [['table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'user']],
],
];
$data['file_usage']['user_to_file'] = [
'title' => $this->t('File'),
'help' => $this->t('A file that is associated with this user, usually because it is in a field on the user.'),
// Only provide this field/relationship/etc.,
// when the 'users' base table is present.
'skip base' => ['file_managed', 'node_field_data', 'node_field_revision', 'comment_field_data', 'taxonomy_term_field_data'],
'real field' => 'fid',
'relationship' => [
'title' => $this->t('File'),
'label' => $this->t('File'),
'base' => 'file_managed',
'base field' => 'fid',
'relationship field' => 'fid',
],
];
// Describes relationships between files and comments.
$data['file_usage']['file_to_comment'] = [
'title' => $this->t('Comment'),
'help' => $this->t('A comment that is associated with this file, usually because this file is in a field on the comment.'),
// Only provide this field/relationship/etc.,
// when the 'file_managed' base table is present.
'skip base' => ['node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'],
'real field' => 'id',
'relationship' => [
'title' => $this->t('Comment'),
'label' => $this->t('Comment'),
'base' => 'comment_field_data',
'base field' => 'cid',
'relationship field' => 'id',
'extra' => [['table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'comment']],
],
];
$data['file_usage']['comment_to_file'] = [
'title' => $this->t('File'),
'help' => $this->t('A file that is associated with this comment, usually because it is in a field on the comment.'),
// Only provide this field/relationship/etc.,
// when the 'comment' base table is present.
'skip base' => ['file_managed', 'node_field_data', 'node_field_revision', 'users_field_data', 'taxonomy_term_field_data'],
'real field' => 'fid',
'relationship' => [
'title' => $this->t('File'),
'label' => $this->t('File'),
'base' => 'file_managed',
'base field' => 'fid',
'relationship field' => 'fid',
],
];
// Describes relationships between files and taxonomy_terms.
$data['file_usage']['file_to_taxonomy_term'] = [
'title' => $this->t('Taxonomy Term'),
'help' => $this->t('A taxonomy term that is associated with this file, usually because this file is in a field on the taxonomy term.'),
// Only provide this field/relationship/etc.,
// when the 'file_managed' base table is present.
'skip base' => ['node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'],
'real field' => 'id',
'relationship' => [
'title' => $this->t('Taxonomy Term'),
'label' => $this->t('Taxonomy Term'),
'base' => 'taxonomy_term_data',
'base field' => 'tid',
'relationship field' => 'id',
'extra' => [['table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'taxonomy_term']],
],
];
$data['file_usage']['taxonomy_term_to_file'] = [
'title' => $this->t('File'),
'help' => $this->t('A file that is associated with this taxonomy term, usually because it is in a field on the taxonomy term.'),
// Only provide this field/relationship/etc.,
// when the 'taxonomy_term_data' base table is present.
'skip base' => ['file_managed', 'node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data'],
'real field' => 'fid',
'relationship' => [
'title' => $this->t('File'),
'label' => $this->t('File'),
'base' => 'file_managed',
'base field' => 'fid',
'relationship field' => 'fid',
],
];
// Provide basic fields from the {file_usage} table to all of the base tables
// we've declared joins to, because there is no 'skip base' property on these
// fields.
$data['file_usage']['module'] = [
'title' => $this->t('Module'),
'help' => $this->t('The module managing this file relationship.'),
'field' => [
'id' => 'standard',
],
'filter' => [
'id' => 'string',
],
'argument' => [
'id' => 'string',
],
'sort' => [
'id' => 'standard',
],
];
$data['file_usage']['type'] = [
'title' => $this->t('Entity type'),
'help' => $this->t('The type of entity that is related to the file.'),
'field' => [
'id' => 'standard',
],
'filter' => [
'id' => 'string',
],
'argument' => [
'id' => 'string',
],
'sort' => [
'id' => 'standard',
],
];
$data['file_usage']['id'] = [
'title' => $this->t('Entity ID'),
'help' => $this->t('The ID of the entity that is related to the file.'),
'field' => [
'id' => 'numeric',
],
'argument' => [
'id' => 'numeric',
],
'filter' => [
'id' => 'numeric',
],
'sort' => [
'id' => 'standard',
],
];
$data['file_usage']['count'] = [
'title' => $this->t('Use count'),
'help' => $this->t('The number of times the file is used by this entity.'),
'field' => [
'id' => 'numeric',
],
'filter' => [
'id' => 'numeric',
],
'sort' => [
'id' => 'standard',
],
];
$data['file_usage']['entity_label'] = [
'title' => $this->t('Entity label'),
'help' => $this->t('The label of the entity that is related to the file.'),
'real field' => 'id',
'field' => [
'id' => 'entity_label',
'entity type field' => 'type',
],
];
return $data;
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Drupal\file\Plugin\EntityReferenceSelection;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
/**
* Provides specific access control for the file entity type.
*
* @EntityReferenceSelection(
* id = "default:file",
* label = @Translation("File selection"),
* entity_types = {"file"},
* group = "default",
* weight = 1
* )
*/
class FileSelection extends DefaultSelection {
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
// Allow referencing :
// - files with status "permanent"
// - or files uploaded by the current user (since newly uploaded files only
// become "permanent" after the containing entity gets validated and
// saved.)
$query->condition($query->orConditionGroup()
->condition('status', FILE_STATUS_PERMANENT)
->condition('uid', $this->currentUser->id()));
return $query;
}
/**
* {@inheritdoc}
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$file = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
// In order to create a referenceable file, it needs to have a "permanent"
// status.
/** @var \Drupal\file\FileInterface $file */
$file->setPermanent();
return $file;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableNewEntities(array $entities) {
$entities = parent::validateReferenceableNewEntities($entities);
$entities = array_filter($entities, function ($file) {
/** @var \Drupal\file\FileInterface $file */
return $file->isPermanent() || $file->getOwnerId() === $this->currentUser->id();
});
return $entities;
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Base class for file formatters, which allow to link to the file download URL.
*/
abstract class BaseFieldFileFormatterBase extends FormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
$settings['link_to_file'] = FALSE;
return $settings;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$form = parent::settingsForm($form, $form_state);
$form['link_to_file'] = [
'#title' => $this->t('Link this field to the file download URL'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('link_to_file'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
$url = NULL;
// Add support to link to the entity itself.
if ($this->getSetting('link_to_file')) {
// @todo Wrap in file_url_transform_relative(). This is currently
// impossible. See below.
$url = file_create_url($items->getEntity()->uri->value);
}
foreach ($items as $delta => $item) {
$view_value = $this->viewValue($item);
if ($url) {
$elements[$delta] = [
'#type' => 'link',
'#title' => $view_value,
'#url' => Url::fromUri($url),
// @todo Remove the 'url.site' cache context by using a relative file
// URL (file_url_transform_relative()). This is currently impossible
// because #type => link requires a Url object, and Url objects do not
// support relative URLs: they require fully qualified URLs. Fix in
// https://www.drupal.org/node/2646744.
'#cache' => [
'contexts' => [
'url.site',
],
],
];
}
else {
$elements[$delta] = is_array($view_value) ? $view_value : ['#markup' => $view_value];
}
}
return $elements;
}
/**
* Generate the output appropriate for one field item.
*
* @param \Drupal\Core\Field\FieldItemInterface $item
* One field item.
*
* @return mixed
* The textual output generated.
*/
abstract protected function viewValue(FieldItemInterface $item);
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return $field_definition->getTargetEntityTypeId() === 'file';
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Formatter for a text field on a file entity that links the field to the file.
*
* @FieldFormatter(
* id = "file_link",
* label = @Translation("File link"),
* field_types = {
* "string"
* }
* )
*/
class DefaultFileFormatter extends BaseFieldFileFormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
$settings = parent::defaultSettings();
$settings['link_to_file'] = TRUE;
return $settings;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
// We don't call the parent in order to bypass the link to file form.
return $form;
}
/**
* {@inheritdoc}
*/
protected function viewValue(FieldItemInterface $item) {
return $item->value;
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Form\FormStateInterface;
/**
* Base class for file formatters that have to deal with file descriptions.
*/
abstract class DescriptionAwareFileFormatterBase extends FileFormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
$settings = parent::defaultSettings();
$settings['use_description_as_link_text'] = TRUE;
return $settings;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$form = parent::settingsForm($form, $form_state);
$form['use_description_as_link_text'] = [
'#title' => $this->t('Use description as link text'),
'#description' => $this->t('Replace the file name by its description when available'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('use_description_as_link_text'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
if ($this->getSetting('use_description_as_link_text')) {
$summary[] = $this->t('Use description as link text');
}
return $summary;
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
/**
* Plugin implementation of the 'file_audio' formatter.
*
* @FieldFormatter(
* id = "file_audio",
* label = @Translation("Audio"),
* description = @Translation("Display the file using an HTML5 audio tag."),
* field_types = {
* "file"
* }
* )
*/
class FileAudioFormatter extends FileMediaFormatterBase {
/**
* {@inheritdoc}
*/
public static function getMediaType() {
return 'audio';
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Formatter to render a filename as file extension.
*
* @FieldFormatter(
* id = "file_extension",
* label = @Translation("File extension"),
* field_types = {
* "string"
* }
* )
*/
class FileExtensionFormatter extends BaseFieldFileFormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
$settings = parent::defaultSettings();
$settings['extension_detect_tar'] = FALSE;
return $settings;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$form = parent::settingsForm($form, $form_state);
$form['extension_detect_tar'] = [
'#type' => 'checkbox',
'#title' => $this->t('Include tar in extension'),
'#description' => $this->t("If the part of the filename just before the extension is '.tar', include this in the extension output."),
'#default_value' => $this->getSetting('extension_detect_tar'),
];
return $form;
}
/**
* {@inheritdoc}
*/
protected function viewValue(FieldItemInterface $item) {
$filename = $item->value;
if (!$this->getSetting('extension_detect_tar')) {
return pathinfo($filename, PATHINFO_EXTENSION);
}
else {
$file_parts = explode('.', basename($filename));
if (count($file_parts) > 1) {
$extension = array_pop($file_parts);
$last_part_in_name = array_pop($file_parts);
if ($last_part_in_name === 'tar') {
$extension = 'tar.' . $extension;
}
return $extension;
}
}
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
// Just show this file extension formatter on the filename field.
return parent::isApplicable($field_definition) && $field_definition->getName() === 'filename';
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
/**
* Base class for file formatters.
*/
abstract class FileFormatterBase extends EntityReferenceFormatterBase {
/**
* {@inheritdoc}
*/
protected function needsEntityLoad(EntityReferenceItem $item) {
return parent::needsEntityLoad($item) && $item->isDisplayed();
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity) {
// Only check access if the current file access control handler explicitly
// opts in by implementing FileAccessFormatterControlHandlerInterface.
$access_handler_class = $entity->getEntityType()->getHandlerClass('access');
if (is_subclass_of($access_handler_class, '\Drupal\file\FileAccessFormatterControlHandlerInterface')) {
return $entity->access('view', NULL, TRUE);
}
else {
return AccessResult::allowed();
}
}
}

View file

@ -0,0 +1,220 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Template\Attribute;
/**
* Base class for media file formatter.
*/
abstract class FileMediaFormatterBase extends FileFormatterBase implements FileMediaFormatterInterface {
/**
* Gets the HTML tag for the formatter.
*
* @return string
* The HTML tag of this formatter.
*/
protected function getHtmlTag() {
return static::getMediaType();
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'controls' => TRUE,
'autoplay' => FALSE,
'loop' => FALSE,
'multiple_file_display_type' => 'tags',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
return [
'controls' => [
'#title' => $this->t('Show playback controls'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('controls'),
],
'autoplay' => [
'#title' => $this->t('Autoplay'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('autoplay'),
],
'loop' => [
'#title' => $this->t('Loop'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('loop'),
],
'multiple_file_display_type' => [
'#title' => $this->t('Display of multiple files'),
'#type' => 'radios',
'#options' => [
'tags' => $this->t('Use multiple @tag tags, each with a single source.', ['@tag' => '<' . $this->getHtmlTag() . '>']),
'sources' => $this->t('Use multiple sources within a single @tag tag.', ['@tag' => '<' . $this->getHtmlTag() . '>']),
],
'#default_value' => $this->getSetting('multiple_file_display_type'),
],
];
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
if (!parent::isApplicable($field_definition)) {
return FALSE;
}
/** @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $extension_mime_type_guesser */
$extension_mime_type_guesser = \Drupal::service('file.mime_type.guesser.extension');
$extension_list = array_filter(preg_split('/\s+/', $field_definition->getSetting('file_extensions')));
foreach ($extension_list as $extension) {
$mime_type = $extension_mime_type_guesser->guess('fakedFile.' . $extension);
if (static::mimeTypeApplies($mime_type)) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$summary[] = $this->t('Playback controls: %controls', ['%controls' => $this->getSetting('controls') ? $this->t('visible') : $this->t('hidden')]);
$summary[] = $this->t('Autoplay: %autoplay', ['%autoplay' => $this->getSetting('autoplay') ? $this->t('yes') : $this->t('no')]);
$summary[] = $this->t('Loop: %loop', ['%loop' => $this->getSetting('loop') ? $this->t('yes') : $this->t('no')]);
switch ($this->getSetting('multiple_file_display_type')) {
case 'tags':
$summary[] = $this->t('Multiple file display: Multiple HTML tags');
break;
case 'sources':
$summary[] = $this->t('Multiple file display: One HTML tag with multiple sources');
break;
}
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
$source_files = $this->getSourceFiles($items, $langcode);
if (empty($source_files)) {
return $elements;
}
$attributes = $this->prepareAttributes();
foreach ($source_files as $delta => $files) {
$elements[$delta] = [
'#theme' => $this->getPluginId(),
'#attributes' => $attributes,
'#files' => $files,
'#cache' => ['tags' => []],
];
$cache_tags = [];
foreach ($files as $file) {
$cache_tags = Cache::mergeTags($cache_tags, $file['file']->getCacheTags());
}
$elements[$delta]['#cache']['tags'] = $cache_tags;
}
return $elements;
}
/**
* Prepare the attributes according to the settings.
*
* @param string[] $additional_attributes
* Additional attributes to be applied to the HTML element. Attribute names
* will be used as key and value in the HTML element.
*
* @return \Drupal\Core\Template\Attribute
* Container with all the attributes for the HTML tag.
*/
protected function prepareAttributes(array $additional_attributes = []) {
$attributes = new Attribute();
foreach (['controls', 'autoplay', 'loop'] + $additional_attributes as $attribute) {
if ($this->getSetting($attribute)) {
$attributes->setAttribute($attribute, $attribute);
}
}
return $attributes;
}
/**
* Check if given MIME type applies to the media type of the formatter.
*
* @param string $mime_type
* The complete MIME type.
*
* @return bool
* TRUE if the MIME type applies, FALSE otherwise.
*/
protected static function mimeTypeApplies($mime_type) {
list($type) = explode('/', $mime_type, 2);
return $type === static::getMediaType();
}
/**
* Gets source files with attributes.
*
* @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items
* The item list.
* @param string $langcode
* The language code of the referenced entities to display.
*
* @return array
* Numerically indexed array, which again contains an associative array with
* the following key/values:
* - file => \Drupal\file\Entity\File
* - source_attributes => \Drupal\Core\Template\Attribute
*/
protected function getSourceFiles(EntityReferenceFieldItemListInterface $items, $langcode) {
$source_files = [];
// Because we can have the files grouped in a single media tag, we do a
// grouping in case the multiple file behavior is not 'tags'.
/** @var \Drupal\file\Entity\File $file */
foreach ($this->getEntitiesToView($items, $langcode) as $file) {
if (static::mimeTypeApplies($file->getMimeType())) {
$source_attributes = new Attribute();
$source_attributes
->setAttribute('src', file_url_transform_relative(file_create_url($file->getFileUri())))
->setAttribute('type', $file->getMimeType());
if ($this->getSetting('multiple_file_display_type') === 'tags') {
$source_files[] = [
[
'file' => $file,
'source_attributes' => $source_attributes,
],
];
}
else {
$source_files[0][] = [
'file' => $file,
'source_attributes' => $source_attributes,
];
}
}
}
return $source_files;
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
/**
* Defines getter methods for FileMediaFormatterBase.
*
* This interface is used on the FileMediaFormatterBase class to ensure that
* each file media formatter will be based on a media type.
*
* Abstract classes are not able to implement abstract static methods,
* this interface will work around that.
*
* @see \Drupal\file\Plugin\Field\FieldFormatter\FileMediaFormatterBase
*/
interface FileMediaFormatterInterface {
/**
* Gets the applicable media type for a formatter.
*
* @return string
* The media type of this formatter.
*/
public static function getMediaType();
}

View file

@ -0,0 +1,42 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
/**
* Formatter that shows the file size in a human readable way.
*
* @FieldFormatter(
* id = "file_size",
* label = @Translation("File size"),
* field_types = {
* "integer"
* }
* )
*/
class FileSize extends FormatterBase {
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return parent::isApplicable($field_definition) && $field_definition->getName() === 'filesize';
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($items as $delta => $item) {
$elements[$delta] = ['#markup' => format_size($item->value)];
}
return $elements;
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Formatter to render the file URI to its download path.
*
* @FieldFormatter(
* id = "file_uri",
* label = @Translation("File URI"),
* field_types = {
* "uri",
* "file_uri",
* }
* )
*/
class FileUriFormatter extends BaseFieldFileFormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
$settings = parent::defaultSettings();
$settings['file_download_path'] = FALSE;
return $settings;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$form = parent::settingsForm($form, $form_state);
$form['file_download_path'] = [
'#title' => $this->t('Display the file download URI'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('file_download_path'),
];
return $form;
}
/**
* {@inheritdoc}
*/
protected function viewValue(FieldItemInterface $item) {
$value = $item->value;
if ($this->getSetting('file_download_path')) {
// @todo Wrap in file_url_transform_relative(). This is currently
// impossible. See BaseFieldFileFormatterBase::viewElements(). Fix in
// https://www.drupal.org/node/2646744.
$value = file_create_url($value);
}
return $value;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return parent::isApplicable($field_definition) && $field_definition->getName() === 'uri';
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'file_video' formatter.
*
* @FieldFormatter(
* id = "file_video",
* label = @Translation("Video"),
* description = @Translation("Display the file using an HTML5 video tag."),
* field_types = {
* "file"
* }
* )
*/
class FileVideoFormatter extends FileMediaFormatterBase {
/**
* {@inheritdoc}
*/
public static function getMediaType() {
return 'video';
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'muted' => FALSE,
'width' => 640,
'height' => 480,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
return parent::settingsForm($form, $form_state) + [
'muted' => [
'#title' => $this->t('Muted'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('muted'),
],
'width' => [
'#type' => 'number',
'#title' => $this->t('Width'),
'#default_value' => $this->getSetting('width'),
'#size' => 5,
'#maxlength' => 5,
'#field_suffix' => $this->t('pixels'),
'#min' => 0,
'#required' => TRUE,
],
'height' => [
'#type' => 'number',
'#title' => $this->t('Height'),
'#default_value' => $this->getSetting('height'),
'#size' => 5,
'#maxlength' => 5,
'#field_suffix' => $this->t('pixels'),
'#min' => 0,
'#required' => TRUE,
],
];
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
$summary[] = $this->t('Muted: %muted', ['%muted' => $this->getSetting('muted') ? $this->t('yes') : $this->t('no')]);
$summary[] = $this->t('Size: %width x %height pixels', [
'%width' => $this->getSetting('width'),
'%height' => $this->getSetting('height'),
]);
return $summary;
}
/**
* {@inheritdoc}
*/
protected function prepareAttributes(array $additional_attributes = []) {
return parent::prepareAttributes(['muted'])
->setAttribute('width', $this->getSetting('width'))
->setAttribute('height', $this->getSetting('height'));
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Formatter to render the file MIME type, with an optional icon.
*
* @FieldFormatter(
* id = "file_filemime",
* label = @Translation("File MIME"),
* field_types = {
* "string"
* }
* )
*/
class FilemimeFormatter extends BaseFieldFileFormatterBase {
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return parent::isApplicable($field_definition) && $field_definition->getName() === 'filemime';
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
$settings = parent::defaultSettings();
$settings['filemime_image'] = FALSE;
return $settings;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$form = parent::settingsForm($form, $form_state);
$form['filemime_image'] = [
'#title' => $this->t('Display an icon'),
'#description' => $this->t('The icon is representing the file type, instead of the MIME text (such as "image/jpeg")'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('filemime_image'),
];
return $form;
}
/**
* {@inheritdoc}
*/
protected function viewValue(FieldItemInterface $item) {
$value = $item->value;
if ($this->getSetting('filemime_image') && $value) {
$file_icon = [
'#theme' => 'image__file_icon',
'#file' => $item->getEntity(),
];
return $file_icon;
}
return $value;
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Plugin implementation of the 'file_default' formatter.
*
* @FieldFormatter(
* id = "file_default",
* label = @Translation("Generic file"),
* field_types = {
* "file"
* }
* )
*/
class GenericFileFormatter extends DescriptionAwareFileFormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($this->getEntitiesToView($items, $langcode) as $delta => $file) {
$item = $file->_referringItem;
$elements[$delta] = [
'#theme' => 'file_link',
'#file' => $file,
'#description' => $this->getSetting('use_description_as_link_text') ? $item->description : NULL,
'#cache' => [
'tags' => $file->getCacheTags(),
],
];
// Pass field item attributes to the theme function.
if (isset($item->_attributes)) {
$elements[$delta] += ['#attributes' => []];
$elements[$delta]['#attributes'] += $item->_attributes;
// Unset field item attributes since they have been included in the
// formatter output and should not be rendered in the field template.
unset($item->_attributes);
}
}
return $elements;
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Plugin implementation of the 'file_rss_enclosure' formatter.
*
* @FieldFormatter(
* id = "file_rss_enclosure",
* label = @Translation("RSS enclosure"),
* field_types = {
* "file"
* }
* )
*/
class RSSEnclosureFormatter extends FileFormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$entity = $items->getEntity();
// Add the first file as an enclosure to the RSS item. RSS allows only one
// enclosure per item. See: http://wikipedia.org/wiki/RSS_enclosure
foreach ($this->getEntitiesToView($items, $langcode) as $delta => $file) {
$entity->rss_elements[] = [
'key' => 'enclosure',
'attributes' => [
// In RSS feeds, it is necessary to use absolute URLs. The 'url.site'
// cache context is already associated with RSS feed responses, so it
// does not need to be specified here.
'url' => file_create_url($file->getFileUri()),
'length' => $file->getSize(),
'type' => $file->getMimeType(),
],
];
}
return [];
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Plugin implementation of the 'file_table' formatter.
*
* @FieldFormatter(
* id = "file_table",
* label = @Translation("Table of files"),
* field_types = {
* "file"
* }
* )
*/
class TableFormatter extends DescriptionAwareFileFormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
if ($files = $this->getEntitiesToView($items, $langcode)) {
$header = [t('Attachment'), t('Size')];
$rows = [];
foreach ($files as $delta => $file) {
$item = $file->_referringItem;
$rows[] = [
[
'data' => [
'#theme' => 'file_link',
'#file' => $file,
'#description' => $this->getSetting('use_description_as_link_text') ? $item->description : NULL,
'#cache' => [
'tags' => $file->getCacheTags(),
],
],
],
['data' => format_size($file->getSize())],
];
}
$elements[0] = [];
if (!empty($rows)) {
$elements[0] = [
'#theme' => 'table__file_formatter_table',
'#header' => $header,
'#rows' => $rows,
];
}
}
return $elements;
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Plugin implementation of the 'file_url_plain' formatter.
*
* @FieldFormatter(
* id = "file_url_plain",
* label = @Translation("URL to file"),
* field_types = {
* "file"
* }
* )
*/
class UrlPlainFormatter extends FileFormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($this->getEntitiesToView($items, $langcode) as $delta => $file) {
$elements[$delta] = [
'#markup' => file_url_transform_relative(file_create_url($file->getFileUri())),
'#cache' => [
'tags' => $file->getCacheTags(),
],
];
}
return $elements;
}
}

View file

@ -0,0 +1,105 @@
<?php
namespace Drupal\file\Plugin\Field\FieldType;
use Drupal\Core\Field\EntityReferenceFieldItemList;
use Drupal\Core\Form\FormStateInterface;
/**
* Represents a configurable entity file field.
*/
class FileFieldItemList extends EntityReferenceFieldItemList {
/**
* {@inheritdoc}
*/
public function defaultValuesForm(array &$form, FormStateInterface $form_state) {}
/**
* {@inheritdoc}
*/
public function postSave($update) {
$entity = $this->getEntity();
if (!$update) {
// Add a new usage for newly uploaded files.
foreach ($this->referencedEntities() as $file) {
\Drupal::service('file.usage')->add($file, 'file', $entity->getEntityTypeId(), $entity->id());
}
}
else {
// Get current target file entities and file IDs.
$files = $this->referencedEntities();
$ids = [];
/** @var \Drupal\file\FileInterface $file */
foreach ($files as $file) {
$ids[] = $file->id();
}
// On new revisions, all files are considered to be a new usage and no
// deletion of previous file usages are necessary.
if (!empty($entity->original) && $entity->getRevisionId() != $entity->original->getRevisionId()) {
foreach ($files as $file) {
\Drupal::service('file.usage')->add($file, 'file', $entity->getEntityTypeId(), $entity->id());
}
return;
}
// Get the file IDs attached to the field before this update.
$field_name = $this->getFieldDefinition()->getName();
$original_ids = [];
$langcode = $this->getLangcode();
$original = $entity->original;
if ($original->hasTranslation($langcode)) {
$original_items = $original->getTranslation($langcode)->{$field_name};
foreach ($original_items as $item) {
$original_ids[] = $item->target_id;
}
}
// Decrement file usage by 1 for files that were removed from the field.
$removed_ids = array_filter(array_diff($original_ids, $ids));
$removed_files = \Drupal::entityManager()->getStorage('file')->loadMultiple($removed_ids);
foreach ($removed_files as $file) {
\Drupal::service('file.usage')->delete($file, 'file', $entity->getEntityTypeId(), $entity->id());
}
// Add new usage entries for newly added files.
foreach ($files as $file) {
if (!in_array($file->id(), $original_ids)) {
\Drupal::service('file.usage')->add($file, 'file', $entity->getEntityTypeId(), $entity->id());
}
}
}
}
/**
* {@inheritdoc}
*/
public function delete() {
parent::delete();
$entity = $this->getEntity();
// If a translation is deleted only decrement the file usage by one. If the
// default translation is deleted remove all file usages within this entity.
$count = $entity->isDefaultTranslation() ? 0 : 1;
foreach ($this->referencedEntities() as $file) {
\Drupal::service('file.usage')->delete($file, 'file', $entity->getEntityTypeId(), $entity->id(), $count);
}
}
/**
* {@inheritdoc}
*/
public function deleteRevision() {
parent::deleteRevision();
$entity = $this->getEntity();
// Decrement the file usage by 1.
foreach ($this->referencedEntities() as $file) {
\Drupal::service('file.usage')->delete($file, 'file', $entity->getEntityTypeId(), $entity->id());
}
}
}

View file

@ -0,0 +1,362 @@
<?php
namespace Drupal\file\Plugin\Field\FieldType;
use Drupal\Component\Utility\Bytes;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\Core\TypedData\DataDefinition;
/**
* Plugin implementation of the 'file' field type.
*
* @FieldType(
* id = "file",
* label = @Translation("File"),
* description = @Translation("This field stores the ID of a file as an integer value."),
* category = @Translation("Reference"),
* default_widget = "file_generic",
* default_formatter = "file_default",
* list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList",
* constraints = {"ReferenceAccess" = {}, "FileValidation" = {}}
* )
*/
class FileItem extends EntityReferenceItem {
/**
* {@inheritdoc}
*/
public static function defaultStorageSettings() {
return [
'target_type' => 'file',
'display_field' => FALSE,
'display_default' => FALSE,
'uri_scheme' => file_default_scheme(),
] + parent::defaultStorageSettings();
}
/**
* {@inheritdoc}
*/
public static function defaultFieldSettings() {
return [
'file_extensions' => 'txt',
'file_directory' => '[date:custom:Y]-[date:custom:m]',
'max_filesize' => '',
'description_field' => 0,
] + parent::defaultFieldSettings();
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'target_id' => [
'description' => 'The ID of the file entity.',
'type' => 'int',
'unsigned' => TRUE,
],
'display' => [
'description' => 'Flag to control whether this file should be displayed when viewing content.',
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'default' => 1,
],
'description' => [
'description' => 'A description of the file.',
'type' => 'text',
],
],
'indexes' => [
'target_id' => ['target_id'],
],
'foreign keys' => [
'target_id' => [
'table' => 'file_managed',
'columns' => ['target_id' => 'fid'],
],
],
];
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties = parent::propertyDefinitions($field_definition);
$properties['display'] = DataDefinition::create('boolean')
->setLabel(t('Display'))
->setDescription(t('Flag to control whether this file should be displayed when viewing content'));
$properties['description'] = DataDefinition::create('string')
->setLabel(t('Description'));
return $properties;
}
/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
$element = [];
$element['#attached']['library'][] = 'file/drupal.file';
$element['display_field'] = [
'#type' => 'checkbox',
'#title' => t('Enable <em>Display</em> field'),
'#default_value' => $this->getSetting('display_field'),
'#description' => t('The display option allows users to choose if a file should be shown when viewing the content.'),
];
$element['display_default'] = [
'#type' => 'checkbox',
'#title' => t('Files displayed by default'),
'#default_value' => $this->getSetting('display_default'),
'#description' => t('This setting only has an effect if the display option is enabled.'),
'#states' => [
'visible' => [
':input[name="settings[display_field]"]' => ['checked' => TRUE],
],
],
];
$scheme_options = \Drupal::service('stream_wrapper_manager')->getNames(StreamWrapperInterface::WRITE_VISIBLE);
$element['uri_scheme'] = [
'#type' => 'radios',
'#title' => t('Upload destination'),
'#options' => $scheme_options,
'#default_value' => $this->getSetting('uri_scheme'),
'#description' => t('Select where the final files should be stored. Private file storage has significantly more overhead than public files, but allows restricted access to files within this field.'),
'#disabled' => $has_data,
];
return $element;
}
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
$element = [];
$settings = $this->getSettings();
$element['file_directory'] = [
'#type' => 'textfield',
'#title' => t('File directory'),
'#default_value' => $settings['file_directory'],
'#description' => t('Optional subdirectory within the upload destination where files will be stored. Do not include preceding or trailing slashes.'),
'#element_validate' => [[get_class($this), 'validateDirectory']],
'#weight' => 3,
];
// Make the extension list a little more human-friendly by comma-separation.
$extensions = str_replace(' ', ', ', $settings['file_extensions']);
$element['file_extensions'] = [
'#type' => 'textfield',
'#title' => t('Allowed file extensions'),
'#default_value' => $extensions,
'#description' => t('Separate extensions with a space or comma and do not include the leading dot.'),
'#element_validate' => [[get_class($this), 'validateExtensions']],
'#weight' => 1,
'#maxlength' => 256,
// By making this field required, we prevent a potential security issue
// that would allow files of any type to be uploaded.
'#required' => TRUE,
];
$element['max_filesize'] = [
'#type' => 'textfield',
'#title' => t('Maximum upload size'),
'#default_value' => $settings['max_filesize'],
'#description' => t('Enter a value like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes) in order to restrict the allowed file size. If left empty the file sizes will be limited only by PHP\'s maximum post and file upload sizes (current limit <strong>%limit</strong>).', ['%limit' => format_size(file_upload_max_size())]),
'#size' => 10,
'#element_validate' => [[get_class($this), 'validateMaxFilesize']],
'#weight' => 5,
];
$element['description_field'] = [
'#type' => 'checkbox',
'#title' => t('Enable <em>Description</em> field'),
'#default_value' => isset($settings['description_field']) ? $settings['description_field'] : '',
'#description' => t('The description field allows users to enter a description about the uploaded file.'),
'#weight' => 11,
];
return $element;
}
/**
* Form API callback
*
* Removes slashes from the beginning and end of the destination value and
* ensures that the file directory path is not included at the beginning of the
* value.
*
* This function is assigned as an #element_validate callback in
* fieldSettingsForm().
*/
public static function validateDirectory($element, FormStateInterface $form_state) {
// Strip slashes from the beginning and end of $element['file_directory'].
$value = trim($element['#value'], '\\/');
$form_state->setValueForElement($element, $value);
}
/**
* Form API callback.
*
* This function is assigned as an #element_validate callback in
* fieldSettingsForm().
*
* This doubles as a convenience clean-up function and a validation routine.
* Commas are allowed by the end-user, but ultimately the value will be stored
* as a space-separated list for compatibility with file_validate_extensions().
*/
public static function validateExtensions($element, FormStateInterface $form_state) {
if (!empty($element['#value'])) {
$extensions = preg_replace('/([, ]+\.?)/', ' ', trim(strtolower($element['#value'])));
$extensions = array_filter(explode(' ', $extensions));
$extensions = implode(' ', array_unique($extensions));
if (!preg_match('/^([a-z0-9]+([.][a-z0-9])* ?)+$/', $extensions)) {
$form_state->setError($element, t('The list of allowed extensions is not valid, be sure to exclude leading dots and to separate extensions with a comma or space.'));
}
else {
$form_state->setValueForElement($element, $extensions);
}
}
}
/**
* Form API callback.
*
* Ensures that a size has been entered and that it can be parsed by
* \Drupal\Component\Utility\Bytes::toInt().
*
* This function is assigned as an #element_validate callback in
* fieldSettingsForm().
*/
public static function validateMaxFilesize($element, FormStateInterface $form_state) {
if (!empty($element['#value']) && !is_numeric(Bytes::toInt($element['#value']))) {
$form_state->setError($element, t('The "@name" option must contain a valid value. You may either leave the text field empty or enter a string like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes).', ['@name' => $element['title']]));
}
}
/**
* Determines the URI for a file field.
*
* @param array $data
* An array of token objects to pass to Token::replace().
*
* @return string
* An unsanitized file directory URI with tokens replaced. The result of
* the token replacement is then converted to plain text and returned.
*
* @see \Drupal\Core\Utility\Token::replace()
*/
public function getUploadLocation($data = []) {
return static::doGetUploadLocation($this->getSettings(), $data);
}
/**
* Determines the URI for a file field.
*
* @param array $settings
* The array of field settings.
* @param array $data
* An array of token objects to pass to Token::replace().
*
* @return string
* An unsanitized file directory URI with tokens replaced. The result of
* the token replacement is then converted to plain text and returned.
*
* @see \Drupal\Core\Utility\Token::replace()
*/
protected static function doGetUploadLocation(array $settings, $data = []) {
$destination = trim($settings['file_directory'], '/');
// Replace tokens. As the tokens might contain HTML we convert it to plain
// text.
$destination = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($destination, $data));
return $settings['uri_scheme'] . '://' . $destination;
}
/**
* Retrieves the upload validators for a file field.
*
* @return array
* An array suitable for passing to file_save_upload() or the file field
* element's '#upload_validators' property.
*/
public function getUploadValidators() {
$validators = [];
$settings = $this->getSettings();
// Cap the upload size according to the PHP limit.
$max_filesize = Bytes::toInt(file_upload_max_size());
if (!empty($settings['max_filesize'])) {
$max_filesize = min($max_filesize, Bytes::toInt($settings['max_filesize']));
}
// There is always a file size limit due to the PHP server limit.
$validators['file_validate_size'] = [$max_filesize];
// Add the extension check if necessary.
if (!empty($settings['file_extensions'])) {
$validators['file_validate_extensions'] = [$settings['file_extensions']];
}
return $validators;
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
$random = new Random();
$settings = $field_definition->getSettings();
// Prepare destination.
$dirname = static::doGetUploadLocation($settings);
file_prepare_directory($dirname, FILE_CREATE_DIRECTORY);
// Generate a file entity.
$destination = $dirname . '/' . $random->name(10, TRUE) . '.txt';
$data = $random->paragraphs(3);
$file = file_save_data($data, $destination, FILE_EXISTS_ERROR);
$values = [
'target_id' => $file->id(),
'display' => (int) $settings['display_default'],
'description' => $random->sentences(10),
];
return $values;
}
/**
* Determines whether an item should be displayed when rendering the field.
*
* @return bool
* TRUE if the item should be displayed, FALSE if not.
*/
public function isDisplayed() {
if ($this->getSetting('display_field')) {
return (bool) $this->display;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public static function getPreconfiguredOptions() {
return [];
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Drupal\file\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\UriItem;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\file\ComputedFileUrl;
/**
* File-specific plugin implementation of a URI item to provide a full URL.
*
* @FieldType(
* id = "file_uri",
* label = @Translation("File URI"),
* description = @Translation("An entity field containing a file URI, and a computed root-relative file URL."),
* no_ui = TRUE,
* default_formatter = "file_uri",
* default_widget = "uri",
* )
*/
class FileUriItem extends UriItem {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties = parent::propertyDefinitions($field_definition);
$properties['url'] = DataDefinition::create('string')
->setLabel(t('Root-relative file URL'))
->setComputed(TRUE)
->setInternal(FALSE)
->setClass(ComputedFileUrl::class);
return $properties;
}
}

View file

@ -0,0 +1,599 @@
<?php
namespace Drupal\file\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\file\Element\ManagedFile;
use Drupal\file\Entity\File;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* Plugin implementation of the 'file_generic' widget.
*
* @FieldWidget(
* id = "file_generic",
* label = @Translation("File"),
* field_types = {
* "file"
* }
* )
*/
class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
/**
* {@inheritdoc}
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
$this->elementInfo = $element_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($plugin_id, $plugin_definition, $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], $container->get('element_info'));
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'progress_indicator' => 'throbber',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element['progress_indicator'] = [
'#type' => 'radios',
'#title' => t('Progress indicator'),
'#options' => [
'throbber' => t('Throbber'),
'bar' => t('Bar with progress meter'),
],
'#default_value' => $this->getSetting('progress_indicator'),
'#description' => t('The throbber display does not show the status of uploads but takes up less space. The progress bar is helpful for monitoring progress on large uploads.'),
'#weight' => 16,
'#access' => file_progress_implementation(),
];
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$summary[] = t('Progress indicator: @progress_indicator', ['@progress_indicator' => $this->getSetting('progress_indicator')]);
return $summary;
}
/**
* Overrides \Drupal\Core\Field\WidgetBase::formMultipleElements().
*
* Special handling for draggable multiple widgets and 'add more' button.
*/
protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
$field_name = $this->fieldDefinition->getName();
$parents = $form['#parents'];
// Load the items for form rebuilds from the field state as they might not
// be in $form_state->getValues() because of validation limitations. Also,
// they are only passed in as $items when editing existing entities.
$field_state = static::getWidgetState($parents, $field_name, $form_state);
if (isset($field_state['items'])) {
$items->setValue($field_state['items']);
}
// Determine the number of widgets to display.
$cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
switch ($cardinality) {
case FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED:
$max = count($items);
$is_multiple = TRUE;
break;
default:
$max = $cardinality - 1;
$is_multiple = ($cardinality > 1);
break;
}
$title = $this->fieldDefinition->getLabel();
$description = $this->getFilteredDescription();
$elements = [];
$delta = 0;
// Add an element for every existing item.
foreach ($items as $item) {
$element = [
'#title' => $title,
'#description' => $description,
];
$element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
if ($element) {
// Input field for the delta (drag-n-drop reordering).
if ($is_multiple) {
// We name the element '_weight' to avoid clashing with elements
// defined by widget.
$element['_weight'] = [
'#type' => 'weight',
'#title' => t('Weight for row @number', ['@number' => $delta + 1]),
'#title_display' => 'invisible',
// Note: this 'delta' is the FAPI #type 'weight' element's property.
'#delta' => $max,
'#default_value' => $item->_weight ?: $delta,
'#weight' => 100,
];
}
$elements[$delta] = $element;
$delta++;
}
}
$empty_single_allowed = ($cardinality == 1 && $delta == 0);
$empty_multiple_allowed = ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || $delta < $cardinality) && !$form_state->isProgrammed();
// Add one more empty row for new uploads except when this is a programmed
// multiple form as it is not necessary.
if ($empty_single_allowed || $empty_multiple_allowed) {
// Create a new empty item.
$items->appendItem();
$element = [
'#title' => $title,
'#description' => $description,
];
$element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
if ($element) {
$element['#required'] = ($element['#required'] && $delta == 0);
$elements[$delta] = $element;
}
}
if ($is_multiple) {
// The group of elements all-together need some extra functionality after
// building up the full list (like draggable table rows).
$elements['#file_upload_delta'] = $delta;
$elements['#type'] = 'details';
$elements['#open'] = TRUE;
$elements['#theme'] = 'file_widget_multiple';
$elements['#theme_wrappers'] = ['details'];
$elements['#process'] = [[get_class($this), 'processMultiple']];
$elements['#title'] = $title;
$elements['#description'] = $description;
$elements['#field_name'] = $field_name;
$elements['#language'] = $items->getLangcode();
// The field settings include defaults for the field type. However, this
// widget is a base class for other widgets (e.g., ImageWidget) that may
// act on field types without these expected settings.
$field_settings = $this->getFieldSettings() + ['display_field' => NULL];
$elements['#display_field'] = (bool) $field_settings['display_field'];
// Add some properties that will eventually be added to the file upload
// field. These are added here so that they may be referenced easily
// through a hook_form_alter().
$elements['#file_upload_title'] = t('Add a new file');
$elements['#file_upload_description'] = [
'#theme' => 'file_upload_help',
'#description' => '',
'#upload_validators' => $elements[0]['#upload_validators'],
'#cardinality' => $cardinality,
];
}
return $elements;
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$field_settings = $this->getFieldSettings();
// The field settings include defaults for the field type. However, this
// widget is a base class for other widgets (e.g., ImageWidget) that may act
// on field types without these expected settings.
$field_settings += [
'display_default' => NULL,
'display_field' => NULL,
'description_field' => NULL,
];
$cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
$defaults = [
'fids' => [],
'display' => (bool) $field_settings['display_default'],
'description' => '',
];
// Essentially we use the managed_file type, extended with some
// enhancements.
$element_info = $this->elementInfo->getInfo('managed_file');
$element += [
'#type' => 'managed_file',
'#upload_location' => $items[$delta]->getUploadLocation(),
'#upload_validators' => $items[$delta]->getUploadValidators(),
'#value_callback' => [get_class($this), 'value'],
'#process' => array_merge($element_info['#process'], [[get_class($this), 'process']]),
'#progress_indicator' => $this->getSetting('progress_indicator'),
// Allows this field to return an array instead of a single value.
'#extended' => TRUE,
// Add properties needed by value() and process() methods.
'#field_name' => $this->fieldDefinition->getName(),
'#entity_type' => $items->getEntity()->getEntityTypeId(),
'#display_field' => (bool) $field_settings['display_field'],
'#display_default' => $field_settings['display_default'],
'#description_field' => $field_settings['description_field'],
'#cardinality' => $cardinality,
];
$element['#weight'] = $delta;
// Field stores FID value in a single mode, so we need to transform it for
// form element to recognize it correctly.
if (!isset($items[$delta]->fids) && isset($items[$delta]->target_id)) {
$items[$delta]->fids = [$items[$delta]->target_id];
}
$element['#default_value'] = $items[$delta]->getValue() + $defaults;
$default_fids = $element['#extended'] ? $element['#default_value']['fids'] : $element['#default_value'];
if (empty($default_fids)) {
$file_upload_help = [
'#theme' => 'file_upload_help',
'#description' => $element['#description'],
'#upload_validators' => $element['#upload_validators'],
'#cardinality' => $cardinality,
];
$element['#description'] = \Drupal::service('renderer')->renderPlain($file_upload_help);
$element['#multiple'] = $cardinality != 1 ? TRUE : FALSE;
if ($cardinality != 1 && $cardinality != -1) {
$element['#element_validate'] = [[get_class($this), 'validateMultipleCount']];
}
}
return $element;
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
// Since file upload widget now supports uploads of more than one file at a
// time it always returns an array of fids. We have to translate this to a
// single fid, as field expects single value.
$new_values = [];
foreach ($values as &$value) {
foreach ($value['fids'] as $fid) {
$new_value = $value;
$new_value['target_id'] = $fid;
unset($new_value['fids']);
$new_values[] = $new_value;
}
}
return $new_values;
}
/**
* {@inheritdoc}
*/
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
parent::extractFormValues($items, $form, $form_state);
// Update reference to 'items' stored during upload to take into account
// changes to values like 'alt' etc.
// @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::submit()
$field_name = $this->fieldDefinition->getName();
$field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
$field_state['items'] = $items->getValue();
static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
}
/**
* Form API callback. Retrieves the value for the file_generic field element.
*
* This method is assigned as a #value_callback in formElement() method.
*/
public static function value($element, $input, FormStateInterface $form_state) {
if ($input) {
// Checkboxes lose their value when empty.
// If the display field is present make sure its unchecked value is saved.
if (empty($input['display'])) {
$input['display'] = $element['#display_field'] ? 0 : 1;
}
}
// We depend on the managed file element to handle uploads.
$return = ManagedFile::valueCallback($element, $input, $form_state);
// Ensure that all the required properties are returned even if empty.
$return += [
'fids' => [],
'display' => 1,
'description' => '',
];
return $return;
}
/**
* Form element validation callback for upload element on file widget. Checks
* if user has uploaded more files than allowed.
*
* This validator is used only when cardinality not set to 1 or unlimited.
*/
public static function validateMultipleCount($element, FormStateInterface $form_state, $form) {
$values = NestedArray::getValue($form_state->getValues(), $element['#parents']);
$array_parents = $element['#array_parents'];
array_pop($array_parents);
$previously_uploaded_count = count(Element::children(NestedArray::getValue($form, $array_parents))) - 1;
$field_storage_definitions = \Drupal::entityManager()->getFieldStorageDefinitions($element['#entity_type']);
$field_storage = $field_storage_definitions[$element['#field_name']];
$newly_uploaded_count = count($values['fids']);
$total_uploaded_count = $newly_uploaded_count + $previously_uploaded_count;
if ($total_uploaded_count > $field_storage->getCardinality()) {
$keep = $newly_uploaded_count - $total_uploaded_count + $field_storage->getCardinality();
$removed_files = array_slice($values['fids'], $keep);
$removed_names = [];
foreach ($removed_files as $fid) {
$file = File::load($fid);
$removed_names[] = $file->getFilename();
}
$args = [
'%field' => $field_storage->getName(),
'@max' => $field_storage->getCardinality(),
'@count' => $total_uploaded_count,
'%list' => implode(', ', $removed_names),
];
$message = t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args);
\Drupal::messenger()->addWarning($message);
$values['fids'] = array_slice($values['fids'], 0, $keep);
NestedArray::setValue($form_state->getValues(), $element['#parents'], $values);
}
}
/**
* Form API callback: Processes a file_generic field element.
*
* Expands the file_generic type to include the description and display
* fields.
*
* This method is assigned as a #process callback in formElement() method.
*/
public static function process($element, FormStateInterface $form_state, $form) {
$item = $element['#value'];
$item['fids'] = $element['fids']['#value'];
// Add the display field if enabled.
if ($element['#display_field']) {
$element['display'] = [
'#type' => empty($item['fids']) ? 'hidden' : 'checkbox',
'#title' => t('Include file in display'),
'#attributes' => ['class' => ['file-display']],
];
if (isset($item['display'])) {
$element['display']['#value'] = $item['display'] ? '1' : '';
}
else {
$element['display']['#value'] = $element['#display_default'];
}
}
else {
$element['display'] = [
'#type' => 'hidden',
'#value' => '1',
];
}
// Add the description field if enabled.
if ($element['#description_field'] && $item['fids']) {
$config = \Drupal::config('file.settings');
$element['description'] = [
'#type' => $config->get('description.type'),
'#title' => t('Description'),
'#value' => isset($item['description']) ? $item['description'] : '',
'#maxlength' => $config->get('description.length'),
'#description' => t('The description may be used as the label of the link to the file.'),
];
}
// Adjust the Ajax settings so that on upload and remove of any individual
// file, the entire group of file fields is updated together.
if ($element['#cardinality'] != 1) {
$parents = array_slice($element['#array_parents'], 0, -1);
$new_options = [
'query' => [
'element_parents' => implode('/', $parents),
],
];
$field_element = NestedArray::getValue($form, $parents);
$new_wrapper = $field_element['#id'] . '-ajax-wrapper';
foreach (Element::children($element) as $key) {
if (isset($element[$key]['#ajax'])) {
$element[$key]['#ajax']['options'] = $new_options;
$element[$key]['#ajax']['wrapper'] = $new_wrapper;
}
}
unset($element['#prefix'], $element['#suffix']);
}
// Add another submit handler to the upload and remove buttons, to implement
// functionality needed by the field widget. This submit handler, along with
// the rebuild logic in file_field_widget_form() requires the entire field,
// not just the individual item, to be valid.
foreach (['upload_button', 'remove_button'] as $key) {
$element[$key]['#submit'][] = [get_called_class(), 'submit'];
$element[$key]['#limit_validation_errors'] = [array_slice($element['#parents'], 0, -1)];
}
return $element;
}
/**
* Form API callback: Processes a group of file_generic field elements.
*
* Adds the weight field to each row so it can be ordered and adds a new Ajax
* wrapper around the entire group so it can be replaced all at once.
*
* This method on is assigned as a #process callback in formMultipleElements()
* method.
*/
public static function processMultiple($element, FormStateInterface $form_state, $form) {
$element_children = Element::children($element, TRUE);
$count = count($element_children);
// Count the number of already uploaded files, in order to display new
// items in \Drupal\file\Element\ManagedFile::uploadAjaxCallback().
if (!$form_state->isRebuilding()) {
$count_items_before = 0;
foreach ($element_children as $children) {
if (!empty($element[$children]['#default_value']['fids'])) {
$count_items_before++;
}
}
$form_state->set('file_upload_delta_initial', $count_items_before);
}
foreach ($element_children as $delta => $key) {
if ($key != $element['#file_upload_delta']) {
$description = static::getDescriptionFromElement($element[$key]);
$element[$key]['_weight'] = [
'#type' => 'weight',
'#title' => $description ? t('Weight for @title', ['@title' => $description]) : t('Weight for new file'),
'#title_display' => 'invisible',
'#delta' => $count,
'#default_value' => $delta,
];
}
else {
// The title needs to be assigned to the upload field so that validation
// errors include the correct widget label.
$element[$key]['#title'] = $element['#title'];
$element[$key]['_weight'] = [
'#type' => 'hidden',
'#default_value' => $delta,
];
}
}
// Add a new wrapper around all the elements for Ajax replacement.
$element['#prefix'] = '<div id="' . $element['#id'] . '-ajax-wrapper">';
$element['#suffix'] = '</div>';
return $element;
}
/**
* Retrieves the file description from a field field element.
*
* This helper static method is used by processMultiple() method.
*
* @param array $element
* An associative array with the element being processed.
*
* @return array|false
* A description of the file suitable for use in the administrative
* interface.
*/
protected static function getDescriptionFromElement($element) {
// Use the actual file description, if it's available.
if (!empty($element['#default_value']['description'])) {
return $element['#default_value']['description'];
}
// Otherwise, fall back to the filename.
if (!empty($element['#default_value']['filename'])) {
return $element['#default_value']['filename'];
}
// This is probably a newly uploaded file; no description is available.
return FALSE;
}
/**
* Form submission handler for upload/remove button of formElement().
*
* This runs in addition to and after file_managed_file_submit().
*
* @see file_managed_file_submit()
*/
public static function submit($form, FormStateInterface $form_state) {
// During the form rebuild, formElement() will create field item widget
// elements using re-indexed deltas, so clear out FormState::$input to
// avoid a mismatch between old and new deltas. The rebuilt elements will
// have #default_value set appropriately for the current state of the field,
// so nothing is lost in doing this.
$button = $form_state->getTriggeringElement();
$parents = array_slice($button['#parents'], 0, -2);
NestedArray::setValue($form_state->getUserInput(), $parents, NULL);
// Go one level up in the form, to the widgets container.
$element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
$field_name = $element['#field_name'];
$parents = $element['#field_parents'];
$submitted_values = NestedArray::getValue($form_state->getValues(), array_slice($button['#parents'], 0, -2));
foreach ($submitted_values as $delta => $submitted_value) {
if (empty($submitted_value['fids'])) {
unset($submitted_values[$delta]);
}
}
// If there are more files uploaded via the same widget, we have to separate
// them, as we display each file in its own widget.
$new_values = [];
foreach ($submitted_values as $delta => $submitted_value) {
if (is_array($submitted_value['fids'])) {
foreach ($submitted_value['fids'] as $fid) {
$new_value = $submitted_value;
$new_value['fids'] = [$fid];
$new_values[] = $new_value;
}
}
else {
$new_value = $submitted_value;
}
}
// Re-index deltas after removing empty items.
$submitted_values = array_values($new_values);
// Update form_state values.
NestedArray::setValue($form_state->getValues(), array_slice($button['#parents'], 0, -2), $submitted_values);
// Update items.
$field_state = static::getWidgetState($parents, $field_name, $form_state);
$field_state['items'] = $submitted_values;
static::setWidgetState($parents, $field_name, $form_state, $field_state);
}
/**
* {@inheritdoc}
*/
public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
// Never flag validation errors for the remove button.
$clicked_button = end($form_state->getTriggeringElement()['#parents']);
if ($clicked_button !== 'remove_button') {
parent::flagErrors($items, $violations, $form, $form_state);
}
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Drupal\file\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Supports validating file URIs.
*
* @Constraint(
* id = "FileUriUnique",
* label = @Translation("File URI", context = "Validation")
* )
*/
class FileUriUnique extends Constraint {
public $message = 'The file %value already exists. Enter a unique file URI.';
/**
* {@inheritdoc}
*/
public function validatedBy() {
return '\Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldValueValidator';
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Drupal\file\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Validation File constraint.
*
* @Constraint(
* id = "FileValidation",
* label = @Translation("File Validation", context = "Validation")
* )
*/
class FileValidationConstraint extends Constraint {
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\file\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Checks that a file referenced in a file field is valid.
*/
class FileValidationConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
// Get the file to execute validators.
$target = $value->get('entity')->getTarget();
if (!$target) {
return;
}
$file = $target->getValue();
// Get the validators.
$validators = $value->getUploadValidators();
// Checks that a file meets the criteria specified by the validators.
if ($errors = file_validate($file, $validators)) {
foreach ($errors as $error) {
$this->context->addViolation($error);
}
}
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Drupal\file\Plugin\migrate\cckfield\d6;
@trigger_error('FileField is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\file\Plugin\migrate\field\d6\FileField instead.', E_USER_DEPRECATED);
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\cckfield\CckFieldPluginBase;
/**
* @MigrateCckField(
* id = "filefield",
* core = {6},
* source_module = "filefield",
* destination_module = "file"
* )
*
* @deprecated in Drupal 8.3.x, to be removed before Drupal 9.0.x. Use
* \Drupal\file\Plugin\migrate\field\d6\FileField instead.
*
* @see https://www.drupal.org/node/2751897
*/
class FileField extends CckFieldPluginBase {
/**
* {@inheritdoc}
*/
public function getFieldWidgetMap() {
return [
'filefield_widget' => 'file_generic',
];
}
/**
* {@inheritdoc}
*/
public function getFieldFormatterMap() {
return [
'default' => 'file_default',
'url_plain' => 'file_url_plain',
'path_plain' => 'file_url_plain',
'image_plain' => 'image',
'image_nodelink' => 'image',
'image_imagelink' => 'image',
];
}
/**
* {@inheritdoc}
*/
public function processCckFieldValues(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'd6_cck_file',
'source' => $field_name,
];
$migration->mergeProcessOfProperty($field_name, $process);
}
/**
* {@inheritdoc}
*/
public function getFieldType(Row $row) {
return $row->getSourceProperty('widget_type') == 'imagefield_widget' ? 'image' : 'file';
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Drupal\file\Plugin\migrate\cckfield\d7;
@trigger_error('FileField is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\file\Plugin\migrate\field\d7\FileField instead.', E_USER_DEPRECATED);
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\cckfield\CckFieldPluginBase;
/**
* @MigrateCckField(
* id = "file",
* core = {7},
* source_module = "file",
* destination_module = "file"
* )
*
* @deprecated in Drupal 8.3.x, to be removed before Drupal 9.0.x. Use
* \Drupal\file\Plugin\migrate\field\d7\FileField instead.
*
* @see https://www.drupal.org/node/2751897
*/
class FileField extends CckFieldPluginBase {
/**
* {@inheritdoc}
*/
public function getFieldWidgetMap() {
return [
'filefield_widget' => 'file_generic',
];
}
/**
* {@inheritdoc}
*/
public function getFieldFormatterMap() {
return [
'default' => 'file_default',
'url_plain' => 'file_url_plain',
'path_plain' => 'file_url_plain',
'image_plain' => 'image',
'image_nodelink' => 'image',
'image_imagelink' => 'image',
];
}
/**
* {@inheritdoc}
*/
public function processCckFieldValues(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'sub_process',
'source' => $field_name,
'process' => [
'target_id' => 'fid',
'display' => 'display',
'description' => 'description',
],
];
$migration->mergeProcessOfProperty($field_name, $process);
}
/**
* {@inheritdoc}
*/
public function getFieldType(Row $row) {
return $row->getSourceProperty('widget_type') == 'imagefield_widget' ? 'image' : 'file';
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Drupal\file\Plugin\migrate\cckfield\d7;
@trigger_error('ImageField is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\image\Plugin\migrate\field\d7\ImageField instead. See https://www.drupal.org/node/2936061.', E_USER_DEPRECATED);
use Drupal\image\Plugin\migrate\cckfield\d7\ImageField as LegacyImageField;
/**
* CCK plugin for image fields.
*
* @deprecated in Drupal 8.3.x, to be removed before Drupal 9.0.x. Use
* \Drupal\image\Plugin\migrate\field\d7\ImageField instead.
*
* @see https://www.drupal.org/node/2936061
*/
class ImageField extends LegacyImageField {}

View file

@ -0,0 +1,68 @@
<?php
namespace Drupal\file\Plugin\migrate\destination;
use Drupal\Core\Field\Plugin\Field\FieldType\UriItem;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
/**
* @MigrateDestination(
* id = "entity:file"
* )
*/
class EntityFile extends EntityContentBase {
/**
* {@inheritdoc}
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
// For stub rows, there is no real file to deal with, let the stubbing
// process take its default path.
if ($row->isStub()) {
return parent::getEntity($row, $old_destination_id_values);
}
// By default the entity key (fid) would be used, but we want to make sure
// we're loading the matching URI.
$destination = $row->getDestinationProperty('uri');
if (empty($destination)) {
throw new MigrateException('Destination property uri not provided');
}
$entity = $this->storage->loadByProperties(['uri' => $destination]);
if ($entity) {
return reset($entity);
}
else {
return parent::getEntity($row, $old_destination_id_values);
}
}
/**
* {@inheritdoc}
*/
protected function processStubRow(Row $row) {
// We stub the uri value ourselves so we can create a real stub file for it.
if (!$row->getDestinationProperty('uri')) {
$field_definitions = $this->entityManager
->getFieldDefinitions($this->storage->getEntityTypeId(),
$this->getKey('bundle'));
$value = UriItem::generateSampleValue($field_definitions['uri']);
if (empty($value)) {
throw new MigrateException('Stubbing failed, unable to generate value for field uri');
}
// generateSampleValue() wraps the value in an array.
$value = reset($value);
// Make it into a proper public file uri, stripping off the existing
// scheme if present.
$value = 'public://' . preg_replace('|^[a-z]+://|i', '', $value);
$value = mb_substr($value, 0, $field_definitions['uri']->getSetting('max_length'));
// Create a real file, so File::preSave() can do filesize() on it.
touch($value);
$row->setDestinationProperty('uri', $value);
}
parent::processStubRow($row);
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\file\Plugin\migrate\field\d6;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
/**
* @MigrateField(
* id = "filefield",
* core = {6},
* source_module = "filefield",
* destination_module = "file"
* )
*/
class FileField extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function getFieldWidgetMap() {
return [
'filefield_widget' => 'file_generic',
];
}
/**
* {@inheritdoc}
*/
public function getFieldFormatterMap() {
return [
'default' => 'file_default',
'url_plain' => 'file_url_plain',
'path_plain' => 'file_url_plain',
'image_plain' => 'image',
'image_nodelink' => 'image',
'image_imagelink' => 'image',
];
}
/**
* {@inheritdoc}
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'd6_field_file',
'source' => $field_name,
];
$migration->mergeProcessOfProperty($field_name, $process);
}
/**
* {@inheritdoc}
*/
public function getFieldType(Row $row) {
return $row->getSourceProperty('widget_type') == 'imagefield_widget' ? 'image' : 'file';
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Drupal\file\Plugin\migrate\field\d6;
@trigger_error('ImageField is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.x. Use \Drupal\image\Plugin\migrate\field\d6\ImageField instead. See https://www.drupal.org/node/2936061.', E_USER_DEPRECATED);
use Drupal\image\Plugin\migrate\field\d6\ImageField as NonLegacyImageField;
/**
* Field plugin for image fields.
*
* @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.x. Use
* \Drupal\image\Plugin\migrate\field\d6\ImageField instead.
*
* @see https://www.drupal.org/node/2936061
*/
class ImageField extends NonLegacyImageField {}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\file\Plugin\migrate\field\d7;
use Drupal\file\Plugin\migrate\field\d6\FileField as D6FileField;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* @MigrateField(
* id = "file",
* core = {7},
* source_module = "file",
* destination_module = "file"
* )
*/
class FileField extends D6FileField {
/**
* {@inheritdoc}
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'sub_process',
'source' => $field_name,
'process' => [
'target_id' => 'fid',
'display' => 'display',
'description' => 'description',
],
];
$migration->mergeProcessOfProperty($field_name, $process);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Drupal\file\Plugin\migrate\field\d7;
@trigger_error('ImageField is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.x. Use \Drupal\image\Plugin\migrate\field\d7\ImageField instead. See https://www.drupal.org/node/2936061.', E_USER_DEPRECATED);
use Drupal\image\Plugin\migrate\field\d7\ImageField as NonLegacyImageField;
/**
* Field plugin for image fields.
*
* @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.x. Use
* \Drupal\image\Plugin\migrate\field\d7\ImageField instead.
*
* @see https://www.drupal.org/node/2936061
*/
class ImageField extends NonLegacyImageField {}

View file

@ -0,0 +1,17 @@
<?php
namespace Drupal\file\Plugin\migrate\process\d6;
@trigger_error('CckFile is deprecated in Drupal 8.3.x and will be be removed before Drupal 9.0.x. Use \Drupal\file\Plugin\migrate\process\d6\FieldFile instead.', E_USER_DEPRECATED);
/**
* @MigrateProcessPlugin(
* id = "d6_cck_file"
* )
*
* @deprecated in Drupal 8.3.x, to be removed before Drupal 9.0.x. Use
* \Drupal\file\Plugin\migrate\process\d6\FieldFile instead.
*
* @see https://www.drupal.org/node/2751897
*/
class CckFile extends FieldFile {}

View file

@ -0,0 +1,92 @@
<?php
namespace Drupal\file\Plugin\migrate\process\d6;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @MigrateProcessPlugin(
* id = "d6_field_file"
* )
*/
class FieldFile extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The migration process plugin, configured for lookups in d6_file.
*
* @var \Drupal\migrate\Plugin\MigrateProcessInterface
*/
protected $migrationPlugin;
/**
* Constructs a FieldFile plugin instance.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The current migration.
* @param \Drupal\migrate\Plugin\MigrateProcessInterface $migration_plugin
* An instance of the 'migration' process plugin.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, MigrateProcessInterface $migration_plugin) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
$this->migrationPlugin = $migration_plugin;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
// Configure the migration process plugin to look up migrated IDs from
// a d6 file migration.
$migration_plugin_configuration = $configuration + [
'migration' => 'd6_file',
'source' => ['fid'],
];
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('plugin.manager.migrate.process')->createInstance('migration', $migration_plugin_configuration, $migration)
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$options = unserialize($value['data']);
// Try to look up the ID of the migrated file. If one cannot be found, it
// means the file referenced by the current field item did not migrate for
// some reason -- file migration is notoriously brittle -- and we do NOT
// want to send invalid file references into the field system (it causes
// fatals), so return an empty item instead.
if ($fid = $this->migrationPlugin->transform($value['fid'], $migrate_executable, $row, $destination_property)) {
return [
'target_id' => $fid,
'display' => $value['list'],
'description' => isset($options['description']) ? $options['description'] : '',
'alt' => isset($options['alt']) ? $options['alt'] : '',
'title' => isset($options['title']) ? $options['title'] : '',
];
}
else {
return [];
}
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Drupal\file\Plugin\migrate\process\d6;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Process the file url into a D8 compatible URL.
*
* @MigrateProcessPlugin(
* id = "file_uri"
* )
*/
class FileUri extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// If we're stubbing a file entity, return a uri of NULL so it will get
// stubbed by the general process.
if ($row->isStub()) {
return NULL;
}
list($filepath, $file_directory_path, $temp_directory_path, $is_public) = $value;
// Specific handling using $temp_directory_path for temporary files.
if (substr($filepath, 0, strlen($temp_directory_path)) === $temp_directory_path) {
$uri = preg_replace('/^' . preg_quote($temp_directory_path, '/') . '/', '', $filepath);
return 'temporary://' . ltrim($uri, '/');
}
// Strip the files path from the uri instead of using basename
// so any additional folders in the path are preserved.
$uri = preg_replace('/^' . preg_quote($file_directory_path, '/') . '/', '', $filepath);
return ($is_public ? 'public' : 'private') . '://' . ltrim($uri, '/');
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace Drupal\file\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 file source from database.
*
* @MigrateSource(
* id = "d6_file",
* source_module = "system"
* )
*/
class File extends DrupalSqlBase {
/**
* The file directory path.
*
* @var string
*/
protected $filePath;
/**
* The temporary file path.
*
* @var string
*/
protected $tempFilePath;
/**
* Flag for private or public file storage.
*
* @var bool
*/
protected $isPublic;
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('files', 'f')
->fields('f')
->condition('filepath', '/tmp%', 'NOT LIKE')
->orderBy('timestamp')
// If two or more files have the same timestamp, they'll end up in a
// non-deterministic order. Ordering by fid (or any other unique field)
// will prevent this.
->orderBy('f.fid');
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$site_path = isset($this->configuration['site_path']) ? $this->configuration['site_path'] : 'sites/default';
$this->filePath = $this->variableGet('file_directory_path', $site_path . '/files') . '/';
$this->tempFilePath = $this->variableGet('file_directory_temp', '/tmp') . '/';
// FILE_DOWNLOADS_PUBLIC == 1 and FILE_DOWNLOADS_PRIVATE == 2.
$this->isPublic = $this->variableGet('file_downloads', 1) == 1;
return parent::initializeIterator();
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$row->setSourceProperty('file_directory_path', $this->filePath);
$row->setSourceProperty('temp_directory_path', $this->tempFilePath);
$row->setSourceProperty('is_public', $this->isPublic);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'fid' => $this->t('File ID'),
'uid' => $this->t('The {users}.uid who added the file. If set to 0, this file was added by an anonymous user.'),
'filename' => $this->t('File name'),
'filepath' => $this->t('File path'),
'filemime' => $this->t('File MIME Type'),
'status' => $this->t('The published status of a file.'),
'timestamp' => $this->t('The time that the file was added.'),
'file_directory_path' => $this->t('The Drupal files path.'),
'is_public' => $this->t('TRUE if the files directory is public otherwise FALSE.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['fid']['type'] = 'integer';
return $ids;
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace Drupal\file\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 upload source from database.
*
* @MigrateSource(
* id = "d6_upload",
* source_module = "upload"
* )
*/
class Upload extends DrupalSqlBase {
/**
* The join options between the node and the upload table.
*/
const JOIN = 'n.nid = u.nid AND n.vid = u.vid';
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('upload', 'u')
->distinct()
->fields('u', ['nid', 'vid']);
$query->innerJoin('node', 'n', static::JOIN);
$query->addField('n', 'type');
$query->addField('n', 'language');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$query = $this->select('upload', 'u')
->fields('u', ['fid', 'description', 'list'])
->condition('u.nid', $row->getSourceProperty('nid'))
->orderBy('u.weight');
$query->innerJoin('node', 'n', static::JOIN);
$row->setSourceProperty('upload', $query->execute()->fetchAll());
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'fid' => $this->t('The file Id.'),
'nid' => $this->t('The node Id.'),
'vid' => $this->t('The version Id.'),
'type' => $this->t('The node type'),
'language' => $this->t('The node language.'),
'description' => $this->t('The file description.'),
'list' => $this->t('Whether the list should be visible on the node page.'),
'weight' => $this->t('The file weight.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
$ids['vid']['alias'] = 'u';
return $ids;
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace Drupal\file\Plugin\migrate\source\d6;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Plugin\migrate\source\DummyQueryTrait;
/**
* Drupal 6 upload instance source from database.
*
* @MigrateSource(
* id = "d6_upload_instance",
* source_module = "upload"
* )
*/
class UploadInstance extends DrupalSqlBase {
use DummyQueryTrait;
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$node_types = $this->select('node_type', 'nt')
->fields('nt', ['type'])
->execute()
->fetchCol();
$variables = array_map(function ($type) {
return 'upload_' . $type;
}, $node_types);
$max_filesize = $this->variableGet('upload_uploadsize_default', 1);
$max_filesize = $max_filesize ? $max_filesize . 'MB' : '';
$file_extensions = $this->variableGet('upload_extensions_default', 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp');
$return = [];
$values = $this->select('variable', 'v')
->fields('v', ['name', 'value'])
->condition('v.name', $variables, 'IN')
->execute()
->fetchAllKeyed();
foreach ($node_types as $node_type) {
$name = 'upload_' . $node_type;
// By default, file attachments in D6 are enabled unless upload_<type> is
// false, so include types where the upload-variable is not set.
$enabled = !isset($values[$name]) || unserialize($values[$name]);
if ($enabled) {
$return[$node_type]['node_type'] = $node_type;
$return[$node_type]['max_filesize'] = $max_filesize;
$return[$node_type]['file_extensions'] = $file_extensions;
}
}
return new \ArrayIterator($return);
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'node_type' => [
'type' => 'string',
],
];
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'node_type' => $this->t('Node type'),
'max_filesize' => $this->t('Max filesize'),
'file_extensions' => $this->t('File extensions'),
];
}
/**
* {@inheritdoc}
*/
public function count($refresh = FALSE) {
return count($this->initializeIterator());
}
}

View file

@ -0,0 +1,119 @@
<?php
namespace Drupal\file\Plugin\migrate\source\d7;
use Drupal\Core\Database\Query\Condition;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 file source from database.
*
* @MigrateSource(
* id = "d7_file",
* source_module = "file"
* )
*/
class File extends DrupalSqlBase {
/**
* The public file directory path.
*
* @var string
*/
protected $publicPath;
/**
* The private file directory path, if any.
*
* @var string
*/
protected $privatePath;
/**
* The temporary file directory path.
*
* @var string
*/
protected $temporaryPath;
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('file_managed', 'f')
->fields('f')
->condition('uri', 'temporary://%', 'NOT LIKE')
->orderBy('f.timestamp');
// Filter by scheme(s), if configured.
if (isset($this->configuration['scheme'])) {
$schemes = [];
// Remove 'temporary' scheme.
$valid_schemes = array_diff((array) $this->configuration['scheme'], ['temporary']);
// Accept either a single scheme, or a list.
foreach ((array) $valid_schemes as $scheme) {
$schemes[] = rtrim($scheme) . '://';
}
$schemes = array_map([$this->getDatabase(), 'escapeLike'], $schemes);
// Add conditions, uri LIKE 'public://%' OR uri LIKE 'private://%'.
$conditions = new Condition('OR');
foreach ($schemes as $scheme) {
$conditions->condition('uri', $scheme . '%', 'LIKE');
}
$query->condition($conditions);
}
return $query;
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$this->publicPath = $this->variableGet('file_public_path', 'sites/default/files');
$this->privatePath = $this->variableGet('file_private_path', NULL);
$this->temporaryPath = $this->variableGet('file_temporary_path', '/tmp');
return parent::initializeIterator();
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Compute the filepath property, which is a physical representation of
// the URI relative to the Drupal root.
$path = str_replace(['public:/', 'private:/', 'temporary:/'], [$this->publicPath, $this->privatePath, $this->temporaryPath], $row->getSourceProperty('uri'));
// At this point, $path could be an absolute path or a relative path,
// depending on how the scheme's variable was set. So we need to shear out
// the source_base_path in order to make them all relative.
$path = str_replace($this->configuration['constants']['source_base_path'], NULL, $path);
$row->setSourceProperty('filepath', $path);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'fid' => $this->t('File ID'),
'uid' => $this->t('The {users}.uid who added the file. If set to 0, this file was added by an anonymous user.'),
'filename' => $this->t('File name'),
'filepath' => $this->t('File path'),
'filemime' => $this->t('File MIME Type'),
'status' => $this->t('The published status of a file.'),
'timestamp' => $this->t('The time that the file was added.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['fid']['type'] = 'integer';
return $ids;
}
}

View file

@ -0,0 +1,586 @@
<?php
namespace Drupal\file\Plugin\rest\resource;
use Drupal\Component\Utility\Bytes;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Config\Config;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Utility\Token;
use Drupal\file\FileInterface;
use Drupal\rest\ModifiedResourceResponse;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\file\Entity\File;
use Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait;
use Drupal\rest\RequestHandler;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* File upload resource.
*
* This is implemented as a field-level resource for the following reasons:
* - Validation for uploaded files is tied to fields (allowed extensions, max
* size, etc..).
* - The actual files do not need to be stored in another temporary location,
* to be later moved when they are referenced from a file field.
* - Permission to upload a file can be determined by a users field level
* create access to the file field.
*
* @RestResource(
* id = "file:upload",
* label = @Translation("File Upload"),
* serialization_class = "Drupal\file\Entity\File",
* uri_paths = {
* "https://www.drupal.org/link-relations/create" = "/file/upload/{entity_type_id}/{bundle}/{field_name}"
* }
* )
*/
class FileUploadResource extends ResourceBase {
use EntityResourceValidationTrait {
validate as resourceValidate;
}
/**
* The regex used to extract the filename from the content disposition header.
*
* @var string
*/
const REQUEST_HEADER_FILENAME_REGEX = '@\bfilename(?<star>\*?)=\"(?<filename>.+)\"@';
/**
* The amount of bytes to read in each iteration when streaming file data.
*
* @var int
*/
const BYTES_TO_READ = 8192;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The currently authenticated user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The MIME type guesser.
*
* @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface
*/
protected $mimeTypeGuesser;
/**
* The token replacement instance.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* The lock service.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $systemFileConfig;
/**
* Constructs a FileUploadResource instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param array $serializer_formats
* The available serialization formats.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The currently authenticated user.
* @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser
* The MIME type guesser.
* @param \Drupal\Core\Utility\Token $token
* The token replacement instance.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock service.
* @param \Drupal\Core\Config\Config $system_file_config
* The system file configuration.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, $serializer_formats, LoggerInterface $logger, FileSystemInterface $file_system, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, AccountInterface $current_user, MimeTypeGuesserInterface $mime_type_guesser, Token $token, LockBackendInterface $lock, Config $system_file_config) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
$this->fileSystem = $file_system;
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->currentUser = $current_user;
$this->mimeTypeGuesser = $mime_type_guesser;
$this->token = $token;
$this->lock = $lock;
$this->systemFileConfig = $system_file_config;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest'),
$container->get('file_system'),
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('current_user'),
$container->get('file.mime_type.guesser'),
$container->get('token'),
$container->get('lock'),
$container->get('config.factory')->get('system.file')
);
}
/**
* {@inheritdoc}
*/
public function permissions() {
// Access to this resource depends on field-level access so no explicit
// permissions are required.
// @see \Drupal\file\Plugin\rest\resource\FileUploadResource::validateAndLoadFieldDefinition()
// @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
return [];
}
/**
* Creates a file from an endpoint.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The entity bundle. This will be the same as $entity_type_id for entity
* types that don't support bundles.
* @param string $field_name
* The field name.
*
* @return \Drupal\rest\ModifiedResourceResponse
* A 201 response, on success.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* Thrown when temporary files cannot be written, a lock cannot be acquired,
* or when temporary files cannot be moved to their new location.
*/
public function post(Request $request, $entity_type_id, $bundle, $field_name) {
$filename = $this->validateAndParseContentDispositionHeader($request);
$field_definition = $this->validateAndLoadFieldDefinition($entity_type_id, $bundle, $field_name);
$destination = $this->getUploadLocation($field_definition->getSettings());
// Check the destination file path is writable.
if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
throw new HttpException(500, 'Destination file path is not writable');
}
$validators = $this->getUploadValidators($field_definition);
$prepared_filename = $this->prepareFilename($filename, $validators);
// Create the file.
$file_uri = "{$destination}/{$prepared_filename}";
$temp_file_path = $this->streamUploadData();
// This will take care of altering $file_uri if a file already exists.
file_unmanaged_prepare($temp_file_path, $file_uri);
// Lock based on the prepared file URI.
$lock_id = $this->generateLockIdFromFileUri($file_uri);
if (!$this->lock->acquire($lock_id)) {
throw new HttpException(503, sprintf('File "%s" is already locked for writing'), NULL, ['Retry-After' => 1]);
}
// Begin building file entity.
$file = File::create([]);
$file->setOwnerId($this->currentUser->id());
$file->setFilename($prepared_filename);
$file->setMimeType($this->mimeTypeGuesser->guess($prepared_filename));
$file->setFileUri($file_uri);
// Set the size. This is done in File::preSave() but we validate the file
// before it is saved.
$file->setSize(@filesize($temp_file_path));
// Validate the file entity against entity-level validation and field-level
// validators.
$this->validate($file, $validators);
// Move the file to the correct location after validation. Use
// FILE_EXISTS_ERROR as the file location has already been determined above
// in file_unmanaged_prepare().
if (!file_unmanaged_move($temp_file_path, $file_uri, FILE_EXISTS_ERROR)) {
throw new HttpException(500, 'Temporary file could not be moved to file location');
}
$file->save();
$this->lock->release($lock_id);
// 201 Created responses return the newly created entity in the response
// body. These responses are not cacheable, so we add no cacheability
// metadata here.
return new ModifiedResourceResponse($file, 201);
}
/**
* Streams file upload data to temporary file and moves to file destination.
*
* @return string
* The temp file path.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* Thrown when input data cannot be read, the temporary file cannot be
* opened, or the temporary file cannot be written.
*/
protected function streamUploadData() {
// 'rb' is needed so reading works correctly on Windows environments too.
$file_data = fopen('php://input', 'rb');
$temp_file_path = $this->fileSystem->tempnam('temporary://', 'file');
$temp_file = fopen($temp_file_path, 'wb');
if ($temp_file) {
while (!feof($file_data)) {
$read = fread($file_data, static::BYTES_TO_READ);
if ($read === FALSE) {
// Close the file streams.
fclose($temp_file);
fclose($file_data);
$this->logger->error('Input data could not be read');
throw new HttpException(500, 'Input file data could not be read');
}
if (fwrite($temp_file, $read) === FALSE) {
// Close the file streams.
fclose($temp_file);
fclose($file_data);
$this->logger->error('Temporary file data for "%path" could not be written', ['%path' => $temp_file_path]);
throw new HttpException(500, 'Temporary file data could not be written');
}
}
// Close the temp file stream.
fclose($temp_file);
}
else {
// Close the file streams.
fclose($temp_file);
fclose($file_data);
$this->logger->error('Temporary file "%path" could not be opened for file upload', ['%path' => $temp_file_path]);
throw new HttpException(500, 'Temporary file could not be opened');
}
// Close the input stream.
fclose($file_data);
return $temp_file_path;
}
/**
* Validates and extracts the filename from the Content-Disposition header.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return string
* The filename extracted from the header.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Thrown when the 'Content-Disposition' request header is invalid.
*/
protected function validateAndParseContentDispositionHeader(Request $request) {
// Firstly, check the header exists.
if (!$request->headers->has('content-disposition')) {
throw new BadRequestHttpException('"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided');
}
$content_disposition = $request->headers->get('content-disposition');
// Parse the header value. This regex does not allow an empty filename.
// i.e. 'filename=""'. This also matches on a word boundary so other keys
// like 'not_a_filename' don't work.
if (!preg_match(static::REQUEST_HEADER_FILENAME_REGEX, $content_disposition, $matches)) {
throw new BadRequestHttpException('No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided');
}
// Check for the "filename*" format. This is currently unsupported.
if (!empty($matches['star'])) {
throw new BadRequestHttpException('The extended "filename*" format is currently not supported in the "Content-Disposition" header');
}
// Don't validate the actual filename here, that will be done by the upload
// validators in validate().
// @see \Drupal\file\Plugin\rest\resource\FileUploadResource::validate()
$filename = $matches['filename'];
// Make sure only the filename component is returned. Path information is
// stripped as per https://tools.ietf.org/html/rfc6266#section-4.3.
return basename($filename);
}
/**
* Validates and loads a field definition instance.
*
* @param string $entity_type_id
* The entity type ID the field is attached to.
* @param string $bundle
* The bundle the field is attached to.
* @param string $field_name
* The field name.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface
* The field definition.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Thrown when the field does not exist.
* @throws \Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException
* Thrown when the target type of the field is not a file, or the current
* user does not have 'edit' access for the field.
*/
protected function validateAndLoadFieldDefinition($entity_type_id, $bundle, $field_name) {
$field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle);
if (!isset($field_definitions[$field_name])) {
throw new NotFoundHttpException(sprintf('Field "%s" does not exist', $field_name));
}
/** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
$field_definition = $field_definitions[$field_name];
if ($field_definition->getSetting('target_type') !== 'file') {
throw new AccessDeniedHttpException(sprintf('"%s" is not a file field', $field_name));
}
$entity_access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type_id);
$bundle = $this->entityTypeManager->getDefinition($entity_type_id)->hasKey('bundle') ? $bundle : NULL;
$access_result = $entity_access_control_handler->createAccess($bundle, NULL, [], TRUE)
->andIf($entity_access_control_handler->fieldAccess('edit', $field_definition, NULL, NULL, TRUE));
if (!$access_result->isAllowed()) {
throw new AccessDeniedHttpException($access_result->getReason());
}
return $field_definition;
}
/**
* Validates the file.
*
* @param \Drupal\file\FileInterface $file
* The file entity to validate.
* @param array $validators
* An array of upload validators to pass to file_validate().
*
* @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
* Thrown when there are file validation errors.
*/
protected function validate(FileInterface $file, array $validators) {
$this->resourceValidate($file);
// Validate the file based on the field definition configuration.
$errors = file_validate($file, $validators);
if (!empty($errors)) {
$message = "Unprocessable Entity: file validation failed.\n";
$message .= implode("\n", array_map(function ($error) {
return PlainTextOutput::renderFromHtml($error);
}, $errors));
throw new UnprocessableEntityHttpException($message);
}
}
/**
* Prepares the filename to strip out any malicious extensions.
*
* @param string $filename
* The file name.
* @param array $validators
* The array of upload validators.
*
* @return string
* The prepared/munged filename.
*/
protected function prepareFilename($filename, array &$validators) {
if (!empty($validators['file_validate_extensions'][0])) {
// If there is a file_validate_extensions validator and a list of
// valid extensions, munge the filename to protect against possible
// malicious extension hiding within an unknown file type. For example,
// "filename.html.foo".
$filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
}
// Rename potentially executable files, to help prevent exploits (i.e. will
// rename filename.php.foo and filename.php to filename.php.foo.txt and
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
// evaluates to TRUE.
if (!$this->systemFileConfig->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename) && (substr($filename, -4) != '.txt')) {
// The destination filename will also later be used to create the URI.
$filename .= '.txt';
// The .txt extension may not be in the allowed list of extensions. We
// have to add it here or else the file upload will fail.
if (!empty($validators['file_validate_extensions'][0])) {
$validators['file_validate_extensions'][0] .= ' txt';
}
}
return $filename;
}
/**
* Determines the URI for a file field.
*
* @param array $settings
* The array of field settings.
*
* @return string
* An un-sanitized file directory URI with tokens replaced. The result of
* the token replacement is then converted to plain text and returned.
*/
protected function getUploadLocation(array $settings) {
$destination = trim($settings['file_directory'], '/');
// Replace tokens. As the tokens might contain HTML we convert it to plain
// text.
$destination = PlainTextOutput::renderFromHtml($this->token->replace($destination, []));
return $settings['uri_scheme'] . '://' . $destination;
}
/**
* Retrieves the upload validators for a field definition.
*
* This is copied from \Drupal\file\Plugin\Field\FieldType\FileItem as there
* is no entity instance available here that that a FileItem would exist for.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition for which to get validators.
*
* @return array
* An array suitable for passing to file_save_upload() or the file field
* element's '#upload_validators' property.
*/
protected function getUploadValidators(FieldDefinitionInterface $field_definition) {
$validators = [
// Add in our check of the file name length.
'file_validate_name_length' => [],
];
$settings = $field_definition->getSettings();
// Cap the upload size according to the PHP limit.
$max_filesize = Bytes::toInt(file_upload_max_size());
if (!empty($settings['max_filesize'])) {
$max_filesize = min($max_filesize, Bytes::toInt($settings['max_filesize']));
}
// There is always a file size limit due to the PHP server limit.
$validators['file_validate_size'] = [$max_filesize];
// Add the extension check if necessary.
if (!empty($settings['file_extensions'])) {
$validators['file_validate_extensions'] = [$settings['file_extensions']];
}
return $validators;
}
/**
* {@inheritdoc}
*/
protected function getBaseRoute($canonical_path, $method) {
return new Route($canonical_path, [
'_controller' => RequestHandler::class . '::handleRaw',
],
$this->getBaseRouteRequirements($method),
[],
'',
[],
// The HTTP method is a requirement for this route.
[$method]
);
}
/**
* {@inheritdoc}
*/
protected function getBaseRouteRequirements($method) {
$requirements = parent::getBaseRouteRequirements($method);
// Add the content type format access check. This will enforce that all
// incoming requests can only use the 'application/octet-stream'
// Content-Type header.
$requirements['_content_type_format'] = 'bin';
return $requirements;
}
/**
* Generates a lock ID based on the file URI.
*
* @param $file_uri
* The file URI.
*
* @return string
* The generated lock ID.
*/
protected static function generateLockIdFromFileUri($file_uri) {
return 'file:rest:' . Crypt::hashBase64($file_uri);
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Drupal\file\Plugin\views\argument;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\views\Plugin\views\argument\NumericArgument;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Argument handler to accept multiple file ids.
*
* @ingroup views_argument_handlers
*
* @ViewsArgument("file_fid")
*/
class Fid extends NumericArgument implements ContainerFactoryPluginInterface {
/**
* The entity manager service
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a Drupal\file\Plugin\views\argument\Fid object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity.manager')
);
}
/**
* Override the behavior of titleQuery(). Get the filenames.
*/
public function titleQuery() {
$storage = $this->entityManager->getStorage('file');
$fids = $storage->getQuery()
->condition('fid', $this->value, 'IN')
->execute();
$files = $storage->loadMultiple($fids);
$titles = [];
foreach ($files as $file) {
$titles[] = $file->getFilename();
}
return $titles;
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Drupal\file\Plugin\views\field;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\field\FieldPluginBase;
/**
* Field handler to provide simple renderer that allows linking to a file.
*
* @ingroup views_field_handlers
*
* @ViewsField("file")
*/
class File extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
if (!empty($options['link_to_file'])) {
$this->additional_fields['uri'] = 'uri';
}
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['link_to_file'] = ['default' => FALSE];
return $options;
}
/**
* Provide link to file option
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['link_to_file'] = [
'#title' => $this->t('Link this field to download the file'),
'#description' => $this->t("Enable to override this field's links."),
'#type' => 'checkbox',
'#default_value' => !empty($this->options['link_to_file']),
];
parent::buildOptionsForm($form, $form_state);
}
/**
* Prepares link to the file.
*
* @param string $data
* The XSS safe string for the link text.
* @param \Drupal\views\ResultRow $values
* The values retrieved from a single row of a view's query result.
*
* @return string
* Returns a string for the link text.
*/
protected function renderLink($data, ResultRow $values) {
if (!empty($this->options['link_to_file']) && $data !== NULL && $data !== '') {
$this->options['alter']['make_link'] = TRUE;
// @todo Wrap in file_url_transform_relative(). This is currently
// impossible. As a work-around, we could add the 'url.site' cache context
// to ensure different file URLs are generated for different sites in a
// multisite setup, including HTTP and HTTPS versions of the same site.
// But unfortunately it's impossible to bubble a cache context here.
// Fix in https://www.drupal.org/node/2646744.
$this->options['alter']['path'] = file_create_url($this->getValue($values, 'uri'));
}
return $data;
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
$value = $this->getValue($values);
return $this->renderLink($this->sanitizeValue($value), $values);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Drupal\file\Plugin\views\filter;
use Drupal\views\Plugin\views\filter\InOperator;
/**
* Filter by file status.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("file_status")
*/
class Status extends InOperator {
public function getValueOptions() {
if (!isset($this->valueOptions)) {
$this->valueOptions = _views_file_status();
}
return $this->valueOptions;
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\file\Plugin\views\wizard;
use Drupal\views\Plugin\views\wizard\WizardPluginBase;
/**
* Tests creating managed files views with the wizard.
*
* @ViewsWizard(
* id = "file_managed",
* base_table = "file_managed",
* title = @Translation("Files")
* )
*/
class File extends WizardPluginBase {
/**
* Set the created column.
*
* @var string
*/
protected $createdColumn = 'created';
/**
* {@inheritdoc}
*/
protected function defaultDisplayOptions() {
$display_options = parent::defaultDisplayOptions();
// Add permission-based access control.
$display_options['access']['type'] = 'perm';
// Remove the default fields, since we are customizing them here.
unset($display_options['fields']);
/* Field: File: Name */
$display_options['fields']['filename']['id'] = 'filename';
$display_options['fields']['filename']['table'] = 'file_managed';
$display_options['fields']['filename']['field'] = 'filename';
$display_options['fields']['filename']['entity_type'] = 'file';
$display_options['fields']['filename']['entity_field'] = 'filename';
$display_options['fields']['filename']['label'] = '';
$display_options['fields']['filename']['alter']['alter_text'] = 0;
$display_options['fields']['filename']['alter']['make_link'] = 0;
$display_options['fields']['filename']['alter']['absolute'] = 0;
$display_options['fields']['filename']['alter']['trim'] = 0;
$display_options['fields']['filename']['alter']['word_boundary'] = 0;
$display_options['fields']['filename']['alter']['ellipsis'] = 0;
$display_options['fields']['filename']['alter']['strip_tags'] = 0;
$display_options['fields']['filename']['alter']['html'] = 0;
$display_options['fields']['filename']['hide_empty'] = 0;
$display_options['fields']['filename']['empty_zero'] = 0;
$display_options['fields']['filename']['plugin_id'] = 'field';
$display_options['fields']['filename']['type'] = 'file_link';
return $display_options;
}
}

View file

@ -0,0 +1,307 @@
<?php
namespace Drupal\file\Tests;
@trigger_error('The ' . __NAMESPACE__ . '\FileFieldTestBase is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. Instead, use \Drupal\Tests\file\Functional\FileFieldTestBase. See https://www.drupal.org/node/2969361.', E_USER_DEPRECATED);
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\file\FileInterface;
use Drupal\simpletest\WebTestBase;
use Drupal\file\Entity\File;
/**
* Provides methods specifically for testing File module's field handling.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use \Drupal\Tests\file\Functional\FileFieldTestBase instead.
*/
abstract class FileFieldTestBase extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'file', 'file_module_test', 'field_ui'];
/**
* An user with administration permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
protected function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(['access content', 'access administration pages', 'administer site configuration', 'administer users', 'administer permissions', 'administer content types', 'administer node fields', 'administer node display', 'administer nodes', 'bypass node access']);
$this->drupalLogin($this->adminUser);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
}
/**
* Retrieves a sample file of the specified type.
*
* @return \Drupal\file\FileInterface
*/
public function getTestFile($type_name, $size = NULL) {
// Get a file to upload.
$file = current($this->drupalGetTestFiles($type_name, $size));
// Add a filesize property to files as would be read by
// \Drupal\file\Entity\File::load().
$file->filesize = filesize($file->uri);
return File::create((array) $file);
}
/**
* Retrieves the fid of the last inserted file.
*/
public function getLastFileId() {
return (int) db_query('SELECT MAX(fid) FROM {file_managed}')->fetchField();
}
/**
* Creates a new file field.
*
* @param string $name
* The name of the new field (all lowercase), exclude the "field_" prefix.
* @param string $entity_type
* The entity type.
* @param string $bundle
* The bundle that this field will be added to.
* @param array $storage_settings
* A list of field storage settings that will be added to the defaults.
* @param array $field_settings
* A list of instance settings that will be added to the instance defaults.
* @param array $widget_settings
* A list of widget settings that will be added to the widget defaults.
*/
public function createFileField($name, $entity_type, $bundle, $storage_settings = [], $field_settings = [], $widget_settings = []) {
$field_storage = FieldStorageConfig::create([
'entity_type' => $entity_type,
'field_name' => $name,
'type' => 'file',
'settings' => $storage_settings,
'cardinality' => !empty($storage_settings['cardinality']) ? $storage_settings['cardinality'] : 1,
]);
$field_storage->save();
$this->attachFileField($name, $entity_type, $bundle, $field_settings, $widget_settings);
return $field_storage;
}
/**
* Attaches a file field to an entity.
*
* @param string $name
* The name of the new field (all lowercase), exclude the "field_" prefix.
* @param string $entity_type
* The entity type this field will be added to.
* @param string $bundle
* The bundle this field will be added to.
* @param array $field_settings
* A list of field settings that will be added to the defaults.
* @param array $widget_settings
* A list of widget settings that will be added to the widget defaults.
*/
public function attachFileField($name, $entity_type, $bundle, $field_settings = [], $widget_settings = []) {
$field = [
'field_name' => $name,
'label' => $name,
'entity_type' => $entity_type,
'bundle' => $bundle,
'required' => !empty($field_settings['required']),
'settings' => $field_settings,
];
FieldConfig::create($field)->save();
entity_get_form_display($entity_type, $bundle, 'default')
->setComponent($name, [
'type' => 'file_generic',
'settings' => $widget_settings,
])
->save();
// Assign display settings.
entity_get_display($entity_type, $bundle, 'default')
->setComponent($name, [
'label' => 'hidden',
'type' => 'file_default',
])
->save();
}
/**
* Updates an existing file field with new settings.
*/
public function updateFileField($name, $type_name, $field_settings = [], $widget_settings = []) {
$field = FieldConfig::loadByName('node', $type_name, $name);
$field->setSettings(array_merge($field->getSettings(), $field_settings));
$field->save();
entity_get_form_display('node', $type_name, 'default')
->setComponent($name, [
'settings' => $widget_settings,
])
->save();
}
/**
* Uploads a file to a node.
*
* @param \Drupal\file\FileInterface $file
* The File to be uploaded.
* @param string $field_name
* The name of the field on which the files should be saved.
* @param $nid_or_type
* A numeric node id to upload files to an existing node, or a string
* indicating the desired bundle for a new node.
* @param bool $new_revision
* The revision number.
* @param array $extras
* Additional values when a new node is created.
*
* @return int
* The node id.
*/
public function uploadNodeFile(FileInterface $file, $field_name, $nid_or_type, $new_revision = TRUE, array $extras = []) {
return $this->uploadNodeFiles([$file], $field_name, $nid_or_type, $new_revision, $extras);
}
/**
* Uploads multiple files to a node.
*
* @param \Drupal\file\FileInterface[] $files
* The files to be uploaded.
* @param string $field_name
* The name of the field on which the files should be saved.
* @param $nid_or_type
* A numeric node id to upload files to an existing node, or a string
* indicating the desired bundle for a new node.
* @param bool $new_revision
* The revision number.
* @param array $extras
* Additional values when a new node is created.
*
* @return int
* The node id.
*/
public function uploadNodeFiles(array $files, $field_name, $nid_or_type, $new_revision = TRUE, array $extras = []) {
$edit = [
'title[0][value]' => $this->randomMachineName(),
'revision' => (string) (int) $new_revision,
];
$node_storage = $this->container->get('entity.manager')->getStorage('node');
if (is_numeric($nid_or_type)) {
$nid = $nid_or_type;
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
}
else {
// Add a new node.
$extras['type'] = $nid_or_type;
$node = $this->drupalCreateNode($extras);
$nid = $node->id();
// Save at least one revision to better simulate a real site.
$node->setNewRevision();
$node->save();
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$this->assertNotEqual($nid, $node->getRevisionId(), 'Node revision exists.');
}
// Attach files to the node.
$field_storage = FieldStorageConfig::loadByName('node', $field_name);
// File input name depends on number of files already uploaded.
$field_num = count($node->{$field_name});
$name = 'files[' . $field_name . "_$field_num]";
if ($field_storage->getCardinality() != 1) {
$name .= '[]';
}
foreach ($files as $file) {
$file_path = $this->container->get('file_system')->realpath($file->getFileUri());
if (count($files) == 1) {
$edit[$name] = $file_path;
}
else {
$edit[$name][] = $file_path;
}
}
$this->drupalPostForm("node/$nid/edit", $edit, t('Save'));
return $nid;
}
/**
* Removes a file from a node.
*
* Note that if replacing a file, it must first be removed then added again.
*/
public function removeNodeFile($nid, $new_revision = TRUE) {
$edit = [
'revision' => (string) (int) $new_revision,
];
$this->drupalPostForm('node/' . $nid . '/edit', [], t('Remove'));
$this->drupalPostForm(NULL, $edit, t('Save'));
}
/**
* Replaces a file within a node.
*/
public function replaceNodeFile($file, $field_name, $nid, $new_revision = TRUE) {
$edit = [
'files[' . $field_name . '_0]' => \Drupal::service('file_system')->realpath($file->getFileUri()),
'revision' => (string) (int) $new_revision,
];
$this->drupalPostForm('node/' . $nid . '/edit', [], t('Remove'));
$this->drupalPostForm(NULL, $edit, t('Save'));
}
/**
* Asserts that a file exists physically on disk.
*/
public function assertFileExists($file, $message = NULL) {
$message = isset($message) ? $message : format_string('File %file exists on the disk.', ['%file' => $file->getFileUri()]);
$this->assertTrue(is_file($file->getFileUri()), $message);
}
/**
* Asserts that a file exists in the database.
*/
public function assertFileEntryExists($file, $message = NULL) {
$this->container->get('entity.manager')->getStorage('file')->resetCache();
$db_file = File::load($file->id());
$message = isset($message) ? $message : format_string('File %file exists in database at the correct path.', ['%file' => $file->getFileUri()]);
$this->assertEqual($db_file->getFileUri(), $file->getFileUri(), $message);
}
/**
* Asserts that a file does not exist on disk.
*/
public function assertFileNotExists($file, $message = NULL) {
$message = isset($message) ? $message : format_string('File %file exists on the disk.', ['%file' => $file->getFileUri()]);
$this->assertFalse(is_file($file->getFileUri()), $message);
}
/**
* Asserts that a file does not exist in the database.
*/
public function assertFileEntryNotExists($file, $message) {
$this->container->get('entity.manager')->getStorage('file')->resetCache();
$message = isset($message) ? $message : format_string('File %file exists in database at the correct path.', ['%file' => $file->getFileUri()]);
$this->assertFalse(File::load($file->id()), $message);
}
/**
* Asserts that a file's status is set to permanent in the database.
*/
public function assertFileIsPermanent(FileInterface $file, $message = NULL) {
$message = isset($message) ? $message : format_string('File %file is permanent.', ['%file' => $file->getFileUri()]);
$this->assertTrue($file->isPermanent(), $message);
}
}

View file

@ -0,0 +1,601 @@
<?php
namespace Drupal\file\Tests;
use Drupal\comment\Entity\Comment;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field_ui\Tests\FieldUiTestTrait;
use Drupal\user\RoleInterface;
use Drupal\file\Entity\File;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
/**
* Tests the file field widget, single and multi-valued, with and without AJAX,
* with public and private files.
*
* @group file
*/
class FileFieldWidgetTest extends FileFieldTestBase {
use CommentTestTrait;
use FieldUiTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
}
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['comment', 'block'];
/**
* Creates a temporary file, for a specific user.
*
* @param string $data
* A string containing the contents of the file.
* @param \Drupal\user\UserInterface $user
* The user of the file owner.
*
* @return \Drupal\file\FileInterface
* A file object, or FALSE on error.
*/
protected function createTemporaryFile($data, UserInterface $user = NULL) {
$file = file_save_data($data, NULL, NULL);
if ($file) {
if ($user) {
$file->setOwner($user);
}
else {
$file->setOwner($this->adminUser);
}
// Change the file status to be temporary.
$file->setTemporary();
// Save the changes.
$file->save();
}
return $file;
}
/**
* Tests upload and remove buttons for a single-valued File field.
*/
public function testSingleValuedWidget() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$type_name = 'article';
$field_name = strtolower($this->randomMachineName());
$this->createFileField($field_name, 'node', $type_name);
$test_file = $this->getTestFile('text');
foreach (['nojs', 'js'] as $type) {
// Create a new node with the uploaded file and ensure it got uploaded
// successfully.
// @todo This only tests a 'nojs' submission, because drupalPostAjaxForm()
// does not yet support file uploads.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
$this->assertFileExists($node_file, 'New file saved to disk on node creation.');
// Ensure the file can be downloaded.
$this->drupalGet(file_create_url($node_file->getFileUri()));
$this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
// Ensure the edit page has a remove button instead of an upload button.
$this->drupalGet("node/$nid/edit");
$this->assertNoFieldByXPath('//input[@type="submit"]', t('Upload'), 'Node with file does not display the "Upload" button.');
$this->assertFieldByXpath('//input[@type="submit"]', t('Remove'), 'Node with file displays the "Remove" button.');
// "Click" the remove button (emulating either a nojs or js submission).
switch ($type) {
case 'nojs':
$this->drupalPostForm(NULL, [], t('Remove'));
break;
case 'js':
$button = $this->xpath('//input[@type="submit" and @value="' . t('Remove') . '"]');
$this->drupalPostAjaxForm(NULL, [], [(string) $button[0]['name'] => (string) $button[0]['value']]);
break;
}
// Ensure the page now has an upload button instead of a remove button.
$this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), 'After clicking the "Remove" button, it is no longer displayed.');
$this->assertFieldByXpath('//input[@type="submit"]', t('Upload'), 'After clicking the "Remove" button, the "Upload" button is displayed.');
// Test label has correct 'for' attribute.
$input = $this->xpath('//input[@name="files[' . $field_name . '_0]"]');
$label = $this->xpath('//label[@for="' . (string) $input[0]['id'] . '"]');
$this->assertTrue(isset($label[0]), 'Label for upload found.');
// Save the node and ensure it does not have the file.
$this->drupalPostForm(NULL, [], t('Save'));
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$this->assertTrue(empty($node->{$field_name}->target_id), 'File was successfully removed from the node.');
}
}
/**
* Tests upload and remove buttons for multiple multi-valued File fields.
*/
public function testMultiValuedWidget() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$type_name = 'article';
// Use explicit names instead of random names for those fields, because of a
// bug in drupalPostForm() with multiple file uploads in one form, where the
// order of uploads depends on the order in which the upload elements are
// added to the $form (which, in the current implementation of
// FileStorage::listAll(), comes down to the alphabetical order on field
// names).
$field_name = 'test_file_field_1';
$field_name2 = 'test_file_field_2';
$cardinality = 3;
$this->createFileField($field_name, 'node', $type_name, ['cardinality' => $cardinality]);
$this->createFileField($field_name2, 'node', $type_name, ['cardinality' => $cardinality]);
$test_file = $this->getTestFile('text');
foreach (['nojs', 'js'] as $type) {
// Visit the node creation form, and upload 3 files for each field. Since
// the field has cardinality of 3, ensure the "Upload" button is displayed
// until after the 3rd file, and after that, isn't displayed. Because
// SimpleTest triggers the last button with a given name, so upload to the
// second field first.
// @todo This is only testing a non-Ajax upload, because drupalPostAjaxForm()
// does not yet emulate jQuery's file upload.
//
$this->drupalGet("node/add/$type_name");
foreach ([$field_name2, $field_name] as $each_field_name) {
for ($delta = 0; $delta < 3; $delta++) {
$edit = ['files[' . $each_field_name . '_' . $delta . '][]' => \Drupal::service('file_system')->realpath($test_file->getFileUri())];
// If the Upload button doesn't exist, drupalPostForm() will automatically
// fail with an assertion message.
$this->drupalPostForm(NULL, $edit, t('Upload'));
}
}
$this->assertNoFieldByXpath('//input[@type="submit"]', t('Upload'), 'After uploading 3 files for each field, the "Upload" button is no longer displayed.');
$num_expected_remove_buttons = 6;
foreach ([$field_name, $field_name2] as $current_field_name) {
// How many uploaded files for the current field are remaining.
$remaining = 3;
// Test clicking each "Remove" button. For extra robustness, test them out
// of sequential order. They are 0-indexed, and get renumbered after each
// iteration, so array(1, 1, 0) means:
// - First remove the 2nd file.
// - Then remove what is then the 2nd file (was originally the 3rd file).
// - Then remove the first file.
foreach ([1, 1, 0] as $delta) {
// Ensure we have the expected number of Remove buttons, and that they
// are numbered sequentially.
$buttons = $this->xpath('//input[@type="submit" and @value="Remove"]');
$this->assertTrue(is_array($buttons) && count($buttons) === $num_expected_remove_buttons, format_string('There are %n "Remove" buttons displayed (JSMode=%type).', ['%n' => $num_expected_remove_buttons, '%type' => $type]));
foreach ($buttons as $i => $button) {
$key = $i >= $remaining ? $i - $remaining : $i;
$check_field_name = $field_name2;
if ($current_field_name == $field_name && $i < $remaining) {
$check_field_name = $field_name;
}
$this->assertIdentical((string) $button['name'], $check_field_name . '_' . $key . '_remove_button');
}
// "Click" the remove button (emulating either a nojs or js submission).
$button_name = $current_field_name . '_' . $delta . '_remove_button';
switch ($type) {
case 'nojs':
// drupalPostForm() takes a $submit parameter that is the value of the
// button whose click we want to emulate. Since we have multiple
// buttons with the value "Remove", and want to control which one we
// use, we change the value of the other ones to something else.
// Since non-clicked buttons aren't included in the submitted POST
// data, and since drupalPostForm() will result in $this being updated
// with a newly rebuilt form, this doesn't cause problems.
foreach ($buttons as $button) {
if ($button['name'] != $button_name) {
$button['value'] = 'DUMMY';
}
}
$this->drupalPostForm(NULL, [], t('Remove'));
break;
case 'js':
// drupalPostAjaxForm() lets us target the button precisely, so we don't
// require the workaround used above for nojs.
$this->drupalPostAjaxForm(NULL, [], [$button_name => t('Remove')]);
break;
}
$num_expected_remove_buttons--;
$remaining--;
// Ensure an "Upload" button for the current field is displayed with the
// correct name.
$upload_button_name = $current_field_name . '_' . $remaining . '_upload_button';
$buttons = $this->xpath('//input[@type="submit" and @value="Upload" and @name=:name]', [':name' => $upload_button_name]);
$this->assertTrue(is_array($buttons) && count($buttons) == 1, format_string('The upload button is displayed with the correct name (JSMode=%type).', ['%type' => $type]));
// Ensure only at most one button per field is displayed.
$buttons = $this->xpath('//input[@type="submit" and @value="Upload"]');
$expected = $current_field_name == $field_name ? 1 : 2;
$this->assertTrue(is_array($buttons) && count($buttons) == $expected, format_string('After removing a file, only one "Upload" button for each possible field is displayed (JSMode=%type).', ['%type' => $type]));
}
}
// Ensure the page now has no Remove buttons.
$this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), format_string('After removing all files, there is no "Remove" button displayed (JSMode=%type).', ['%type' => $type]));
// Save the node and ensure it does not have any files.
$this->drupalPostForm(NULL, ['title[0][value]' => $this->randomMachineName()], t('Save'));
preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches);
$nid = $matches[1];
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$this->assertTrue(empty($node->{$field_name}->target_id), 'Node was successfully saved without any files.');
}
$upload_files_node_creation = [$test_file, $test_file];
// Try to upload multiple files, but fewer than the maximum.
$nid = $this->uploadNodeFiles($upload_files_node_creation, $field_name, $type_name);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$this->assertEqual(count($node->{$field_name}), count($upload_files_node_creation), 'Node was successfully saved with mulitple files.');
// Try to upload more files than allowed on revision.
$upload_files_node_revision = [$test_file, $test_file, $test_file, $test_file];
$this->uploadNodeFiles($upload_files_node_revision, $field_name, $nid, 1);
$args = [
'%field' => $field_name,
'@max' => $cardinality,
'@count' => count($upload_files_node_creation) + count($upload_files_node_revision),
'%list' => implode(', ', array_fill(0, 3, $test_file->getFilename())),
];
$this->assertRaw(t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args));
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$this->assertEqual(count($node->{$field_name}), $cardinality, 'More files than allowed could not be saved to node.');
// Try to upload exactly the allowed number of files on revision. Create an
// empty node first, to fill it in its first revision.
$node = $this->drupalCreateNode([
'type' => $type_name,
]);
$this->uploadNodeFile($test_file, $field_name, $node->id(), 1);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$this->assertEqual(count($node->{$field_name}), $cardinality, 'Node was successfully revised to maximum number of files.');
// Try to upload exactly the allowed number of files, new node.
$upload_files = array_fill(0, $cardinality, $test_file);
$nid = $this->uploadNodeFiles($upload_files, $field_name, $type_name);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$this->assertEqual(count($node->{$field_name}), $cardinality, 'Node was successfully saved with maximum number of files.');
// Try to upload more files than allowed, new node.
$upload_files[] = $test_file;
$this->uploadNodeFiles($upload_files, $field_name, $type_name);
$args = [
'%field' => $field_name,
'@max' => $cardinality,
'@count' => count($upload_files),
'%list' => $test_file->getFileName(),
];
$this->assertRaw(t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args));
}
/**
* Tests a file field with a "Private files" upload destination setting.
*/
public function testPrivateFileSetting() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
// Grant the admin user required permissions.
user_role_grant_permissions($this->adminUser->roles[0]->target_id, ['administer node fields']);
$type_name = 'article';
$field_name = strtolower($this->randomMachineName());
$this->createFileField($field_name, 'node', $type_name);
$field = FieldConfig::loadByName('node', $type_name, $field_name);
$field_id = $field->id();
$test_file = $this->getTestFile('text');
// Change the field setting to make its files private, and upload a file.
$edit = ['settings[uri_scheme]' => 'private'];
$this->drupalPostForm("admin/structure/types/manage/$type_name/fields/$field_id/storage", $edit, t('Save field settings'));
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
$this->assertFileExists($node_file, 'New file saved to disk on node creation.');
// Ensure the private file is available to the user who uploaded it.
$this->drupalGet(file_create_url($node_file->getFileUri()));
$this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
// Ensure we can't change 'uri_scheme' field settings while there are some
// entities with uploaded files.
$this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_id/storage");
$this->assertFieldByXpath('//input[@id="edit-settings-uri-scheme-public" and @disabled="disabled"]', 'public', 'Upload destination setting disabled.');
// Delete node and confirm that setting could be changed.
$node->delete();
$this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_id/storage");
$this->assertFieldByXpath('//input[@id="edit-settings-uri-scheme-public" and not(@disabled)]', 'public', 'Upload destination setting enabled.');
}
/**
* Tests that download restrictions on private files work on comments.
*/
public function testPrivateFileComment() {
$user = $this->drupalCreateUser(['access comments']);
// Grant the admin user required comment permissions.
$roles = $this->adminUser->getRoles();
user_role_grant_permissions($roles[1], ['administer comment fields', 'administer comments']);
// Revoke access comments permission from anon user, grant post to
// authenticated.
user_role_revoke_permissions(RoleInterface::ANONYMOUS_ID, ['access comments']);
user_role_grant_permissions(RoleInterface::AUTHENTICATED_ID, ['post comments', 'skip comment approval']);
// Create a new field.
$this->addDefaultCommentField('node', 'article');
$name = strtolower($this->randomMachineName());
$label = $this->randomMachineName();
$storage_edit = ['settings[uri_scheme]' => 'private'];
$this->fieldUIAddNewField('admin/structure/comment/manage/comment', $name, $label, 'file', $storage_edit);
// Manually clear cache on the tester side.
\Drupal::entityManager()->clearCachedFieldDefinitions();
// Create node.
$edit = [
'title[0][value]' => $this->randomMachineName(),
];
$this->drupalPostForm('node/add/article', $edit, t('Save'));
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
// Add a comment with a file.
$text_file = $this->getTestFile('text');
$edit = [
'files[field_' . $name . '_' . 0 . ']' => \Drupal::service('file_system')->realpath($text_file->getFileUri()),
'comment_body[0][value]' => $comment_body = $this->randomMachineName(),
];
$this->drupalPostForm('node/' . $node->id(), $edit, t('Save'));
// Get the comment ID.
preg_match('/comment-([0-9]+)/', $this->getUrl(), $matches);
$cid = $matches[1];
// Log in as normal user.
$this->drupalLogin($user);
$comment = Comment::load($cid);
$comment_file = $comment->{'field_' . $name}->entity;
$this->assertFileExists($comment_file, 'New file saved to disk on node creation.');
// Test authenticated file download.
$url = file_create_url($comment_file->getFileUri());
$this->assertNotEqual($url, NULL, 'Confirmed that the URL is valid');
$this->drupalGet(file_create_url($comment_file->getFileUri()));
$this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
// Test anonymous file download.
$this->drupalLogout();
$this->drupalGet(file_create_url($comment_file->getFileUri()));
$this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.');
// Unpublishes node.
$this->drupalLogin($this->adminUser);
$edit = ['status[value]' => FALSE];
$this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
// Ensures normal user can no longer download the file.
$this->drupalLogin($user);
$this->drupalGet(file_create_url($comment_file->getFileUri()));
$this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.');
}
/**
* Tests validation with the Upload button.
*/
public function testWidgetValidation() {
$type_name = 'article';
$field_name = strtolower($this->randomMachineName());
$this->createFileField($field_name, 'node', $type_name);
$this->updateFileField($field_name, $type_name, ['file_extensions' => 'txt']);
foreach (['nojs', 'js'] as $type) {
// Create node and prepare files for upload.
$node = $this->drupalCreateNode(['type' => 'article']);
$nid = $node->id();
$this->drupalGet("node/$nid/edit");
$test_file_text = $this->getTestFile('text');
$test_file_image = $this->getTestFile('image');
$name = 'files[' . $field_name . '_0]';
// Upload file with incorrect extension, check for validation error.
$edit[$name] = \Drupal::service('file_system')->realpath($test_file_image->getFileUri());
switch ($type) {
case 'nojs':
$this->drupalPostForm(NULL, $edit, t('Upload'));
break;
case 'js':
$button = $this->xpath('//input[@type="submit" and @value="' . t('Upload') . '"]');
$this->drupalPostAjaxForm(NULL, $edit, [(string) $button[0]['name'] => (string) $button[0]['value']]);
break;
}
$error_message = t('Only files with the following extensions are allowed: %files-allowed.', ['%files-allowed' => 'txt']);
$this->assertRaw($error_message, t('Validation error when file with wrong extension uploaded (JSMode=%type).', ['%type' => $type]));
// Upload file with correct extension, check that error message is removed.
$edit[$name] = \Drupal::service('file_system')->realpath($test_file_text->getFileUri());
switch ($type) {
case 'nojs':
$this->drupalPostForm(NULL, $edit, t('Upload'));
break;
case 'js':
$button = $this->xpath('//input[@type="submit" and @value="' . t('Upload') . '"]');
$this->drupalPostAjaxForm(NULL, $edit, [(string) $button[0]['name'] => (string) $button[0]['value']]);
break;
}
$this->assertNoRaw($error_message, t('Validation error removed when file with correct extension uploaded (JSMode=%type).', ['%type' => $type]));
}
}
/**
* Tests file widget element.
*/
public function testWidgetElement() {
$field_name = mb_strtolower($this->randomMachineName());
$html_name = str_replace('_', '-', $field_name);
$this->createFileField($field_name, 'node', 'article', ['cardinality' => FieldStorageConfig::CARDINALITY_UNLIMITED]);
$file = $this->getTestFile('text');
$xpath = "//details[@data-drupal-selector='edit-$html_name']/div[@class='details-wrapper']/table";
$this->drupalGet('node/add/article');
$elements = $this->xpath($xpath);
// If the field has no item, the table should not be visible.
$this->assertIdentical(count($elements), 0);
// Upload a file.
$edit['files[' . $field_name . '_0][]'] = $this->container->get('file_system')->realpath($file->getFileUri());
$this->drupalPostAjaxForm(NULL, $edit, "{$field_name}_0_upload_button");
$elements = $this->xpath($xpath);
// If the field has at least a item, the table should be visible.
$this->assertIdentical(count($elements), 1);
// Test for AJAX error when using progress bar on file field widget
$key = $this->randomMachineName();
$this->drupalPost('file/progress/' . $key, 'application/json', []);
$this->assertNoResponse(500, t('No AJAX error when using progress bar on file field widget'));
$this->assertText('Starting upload...');
}
/**
* Tests exploiting the temporary file removal of another user using fid.
*/
public function testTemporaryFileRemovalExploit() {
// Create a victim user.
$victim_user = $this->drupalCreateUser();
// Create an attacker user.
$attacker_user = $this->drupalCreateUser([
'access content',
'create article content',
'edit any article content',
]);
// Log in as the attacker user.
$this->drupalLogin($attacker_user);
// Perform tests using the newly created users.
$this->doTestTemporaryFileRemovalExploit($victim_user, $attacker_user);
}
/**
* Tests exploiting the temporary file removal for anonymous users using fid.
*/
public function testTemporaryFileRemovalExploitAnonymous() {
// Set up an anonymous victim user.
$victim_user = User::getAnonymousUser();
// Set up an anonymous attacker user.
$attacker_user = User::getAnonymousUser();
// Set up permissions for anonymous attacker user.
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, [
'access content' => TRUE,
'create article content' => TRUE,
'edit any article content' => TRUE,
]);
// Log out so as to be the anonymous attacker user.
$this->drupalLogout();
// Perform tests using the newly set up anonymous users.
$this->doTestTemporaryFileRemovalExploit($victim_user, $attacker_user);
}
/**
* Helper for testing exploiting the temporary file removal using fid.
*
* @param \Drupal\user\UserInterface $victim_user
* The victim user.
* @param \Drupal\user\UserInterface $attacker_user
* The attacker user.
*/
protected function doTestTemporaryFileRemovalExploit(UserInterface $victim_user, UserInterface $attacker_user) {
$type_name = 'article';
$field_name = 'test_file_field';
$this->createFileField($field_name, 'node', $type_name);
$test_file = $this->getTestFile('text');
foreach (['nojs', 'js'] as $type) {
// Create a temporary file owned by the victim user. This will be as if
// they had uploaded the file, but not saved the node they were editing
// or creating.
$victim_tmp_file = $this->createTemporaryFile('some text', $victim_user);
$victim_tmp_file = File::load($victim_tmp_file->id());
$this->assertTrue($victim_tmp_file->isTemporary(), 'New file saved to disk is temporary.');
$this->assertFalse(empty($victim_tmp_file->id()), 'New file has an fid.');
$this->assertEqual($victim_user->id(), $victim_tmp_file->getOwnerId(), 'New file belongs to the victim.');
// Have attacker create a new node with a different uploaded file and
// ensure it got uploaded successfully.
$edit = [
'title[0][value]' => $type . '-title' ,
];
// Attach a file to a node.
$edit['files[' . $field_name . '_0]'] = $this->container->get('file_system')->realpath($test_file->getFileUri());
$this->drupalPostForm(Url::fromRoute('node.add', ['node_type' => $type_name]), $edit, t('Save'));
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
/** @var \Drupal\file\FileInterface $node_file */
$node_file = File::load($node->{$field_name}->target_id);
$this->assertFileExists($node_file, 'A file was saved to disk on node creation');
$this->assertEqual($attacker_user->id(), $node_file->getOwnerId(), 'New file belongs to the attacker.');
// Ensure the file can be downloaded.
$this->drupalGet(file_create_url($node_file->getFileUri()));
$this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
// "Click" the remove button (emulating either a nojs or js submission).
// In this POST request, the attacker "guesses" the fid of the victim's
// temporary file and uses that to remove this file.
$this->drupalGet($node->toUrl('edit-form'));
switch ($type) {
case 'nojs':
$this->drupalPostForm(NULL, [$field_name . '[0][fids]' => (string) $victim_tmp_file->id()], 'Remove');
break;
case 'js':
$this->drupalPostAjaxForm(NULL, [$field_name . '[0][fids]' => (string) $victim_tmp_file->id()], ["{$field_name}_0_remove_button" => 'Remove']);
break;
}
// The victim's temporary file should not be removed by the attacker's
// POST request.
$this->assertFileExists($victim_tmp_file);
}
}
}

View file

@ -0,0 +1,205 @@
<?php
namespace Drupal\file\Tests;
@trigger_error('The ' . __NAMESPACE__ . '\FileManagedTestBase is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. Instead, use \Drupal\Tests\file\Functional\FileManagedTestBase. See https://www.drupal.org/node/2969361.', E_USER_DEPRECATED);
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\simpletest\WebTestBase;
/**
* Base class for file tests that use the file_test module to test uploads and
* hooks.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use \Drupal\Tests\file\Functional\FileManagedTestBase instead.
*/
abstract class FileManagedTestBase extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['file_test', 'file'];
protected function setUp() {
parent::setUp();
// Clear out any hook calls.
file_test_reset();
}
/**
* Assert that all of the specified hook_file_* hooks were called once, other
* values result in failure.
*
* @param string[] $expected
* An array of strings containing with the hook name; for example, 'load',
* 'save', 'insert', etc.
*/
public function assertFileHooksCalled($expected) {
\Drupal::state()->resetCache();
// Determine which hooks were called.
$actual = array_keys(array_filter(file_test_get_all_calls()));
// Determine if there were any expected that were not called.
$uncalled = array_diff($expected, $actual);
if (count($uncalled)) {
$this->assertTrue(FALSE, format_string('Expected hooks %expected to be called but %uncalled was not called.', ['%expected' => implode(', ', $expected), '%uncalled' => implode(', ', $uncalled)]));
}
else {
$this->assertTrue(TRUE, format_string('All the expected hooks were called: %expected', ['%expected' => empty($expected) ? '(none)' : implode(', ', $expected)]));
}
// Determine if there were any unexpected calls.
$unexpected = array_diff($actual, $expected);
if (count($unexpected)) {
$this->assertTrue(FALSE, format_string('Unexpected hooks were called: %unexpected.', ['%unexpected' => empty($unexpected) ? '(none)' : implode(', ', $unexpected)]));
}
else {
$this->assertTrue(TRUE, 'No unexpected hooks were called.');
}
}
/**
* Assert that a hook_file_* hook was called a certain number of times.
*
* @param string $hook
* String with the hook name; for instance, 'load', 'save', 'insert', etc.
* @param int $expected_count
* Optional integer count.
* @param string|null $message
* Optional translated string message.
*/
public function assertFileHookCalled($hook, $expected_count = 1, $message = NULL) {
$actual_count = count(file_test_get_calls($hook));
if (!isset($message)) {
if ($actual_count == $expected_count) {
$message = format_string('hook_file_@name was called correctly.', ['@name' => $hook]);
}
elseif ($expected_count == 0) {
$message = \Drupal::translation()->formatPlural($actual_count, 'hook_file_@name was not expected to be called but was actually called once.', 'hook_file_@name was not expected to be called but was actually called @count times.', ['@name' => $hook, '@count' => $actual_count]);
}
else {
$message = format_string('hook_file_@name was expected to be called %expected times but was called %actual times.', ['@name' => $hook, '%expected' => $expected_count, '%actual' => $actual_count]);
}
}
$this->assertEqual($actual_count, $expected_count, $message);
}
/**
* Asserts that two files have the same values (except timestamp).
*
* @param \Drupal\file\FileInterface $before
* File object to compare.
* @param \Drupal\file\FileInterface $after
* File object to compare.
*/
public function assertFileUnchanged(FileInterface $before, FileInterface $after) {
$this->assertEqual($before->id(), $after->id(), t('File id is the same: %file1 == %file2.', ['%file1' => $before->id(), '%file2' => $after->id()]), 'File unchanged');
$this->assertEqual($before->getOwner()->id(), $after->getOwner()->id(), t('File owner is the same: %file1 == %file2.', ['%file1' => $before->getOwner()->id(), '%file2' => $after->getOwner()->id()]), 'File unchanged');
$this->assertEqual($before->getFilename(), $after->getFilename(), t('File name is the same: %file1 == %file2.', ['%file1' => $before->getFilename(), '%file2' => $after->getFilename()]), 'File unchanged');
$this->assertEqual($before->getFileUri(), $after->getFileUri(), t('File path is the same: %file1 == %file2.', ['%file1' => $before->getFileUri(), '%file2' => $after->getFileUri()]), 'File unchanged');
$this->assertEqual($before->getMimeType(), $after->getMimeType(), t('File MIME type is the same: %file1 == %file2.', ['%file1' => $before->getMimeType(), '%file2' => $after->getMimeType()]), 'File unchanged');
$this->assertEqual($before->getSize(), $after->getSize(), t('File size is the same: %file1 == %file2.', ['%file1' => $before->getSize(), '%file2' => $after->getSize()]), 'File unchanged');
$this->assertEqual($before->isPermanent(), $after->isPermanent(), t('File status is the same: %file1 == %file2.', ['%file1' => $before->isPermanent(), '%file2' => $after->isPermanent()]), 'File unchanged');
}
/**
* Asserts that two files are not the same by comparing the fid and filepath.
*
* @param \Drupal\file\FileInterface $file1
* File object to compare.
* @param \Drupal\file\FileInterface $file2
* File object to compare.
*/
public function assertDifferentFile(FileInterface $file1, FileInterface $file2) {
$this->assertNotEqual($file1->id(), $file2->id(), t('Files have different ids: %file1 != %file2.', ['%file1' => $file1->id(), '%file2' => $file2->id()]), 'Different file');
$this->assertNotEqual($file1->getFileUri(), $file2->getFileUri(), t('Files have different paths: %file1 != %file2.', ['%file1' => $file1->getFileUri(), '%file2' => $file2->getFileUri()]), 'Different file');
}
/**
* Asserts that two files are the same by comparing the fid and filepath.
*
* @param \Drupal\file\FileInterface $file1
* File object to compare.
* @param \Drupal\file\FileInterface $file2
* File object to compare.
*/
public function assertSameFile(FileInterface $file1, FileInterface $file2) {
$this->assertEqual($file1->id(), $file2->id(), t('Files have the same ids: %file1 == %file2.', ['%file1' => $file1->id(), '%file2-fid' => $file2->id()]), 'Same file');
$this->assertEqual($file1->getFileUri(), $file2->getFileUri(), t('Files have the same path: %file1 == %file2.', ['%file1' => $file1->getFileUri(), '%file2' => $file2->getFileUri()]), 'Same file');
}
/**
* Create a file and save it to the files table and assert that it occurs
* correctly.
*
* @param string $filepath
* Optional string specifying the file path. If none is provided then a
* randomly named file will be created in the site's files directory.
* @param string $contents
* Optional contents to save into the file. If a NULL value is provided an
* arbitrary string will be used.
* @param string $scheme
* Optional string indicating the stream scheme to use. Drupal core includes
* public, private, and temporary. The public wrapper is the default.
* @return \Drupal\file\FileInterface
* File entity.
*/
public function createFile($filepath = NULL, $contents = NULL, $scheme = NULL) {
// Don't count hook invocations caused by creating the file.
\Drupal::state()->set('file_test.count_hook_invocations', FALSE);
$file = File::create([
'uri' => $this->createUri($filepath, $contents, $scheme),
'uid' => 1,
]);
$file->save();
// Write the record directly rather than using the API so we don't invoke
// the hooks.
$this->assertTrue($file->id() > 0, 'The file was added to the database.', 'Create test file');
\Drupal::state()->set('file_test.count_hook_invocations', TRUE);
return $file;
}
/**
* Creates a file and returns its URI.
*
* @param string $filepath
* Optional string specifying the file path. If none is provided then a
* randomly named file will be created in the site's files directory.
* @param string $contents
* Optional contents to save into the file. If a NULL value is provided an
* arbitrary string will be used.
* @param string $scheme
* Optional string indicating the stream scheme to use. Drupal core includes
* public, private, and temporary. The public wrapper is the default.
*
* @return string
* File URI.
*/
public function createUri($filepath = NULL, $contents = NULL, $scheme = NULL) {
if (!isset($filepath)) {
// Prefix with non-latin characters to ensure that all file-related
// tests work with international filenames.
$filepath = 'Файл для тестирования ' . $this->randomMachineName();
}
if (!isset($scheme)) {
$scheme = file_default_scheme();
}
$filepath = $scheme . '://' . $filepath;
if (!isset($contents)) {
$contents = "file_put_contents() doesn't seem to appreciate empty strings so let's put in some data.";
}
file_put_contents($filepath, $contents);
$this->assertTrue(is_file($filepath), t('The test file exists on the disk.'), 'Create test file');
return $filepath;
}
}

View file

@ -0,0 +1,22 @@
{#
/**
* @file
* Default theme implementation to display the file entity as an audio tag.
*
* Available variables:
* - attributes: An array of HTML attributes, intended to be added to the
* audio tag.
* - files: And array of files to be added as sources for the audio tag. Each
* element is an array with the following elements:
* - file: The full file object.
* - source_attributes: An array of HTML attributes for to be added to the
* source tag.
*
* @ingroup themeable
*/
#}
<audio {{ attributes }}>
{% for file in files %}
<source {{ file.source_attributes }} />
{% endfor %}
</audio>

View file

@ -0,0 +1,15 @@
{#
/**
* @file
* Default theme implementation for a link to a file.
*
* Available variables:
* - attributes: The HTML attributes for the containing element.
* - link: A link to the file.
*
* @see template_preprocess_file_link()
*
* @ingroup themeable
*/
#}
<span{{ attributes }}>{{ link }}</span>

View file

@ -0,0 +1,23 @@
{#
/**
* @file
* Default theme implementation to display a file form widget.
*
* Available variables:
* - element: Form element for the file upload.
* - attributes: HTML attributes for the containing element.
*
* @see template_preprocess_file_managed_file()
*
* @ingroup themeable
*/
#}
{%
set classes = [
'js-form-managed-file',
'form-managed-file',
]
%}
<div{{ attributes.addClass(classes) }}>
{{ element }}
</div>

View file

@ -0,0 +1,14 @@
{#
/**
* @file
* Default theme implementation to display help text for file fields.
*
* Available variables:
* - descriptions: Lines of help text for uploading a file.
*
* @see template_preprocess_file_upload_help()
*
* @ingroup themeable
*/
#}
{{ descriptions|safe_join('<br />') }}

View file

@ -0,0 +1,22 @@
{#
/**
* @file
* Default theme implementation to display the file entity as a video tag.
*
* Available variables:
* - attributes: An array of HTML attributes, intended to be added to the
* video tag.
* - files: And array of files to be added as sources for the video tag. Each
* element is an array with the following elements:
* - file: The full file object.
* - source_attributes: An array of HTML attributes for to be added to the
* source tag.
*
* @ingroup themeable
*/
#}
<video {{ attributes }}>
{% for file in files %}
<source {{ file.source_attributes }} />
{% endfor %}
</video>

View file

@ -0,0 +1,16 @@
{#
/**
* @file
* Default theme implementation to display a multi file form widget.
*
* Available variables:
* - table: Table of previously uploaded files.
* - element: The form element for uploading another file.
*
* @see template_preprocess_file_widget_multiple()
*
* @ingroup themeable
*/
#}
{{ table }}
{{ element }}

View file

@ -0,0 +1,6 @@
name: 'File test'
type: module
description: 'Provides hooks for testing File module functionality.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,10 @@
file_module_test.managed_test:
path: '/file/test/{tree}/{extended}/{multiple}/{default_fids}'
defaults:
_form: '\Drupal\file_module_test\Form\FileModuleTestForm'
tree: TRUE
extended: TRUE
multiple: FALSE
default_fids: NULL
requirements:
_access: 'TRUE'

View file

@ -0,0 +1,91 @@
<?php
namespace Drupal\file_module_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form controller for file_module_test module.
*
* @internal
*/
class FileModuleTestForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'file_module_test_form';
}
/**
* {@inheritdoc}
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param bool $tree
* (optional) If the form should use #tree. Defaults to TRUE.
* @param bool $extended
* (optional) If the form should use #extended. Defaults to TRUE.
* @param bool $multiple
* (optional) If the form should use #multiple. Defaults to FALSE.
* @param array $default_fids
* (optional) Any default file IDs to use.
*/
public function buildForm(array $form, FormStateInterface $form_state, $tree = TRUE, $extended = TRUE, $multiple = FALSE, $default_fids = NULL) {
$form['#tree'] = (bool) $tree;
$form['nested']['file'] = [
'#type' => 'managed_file',
'#title' => $this->t('Managed <em>@type</em>', ['@type' => 'file & butter']),
'#upload_location' => 'public://test',
'#progress_message' => $this->t('Please wait...'),
'#extended' => (bool) $extended,
'#size' => 13,
'#multiple' => (bool) $multiple,
];
if ($default_fids) {
$default_fids = explode(',', $default_fids);
$form['nested']['file']['#default_value'] = $extended ? ['fids' => $default_fids] : $default_fids;
}
$form['textfield'] = [
'#type' => 'textfield',
'#title' => $this->t('Type a value and ensure it stays'),
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form['#tree']) {
$uploads = $form_state->getValue(['nested', 'file']);
}
else {
$uploads = $form_state->getValue('file');
}
if ($form['nested']['file']['#extended']) {
$uploads = $uploads['fids'];
}
$fids = [];
foreach ($uploads as $fid) {
$fids[] = $fid;
}
\Drupal::messenger()->addStatus($this->t('The file ids are %fids.', ['%fids' => implode(',', $fids)]));
}
}

View file

@ -0,0 +1,6 @@
name: 'File test'
type: module
description: 'Support module for file handling tests.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,351 @@
<?php
/**
* @file
* Helper module for the file tests.
*
* The caller is must call file_test_reset() to initializing this module before
* calling file_test_get_calls() or file_test_set_return().
*/
use Drupal\file\Entity\File;
const FILE_URL_TEST_CDN_1 = 'http://cdn1.example.com';
const FILE_URL_TEST_CDN_2 = 'http://cdn2.example.com';
/**
* Reset/initialize the history of calls to the file_* hooks.
*
* @see file_test_get_calls()
* @see file_test_reset()
*/
function file_test_reset() {
// Keep track of calls to these hooks
$results = [
'load' => [],
'validate' => [],
'download' => [],
'insert' => [],
'update' => [],
'copy' => [],
'move' => [],
'delete' => [],
];
\Drupal::state()->set('file_test.results', $results);
// These hooks will return these values, see file_test_set_return().
$return = [
'validate' => [],
'download' => NULL,
];
\Drupal::state()->set('file_test.return', $return);
}
/**
* Get the arguments passed to invocation of a given hook since
* file_test_reset() was last called.
*
* @param string $op
* One of the hook_file_* operations: 'load', 'validate', 'download',
* 'insert', 'update', 'copy', 'move', 'delete'.
*
* @return array
* Array of the parameters passed to each call.
*
* @see _file_test_log_call()
* @see file_test_reset()
*/
function file_test_get_calls($op) {
$results = \Drupal::state()->get('file_test.results') ?: [];
return $results[$op];
}
/**
* Get an array with the calls for all hooks.
*
* @return
* An array keyed by hook name ('load', 'validate', 'download', 'insert',
* 'update', 'copy', 'move', 'delete') with values being arrays of parameters
* passed to each call.
*/
function file_test_get_all_calls() {
return \Drupal::state()->get('file_test.results') ?: [];
}
/**
* Store the values passed to a hook invocation.
*
* @param string $op
* One of the hook_file_* operations: 'load', 'validate', 'download',
* 'insert', 'update', 'copy', 'move', 'delete'.
* @param array $args
* Values passed to hook.
*
* @see file_test_get_calls()
* @see file_test_reset()
*/
function _file_test_log_call($op, $args) {
if (\Drupal::state()->get('file_test.count_hook_invocations', TRUE)) {
$results = \Drupal::state()->get('file_test.results') ?: [];
$results[$op][] = $args;
\Drupal::state()->set('file_test.results', $results);
}
}
/**
* Load the appropriate return value.
*
* @param string $op
* One of the hook_file_[validate,download] operations.
*
* @return mixed
* Value set by file_test_set_return().
*
* @see file_test_set_return()
* @see file_test_reset()
*/
function _file_test_get_return($op) {
$return = \Drupal::state()->get('file_test.return') ?: [$op => NULL];
return $return[$op];
}
/**
* Assign a return value for a given operation.
*
* @param string $op
* One of the hook_file_[validate,download] operations.
* @param mixed $value
* Value for the hook to return.
*
* @see _file_test_get_return()
* @see file_test_reset()
*/
function file_test_set_return($op, $value) {
$return = \Drupal::state()->get('file_test.return') ?: [];
$return[$op] = $value;
\Drupal::state()->set('file_test.return', $return);
}
/**
* Implements hook_ENTITY_TYPE_load() for file entities.
*/
function file_test_file_load($files) {
foreach ($files as $file) {
_file_test_log_call('load', [$file->id()]);
// Assign a value on the object so that we can test that the $file is passed
// by reference.
$file->file_test['loaded'] = TRUE;
}
}
/**
* Implements hook_file_validate().
*/
function file_test_file_validate(File $file) {
_file_test_log_call('validate', [$file->id()]);
return _file_test_get_return('validate');
}
/**
* Implements hook_file_download().
*/
function file_test_file_download($uri) {
if (\Drupal::state()->get('file_test.allow_all', FALSE)) {
$files = entity_load_multiple_by_properties('file', ['uri' => $uri]);
$file = reset($files);
return file_get_content_headers($file);
}
_file_test_log_call('download', [$uri]);
return _file_test_get_return('download');
}
/**
* Implements hook_ENTITY_TYPE_insert() for file entities.
*/
function file_test_file_insert(File $file) {
_file_test_log_call('insert', [$file->id()]);
}
/**
* Implements hook_ENTITY_TYPE_update() for file entities.
*/
function file_test_file_update(File $file) {
_file_test_log_call('update', [$file->id()]);
}
/**
* Implements hook_file_copy().
*/
function file_test_file_copy(File $file, $source) {
_file_test_log_call('copy', [$file->id(), $source->id()]);
}
/**
* Implements hook_file_move().
*/
function file_test_file_move(File $file, File $source) {
_file_test_log_call('move', [$file->id(), $source->id()]);
}
/**
* Implements hook_ENTITY_TYPE_predelete() for file entities.
*/
function file_test_file_predelete(File $file) {
_file_test_log_call('delete', [$file->id()]);
}
/**
* Implements hook_file_url_alter().
*/
function file_test_file_url_alter(&$uri) {
// Only run this hook when this variable is set. Otherwise, we'd have to add
// another hidden test module just for this hook.
$alter_mode = \Drupal::state()->get('file_test.hook_file_url_alter');
if (!$alter_mode) {
return;
}
// Test alteration of file URLs to use a CDN.
elseif ($alter_mode == 'cdn') {
$cdn_extensions = ['css', 'js', 'gif', 'jpg', 'jpeg', 'png'];
// Most CDNs don't support private file transfers without a lot of hassle,
// so don't support this in the common case.
$schemes = ['public'];
$scheme = file_uri_scheme($uri);
// Only serve shipped files and public created files from the CDN.
if (!$scheme || in_array($scheme, $schemes)) {
// Shipped files.
if (!$scheme) {
$path = $uri;
}
// Public created files.
else {
$wrapper = \Drupal::service('stream_wrapper_manager')->getViaScheme($scheme);
$path = $wrapper->getDirectoryPath() . '/' . file_uri_target($uri);
}
// Clean up Windows paths.
$path = str_replace('\\', '/', $path);
// Serve files with one of the CDN extensions from CDN 1, all others from
// CDN 2.
$pathinfo = pathinfo($path);
if (array_key_exists('extension', $pathinfo) && in_array($pathinfo['extension'], $cdn_extensions)) {
$uri = FILE_URL_TEST_CDN_1 . '/' . $path;
}
else {
$uri = FILE_URL_TEST_CDN_2 . '/' . $path;
}
}
}
// Test alteration of file URLs to use root-relative URLs.
elseif ($alter_mode == 'root-relative') {
// Only serve shipped files and public created files with root-relative
// URLs.
$scheme = file_uri_scheme($uri);
if (!$scheme || $scheme == 'public') {
// Shipped files.
if (!$scheme) {
$path = $uri;
}
// Public created files.
else {
$wrapper = \Drupal::service('stream_wrapper_manager')->getViaScheme($scheme);
$path = $wrapper->getDirectoryPath() . '/' . file_uri_target($uri);
}
// Clean up Windows paths.
$path = str_replace('\\', '/', $path);
// Generate a root-relative URL.
$uri = base_path() . '/' . $path;
}
}
// Test alteration of file URLs to use protocol-relative URLs.
elseif ($alter_mode == 'protocol-relative') {
// Only serve shipped files and public created files with protocol-relative
// URLs.
$scheme = file_uri_scheme($uri);
if (!$scheme || $scheme == 'public') {
// Shipped files.
if (!$scheme) {
$path = $uri;
}
// Public created files.
else {
$wrapper = \Drupal::service('stream_wrapper_manager')->getViaScheme($scheme);
$path = $wrapper->getDirectoryPath() . '/' . file_uri_target($uri);
}
// Clean up Windows paths.
$path = str_replace('\\', '/', $path);
// Generate a protocol-relative URL.
$uri = '/' . base_path() . '/' . $path;
}
}
}
/**
* Implements hook_file_mimetype_mapping_alter().
*/
function file_test_file_mimetype_mapping_alter(&$mapping) {
// Add new mappings.
$mapping['mimetypes']['file_test_mimetype_1'] = 'madeup/file_test_1';
$mapping['mimetypes']['file_test_mimetype_2'] = 'madeup/file_test_2';
$mapping['mimetypes']['file_test_mimetype_3'] = 'madeup/doc';
$mapping['extensions']['file_test_1'] = 'file_test_mimetype_1';
$mapping['extensions']['file_test_2'] = 'file_test_mimetype_2';
$mapping['extensions']['file_test_3'] = 'file_test_mimetype_2';
// Override existing mapping.
$mapping['extensions']['doc'] = 'file_test_mimetype_3';
}
/**
* Helper validator that returns the $errors parameter.
*/
function file_test_validator(File $file, $errors) {
return $errors;
}
/**
* Helper function for testing file_scan_directory().
*
* Each time the function is called the file is stored in a static variable.
* When the function is called with no $filepath parameter, the results are
* returned.
*
* @param string|null $filepath
* File path
* @return array
* If $filepath is NULL, an array of all previous $filepath parameters
*/
function file_test_file_scan_callback($filepath = NULL) {
$files = &drupal_static(__FUNCTION__, []);
if (isset($filepath)) {
$files[] = $filepath;
}
else {
return $files;
}
}
/**
* Reset static variables used by file_test_file_scan_callback().
*/
function file_test_file_scan_callback_reset() {
drupal_static_reset('file_test_file_scan_callback');
}
/**
* Implements hook_entity_info_alter().
*/
function file_test_entity_type_alter(&$entity_types) {
if (\Drupal::state()->get('file_test_alternate_access_handler', FALSE)) {
/** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
$entity_types['file']
->setAccessClass('Drupal\file_test\FileTestAccessControlHandler');
}
}

View file

@ -0,0 +1,12 @@
file.test:
path: '/file-test/upload'
defaults:
_form: 'Drupal\file_test\Form\FileTestForm'
requirements:
_access: 'TRUE'
file.save_upload_from_form_test:
path: '/file-test/save_upload_from_form_test'
defaults:
_form: 'Drupal\file_test\Form\FileTestSaveUploadFromForm'
requirements:
_access: 'TRUE'

View file

@ -0,0 +1,13 @@
services:
stream_wrapper.dummy_readonly:
class: Drupal\file_test\StreamWrapper\DummyReadOnlyStreamWrapper
tags:
- { name: stream_wrapper, scheme: dummy-readonly }
stream_wrapper.dummy_remote:
class: Drupal\file_test\StreamWrapper\DummyRemoteStreamWrapper
tags:
- { name: stream_wrapper, scheme: dummy-remote }
stream_wrapper.dummy:
class: Drupal\file_test\StreamWrapper\DummyStreamWrapper
tags:
- { name: stream_wrapper, scheme: dummy }

Some files were not shown because too many files have changed in this diff Show more