Move into nested docroot
This commit is contained in:
parent
83a0d3a149
commit
c8b70abde9
13405 changed files with 0 additions and 0 deletions
6
web/core/modules/file/config/install/file.settings.yml
Normal file
6
web/core/modules/file/config/install/file.settings.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
description:
|
||||
type: 'textfield'
|
||||
length: 128
|
||||
icon:
|
||||
directory: 'core/modules/file/icons'
|
||||
|
1121
web/core/modules/file/config/optional/views.view.files.yml
Normal file
1121
web/core/modules/file/config/optional/views.view.files.yml
Normal file
File diff suppressed because it is too large
Load diff
123
web/core/modules/file/config/schema/file.schema.yml
Normal file
123
web/core/modules/file/config/schema/file.schema.yml
Normal file
|
@ -0,0 +1,123 @@
|
|||
# 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'
|
||||
|
||||
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: 'Entity reference selection 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'
|
||||
|
||||
field.formatter.settings.file_default:
|
||||
type: mapping
|
||||
label: 'Generic file format settings'
|
||||
|
||||
field.formatter.settings.file_rss_enclosure:
|
||||
type: mapping
|
||||
label: 'RSS enclosure format settings'
|
||||
|
||||
field.formatter.settings.file_table:
|
||||
type: mapping
|
||||
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'
|
42
web/core/modules/file/config/schema/file.views.schema.yml
Normal file
42
web/core/modules/file/config/schema/file.views.schema.yml
Normal 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'
|
82
web/core/modules/file/file.api.php
Normal file
82
web/core/modules/file/file.api.php
Normal 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 = array();
|
||||
|
||||
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', array('%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', array('%source' => $source->filename, '%destination' => $file->getFilename()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
209
web/core/modules/file/file.field.inc
Normal file
209
web/core/modules/file/file.field.inc
Normal 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 = array();
|
||||
$headers[] = t('File information');
|
||||
if ($element['#display_field']) {
|
||||
$headers[] = array(
|
||||
'data' => t('Display'),
|
||||
'class' => array('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 = array();
|
||||
foreach (Element::children($element) as $key) {
|
||||
$widgets[] = &$element[$key];
|
||||
}
|
||||
usort($widgets, '_field_multiple_value_form_sort_helper');
|
||||
|
||||
$rows = array();
|
||||
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 = array();
|
||||
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'] = array();
|
||||
$information = drupal_render($widget);
|
||||
$display = '';
|
||||
if ($element['#display_field']) {
|
||||
unset($widget['display']['#title']);
|
||||
$display = array(
|
||||
'data' => render($widget['display']),
|
||||
'class' => array('checkbox'),
|
||||
);
|
||||
}
|
||||
$widget['_weight']['#attributes']['class'] = array($weight_class);
|
||||
$weight = render($widget['_weight']);
|
||||
|
||||
// Arrange the row with all of the rendered columns.
|
||||
$row = array();
|
||||
$row[] = $information;
|
||||
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[] = array(
|
||||
'data' => $operations_elements,
|
||||
);
|
||||
$rows[] = array(
|
||||
'data' => $row,
|
||||
'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], array('draggable')) : array('draggable'),
|
||||
);
|
||||
}
|
||||
|
||||
$variables['table'] = array(
|
||||
'#type' => 'table',
|
||||
'#header' => $headers,
|
||||
'#rows' => $rows,
|
||||
'#attributes' => array(
|
||||
'id' => $table_id,
|
||||
),
|
||||
'#tabledrag' => array(
|
||||
array(
|
||||
'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 = array();
|
||||
|
||||
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.', array('@size' => format_size($upload_validators['file_validate_size'][0])));
|
||||
}
|
||||
if (isset($upload_validators['file_validate_extensions'])) {
|
||||
$descriptions[] = t('Allowed types: @extensions.', array('@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.', array('@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.', array('@min' => $min, '@max' => $max));
|
||||
}
|
||||
elseif ($min) {
|
||||
$descriptions[] = t('Images must be larger than <strong>@min</strong> pixels.', array('@min' => $min));
|
||||
}
|
||||
elseif ($max) {
|
||||
$descriptions[] = t('Images larger than <strong>@max</strong> pixels will be resized.', array('@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;
|
||||
}
|
8
web/core/modules/file/file.info.yml
Normal file
8
web/core/modules/file/file.info.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
name: File
|
||||
type: module
|
||||
description: 'Defines a file field type.'
|
||||
package: Field types
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- field
|
118
web/core/modules/file/file.install
Normal file
118
web/core/modules/file/file.install
Normal file
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for File module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_schema().
|
||||
*/
|
||||
function file_schema() {
|
||||
$schema['file_usage'] = array(
|
||||
'description' => 'Track where a file is used.',
|
||||
'fields' => array(
|
||||
'fid' => array(
|
||||
'description' => 'File ID.',
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'module' => array(
|
||||
'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' => array(
|
||||
'description' => 'The name of the object type in which the file is used.',
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 64,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
'id' => array(
|
||||
'description' => 'The primary key of the object using the file.',
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 64,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
),
|
||||
'count' => array(
|
||||
'description' => 'The number of times this file is used by this object.',
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
),
|
||||
),
|
||||
'primary key' => array('fid', 'type', 'id', 'module'),
|
||||
'indexes' => array(
|
||||
'type_id' => array('type', 'id'),
|
||||
'fid_count' => array('fid', 'count'),
|
||||
'fid_module' => array('fid', 'module'),
|
||||
),
|
||||
);
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_requirements().
|
||||
*
|
||||
* Display information about getting upload progress bars working.
|
||||
*/
|
||||
function file_requirements($phase) {
|
||||
$requirements = array();
|
||||
|
||||
// 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'] = array(
|
||||
'title' => t('Upload progress'),
|
||||
'value' => $value,
|
||||
'description' => $description,
|
||||
);
|
||||
}
|
||||
|
||||
return $requirements;
|
||||
}
|
257
web/core/modules/file/file.js
Normal file
257
web/core/modules/file/file.js
Normal file
|
@ -0,0 +1,257 @@
|
|||
/**
|
||||
* @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) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 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: function (context, settings) {
|
||||
var $context = $(context);
|
||||
var 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: function (context, settings, trigger) {
|
||||
var $context = $(context);
|
||||
var 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: function (context) {
|
||||
$(context).find('input[type="file"]').once('auto-file-upload').on('change.autoFileUpload', Drupal.file.triggerUploadButton);
|
||||
},
|
||||
detach: function (context, setting, 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: function (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 (context) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: function (context) {
|
||||
$(context).find('div.js-form-managed-file .file a').on('click', Drupal.file.openInNewWindow);
|
||||
},
|
||||
detach: function (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: function (event) {
|
||||
event.preventDefault();
|
||||
// Remove any previous errors.
|
||||
$('.file-upload-js-error').remove();
|
||||
|
||||
// Add client side validation for the input[type=file].
|
||||
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.', {
|
||||
// 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: function (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: function (event) {
|
||||
var $clickedButton = $(this).findOnce('ajax');
|
||||
|
||||
// Only disable upload fields for Ajax buttons.
|
||||
if (!$clickedButton.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're working with an "Upload" button.
|
||||
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');
|
||||
}
|
||||
|
||||
// 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?
|
||||
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);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add progress bar support if possible.
|
||||
*
|
||||
* @name Drupal.file.progressBar
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered, most likely a `mousedown` event.
|
||||
*/
|
||||
progressBar: function (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');
|
||||
|
||||
// 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(function () {
|
||||
$progressId.attr('name', originalName);
|
||||
}, 1000);
|
||||
}
|
||||
// Show the progress bar if the upload takes longer than half a second.
|
||||
setTimeout(function () {
|
||||
$clickedButton.closest('div.js-form-managed-file').find('div.ajax-progress-bar').slideDown();
|
||||
}, 500);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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: function (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);
|
9
web/core/modules/file/file.libraries.yml
Normal file
9
web/core/modules/file/file.libraries.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
drupal.file:
|
||||
version: VERSION
|
||||
js:
|
||||
file.js: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/jquery.once
|
||||
- core/drupal
|
||||
- core/drupalSettings
|
1562
web/core/modules/file/file.module
Normal file
1562
web/core/modules/file/file.module
Normal file
File diff suppressed because it is too large
Load diff
2
web/core/modules/file/file.permissions.yml
Normal file
2
web/core/modules/file/file.permissions.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
access files overview:
|
||||
title: 'Access the Files overview page'
|
6
web/core/modules/file/file.routing.yml
Normal file
6
web/core/modules/file/file.routing.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
file.ajax_progress:
|
||||
path: '/file/progress/{key}'
|
||||
defaults:
|
||||
_controller: '\Drupal\file\Controller\FileWidgetAjaxController::progress'
|
||||
requirements:
|
||||
_permission: 'access content'
|
6
web/core/modules/file/file.services.yml
Normal file
6
web/core/modules/file/file.services.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
services:
|
||||
file.usage:
|
||||
class: Drupal\file\FileUsage\DatabaseFileUsageBackend
|
||||
arguments: ['@database']
|
||||
tags:
|
||||
- { name: backend_overridable }
|
70
web/core/modules/file/file.views.inc
Normal file
70
web/core/modules/file/file.views.inc
Normal 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'] = array(
|
||||
'id' => 'standard',
|
||||
'base' => 'file_managed',
|
||||
'entity type' => 'file',
|
||||
'base field' => 'fid',
|
||||
'label' => t('file from @field_name', array('@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'] = array(
|
||||
'title' => t('@entity using @field', array('@entity' => $entity_type->getLabel(), '@field' => $label)),
|
||||
'label' => t('@field_name', array('@field_name' => $field_name)),
|
||||
'group' => $entity_type->getLabel(),
|
||||
'help' => t('Relate each @entity with a @field set to the file.', array('@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' => array(
|
||||
0 => array(
|
||||
'field' => 'deleted',
|
||||
'value' => 0,
|
||||
'numeric' => TRUE,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
48
web/core/modules/file/migration_templates/d6_file.yml
Normal file
48
web/core/modules/file/migration_templates/d6_file.yml
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Every migration that references a file by Drupal 6 fid should specify this
|
||||
# migration as an optional dependency.
|
||||
id: d6_file
|
||||
label: Files
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
source:
|
||||
plugin: d6_file
|
||||
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 both this migration and d6_user_picture_file in a custom
|
||||
# migration and executing migrations incrementally, it is recommended that
|
||||
# you remove the fid mapping here to avoid potential ID conflicts.
|
||||
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
|
||||
filesize: filesize
|
||||
status: status
|
||||
changed: timestamp
|
||||
uid: uid
|
||||
destination:
|
||||
plugin: entity:file
|
27
web/core/modules/file/migration_templates/d6_upload.yml
Normal file
27
web/core/modules/file/migration_templates/d6_upload.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
id: d6_upload
|
||||
label: File uploads
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
source:
|
||||
plugin: d6_upload
|
||||
process:
|
||||
nid: nid
|
||||
vid: vid
|
||||
type: type
|
||||
upload:
|
||||
plugin: iterator
|
||||
source: upload
|
||||
process:
|
||||
target_id:
|
||||
plugin: migration
|
||||
migration: d6_file
|
||||
source: fid
|
||||
display: list
|
||||
description: description
|
||||
destination:
|
||||
plugin: entity:node
|
||||
migration_dependencies:
|
||||
required:
|
||||
- d6_file
|
||||
- d6_node
|
||||
- d6_upload_field_instance
|
|
@ -0,0 +1,27 @@
|
|||
id: d6_upload_entity_display
|
||||
label: Upload display configuration
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
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: node_type
|
||||
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
|
|
@ -0,0 +1,28 @@
|
|||
id: d6_upload_entity_form_display
|
||||
label: Upload form display configuration
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
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: node_type
|
||||
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
|
|
@ -0,0 +1,26 @@
|
|||
id: d6_upload_field
|
||||
label: Upload field configuration
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
source:
|
||||
# We do an empty source and a proper destination to have an idmap for
|
||||
# migration_dependencies.
|
||||
plugin: md_empty
|
||||
provider: 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
|
|
@ -0,0 +1,31 @@
|
|||
id: d6_upload_field_instance
|
||||
label: Upload field instance configuration
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
source:
|
||||
plugin: d6_upload_instance
|
||||
constants:
|
||||
entity_type: node
|
||||
name: upload
|
||||
settings:
|
||||
description_field: 1
|
||||
process:
|
||||
entity_type: 'constants/entity_type'
|
||||
bundle:
|
||||
-
|
||||
plugin: migration
|
||||
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
|
45
web/core/modules/file/migration_templates/d7_file.yml
Normal file
45
web/core/modules/file/migration_templates/d7_file.yml
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Every migration that references a file by Drupal 7 fid should specify this
|
||||
# migration as an optional dependency.
|
||||
id: d7_file
|
||||
label: Files
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
source:
|
||||
plugin: d7_file
|
||||
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
|
||||
-
|
||||
plugin: urlencode
|
||||
uri:
|
||||
plugin: file_copy
|
||||
source:
|
||||
- '@source_full_path'
|
||||
- uri
|
||||
filemime: filemime
|
||||
# filesize is dynamically computed when file entities are saved, so there is
|
||||
# no point in migrating it.
|
||||
# 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
|
18
web/core/modules/file/migration_templates/file_settings.yml
Normal file
18
web/core/modules/file/migration_templates/file_settings.yml
Normal file
|
@ -0,0 +1,18 @@
|
|||
id: file_settings
|
||||
label: File configuration
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- Drupal 7
|
||||
source:
|
||||
plugin: variable
|
||||
variables:
|
||||
- file_description_type
|
||||
- file_description_length
|
||||
- file_icon_directory
|
||||
process:
|
||||
'description/type': file_description_type
|
||||
'description/length': file_description_length
|
||||
'icon/directory': file_icon_directory
|
||||
destination:
|
||||
plugin: config
|
||||
config_name: file.settings
|
|
@ -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 = array(
|
||||
'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)', array('@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)', array('@current' => format_size($status['current']), '@total' => format_size($status['total'])));
|
||||
$progress['percentage'] = round(100 * $status['current'] / $status['total']);
|
||||
}
|
||||
}
|
||||
|
||||
return new JsonResponse($progress);
|
||||
}
|
||||
|
||||
}
|
444
web/core/modules/file/src/Element/ManagedFile.php
Normal file
444
web/core/modules/file/src/Element/ManagedFile.php
Normal file
|
@ -0,0 +1,444 @@
|
|||
<?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\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'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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'], array('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'));
|
||||
|
||||
// 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]);
|
||||
}
|
||||
|
||||
// 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($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'] = array(
|
||||
'#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) {
|
||||
// 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.
|
||||
$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 ($file->isPermanent()) {
|
||||
$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');
|
||||
}
|
||||
|
||||
}
|
277
web/core/modules/file/src/Entity/File.php
Normal file
277
web/core/modules/file/src/Entity/File.php
Normal file
|
@ -0,0 +1,277 @@
|
|||
<?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"),
|
||||
* 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 = array()) {
|
||||
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();
|
||||
if ($size = @filesize($uri)) {
|
||||
$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('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;
|
||||
}
|
||||
|
||||
}
|
104
web/core/modules/file/src/FileAccessControlHandler.php
Normal file
104
web/core/modules/file/src/FileAccessControlHandler.php
Normal file
|
@ -0,0 +1,104 @@
|
|||
<?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') {
|
||||
// Always allow access to file in public file system.
|
||||
return AccessResult::allowed();
|
||||
}
|
||||
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 always access if it's not yet used.
|
||||
return AccessResult::allowed();
|
||||
}
|
||||
}
|
||||
|
||||
if ($operation == 'delete' || $operation == 'update') {
|
||||
$account = $this->prepareUser($account);
|
||||
$file_uid = $entity->get('uid')->getValue();
|
||||
// Only the file owner can delete and update the file entity.
|
||||
if ($account->id() == $file_uid[0]['target_id']) {
|
||||
return AccessResult::allowed();
|
||||
}
|
||||
return AccessResult::forbidden();
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// No user can edit the status of a file. Prevents saving a new file as
|
||||
// persistent before even validating it.
|
||||
if ($field_definition->getName() === 'status' && $operation === 'edit') {
|
||||
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.
|
||||
// @todo Update comment to mention REST module when
|
||||
// https://www.drupal.org/node/1927648 is fixed.
|
||||
return AccessResult::neutral();
|
||||
}
|
||||
|
||||
}
|
|
@ -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 { }
|
119
web/core/modules/file/src/FileInterface.php
Normal file
119
web/core/modules/file/src/FileInterface.php
Normal 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();
|
||||
|
||||
}
|
25
web/core/modules/file/src/FileStorage.php
Normal file
25
web/core/modules/file/src/FileStorage.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
27
web/core/modules/file/src/FileStorageInterface.php
Normal file
27
web/core/modules/file/src/FileStorageInterface.php
Normal 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);
|
||||
|
||||
}
|
36
web/core/modules/file/src/FileStorageSchema.php
Normal file
36
web/core/modules/file/src/FileStorageSchema.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?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 == 'file_managed') {
|
||||
switch ($field_name) {
|
||||
case 'status':
|
||||
case 'changed':
|
||||
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
|
||||
break;
|
||||
|
||||
case 'uri':
|
||||
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
}
|
111
web/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php
Normal file
111
web/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php
Normal file
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\FileUsage;
|
||||
|
||||
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'.
|
||||
*/
|
||||
public function __construct(Connection $connection, $table = 'file_usage') {
|
||||
$this->connection = $connection;
|
||||
|
||||
$this->tableName = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function add(FileInterface $file, $module, $type, $id, $count = 1) {
|
||||
$this->connection->merge($this->tableName)
|
||||
->keys(array(
|
||||
'fid' => $file->id(),
|
||||
'module' => $module,
|
||||
'type' => $type,
|
||||
'id' => $id,
|
||||
))
|
||||
->fields(array('count' => $count))
|
||||
->expression('count', 'count + :count', array(':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', array(':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', array('module', 'type', 'id', 'count'))
|
||||
->condition('fid', $file->id())
|
||||
->condition('count', 0, '>')
|
||||
->execute();
|
||||
$references = array();
|
||||
foreach ($result as $usage) {
|
||||
$references[$usage->module][$usage->type][$usage->id] = $usage->count;
|
||||
}
|
||||
return $references;
|
||||
}
|
||||
|
||||
}
|
36
web/core/modules/file/src/FileUsage/FileUsageBase.php
Normal file
36
web/core/modules/file/src/FileUsage/FileUsageBase.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\FileUsage;
|
||||
|
||||
use Drupal\file\FileInterface;
|
||||
|
||||
/**
|
||||
* Defines the base class for database file usage backend.
|
||||
*/
|
||||
abstract class FileUsageBase implements FileUsageInterface {
|
||||
|
||||
/**
|
||||
* {@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) {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
70
web/core/modules/file/src/FileUsage/FileUsageInterface.php
Normal file
70
web/core/modules/file/src/FileUsage/FileUsageInterface.php
Normal 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 int $id
|
||||
* The unique, numeric 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 int $id
|
||||
* (optional) The unique, numeric 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);
|
||||
|
||||
}
|
324
web/core/modules/file/src/FileViewsData.php
Normal file
324
web/core/modules/file/src/FileViewsData.php
Normal 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'] = array(
|
||||
'id' => 'file_fid',
|
||||
// The field to display in the summary.
|
||||
'name field' => 'filename',
|
||||
'numeric' => TRUE,
|
||||
);
|
||||
$data['file_managed']['fid']['relationship'] = array(
|
||||
'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'] = array(
|
||||
'title' => $this->t('Extension'),
|
||||
'help' => $this->t('The extension of the file.'),
|
||||
'real field' => 'filename',
|
||||
'field' => array(
|
||||
'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'] = array(
|
||||
'file_managed' => array(
|
||||
'field' => 'fid',
|
||||
'left_field' => 'fid',
|
||||
),
|
||||
// Link ourselves to the {node_field_data} table
|
||||
// so we can provide node->file relationships.
|
||||
'node_field_data' => array(
|
||||
'field' => 'id',
|
||||
'left_field' => 'nid',
|
||||
'extra' => array(array('field' => 'type', 'value' => 'node')),
|
||||
),
|
||||
// Link ourselves to the {users_field_data} table
|
||||
// so we can provide user->file relationships.
|
||||
'users_field_data' => array(
|
||||
'field' => 'id',
|
||||
'left_field' => 'uid',
|
||||
'extra' => array(array('field' => 'type', 'value' => 'user')),
|
||||
),
|
||||
// Link ourselves to the {comment_field_data} table
|
||||
// so we can provide comment->file relationships.
|
||||
'comment' => array(
|
||||
'field' => 'id',
|
||||
'left_field' => 'cid',
|
||||
'extra' => array(array('field' => 'type', 'value' => 'comment')),
|
||||
),
|
||||
// Link ourselves to the {taxonomy_term_field_data} table
|
||||
// so we can provide taxonomy_term->file relationships.
|
||||
'taxonomy_term_data' => array(
|
||||
'field' => 'id',
|
||||
'left_field' => 'tid',
|
||||
'extra' => array(array('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'] = array(
|
||||
'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' => array('node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'),
|
||||
'real field' => 'id',
|
||||
'relationship' => array(
|
||||
'title' => $this->t('Content'),
|
||||
'label' => $this->t('Content'),
|
||||
'base' => 'node_field_data',
|
||||
'base field' => 'nid',
|
||||
'relationship field' => 'id',
|
||||
'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'node')),
|
||||
),
|
||||
);
|
||||
$data['file_usage']['node_to_file'] = array(
|
||||
'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' => array('file_managed', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'),
|
||||
'real field' => 'fid',
|
||||
'relationship' => array(
|
||||
'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'] = array(
|
||||
'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' => array('node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'),
|
||||
'real field' => 'id',
|
||||
'relationship' => array(
|
||||
'title' => $this->t('User'),
|
||||
'label' => $this->t('User'),
|
||||
'base' => 'users',
|
||||
'base field' => 'uid',
|
||||
'relationship field' => 'id',
|
||||
'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'user')),
|
||||
),
|
||||
);
|
||||
$data['file_usage']['user_to_file'] = array(
|
||||
'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' => array('file_managed', 'node_field_data', 'node_field_revision', 'comment_field_data', 'taxonomy_term_field_data'),
|
||||
'real field' => 'fid',
|
||||
'relationship' => array(
|
||||
'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'] = array(
|
||||
'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' => array('node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'),
|
||||
'real field' => 'id',
|
||||
'relationship' => array(
|
||||
'title' => $this->t('Comment'),
|
||||
'label' => $this->t('Comment'),
|
||||
'base' => 'comment_field_data',
|
||||
'base field' => 'cid',
|
||||
'relationship field' => 'id',
|
||||
'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'comment')),
|
||||
),
|
||||
);
|
||||
$data['file_usage']['comment_to_file'] = array(
|
||||
'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' => array('file_managed', 'node_field_data', 'node_field_revision', 'users_field_data', 'taxonomy_term_field_data'),
|
||||
'real field' => 'fid',
|
||||
'relationship' => array(
|
||||
'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'] = array(
|
||||
'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' => array('node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'),
|
||||
'real field' => 'id',
|
||||
'relationship' => array(
|
||||
'title' => $this->t('Taxonomy Term'),
|
||||
'label' => $this->t('Taxonomy Term'),
|
||||
'base' => 'taxonomy_term_data',
|
||||
'base field' => 'tid',
|
||||
'relationship field' => 'id',
|
||||
'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'taxonomy_term')),
|
||||
),
|
||||
);
|
||||
$data['file_usage']['taxonomy_term_to_file'] = array(
|
||||
'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' => array('file_managed', 'node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data'),
|
||||
'real field' => 'fid',
|
||||
'relationship' => array(
|
||||
'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'] = array(
|
||||
'title' => $this->t('Module'),
|
||||
'help' => $this->t('The module managing this file relationship.'),
|
||||
'field' => array(
|
||||
'id' => 'standard',
|
||||
),
|
||||
'filter' => array(
|
||||
'id' => 'string',
|
||||
),
|
||||
'argument' => array(
|
||||
'id' => 'string',
|
||||
),
|
||||
'sort' => array(
|
||||
'id' => 'standard',
|
||||
),
|
||||
);
|
||||
$data['file_usage']['type'] = array(
|
||||
'title' => $this->t('Entity type'),
|
||||
'help' => $this->t('The type of entity that is related to the file.'),
|
||||
'field' => array(
|
||||
'id' => 'standard',
|
||||
),
|
||||
'filter' => array(
|
||||
'id' => 'string',
|
||||
),
|
||||
'argument' => array(
|
||||
'id' => 'string',
|
||||
),
|
||||
'sort' => array(
|
||||
'id' => 'standard',
|
||||
),
|
||||
);
|
||||
$data['file_usage']['id'] = array(
|
||||
'title' => $this->t('Entity ID'),
|
||||
'help' => $this->t('The ID of the entity that is related to the file.'),
|
||||
'field' => array(
|
||||
'id' => 'numeric',
|
||||
),
|
||||
'argument' => array(
|
||||
'id' => 'numeric',
|
||||
),
|
||||
'filter' => array(
|
||||
'id' => 'numeric',
|
||||
),
|
||||
'sort' => array(
|
||||
'id' => 'standard',
|
||||
),
|
||||
);
|
||||
$data['file_usage']['count'] = array(
|
||||
'title' => $this->t('Use count'),
|
||||
'help' => $this->t('The number of times the file is used by this entity.'),
|
||||
'field' => array(
|
||||
'id' => 'numeric',
|
||||
),
|
||||
'filter' => array(
|
||||
'id' => 'numeric',
|
||||
),
|
||||
'sort' => array(
|
||||
'id' => 'standard',
|
||||
),
|
||||
);
|
||||
$data['file_usage']['entity_label'] = array(
|
||||
'title' => $this->t('Entity label'),
|
||||
'help' => $this->t('The label of the entity that is related to the file.'),
|
||||
'real field' => 'id',
|
||||
'field' => array(
|
||||
'id' => 'entity_label',
|
||||
'entity type field' => 'type',
|
||||
),
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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'] = array(
|
||||
'#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';
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<?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"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
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';
|
||||
}
|
||||
|
||||
}
|
|
@ -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'] = array(
|
||||
'#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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 FileFormatterBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function viewElements(FieldItemListInterface $items, $langcode) {
|
||||
$elements = array();
|
||||
|
||||
foreach ($this->getEntitiesToView($items, $langcode) as $delta => $file) {
|
||||
$item = $file->_referringItem;
|
||||
$elements[$delta] = array(
|
||||
'#theme' => 'file_link',
|
||||
'#file' => $file,
|
||||
'#description' => $item->description,
|
||||
'#cache' => array(
|
||||
'tags' => $file->getCacheTags(),
|
||||
),
|
||||
);
|
||||
// Pass field item attributes to the theme function.
|
||||
if (isset($item->_attributes)) {
|
||||
$elements[$delta] += array('#attributes' => array());
|
||||
$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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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[] = array(
|
||||
'key' => 'enclosure',
|
||||
'attributes' => array(
|
||||
// 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 [];
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<?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 FileFormatterBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function viewElements(FieldItemListInterface $items, $langcode) {
|
||||
$elements = array();
|
||||
|
||||
if ($files = $this->getEntitiesToView($items, $langcode)) {
|
||||
$header = array(t('Attachment'), t('Size'));
|
||||
$rows = array();
|
||||
foreach ($files as $delta => $file) {
|
||||
$rows[] = array(
|
||||
array(
|
||||
'data' => array(
|
||||
'#theme' => 'file_link',
|
||||
'#file' => $file,
|
||||
'#cache' => array(
|
||||
'tags' => $file->getCacheTags(),
|
||||
),
|
||||
),
|
||||
),
|
||||
array('data' => format_size($file->getSize())),
|
||||
);
|
||||
}
|
||||
|
||||
$elements[0] = array();
|
||||
if (!empty($rows)) {
|
||||
$elements[0] = array(
|
||||
'#theme' => 'table__file_formatter_table',
|
||||
'#header' => $header,
|
||||
'#rows' => $rows,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 = array();
|
||||
|
||||
foreach ($this->getEntitiesToView($items, $langcode) as $delta => $file) {
|
||||
$elements[$delta] = array(
|
||||
'#markup' => file_url_transform_relative(file_create_url($file->getFileUri())),
|
||||
'#cache' => array(
|
||||
'tags' => $file->getCacheTags(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 = array();
|
||||
|
||||
/** @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 = array();
|
||||
$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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
360
web/core/modules/file/src/Plugin/Field/FieldType/FileItem.php
Normal file
360
web/core/modules/file/src/Plugin/Field/FieldType/FileItem.php
Normal file
|
@ -0,0 +1,360 @@
|
|||
<?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 array(
|
||||
'target_type' => 'file',
|
||||
'display_field' => FALSE,
|
||||
'display_default' => FALSE,
|
||||
'uri_scheme' => file_default_scheme(),
|
||||
) + parent::defaultStorageSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function defaultFieldSettings() {
|
||||
return array(
|
||||
'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 array(
|
||||
'columns' => array(
|
||||
'target_id' => array(
|
||||
'description' => 'The ID of the file entity.',
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
),
|
||||
'display' => array(
|
||||
'description' => 'Flag to control whether this file should be displayed when viewing content.',
|
||||
'type' => 'int',
|
||||
'size' => 'tiny',
|
||||
'unsigned' => TRUE,
|
||||
'default' => 1,
|
||||
),
|
||||
'description' => array(
|
||||
'description' => 'A description of the file.',
|
||||
'type' => 'text',
|
||||
),
|
||||
),
|
||||
'indexes' => array(
|
||||
'target_id' => array('target_id'),
|
||||
),
|
||||
'foreign keys' => array(
|
||||
'target_id' => array(
|
||||
'table' => 'file_managed',
|
||||
'columns' => array('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 = array();
|
||||
|
||||
$element['#attached']['library'][] = 'file/drupal.file';
|
||||
|
||||
$element['display_field'] = array(
|
||||
'#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'] = array(
|
||||
'#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' => array(
|
||||
'visible' => array(
|
||||
':input[name="settings[display_field]"]' => array('checked' => TRUE),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$scheme_options = \Drupal::service('stream_wrapper_manager')->getNames(StreamWrapperInterface::WRITE_VISIBLE);
|
||||
$element['uri_scheme'] = array(
|
||||
'#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 = array();
|
||||
$settings = $this->getSettings();
|
||||
|
||||
$element['file_directory'] = array(
|
||||
'#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' => array(array(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'] = array(
|
||||
'#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' => array(array(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'] = array(
|
||||
'#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>).', array('%limit' => format_size(file_upload_max_size()))),
|
||||
'#size' => 10,
|
||||
'#element_validate' => array(array(get_class($this), 'validateMaxFilesize')),
|
||||
'#weight' => 5,
|
||||
);
|
||||
|
||||
$element['description_field'] = array(
|
||||
'#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).', array('@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 token_replace()
|
||||
*/
|
||||
public function getUploadLocation($data = array()) {
|
||||
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.
|
||||
*/
|
||||
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 = array();
|
||||
$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'] = array($max_filesize);
|
||||
|
||||
// Add the extension check if necessary.
|
||||
if (!empty($settings['file_extensions'])) {
|
||||
$validators['file_validate_extensions'] = array($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 = array(
|
||||
'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 [];
|
||||
}
|
||||
|
||||
}
|
|
@ -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 array(
|
||||
'progress_indicator' => 'throbber',
|
||||
) + parent::defaultSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state) {
|
||||
$element['progress_indicator'] = array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('Progress indicator'),
|
||||
'#options' => array(
|
||||
'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 = array();
|
||||
$summary[] = t('Progress indicator: @progress_indicator', array('@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 = array();
|
||||
|
||||
$delta = 0;
|
||||
// Add an element for every existing item.
|
||||
foreach ($items as $item) {
|
||||
$element = array(
|
||||
'#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'] = array(
|
||||
'#type' => 'weight',
|
||||
'#title' => t('Weight for row @number', array('@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 = array(
|
||||
'#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'] = array('details');
|
||||
$elements['#process'] = array(array(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() + array('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'] = array(
|
||||
'#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 += array(
|
||||
'display_default' => NULL,
|
||||
'display_field' => NULL,
|
||||
'description_field' => NULL,
|
||||
);
|
||||
|
||||
$cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
|
||||
$defaults = array(
|
||||
'fids' => array(),
|
||||
'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 += array(
|
||||
'#type' => 'managed_file',
|
||||
'#upload_location' => $items[$delta]->getUploadLocation(),
|
||||
'#upload_validators' => $items[$delta]->getUploadValidators(),
|
||||
'#value_callback' => array(get_class($this), 'value'),
|
||||
'#process' => array_merge($element_info['#process'], array(array(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 = array($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 = array(
|
||||
'#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'] = array(array(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 = array();
|
||||
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 = FALSE, 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 += array(
|
||||
'fids' => array(),
|
||||
'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 = array();
|
||||
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_set_message($message, 'warning');
|
||||
$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'] = array(
|
||||
'#type' => empty($item['fids']) ? 'hidden' : 'checkbox',
|
||||
'#title' => t('Include file in display'),
|
||||
'#attributes' => array('class' => array('file-display')),
|
||||
);
|
||||
if (isset($item['display'])) {
|
||||
$element['display']['#value'] = $item['display'] ? '1' : '';
|
||||
}
|
||||
else {
|
||||
$element['display']['#value'] = $element['#display_default'];
|
||||
}
|
||||
}
|
||||
else {
|
||||
$element['display'] = array(
|
||||
'#type' => 'hidden',
|
||||
'#value' => '1',
|
||||
);
|
||||
}
|
||||
|
||||
// Add the description field if enabled.
|
||||
if ($element['#description_field'] && $item['fids']) {
|
||||
$config = \Drupal::config('file.settings');
|
||||
$element['description'] = array(
|
||||
'#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 = array(
|
||||
'query' => array(
|
||||
'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 (array('upload_button', 'remove_button') as $key) {
|
||||
$element[$key]['#submit'][] = array(get_called_class(), 'submit');
|
||||
$element[$key]['#limit_validation_errors'] = array(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'] = array(
|
||||
'#type' => 'weight',
|
||||
'#title' => $description ? t('Weight for @title', array('@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'] = array(
|
||||
'#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 = array();
|
||||
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'] = array($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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?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.
|
||||
$file = $value->get('entity')->getTarget()->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Plugin\migrate\cckfield\d6;
|
||||
|
||||
use Drupal\migrate\Plugin\MigrationInterface;
|
||||
use Drupal\migrate\Row;
|
||||
use Drupal\migrate_drupal\Plugin\migrate\cckfield\CckFieldPluginBase;
|
||||
|
||||
/**
|
||||
* @MigrateCckField(
|
||||
* id = "filefield",
|
||||
* core = {6}
|
||||
* )
|
||||
*/
|
||||
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';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Plugin\migrate\cckfield\d7;
|
||||
|
||||
use Drupal\migrate\Plugin\MigrationInterface;
|
||||
use Drupal\migrate\Row;
|
||||
use Drupal\migrate_drupal\Plugin\migrate\cckfield\CckFieldPluginBase;
|
||||
|
||||
/**
|
||||
* @MigrateCckField(
|
||||
* id = "file",
|
||||
* core = {7}
|
||||
* )
|
||||
*/
|
||||
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' => 'iterator',
|
||||
'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';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Plugin\migrate\cckfield\d7;
|
||||
|
||||
use Drupal\migrate\Plugin\MigrationInterface;
|
||||
use Drupal\migrate_drupal\Plugin\migrate\cckfield\CckFieldPluginBase;
|
||||
|
||||
/**
|
||||
* @MigrateCckField(
|
||||
* id = "image",
|
||||
* core = {7}
|
||||
* )
|
||||
*/
|
||||
class ImageField extends CckFieldPluginBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFieldFormatterMap() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function processCckFieldValues(MigrationInterface $migration, $field_name, $data) {
|
||||
$process = [
|
||||
'plugin' => 'iterator',
|
||||
'source' => $field_name,
|
||||
'process' => [
|
||||
'target_id' => 'fid',
|
||||
'alt' => 'alt',
|
||||
'title' => 'title',
|
||||
'width' => 'width',
|
||||
'height' => 'height',
|
||||
],
|
||||
];
|
||||
$migration->mergeProcessOfProperty($field_name, $process);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Plugin\migrate\destination;
|
||||
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
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 = Unicode::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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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_cck_file"
|
||||
* )
|
||||
*/
|
||||
class CckFile extends ProcessPluginBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The migration process plugin, configured for lookups in d6_file.
|
||||
*
|
||||
* @var \Drupal\migrate\Plugin\MigrateProcessInterface
|
||||
*/
|
||||
protected $migrationPlugin;
|
||||
|
||||
/**
|
||||
* Constructs a CckFile 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 [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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, '/');
|
||||
}
|
||||
|
||||
}
|
98
web/core/modules/file/src/Plugin/migrate/source/d6/File.php
Normal file
98
web/core/modules/file/src/Plugin/migrate/source/d6/File.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?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"
|
||||
* )
|
||||
*/
|
||||
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')
|
||||
->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 array(
|
||||
'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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
<?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_provider = "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', array('nid', 'vid'));
|
||||
$query->innerJoin('node', 'n', static::JOIN);
|
||||
$query->addField('n', 'type');
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prepareRow(Row $row) {
|
||||
$query = $this->select('upload', 'u')
|
||||
->fields('u', array('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 array(
|
||||
'fid' => $this->t('The file Id.'),
|
||||
'nid' => $this->t('The node Id.'),
|
||||
'vid' => $this->t('The version Id.'),
|
||||
'type' => $this->t('The node type'),
|
||||
'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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
<?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_provider = "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 = array();
|
||||
$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;
|
||||
if (isset($values[$name])) {
|
||||
$enabled = 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 array(
|
||||
'node_type' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function fields() {
|
||||
return array(
|
||||
'node_type' => $this->t('Node type'),
|
||||
'max_filesize' => $this->t('Max filesize'),
|
||||
'file_extensions' => $this->t('File extensions'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function count() {
|
||||
return count($this->initializeIterator());
|
||||
}
|
||||
|
||||
}
|
115
web/core/modules/file/src/Plugin/migrate/source/d7/File.php
Normal file
115
web/core/modules/file/src/Plugin/migrate/source/d7/File.php
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?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"
|
||||
* )
|
||||
*/
|
||||
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')
|
||||
->orderBy('f.timestamp');
|
||||
|
||||
// Filter by scheme(s), if configured.
|
||||
if (isset($this->configuration['scheme'])) {
|
||||
$schemes = array();
|
||||
// Accept either a single scheme, or a list.
|
||||
foreach ((array) $this->configuration['scheme'] as $scheme) {
|
||||
$schemes[] = rtrim($scheme) . '://';
|
||||
}
|
||||
$schemes = array_map([$this->getDatabase(), 'escapeLike'], $schemes);
|
||||
|
||||
// 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 array(
|
||||
'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;
|
||||
}
|
||||
|
||||
}
|
83
web/core/modules/file/src/Plugin/views/argument/Fid.php
Normal file
83
web/core/modules/file/src/Plugin/views/argument/Fid.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Plugin\views\argument;
|
||||
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\Entity\Query\QueryFactory;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The entity query factory service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\Query\QueryFactory
|
||||
*/
|
||||
protected $entityQuery;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param \Drupal\Core\Entity\Query\QueryFactory $entity_query
|
||||
* The entity query factory.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, QueryFactory $entity_query) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->entityManager = $entity_manager;
|
||||
$this->entityQuery = $entity_query;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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'),
|
||||
$container->get('entity.query')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the behavior of titleQuery(). Get the filenames.
|
||||
*/
|
||||
public function titleQuery() {
|
||||
$fids = $this->entityQuery->get('file')
|
||||
->condition('fid', $this->value, 'IN')
|
||||
->execute();
|
||||
$controller = $this->entityManager->getStorage('file');
|
||||
$files = $controller->loadMultiple($fids);
|
||||
$titles = array();
|
||||
foreach ($files as $file) {
|
||||
$titles[] = $file->getFilename();
|
||||
}
|
||||
return $titles;
|
||||
}
|
||||
|
||||
}
|
87
web/core/modules/file/src/Plugin/views/field/File.php
Normal file
87
web/core/modules/file/src/Plugin/views/field/File.php
Normal 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'] = array('default' => FALSE);
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide link to file option
|
||||
*/
|
||||
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
|
||||
$form['link_to_file'] = array(
|
||||
'#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);
|
||||
}
|
||||
|
||||
}
|
23
web/core/modules/file/src/Plugin/views/filter/Status.php
Normal file
23
web/core/modules/file/src/Plugin/views/filter/Status.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
58
web/core/modules/file/src/Plugin/views/wizard/File.php
Normal file
58
web/core/modules/file/src/Plugin/views/wizard/File.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
172
web/core/modules/file/src/Tests/DownloadTest.php
Normal file
172
web/core/modules/file/src/Tests/DownloadTest.php
Normal file
|
@ -0,0 +1,172 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
/**
|
||||
* Tests for download/file transfer functions.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class DownloadTest extends FileManagedTestBase {
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
// Clear out any hook calls.
|
||||
file_test_reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the public file transfer system.
|
||||
*/
|
||||
function testPublicFileTransfer() {
|
||||
// Test generating a URL to a created file.
|
||||
$file = $this->createFile();
|
||||
$url = file_create_url($file->getFileUri());
|
||||
// URLs can't contain characters outside the ASCII set so $filename has to be
|
||||
// encoded.
|
||||
$filename = $GLOBALS['base_url'] . '/' . \Drupal::service('stream_wrapper_manager')->getViaScheme('public')->getDirectoryPath() . '/' . rawurlencode($file->getFilename());
|
||||
$this->assertEqual($filename, $url, 'Correctly generated a URL for a created file.');
|
||||
$this->drupalHead($url);
|
||||
$this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the created file.');
|
||||
|
||||
// Test generating a URL to a shipped file (i.e. a file that is part of
|
||||
// Drupal core, a module or a theme, for example a JavaScript file).
|
||||
$filepath = 'core/assets/vendor/jquery/jquery.min.js';
|
||||
$url = file_create_url($filepath);
|
||||
$this->assertEqual($GLOBALS['base_url'] . '/' . $filepath, $url, 'Correctly generated a URL for a shipped file.');
|
||||
$this->drupalHead($url);
|
||||
$this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the private file transfer system.
|
||||
*/
|
||||
public function testPrivateFileTransferWithoutPageCache() {
|
||||
$this->doPrivateFileTransferTest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the private file transfer system.
|
||||
*/
|
||||
protected function doPrivateFileTransferTest() {
|
||||
// Set file downloads to private so handler functions get called.
|
||||
|
||||
// Create a file.
|
||||
$contents = $this->randomMachineName(8);
|
||||
$file = $this->createFile(NULL, $contents, 'private');
|
||||
// Created private files without usage are by default not accessible
|
||||
// for a user different from the owner, but createFile always uses uid 1
|
||||
// as the owner of the files. Therefore make it permanent to allow access
|
||||
// if a module allows it.
|
||||
$file->setPermanent();
|
||||
$file->save();
|
||||
|
||||
$url = file_create_url($file->getFileUri());
|
||||
|
||||
// Set file_test access header to allow the download.
|
||||
file_test_set_return('download', array('x-foo' => 'Bar'));
|
||||
$this->drupalGet($url);
|
||||
$this->assertEqual($this->drupalGetHeader('x-foo'), 'Bar', 'Found header set by file_test module on private download.');
|
||||
$this->assertFalse($this->drupalGetHeader('x-drupal-cache'), 'Page cache is disabled on private file download.');
|
||||
$this->assertResponse(200, 'Correctly allowed access to a file when file_test provides headers.');
|
||||
|
||||
// Test that the file transferred correctly.
|
||||
$this->assertEqual($contents, $this->content, 'Contents of the file are correct.');
|
||||
|
||||
// Deny access to all downloads via a -1 header.
|
||||
file_test_set_return('download', -1);
|
||||
$this->drupalHead($url);
|
||||
$this->assertResponse(403, 'Correctly denied access to a file when file_test sets the header to -1.');
|
||||
|
||||
// Try non-existent file.
|
||||
$url = file_create_url('private://' . $this->randomMachineName());
|
||||
$this->drupalHead($url);
|
||||
$this->assertResponse(404, 'Correctly returned 404 response for a non-existent file.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test file_create_url().
|
||||
*/
|
||||
function testFileCreateUrl() {
|
||||
|
||||
// Tilde (~) is excluded from this test because it is encoded by
|
||||
// rawurlencode() in PHP 5.2 but not in PHP 5.3, as per RFC 3986.
|
||||
// @see http://php.net/manual/function.rawurlencode.php#86506
|
||||
$basename = " -._!$'\"()*@[]?&+%#,;=:\n\x00" . // "Special" ASCII characters.
|
||||
"%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string.
|
||||
"éøïвβ中國書۞"; // Characters from various non-ASCII alphabets.
|
||||
$basename_encoded = '%20-._%21%24%27%22%28%29%2A%40%5B%5D%3F%26%2B%25%23%2C%3B%3D%3A__' .
|
||||
'%2523%2525%2526%252B%252F%253F' .
|
||||
'%C3%A9%C3%B8%C3%AF%D0%B2%CE%B2%E4%B8%AD%E5%9C%8B%E6%9B%B8%DB%9E';
|
||||
|
||||
// Public files should not be served by Drupal, so their URLs should not be
|
||||
// routed through Drupal, whereas private files should be served by Drupal,
|
||||
// so they need to be. The difference is most apparent when $script_path
|
||||
// is not empty (i.e., when not using clean URLs).
|
||||
$clean_url_settings = array(
|
||||
'clean' => '',
|
||||
'unclean' => 'index.php/',
|
||||
);
|
||||
$public_directory_path = \Drupal::service('stream_wrapper_manager')->getViaScheme('public')->getDirectoryPath();
|
||||
foreach ($clean_url_settings as $clean_url_setting => $script_path) {
|
||||
$clean_urls = $clean_url_setting == 'clean';
|
||||
$request = $this->prepareRequestForGenerator($clean_urls);
|
||||
$base_path = $request->getSchemeAndHttpHost() . $request->getBasePath();
|
||||
$this->checkUrl('public', '', $basename, $base_path . '/' . $public_directory_path . '/' . $basename_encoded);
|
||||
$this->checkUrl('private', '', $basename, $base_path . '/' . $script_path . 'system/files/' . $basename_encoded);
|
||||
}
|
||||
$this->assertEqual(file_create_url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='), 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==', t('Generated URL matches expected URL.'));
|
||||
// Test public files with a different host name from settings.
|
||||
$test_base_url = 'http://www.example.com/cdn';
|
||||
$this->settingsSet('file_public_base_url', $test_base_url);
|
||||
$filepath = file_create_filename('test.txt', '');
|
||||
$directory_uri = 'public://' . dirname($filepath);
|
||||
file_prepare_directory($directory_uri, FILE_CREATE_DIRECTORY);
|
||||
$file = $this->createFile($filepath, NULL, 'public');
|
||||
$url = file_create_url($file->getFileUri());
|
||||
$expected_url = $test_base_url . '/' . basename($filepath);
|
||||
$this->assertEqual($url, $expected_url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file from the URL generated by file_create_url().
|
||||
*
|
||||
* Create a file with the specified scheme, directory and filename; check that
|
||||
* the URL generated by file_create_url() for the specified file equals the
|
||||
* specified URL; fetch the URL and then compare the contents to the file.
|
||||
*
|
||||
* @param string $scheme
|
||||
* A scheme, e.g. "public".
|
||||
* @param string $directory
|
||||
* A directory, possibly "".
|
||||
* @param string $filename
|
||||
* A filename.
|
||||
* @param string $expected_url
|
||||
* The expected URL.
|
||||
*/
|
||||
private function checkUrl($scheme, $directory, $filename, $expected_url) {
|
||||
// Convert $filename to a valid filename, i.e. strip characters not
|
||||
// supported by the filesystem, and create the file in the specified
|
||||
// directory.
|
||||
$filepath = file_create_filename($filename, $directory);
|
||||
$directory_uri = $scheme . '://' . dirname($filepath);
|
||||
file_prepare_directory($directory_uri, FILE_CREATE_DIRECTORY);
|
||||
$file = $this->createFile($filepath, NULL, $scheme);
|
||||
|
||||
$url = file_create_url($file->getFileUri());
|
||||
$this->assertEqual($url, $expected_url);
|
||||
|
||||
if ($scheme == 'private') {
|
||||
// Tell the implementation of hook_file_download() in file_test.module
|
||||
// that this file may be downloaded.
|
||||
file_test_set_return('download', array('x-foo' => 'Bar'));
|
||||
}
|
||||
|
||||
$this->drupalGet($url);
|
||||
if ($this->assertResponse(200) == 'pass') {
|
||||
$this->assertRaw(file_get_contents($file->getFileUri()), 'Contents of the file are correct.');
|
||||
}
|
||||
|
||||
$file->delete();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\user\RoleInterface;
|
||||
use Drupal\file\Entity\File;
|
||||
|
||||
/**
|
||||
* Confirm that file field submissions work correctly for anonymous visitors.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class FileFieldAnonymousSubmissionTest extends FileFieldTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
// Set up permissions for anonymous attacker user.
|
||||
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, array(
|
||||
'create article content' => TRUE,
|
||||
'access content' => TRUE,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the basic node submission for an anonymous visitor.
|
||||
*/
|
||||
public function testAnonymousNode() {
|
||||
$bundle_label = 'Article';
|
||||
$node_title = 'test page';
|
||||
|
||||
// Load the node form.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/add/article');
|
||||
$this->assertResponse(200, 'Loaded the article node form.');
|
||||
$this->assertText(strip_tags(t('Create @name', array('@name' => $bundle_label))));
|
||||
|
||||
$edit = array(
|
||||
'title[0][value]' => $node_title,
|
||||
'body[0][value]' => 'Test article',
|
||||
);
|
||||
$this->drupalPostForm(NULL, $edit, 'Save');
|
||||
$this->assertResponse(200);
|
||||
$t_args = array('@type' => $bundle_label, '%title' => $node_title);
|
||||
$this->assertText(strip_tags(t('@type %title has been created.', $t_args)), 'The node was created.');
|
||||
$matches = array();
|
||||
if (preg_match('@node/(\d+)$@', $this->getUrl(), $matches)) {
|
||||
$nid = end($matches);
|
||||
$this->assertNotEqual($nid, 0, 'The node ID was extracted from the URL.');
|
||||
$node = Node::load($nid);
|
||||
$this->assertNotEqual($node, NULL, 'The node was loaded successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests file submission for an anonymous visitor.
|
||||
*/
|
||||
public function testAnonymousNodeWithFile() {
|
||||
$bundle_label = 'Article';
|
||||
$node_title = 'Test page';
|
||||
$this->createFileField('field_image', 'node', 'article', array(), array('file_extensions' => 'txt png'));
|
||||
|
||||
// Load the node form.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/add/article');
|
||||
$this->assertResponse(200, 'Loaded the article node form.');
|
||||
$this->assertText(strip_tags(t('Create @name', array('@name' => $bundle_label))));
|
||||
|
||||
// Generate an image file.
|
||||
$image = $this->getTestFile('image');
|
||||
|
||||
// Submit the form.
|
||||
$edit = array(
|
||||
'title[0][value]' => $node_title,
|
||||
'body[0][value]' => 'Test article',
|
||||
'files[field_image_0]' => $this->container->get('file_system')->realpath($image->getFileUri()),
|
||||
);
|
||||
$this->drupalPostForm(NULL, $edit, 'Save');
|
||||
$this->assertResponse(200);
|
||||
$t_args = array('@type' => $bundle_label, '%title' => $node_title);
|
||||
$this->assertText(strip_tags(t('@type %title has been created.', $t_args)), 'The node was created.');
|
||||
$matches = array();
|
||||
if (preg_match('@node/(\d+)$@', $this->getUrl(), $matches)) {
|
||||
$nid = end($matches);
|
||||
$this->assertNotEqual($nid, 0, 'The node ID was extracted from the URL.');
|
||||
$node = Node::load($nid);
|
||||
$this->assertNotEqual($node, NULL, 'The node was loaded successfully.');
|
||||
$this->assertFileExists(File::load($node->field_image->target_id), 'The image was uploaded successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests file submission for an anonymous visitor with a missing node title.
|
||||
*/
|
||||
public function testAnonymousNodeWithFileWithoutTitle() {
|
||||
$this->drupalLogout();
|
||||
$this->doTestNodeWithFileWithoutTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests file submission for an authenticated user with a missing node title.
|
||||
*/
|
||||
public function testAuthenticatedNodeWithFileWithoutTitle() {
|
||||
$admin_user = $this->drupalCreateUser(array(
|
||||
'bypass node access',
|
||||
'access content overview',
|
||||
'administer nodes',
|
||||
));
|
||||
$this->drupalLogin($admin_user);
|
||||
$this->doTestNodeWithFileWithoutTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to test file submissions with missing node titles.
|
||||
*/
|
||||
protected function doTestNodeWithFileWithoutTitle() {
|
||||
$bundle_label = 'Article';
|
||||
$node_title = 'Test page';
|
||||
$this->createFileField('field_image', 'node', 'article', array(), array('file_extensions' => 'txt png'));
|
||||
|
||||
// Load the node form.
|
||||
$this->drupalGet('node/add/article');
|
||||
$this->assertResponse(200, 'Loaded the article node form.');
|
||||
$this->assertText(strip_tags(t('Create @name', array('@name' => $bundle_label))));
|
||||
|
||||
// Generate an image file.
|
||||
$image = $this->getTestFile('image');
|
||||
|
||||
// Submit the form but exclude the title field.
|
||||
$edit = array(
|
||||
'body[0][value]' => 'Test article',
|
||||
'files[field_image_0]' => $this->container->get('file_system')->realpath($image->getFileUri()),
|
||||
);
|
||||
if (!$this->loggedInUser) {
|
||||
$label = 'Save';
|
||||
}
|
||||
else {
|
||||
$label = 'Save and publish';
|
||||
}
|
||||
$this->drupalPostForm(NULL, $edit, $label);
|
||||
$this->assertResponse(200);
|
||||
$t_args = array('@type' => $bundle_label, '%title' => $node_title);
|
||||
$this->assertNoText(strip_tags(t('@type %title has been created.', $t_args)), 'The node was created.');
|
||||
$this->assertText('Title field is required.');
|
||||
|
||||
// Submit the form again but this time with the missing title field. This
|
||||
// should still work.
|
||||
$edit = array(
|
||||
'title[0][value]' => $node_title,
|
||||
);
|
||||
$this->drupalPostForm(NULL, $edit, $label);
|
||||
|
||||
// Confirm the final submission actually worked.
|
||||
$t_args = array('@type' => $bundle_label, '%title' => $node_title);
|
||||
$this->assertText(strip_tags(t('@type %title has been created.', $t_args)), 'The node was created.');
|
||||
$matches = array();
|
||||
if (preg_match('@node/(\d+)$@', $this->getUrl(), $matches)) {
|
||||
$nid = end($matches);
|
||||
$this->assertNotEqual($nid, 0, 'The node ID was extracted from the URL.');
|
||||
$node = Node::load($nid);
|
||||
$this->assertNotEqual($node, NULL, 'The node was loaded successfully.');
|
||||
$this->assertFileExists(File::load($node->field_image->target_id), 'The image was uploaded successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
168
web/core/modules/file/src/Tests/FileFieldDisplayTest.php
Normal file
168
web/core/modules/file/src/Tests/FileFieldDisplayTest.php
Normal file
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\file\Entity\File;
|
||||
|
||||
/**
|
||||
* Tests the display of file fields in node and views.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class FileFieldDisplayTest extends FileFieldTestBase {
|
||||
|
||||
/**
|
||||
* Tests normal formatter display on node display.
|
||||
*/
|
||||
function testNodeDisplay() {
|
||||
$field_name = strtolower($this->randomMachineName());
|
||||
$type_name = 'article';
|
||||
$field_storage_settings = array(
|
||||
'display_field' => '1',
|
||||
'display_default' => '1',
|
||||
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
|
||||
);
|
||||
$field_settings = array(
|
||||
'description_field' => '1',
|
||||
);
|
||||
$widget_settings = array();
|
||||
$this->createFileField($field_name, 'node', $type_name, $field_storage_settings, $field_settings, $widget_settings);
|
||||
|
||||
// Create a new node *without* the file field set, and check that the field
|
||||
// is not shown for each node display.
|
||||
$node = $this->drupalCreateNode(array('type' => $type_name));
|
||||
// Check file_default last as the assertions below assume that this is the
|
||||
// case.
|
||||
$file_formatters = array('file_table', 'file_url_plain', 'hidden', 'file_default');
|
||||
foreach ($file_formatters as $formatter) {
|
||||
$edit = array(
|
||||
"fields[$field_name][type]" => $formatter,
|
||||
);
|
||||
$this->drupalPostForm("admin/structure/types/manage/$type_name/display", $edit, t('Save'));
|
||||
$this->drupalGet('node/' . $node->id());
|
||||
$this->assertNoText($field_name, format_string('Field label is hidden when no file attached for formatter %formatter', array('%formatter' => $formatter)));
|
||||
}
|
||||
|
||||
$test_file = $this->getTestFile('text');
|
||||
simpletest_generate_file('escaped-&-text', 64, 10, 'text');
|
||||
$test_file = File::create([
|
||||
'uri' => 'public://escaped-&-text.txt',
|
||||
'name' => 'escaped-&-text',
|
||||
'filesize' => filesize('public://escaped-&-text.txt'),
|
||||
]);
|
||||
|
||||
// Create a new node with the uploaded file.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
|
||||
// Check that the default formatter is displaying with the file name.
|
||||
$node_storage = $this->container->get('entity.manager')->getStorage('node');
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file = File::load($node->{$field_name}->target_id);
|
||||
$file_link = array(
|
||||
'#theme' => 'file_link',
|
||||
'#file' => $node_file,
|
||||
);
|
||||
$default_output = \Drupal::service('renderer')->renderRoot($file_link);
|
||||
$this->assertRaw($default_output, 'Default formatter displaying correctly on full node view.');
|
||||
|
||||
// Turn the "display" option off and check that the file is no longer displayed.
|
||||
$edit = array($field_name . '[0][display]' => FALSE);
|
||||
$this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save and keep published'));
|
||||
|
||||
$this->assertNoRaw($default_output, 'Field is hidden when "display" option is unchecked.');
|
||||
|
||||
// Add a description and make sure that it is displayed.
|
||||
$description = $this->randomMachineName();
|
||||
$edit = array(
|
||||
$field_name . '[0][description]' => $description,
|
||||
$field_name . '[0][display]' => TRUE,
|
||||
);
|
||||
$this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save and keep published'));
|
||||
$this->assertText($description);
|
||||
|
||||
// Ensure the filename in the link's title attribute is escaped.
|
||||
$this->assertRaw('title="escaped-&-text.txt"');
|
||||
|
||||
// Test that fields appear as expected after during the preview.
|
||||
// Add a second file.
|
||||
$name = 'files[' . $field_name . '_1][]';
|
||||
$edit[$name] = drupal_realpath($test_file->getFileUri());
|
||||
|
||||
// Uncheck the display checkboxes and go to the preview.
|
||||
$edit[$field_name . '[0][display]'] = FALSE;
|
||||
$edit[$field_name . '[1][display]'] = FALSE;
|
||||
$this->drupalPostForm("node/$nid/edit", $edit, t('Preview'));
|
||||
$this->clickLink(t('Back to content editing'));
|
||||
$this->assertRaw($field_name . '[0][display]', 'First file appears as expected.');
|
||||
$this->assertRaw($field_name . '[1][display]', 'Second file appears as expected.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests default display of File Field.
|
||||
*/
|
||||
function testDefaultFileFieldDisplay() {
|
||||
$field_name = strtolower($this->randomMachineName());
|
||||
$type_name = 'article';
|
||||
$field_storage_settings = array(
|
||||
'display_field' => '1',
|
||||
'display_default' => '0',
|
||||
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
|
||||
);
|
||||
$field_settings = array(
|
||||
'description_field' => '1',
|
||||
);
|
||||
$widget_settings = array();
|
||||
$this->createFileField($field_name, 'node', $type_name, $field_storage_settings, $field_settings, $widget_settings);
|
||||
|
||||
$test_file = $this->getTestFile('text');
|
||||
|
||||
// Create a new node with the uploaded file.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
|
||||
$this->drupalGet('node/' . $nid . '/edit');
|
||||
$this->assertFieldByXPath('//input[@type="checkbox" and @name="' . $field_name . '[0][display]"]', NULL, 'Default file display checkbox field exists.');
|
||||
$this->assertFieldByXPath('//input[@type="checkbox" and @name="' . $field_name . '[0][display]" and not(@checked)]', NULL, 'Default file display is off.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests description toggle for field instance configuration.
|
||||
*/
|
||||
function testDescToggle() {
|
||||
$type_name = 'test';
|
||||
$field_type = 'file';
|
||||
$field_name = strtolower($this->randomMachineName());
|
||||
// Use the UI to add a new content type that also contains a file field.
|
||||
$edit = array(
|
||||
'name' => $type_name,
|
||||
'type' => $type_name,
|
||||
);
|
||||
$this->drupalPostForm('admin/structure/types/add', $edit, t('Save and manage fields'));
|
||||
$edit = array(
|
||||
'new_storage_type' => $field_type,
|
||||
'field_name' => $field_name,
|
||||
'label' => $this->randomString(),
|
||||
);
|
||||
$this->drupalPostForm('/admin/structure/types/manage/' . $type_name . '/fields/add-field', $edit, t('Save and continue'));
|
||||
$this->drupalPostForm(NULL, array(), t('Save field settings'));
|
||||
// Ensure the description field is selected on the field instance settings
|
||||
// form. That's what this test is all about.
|
||||
$edit = array(
|
||||
'settings[description_field]' => TRUE,
|
||||
);
|
||||
$this->drupalPostForm(NULL, $edit, t('Save settings'));
|
||||
// Add a node of our new type and upload a file to it.
|
||||
$file = current($this->drupalGetTestFiles('text'));
|
||||
$title = $this->randomString();
|
||||
$edit = array(
|
||||
'title[0][value]' => $title,
|
||||
'files[field_' . $field_name . '_0]' => drupal_realpath($file->uri),
|
||||
);
|
||||
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
|
||||
$node = $this->drupalGetNodeByTitle($title);
|
||||
$this->drupalGet('node/' . $node->id() . '/edit');
|
||||
$this->assertText(t('The description may be used as the label of the link to the file.'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
|
||||
/**
|
||||
* Tests file formatter access.
|
||||
* @group file
|
||||
*/
|
||||
class FileFieldFormatterAccessTest extends FileFieldTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['node', 'file', 'field_ui', 'file_test'];
|
||||
|
||||
/**
|
||||
* Tests the custom access handler is invoked.
|
||||
*/
|
||||
public function testFileAccessHandler() {
|
||||
$type_name = 'article';
|
||||
$field_name = strtolower($this->randomMachineName());
|
||||
$this->createFileField($field_name, 'node', $type_name);
|
||||
\Drupal::state()->set('file_test_alternate_access_handler', TRUE);
|
||||
\Drupal::entityManager()->clearCachedDefinitions();
|
||||
$test_file = $this->getTestFile('text');
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
$this->drupalGet('node/' . $nid);
|
||||
$this->assertTrue(\Drupal::state()->get('file_access_formatter_check', FALSE));
|
||||
}
|
||||
|
||||
}
|
93
web/core/modules/file/src/Tests/FileFieldPathTest.php
Normal file
93
web/core/modules/file/src/Tests/FileFieldPathTest.php
Normal file
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\file\Entity\File;
|
||||
|
||||
/**
|
||||
* Tests that files are uploaded to proper locations.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class FileFieldPathTest extends FileFieldTestBase {
|
||||
/**
|
||||
* Tests the normal formatter display on node display.
|
||||
*/
|
||||
function testUploadPath() {
|
||||
/** @var \Drupal\node\NodeStorageInterface $node_storage */
|
||||
$node_storage = $this->container->get('entity.manager')->getStorage('node');
|
||||
$field_name = strtolower($this->randomMachineName());
|
||||
$type_name = 'article';
|
||||
$this->createFileField($field_name, 'node', $type_name);
|
||||
/** @var \Drupal\file\FileInterface $test_file */
|
||||
$test_file = $this->getTestFile('text');
|
||||
|
||||
// Create a new node.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
|
||||
// Check that the file was uploaded to the correct location.
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
/** @var \Drupal\file\FileInterface $node_file */
|
||||
$node_file = $node->{$field_name}->entity;
|
||||
$date_formatter = $this->container->get('date.formatter');
|
||||
$expected_filename =
|
||||
'public://' .
|
||||
$date_formatter->format(REQUEST_TIME, 'custom', 'Y') . '-' .
|
||||
$date_formatter->format(REQUEST_TIME, 'custom', 'm') . '/' .
|
||||
$test_file->getFilename();
|
||||
$this->assertPathMatch($expected_filename, $node_file->getFileUri(), format_string('The file %file was uploaded to the correct path.', array('%file' => $node_file->getFileUri())));
|
||||
|
||||
// Change the path to contain multiple subdirectories.
|
||||
$this->updateFileField($field_name, $type_name, array('file_directory' => 'foo/bar/baz'));
|
||||
|
||||
// Upload a new file into the subdirectories.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
|
||||
// Check that the file was uploaded into the subdirectory.
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file = File::load($node->{$field_name}->target_id);
|
||||
$this->assertPathMatch('public://foo/bar/baz/' . $test_file->getFilename(), $node_file->getFileUri(), format_string('The file %file was uploaded to the correct path.', array('%file' => $node_file->getFileUri())));
|
||||
|
||||
// Check the path when used with tokens.
|
||||
// Change the path to contain multiple token directories.
|
||||
$this->updateFileField($field_name, $type_name, array('file_directory' => '[current-user:uid]/[current-user:name]'));
|
||||
|
||||
// Upload a new file into the token subdirectories.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
|
||||
// Check that the file was uploaded into the subdirectory.
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file = File::load($node->{$field_name}->target_id);
|
||||
// Do token replacement using the same user which uploaded the file, not
|
||||
// the user running the test case.
|
||||
$data = array('user' => $this->adminUser);
|
||||
$subdirectory = \Drupal::token()->replace('[user:uid]/[user:name]', $data);
|
||||
$this->assertPathMatch('public://' . $subdirectory . '/' . $test_file->getFilename(), $node_file->getFileUri(), format_string('The file %file was uploaded to the correct path with token replacements.', array('%file' => $node_file->getFileUri())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a file is uploaded to the right location.
|
||||
*
|
||||
* @param string $expected_path
|
||||
* The location where the file is expected to be uploaded. Duplicate file
|
||||
* names to not need to be taken into account.
|
||||
* @param string $actual_path
|
||||
* Where the file was actually uploaded.
|
||||
* @param string $message
|
||||
* The message to display with this assertion.
|
||||
*/
|
||||
function assertPathMatch($expected_path, $actual_path, $message) {
|
||||
// Strip off the extension of the expected path to allow for _0, _1, etc.
|
||||
// suffixes when the file hits a duplicate name.
|
||||
$pos = strrpos($expected_path, '.');
|
||||
$base_path = substr($expected_path, 0, $pos);
|
||||
$extension = substr($expected_path, $pos + 1);
|
||||
|
||||
$result = preg_match('/' . preg_quote($base_path, '/') . '(_[0-9]+)?\.' . preg_quote($extension, '/') . '/', $actual_path);
|
||||
$this->assertTrue($result, $message);
|
||||
}
|
||||
|
||||
}
|
68
web/core/modules/file/src/Tests/FileFieldRSSContentTest.php
Normal file
68
web/core/modules/file/src/Tests/FileFieldRSSContentTest.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\file\Entity\File;
|
||||
|
||||
/**
|
||||
* Ensure that files added to nodes appear correctly in RSS feeds.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class FileFieldRSSContentTest extends FileFieldTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('node', 'views');
|
||||
|
||||
/**
|
||||
* Tests RSS enclosure formatter display for RSS feeds.
|
||||
*/
|
||||
function testFileFieldRSSContent() {
|
||||
$node_storage = $this->container->get('entity.manager')->getStorage('node');
|
||||
$field_name = strtolower($this->randomMachineName());
|
||||
$type_name = 'article';
|
||||
|
||||
$this->createFileField($field_name, 'node', $type_name);
|
||||
|
||||
// RSS display must be added manually.
|
||||
$this->drupalGet("admin/structure/types/manage/$type_name/display");
|
||||
$edit = array(
|
||||
"display_modes_custom[rss]" => '1',
|
||||
);
|
||||
$this->drupalPostForm(NULL, $edit, t('Save'));
|
||||
|
||||
// Change the format to 'RSS enclosure'.
|
||||
$this->drupalGet("admin/structure/types/manage/$type_name/display/rss");
|
||||
$edit = array("fields[$field_name][type]" => 'file_rss_enclosure');
|
||||
$this->drupalPostForm(NULL, $edit, t('Save'));
|
||||
|
||||
// Create a new node with a file field set. Promote to frontpage
|
||||
// needs to be set so this node will appear in the RSS feed.
|
||||
$node = $this->drupalCreateNode(array('type' => $type_name, 'promote' => 1));
|
||||
$test_file = $this->getTestFile('text');
|
||||
|
||||
// Create a new node with the uploaded file.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $node->id());
|
||||
|
||||
// Get the uploaded file from the node.
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file = File::load($node->{$field_name}->target_id);
|
||||
|
||||
// Check that the RSS enclosure appears in the RSS feed.
|
||||
$this->drupalGet('rss.xml');
|
||||
$uploaded_filename = str_replace('public://', '', $node_file->getFileUri());
|
||||
$selector = sprintf(
|
||||
'enclosure[url="%s"][length="%s"][type="%s"]',
|
||||
file_create_url("public://$uploaded_filename", array('absolute' => TRUE)),
|
||||
$node_file->getSize(),
|
||||
$node_file->getMimeType()
|
||||
);
|
||||
$this->assertTrue(!empty($this->cssSelect($selector)), 'File field RSS enclosure is displayed when viewing the RSS feed.');
|
||||
}
|
||||
|
||||
}
|
142
web/core/modules/file/src/Tests/FileFieldRevisionTest.php
Normal file
142
web/core/modules/file/src/Tests/FileFieldRevisionTest.php
Normal file
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\file\Entity\File;
|
||||
|
||||
/**
|
||||
* Tests creating and deleting revisions with files attached.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class FileFieldRevisionTest extends FileFieldTestBase {
|
||||
/**
|
||||
* Tests creating multiple revisions of a node and managing attached files.
|
||||
*
|
||||
* Expected behaviors:
|
||||
* - Adding a new revision will make another entry in the field table, but
|
||||
* the original file will not be duplicated.
|
||||
* - Deleting a revision should not delete the original file if the file
|
||||
* is in use by another revision.
|
||||
* - When the last revision that uses a file is deleted, the original file
|
||||
* should be deleted also.
|
||||
*/
|
||||
function testRevisions() {
|
||||
$node_storage = $this->container->get('entity.manager')->getStorage('node');
|
||||
$type_name = 'article';
|
||||
$field_name = strtolower($this->randomMachineName());
|
||||
$this->createFileField($field_name, 'node', $type_name);
|
||||
// Create the same fields for users.
|
||||
$this->createFileField($field_name, 'user', 'user');
|
||||
|
||||
$test_file = $this->getTestFile('text');
|
||||
|
||||
// Create a new node with the uploaded file.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
|
||||
// Check that the file exists on disk and in the database.
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file_r1 = File::load($node->{$field_name}->target_id);
|
||||
$node_vid_r1 = $node->getRevisionId();
|
||||
$this->assertFileExists($node_file_r1, 'New file saved to disk on node creation.');
|
||||
$this->assertFileEntryExists($node_file_r1, 'File entry exists in database on node creation.');
|
||||
$this->assertFileIsPermanent($node_file_r1, 'File is permanent.');
|
||||
|
||||
// Upload another file to the same node in a new revision.
|
||||
$this->replaceNodeFile($test_file, $field_name, $nid);
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file_r2 = File::load($node->{$field_name}->target_id);
|
||||
$node_vid_r2 = $node->getRevisionId();
|
||||
$this->assertFileExists($node_file_r2, 'Replacement file exists on disk after creating new revision.');
|
||||
$this->assertFileEntryExists($node_file_r2, 'Replacement file entry exists in database after creating new revision.');
|
||||
$this->assertFileIsPermanent($node_file_r2, 'Replacement file is permanent.');
|
||||
|
||||
// Check that the original file is still in place on the first revision.
|
||||
$node = node_revision_load($node_vid_r1);
|
||||
$current_file = File::load($node->{$field_name}->target_id);
|
||||
$this->assertEqual($node_file_r1->id(), $current_file->id(), 'Original file still in place after replacing file in new revision.');
|
||||
$this->assertFileExists($node_file_r1, 'Original file still in place after replacing file in new revision.');
|
||||
$this->assertFileEntryExists($node_file_r1, 'Original file entry still in place after replacing file in new revision');
|
||||
$this->assertFileIsPermanent($node_file_r1, 'Original file is still permanent.');
|
||||
|
||||
// Save a new version of the node without any changes.
|
||||
// Check that the file is still the same as the previous revision.
|
||||
$this->drupalPostForm('node/' . $nid . '/edit', array('revision' => '1'), t('Save and keep published'));
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file_r3 = File::load($node->{$field_name}->target_id);
|
||||
$node_vid_r3 = $node->getRevisionId();
|
||||
$this->assertEqual($node_file_r2->id(), $node_file_r3->id(), 'Previous revision file still in place after creating a new revision without a new file.');
|
||||
$this->assertFileIsPermanent($node_file_r3, 'New revision file is permanent.');
|
||||
|
||||
// Revert to the first revision and check that the original file is active.
|
||||
$this->drupalPostForm('node/' . $nid . '/revisions/' . $node_vid_r1 . '/revert', array(), t('Revert'));
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file_r4 = File::load($node->{$field_name}->target_id);
|
||||
$this->assertEqual($node_file_r1->id(), $node_file_r4->id(), 'Original revision file still in place after reverting to the original revision.');
|
||||
$this->assertFileIsPermanent($node_file_r4, 'Original revision file still permanent after reverting to the original revision.');
|
||||
|
||||
// Delete the second revision and check that the file is kept (since it is
|
||||
// still being used by the third revision).
|
||||
$this->drupalPostForm('node/' . $nid . '/revisions/' . $node_vid_r2 . '/delete', array(), t('Delete'));
|
||||
$this->assertFileExists($node_file_r3, 'Second file is still available after deleting second revision, since it is being used by the third revision.');
|
||||
$this->assertFileEntryExists($node_file_r3, 'Second file entry is still available after deleting second revision, since it is being used by the third revision.');
|
||||
$this->assertFileIsPermanent($node_file_r3, 'Second file entry is still permanent after deleting second revision, since it is being used by the third revision.');
|
||||
|
||||
// Attach the second file to a user.
|
||||
$user = $this->drupalCreateUser();
|
||||
$user->$field_name->target_id = $node_file_r3->id();
|
||||
$user->$field_name->display = 1;
|
||||
$user->save();
|
||||
$this->drupalGet('user/' . $user->id() . '/edit');
|
||||
|
||||
// Delete the third revision and check that the file is not deleted yet.
|
||||
$this->drupalPostForm('node/' . $nid . '/revisions/' . $node_vid_r3 . '/delete', array(), t('Delete'));
|
||||
$this->assertFileExists($node_file_r3, 'Second file is still available after deleting third revision, since it is being used by the user.');
|
||||
$this->assertFileEntryExists($node_file_r3, 'Second file entry is still available after deleting third revision, since it is being used by the user.');
|
||||
$this->assertFileIsPermanent($node_file_r3, 'Second file entry is still permanent after deleting third revision, since it is being used by the user.');
|
||||
|
||||
// Delete the user and check that the file is also deleted.
|
||||
$user->delete();
|
||||
// TODO: This seems like a bug in File API. Clearing the stat cache should
|
||||
// not be necessary here. The file really is deleted, but stream wrappers
|
||||
// doesn't seem to think so unless we clear the PHP file stat() cache.
|
||||
clearstatcache($node_file_r1->getFileUri());
|
||||
clearstatcache($node_file_r2->getFileUri());
|
||||
clearstatcache($node_file_r3->getFileUri());
|
||||
clearstatcache($node_file_r4->getFileUri());
|
||||
|
||||
// Call file_cron() to clean up the file. Make sure the changed timestamp
|
||||
// of the file is older than the system.file.temporary_maximum_age
|
||||
// configuration value.
|
||||
db_update('file_managed')
|
||||
->fields(array(
|
||||
'changed' => REQUEST_TIME - ($this->config('system.file')->get('temporary_maximum_age') + 1),
|
||||
))
|
||||
->condition('fid', $node_file_r3->id())
|
||||
->execute();
|
||||
\Drupal::service('cron')->run();
|
||||
|
||||
$this->assertFileNotExists($node_file_r3, 'Second file is now deleted after deleting third revision, since it is no longer being used by any other nodes.');
|
||||
$this->assertFileEntryNotExists($node_file_r3, 'Second file entry is now deleted after deleting third revision, since it is no longer being used by any other nodes.');
|
||||
|
||||
// Delete the entire node and check that the original file is deleted.
|
||||
$this->drupalPostForm('node/' . $nid . '/delete', array(), t('Delete'));
|
||||
// Call file_cron() to clean up the file. Make sure the changed timestamp
|
||||
// of the file is older than the system.file.temporary_maximum_age
|
||||
// configuration value.
|
||||
db_update('file_managed')
|
||||
->fields(array(
|
||||
'changed' => REQUEST_TIME - ($this->config('system.file')->get('temporary_maximum_age') + 1),
|
||||
))
|
||||
->condition('fid', $node_file_r1->id())
|
||||
->execute();
|
||||
\Drupal::service('cron')->run();
|
||||
$this->assertFileNotExists($node_file_r1, 'Original file is deleted after deleting the entire node with two revisions remaining.');
|
||||
$this->assertFileEntryNotExists($node_file_r1, 'Original file entry is deleted after deleting the entire node with two revisions remaining.');
|
||||
}
|
||||
|
||||
}
|
302
web/core/modules/file/src/Tests/FileFieldTestBase.php
Normal file
302
web/core/modules/file/src/Tests/FileFieldTestBase.php
Normal file
|
@ -0,0 +1,302 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
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.
|
||||
*/
|
||||
abstract class FileFieldTestBase extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('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(array('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(array('type' => 'article', 'name' => 'Article'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a sample file of the specified type.
|
||||
*
|
||||
* @return \Drupal\file\FileInterface
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
function createFileField($name, $entity_type, $bundle, $storage_settings = array(), $field_settings = array(), $widget_settings = array()) {
|
||||
$field_storage = FieldStorageConfig::create(array(
|
||||
'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.
|
||||
*/
|
||||
function attachFileField($name, $entity_type, $bundle, $field_settings = array(), $widget_settings = array()) {
|
||||
$field = array(
|
||||
'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, array(
|
||||
'type' => 'file_generic',
|
||||
'settings' => $widget_settings,
|
||||
))
|
||||
->save();
|
||||
// Assign display settings.
|
||||
entity_get_display($entity_type, $bundle, 'default')
|
||||
->setComponent($name, array(
|
||||
'label' => 'hidden',
|
||||
'type' => 'file_default',
|
||||
))
|
||||
->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing file field with new settings.
|
||||
*/
|
||||
function updateFileField($name, $type_name, $field_settings = array(), $widget_settings = array()) {
|
||||
$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, array(
|
||||
'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.
|
||||
*/
|
||||
function uploadNodeFile(FileInterface $file, $field_name, $nid_or_type, $new_revision = TRUE, array $extras = array()) {
|
||||
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.
|
||||
*/
|
||||
function uploadNodeFiles(array $files, $field_name, $nid_or_type, $new_revision = TRUE, array $extras = array()) {
|
||||
$edit = array(
|
||||
'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(array($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(array($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 and keep published'));
|
||||
|
||||
return $nid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a file from a node.
|
||||
*
|
||||
* Note that if replacing a file, it must first be removed then added again.
|
||||
*/
|
||||
function removeNodeFile($nid, $new_revision = TRUE) {
|
||||
$edit = array(
|
||||
'revision' => (string) (int) $new_revision,
|
||||
);
|
||||
|
||||
$this->drupalPostForm('node/' . $nid . '/edit', array(), t('Remove'));
|
||||
$this->drupalPostForm(NULL, $edit, t('Save and keep published'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a file within a node.
|
||||
*/
|
||||
function replaceNodeFile($file, $field_name, $nid, $new_revision = TRUE) {
|
||||
$edit = array(
|
||||
'files[' . $field_name . '_0]' => drupal_realpath($file->getFileUri()),
|
||||
'revision' => (string) (int) $new_revision,
|
||||
);
|
||||
|
||||
$this->drupalPostForm('node/' . $nid . '/edit', array(), t('Remove'));
|
||||
$this->drupalPostForm(NULL, $edit, t('Save and keep published'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a file exists physically on disk.
|
||||
*/
|
||||
function assertFileExists($file, $message = NULL) {
|
||||
$message = isset($message) ? $message : format_string('File %file exists on the disk.', array('%file' => $file->getFileUri()));
|
||||
$this->assertTrue(is_file($file->getFileUri()), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a file exists in the database.
|
||||
*/
|
||||
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.', array('%file' => $file->getFileUri()));
|
||||
$this->assertEqual($db_file->getFileUri(), $file->getFileUri(), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a file does not exist on disk.
|
||||
*/
|
||||
function assertFileNotExists($file, $message = NULL) {
|
||||
$message = isset($message) ? $message : format_string('File %file exists on the disk.', array('%file' => $file->getFileUri()));
|
||||
$this->assertFalse(is_file($file->getFileUri()), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a file does not exist in the database.
|
||||
*/
|
||||
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.', array('%file' => $file->getFileUri()));
|
||||
$this->assertFalse(File::load($file->id()), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a file's status is set to permanent in the database.
|
||||
*/
|
||||
function assertFileIsPermanent(FileInterface $file, $message = NULL) {
|
||||
$message = isset($message) ? $message : format_string('File %file is permanent.', array('%file' => $file->getFileUri()));
|
||||
$this->assertTrue($file->isPermanent(), $message);
|
||||
}
|
||||
|
||||
}
|
188
web/core/modules/file/src/Tests/FileFieldValidateTest.php
Normal file
188
web/core/modules/file/src/Tests/FileFieldValidateTest.php
Normal file
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\file\Entity\File;
|
||||
|
||||
/**
|
||||
* Tests validation functions such as file type, max file size, max size per
|
||||
* node, and required.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class FileFieldValidateTest extends FileFieldTestBase {
|
||||
|
||||
/**
|
||||
* Tests the required property on file fields.
|
||||
*/
|
||||
function testRequired() {
|
||||
$node_storage = $this->container->get('entity.manager')->getStorage('node');
|
||||
$type_name = 'article';
|
||||
$field_name = strtolower($this->randomMachineName());
|
||||
$storage = $this->createFileField($field_name, 'node', $type_name, array(), array('required' => '1'));
|
||||
$field = FieldConfig::loadByName('node', $type_name, $field_name);
|
||||
|
||||
$test_file = $this->getTestFile('text');
|
||||
|
||||
// Try to post a new node without uploading a file.
|
||||
$edit = array();
|
||||
$edit['title[0][value]'] = $this->randomMachineName();
|
||||
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
|
||||
$this->assertRaw(t('@title field is required.', array('@title' => $field->getLabel())), 'Node save failed when required file field was empty.');
|
||||
|
||||
// Create a new node with the uploaded file.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
$this->assertTrue($nid !== FALSE, format_string('uploadNodeFile(@test_file, @field_name, @type_name) succeeded', array('@test_file' => $test_file->getFileUri(), '@field_name' => $field_name, '@type_name' => $type_name)));
|
||||
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
|
||||
$node_file = File::load($node->{$field_name}->target_id);
|
||||
$this->assertFileExists($node_file, 'File exists after uploading to the required field.');
|
||||
$this->assertFileEntryExists($node_file, 'File entry exists after uploading to the required field.');
|
||||
|
||||
// Try again with a multiple value field.
|
||||
$storage->delete();
|
||||
$this->createFileField($field_name, 'node', $type_name, array('cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED), array('required' => '1'));
|
||||
|
||||
// Try to post a new node without uploading a file in the multivalue field.
|
||||
$edit = array();
|
||||
$edit['title[0][value]'] = $this->randomMachineName();
|
||||
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
|
||||
$this->assertRaw(t('@title field is required.', array('@title' => $field->getLabel())), 'Node save failed when required multiple value file field was empty.');
|
||||
|
||||
// Create a new node with the uploaded file into the multivalue field.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file = File::load($node->{$field_name}->target_id);
|
||||
$this->assertFileExists($node_file, 'File exists after uploading to the required multiple value field.');
|
||||
$this->assertFileEntryExists($node_file, 'File entry exists after uploading to the required multiple value field.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the max file size validator.
|
||||
*/
|
||||
function testFileMaxSize() {
|
||||
$node_storage = $this->container->get('entity.manager')->getStorage('node');
|
||||
$type_name = 'article';
|
||||
$field_name = strtolower($this->randomMachineName());
|
||||
$this->createFileField($field_name, 'node', $type_name, array(), array('required' => '1'));
|
||||
|
||||
$small_file = $this->getTestFile('text', 131072); // 128KB.
|
||||
$large_file = $this->getTestFile('text', 1310720); // 1.2MB
|
||||
|
||||
// Test uploading both a large and small file with different increments.
|
||||
$sizes = array(
|
||||
'1M' => 1048576,
|
||||
'1024K' => 1048576,
|
||||
'1048576' => 1048576,
|
||||
);
|
||||
|
||||
foreach ($sizes as $max_filesize => $file_limit) {
|
||||
// Set the max file upload size.
|
||||
$this->updateFileField($field_name, $type_name, array('max_filesize' => $max_filesize));
|
||||
|
||||
// Create a new node with the small file, which should pass.
|
||||
$nid = $this->uploadNodeFile($small_file, $field_name, $type_name);
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file = File::load($node->{$field_name}->target_id);
|
||||
$this->assertFileExists($node_file, format_string('File exists after uploading a file (%filesize) under the max limit (%maxsize).', array('%filesize' => format_size($small_file->getSize()), '%maxsize' => $max_filesize)));
|
||||
$this->assertFileEntryExists($node_file, format_string('File entry exists after uploading a file (%filesize) under the max limit (%maxsize).', array('%filesize' => format_size($small_file->getSize()), '%maxsize' => $max_filesize)));
|
||||
|
||||
// Check that uploading the large file fails (1M limit).
|
||||
$this->uploadNodeFile($large_file, $field_name, $type_name);
|
||||
$error_message = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($large_file->getSize()), '%maxsize' => format_size($file_limit)));
|
||||
$this->assertRaw($error_message, format_string('Node save failed when file (%filesize) exceeded the max upload size (%maxsize).', array('%filesize' => format_size($large_file->getSize()), '%maxsize' => $max_filesize)));
|
||||
}
|
||||
|
||||
// Turn off the max filesize.
|
||||
$this->updateFileField($field_name, $type_name, array('max_filesize' => ''));
|
||||
|
||||
// Upload the big file successfully.
|
||||
$nid = $this->uploadNodeFile($large_file, $field_name, $type_name);
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file = File::load($node->{$field_name}->target_id);
|
||||
$this->assertFileExists($node_file, format_string('File exists after uploading a file (%filesize) with no max limit.', array('%filesize' => format_size($large_file->getSize()))));
|
||||
$this->assertFileEntryExists($node_file, format_string('File entry exists after uploading a file (%filesize) with no max limit.', array('%filesize' => format_size($large_file->getSize()))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests file extension checking.
|
||||
*/
|
||||
function testFileExtension() {
|
||||
$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('image');
|
||||
list(, $test_file_extension) = explode('.', $test_file->getFilename());
|
||||
|
||||
// Disable extension checking.
|
||||
$this->updateFileField($field_name, $type_name, array('file_extensions' => ''));
|
||||
|
||||
// Check that the file can be uploaded with no extension checking.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file = File::load($node->{$field_name}->target_id);
|
||||
$this->assertFileExists($node_file, 'File exists after uploading a file with no extension checking.');
|
||||
$this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with no extension checking.');
|
||||
|
||||
// Enable extension checking for text files.
|
||||
$this->updateFileField($field_name, $type_name, array('file_extensions' => 'txt'));
|
||||
|
||||
// Check that the file with the wrong extension cannot be uploaded.
|
||||
$this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
$error_message = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => 'txt'));
|
||||
$this->assertRaw($error_message, 'Node save failed when file uploaded with the wrong extension.');
|
||||
|
||||
// Enable extension checking for text and image files.
|
||||
$this->updateFileField($field_name, $type_name, array('file_extensions' => "txt $test_file_extension"));
|
||||
|
||||
// Check that the file can be uploaded with extension checking.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file = File::load($node->{$field_name}->target_id);
|
||||
$this->assertFileExists($node_file, 'File exists after uploading a file with extension checking.');
|
||||
$this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with extension checking.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a file can always be removed if it does not pass validation.
|
||||
*/
|
||||
public function testFileRemoval() {
|
||||
$node_storage = $this->container->get('entity.manager')->getStorage('node');
|
||||
$type_name = 'article';
|
||||
$field_name = 'file_test';
|
||||
$this->createFileField($field_name, 'node', $type_name);
|
||||
|
||||
$test_file = $this->getTestFile('image');
|
||||
|
||||
// Disable extension checking.
|
||||
$this->updateFileField($field_name, $type_name, array('file_extensions' => ''));
|
||||
|
||||
// Check that the file can be uploaded with no extension checking.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file = File::load($node->{$field_name}->target_id);
|
||||
$this->assertFileExists($node_file, 'File exists after uploading a file with no extension checking.');
|
||||
$this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with no extension checking.');
|
||||
|
||||
// Enable extension checking for text files.
|
||||
$this->updateFileField($field_name, $type_name, array('file_extensions' => 'txt'));
|
||||
|
||||
// Check that the file can still be removed.
|
||||
$this->removeNodeFile($nid);
|
||||
$this->assertNoText('Only files with the following extensions are allowed: txt.');
|
||||
$this->assertText('Article ' . $node->getTitle() . ' has been updated.');
|
||||
}
|
||||
|
||||
}
|
602
web/core/modules/file/src/Tests/FileFieldWidgetTest.php
Normal file
602
web/core/modules/file/src/Tests/FileFieldWidgetTest.php
Normal file
|
@ -0,0 +1,602 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\comment\Entity\Comment;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
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 = array('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.
|
||||
*/
|
||||
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 (array('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(array($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, array(), t('Remove'));
|
||||
break;
|
||||
case 'js':
|
||||
$button = $this->xpath('//input[@type="submit" and @value="' . t('Remove') . '"]');
|
||||
$this->drupalPostAjaxForm(NULL, array(), array((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, array(), t('Save and keep published'));
|
||||
$node_storage->resetCache(array($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.
|
||||
*/
|
||||
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, array('cardinality' => $cardinality));
|
||||
$this->createFileField($field_name2, 'node', $type_name, array('cardinality' => $cardinality));
|
||||
|
||||
$test_file = $this->getTestFile('text');
|
||||
|
||||
foreach (array('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 (array($field_name2, $field_name) as $each_field_name) {
|
||||
for ($delta = 0; $delta < 3; $delta++) {
|
||||
$edit = array('files[' . $each_field_name . '_' . $delta . '][]' => drupal_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 (array($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 (array(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).', array('%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, array(), 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, array(), array($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]', array(':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).', array('%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).', array('%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).', array('%type' => $type)));
|
||||
|
||||
// Save the node and ensure it does not have any files.
|
||||
$this->drupalPostForm(NULL, array('title[0][value]' => $this->randomMachineName()), t('Save and publish'));
|
||||
$matches = array();
|
||||
preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches);
|
||||
$nid = $matches[1];
|
||||
$node_storage->resetCache(array($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 = array($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(array($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 = array($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(array($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(array($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(array($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.
|
||||
*/
|
||||
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, array('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 = array('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(array($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.
|
||||
*/
|
||||
function testPrivateFileComment() {
|
||||
$user = $this->drupalCreateUser(array('access comments'));
|
||||
|
||||
// Grant the admin user required comment permissions.
|
||||
$roles = $this->adminUser->getRoles();
|
||||
user_role_grant_permissions($roles[1], array('administer comment fields', 'administer comments'));
|
||||
|
||||
// Revoke access comments permission from anon user, grant post to
|
||||
// authenticated.
|
||||
user_role_revoke_permissions(RoleInterface::ANONYMOUS_ID, array('access comments'));
|
||||
user_role_grant_permissions(RoleInterface::AUTHENTICATED_ID, array('post comments', 'skip comment approval'));
|
||||
|
||||
// Create a new field.
|
||||
$this->addDefaultCommentField('node', 'article');
|
||||
|
||||
$name = strtolower($this->randomMachineName());
|
||||
$label = $this->randomMachineName();
|
||||
$storage_edit = array('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 = array(
|
||||
'title[0][value]' => $this->randomMachineName(),
|
||||
);
|
||||
$this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
|
||||
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
|
||||
|
||||
// Add a comment with a file.
|
||||
$text_file = $this->getTestFile('text');
|
||||
$edit = array(
|
||||
'files[field_' . $name . '_' . 0 . ']' => drupal_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);
|
||||
$this->drupalPostForm('node/' . $node->id() . '/edit', array(), t('Save and unpublish'));
|
||||
|
||||
// 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.
|
||||
*/
|
||||
function testWidgetValidation() {
|
||||
$type_name = 'article';
|
||||
$field_name = strtolower($this->randomMachineName());
|
||||
$this->createFileField($field_name, 'node', $type_name);
|
||||
$this->updateFileField($field_name, $type_name, array('file_extensions' => 'txt'));
|
||||
|
||||
foreach (array('nojs', 'js') as $type) {
|
||||
// Create node and prepare files for upload.
|
||||
$node = $this->drupalCreateNode(array('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_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, array((string) $button[0]['name'] => (string) $button[0]['value']));
|
||||
break;
|
||||
}
|
||||
$error_message = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => 'txt'));
|
||||
$this->assertRaw($error_message, t('Validation error when file with wrong extension uploaded (JSMode=%type).', array('%type' => $type)));
|
||||
|
||||
// Upload file with correct extension, check that error message is removed.
|
||||
$edit[$name] = drupal_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, array((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).', array('%type' => $type)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests file widget element.
|
||||
*/
|
||||
public function testWidgetElement() {
|
||||
$field_name = Unicode::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(array(
|
||||
'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, array(
|
||||
'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 (array('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', array('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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
222
web/core/modules/file/src/Tests/FileListingTest.php
Normal file
222
web/core/modules/file/src/Tests/FileListingTest.php
Normal file
|
@ -0,0 +1,222 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\entity_test\Entity\EntityTestConstraints;
|
||||
|
||||
/**
|
||||
* Tests file listing page functionality.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class FileListingTest extends FileFieldTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('views', 'file', 'image', 'entity_test');
|
||||
|
||||
/**
|
||||
* An authenticated user.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $baseUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser(array('access files overview', 'bypass node access'));
|
||||
$this->baseUser = $this->drupalCreateUser();
|
||||
$this->createFileField('file', 'node', 'article', array(), array('file_extensions' => 'txt png'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates total count of usages for a file.
|
||||
*
|
||||
* @param $usage array
|
||||
* Array of file usage information as returned from file_usage subsystem.
|
||||
* @return int
|
||||
* Total usage count.
|
||||
*/
|
||||
protected function sumUsages($usage) {
|
||||
$count = 0;
|
||||
foreach ($usage as $module) {
|
||||
foreach ($module as $entity_type) {
|
||||
foreach ($entity_type as $entity) {
|
||||
$count += $entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests file overview with different user permissions.
|
||||
*/
|
||||
function testFileListingPages() {
|
||||
$file_usage = $this->container->get('file.usage');
|
||||
// Users without sufficient permissions should not see file listing.
|
||||
$this->drupalLogin($this->baseUser);
|
||||
$this->drupalGet('admin/content/files');
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Log in with user with right permissions and test listing.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$nodes[] = $this->drupalCreateNode(array('type' => 'article'));
|
||||
}
|
||||
|
||||
$this->drupalGet('admin/content/files');
|
||||
$this->assertResponse(200);
|
||||
$this->assertText('No files available.');
|
||||
$this->drupalGet('admin/content/files');
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Create a file with no usage.
|
||||
$file = $this->createFile();
|
||||
|
||||
$this->drupalGet('admin/content/files/usage/' . $file->id());
|
||||
$this->assertResponse(200);
|
||||
$this->assertTitle(t('File usage information for @file | Drupal', array('@file' => $file->getFilename())));
|
||||
|
||||
foreach ($nodes as &$node) {
|
||||
$this->drupalGet('node/' . $node->id() . '/edit');
|
||||
$file = $this->getTestFile('image');
|
||||
|
||||
$edit = array(
|
||||
'files[file_0]' => drupal_realpath($file->getFileUri()),
|
||||
);
|
||||
$this->drupalPostForm(NULL, $edit, t('Save'));
|
||||
$node = Node::load($node->id());
|
||||
}
|
||||
|
||||
$this->drupalGet('admin/content/files');
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
$file = File::load($node->file->target_id);
|
||||
$this->assertText($file->getFilename());
|
||||
$this->assertLinkByHref(file_create_url($file->getFileUri()));
|
||||
$this->assertLinkByHref('admin/content/files/usage/' . $file->id());
|
||||
}
|
||||
$this->assertFalse(preg_match('/views-field-status priority-low\">\s*' . t('Temporary') . '/', $this->getRawContent()), 'All files are stored as permanent.');
|
||||
|
||||
// Use one file two times and check usage information.
|
||||
$orphaned_file = $nodes[1]->file->target_id;
|
||||
$used_file = $nodes[0]->file->target_id;
|
||||
$nodes[1]->file->target_id = $used_file;
|
||||
$nodes[1]->save();
|
||||
|
||||
$this->drupalGet('admin/content/files');
|
||||
$file = File::load($orphaned_file);
|
||||
$usage = $this->sumUsages($file_usage->listUsage($file));
|
||||
$this->assertRaw('admin/content/files/usage/' . $file->id() . '">' . $usage);
|
||||
|
||||
$file = File::load($used_file);
|
||||
$usage = $this->sumUsages($file_usage->listUsage($file));
|
||||
$this->assertRaw('admin/content/files/usage/' . $file->id() . '">' . $usage);
|
||||
|
||||
$result = $this->xpath("//td[contains(@class, 'views-field-status') and contains(text(), :value)]", array(':value' => t('Temporary')));
|
||||
$this->assertEqual(1, count($result), 'Unused file marked as temporary.');
|
||||
|
||||
// Test file usage page.
|
||||
foreach ($nodes as $node) {
|
||||
$file = File::load($node->file->target_id);
|
||||
$usage = $file_usage->listUsage($file);
|
||||
$this->drupalGet('admin/content/files/usage/' . $file->id());
|
||||
$this->assertResponse(200);
|
||||
$this->assertText($node->getTitle(), 'Node title found on usage page.');
|
||||
$this->assertText('node', 'Registering entity type found on usage page.');
|
||||
$this->assertText('file', 'Registering module found on usage page.');
|
||||
foreach ($usage as $module) {
|
||||
foreach ($module as $entity_type) {
|
||||
foreach ($entity_type as $entity) {
|
||||
$this->assertText($entity, 'Usage count found on usage page.');
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->assertLinkByHref('node/' . $node->id(), 0, 'Link to registering entity found on usage page.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests file listing usage page for entities with no canonical link template.
|
||||
*/
|
||||
function testFileListingUsageNoLink() {
|
||||
// Login with user with right permissions and test listing.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
// Create a bundle and attach a File field to the bundle.
|
||||
$bundle = $this->randomMachineName();
|
||||
entity_test_create_bundle($bundle, NULL, 'entity_test_constraints');
|
||||
$this->createFileField('field_test_file', 'entity_test_constraints', $bundle, array(), array('file_extensions' => 'txt png'));
|
||||
|
||||
// Create file to attach to entity.
|
||||
$file = File::create([
|
||||
'filename' => 'druplicon.txt',
|
||||
'uri' => 'public://druplicon.txt',
|
||||
'filemime' => 'text/plain',
|
||||
]);
|
||||
$file->setPermanent();
|
||||
file_put_contents($file->getFileUri(), 'hello world');
|
||||
$file->save();
|
||||
|
||||
// Create entity and attach the created file.
|
||||
$entity_name = $this->randomMachineName();
|
||||
$entity = EntityTestConstraints::create(array(
|
||||
'uid' => 1,
|
||||
'name' => $entity_name,
|
||||
'type' => $bundle,
|
||||
'field_test_file' => array(
|
||||
'target_id' => $file->id(),
|
||||
),
|
||||
));
|
||||
$entity->save();
|
||||
|
||||
// Create node entity and attach the created file.
|
||||
$node = $this->drupalCreateNode(array('type' => 'article', 'file' => $file));
|
||||
$node->save();
|
||||
|
||||
// Load the file usage page for the created and attached file.
|
||||
$this->drupalGet('admin/content/files/usage/' . $file->id());
|
||||
|
||||
$this->assertResponse(200);
|
||||
// Entity name should be displayed, but not linked if Entity::toUrl
|
||||
// throws an exception
|
||||
$this->assertText($entity_name, 'Entity name is added to file usage listing.');
|
||||
$this->assertNoLink($entity_name, 'Linked entity name not added to file usage listing.');
|
||||
$this->assertLink($node->getTitle());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and saves a test file.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* A file entity.
|
||||
*/
|
||||
protected function createFile() {
|
||||
// Create a new file entity.
|
||||
$file = File::create([
|
||||
'uid' => 1,
|
||||
'filename' => 'druplicon.txt',
|
||||
'uri' => 'public://druplicon.txt',
|
||||
'filemime' => 'text/plain',
|
||||
'created' => 1,
|
||||
'changed' => 1,
|
||||
'status' => FILE_STATUS_PERMANENT,
|
||||
]);
|
||||
file_put_contents($file->getFileUri(), 'hello world');
|
||||
|
||||
// Save it, inserting a new record.
|
||||
$file->save();
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
}
|
69
web/core/modules/file/src/Tests/FileManagedAccessTest.php
Normal file
69
web/core/modules/file/src/Tests/FileManagedAccessTest.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\file\Entity\File;
|
||||
|
||||
/**
|
||||
* Tests access to managed files.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class FileManagedAccessTest extends FileManagedTestBase {
|
||||
|
||||
/**
|
||||
* Tests if public file is always accessible.
|
||||
*/
|
||||
function testFileAccess() {
|
||||
// Create a new file entity.
|
||||
$file = File::create(array(
|
||||
'uid' => 1,
|
||||
'filename' => 'drupal.txt',
|
||||
'uri' => 'public://drupal.txt',
|
||||
'filemime' => 'text/plain',
|
||||
'status' => FILE_STATUS_PERMANENT,
|
||||
));
|
||||
file_put_contents($file->getFileUri(), 'hello world');
|
||||
|
||||
// Save it, inserting a new record.
|
||||
$file->save();
|
||||
|
||||
// Create authenticated user to check file access.
|
||||
$account = $this->createUser(array('access site reports'));
|
||||
|
||||
$this->assertTrue($file->access('view', $account), 'Public file is viewable to authenticated user');
|
||||
$this->assertTrue($file->access('download', $account), 'Public file is downloadable to authenticated user');
|
||||
|
||||
// Create anonymous user to check file access.
|
||||
$account = $this->createUser()->getAnonymousUser();
|
||||
|
||||
$this->assertTrue($file->access('view', $account), 'Public file is viewable to anonymous user');
|
||||
$this->assertTrue($file->access('download', $account), 'Public file is downloadable to anonymous user');
|
||||
|
||||
// Create a new file entity.
|
||||
$file = File::create(array(
|
||||
'uid' => 1,
|
||||
'filename' => 'drupal.txt',
|
||||
'uri' => 'private://drupal.txt',
|
||||
'filemime' => 'text/plain',
|
||||
'status' => FILE_STATUS_PERMANENT,
|
||||
));
|
||||
file_put_contents($file->getFileUri(), 'hello world');
|
||||
|
||||
// Save it, inserting a new record.
|
||||
$file->save();
|
||||
|
||||
// Create authenticated user to check file access.
|
||||
$account = $this->createUser(array('access site reports'));
|
||||
|
||||
$this->assertFalse($file->access('view', $account), 'Private file is not viewable to authenticated user');
|
||||
$this->assertFalse($file->access('download', $account), 'Private file is not downloadable to authenticated user');
|
||||
|
||||
// Create anonymous user to check file access.
|
||||
$account = $this->createUser()->getAnonymousUser();
|
||||
|
||||
$this->assertFalse($file->access('view', $account), 'Private file is not viewable to anonymous user');
|
||||
$this->assertFalse($file->access('download', $account), 'Private file is not downloadable to anonymous user');
|
||||
}
|
||||
|
||||
}
|
197
web/core/modules/file/src/Tests/FileManagedFileElementTest.php
Normal file
197
web/core/modules/file/src/Tests/FileManagedFileElementTest.php
Normal file
|
@ -0,0 +1,197 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
/**
|
||||
* Tests the 'managed_file' element type.
|
||||
*
|
||||
* @group file
|
||||
* @todo Create a FileTestBase class and move FileFieldTestBase methods
|
||||
* that aren't related to fields into it.
|
||||
*/
|
||||
class FileManagedFileElementTest extends FileFieldTestBase {
|
||||
/**
|
||||
* Tests the managed_file element type.
|
||||
*/
|
||||
function testManagedFile() {
|
||||
// Check that $element['#size'] is passed to the child upload element.
|
||||
$this->drupalGet('file/test');
|
||||
$this->assertFieldByXpath('//input[@name="files[nested_file]" and @size="13"]', NULL, 'The custom #size attribute is passed to the child upload element.');
|
||||
|
||||
// Perform the tests with all permutations of $form['#tree'],
|
||||
// $element['#extended'], and $element['#multiple'].
|
||||
$test_file = $this->getTestFile('text');
|
||||
foreach (array(0, 1) as $tree) {
|
||||
foreach (array(0, 1) as $extended) {
|
||||
foreach (array(0, 1) as $multiple) {
|
||||
$path = 'file/test/' . $tree . '/' . $extended . '/' . $multiple;
|
||||
$input_base_name = $tree ? 'nested_file' : 'file';
|
||||
$file_field_name = $multiple ? 'files[' . $input_base_name . '][]' : 'files[' . $input_base_name . ']';
|
||||
|
||||
// Submit without a file.
|
||||
$this->drupalPostForm($path, array(), t('Save'));
|
||||
$this->assertRaw(t('The file ids are %fids.', array('%fids' => implode(',', array()))), 'Submitted without a file.');
|
||||
|
||||
// Submit with a file, but with an invalid form token. Ensure the file
|
||||
// was not saved.
|
||||
$last_fid_prior = $this->getLastFileId();
|
||||
$edit = [
|
||||
$file_field_name => drupal_realpath($test_file->getFileUri()),
|
||||
'form_token' => 'invalid token',
|
||||
];
|
||||
$this->drupalPostForm($path, $edit, t('Save'));
|
||||
$this->assertText('The form has become outdated. Copy any unsaved work in the form below');
|
||||
$last_fid = $this->getLastFileId();
|
||||
$this->assertEqual($last_fid_prior, $last_fid, 'File was not saved when uploaded with an invalid form token.');
|
||||
|
||||
// Submit a new file, without using the Upload button.
|
||||
$last_fid_prior = $this->getLastFileId();
|
||||
$edit = array($file_field_name => drupal_realpath($test_file->getFileUri()));
|
||||
$this->drupalPostForm($path, $edit, t('Save'));
|
||||
$last_fid = $this->getLastFileId();
|
||||
$this->assertTrue($last_fid > $last_fid_prior, 'New file got saved.');
|
||||
$this->assertRaw(t('The file ids are %fids.', array('%fids' => implode(',', array($last_fid)))), 'Submit handler has correct file info.');
|
||||
|
||||
// Submit no new input, but with a default file.
|
||||
$this->drupalPostForm($path . '/' . $last_fid, array(), t('Save'));
|
||||
$this->assertRaw(t('The file ids are %fids.', array('%fids' => implode(',', array($last_fid)))), 'Empty submission did not change an existing file.');
|
||||
|
||||
// Now, test the Upload and Remove buttons, with and without Ajax.
|
||||
foreach (array(FALSE, TRUE) as $ajax) {
|
||||
// Upload, then Submit.
|
||||
$last_fid_prior = $this->getLastFileId();
|
||||
$this->drupalGet($path);
|
||||
$edit = array($file_field_name => drupal_realpath($test_file->getFileUri()));
|
||||
if ($ajax) {
|
||||
$this->drupalPostAjaxForm(NULL, $edit, $input_base_name . '_upload_button');
|
||||
}
|
||||
else {
|
||||
$this->drupalPostForm(NULL, $edit, t('Upload'));
|
||||
}
|
||||
$last_fid = $this->getLastFileId();
|
||||
$this->assertTrue($last_fid > $last_fid_prior, 'New file got uploaded.');
|
||||
$this->drupalPostForm(NULL, array(), t('Save'));
|
||||
$this->assertRaw(t('The file ids are %fids.', array('%fids' => implode(',', array($last_fid)))), 'Submit handler has correct file info.');
|
||||
|
||||
// Remove, then Submit.
|
||||
$remove_button_title = $multiple ? t('Remove selected') : t('Remove');
|
||||
$remove_edit = array();
|
||||
if ($multiple) {
|
||||
$selected_checkbox = ($tree ? 'nested[file]' : 'file') . '[file_' . $last_fid . '][selected]';
|
||||
$remove_edit = array($selected_checkbox => '1');
|
||||
}
|
||||
$this->drupalGet($path . '/' . $last_fid);
|
||||
if ($ajax) {
|
||||
$this->drupalPostAjaxForm(NULL, $remove_edit, $input_base_name . '_remove_button');
|
||||
}
|
||||
else {
|
||||
$this->drupalPostForm(NULL, $remove_edit, $remove_button_title);
|
||||
}
|
||||
$this->drupalPostForm(NULL, array(), t('Save'));
|
||||
$this->assertRaw(t('The file ids are %fids.', array('%fids' => '')), 'Submission after file removal was successful.');
|
||||
|
||||
// Upload, then Remove, then Submit.
|
||||
$this->drupalGet($path);
|
||||
$edit = array($file_field_name => drupal_realpath($test_file->getFileUri()));
|
||||
if ($ajax) {
|
||||
$this->drupalPostAjaxForm(NULL, $edit, $input_base_name . '_upload_button');
|
||||
}
|
||||
else {
|
||||
$this->drupalPostForm(NULL, $edit, t('Upload'));
|
||||
}
|
||||
$remove_edit = array();
|
||||
if ($multiple) {
|
||||
$selected_checkbox = ($tree ? 'nested[file]' : 'file') . '[file_' . $this->getLastFileId() . '][selected]';
|
||||
$remove_edit = array($selected_checkbox => '1');
|
||||
}
|
||||
if ($ajax) {
|
||||
$this->drupalPostAjaxForm(NULL, $remove_edit, $input_base_name . '_remove_button');
|
||||
}
|
||||
else {
|
||||
$this->drupalPostForm(NULL, $remove_edit, $remove_button_title);
|
||||
}
|
||||
|
||||
$this->drupalPostForm(NULL, array(), t('Save'));
|
||||
$this->assertRaw(t('The file ids are %fids.', array('%fids' => '')), 'Submission after file upload and removal was successful.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The multiple file upload has additional conditions that need checking.
|
||||
$path = 'file/test/1/1/1';
|
||||
$edit = array('files[nested_file][]' => drupal_realpath($test_file->getFileUri()));
|
||||
$fid_list = array();
|
||||
|
||||
$this->drupalGet($path);
|
||||
|
||||
// Add a single file to the upload field.
|
||||
$this->drupalPostForm(NULL, $edit, t('Upload'));
|
||||
$fid_list[] = $this->getLastFileId();
|
||||
$this->assertFieldByXpath('//input[@name="nested[file][file_' . $fid_list[0] . '][selected]"]', NULL, 'First file successfully uploaded to multiple file element.');
|
||||
|
||||
// Add another file to the same upload field.
|
||||
$this->drupalPostForm(NULL, $edit, t('Upload'));
|
||||
$fid_list[] = $this->getLastFileId();
|
||||
$this->assertFieldByXpath('//input[@name="nested[file][file_' . $fid_list[1] . '][selected]"]', NULL, 'Second file successfully uploaded to multiple file element.');
|
||||
|
||||
// Save the entire form.
|
||||
$this->drupalPostForm(NULL, array(), t('Save'));
|
||||
$this->assertRaw(t('The file ids are %fids.', array('%fids' => implode(',', $fid_list))), 'Two files saved into a single multiple file element.');
|
||||
|
||||
// Delete only the first file.
|
||||
$edit = array(
|
||||
'nested[file][file_' . $fid_list[0] . '][selected]' => '1',
|
||||
);
|
||||
$this->drupalPostForm($path . '/' . implode(',', $fid_list), $edit, t('Remove selected'));
|
||||
|
||||
// Check that the first file has been deleted but not the second.
|
||||
$this->assertNoFieldByXpath('//input[@name="nested[file][file_' . $fid_list[0] . '][selected]"]', NULL, 'An individual file can be deleted from a multiple file element.');
|
||||
$this->assertFieldByXpath('//input[@name="nested[file][file_' . $fid_list[1] . '][selected]"]', NULL, 'Second individual file not deleted when the first file is deleted from a multiple file element.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that warning is shown if file on the field has been removed.
|
||||
*/
|
||||
public function testManagedFileRemoved() {
|
||||
$this->drupalGet('file/test/1/0/1');
|
||||
$test_file = $this->getTestFile('text');
|
||||
$file_field_name = 'files[nested_file][]';
|
||||
|
||||
$edit = [$file_field_name => drupal_realpath($test_file->getFileUri())];
|
||||
$this->drupalPostForm(NULL, $edit, t('Upload'));
|
||||
|
||||
$fid = $this->getLastFileId();
|
||||
$file = \Drupal::entityManager()->getStorage('file')->load($fid);
|
||||
$file->delete();
|
||||
|
||||
$this->drupalPostForm(NULL, $edit, t('Upload'));
|
||||
// We expect the title 'Managed <em>file & butter</em>' which got escaped
|
||||
// via a t() call before.
|
||||
$this->assertRaw('The file referenced by the Managed <em>file & butter</em> field does not exist.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a file entity can be saved when the file does not exist on disk.
|
||||
*/
|
||||
public function testFileRemovedFromDisk() {
|
||||
$this->drupalGet('file/test/1/0/1');
|
||||
$test_file = $this->getTestFile('text');
|
||||
$file_field_name = 'files[nested_file][]';
|
||||
|
||||
$edit = [$file_field_name => drupal_realpath($test_file->getFileUri())];
|
||||
$this->drupalPostForm(NULL, $edit, t('Upload'));
|
||||
$this->drupalPostForm(NULL, array(), t('Save'));
|
||||
|
||||
$fid = $this->getLastFileId();
|
||||
/** @var $file \Drupal\file\FileInterface */
|
||||
$file = $this->container->get('entity_type.manager')->getStorage('file')->load($fid);
|
||||
$file->setPermanent();
|
||||
$file->save();
|
||||
$this->assertTrue(file_unmanaged_delete($file->getFileUri()));
|
||||
$file->save();
|
||||
$this->assertTrue($file->isPermanent());
|
||||
$file->delete();
|
||||
}
|
||||
|
||||
}
|
200
web/core/modules/file/src/Tests/FileManagedTestBase.php
Normal file
200
web/core/modules/file/src/Tests/FileManagedTestBase.php
Normal file
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
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.
|
||||
*/
|
||||
abstract class FileManagedTestBase extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('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.
|
||||
*/
|
||||
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.', array('%expected' => implode(', ', $expected), '%uncalled' => implode(', ', $uncalled))));
|
||||
}
|
||||
else {
|
||||
$this->assertTrue(TRUE, format_string('All the expected hooks were called: %expected', array('%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.', array('%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.
|
||||
*/
|
||||
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.', array('@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.', array('@name' => $hook, '@count' => $actual_count));
|
||||
}
|
||||
else {
|
||||
$message = format_string('hook_file_@name was expected to be called %expected times but was called %actual times.', array('@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.
|
||||
*/
|
||||
function assertFileUnchanged(FileInterface $before, FileInterface $after) {
|
||||
$this->assertEqual($before->id(), $after->id(), t('File id is the same: %file1 == %file2.', array('%file1' => $before->id(), '%file2' => $after->id())), 'File unchanged');
|
||||
$this->assertEqual($before->getOwner()->id(), $after->getOwner()->id(), t('File owner is the same: %file1 == %file2.', array('%file1' => $before->getOwner()->id(), '%file2' => $after->getOwner()->id())), 'File unchanged');
|
||||
$this->assertEqual($before->getFilename(), $after->getFilename(), t('File name is the same: %file1 == %file2.', array('%file1' => $before->getFilename(), '%file2' => $after->getFilename())), 'File unchanged');
|
||||
$this->assertEqual($before->getFileUri(), $after->getFileUri(), t('File path is the same: %file1 == %file2.', array('%file1' => $before->getFileUri(), '%file2' => $after->getFileUri())), 'File unchanged');
|
||||
$this->assertEqual($before->getMimeType(), $after->getMimeType(), t('File MIME type is the same: %file1 == %file2.', array('%file1' => $before->getMimeType(), '%file2' => $after->getMimeType())), 'File unchanged');
|
||||
$this->assertEqual($before->getSize(), $after->getSize(), t('File size is the same: %file1 == %file2.', array('%file1' => $before->getSize(), '%file2' => $after->getSize())), 'File unchanged');
|
||||
$this->assertEqual($before->isPermanent(), $after->isPermanent(), t('File status is the same: %file1 == %file2.', array('%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.
|
||||
*/
|
||||
function assertDifferentFile(FileInterface $file1, FileInterface $file2) {
|
||||
$this->assertNotEqual($file1->id(), $file2->id(), t('Files have different ids: %file1 != %file2.', array('%file1' => $file1->id(), '%file2' => $file2->id())), 'Different file');
|
||||
$this->assertNotEqual($file1->getFileUri(), $file2->getFileUri(), t('Files have different paths: %file1 != %file2.', array('%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.
|
||||
*/
|
||||
function assertSameFile(FileInterface $file1, FileInterface $file2) {
|
||||
$this->assertEqual($file1->id(), $file2->id(), t('Files have the same ids: %file1 == %file2.', array('%file1' => $file1->id(), '%file2-fid' => $file2->id())), 'Same file');
|
||||
$this->assertEqual($file1->getFileUri(), $file2->getFileUri(), t('Files have the same path: %file1 == %file2.', array('%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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
200
web/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php
Normal file
200
web/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php
Normal file
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\file\Entity\File;
|
||||
|
||||
/**
|
||||
* Uploads files to translated nodes.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class FileOnTranslatedEntityTest extends FileFieldTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = array('language', 'content_translation');
|
||||
|
||||
/**
|
||||
* The name of the file field used in the test.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fieldName;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create the "Basic page" node type.
|
||||
// @todo Remove the disabling of new revision creation in
|
||||
// https://www.drupal.org/node/1239558.
|
||||
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page', 'new_revision' => FALSE]);
|
||||
|
||||
// Create a file field on the "Basic page" node type.
|
||||
$this->fieldName = strtolower($this->randomMachineName());
|
||||
$this->createFileField($this->fieldName, 'node', 'page');
|
||||
|
||||
// Create and log in user.
|
||||
$permissions = array(
|
||||
'access administration pages',
|
||||
'administer content translation',
|
||||
'administer content types',
|
||||
'administer languages',
|
||||
'create content translations',
|
||||
'create page content',
|
||||
'edit any page content',
|
||||
'translate any entity',
|
||||
'delete any page content',
|
||||
);
|
||||
$admin_user = $this->drupalCreateUser($permissions);
|
||||
$this->drupalLogin($admin_user);
|
||||
|
||||
// Add a second and third language.
|
||||
$edit = array();
|
||||
$edit['predefined_langcode'] = 'fr';
|
||||
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
|
||||
|
||||
$edit = array();
|
||||
$edit['predefined_langcode'] = 'nl';
|
||||
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
|
||||
|
||||
// Enable translation for "Basic page" nodes.
|
||||
$edit = array(
|
||||
'entity_types[node]' => 1,
|
||||
'settings[node][page][translatable]' => 1,
|
||||
"settings[node][page][fields][$this->fieldName]" => 1,
|
||||
);
|
||||
$this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration'));
|
||||
\Drupal::entityManager()->clearCachedDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests synced file fields on translated nodes.
|
||||
*/
|
||||
public function testSyncedFiles() {
|
||||
// Verify that the file field on the "Basic page" node type is translatable.
|
||||
$definitions = \Drupal::entityManager()->getFieldDefinitions('node', 'page');
|
||||
$this->assertTrue($definitions[$this->fieldName]->isTranslatable(), 'Node file field is translatable.');
|
||||
|
||||
// Create a default language node.
|
||||
$default_language_node = $this->drupalCreateNode(array('type' => 'page', 'title' => 'Lost in translation'));
|
||||
|
||||
// Edit the node to upload a file.
|
||||
$edit = array();
|
||||
$name = 'files[' . $this->fieldName . '_0]';
|
||||
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[0]->uri);
|
||||
$this->drupalPostForm('node/' . $default_language_node->id() . '/edit', $edit, t('Save'));
|
||||
$first_fid = $this->getLastFileId();
|
||||
|
||||
// Translate the node into French: remove the existing file.
|
||||
$this->drupalPostForm('node/' . $default_language_node->id() . '/translations/add/en/fr', array(), t('Remove'));
|
||||
|
||||
// Upload a different file.
|
||||
$edit = array();
|
||||
$edit['title[0][value]'] = 'Bill Murray';
|
||||
$name = 'files[' . $this->fieldName . '_0]';
|
||||
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[1]->uri);
|
||||
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
|
||||
// This inspects the HTML after the post of the translation, the file
|
||||
// should be displayed on the original node.
|
||||
$this->assertRaw('file--mime-text-plain');
|
||||
$second_fid = $this->getLastFileId();
|
||||
|
||||
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
|
||||
|
||||
/* @var $file \Drupal\file\FileInterface */
|
||||
|
||||
// Ensure the file status of the first file permanent.
|
||||
$file = File::load($first_fid);
|
||||
$this->assertTrue($file->isPermanent());
|
||||
|
||||
// Ensure the file status of the second file is permanent.
|
||||
$file = File::load($second_fid);
|
||||
$this->assertTrue($file->isPermanent());
|
||||
|
||||
// Translate the node into dutch: remove the existing file.
|
||||
$this->drupalPostForm('node/' . $default_language_node->id() . '/translations/add/en/nl', array(), t('Remove'));
|
||||
|
||||
// Upload a different file.
|
||||
$edit = array();
|
||||
$edit['title[0][value]'] = 'Scarlett Johansson';
|
||||
$name = 'files[' . $this->fieldName . '_0]';
|
||||
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[2]->uri);
|
||||
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
|
||||
$third_fid = $this->getLastFileId();
|
||||
|
||||
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
|
||||
|
||||
// Ensure the first file is untouched.
|
||||
$file = File::load($first_fid);
|
||||
$this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.');
|
||||
// This inspects the HTML after the post of the translation, the file
|
||||
// should be displayed on the original node.
|
||||
$this->assertRaw('file--mime-text-plain');
|
||||
|
||||
// Ensure the file status of the second file is permanent.
|
||||
$file = File::load($second_fid);
|
||||
$this->assertTrue($file->isPermanent());
|
||||
|
||||
// Ensure the file status of the third file is permanent.
|
||||
$file = File::load($third_fid);
|
||||
$this->assertTrue($file->isPermanent());
|
||||
|
||||
// Edit the second translation: remove the existing file.
|
||||
$this->drupalPostForm('fr/node/' . $default_language_node->id() . '/edit', array(), t('Remove'));
|
||||
|
||||
// Upload a different file.
|
||||
$edit = array();
|
||||
$edit['title[0][value]'] = 'David Bowie';
|
||||
$name = 'files[' . $this->fieldName . '_0]';
|
||||
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[3]->uri);
|
||||
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
|
||||
$replaced_second_fid = $this->getLastFileId();
|
||||
|
||||
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
|
||||
|
||||
// Ensure the first and third files are untouched.
|
||||
$file = File::load($first_fid);
|
||||
$this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.');
|
||||
|
||||
$file = File::load($third_fid);
|
||||
$this->assertTrue($file->isPermanent());
|
||||
|
||||
// Ensure the file status of the replaced second file is permanent.
|
||||
$file = File::load($replaced_second_fid);
|
||||
$this->assertTrue($file->isPermanent());
|
||||
|
||||
// Delete the third translation.
|
||||
$this->drupalPostForm('nl/node/' . $default_language_node->id() . '/delete', array(), t('Delete Dutch translation'));
|
||||
|
||||
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
|
||||
|
||||
// Ensure the first and replaced second files are untouched.
|
||||
$file = File::load($first_fid);
|
||||
$this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.');
|
||||
|
||||
$file = File::load($replaced_second_fid);
|
||||
$this->assertTrue($file->isPermanent());
|
||||
|
||||
// Ensure the file status of the third file is now temporary.
|
||||
$file = File::load($third_fid);
|
||||
$this->assertTrue($file->isTemporary());
|
||||
|
||||
// Delete the all translations.
|
||||
$this->drupalPostForm('node/' . $default_language_node->id() . '/delete', array(), t('Delete all translations'));
|
||||
|
||||
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
|
||||
|
||||
// Ensure the file status of the all files are now temporary.
|
||||
$file = File::load($first_fid);
|
||||
$this->assertTrue($file->isTemporary(), 'First file still exists and is temporary.');
|
||||
|
||||
$file = File::load($replaced_second_fid);
|
||||
$this->assertTrue($file->isTemporary());
|
||||
}
|
||||
|
||||
}
|
115
web/core/modules/file/src/Tests/FilePrivateTest.php
Normal file
115
web/core/modules/file/src/Tests/FilePrivateTest.php
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\Core\Entity\Plugin\Validation\Constraint\ReferenceAccessConstraint;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* Uploads a test to a private node and checks access.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class FilePrivateTest extends FileFieldTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('node_access_test', 'field_test');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
node_access_test_add_field(NodeType::load('article'));
|
||||
node_access_rebuild();
|
||||
\Drupal::state()->set('node_access_test.private', TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests file access for file uploaded to a private node.
|
||||
*/
|
||||
function testPrivateFile() {
|
||||
$node_storage = $this->container->get('entity.manager')->getStorage('node');
|
||||
$type_name = 'article';
|
||||
$field_name = strtolower($this->randomMachineName());
|
||||
$this->createFileField($field_name, 'node', $type_name, array('uri_scheme' => 'private'));
|
||||
|
||||
$test_file = $this->getTestFile('text');
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name, TRUE, array('private' => TRUE));
|
||||
\Drupal::entityManager()->getStorage('node')->resetCache(array($nid));
|
||||
/* @var \Drupal\node\NodeInterface $node */
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file = File::load($node->{$field_name}->target_id);
|
||||
// Ensure the file can be viewed.
|
||||
$this->drupalGet('node/' . $node->id());
|
||||
$this->assertRaw($node_file->getFilename(), 'File reference is displayed after attaching it');
|
||||
// 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.');
|
||||
$this->drupalLogOut();
|
||||
$this->drupalGet(file_create_url($node_file->getFileUri()));
|
||||
$this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.');
|
||||
|
||||
// Create a field with no view access. See
|
||||
// field_test_entity_field_access().
|
||||
$no_access_field_name = 'field_no_view_access';
|
||||
$this->createFileField($no_access_field_name, 'node', $type_name, array('uri_scheme' => 'private'));
|
||||
// Test with the field that should deny access through field access.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$nid = $this->uploadNodeFile($test_file, $no_access_field_name, $type_name, TRUE, array('private' => TRUE));
|
||||
\Drupal::entityManager()->getStorage('node')->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$node_file = File::load($node->{$no_access_field_name}->target_id);
|
||||
|
||||
// Ensure the file cannot be downloaded.
|
||||
$file_url = file_create_url($node_file->getFileUri());
|
||||
$this->drupalGet($file_url);
|
||||
$this->assertResponse(403, 'Confirmed that access is denied for the file without view field access permission.');
|
||||
|
||||
// Attempt to reuse the file when editing a node.
|
||||
$edit = array();
|
||||
$edit['title[0][value]'] = $this->randomMachineName();
|
||||
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
|
||||
$new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
|
||||
$edit[$field_name . '[0][fids]'] = $node_file->id();
|
||||
$this->drupalPostForm('node/' . $new_node->id() . '/edit', $edit, t('Save and keep published'));
|
||||
// Make sure the form submit failed - we stayed on the edit form.
|
||||
$this->assertUrl('node/' . $new_node->id() . '/edit');
|
||||
// Check that we got the expected constraint form error.
|
||||
$constraint = new ReferenceAccessConstraint();
|
||||
$this->assertRaw(SafeMarkup::format($constraint->message, array('%type' => 'file', '%id' => $node_file->id())));
|
||||
// Attempt to reuse the existing file when creating a new node, and confirm
|
||||
// that access is still denied.
|
||||
$edit = array();
|
||||
$edit['title[0][value]'] = $this->randomMachineName();
|
||||
$edit[$field_name . '[0][fids]'] = $node_file->id();
|
||||
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
|
||||
$new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
|
||||
$this->assertTrue(empty($new_node), 'Node was not created.');
|
||||
$this->assertUrl('node/add/' . $type_name);
|
||||
$this->assertRaw(SafeMarkup::format($constraint->message, array('%type' => 'file', '%id' => $node_file->id())));
|
||||
|
||||
// Now make file_test_file_download() return everything.
|
||||
\Drupal::state()->set('file_test.allow_all', TRUE);
|
||||
// Delete the node.
|
||||
$node->delete();
|
||||
// Ensure the file can still be downloaded by the owner.
|
||||
$this->drupalGet($file_url);
|
||||
$this->assertResponse(200, 'Confirmed that the owner still has access to the temporary file.');
|
||||
|
||||
// Ensure the file cannot be downloaded by an anonymous user.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet($file_url);
|
||||
$this->assertResponse(403, 'Confirmed that access is denied for an anonymous user to the temporary file.');
|
||||
|
||||
// Ensure the file cannot be downloaded by another user.
|
||||
$account = $this->drupalCreateUser();
|
||||
$this->drupalLogin($account);
|
||||
$this->drupalGet($file_url);
|
||||
$this->assertResponse(403, 'Confirmed that access is denied for another user to the temporary file.');
|
||||
}
|
||||
|
||||
}
|
97
web/core/modules/file/src/Tests/FileTokenReplaceTest.php
Normal file
97
web/core/modules/file/src/Tests/FileTokenReplaceTest.php
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\file\Entity\File;
|
||||
|
||||
/**
|
||||
* Generates text using placeholders for dummy content to check file token
|
||||
* replacement.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class FileTokenReplaceTest extends FileFieldTestBase {
|
||||
/**
|
||||
* Creates a file, then tests the tokens generated from it.
|
||||
*/
|
||||
function testFileTokenReplacement() {
|
||||
$node_storage = $this->container->get('entity.manager')->getStorage('node');
|
||||
$token_service = \Drupal::token();
|
||||
$language_interface = \Drupal::languageManager()->getCurrentLanguage();
|
||||
|
||||
// Create file field.
|
||||
$type_name = 'article';
|
||||
$field_name = 'field_' . strtolower($this->randomMachineName());
|
||||
$this->createFileField($field_name, 'node', $type_name);
|
||||
|
||||
$test_file = $this->getTestFile('text');
|
||||
// Coping a file to test uploads with non-latin filenames.
|
||||
$filename = drupal_dirname($test_file->getFileUri()) . '/текстовый файл.txt';
|
||||
$test_file = file_copy($test_file, $filename);
|
||||
|
||||
// Create a new node with the uploaded file.
|
||||
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
|
||||
|
||||
// Load the node and the file.
|
||||
$node_storage->resetCache(array($nid));
|
||||
$node = $node_storage->load($nid);
|
||||
$file = File::load($node->{$field_name}->target_id);
|
||||
|
||||
// Generate and test sanitized tokens.
|
||||
$tests = array();
|
||||
$tests['[file:fid]'] = $file->id();
|
||||
$tests['[file:name]'] = Html::escape($file->getFilename());
|
||||
$tests['[file:path]'] = Html::escape($file->getFileUri());
|
||||
$tests['[file:mime]'] = Html::escape($file->getMimeType());
|
||||
$tests['[file:size]'] = format_size($file->getSize());
|
||||
$tests['[file:url]'] = Html::escape(file_create_url($file->getFileUri()));
|
||||
$tests['[file:created]'] = format_date($file->getCreatedTime(), 'medium', '', NULL, $language_interface->getId());
|
||||
$tests['[file:created:short]'] = format_date($file->getCreatedTime(), 'short', '', NULL, $language_interface->getId());
|
||||
$tests['[file:changed]'] = format_date($file->getChangedTime(), 'medium', '', NULL, $language_interface->getId());
|
||||
$tests['[file:changed:short]'] = format_date($file->getChangedTime(), 'short', '', NULL, $language_interface->getId());
|
||||
$tests['[file:owner]'] = Html::escape($this->adminUser->getDisplayName());
|
||||
$tests['[file:owner:uid]'] = $file->getOwnerId();
|
||||
|
||||
$base_bubbleable_metadata = BubbleableMetadata::createFromObject($file);
|
||||
$metadata_tests = [];
|
||||
$metadata_tests['[file:fid]'] = $base_bubbleable_metadata;
|
||||
$metadata_tests['[file:name]'] = $base_bubbleable_metadata;
|
||||
$metadata_tests['[file:path]'] = $base_bubbleable_metadata;
|
||||
$metadata_tests['[file:mime]'] = $base_bubbleable_metadata;
|
||||
$metadata_tests['[file:size]'] = $base_bubbleable_metadata;
|
||||
$bubbleable_metadata = clone $base_bubbleable_metadata;
|
||||
$metadata_tests['[file:url]'] = $bubbleable_metadata->addCacheContexts(['url.site']);
|
||||
$bubbleable_metadata = clone $base_bubbleable_metadata;
|
||||
$metadata_tests['[file:created]'] = $bubbleable_metadata->addCacheTags(['rendered']);
|
||||
$metadata_tests['[file:created:short]'] = $bubbleable_metadata;
|
||||
$metadata_tests['[file:changed]'] = $bubbleable_metadata;
|
||||
$metadata_tests['[file:changed:short]'] = $bubbleable_metadata;
|
||||
$bubbleable_metadata = clone $base_bubbleable_metadata;
|
||||
$metadata_tests['[file:owner]'] = $bubbleable_metadata->addCacheTags(['user:2']);
|
||||
$metadata_tests['[file:owner:uid]'] = $bubbleable_metadata;
|
||||
|
||||
// Test to make sure that we generated something for each token.
|
||||
$this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.');
|
||||
|
||||
foreach ($tests as $input => $expected) {
|
||||
$bubbleable_metadata = new BubbleableMetadata();
|
||||
$output = $token_service->replace($input, array('file' => $file), array('langcode' => $language_interface->getId()), $bubbleable_metadata);
|
||||
$this->assertEqual($output, $expected, format_string('Sanitized file token %token replaced.', array('%token' => $input)));
|
||||
$this->assertEqual($bubbleable_metadata, $metadata_tests[$input]);
|
||||
}
|
||||
|
||||
// Generate and test unsanitized tokens.
|
||||
$tests['[file:name]'] = $file->getFilename();
|
||||
$tests['[file:path]'] = $file->getFileUri();
|
||||
$tests['[file:mime]'] = $file->getMimeType();
|
||||
$tests['[file:size]'] = format_size($file->getSize());
|
||||
|
||||
foreach ($tests as $input => $expected) {
|
||||
$output = $token_service->replace($input, array('file' => $file), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
|
||||
$this->assertEqual($output, $expected, format_string('Unsanitized file token %token replaced.', array('%token' => $input)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\node\Entity\Node;
|
||||
|
||||
/**
|
||||
* Uploads private files to translated node and checks access.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class PrivateFileOnTranslatedEntityTest extends FileFieldTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = array('language', 'content_translation');
|
||||
|
||||
/**
|
||||
* The name of the file field used in the test.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fieldName;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create the "Basic page" node type.
|
||||
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
|
||||
|
||||
// Create a file field on the "Basic page" node type.
|
||||
$this->fieldName = strtolower($this->randomMachineName());
|
||||
$this->createFileField($this->fieldName, 'node', 'page', array('uri_scheme' => 'private'));
|
||||
|
||||
// Create and log in user.
|
||||
$permissions = array(
|
||||
'access administration pages',
|
||||
'administer content translation',
|
||||
'administer content types',
|
||||
'administer languages',
|
||||
'create content translations',
|
||||
'create page content',
|
||||
'edit any page content',
|
||||
'translate any entity',
|
||||
);
|
||||
$admin_user = $this->drupalCreateUser($permissions);
|
||||
$this->drupalLogin($admin_user);
|
||||
|
||||
// Add a second language.
|
||||
$edit = array();
|
||||
$edit['predefined_langcode'] = 'fr';
|
||||
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
|
||||
|
||||
// Enable translation for "Basic page" nodes.
|
||||
$edit = array(
|
||||
'entity_types[node]' => 1,
|
||||
'settings[node][page][translatable]' => 1,
|
||||
"settings[node][page][fields][$this->fieldName]" => 1,
|
||||
);
|
||||
$this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration'));
|
||||
\Drupal::entityManager()->clearCachedDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests private file fields on translated nodes.
|
||||
*/
|
||||
public function testPrivateLanguageFile() {
|
||||
// Verify that the file field on the "Basic page" node type is translatable.
|
||||
$definitions = \Drupal::entityManager()->getFieldDefinitions('node', 'page');
|
||||
$this->assertTrue($definitions[$this->fieldName]->isTranslatable(), 'Node file field is translatable.');
|
||||
|
||||
// Create a default language node.
|
||||
$default_language_node = $this->drupalCreateNode(array('type' => 'page'));
|
||||
|
||||
// Edit the node to upload a file.
|
||||
$edit = array();
|
||||
$name = 'files[' . $this->fieldName . '_0]';
|
||||
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[0]->uri);
|
||||
$this->drupalPostForm('node/' . $default_language_node->id() . '/edit', $edit, t('Save'));
|
||||
$last_fid_prior = $this->getLastFileId();
|
||||
|
||||
// Languages are cached on many levels, and we need to clear those caches.
|
||||
$this->rebuildContainer();
|
||||
|
||||
// Ensure the file can be downloaded.
|
||||
\Drupal::entityManager()->getStorage('node')->resetCache(array($default_language_node->id()));
|
||||
$node = Node::load($default_language_node->id());
|
||||
$node_file = File::load($node->{$this->fieldName}->target_id);
|
||||
$this->drupalGet(file_create_url($node_file->getFileUri()));
|
||||
$this->assertResponse(200, 'Confirmed that the file attached to the English node can be downloaded.');
|
||||
|
||||
// Translate the node into French.
|
||||
$this->drupalGet('node/' . $default_language_node->id() . '/translations');
|
||||
$this->clickLink(t('Add'));
|
||||
|
||||
// Remove the existing file.
|
||||
$this->drupalPostForm(NULL, array(), t('Remove'));
|
||||
|
||||
// Upload a different file.
|
||||
$edit = array();
|
||||
$edit['title[0][value]'] = $this->randomMachineName();
|
||||
$name = 'files[' . $this->fieldName . '_0]';
|
||||
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[1]->uri);
|
||||
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
|
||||
$last_fid = $this->getLastFileId();
|
||||
|
||||
// Verify the translation was created.
|
||||
\Drupal::entityManager()->getStorage('node')->resetCache(array($default_language_node->id()));
|
||||
$default_language_node = Node::load($default_language_node->id());
|
||||
$this->assertTrue($default_language_node->hasTranslation('fr'), 'Node found in database.');
|
||||
$this->assertTrue($last_fid > $last_fid_prior, 'New file got saved.');
|
||||
|
||||
// Ensure the file attached to the translated node can be downloaded.
|
||||
$french_node = $default_language_node->getTranslation('fr');
|
||||
$node_file = File::load($french_node->{$this->fieldName}->target_id);
|
||||
$this->drupalGet(file_create_url($node_file->getFileUri()));
|
||||
$this->assertResponse(200, 'Confirmed that the file attached to the French node can be downloaded.');
|
||||
}
|
||||
|
||||
}
|
24
web/core/modules/file/src/Tests/RemoteFileSaveUploadTest.php
Normal file
24
web/core/modules/file/src/Tests/RemoteFileSaveUploadTest.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
/**
|
||||
* Tests the file uploading functions.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class RemoteFileSaveUploadTest extends SaveUploadTest {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('file_test');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->config('system.file')->set('default_scheme', 'dummy-remote')->save();
|
||||
}
|
||||
|
||||
}
|
362
web/core/modules/file/src/Tests/SaveUploadTest.php
Normal file
362
web/core/modules/file/src/Tests/SaveUploadTest.php
Normal file
|
@ -0,0 +1,362 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests;
|
||||
|
||||
use Drupal\file\Entity\File;
|
||||
|
||||
/**
|
||||
* Tests the file_save_upload() function.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class SaveUploadTest extends FileManagedTestBase {
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('dblog');
|
||||
|
||||
/**
|
||||
* An image file path for uploading.
|
||||
*
|
||||
* @var \Drupal\file\FileInterface
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* A PHP file path for upload security testing.
|
||||
*/
|
||||
protected $phpfile;
|
||||
|
||||
/**
|
||||
* The largest file id when the test starts.
|
||||
*/
|
||||
protected $maxFidBefore;
|
||||
|
||||
/**
|
||||
* Extension of the image filename.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $imageExtension;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$account = $this->drupalCreateUser(array('access site reports'));
|
||||
$this->drupalLogin($account);
|
||||
|
||||
$image_files = $this->drupalGetTestFiles('image');
|
||||
$this->image = File::create((array) current($image_files));
|
||||
|
||||
list(, $this->imageExtension) = explode('.', $this->image->getFilename());
|
||||
$this->assertTrue(is_file($this->image->getFileUri()), "The image file we're going to upload exists.");
|
||||
|
||||
$this->phpfile = current($this->drupalGetTestFiles('php'));
|
||||
$this->assertTrue(is_file($this->phpfile->uri), 'The PHP file we are going to upload exists.');
|
||||
|
||||
$this->maxFidBefore = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField();
|
||||
|
||||
// Upload with replace to guarantee there's something there.
|
||||
$edit = array(
|
||||
'file_test_replace' => FILE_EXISTS_REPLACE,
|
||||
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri()),
|
||||
);
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$this->assertRaw(t('You WIN!'), 'Found the success message.');
|
||||
|
||||
// Check that the correct hooks were called then clean out the hook
|
||||
// counters.
|
||||
$this->assertFileHooksCalled(array('validate', 'insert'));
|
||||
file_test_reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the file_save_upload() function.
|
||||
*/
|
||||
function testNormal() {
|
||||
$max_fid_after = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField();
|
||||
$this->assertTrue($max_fid_after > $this->maxFidBefore, 'A new file was created.');
|
||||
$file1 = File::load($max_fid_after);
|
||||
$this->assertTrue($file1, 'Loaded the file.');
|
||||
// MIME type of the uploaded image may be either image/jpeg or image/png.
|
||||
$this->assertEqual(substr($file1->getMimeType(), 0, 5), 'image', 'A MIME type was set.');
|
||||
|
||||
// Reset the hook counters to get rid of the 'load' we just called.
|
||||
file_test_reset();
|
||||
|
||||
// Upload a second file.
|
||||
$image2 = current($this->drupalGetTestFiles('image'));
|
||||
$edit = array('files[file_test_upload]' => drupal_realpath($image2->uri));
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$this->assertRaw(t('You WIN!'));
|
||||
$max_fid_after = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField();
|
||||
|
||||
// Check that the correct hooks were called.
|
||||
$this->assertFileHooksCalled(array('validate', 'insert'));
|
||||
|
||||
$file2 = File::load($max_fid_after);
|
||||
$this->assertTrue($file2, 'Loaded the file');
|
||||
// MIME type of the uploaded image may be either image/jpeg or image/png.
|
||||
$this->assertEqual(substr($file2->getMimeType(), 0, 5), 'image', 'A MIME type was set.');
|
||||
|
||||
// Load both files using File::loadMultiple().
|
||||
$files = File::loadMultiple(array($file1->id(), $file2->id()));
|
||||
$this->assertTrue(isset($files[$file1->id()]), 'File was loaded successfully');
|
||||
$this->assertTrue(isset($files[$file2->id()]), 'File was loaded successfully');
|
||||
|
||||
// Upload a third file to a subdirectory.
|
||||
$image3 = current($this->drupalGetTestFiles('image'));
|
||||
$image3_realpath = drupal_realpath($image3->uri);
|
||||
$dir = $this->randomMachineName();
|
||||
$edit = array(
|
||||
'files[file_test_upload]' => $image3_realpath,
|
||||
'file_subdir' => $dir,
|
||||
);
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$this->assertRaw(t('You WIN!'));
|
||||
$this->assertTrue(is_file('temporary://' . $dir . '/' . trim(drupal_basename($image3_realpath))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test extension handling.
|
||||
*/
|
||||
function testHandleExtension() {
|
||||
// The file being tested is a .gif which is in the default safe list
|
||||
// of extensions to allow when the extension validator isn't used. This is
|
||||
// implicitly tested at the testNormal() test. Here we tell
|
||||
// file_save_upload() to only allow ".foo".
|
||||
$extensions = 'foo';
|
||||
$edit = array(
|
||||
'file_test_replace' => FILE_EXISTS_REPLACE,
|
||||
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri()),
|
||||
'extensions' => $extensions,
|
||||
);
|
||||
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$message = t('Only files with the following extensions are allowed:') . ' <em class="placeholder">' . $extensions . '</em>';
|
||||
$this->assertRaw($message, 'Cannot upload a disallowed extension');
|
||||
$this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');
|
||||
|
||||
// Check that the correct hooks were called.
|
||||
$this->assertFileHooksCalled(array('validate'));
|
||||
|
||||
// Reset the hook counters.
|
||||
file_test_reset();
|
||||
|
||||
$extensions = 'foo ' . $this->imageExtension;
|
||||
// Now tell file_save_upload() to allow the extension of our test image.
|
||||
$edit = array(
|
||||
'file_test_replace' => FILE_EXISTS_REPLACE,
|
||||
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri()),
|
||||
'extensions' => $extensions,
|
||||
);
|
||||
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$this->assertNoRaw(t('Only files with the following extensions are allowed:'), 'Can upload an allowed extension.');
|
||||
$this->assertRaw(t('You WIN!'), 'Found the success message.');
|
||||
|
||||
// Check that the correct hooks were called.
|
||||
$this->assertFileHooksCalled(array('validate', 'load', 'update'));
|
||||
|
||||
// Reset the hook counters.
|
||||
file_test_reset();
|
||||
|
||||
// Now tell file_save_upload() to allow any extension.
|
||||
$edit = array(
|
||||
'file_test_replace' => FILE_EXISTS_REPLACE,
|
||||
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri()),
|
||||
'allow_all_extensions' => TRUE,
|
||||
);
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$this->assertNoRaw(t('Only files with the following extensions are allowed:'), 'Can upload any extension.');
|
||||
$this->assertRaw(t('You WIN!'), 'Found the success message.');
|
||||
|
||||
// Check that the correct hooks were called.
|
||||
$this->assertFileHooksCalled(array('validate', 'load', 'update'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test dangerous file handling.
|
||||
*/
|
||||
function testHandleDangerousFile() {
|
||||
$config = $this->config('system.file');
|
||||
// Allow the .php extension and make sure it gets renamed to .txt for
|
||||
// safety. Also check to make sure its MIME type was changed.
|
||||
$edit = array(
|
||||
'file_test_replace' => FILE_EXISTS_REPLACE,
|
||||
'files[file_test_upload]' => drupal_realpath($this->phpfile->uri),
|
||||
'is_image_file' => FALSE,
|
||||
'extensions' => 'php',
|
||||
);
|
||||
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$message = t('For security reasons, your upload has been renamed to') . ' <em class="placeholder">' . $this->phpfile->filename . '.txt' . '</em>';
|
||||
$this->assertRaw($message, 'Dangerous file was renamed.');
|
||||
$this->assertRaw(t('File MIME type is text/plain.'), "Dangerous file's MIME type was changed.");
|
||||
$this->assertRaw(t('You WIN!'), 'Found the success message.');
|
||||
|
||||
// Check that the correct hooks were called.
|
||||
$this->assertFileHooksCalled(array('validate', 'insert'));
|
||||
|
||||
// Ensure dangerous files are not renamed when insecure uploads is TRUE.
|
||||
// Turn on insecure uploads.
|
||||
$config->set('allow_insecure_uploads', 1)->save();
|
||||
// Reset the hook counters.
|
||||
file_test_reset();
|
||||
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$this->assertNoRaw(t('For security reasons, your upload has been renamed'), 'Found no security message.');
|
||||
$this->assertRaw(t('File name is @filename', array('@filename' => $this->phpfile->filename)), 'Dangerous file was not renamed when insecure uploads is TRUE.');
|
||||
$this->assertRaw(t('You WIN!'), 'Found the success message.');
|
||||
|
||||
// Check that the correct hooks were called.
|
||||
$this->assertFileHooksCalled(array('validate', 'insert'));
|
||||
|
||||
// Turn off insecure uploads.
|
||||
$config->set('allow_insecure_uploads', 0)->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test file munge handling.
|
||||
*/
|
||||
function testHandleFileMunge() {
|
||||
// Ensure insecure uploads are disabled for this test.
|
||||
$this->config('system.file')->set('allow_insecure_uploads', 0)->save();
|
||||
$this->image = file_move($this->image, $this->image->getFileUri() . '.foo.' . $this->imageExtension);
|
||||
|
||||
// Reset the hook counters to get rid of the 'move' we just called.
|
||||
file_test_reset();
|
||||
|
||||
$extensions = $this->imageExtension;
|
||||
$edit = array(
|
||||
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri()),
|
||||
'extensions' => $extensions,
|
||||
);
|
||||
|
||||
$munged_filename = $this->image->getFilename();
|
||||
$munged_filename = substr($munged_filename, 0, strrpos($munged_filename, '.'));
|
||||
$munged_filename .= '_.' . $this->imageExtension;
|
||||
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$this->assertRaw(t('For security reasons, your upload has been renamed'), 'Found security message.');
|
||||
$this->assertRaw(t('File name is @filename', array('@filename' => $munged_filename)), 'File was successfully munged.');
|
||||
$this->assertRaw(t('You WIN!'), 'Found the success message.');
|
||||
|
||||
// Check that the correct hooks were called.
|
||||
$this->assertFileHooksCalled(array('validate', 'insert'));
|
||||
|
||||
// Ensure we don't munge files if we're allowing any extension.
|
||||
// Reset the hook counters.
|
||||
file_test_reset();
|
||||
|
||||
$edit = array(
|
||||
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri()),
|
||||
'allow_all_extensions' => TRUE,
|
||||
);
|
||||
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$this->assertNoRaw(t('For security reasons, your upload has been renamed'), 'Found no security message.');
|
||||
$this->assertRaw(t('File name is @filename', array('@filename' => $this->image->getFilename())), 'File was not munged when allowing any extension.');
|
||||
$this->assertRaw(t('You WIN!'), 'Found the success message.');
|
||||
|
||||
// Check that the correct hooks were called.
|
||||
$this->assertFileHooksCalled(array('validate', 'insert'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test renaming when uploading over a file that already exists.
|
||||
*/
|
||||
function testExistingRename() {
|
||||
$edit = array(
|
||||
'file_test_replace' => FILE_EXISTS_RENAME,
|
||||
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri())
|
||||
);
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$this->assertRaw(t('You WIN!'), 'Found the success message.');
|
||||
|
||||
// Check that the correct hooks were called.
|
||||
$this->assertFileHooksCalled(array('validate', 'insert'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test replacement when uploading over a file that already exists.
|
||||
*/
|
||||
function testExistingReplace() {
|
||||
$edit = array(
|
||||
'file_test_replace' => FILE_EXISTS_REPLACE,
|
||||
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri())
|
||||
);
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$this->assertRaw(t('You WIN!'), 'Found the success message.');
|
||||
|
||||
// Check that the correct hooks were called.
|
||||
$this->assertFileHooksCalled(array('validate', 'load', 'update'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for failure when uploading over a file that already exists.
|
||||
*/
|
||||
function testExistingError() {
|
||||
$edit = array(
|
||||
'file_test_replace' => FILE_EXISTS_ERROR,
|
||||
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri())
|
||||
);
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');
|
||||
|
||||
// Check that the no hooks were called while failing.
|
||||
$this->assertFileHooksCalled(array());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for no failures when not uploading a file.
|
||||
*/
|
||||
function testNoUpload() {
|
||||
$this->drupalPostForm('file-test/upload', array(), t('Submit'));
|
||||
$this->assertNoRaw(t('Epic upload FAIL!'), 'Failure message not found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for log entry on failing destination.
|
||||
*/
|
||||
function testDrupalMovingUploadedFileError() {
|
||||
// Create a directory and make it not writable.
|
||||
$test_directory = 'test_drupal_move_uploaded_file_fail';
|
||||
drupal_mkdir('temporary://' . $test_directory, 0000);
|
||||
$this->assertTrue(is_dir('temporary://' . $test_directory));
|
||||
|
||||
$edit = array(
|
||||
'file_subdir' => $test_directory,
|
||||
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri())
|
||||
);
|
||||
|
||||
\Drupal::state()->set('file_test.disable_error_collection', TRUE);
|
||||
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
|
||||
$this->assertResponse(200, 'Received a 200 response for posted test file.');
|
||||
$this->assertRaw(t('File upload error. Could not move uploaded file.'), 'Found the failure message.');
|
||||
$this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');
|
||||
|
||||
// Uploading failed. Now check the log.
|
||||
$this->drupalGet('admin/reports/dblog');
|
||||
$this->assertResponse(200);
|
||||
$this->assertRaw(t('Upload error. Could not move uploaded file @file to destination @destination.', array(
|
||||
'@file' => $this->image->getFilename(),
|
||||
'@destination' => 'temporary://' . $test_directory . '/' . $this->image->getFilename()
|
||||
)), 'Found upload error log entry.');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file\Tests\Views;
|
||||
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\views\Tests\ViewTestBase;
|
||||
use Drupal\views\Views;
|
||||
use Drupal\views\Tests\ViewTestData;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
|
||||
/**
|
||||
* Tests file on user relationship handler.
|
||||
*
|
||||
* @group file
|
||||
*/
|
||||
class RelationshipUserFileDataTest extends ViewTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('file', 'file_test_views', 'user');
|
||||
|
||||
/**
|
||||
* Views used by this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $testViews = array('test_file_user_file_data');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create the user profile field and instance.
|
||||
FieldStorageConfig::create(array(
|
||||
'entity_type' => 'user',
|
||||
'field_name' => 'user_file',
|
||||
'type' => 'file',
|
||||
'translatable' => '0',
|
||||
))->save();
|
||||
FieldConfig::create([
|
||||
'label' => 'User File',
|
||||
'description' => '',
|
||||
'field_name' => 'user_file',
|
||||
'entity_type' => 'user',
|
||||
'bundle' => 'user',
|
||||
'required' => 0,
|
||||
])->save();
|
||||
|
||||
ViewTestData::createTestViews(get_class($this), array('file_test_views'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the views file relationship.
|
||||
*/
|
||||
public function testViewsHandlerRelationshipUserFileData() {
|
||||
$file = File::create([
|
||||
'fid' => 2,
|
||||
'uid' => 2,
|
||||
'filename' => 'image-test.jpg',
|
||||
'uri' => "public://image-test.jpg",
|
||||
'filemime' => 'image/jpeg',
|
||||
'created' => 1,
|
||||
'changed' => 1,
|
||||
'status' => FILE_STATUS_PERMANENT,
|
||||
]);
|
||||
$file->enforceIsNew();
|
||||
file_put_contents($file->getFileUri(), file_get_contents('core/modules/simpletest/files/image-1.png'));
|
||||
$file->save();
|
||||
|
||||
$account = $this->drupalCreateUser();
|
||||
$account->user_file->target_id = 2;
|
||||
$account->save();
|
||||
|
||||
$view = Views::getView('test_file_user_file_data');
|
||||
// Tests \Drupal\taxonomy\Plugin\views\relationship\NodeTermData::calculateDependencies().
|
||||
$expected = [
|
||||
'module' => [
|
||||
'file',
|
||||
'user',
|
||||
],
|
||||
];
|
||||
$this->assertIdentical($expected, $view->getDependencies());
|
||||
$this->executeView($view);
|
||||
$expected_result = array(
|
||||
array(
|
||||
'file_managed_user__user_file_fid' => '2',
|
||||
),
|
||||
);
|
||||
$column_map = array('file_managed_user__user_file_fid' => 'file_managed_user__user_file_fid');
|
||||
$this->assertIdenticalResultset($view, $expected_result, $column_map);
|
||||
}
|
||||
|
||||
}
|
15
web/core/modules/file/templates/file-link.html.twig
Normal file
15
web/core/modules/file/templates/file-link.html.twig
Normal 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>
|
23
web/core/modules/file/templates/file-managed-file.html.twig
Normal file
23
web/core/modules/file/templates/file-managed-file.html.twig
Normal 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>
|
14
web/core/modules/file/templates/file-upload-help.html.twig
Normal file
14
web/core/modules/file/templates/file-upload-help.html.twig
Normal 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 />') }}
|
|
@ -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 }}
|
|
@ -0,0 +1,6 @@
|
|||
name: 'File test'
|
||||
type: module
|
||||
description: 'Provides hooks for testing File module functionality.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
|
@ -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'
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\file_module_test\Form;
|
||||
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Form controller for file_module_test module.
|
||||
*/
|
||||
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'] = array(
|
||||
'#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 ? array('fids' => $default_fids) : $default_fids;
|
||||
}
|
||||
|
||||
$form['textfield'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Type a value and ensure it stays'),
|
||||
);
|
||||
|
||||
$form['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Save'),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
if ($form['#tree']) {
|
||||
$uploads = $form_state->getValue(array('nested', 'file'));
|
||||
}
|
||||
else {
|
||||
$uploads = $form_state->getValue('file');
|
||||
}
|
||||
|
||||
if ($form['nested']['file']['#extended']) {
|
||||
$uploads = $uploads['fids'];
|
||||
}
|
||||
|
||||
$fids = array();
|
||||
foreach ($uploads as $fid) {
|
||||
$fids[] = $fid;
|
||||
}
|
||||
|
||||
drupal_set_message($this->t('The file ids are %fids.', array('%fids' => implode(',', $fids))));
|
||||
}
|
||||
|
||||
}
|
6
web/core/modules/file/tests/file_test/file_test.info.yml
Normal file
6
web/core/modules/file/tests/file_test/file_test.info.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
name: 'File test'
|
||||
type: module
|
||||
description: 'Support module for file handling tests.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
351
web/core/modules/file/tests/file_test/file_test.module
Normal file
351
web/core/modules/file/tests/file_test/file_test.module
Normal 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 = array(
|
||||
'load' => array(),
|
||||
'validate' => array(),
|
||||
'download' => array(),
|
||||
'insert' => array(),
|
||||
'update' => array(),
|
||||
'copy' => array(),
|
||||
'move' => array(),
|
||||
'delete' => array(),
|
||||
);
|
||||
\Drupal::state()->set('file_test.results', $results);
|
||||
|
||||
// These hooks will return these values, see file_test_set_return().
|
||||
$return = array(
|
||||
'validate' => array(),
|
||||
'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') ?: array();
|
||||
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') ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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') ?: array();
|
||||
$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') ?: array($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') ?: array();
|
||||
$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', array($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', array($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', array('uri' => $uri));
|
||||
$file = reset($files);
|
||||
return file_get_content_headers($file);
|
||||
}
|
||||
_file_test_log_call('download', array($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', array($file->id()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_ENTITY_TYPE_update() for file entities.
|
||||
*/
|
||||
function file_test_file_update(File $file) {
|
||||
_file_test_log_call('update', array($file->id()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_file_copy().
|
||||
*/
|
||||
function file_test_file_copy(File $file, $source) {
|
||||
_file_test_log_call('copy', array($file->id(), $source->id()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_file_move().
|
||||
*/
|
||||
function file_test_file_move(File $file, File $source) {
|
||||
_file_test_log_call('move', array($file->id(), $source->id()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_ENTITY_TYPE_predelete() for file entities.
|
||||
*/
|
||||
function file_test_file_predelete(File $file) {
|
||||
_file_test_log_call('delete', array($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 = array('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 = array('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__, array());
|
||||
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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
file.test:
|
||||
path: '/file-test/upload'
|
||||
defaults:
|
||||
_form: 'Drupal\file_test\Form\FileTestForm'
|
||||
requirements:
|
||||
_access: 'TRUE'
|
13
web/core/modules/file/tests/file_test/file_test.services.yml
Normal file
13
web/core/modules/file/tests/file_test/file_test.services.yml
Normal 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
Reference in a new issue