Skip to content
Snippets Groups Projects
Verified Commit 96b4670a authored by Lee Rowlands's avatar Lee Rowlands
Browse files

Issue #3493951 by oily, kim.pepper, nicxvan: Split File oop hook...

Issue #3493951 by oily, kim.pepper, nicxvan: Split File oop hook implementations into separate classes
parent db573665
No related branches found
No related tags found
7 merge requests!11197Issue #3506427 by eduardo morales alberti: Remove responsive_image.ajax from hook,!11131[10.4.x-only-DO-NOT-MERGE]: Issue ##2842525 Ajax attached to Views exposed filter form does not trigger callbacks,!10786Issue #3490579 by shalini_jha, mstrelan: Add void return to all views...,!3878Removed unused condition head title for views,!3818Issue #2140179: $entity->original gets stale between updates,!2062Issue #3246454: Add weekly granularity to views date sort,!617Issue #3043725: Provide a Entity Handler for user cancelation
Pipeline #373242 passed with warnings
......@@ -19366,30 +19366,6 @@
'count' => 1,
'path' => __DIR__ . '/modules/file/src/FileUsage/FileUsageInterface.php',
];
$ignoreErrors[] = [
// identifier: missingType.return
'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileHooks\\:\\:fileDownload\\(\\) has no return type specified\\.$#',
'count' => 1,
'path' => __DIR__ . '/modules/file/src/Hook/FileHooks.php',
];
$ignoreErrors[] = [
// identifier: missingType.return
'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileHooks\\:\\:filePredelete\\(\\) has no return type specified\\.$#',
'count' => 1,
'path' => __DIR__ . '/modules/file/src/Hook/FileHooks.php',
];
$ignoreErrors[] = [
// identifier: missingType.return
'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileHooks\\:\\:help\\(\\) has no return type specified\\.$#',
'count' => 1,
'path' => __DIR__ . '/modules/file/src/Hook/FileHooks.php',
];
$ignoreErrors[] = [
// identifier: missingType.return
'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileHooks\\:\\:tokenInfo\\(\\) has no return type specified\\.$#',
'count' => 1,
'path' => __DIR__ . '/modules/file/src/Hook/FileHooks.php',
];
$ignoreErrors[] = [
// identifier: empty.variable
'message' => '#^Variable \\$rows in empty\\(\\) always exists and is not falsy\\.$#',
......@@ -9,6 +9,7 @@ services:
arguments: ['@config.factory', '@database', 'file_usage']
tags:
- { name: backend_overridable }
Drupal\file\FileUsage\FileUsageInterface: '@file.usage'
file.upload_handler:
class: Drupal\file\Upload\FileUploadHandler
arguments: ['@file_system', '@entity_type.manager', '@stream_wrapper_manager', '@event_dispatcher', '@file.mime_type.guesser', '@current_user', '@request_stack', '@file.repository', '@file.validator', '@lock', '@validation.basic_recursive_validator_factory']
......
<?php
declare(strict_types=1);
namespace Drupal\file\Hook;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\file\FileInterface;
use Drupal\file\FileUsage\FileUsageInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* Implements hook_cron().
*/
#[Hook('cron')]
class CronHook {
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly StreamWrapperManagerInterface $streamWrapperManager,
private readonly ConfigFactoryInterface $configFactory,
private readonly FileUsageInterface $fileUsage,
private readonly TimeInterface $time,
#[Autowire('@logger.channel.file')]
private readonly LoggerInterface $logger,
) {}
/**
* Implements hook_cron().
*/
public function __invoke(): void {
$age = $this->configFactory->get('system.file')->get('temporary_maximum_age');
$fileStorage = $this->entityTypeManager->getStorage('file');
// Only delete temporary files if older than $age. Note that automatic cleanup
// is disabled if $age set to 0.
if ($age) {
$fids = $fileStorage->getQuery()->accessCheck(FALSE)->condition('status', FileInterface::STATUS_PERMANENT, '<>')->condition('changed', $this->time->getRequestTime() - $age, '<')->range(0, 100)->execute();
/** @var \Drupal\file\FileInterface[] $files */
$files = $fileStorage->loadMultiple($fids);
foreach ($files as $file) {
$references = $this->fileUsage->listUsage($file);
if (empty($references)) {
if (!file_exists($file->getFileUri())) {
if (!$this->streamWrapperManager->isValidUri($file->getFileUri())) {
$this->logger->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 {
$this->logger->warning('Temporary file "%path" that was deleted during garbage collection did not exist on the filesystem.', ['%path' => $file->getFileUri()]);
}
}
// Delete the file entity. If the file does not exist, this will
// generate a second notice in the watchdog.
$file->delete();
}
else {
$this->logger->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)),
]);
}
}
}
}
}
<?php
declare(strict_types=1);
namespace Drupal\file\Hook;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Session\AccountInterface;
use Drupal\file\FileRepositoryInterface;
use Drupal\file\FileUsage\FileUsageInterface;
/**
* Implements hook_file_download().
*/
#[Hook('file_download')]
class FileDownloadHook {
public function __construct(
private readonly FileRepositoryInterface $fileRepository,
private readonly FileUsageInterface $fileUsage,
private readonly AccountInterface $currentUser,
) {}
/**
* Implements hook_file_download().
*/
public function __invoke($uri): array|int|null {
// Get the file record based on the URI. If not in the database just return.
$file = $this->fileRepository->loadByUri($uri);
if (!$file) {
return NULL;
}
// Find out if a temporary file is still used in the system.
if ($file->isTemporary()) {
$usage = $this->fileUsage->listUsage($file);
if (empty($usage) && $file->getOwnerId() != $this->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.
$references = file_get_file_references($file, NULL, EntityStorageInterface::FIELD_LOAD_CURRENT, NULL);
// 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.
if (empty($references) && ($file->isPermanent() || $file->getOwnerId() != $this->currentUser->id())) {
return NULL;
}
if (!$file->access('download')) {
return -1;
}
// Access is granted.
return file_get_content_headers($file);
}
}
......@@ -3,12 +3,7 @@
namespace Drupal\file\Hook;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\Core\StringTranslation\ByteSizeMarkup;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Url;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Hook\Attribute\Hook;
......@@ -17,13 +12,14 @@
* Hook implementations for file.
*/
class FileHooks {
// cspell:ignore widthx
/**
* Implements hook_help().
*/
#[Hook('help')]
public function help($route_name, RouteMatchInterface $route_match) {
public function help($route_name, RouteMatchInterface $route_match): string|array|null {
switch ($route_name) {
case 'help.page.file':
$output = '';
......@@ -61,6 +57,7 @@ public function help($route_name, RouteMatchInterface $route_match) {
$output .= '</dl>';
return $output;
}
return NULL;
}
/**
......@@ -78,9 +75,9 @@ public function fieldWidgetInfoAlter(array &$info): void {
* Implements hook_theme().
*/
#[Hook('theme')]
public function theme() : array {
public function theme(): array {
return [
// From file.module.
// From file.module.
'file_link' => [
'variables' => [
'file' => NULL,
......@@ -116,225 +113,12 @@ public function theme() : array {
];
}
/**
* Implements hook_file_download().
*/
#[Hook('file_download')]
public function fileDownload($uri) {
// Get the file record based on the URI. If not in the database just return.
/** @var \Drupal\file\FileRepositoryInterface $file_repository */
$file_repository = \Drupal::service('file.repository');
$file = $file_repository->loadByUri($uri);
if (!$file) {
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.
$references = file_get_file_references($file, NULL, EntityStorageInterface::FIELD_LOAD_CURRENT, NULL);
// 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.
if (empty($references) && ($file->isPermanent() || $file->getOwnerId() != \Drupal::currentUser()->id())) {
return;
}
if (!$file->access('download')) {
return -1;
}
// Access is granted.
$headers = file_get_content_headers($file);
return $headers;
}
/**
* Implements hook_cron().
*/
#[Hook('cron')]
public function cron(): void {
$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, '<>')->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);
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()]);
}
}
// Delete the file entity. If the file does not exist, this will
// generate a second notice in the watchdog.
$file->delete();
}
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)),
]);
}
}
}
}
/**
* Implements hook_ENTITY_TYPE_predelete() for file entities.
*/
#[Hook('file_predelete')]
public function filePredelete(File $file) {
// @todo Remove references to a file that is in-use.
}
/**
* Implements hook_tokens().
*/
#[Hook('tokens')]
public function tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata): array {
$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;
case 'uuid':
$replacements[$original] = $file->uuid();
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().
*/
#[Hook('token_info')]
public function tokenInfo() {
$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['uuid'] = ['name' => t('UUID'), 'description' => t('The UUID 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]];
public function filePredelete(File $file): void {
// @todo Remove references to a file that is in-use. See https://www.drupal.org/project/drupal/issues/1506314
}
/**
......@@ -350,7 +134,7 @@ public function tokenInfo() {
* @see \Drupal\file\EventSubscriber\FileEventSubscriber
*/
#[Hook('form_system_file_system_settings_alter')]
public function formSystemFileSystemSettingsAlter(array &$form, FormStateInterface $form_state) : void {
public function formSystemFileSystemSettingsAlter(array &$form, FormStateInterface $form_state): void {
$config = \Drupal::config('file.settings');
$form['filename_sanitization'] = [
'#type' => 'details',
......
<?php
declare(strict_types=1);
namespace Drupal\file\Hook;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\Core\Hook\Attribute\Hook;
......@@ -10,6 +13,10 @@
*/
class FileViewsHooks {
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* Implements hook_field_views_data().
*
......@@ -44,12 +51,11 @@ public function fieldViewsData(FieldStorageConfigInterface $field_storage): arra
#[Hook('field_views_data_views_data_alter')]
public function fieldViewsDataViewsDataAlter(array &$data, FieldStorageConfigInterface $field_storage): void {
$entity_type_id = $field_storage->getTargetEntityTypeId();
$entity_type_manager = \Drupal::entityTypeManager();
$entity_type = $entity_type_manager->getDefinition($entity_type_id);
$entity_type = $this->entityTypeManager->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_type_manager->getStorage($entity_type_id)->getTableMapping();
$table_mapping = $this->entityTypeManager->getStorage($entity_type_id)->getTableMapping();
[$label] = views_entity_field_label($entity_type_id, $field_name);
$data['file_managed'][$pseudo_field_name]['relationship'] = [
'title' => t('@entity using @field', [
......
<?php
declare(strict_types=1);
namespace Drupal\file\Hook;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\StringTranslation\ByteSizeMarkup;
use Drupal\Core\Utility\Token;
/**
* Hook implementations for file tokens.
*/
class TokenHooks {
public function __construct(
private readonly Token $token,
private readonly LanguageManagerInterface $languageManager,
private readonly DateFormatterInterface $dateFormatter,
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* Implements hook_tokens().
*/
#[Hook('tokens')]
public function tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata): array {
$url_options = ['absolute' => TRUE];
if (isset($options['langcode'])) {
$url_options['language'] = $this->languageManager->getLanguage($options['langcode']);
$langcode = $options['langcode'];
}
else {
$langcode = NULL;
}
$replacements = [];
if ($type == 'file' && !empty($data['file'])) {
$dateFormatStorage = $this->entityTypeManager->getStorage('date_format');
/** @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;
case 'uuid':
$replacements[$original] = $file->uuid();
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 = $dateFormatStorage->load('medium');
$bubbleable_metadata->addCacheableDependency($date_format);
$replacements[$original] = $this->dateFormatter->format($file->getCreatedTime(), 'medium', '', NULL, $langcode);
break;
case 'changed':
$date_format = $dateFormatStorage->load('medium');
$bubbleable_metadata = $bubbleable_metadata->addCacheableDependency($date_format);
$replacements[$original] = $this->dateFormatter->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 = $this->token->findWithPrefix($tokens, 'created')) {
$replacements += $this->token->generate('date', $date_tokens, ['date' => $file->getCreatedTime()], $options, $bubbleable_metadata);
}
if ($date_tokens = $this->token->findWithPrefix($tokens, 'changed')) {
$replacements += $this->token->generate('date', $date_tokens, ['date' => $file->getChangedTime()], $options, $bubbleable_metadata);
}
if (($owner_tokens = $this->token->findWithPrefix($tokens, 'owner')) && $file->getOwner()) {
$replacements += $this->token->generate('user', $owner_tokens, ['user' => $file->getOwner()], $options, $bubbleable_metadata);
}
}
return $replacements;
}
/**
* Implements hook_token_info().
*/
#[Hook('token_info')]
public function tokenInfo(): array {
$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['uuid'] = ['name' => t('UUID'), 'description' => t('The UUID 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]];
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment