Newer
Older
Dries Buytaert
committed
<?php
/**
* @file
* Defines a "managed_file" Form API field and a "file" field for Field module.
*/
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\Core\Entity\EntityStorageInterface;
Alex Pott
committed
use Drupal\Core\Field\FieldDefinitionInterface;
catch
committed
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\Exception\FileExistsException;
use Drupal\Core\File\Exception\FileWriteException;
use Drupal\Core\File\Exception\InvalidStreamWrapperException;
Alex Pott
committed
use Drupal\Core\File\FileExists;
use Drupal\Core\File\FileSystemInterface;
Dries Buytaert
committed
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
Alex Pott
committed
use Drupal\Core\Lock\LockAcquiringException;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\BubbleableMetadata;
Angie Byron
committed
use Drupal\Core\Render\Element;
Angie Byron
committed
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\ByteSizeMarkup;
use Drupal\Core\Template\Attribute;
Alex Pott
committed
use Drupal\Core\Url;
use Drupal\file\Entity\File;
Angie Byron
committed
use Drupal\file\FileInterface;
Dave Long
committed
use Drupal\file\IconMimeTypes;
Lee Rowlands
committed
use Drupal\file\Upload\FormUploadedFile;
Dave Long
committed
// cspell:ignore widthx
/**
* Implements hook_help().
*/
Angie Byron
committed
function file_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.file':
$output = '';
$output .= '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The File module allows you to create fields that contain files. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":file_documentation">online documentation for the File module</a>.', [':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':file_documentation' => 'https://www.drupal.org/documentation/modules/file']) . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
Jennifer Hodgdon
committed
$output .= '<dt>' . t('Managing and displaying file fields') . '</dt>';
$output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the file field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>';
Jennifer Hodgdon
committed
$output .= '<dt>' . t('Allowing file extensions') . '</dt>';
$output .= '<dd>' . t('In the field settings, you can define the allowed file extensions (for example <em>pdf docx psd</em>) for the files that will be uploaded with the file field.') . '</dd>';
$output .= '<dt>' . t('Storing files') . '</dt>';
$output .= '<dd>' . t('Uploaded files can either be stored as <em>public</em> or <em>private</em>, depending on the <a href=":file-system">File system settings</a>. For more information, see the <a href=":system-help">System module help page</a>.', [':file-system' => Url::fromRoute('system.file_system_settings')->toString(), ':system-help' => Url::fromRoute('help.page', ['name' => 'system'])->toString()]) . '</dd>';
Jennifer Hodgdon
committed
$output .= '<dt>' . t('Restricting the maximum file size') . '</dt>';
$output .= '<dd>' . t('The maximum file size that users can upload is limited by PHP settings of the server, but you can restrict by entering the desired value as the <em>Maximum upload size</em> setting. The maximum file size is automatically displayed to users in the help text of the file field.') . '</dd>';
$output .= '<dt>' . t('Displaying files and descriptions') . '<dt>';
$output .= '<dd>' . t('In the field settings, you can allow users to toggle whether individual files are displayed. In the display settings, you can then choose one of the following formats: <ul><li><em>Generic file</em> displays links to the files and adds icons that symbolize the file extensions. If <em>descriptions</em> are enabled and have been submitted, then the description is displayed instead of the file name.</li><li><em>URL to file</em> displays the full path to the file as plain text.</li><li><em>Table of files</em> lists links to the files and the file sizes in a table.</li><li><em>RSS enclosure</em> only displays the first file, and only in a RSS feed, formatted according to the RSS 2.0 syntax for enclosures.</li></ul> A file can still be linked to directly by its URI even if it is not displayed.') . '</dd>';
$output .= '</dl>';
return $output;
}
}
Alex Bronstein
committed
/**
* Implements hook_field_widget_info_alter().
*/
function file_field_widget_info_alter(array &$info) {
// Allows using the 'uri' widget for the 'file_uri' field type, which uses it
// as the default widget.
// @see \Drupal\file\Plugin\Field\FieldType\FileUriItem
$info['uri']['field_types'][] = 'file_uri';
}
Angie Byron
committed
/**
* Examines a file entity and returns appropriate content headers for download.
*
Angie Byron
committed
* @param \Drupal\file\FileInterface $file
Angie Byron
committed
* A file entity.
*
Alex Pott
committed
* @return array
Angie Byron
committed
* An associative array of headers, as expected by
* \Symfony\Component\HttpFoundation\StreamedResponse.
*/
Angie Byron
committed
function file_get_content_headers(FileInterface $file) {
return [
'Content-Type' => $file->getMimeType(),
'Content-Length' => $file->getSize(),
Angie Byron
committed
'Cache-Control' => 'private',
];
Angie Byron
committed
}
Dries Buytaert
committed
/**
Dries Buytaert
committed
* Implements hook_theme().
Dries Buytaert
committed
*/
function file_theme() {
return [
Angie Byron
committed
// From file.module.
'file_link' => [
'variables' => ['file' => NULL, 'description' => NULL, 'attributes' => []],
],
'file_managed_file' => [
'render element' => 'element',
],
'file_audio' => [
'variables' => ['files' => [], 'attributes' => NULL],
],
'file_video' => [
'variables' => ['files' => [], 'attributes' => NULL],
],
'file_widget_multiple' => [
'render element' => 'element',
],
'file_upload_help' => [
'variables' => ['description' => NULL, 'upload_validators' => NULL, 'cardinality' => NULL],
],
];
Dries Buytaert
committed
}
/**
Dries Buytaert
committed
* Implements hook_file_download().
Dries Buytaert
committed
*/
Alex Pott
committed
function file_file_download($uri) {
Dries Buytaert
committed
// Get the file record based on the URI. If not in the database just return.
Lee Rowlands
committed
/** @var \Drupal\file\FileRepositoryInterface $file_repository */
$file_repository = \Drupal::service('file.repository');
$file = $file_repository->loadByUri($uri);
if (!$file) {
Dries Buytaert
committed
return;
}
// Find out if a temporary file is still used in the system.
if ($file->isTemporary()) {
$usage = \Drupal::service('file.usage')->listUsage($file);
if (empty($usage) && $file->getOwnerId() != \Drupal::currentUser()->id()) {
// Deny access to temporary files without usage that are not owned by the
// same user. This prevents the security issue that a private file that
// was protected by field permissions becomes available after its usage
// was removed and before it is actually deleted from the file system.
// Modules that depend on this behavior should make the file permanent
// instead.
return -1;
}
}
// Find out which (if any) fields of this type contain the file.
Alex Pott
committed
$references = file_get_file_references($file, NULL, EntityStorageInterface::FIELD_LOAD_CURRENT, NULL);
Dries Buytaert
committed
Angie Byron
committed
// Stop processing if there are no references in order to avoid returning
// headers for files controlled by other modules. Make an exception for
// temporary files where the host entity has not yet been saved (for example,
// an image preview on a node/add form) in which case, allow download by the
// file's owner.
Alex Pott
committed
if (empty($references) && ($file->isPermanent() || $file->getOwnerId() != \Drupal::currentUser()->id())) {
Alex Pott
committed
if (!$file->access('download')) {
Dries Buytaert
committed
return -1;
}
// Access is granted.
Dries Buytaert
committed
$headers = file_get_content_headers($file);
return $headers;
Dries Buytaert
committed
}
Angie Byron
committed
/**
Angie Byron
committed
* Implements hook_cron().
Angie Byron
committed
*/
function file_cron() {
$age = \Drupal::config('system.file')->get('temporary_maximum_age');
$file_storage = \Drupal::entityTypeManager()->getStorage('file');
/** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
$stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
// Only delete temporary files if older than $age. Note that automatic cleanup
// is disabled if $age set to 0.
if ($age) {
$fids = Drupal::entityQuery('file')
->accessCheck(FALSE)
->condition('status', FileInterface::STATUS_PERMANENT, '<>')
Alex Pott
committed
->condition('changed', \Drupal::time()->getRequestTime() - $age, '<')
->range(0, 100)
->execute();
$files = $file_storage->loadMultiple($fids);
foreach ($files as $file) {
$references = \Drupal::service('file.usage')->listUsage($file);
Angie Byron
committed
if (empty($references)) {
if (!file_exists($file->getFileUri())) {
if (!$stream_wrapper_manager->isValidUri($file->getFileUri())) {
\Drupal::logger('file system')->warning('Temporary file "%path" that was deleted during garbage collection did not exist on the filesystem. This could be caused by a missing stream wrapper.', ['%path' => $file->getFileUri()]);
}
else {
\Drupal::logger('file system')->warning('Temporary file "%path" that was deleted during garbage collection did not exist on the filesystem.', ['%path' => $file->getFileUri()]);
}
Angie Byron
committed
}
// Delete the file entity. If the file does not exist, this will
// generate a second notice in the watchdog.
$file->delete();
Angie Byron
committed
}
else {
\Drupal::logger('file system')->info('Did not delete temporary file "%path" during garbage collection because it is in use by the following modules: %modules.', ['%path' => $file->getFileUri(), '%modules' => implode(', ', array_keys($references))]);
Angie Byron
committed
}
}
}
}
/**
* Saves form file uploads.
*
* The files will be added to the {file_managed} table as temporary files.
* Temporary files are periodically cleaned. Use the 'file.usage' service to
* register the usage of the file which will automatically mark it as permanent.
*
* @param array $element
* The FAPI element whose values are being saved.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param null|int $delta
* (optional) The delta of the file to return the file entity.
* Defaults to NULL.
Alex Pott
committed
* @param \Drupal\Core\File\FileExists|int $fileExists
* (optional) The replace behavior when the destination file already exists.
*
* @return array|\Drupal\file\FileInterface|null|false
* An array of file entities or a single file entity if $delta != NULL. Each
* array element contains the file entity if the upload succeeded or FALSE if
* there was an error. Function returns NULL if no file was uploaded.
*
Alex Pott
committed
* @throws \ValueError
* Thrown if $fileExists is a legacy int and not a valid value.
*
* @internal
Lee Rowlands
committed
* This function is internal, and may be removed in a minor version release.
* It wraps file_save_upload() to allow correct error handling in forms.
* Contrib and custom code should not call this function, they should use the
* managed file upload widgets in core.
*
Lee Rowlands
committed
* @see https://www.drupal.org/project/drupal/issues/3069020
* @see https://www.drupal.org/project/drupal/issues/2482783
*/
Alex Pott
committed
function _file_save_upload_from_form(array $element, FormStateInterface $form_state, $delta = NULL, FileExists|int $fileExists = FileExists::Rename) {
if (!$fileExists instanceof FileExists) {
// @phpstan-ignore-next-line
$fileExists = FileExists::fromLegacyInt($fileExists, __METHOD__);
}
// Get all errors set before calling this method. This will also clear them
// from the messenger service.
$errors_before = \Drupal::messenger()->deleteByType(MessengerInterface::TYPE_ERROR);
$upload_location = $element['#upload_location'] ?? FALSE;
$upload_name = implode('_', $element['#parents']);
$upload_validators = $element['#upload_validators'] ?? [];
Alex Pott
committed
$result = file_save_upload($upload_name, $upload_validators, $upload_location, $delta, $fileExists);
// Get new errors that are generated while trying to save the upload. This
// will also clear them from the messenger service.
$errors_new = \Drupal::messenger()->deleteByType(MessengerInterface::TYPE_ERROR);
if (!empty($errors_new)) {
if (count($errors_new) > 1) {
// Render multiple errors into a single message.
// This is needed because only one error per element is supported.
$render_array = [
'error' => [
'#markup' => t('One or more files could not be uploaded.'),
],
'item_list' => [
'#theme' => 'item_list',
'#items' => $errors_new,
],
];
$error_message = \Drupal::service('renderer')->renderInIsolation($render_array);
}
else {
$error_message = reset($errors_new);
}
$form_state->setError($element, $error_message);
}
// Ensure that errors set prior to calling this method are still shown to the
// user.
if (!empty($errors_before)) {
foreach ($errors_before as $error) {
\Drupal::messenger()->addError($error);
}
}
return $result;
}
/**
* Saves file uploads to a new location.
*
* The files will be added to the {file_managed} table as temporary files.
* Temporary files are periodically cleaned. Use the 'file.usage' service to
* register the usage of the file which will automatically mark it as permanent.
* Note that this function does not support correct form error handling. The
* file upload widgets in core do support this. It is advised to use these in
* any custom form, instead of calling this function.
*
Alex Pott
committed
* @param string $form_field_name
* A string that is the associative array key of the upload form element in
* the form array.
Alex Pott
committed
* @param array $validators
* (optional) An associative array of Validation Constraint plugins used to
* validate the file.
* If the array is empty, 'FileExtension' will be used by default with a safe
* list of extensions, as follows: "jpg jpeg gif png txt doc xls pdf ppt pps
* odt ods odp". To allow all extensions, you must explicitly set this array
* to ['FileExtension' => []]. (Beware: this is not safe and should only be
* allowed for trusted users, if at all.)
* @param string|false $destination
* (optional) A string containing the URI that the file should be copied to.
* This must be a stream wrapper URI. If this value is omitted or set to
* FALSE, Drupal's temporary files scheme will be used ("temporary://").
* @param null|int $delta
* (optional) The delta of the file to return the file entity.
* Defaults to NULL.
Alex Pott
committed
* @param \Drupal\Core\File\FileExists|int $fileExists
* (optional) The replace behavior when the destination file already exists.
* @return array|\Drupal\file\FileInterface|null|false
* An array of file entities or a single file entity if $delta != NULL. Each
* array element contains the file entity if the upload succeeded or FALSE if
* there was an error. Function returns NULL if no file was uploaded.
*
Alex Pott
committed
* @throws \ValueError
* Thrown if $fileExists is a legacy int and not a valid value.
*
* @see _file_save_upload_from_form()
Alex Pott
committed
function file_save_upload($form_field_name, $validators = [], $destination = FALSE, $delta = NULL, FileExists|int $fileExists = FileExists::Rename) {
if (!$fileExists instanceof FileExists) {
// @phpstan-ignore-next-line
$fileExists = FileExists::fromLegacyInt($fileExists, __METHOD__);
}
static $upload_cache;
$all_files = \Drupal::request()->files->get('files', []);
// Make sure there's an upload to process.
if (empty($all_files[$form_field_name])) {
return NULL;
}
$file_upload = $all_files[$form_field_name];
// Return cached objects without processing since the file will have
// already been processed and the paths in $_FILES will be invalid.
if (isset($upload_cache[$form_field_name])) {
if (isset($delta)) {
return $upload_cache[$form_field_name][$delta];
}
return $upload_cache[$form_field_name];
}
// Prepare uploaded files info. Representation is slightly different
// for multiple uploads and we fix that here.
Angie Byron
committed
$uploaded_files = $file_upload;
if (!is_array($file_upload)) {
$uploaded_files = [$file_upload];
if ($destination === FALSE || $destination === NULL) {
$destination = 'temporary://';
}
/** @var \Drupal\file\Upload\FileUploadHandler $file_upload_handler */
$file_upload_handler = \Drupal::service('file.upload_handler');
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$files = [];
/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $uploaded_file */
foreach ($uploaded_files as $i => $uploaded_file) {
try {
Lee Rowlands
committed
$form_uploaded_file = new FormUploadedFile($uploaded_file);
$result = $file_upload_handler->handleFileUpload($form_uploaded_file, $validators, $destination, $fileExists);
Lee Rowlands
committed
if ($result->hasViolations()) {
$errors = [];
foreach ($result->getViolations() as $violation) {
$errors[] = $violation->getMessage();
}
$message = [
'error' => [
'#markup' => t('The specified file %name could not be uploaded.', ['%name' => $uploaded_file->getClientOriginalName()]),
],
'item_list' => [
'#theme' => 'item_list',
'#items' => $errors,
],
];
// @todo Add support for render arrays in
// \Drupal\Core\Messenger\MessengerInterface::addMessage()?
// @see https://www.drupal.org/node/2505497.
\Drupal::messenger()->addError($renderer->renderInIsolation($message));
Lee Rowlands
committed
$files[$i] = FALSE;
continue;
}
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
$file = $result->getFile();
// If the filename has been modified, let the user know.
if ($result->isRenamed()) {
if ($result->isSecurityRename()) {
$message = t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]);
}
else {
$message = t('Your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]);
}
\Drupal::messenger()->addStatus($message);
}
$files[$i] = $file;
}
catch (FileExistsException $e) {
\Drupal::messenger()->addError(t('Destination file "%file" exists', ['%file' => $destination . $uploaded_file->getFilename()]));
$files[$i] = FALSE;
}
catch (InvalidStreamWrapperException $e) {
\Drupal::messenger()->addError(t('The file could not be uploaded because the destination "%destination" is invalid.', ['%destination' => $destination]));
$files[$i] = FALSE;
}
catch (FileWriteException $e) {
\Drupal::messenger()->addError(t('File upload error. Could not move uploaded file.'));
\Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $uploaded_file->getClientOriginalName(), '%destination' => $destination . '/' . $uploaded_file->getClientOriginalName()]);
$files[$i] = FALSE;
}
catch (FileException $e) {
\Drupal::messenger()->addError(t('The file %filename could not be uploaded because the name is invalid.', ['%filename' => $uploaded_file->getClientOriginalName()]));
$files[$i] = FALSE;
}
Alex Pott
committed
catch (LockAcquiringException $e) {
\Drupal::messenger()->addError(t('File already locked for writing.'));
$files[$i] = FALSE;
}
}
// Add files to the cache.
$upload_cache[$form_field_name] = $files;
return isset($delta) ? $files[$delta] : $files;
}
Dries Buytaert
committed
/**
* Implements hook_ENTITY_TYPE_predelete() for file entities.
Dries Buytaert
committed
*/
Dries Buytaert
committed
function file_file_predelete(File $file) {
// @todo Remove references to a file that is in-use.
Dries Buytaert
committed
}
/**
* Implements hook_tokens().
*/
function file_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
$token_service = \Drupal::token();
$url_options = ['absolute' => TRUE];
if (isset($options['langcode'])) {
$url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
$langcode = $options['langcode'];
}
else {
$langcode = NULL;
}
$replacements = [];
if ($type == 'file' && !empty($data['file'])) {
/** @var \Drupal\file\FileInterface $file */
$file = $data['file'];
foreach ($tokens as $name => $original) {
switch ($name) {
// Basic keys and values.
case 'fid':
$replacements[$original] = $file->id();
break;
// Essential file data
case 'name':
$replacements[$original] = $file->getFilename();
break;
case 'path':
$replacements[$original] = $file->getFileUri();
break;
case 'mime':
$replacements[$original] = $file->getMimeType();
break;
case 'size':
$replacements[$original] = ByteSizeMarkup::create($file->getSize());
break;
case 'url':
// Ideally, this would use return a relative URL, but because tokens
// are also often used in emails, it's better to keep absolute file
// URLs. The 'url.site' cache context is associated to ensure the
// correct absolute URL is used in case of a multisite setup.
$replacements[$original] = $file->createFileUrl(FALSE);
$bubbleable_metadata->addCacheContexts(['url.site']);
break;
// These tokens are default variations on the chained tokens handled below.
case 'created':
$date_format = DateFormat::load('medium');
$bubbleable_metadata->addCacheableDependency($date_format);
$replacements[$original] = \Drupal::service('date.formatter')->format($file->getCreatedTime(), 'medium', '', NULL, $langcode);
break;
case 'changed':
$date_format = DateFormat::load('medium');
$bubbleable_metadata = $bubbleable_metadata->addCacheableDependency($date_format);
$replacements[$original] = \Drupal::service('date.formatter')->format($file->getChangedTime(), 'medium', '', NULL, $langcode);
break;
case 'owner':
$owner = $file->getOwner();
$bubbleable_metadata->addCacheableDependency($owner);
$name = $owner->label();
$replacements[$original] = $name;
break;
}
}
if ($date_tokens = $token_service->findWithPrefix($tokens, 'created')) {
$replacements += $token_service->generate('date', $date_tokens, ['date' => $file->getCreatedTime()], $options, $bubbleable_metadata);
}
if ($date_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
$replacements += $token_service->generate('date', $date_tokens, ['date' => $file->getChangedTime()], $options, $bubbleable_metadata);
}
if (($owner_tokens = $token_service->findWithPrefix($tokens, 'owner')) && $file->getOwner()) {
$replacements += $token_service->generate('user', $owner_tokens, ['user' => $file->getOwner()], $options, $bubbleable_metadata);
}
}
return $replacements;
}
/**
* Implements hook_token_info().
*/
function file_token_info() {
$types['file'] = [
'name' => t("Files"),
'description' => t("Tokens related to uploaded files."),
'needs-data' => 'file',
];
// File related tokens.
$file['fid'] = [
'name' => t("File ID"),
'description' => t("The unique ID of the uploaded file."),
];
$file['name'] = [
'name' => t("File name"),
'description' => t("The name of the file on disk."),
];
$file['path'] = [
'name' => t("Path"),
'description' => t("The location of the file relative to Drupal root."),
];
$file['mime'] = [
'name' => t("MIME type"),
'description' => t("The MIME type of the file."),
];
$file['size'] = [
'name' => t("File size"),
'description' => t("The size of the file."),
];
$file['url'] = [
'name' => t("URL"),
'description' => t("The web-accessible URL for the file."),
];
$file['created'] = [
'name' => t("Created"),
'description' => t("The date the file created."),
'type' => 'date',
];
$file['changed'] = [
'name' => t("Changed"),
'description' => t("The date the file was most recently changed."),
'type' => 'date',
];
$file['owner'] = [
'name' => t("Owner"),
'description' => t("The user who originally uploaded the file."),
'type' => 'user',
];
return [
'types' => $types,
'tokens' => [
'file' => $file,
],
];
Dries Buytaert
committed
/**
* Form submission handler for upload / remove buttons of managed_file elements.
*
Alex Pott
committed
* @see \Drupal\file\Element\ManagedFile::processManagedFile()
Dries Buytaert
committed
*/
Dries Buytaert
committed
function file_managed_file_submit($form, FormStateInterface $form_state) {
// Determine whether it was the upload or the remove button that was clicked,
// and set $element to the managed_file element that contains that button.
Angie Byron
committed
$parents = $form_state->getTriggeringElement()['#array_parents'];
$button_key = array_pop($parents);
Dries Buytaert
committed
$element = NestedArray::getValue($form, $parents);
// No action is needed here for the upload button, because all file uploads on
Alex Pott
committed
// the form are processed by \Drupal\file\Element\ManagedFile::valueCallback()
// regardless of which button was clicked. Action is needed here for the
// remove button, because we only remove a file in response to its remove
// button being clicked.
if ($button_key == 'remove_button') {
Angie Byron
committed
$fids = array_keys($element['#files']);
// Get files that will be removed.
if ($element['#multiple']) {
$remove_fids = [];
Angie Byron
committed
foreach (Element::children($element) as $name) {
if (str_starts_with($name, 'file_') && $element[$name]['selected']['#value']) {
Angie Byron
committed
$remove_fids[] = (int) substr($name, 5);
}
}
$fids = array_diff($fids, $remove_fids);
}
else {
Alex Pott
committed
// If we deal with single upload element remove the file and set
// element's value to empty array (file could not be removed from
// element if we don't do that).
Angie Byron
committed
$remove_fids = $fids;
$fids = [];
Angie Byron
committed
}
foreach ($remove_fids as $fid) {
// If it's a temporary file we can safely remove it immediately, otherwise
// it's up to the implementing module to remove usages of files to have them
// removed.
if ($element['#files'][$fid] && $element['#files'][$fid]->isTemporary()) {
$element['#files'][$fid]->delete();
Angie Byron
committed
}
}
Angie Byron
committed
// Update both $form_state->getValues() and FormState::$input to reflect
// that the file has been removed, so that the form is rebuilt correctly.
Alex Pott
committed
// $form_state->getValues() must be updated in case additional submit
// handlers run, and for form building functions that run during the
// rebuild, such as when the managed_file element is part of a field widget.
Alex Pott
committed
// FormState::$input must be updated so that
// \Drupal\file\Element\ManagedFile::valueCallback() has correct information
// during the rebuild.
Alex Pott
committed
$form_state->setValueForElement($element['fids'], implode(' ', $fids));
Angie Byron
committed
NestedArray::setValue($form_state->getUserInput(), $element['fids']['#parents'], implode(' ', $fids));
}
// Set the form to rebuild so that $form is correctly updated in response to
// processing the file removal. Since this function did not change $form_state
// if the upload button was clicked, a rebuild isn't necessary in that
Angie Byron
committed
// situation and calling $form_state->disableRedirect() would suffice.
// However, we choose to always rebuild, to keep the form processing workflow
// consistent between the two buttons.
Angie Byron
committed
$form_state->setRebuild();
Dries Buytaert
committed
}
/**
* Saves any files that have been uploaded into a managed_file element.
Dries Buytaert
committed
*
Alex Pott
committed
* @param array $element
Dries Buytaert
committed
* The FAPI element whose values are being saved.
Dries Buytaert
committed
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @return array|false
Angie Byron
committed
* An array of file entities for each file that was saved, keyed by its file
* ID. Each array element contains a file entity. Function returns FALSE if
* upload directory could not be created or no files were uploaded.
Dries Buytaert
committed
*/
Dries Buytaert
committed
function file_managed_file_save_upload($element, FormStateInterface $form_state) {
Dries Buytaert
committed
$upload_name = implode('_', $element['#parents']);
$all_files = \Drupal::request()->files->get('files', []);
if (empty($all_files[$upload_name])) {
Dries Buytaert
committed
return FALSE;
}
$file_upload = $all_files[$upload_name];
Dries Buytaert
committed
$destination = $element['#upload_location'] ?? NULL;
if (isset($destination) && !\Drupal::service('file_system')->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY)) {
\Drupal::logger('file')->notice('The upload directory %directory for the file field %name could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', ['%directory' => $destination, '%name' => $element['#field_name']]);
$form_state->setError($element, t('The file could not be uploaded.'));
Dries Buytaert
committed
return FALSE;
}
Angie Byron
committed
// Save attached files to the database.
Angie Byron
committed
$files_uploaded = $element['#multiple'] && count(array_filter($file_upload)) > 0;
$files_uploaded |= !$element['#multiple'] && !empty($file_upload);
Angie Byron
committed
if ($files_uploaded) {
if (!$files = _file_save_upload_from_form($element, $form_state)) {
\Drupal::logger('file')->notice('The file upload failed. %upload', ['%upload' => $upload_name]);
return [];
Angie Byron
committed
}
// Value callback expects FIDs to be keys.
$files = array_filter($files);
$fids = array_map(function ($file) {
return $file->id();
}, $files);
Angie Byron
committed
return empty($files) ? [] : array_combine($fids, $files);
Dries Buytaert
committed
}
return [];
Dries Buytaert
committed
}
/**
Angie Byron
committed
* Prepares variables for file form widget templates.
*
* Default template: file-managed-file.html.twig.
*
Angie Byron
committed
* @param array $variables
* An associative array containing:
* - element: A render element representing the file.
Dries Buytaert
committed
*/
Angie Byron
committed
function template_preprocess_file_managed_file(&$variables) {
Dries Buytaert
committed
$element = $variables['element'];
$variables['attributes'] = [];
if (isset($element['#id'])) {
Angie Byron
committed
$variables['attributes']['id'] = $element['#id'];
}
if (!empty($element['#attributes']['class'])) {
Angie Byron
committed
$variables['attributes']['class'] = (array) $element['#attributes']['class'];
}
Dries Buytaert
committed
}
/**
Angie Byron
committed
* Prepares variables for file link templates.
*
* Default template: file-link.html.twig.
Dries Buytaert
committed
*
Angie Byron
committed
* @param array $variables
Dries Buytaert
committed
* An associative array containing:
Alex Pott
committed
* - file: A File entity to which the link will be created.
* - icon_directory: (optional) A path to a directory of icons to be used for
Angie Byron
committed
* files. Defaults to the value of the "icon.directory" variable.
catch
committed
* - description: A description to be displayed instead of the filename.
* - attributes: An associative array of attributes to be placed in the a tag.
Dries Buytaert
committed
*/
Angie Byron
committed
function template_preprocess_file_link(&$variables) {
Dries Buytaert
committed
$file = $variables['file'];
$options = [];
Angie Byron
committed
/** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
$file_url_generator = \Drupal::service('file_url_generator');
$url = $file_url_generator->generate($file->getFileUri());
Angie Byron
committed
$mime_type = $file->getMimeType();
$options['attributes']['type'] = $mime_type;
Dries Buytaert
committed
// Use the description as the link text if available.
catch
committed
if (empty($variables['description'])) {
Alex Pott
committed
$link_text = $file->getFilename();
Dries Buytaert
committed
}
else {
catch
committed
$link_text = $variables['description'];
Alex Pott
committed
$options['attributes']['title'] = $file->getFilename();
Dries Buytaert
committed
}
// Classes to add to the file field for icons.
$classes = [
'file',
// Add a specific class for each and every mime type.
'file--mime-' . strtr($mime_type, ['/' => '-', '.' => '-']),
// Add a more general class for groups of well known MIME types.
Dave Long
committed
'file--' . IconMimeTypes::getIconClass($mime_type),
];
// Set file classes to the options array.
$variables['attributes'] = new Attribute($variables['attributes']);
$variables['attributes']->addClass($classes);
$variables['file_size'] = $file->getSize() !== NULL ? ByteSizeMarkup::create($file->getSize()) : '';
$variables['link'] = Link::fromTextAndUrl($link_text, $url->mergeOptions($options))->toRenderable();
Dries Buytaert
committed
}
catch
committed
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
/**
* Prepares variables for multi file form widget templates.
*
* Default template: file-widget-multiple.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: A render element representing the widgets.
*/
function template_preprocess_file_widget_multiple(&$variables) {
$element = $variables['element'];
// Special ID and classes for draggable tables.
$weight_class = $element['#id'] . '-weight';
$table_id = $element['#id'] . '-table';
// Build up a table of applicable fields.
$headers = [];
$headers[] = t('File information');
if ($element['#display_field']) {
$headers[] = [
'data' => t('Display'),
'class' => ['checkbox'],
];
}
$headers[] = t('Weight');
$headers[] = t('Operations');
// Get our list of widgets in order (needed when the form comes back after
// preview or failed validation).
$widgets = [];
foreach (Element::children($element) as $key) {
$widgets[] = &$element[$key];
}
usort($widgets, '_field_multiple_value_form_sort_helper');
$rows = [];
foreach ($widgets as &$widget) {
// Save the uploading row for last.
if (empty($widget['#files'])) {
$widget['#title'] = $element['#file_upload_title'];
$widget['#description'] = \Drupal::service('renderer')->renderInIsolation($element['#file_upload_description']);
catch
committed
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
continue;
}
// Delay rendering of the buttons, so that they can be rendered later in the
// "operations" column.
$operations_elements = [];
foreach (Element::children($widget) as $key) {
if (isset($widget[$key]['#type']) && $widget[$key]['#type'] == 'submit') {
hide($widget[$key]);
$operations_elements[] = &$widget[$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']);
$widget['_weight']['#attributes']['class'] = [$weight_class];
// Render everything else together in a column, without the normal wrappers.
$row = [];
$widget['#theme_wrappers'] = [];
$row[] = \Drupal::service('renderer')->render($widget);
// Arrange the row with the rest of the rendered columns.
if ($element['#display_field']) {
unset($widget['display']['#title']);
$row[] = [
'data' => $widget['display'],
'class' => ['checkbox'],
];
}
$row[] = [
'data' => $widget['_weight'],
];
// Show the buttons that had previously been marked as hidden in this
// preprocess function. We use show() to undo the earlier hide().
foreach (Element::children($operations_elements) as $key) {
show($operations_elements[$key]);
}
$row[] = [
'data' => $operations_elements,
];
$rows[] = [
'data' => $row,
'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], ['draggable']) : ['draggable'],
];
}
$variables['table'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#attributes' => [
'id' => $table_id,
],
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => $weight_class,
],
],
'#access' => !empty($rows),
];
$variables['element'] = $element;
}
/**
* Prepares variables for file upload help text templates.
*
* Default template: file-upload-help.html.twig.
*
* @param array $variables
* An associative array containing:
* - description: The normal description for this field, specified by the
* user.
* - upload_validators: An array of upload validators as used in
* $element['#upload_validators'].
*/
function template_preprocess_file_upload_help(&$variables) {
$description = $variables['description'];
$upload_validators = $variables['upload_validators'];
$cardinality = $variables['cardinality'];
$descriptions = [];
if (!empty($description)) {
$descriptions[] = FieldFilteredMarkup::create($description);
}
if (isset($cardinality)) {
if ($cardinality == -1) {
$descriptions[] = t('Unlimited number of files can be uploaded to this field.');
}
else {
$descriptions[] = \Drupal::translation()->formatPlural($cardinality, 'One file only.', 'Maximum @count files.');
}
}
catch
committed
if (isset($upload_validators['FileSizeLimit'])) {
$descriptions[] = t('@size limit.', ['@size' => ByteSizeMarkup::create($upload_validators['FileSizeLimit']['fileLimit'])]);
catch
committed
}
if (isset($upload_validators['FileExtension'])) {
$descriptions[] = t('Allowed types: @extensions.', ['@extensions' => $upload_validators['FileExtension']['extensions']]);
}
if (isset($upload_validators['FileImageDimensions'])) {
$max = $upload_validators['FileImageDimensions']['maxDimensions'];
$min = $upload_validators['FileImageDimensions']['minDimensions'];
catch
committed
if ($min && $max && $min == $max) {
$descriptions[] = t('Images must be exactly <strong>@size</strong> pixels.', ['@size' => $max]);
}
elseif ($min && $max) {
$descriptions[] = t('Images must be larger than <strong>@min</strong> pixels. Images larger than <strong>@max</strong> pixels will be resized.', [
'@min' => $min,
'@max' => $max,
]);
}
elseif ($min) {
$descriptions[] = t('Images must be larger than <strong>@min</strong> pixels.', ['@min' => $min]);
}
elseif ($max) {
$descriptions[] = t('Images larger than <strong>@max</strong> pixels will be resized.', ['@max' => $max]);
}
}
$variables['descriptions'] = $descriptions;
}
Dries Buytaert
committed
/**
* Retrieves a list of references to a file.
Dries Buytaert
committed
*
Angie Byron
committed
* @param \Drupal\file\FileInterface $file
Dries Buytaert
committed
* A file entity.
* @param \Drupal\Core\Field\FieldDefinitionInterface|null $field
* (optional) A field definition to be used for this check. If given,
* limits the reference check to the given field. Defaults to NULL.
Alex Pott
committed
* @param int $age
Dries Buytaert
committed
* (optional) A constant that specifies which references to count. Use
* EntityStorageInterface::FIELD_LOAD_REVISION (the default) to retrieve all
* references within all revisions or
* EntityStorageInterface::FIELD_LOAD_CURRENT to retrieve references only in
* the current revisions of all entities that have references to this file.
Alex Pott
committed
* @param string $field_type
Dries Buytaert
committed
* (optional) The name of a field type. If given, limits the reference check
* to fields of the given type. If both $field and $field_type are given but
* $field is not the same type as $field_type, an empty array will be
* returned. Defaults to 'file'.
Dries Buytaert
committed
*
Alex Pott
committed
* @return array
* A multidimensional array. The keys are field_name, entity_type,
* entity_id and the value is an entity referencing this file.
*
* @ingroup file
Dries Buytaert
committed
*/
Angie Byron
committed
function file_get_file_references(FileInterface $file, FieldDefinitionInterface $field = NULL, $age = EntityStorageInterface::FIELD_LOAD_REVISION, $field_type = 'file') {
$references = &drupal_static(__FUNCTION__, []);
$field_columns = &drupal_static(__FUNCTION__ . ':field_columns', []);
// Fill the static cache, disregard $field and $field_type for now.
if (!isset($references[$file->id()][$age])) {
$references[$file->id()][$age] = [];
$usage_list = \Drupal::service('file.usage')->listUsage($file);
$file_usage_list = $usage_list['file'] ?? [];
Alex Pott
committed
foreach ($file_usage_list as $entity_type_id => $entity_ids) {
catch
committed
$entities = \Drupal::entityTypeManager()
->getStorage($entity_type_id)->loadMultiple(array_keys($entity_ids));
foreach ($entities as $entity) {
$bundle = $entity->bundle();
// We need to find file fields for this entity type and bundle.