Loading modules/core_event_dispatcher/core_event_dispatcher.module +12 −0 Original line number Diff line number Diff line Loading @@ -61,6 +61,7 @@ use Drupal\core_event_dispatcher\Event\Entity\EntityUpdateEvent; use Drupal\core_event_dispatcher\Event\Entity\EntityViewAlterEvent; use Drupal\core_event_dispatcher\Event\Entity\EntityViewEvent; use Drupal\core_event_dispatcher\Event\File\ArchiverInfoAlterEvent; use Drupal\core_event_dispatcher\Event\File\FileDownloadEvent; use Drupal\core_event_dispatcher\Event\File\FileMimetypeMappingAlterEvent; use Drupal\core_event_dispatcher\Event\File\FileTransferInfoAlterEvent; use Drupal\core_event_dispatcher\Event\File\FileTransferInfoEvent; Loading Loading @@ -584,6 +585,17 @@ function core_event_dispatcher_entity_extra_field_info_alter(array &$info) { $manager->register(new EntityExtraFieldInfoAlterEvent($info)); } /** * Implements hook_file_download(). */ function core_event_dispatcher_file_download($uri) { /** @var \Drupal\hook_event_dispatcher\Manager\HookEventDispatcherManagerInterface $manager */ $manager = Drupal::service('hook_event_dispatcher.manager'); $event = new FileDownloadEvent($uri); $manager->register($event); return $event->isForbidden() ? -1 : $event->getHeaders(); } /** * Implements hook_file_mimetype_mapping_alter(). */ Loading modules/core_event_dispatcher/src/Event/File/FileDownloadEvent.php 0 → 100644 +88 −0 Original line number Diff line number Diff line <?php namespace Drupal\core_event_dispatcher\Event\File; use Drupal\Component\EventDispatcher\Event; use Drupal\hook_event_dispatcher\Event\EventInterface; use Drupal\hook_event_dispatcher\HookEventDispatcherInterface; /** * Class FileDownloadEvent. */ class FileDownloadEvent extends Event implements EventInterface { /** * Forbids the download if set to TRUE. * * @var bool */ protected $forbidden = FALSE; /** * Response headers that will be set for the downloaded file. * * @var array */ protected $headers; /** * The URI of the file. * * @var string */ protected $uri; /** * FileDownloadEvent constructor. */ public function __construct(string $uri) { $this->uri = $uri; } /** * Checks if the download is forbidden. * * @return bool * TRUE if the download is forbidden. */ public function isForbidden(): bool { return $this->forbidden; } /** * Sets the download as forbidden. */ public function setForbidden(): void { $this->forbidden = TRUE; } /** * Gets the response headers. * * @return array * The response headers. */ public function getHeaders(): ?array { return $this->headers; } /** * Sets the header. * * @param string $name * The header name. * @param mixed $value * The header value. */ public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } /** * {@inheritdoc} */ public function getDispatcherType(): string { return HookEventDispatcherInterface::FILE_DOWNLOAD; } } modules/core_event_dispatcher/tests/src/Kernel/File/FileDownloadEventTest.php 0 → 100644 +123 −0 Original line number Diff line number Diff line <?php namespace Drupal\Tests\core_event_dispatcher\Kernel\File; use Drupal\core_event_dispatcher\Event\File\FileDownloadEvent; use Drupal\hook_event_dispatcher\HookEventDispatcherInterface; use Drupal\KernelTests\KernelTestBase; use Drupal\system\FileDownloadController; use Drupal\Tests\hook_event_dispatcher\Kernel\ListenerTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * Class FileDownloadEvent. * * @group hook_event_dispatcher * @group core_event_dispatcher * * @see core_event_dispatcher_file_download() */ class FileDownloadEventTest extends KernelTestBase { use ListenerTrait; /** * {@inheritdoc} */ protected static $modules = [ 'file_test', 'hook_event_dispatcher', 'core_event_dispatcher', ]; /** * The file download controller. * * @var \Drupal\system\FileDownloadController */ protected $controller; /** * {@inheritdoc} * * @throws \Exception */ protected function setUp(): void { parent::setUp(); $this->controller = new FileDownloadController($this->container->get('stream_wrapper_manager')); } /** * Test FileDownloadEvent without subscribers. * * @throws \Exception */ public function testFileDownloadEventEmpty(): void { $this->expectException(AccessDeniedHttpException::class); $this->controller->download(Request::create('/dummy/example.txt', 'GET', ['file' => $this->generateTestFile()]), 'dummy-readonly'); } /** * Test FileDownloadEvent with forbidden access. * * @throws \Exception */ public function testFileDownloadEventForbidden(): void { $this->listen(HookEventDispatcherInterface::FILE_DOWNLOAD, 'onFileDownloadForbidden'); $this->expectException(AccessDeniedHttpException::class); $this->controller->download(Request::create('/dummy/example.txt', 'GET', ['file' => $this->generateTestFile()]), 'dummy-readonly'); } /** * Callback for FileDownloadEventForbidden. * * @param \Drupal\core_event_dispatcher\Event\File\FileDownloadEvent $event * The event. */ public function onFileDownloadForbidden(FileDownloadEvent $event): void { $event->setForbidden(); } /** * Test FileDownloadEvent. * * @throws \Exception */ public function testFileDownloadEvent(): void { $this->listen(HookEventDispatcherInterface::FILE_DOWNLOAD, 'onFileDownload'); $filename = $this->generateTestFile(); $response = $this->controller->download(Request::create('/dummy/example.txt', 'GET', ['file' => $filename]), 'dummy-readonly'); $this->assertTrue($response->headers->has('x-foo')); $this->assertEquals('Bar', $response->headers->get('x-foo')); $this->assertEquals($filename, $response->getFile()->getFilename()); } /** * Callback for FileDownloadEvent. * * @param \Drupal\core_event_dispatcher\Event\File\FileDownloadEvent $event * The event. */ public function onFileDownload(FileDownloadEvent $event): void { $event->setHeader('x-foo', 'Bar'); } /** * Generate a test file. * * @return string * The filename of the test file. */ protected function generateTestFile(): string { $filename = $this->randomMachineName(); $sitePath = $this->container->getParameter('site.path'); $filepath = $sitePath . '/files/' . $filename; file_put_contents($filepath, $filename); return $filename; } } src/HookEventDispatcherInterface.php +12 −0 Original line number Diff line number Diff line Loading @@ -422,6 +422,18 @@ interface HookEventDispatcherInterface { public const FIELD_WIDGET_THIRD_PARTY_SETTINGS_FORM = self::PREFIX . 'field_widget.third_party.settings_form'; // File EVENTS. /** * Control access to private file downloads and specify HTTP headers. * * @Event * * @see core_event_dispatcher_file_download() * @see hook_file_download() * * @var string */ public const FILE_DOWNLOAD = self::PREFIX . 'file.download'; /** * Alter MIME type mappings used to determine MIME type from a file extension. * Loading Loading
modules/core_event_dispatcher/core_event_dispatcher.module +12 −0 Original line number Diff line number Diff line Loading @@ -61,6 +61,7 @@ use Drupal\core_event_dispatcher\Event\Entity\EntityUpdateEvent; use Drupal\core_event_dispatcher\Event\Entity\EntityViewAlterEvent; use Drupal\core_event_dispatcher\Event\Entity\EntityViewEvent; use Drupal\core_event_dispatcher\Event\File\ArchiverInfoAlterEvent; use Drupal\core_event_dispatcher\Event\File\FileDownloadEvent; use Drupal\core_event_dispatcher\Event\File\FileMimetypeMappingAlterEvent; use Drupal\core_event_dispatcher\Event\File\FileTransferInfoAlterEvent; use Drupal\core_event_dispatcher\Event\File\FileTransferInfoEvent; Loading Loading @@ -584,6 +585,17 @@ function core_event_dispatcher_entity_extra_field_info_alter(array &$info) { $manager->register(new EntityExtraFieldInfoAlterEvent($info)); } /** * Implements hook_file_download(). */ function core_event_dispatcher_file_download($uri) { /** @var \Drupal\hook_event_dispatcher\Manager\HookEventDispatcherManagerInterface $manager */ $manager = Drupal::service('hook_event_dispatcher.manager'); $event = new FileDownloadEvent($uri); $manager->register($event); return $event->isForbidden() ? -1 : $event->getHeaders(); } /** * Implements hook_file_mimetype_mapping_alter(). */ Loading
modules/core_event_dispatcher/src/Event/File/FileDownloadEvent.php 0 → 100644 +88 −0 Original line number Diff line number Diff line <?php namespace Drupal\core_event_dispatcher\Event\File; use Drupal\Component\EventDispatcher\Event; use Drupal\hook_event_dispatcher\Event\EventInterface; use Drupal\hook_event_dispatcher\HookEventDispatcherInterface; /** * Class FileDownloadEvent. */ class FileDownloadEvent extends Event implements EventInterface { /** * Forbids the download if set to TRUE. * * @var bool */ protected $forbidden = FALSE; /** * Response headers that will be set for the downloaded file. * * @var array */ protected $headers; /** * The URI of the file. * * @var string */ protected $uri; /** * FileDownloadEvent constructor. */ public function __construct(string $uri) { $this->uri = $uri; } /** * Checks if the download is forbidden. * * @return bool * TRUE if the download is forbidden. */ public function isForbidden(): bool { return $this->forbidden; } /** * Sets the download as forbidden. */ public function setForbidden(): void { $this->forbidden = TRUE; } /** * Gets the response headers. * * @return array * The response headers. */ public function getHeaders(): ?array { return $this->headers; } /** * Sets the header. * * @param string $name * The header name. * @param mixed $value * The header value. */ public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } /** * {@inheritdoc} */ public function getDispatcherType(): string { return HookEventDispatcherInterface::FILE_DOWNLOAD; } }
modules/core_event_dispatcher/tests/src/Kernel/File/FileDownloadEventTest.php 0 → 100644 +123 −0 Original line number Diff line number Diff line <?php namespace Drupal\Tests\core_event_dispatcher\Kernel\File; use Drupal\core_event_dispatcher\Event\File\FileDownloadEvent; use Drupal\hook_event_dispatcher\HookEventDispatcherInterface; use Drupal\KernelTests\KernelTestBase; use Drupal\system\FileDownloadController; use Drupal\Tests\hook_event_dispatcher\Kernel\ListenerTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * Class FileDownloadEvent. * * @group hook_event_dispatcher * @group core_event_dispatcher * * @see core_event_dispatcher_file_download() */ class FileDownloadEventTest extends KernelTestBase { use ListenerTrait; /** * {@inheritdoc} */ protected static $modules = [ 'file_test', 'hook_event_dispatcher', 'core_event_dispatcher', ]; /** * The file download controller. * * @var \Drupal\system\FileDownloadController */ protected $controller; /** * {@inheritdoc} * * @throws \Exception */ protected function setUp(): void { parent::setUp(); $this->controller = new FileDownloadController($this->container->get('stream_wrapper_manager')); } /** * Test FileDownloadEvent without subscribers. * * @throws \Exception */ public function testFileDownloadEventEmpty(): void { $this->expectException(AccessDeniedHttpException::class); $this->controller->download(Request::create('/dummy/example.txt', 'GET', ['file' => $this->generateTestFile()]), 'dummy-readonly'); } /** * Test FileDownloadEvent with forbidden access. * * @throws \Exception */ public function testFileDownloadEventForbidden(): void { $this->listen(HookEventDispatcherInterface::FILE_DOWNLOAD, 'onFileDownloadForbidden'); $this->expectException(AccessDeniedHttpException::class); $this->controller->download(Request::create('/dummy/example.txt', 'GET', ['file' => $this->generateTestFile()]), 'dummy-readonly'); } /** * Callback for FileDownloadEventForbidden. * * @param \Drupal\core_event_dispatcher\Event\File\FileDownloadEvent $event * The event. */ public function onFileDownloadForbidden(FileDownloadEvent $event): void { $event->setForbidden(); } /** * Test FileDownloadEvent. * * @throws \Exception */ public function testFileDownloadEvent(): void { $this->listen(HookEventDispatcherInterface::FILE_DOWNLOAD, 'onFileDownload'); $filename = $this->generateTestFile(); $response = $this->controller->download(Request::create('/dummy/example.txt', 'GET', ['file' => $filename]), 'dummy-readonly'); $this->assertTrue($response->headers->has('x-foo')); $this->assertEquals('Bar', $response->headers->get('x-foo')); $this->assertEquals($filename, $response->getFile()->getFilename()); } /** * Callback for FileDownloadEvent. * * @param \Drupal\core_event_dispatcher\Event\File\FileDownloadEvent $event * The event. */ public function onFileDownload(FileDownloadEvent $event): void { $event->setHeader('x-foo', 'Bar'); } /** * Generate a test file. * * @return string * The filename of the test file. */ protected function generateTestFile(): string { $filename = $this->randomMachineName(); $sitePath = $this->container->getParameter('site.path'); $filepath = $sitePath . '/files/' . $filename; file_put_contents($filepath, $filename); return $filename; } }
src/HookEventDispatcherInterface.php +12 −0 Original line number Diff line number Diff line Loading @@ -422,6 +422,18 @@ interface HookEventDispatcherInterface { public const FIELD_WIDGET_THIRD_PARTY_SETTINGS_FORM = self::PREFIX . 'field_widget.third_party.settings_form'; // File EVENTS. /** * Control access to private file downloads and specify HTTP headers. * * @Event * * @see core_event_dispatcher_file_download() * @see hook_file_download() * * @var string */ public const FILE_DOWNLOAD = self::PREFIX . 'file.download'; /** * Alter MIME type mappings used to determine MIME type from a file extension. * Loading