Loading README.md +6 −3 Original line number Diff line number Diff line Loading @@ -23,7 +23,9 @@ REQUIREMENTS This module requires: * PHP > 7.2CONTENTS OF THIS FILE * PHP > 7.4 CONTENTS OF THIS FILE --------------------- * Introduction Loading @@ -48,7 +50,7 @@ REQUIREMENTS This module requires: * PHP > 7.2 * PHP > 7.4 * Drupal Core Media Loading @@ -69,7 +71,8 @@ TRICKS ------ * If you're viewing a file with an alias and need help finding the media object just append ?edit-media. This will redirect you straight to the media edit page. `?edit-media` to the URL, this will redirect you straight to the media edit page. * If you want the media file to be downloaded, you may append `?download` or `?dl` to the URL. CONFIGURATION ------------- Loading media_alias_display.info.yml +0 −1 Original line number Diff line number Diff line Loading @@ -7,4 +7,3 @@ package: Media configure: media_alias_display.settings_form dependencies: - drupal:media - drupal:path_alias src/Controller/DisplayController.php +90 −89 Original line number Diff line number Diff line Loading @@ -2,25 +2,26 @@ namespace Drupal\media_alias_display\Controller; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ImmutableConfig; use Drupal\Core\Entity\Controller\EntityViewController; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\Path\CurrentPathStack; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface; use Drupal\file\Entity\File; use Drupal\path_alias\AliasManagerInterface; use Drupal\file\FileInterface; use Drupal\media\MediaInterface; use Drupal\media\Plugin\media\Source\File as FileMediaSource; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Drupal\Core\Url; use Drupal\media\Entity\Media; /** * Defines a controller to render a file with Media Alias being used. Loading @@ -35,35 +36,21 @@ class DisplayController extends EntityViewController { protected AccountInterface $currentUser; /** * The logger factory. * The media alias display logger. * * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface * @var \Drupal\Core\Logger\LoggerChannelInterface */ protected LoggerChannelFactoryInterface $loggerFactory; protected LoggerChannelInterface $logger; /** * The request stack. * The current request. * * @var \Symfony\Component\HttpFoundation\Request */ protected Request $request; /** * The current path. * * @var \Drupal\Core\Path\CurrentPathStack */ protected CurrentPathStack $currentPath; /** * The path alias manager. * * @var \Drupal\path_alias\AliasManagerInterface */ protected AliasManagerInterface $aliasManager; /** * Drupal\Core\Config\ConfigManagerInterface definition. * The config factory. * * @var \Drupal\Core\Config\ConfigFactoryInterface */ Loading @@ -76,6 +63,13 @@ class DisplayController extends EntityViewController { */ protected StreamWrapperManagerInterface $streamWrapperManager; /** * The module handler. * * @var \Drupal\Core\Extension\ModuleHandlerInterface */ protected ModuleHandlerInterface $moduleHandler; /** * The controller constructor. * Loading @@ -85,37 +79,33 @@ class DisplayController extends EntityViewController { * The renderer service. * @param \Drupal\Core\Session\AccountInterface $current_user * Current user. * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory * The logger factory. * @param \Drupal\Core\Logger\LoggerChannelInterface $logger * The media alias display logger. * @param \Symfony\Component\HttpFoundation\Request $request_stack * Request stack. * @param \Drupal\Core\Path\CurrentPathStack $current_path * The current path. * @param \Drupal\path_alias\AliasManagerInterface $alias_manager * The path alias manager. * @param \Drupal\Core\Config\ConfigFactoryInterface $config * Configuration Interface. * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager * The stream wrapper manager. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, AccountInterface $current_user, LoggerChannelFactoryInterface $loggerFactory, LoggerChannelInterface $logger, Request $request_stack, CurrentPathStack $current_path, AliasManagerInterface $alias_manager, ConfigFactoryInterface $config, StreamWrapperManagerInterface $stream_wrapper_manager StreamWrapperManagerInterface $stream_wrapper_manager, ModuleHandlerInterface $module_handler ) { parent::__construct($entity_type_manager, $renderer); $this->currentUser = $current_user; $this->loggerFactory = $loggerFactory; $this->logger = $logger; $this->request = $request_stack; $this->currentPath = $current_path; $this->aliasManager = $alias_manager; $this->configManager = $config; $this->streamWrapperManager = $stream_wrapper_manager; $this->moduleHandler = $module_handler; } /** Loading @@ -126,12 +116,11 @@ class DisplayController extends EntityViewController { $container->get('entity_type.manager'), $container->get('renderer'), $container->get('current_user'), $container->get('logger.factory'), $container->get('logger.factory')->get('media_alias_display'), $container->get('request_stack')->getCurrentRequest(), $container->get('path.current'), $container->get('path_alias.manager'), $container->get('config.factory'), $container->get('stream_wrapper_manager'), $container->get('module_handler') ); } Loading @@ -139,96 +128,108 @@ class DisplayController extends EntityViewController { * {@inheritdoc} */ public function view(EntityInterface $media, $view_mode = 'full', $langcode = NULL) { assert($media instanceof MediaInterface); $config = $this->configManager->get('media_alias_display.settings'); if (!empty($config->get('kill_switch')) && $config->get('kill_switch')) { return parent::view($media, $view_mode); return $this->updateRenderCache(parent::view($media, $view_mode), $config); } $media_bundle = $media->bundle(); $allowed_bundles = $config->get('media_bundles'); if (!empty($config->get('media_bundles'))) { $allowAllBundles = TRUE; foreach ($config->get('media_bundles') as $bundle) { if ($bundle !== 0) { $allowAllBundles = FALSE; $allow_all_bundles = TRUE; foreach ($config->get('media_bundles') as $allowed_bundle) { if ($allowed_bundle !== 0) { $allow_all_bundles = FALSE; break; } } if (!$allowAllBundles && isset($allowed_bundles[$media->bundle()]) && $allowed_bundles[$media->bundle()] === 0) { return parent::view($media, $view_mode); if (!$allow_all_bundles && isset($allowed_bundles[$media_bundle]) && $allowed_bundles[$media_bundle] === 0) { return $this->updateRenderCache(parent::view($media, $view_mode), $config); } } $current_path = $this->currentPath->getPath(); $alias = $this->aliasManager->getPathByAlias($current_path); $params = Url::fromUri('internal:' . $alias)->getRouteParameters(); $entity_type = key($params); $mid = $params[$entity_type]; $media = Media::load($mid); $bundle = $media->bundle(); $edit_own = 'edit own ' . $bundle . ' media'; $edit_any = 'edit any ' . $bundle . ' media'; $edit_own = 'edit own ' . $media_bundle . ' media'; $edit_any = 'edit any ' . $media_bundle . ' media'; // Skip redirect and go straight to media object. if ($this->request->query->has('edit-media') && (($this->currentUser->hasPermission($edit_own) || $this->currentUser->hasPermission($edit_any)) || $this->currentUser->hasPermission('administer media'))) { return new RedirectResponse('/media/' . $mid . '/edit'); return new RedirectResponse($media->toUrl('edit-form')->toString()); } if (\Drupal::moduleHandler()->moduleExists('media_alias_display_field_override')) { if ( $this->moduleHandler->moduleExists('media_alias_display_field_override') && $media->hasField('field_override_mad_module') ) { $override_module = $media->get('field_override_mad_module')->value; if (isset($override_module) && $override_module) { return parent::view($media, $view_mode); return $this->updateRenderCache(parent::view($media, $view_mode), $config); } } $source = $media->getSource(); $config = $source->getConfiguration(); $field = $config['source_field']; $fid = $media->{$field}->target_id; // If media has no file item. if (!$fid) { $this->loggerFactory->get('media_alias_display') ->notice('The media item requested has no file referenced/uploaded for @path', [ '@path' => $current_path, if (!($source instanceof FileMediaSource)) { // The module only supports file media sources at the moment. Could // potentially add support for redirect to oEmbed sources. $this->logger ->notice('Media item "@media_entity_id" does not have a file media source', [ '@media_entity_id' => $media->id(), ]); return parent::view($media, $view_mode); return $this->updateRenderCache(parent::view($media, $view_mode), $config); } $file = File::load($fid); $file = $media->get($source->getConfiguration()['source_field'])->entity; // Or file entity could not be loaded. Very unlikely to happen. // If media has no file item. if (!$file) { $this->loggerFactory->get('media_alias_display') ->notice('File id could not be loaded for ' . $current_path); return parent::view($media, $view_mode); $this->logger ->notice('Media item "@media_entity_id" does not have a file entity attached', [ '@media_entity_id' => $media->id(), ]); return $this->updateRenderCache(parent::view($media, $view_mode), $config); } assert($file instanceof FileInterface); $uri = $file->getFileUri(); $scheme = $this->streamWrapperManager::getScheme($uri); // Or item does not exist on disk. if (!$this->streamWrapperManager->isValidScheme($scheme) || !file_exists($uri)) { $this->loggerFactory->get('media_alias_display') ->notice('File does not exist for @path', [ '@path' => $current_path, if (!$this->streamWrapperManager->isValidScheme($scheme) || !is_file($uri)) { $this->logger ->notice('File attached to Media item "@media_entity_id" does not exist on disk', [ '@media_entity_id' => $media->id(), ]); return parent::view($media, $view_mode); return $this->updateRenderCache(parent::view($media, $view_mode), $config); } $filename = $file->getFilename(); $response = new BinaryFileResponse($uri, Response::HTTP_OK, [], $scheme !== 'private'); // Force a direct download if a "dl" or "download" query string is present. if ($this->request->query->has('dl') || $this->request->query->has('download')) { $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $file->getFilename()); } $response = new BinaryFileResponse($uri); $response->setContentDisposition( ResponseHeaderBag::DISPOSITION_INLINE, $filename ); return $response; } return new BinaryFileResponse($uri, Response::HTTP_OK, [], $scheme !== 'private'); /** * Add appropriate cache tags to the render array. */ protected function updateRenderCache($response, ImmutableConfig $config) { if (!is_array($response)) { return $response; } CacheableMetadata::createFromRenderArray($response) ->addCacheableDependency($config) ->addCacheContexts([ 'url.query_args:dl', 'url.query_args:download', ]) ->applyTo($response); return $response; } } tests/src/Functional/MediaAliasDisplayControllerTest.php +92 −22 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ namespace Drupal\Tests\media_alias_display\Functional; use Drupal\Core\File\FileSystemInterface; use Drupal\field\Entity\FieldConfig; use Drupal\file\Entity\File; use Drupal\file\FileInterface; Loading @@ -11,6 +12,7 @@ use Drupal\Tests\media\Functional\MediaFunctionalTestBase; use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; use Drupal\Tests\TestFileCreationTrait; use Drupal\user\Entity\Role; use Symfony\Component\HttpFoundation\ResponseHeaderBag; /** * Simple test to ensure that main page loads with module enabled. Loading @@ -28,7 +30,7 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { * @var array */ protected static $modules = [ 'media_alias_display', 'media_alias_display', 'path', ]; /** Loading @@ -41,7 +43,7 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { * * @var \Drupal\file\FileInterface */ protected FileInterface $file; protected ?FileInterface $file; /** * Store Media Type. Loading Loading @@ -86,7 +88,7 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { * * @throws \Drupal\Core\Entity\EntityStorageException */ protected function createMedia($include_file = TRUE) { protected function createMedia($include_file = TRUE, $path_alias = NULL, $index = 0, $private_file = FALSE) { $media_type_id = $this->mediaType->id(); /** @var \Drupal\field\FieldConfigInterface $field */ $field = FieldConfig::load("media.$media_type_id.field_media_image"); Loading @@ -100,11 +102,26 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { 'name' => 'Custom name', 'bundle' => $media_type_id, 'status' => TRUE, 'path' => $path_alias, ]); $this->file = NULL; if ($include_file) { /** @var \Drupal\Core\File\FileSystemInterface $file_system */ $file_system = \Drupal::service('file_system'); $original_uri = $this->getTestFiles('image')[$index]->uri; // Use a separate media_alias_display directory and file for this. // This should allow us to manipulate the files without any side effects. $destination_dir = ($private_file ? 'private://' : 'public://') . 'media_alias_display/'; $file_system->prepareDirectory($destination_dir, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); $destination_uri = $destination_dir . '/' . pathinfo($original_uri, PATHINFO_BASENAME); $uri = $file_system->copy($original_uri, $destination_uri, FileSystemInterface::EXISTS_REPLACE); $file = File::create([ 'uri' => $this->getTestFiles('image')[0]->uri, // Add a custom file name that might be different to the real internal // file. 'filename' => 'custom-file-name-' . $file_system->basename($uri), 'uri' => $uri, ]); $file->save(); $this->file = $file; Loading @@ -126,16 +143,24 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { * * @throws \Drupal\Core\Entity\EntityStorageException * @throws \Behat\Mink\Exception\ExpectationException * * @dataProvider providerDisplayController */ public function testDisplayController() { public function testDisplayController($current_alias, bool $private_file) { $assert_session = $this->assertSession(); $media = $this->createMedia(); $media = $this->createMedia(TRUE, $current_alias, 0, $private_file); $media_file = $this->file; if ($current_alias) { $path_alias = ltrim($current_alias, '/'); } else { $path_alias = 'media/' . $media->id(); } // Verifies kill switch isn't enabled + verifies it's not an allowed bundle. $this->drupalGet('media/' . $media->id()); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', $this->file->getMimeType()); $assert_session->responseHeaderContains('Content-Type', $media_file->getMimeType()); // Test when the kill switch is enabled. \Drupal::configFactory() Loading @@ -153,11 +178,13 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { ->getEditable('media_alias_display.settings') ->set('kill_switch', FALSE) ->save(TRUE); $this->resetAll(); // There should be no need to explicitly flush the Drupal cache assuming // we pass in all the relevant cache tags to the original responses. /* $this->resetAll(); */ $this->drupalGet('media/' . $media->id()); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', $this->file->getMimeType()); $assert_session->responseHeaderContains('Content-Type', $media_file->getMimeType()); // Test when a single bundle is selected. \Drupal::configFactory() Loading @@ -168,15 +195,15 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { $this->drupalGet('media/' . $media->id()); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', $this->file->getMimeType()); $assert_session->responseHeaderContains('Content-Type', $media_file->getMimeType()); // Test when edit-media is placed in the URL. // Should redirect to media edit page. User should have permission. $this->drupalGet('media/' . $media->id(), [ 'query' => ['edit-media' => 1], 'query' => ['edit-media' => ''], 'absolute' => TRUE, ]); $this->assertSession()->addressEquals('media/' . $media->id() . '/edit'); $assert_session->addressEquals('media/' . $media->id() . '/edit'); // Test when user doesn't have permission. $current_roles = $this->loggedInUser->getRoles(TRUE); Loading @@ -188,7 +215,7 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { 'absolute' => TRUE, ]); // User won't be redirected because they don't have permission. $this->assertSession()->addressEquals('media/' . $media->id()); $assert_session->addressEquals($path_alias); // Grant "edit any bundle media" permission that should allow a user to // access the edit page. Loading @@ -197,7 +224,7 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { 'query' => ['edit-media' => 1], 'absolute' => TRUE, ]); $this->assertSession()->addressEquals('media/' . $media->id() . '/edit'); $assert_session->addressEquals('media/' . $media->id() . '/edit'); user_role_revoke_permissions($role->id(), ['edit any ' . $this->mediaType->id() . ' media']); // Grant "edit own bundle media" permission that should allow a user to Loading @@ -207,24 +234,67 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { 'query' => ['edit-media' => 1], 'absolute' => TRUE, ]); $this->assertSession()->addressEquals('media/' . $media->id() . '/edit'); $assert_session->addressEquals('media/' . $media->id() . '/edit'); // Test content dispositions. $this->drupalGet('media/' . $media->id()); // The content disposition header should not exist by default. $assert_session->responseHeaderDoesNotExist('Content-Disposition'); $this->drupalGet('media/' . $media->id(), ['query' => ['download' => '']]); $assert_session->responseHeaderContains('Content-Disposition', ResponseHeaderBag::DISPOSITION_ATTACHMENT); $assert_session->responseHeaderContains('Content-Disposition', $media_file->getFilename()); $this->drupalGet('media/' . $media->id(), ['query' => ['dl' => '1']]); $assert_session->responseHeaderContains('Content-Disposition', ResponseHeaderBag::DISPOSITION_ATTACHMENT); $assert_session->responseHeaderContains('Content-Disposition', $media_file->getFilename()); // Test when there is no file attached to an allowed bundle. $media_no_file = $this->createMedia(FALSE); $media_no_file_alias = $current_alias ? "$current_alias-no-file" : NULL; $media_no_file = $this->createMedia(FALSE, $media_no_file_alias, 1, $private_file); $this->drupalGet('media/' . $media_no_file->id()); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', 'text/html'); // Test disposition behaviour without files. $this->drupalGet('media/' . $media_no_file->id(), ['query' => ['download' => '']]); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', 'text/html'); $assert_session->responseHeaderDoesNotExist('Content-Disposition'); // Test when file doesn't exist on server by deleting it. // Create a new media object. $media = $this->createMedia(); $this->assertFileExists($this->file->getFileUri()); $file_uri = $this->file->getFileUri(); $media_with_deleted_file_alias = $current_alias ? "$current_alias-deleted-file" : NULL; $media_with_deleted_file = $this->createMedia(TRUE, $media_with_deleted_file_alias, 2, $private_file); $media_with_deleted_file_file = $this->file; $this->assertFileExists($media_with_deleted_file_file->getFileUri()); // Test disposition behaviour before files were deleted. $this->drupalGet('media/' . $media_with_deleted_file->id(), ['query' => ['download' => '']]); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Disposition', ResponseHeaderBag::DISPOSITION_ATTACHMENT); $file_uri = $media_with_deleted_file_file->getFileUri(); unlink($file_uri); $this->assertFileDoesNotExist($this->file->getFileUri()); $this->drupalGet('media/' . $media->id()); self::assertFileDoesNotExist($media_with_deleted_file_file->getFileUri()); $this->drupalGet('media/' . $media_with_deleted_file->id()); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', 'text/html'); // Test disposition behaviour after file was deleted. $this->drupalGet('media/' . $media_with_deleted_file->id(), ['query' => ['download' => '']]); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', 'text/html'); $assert_session->responseHeaderDoesNotExist('Content-Disposition'); } /** * Data provider for testDisplayController(). */ public function providerDisplayController() { return [ 'media with custom alias' => ['/custom-media-alias', FALSE], 'media without alias' => [NULL, FALSE], 'private media with custom alias' => ['/custom-media-alias', TRUE], 'private media without alias' => [NULL, TRUE], ]; } } Loading
README.md +6 −3 Original line number Diff line number Diff line Loading @@ -23,7 +23,9 @@ REQUIREMENTS This module requires: * PHP > 7.2CONTENTS OF THIS FILE * PHP > 7.4 CONTENTS OF THIS FILE --------------------- * Introduction Loading @@ -48,7 +50,7 @@ REQUIREMENTS This module requires: * PHP > 7.2 * PHP > 7.4 * Drupal Core Media Loading @@ -69,7 +71,8 @@ TRICKS ------ * If you're viewing a file with an alias and need help finding the media object just append ?edit-media. This will redirect you straight to the media edit page. `?edit-media` to the URL, this will redirect you straight to the media edit page. * If you want the media file to be downloaded, you may append `?download` or `?dl` to the URL. CONFIGURATION ------------- Loading
media_alias_display.info.yml +0 −1 Original line number Diff line number Diff line Loading @@ -7,4 +7,3 @@ package: Media configure: media_alias_display.settings_form dependencies: - drupal:media - drupal:path_alias
src/Controller/DisplayController.php +90 −89 Original line number Diff line number Diff line Loading @@ -2,25 +2,26 @@ namespace Drupal\media_alias_display\Controller; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ImmutableConfig; use Drupal\Core\Entity\Controller\EntityViewController; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\Path\CurrentPathStack; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface; use Drupal\file\Entity\File; use Drupal\path_alias\AliasManagerInterface; use Drupal\file\FileInterface; use Drupal\media\MediaInterface; use Drupal\media\Plugin\media\Source\File as FileMediaSource; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Drupal\Core\Url; use Drupal\media\Entity\Media; /** * Defines a controller to render a file with Media Alias being used. Loading @@ -35,35 +36,21 @@ class DisplayController extends EntityViewController { protected AccountInterface $currentUser; /** * The logger factory. * The media alias display logger. * * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface * @var \Drupal\Core\Logger\LoggerChannelInterface */ protected LoggerChannelFactoryInterface $loggerFactory; protected LoggerChannelInterface $logger; /** * The request stack. * The current request. * * @var \Symfony\Component\HttpFoundation\Request */ protected Request $request; /** * The current path. * * @var \Drupal\Core\Path\CurrentPathStack */ protected CurrentPathStack $currentPath; /** * The path alias manager. * * @var \Drupal\path_alias\AliasManagerInterface */ protected AliasManagerInterface $aliasManager; /** * Drupal\Core\Config\ConfigManagerInterface definition. * The config factory. * * @var \Drupal\Core\Config\ConfigFactoryInterface */ Loading @@ -76,6 +63,13 @@ class DisplayController extends EntityViewController { */ protected StreamWrapperManagerInterface $streamWrapperManager; /** * The module handler. * * @var \Drupal\Core\Extension\ModuleHandlerInterface */ protected ModuleHandlerInterface $moduleHandler; /** * The controller constructor. * Loading @@ -85,37 +79,33 @@ class DisplayController extends EntityViewController { * The renderer service. * @param \Drupal\Core\Session\AccountInterface $current_user * Current user. * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory * The logger factory. * @param \Drupal\Core\Logger\LoggerChannelInterface $logger * The media alias display logger. * @param \Symfony\Component\HttpFoundation\Request $request_stack * Request stack. * @param \Drupal\Core\Path\CurrentPathStack $current_path * The current path. * @param \Drupal\path_alias\AliasManagerInterface $alias_manager * The path alias manager. * @param \Drupal\Core\Config\ConfigFactoryInterface $config * Configuration Interface. * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager * The stream wrapper manager. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, AccountInterface $current_user, LoggerChannelFactoryInterface $loggerFactory, LoggerChannelInterface $logger, Request $request_stack, CurrentPathStack $current_path, AliasManagerInterface $alias_manager, ConfigFactoryInterface $config, StreamWrapperManagerInterface $stream_wrapper_manager StreamWrapperManagerInterface $stream_wrapper_manager, ModuleHandlerInterface $module_handler ) { parent::__construct($entity_type_manager, $renderer); $this->currentUser = $current_user; $this->loggerFactory = $loggerFactory; $this->logger = $logger; $this->request = $request_stack; $this->currentPath = $current_path; $this->aliasManager = $alias_manager; $this->configManager = $config; $this->streamWrapperManager = $stream_wrapper_manager; $this->moduleHandler = $module_handler; } /** Loading @@ -126,12 +116,11 @@ class DisplayController extends EntityViewController { $container->get('entity_type.manager'), $container->get('renderer'), $container->get('current_user'), $container->get('logger.factory'), $container->get('logger.factory')->get('media_alias_display'), $container->get('request_stack')->getCurrentRequest(), $container->get('path.current'), $container->get('path_alias.manager'), $container->get('config.factory'), $container->get('stream_wrapper_manager'), $container->get('module_handler') ); } Loading @@ -139,96 +128,108 @@ class DisplayController extends EntityViewController { * {@inheritdoc} */ public function view(EntityInterface $media, $view_mode = 'full', $langcode = NULL) { assert($media instanceof MediaInterface); $config = $this->configManager->get('media_alias_display.settings'); if (!empty($config->get('kill_switch')) && $config->get('kill_switch')) { return parent::view($media, $view_mode); return $this->updateRenderCache(parent::view($media, $view_mode), $config); } $media_bundle = $media->bundle(); $allowed_bundles = $config->get('media_bundles'); if (!empty($config->get('media_bundles'))) { $allowAllBundles = TRUE; foreach ($config->get('media_bundles') as $bundle) { if ($bundle !== 0) { $allowAllBundles = FALSE; $allow_all_bundles = TRUE; foreach ($config->get('media_bundles') as $allowed_bundle) { if ($allowed_bundle !== 0) { $allow_all_bundles = FALSE; break; } } if (!$allowAllBundles && isset($allowed_bundles[$media->bundle()]) && $allowed_bundles[$media->bundle()] === 0) { return parent::view($media, $view_mode); if (!$allow_all_bundles && isset($allowed_bundles[$media_bundle]) && $allowed_bundles[$media_bundle] === 0) { return $this->updateRenderCache(parent::view($media, $view_mode), $config); } } $current_path = $this->currentPath->getPath(); $alias = $this->aliasManager->getPathByAlias($current_path); $params = Url::fromUri('internal:' . $alias)->getRouteParameters(); $entity_type = key($params); $mid = $params[$entity_type]; $media = Media::load($mid); $bundle = $media->bundle(); $edit_own = 'edit own ' . $bundle . ' media'; $edit_any = 'edit any ' . $bundle . ' media'; $edit_own = 'edit own ' . $media_bundle . ' media'; $edit_any = 'edit any ' . $media_bundle . ' media'; // Skip redirect and go straight to media object. if ($this->request->query->has('edit-media') && (($this->currentUser->hasPermission($edit_own) || $this->currentUser->hasPermission($edit_any)) || $this->currentUser->hasPermission('administer media'))) { return new RedirectResponse('/media/' . $mid . '/edit'); return new RedirectResponse($media->toUrl('edit-form')->toString()); } if (\Drupal::moduleHandler()->moduleExists('media_alias_display_field_override')) { if ( $this->moduleHandler->moduleExists('media_alias_display_field_override') && $media->hasField('field_override_mad_module') ) { $override_module = $media->get('field_override_mad_module')->value; if (isset($override_module) && $override_module) { return parent::view($media, $view_mode); return $this->updateRenderCache(parent::view($media, $view_mode), $config); } } $source = $media->getSource(); $config = $source->getConfiguration(); $field = $config['source_field']; $fid = $media->{$field}->target_id; // If media has no file item. if (!$fid) { $this->loggerFactory->get('media_alias_display') ->notice('The media item requested has no file referenced/uploaded for @path', [ '@path' => $current_path, if (!($source instanceof FileMediaSource)) { // The module only supports file media sources at the moment. Could // potentially add support for redirect to oEmbed sources. $this->logger ->notice('Media item "@media_entity_id" does not have a file media source', [ '@media_entity_id' => $media->id(), ]); return parent::view($media, $view_mode); return $this->updateRenderCache(parent::view($media, $view_mode), $config); } $file = File::load($fid); $file = $media->get($source->getConfiguration()['source_field'])->entity; // Or file entity could not be loaded. Very unlikely to happen. // If media has no file item. if (!$file) { $this->loggerFactory->get('media_alias_display') ->notice('File id could not be loaded for ' . $current_path); return parent::view($media, $view_mode); $this->logger ->notice('Media item "@media_entity_id" does not have a file entity attached', [ '@media_entity_id' => $media->id(), ]); return $this->updateRenderCache(parent::view($media, $view_mode), $config); } assert($file instanceof FileInterface); $uri = $file->getFileUri(); $scheme = $this->streamWrapperManager::getScheme($uri); // Or item does not exist on disk. if (!$this->streamWrapperManager->isValidScheme($scheme) || !file_exists($uri)) { $this->loggerFactory->get('media_alias_display') ->notice('File does not exist for @path', [ '@path' => $current_path, if (!$this->streamWrapperManager->isValidScheme($scheme) || !is_file($uri)) { $this->logger ->notice('File attached to Media item "@media_entity_id" does not exist on disk', [ '@media_entity_id' => $media->id(), ]); return parent::view($media, $view_mode); return $this->updateRenderCache(parent::view($media, $view_mode), $config); } $filename = $file->getFilename(); $response = new BinaryFileResponse($uri, Response::HTTP_OK, [], $scheme !== 'private'); // Force a direct download if a "dl" or "download" query string is present. if ($this->request->query->has('dl') || $this->request->query->has('download')) { $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $file->getFilename()); } $response = new BinaryFileResponse($uri); $response->setContentDisposition( ResponseHeaderBag::DISPOSITION_INLINE, $filename ); return $response; } return new BinaryFileResponse($uri, Response::HTTP_OK, [], $scheme !== 'private'); /** * Add appropriate cache tags to the render array. */ protected function updateRenderCache($response, ImmutableConfig $config) { if (!is_array($response)) { return $response; } CacheableMetadata::createFromRenderArray($response) ->addCacheableDependency($config) ->addCacheContexts([ 'url.query_args:dl', 'url.query_args:download', ]) ->applyTo($response); return $response; } }
tests/src/Functional/MediaAliasDisplayControllerTest.php +92 −22 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ namespace Drupal\Tests\media_alias_display\Functional; use Drupal\Core\File\FileSystemInterface; use Drupal\field\Entity\FieldConfig; use Drupal\file\Entity\File; use Drupal\file\FileInterface; Loading @@ -11,6 +12,7 @@ use Drupal\Tests\media\Functional\MediaFunctionalTestBase; use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; use Drupal\Tests\TestFileCreationTrait; use Drupal\user\Entity\Role; use Symfony\Component\HttpFoundation\ResponseHeaderBag; /** * Simple test to ensure that main page loads with module enabled. Loading @@ -28,7 +30,7 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { * @var array */ protected static $modules = [ 'media_alias_display', 'media_alias_display', 'path', ]; /** Loading @@ -41,7 +43,7 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { * * @var \Drupal\file\FileInterface */ protected FileInterface $file; protected ?FileInterface $file; /** * Store Media Type. Loading Loading @@ -86,7 +88,7 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { * * @throws \Drupal\Core\Entity\EntityStorageException */ protected function createMedia($include_file = TRUE) { protected function createMedia($include_file = TRUE, $path_alias = NULL, $index = 0, $private_file = FALSE) { $media_type_id = $this->mediaType->id(); /** @var \Drupal\field\FieldConfigInterface $field */ $field = FieldConfig::load("media.$media_type_id.field_media_image"); Loading @@ -100,11 +102,26 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { 'name' => 'Custom name', 'bundle' => $media_type_id, 'status' => TRUE, 'path' => $path_alias, ]); $this->file = NULL; if ($include_file) { /** @var \Drupal\Core\File\FileSystemInterface $file_system */ $file_system = \Drupal::service('file_system'); $original_uri = $this->getTestFiles('image')[$index]->uri; // Use a separate media_alias_display directory and file for this. // This should allow us to manipulate the files without any side effects. $destination_dir = ($private_file ? 'private://' : 'public://') . 'media_alias_display/'; $file_system->prepareDirectory($destination_dir, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); $destination_uri = $destination_dir . '/' . pathinfo($original_uri, PATHINFO_BASENAME); $uri = $file_system->copy($original_uri, $destination_uri, FileSystemInterface::EXISTS_REPLACE); $file = File::create([ 'uri' => $this->getTestFiles('image')[0]->uri, // Add a custom file name that might be different to the real internal // file. 'filename' => 'custom-file-name-' . $file_system->basename($uri), 'uri' => $uri, ]); $file->save(); $this->file = $file; Loading @@ -126,16 +143,24 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { * * @throws \Drupal\Core\Entity\EntityStorageException * @throws \Behat\Mink\Exception\ExpectationException * * @dataProvider providerDisplayController */ public function testDisplayController() { public function testDisplayController($current_alias, bool $private_file) { $assert_session = $this->assertSession(); $media = $this->createMedia(); $media = $this->createMedia(TRUE, $current_alias, 0, $private_file); $media_file = $this->file; if ($current_alias) { $path_alias = ltrim($current_alias, '/'); } else { $path_alias = 'media/' . $media->id(); } // Verifies kill switch isn't enabled + verifies it's not an allowed bundle. $this->drupalGet('media/' . $media->id()); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', $this->file->getMimeType()); $assert_session->responseHeaderContains('Content-Type', $media_file->getMimeType()); // Test when the kill switch is enabled. \Drupal::configFactory() Loading @@ -153,11 +178,13 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { ->getEditable('media_alias_display.settings') ->set('kill_switch', FALSE) ->save(TRUE); $this->resetAll(); // There should be no need to explicitly flush the Drupal cache assuming // we pass in all the relevant cache tags to the original responses. /* $this->resetAll(); */ $this->drupalGet('media/' . $media->id()); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', $this->file->getMimeType()); $assert_session->responseHeaderContains('Content-Type', $media_file->getMimeType()); // Test when a single bundle is selected. \Drupal::configFactory() Loading @@ -168,15 +195,15 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { $this->drupalGet('media/' . $media->id()); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', $this->file->getMimeType()); $assert_session->responseHeaderContains('Content-Type', $media_file->getMimeType()); // Test when edit-media is placed in the URL. // Should redirect to media edit page. User should have permission. $this->drupalGet('media/' . $media->id(), [ 'query' => ['edit-media' => 1], 'query' => ['edit-media' => ''], 'absolute' => TRUE, ]); $this->assertSession()->addressEquals('media/' . $media->id() . '/edit'); $assert_session->addressEquals('media/' . $media->id() . '/edit'); // Test when user doesn't have permission. $current_roles = $this->loggedInUser->getRoles(TRUE); Loading @@ -188,7 +215,7 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { 'absolute' => TRUE, ]); // User won't be redirected because they don't have permission. $this->assertSession()->addressEquals('media/' . $media->id()); $assert_session->addressEquals($path_alias); // Grant "edit any bundle media" permission that should allow a user to // access the edit page. Loading @@ -197,7 +224,7 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { 'query' => ['edit-media' => 1], 'absolute' => TRUE, ]); $this->assertSession()->addressEquals('media/' . $media->id() . '/edit'); $assert_session->addressEquals('media/' . $media->id() . '/edit'); user_role_revoke_permissions($role->id(), ['edit any ' . $this->mediaType->id() . ' media']); // Grant "edit own bundle media" permission that should allow a user to Loading @@ -207,24 +234,67 @@ class MediaAliasDisplayControllerTest extends MediaFunctionalTestBase { 'query' => ['edit-media' => 1], 'absolute' => TRUE, ]); $this->assertSession()->addressEquals('media/' . $media->id() . '/edit'); $assert_session->addressEquals('media/' . $media->id() . '/edit'); // Test content dispositions. $this->drupalGet('media/' . $media->id()); // The content disposition header should not exist by default. $assert_session->responseHeaderDoesNotExist('Content-Disposition'); $this->drupalGet('media/' . $media->id(), ['query' => ['download' => '']]); $assert_session->responseHeaderContains('Content-Disposition', ResponseHeaderBag::DISPOSITION_ATTACHMENT); $assert_session->responseHeaderContains('Content-Disposition', $media_file->getFilename()); $this->drupalGet('media/' . $media->id(), ['query' => ['dl' => '1']]); $assert_session->responseHeaderContains('Content-Disposition', ResponseHeaderBag::DISPOSITION_ATTACHMENT); $assert_session->responseHeaderContains('Content-Disposition', $media_file->getFilename()); // Test when there is no file attached to an allowed bundle. $media_no_file = $this->createMedia(FALSE); $media_no_file_alias = $current_alias ? "$current_alias-no-file" : NULL; $media_no_file = $this->createMedia(FALSE, $media_no_file_alias, 1, $private_file); $this->drupalGet('media/' . $media_no_file->id()); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', 'text/html'); // Test disposition behaviour without files. $this->drupalGet('media/' . $media_no_file->id(), ['query' => ['download' => '']]); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', 'text/html'); $assert_session->responseHeaderDoesNotExist('Content-Disposition'); // Test when file doesn't exist on server by deleting it. // Create a new media object. $media = $this->createMedia(); $this->assertFileExists($this->file->getFileUri()); $file_uri = $this->file->getFileUri(); $media_with_deleted_file_alias = $current_alias ? "$current_alias-deleted-file" : NULL; $media_with_deleted_file = $this->createMedia(TRUE, $media_with_deleted_file_alias, 2, $private_file); $media_with_deleted_file_file = $this->file; $this->assertFileExists($media_with_deleted_file_file->getFileUri()); // Test disposition behaviour before files were deleted. $this->drupalGet('media/' . $media_with_deleted_file->id(), ['query' => ['download' => '']]); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Disposition', ResponseHeaderBag::DISPOSITION_ATTACHMENT); $file_uri = $media_with_deleted_file_file->getFileUri(); unlink($file_uri); $this->assertFileDoesNotExist($this->file->getFileUri()); $this->drupalGet('media/' . $media->id()); self::assertFileDoesNotExist($media_with_deleted_file_file->getFileUri()); $this->drupalGet('media/' . $media_with_deleted_file->id()); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', 'text/html'); // Test disposition behaviour after file was deleted. $this->drupalGet('media/' . $media_with_deleted_file->id(), ['query' => ['download' => '']]); $assert_session->statusCodeEquals(200); $assert_session->responseHeaderContains('Content-Type', 'text/html'); $assert_session->responseHeaderDoesNotExist('Content-Disposition'); } /** * Data provider for testDisplayController(). */ public function providerDisplayController() { return [ 'media with custom alias' => ['/custom-media-alias', FALSE], 'media without alias' => [NULL, FALSE], 'private media with custom alias' => ['/custom-media-alias', TRUE], 'private media without alias' => [NULL, TRUE], ]; } }