Commit 8dcae97d authored by Luhur Abdi Rizal's avatar Luhur Abdi Rizal
Browse files

Issue #3262750 by el7cosmos: Implements hook_file_download

parent dff9785a
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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().
 */
+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;
  }

}
+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;
  }

}
+12 −0
Original line number Diff line number Diff line
@@ -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.
   *