Skip to content
Snippets Groups Projects
Commit c95109e2 authored by Alex Bronstein's avatar Alex Bronstein
Browse files

Issue #3312198 by catch, alexpott, longwave, Nixou, Spokje, darvanen:...

Issue #3312198 by catch, alexpott, longwave, Nixou, Spokje, darvanen: Regression concerning the cache of private files
parent fcf0932d
Branches
Tags
36 merge requests!12227Issue #3181946 by jonmcl, mglaman,!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!54479.5.x SF update,!5014Issue #3071143: Table Render Array Example Is Incorrect,!4868Issue #1428520: Improve menu parent link selection,!4289Issue #1344552 by marcingy, Niklas Fiekas, Ravi.J, aleevas, Eduardo Morales...,!4114Issue #2707291: Disable body-level scrolling when a dialog is open as a modal,!4100Issue #3249600: Add support for PHP 8.1 Enums as allowed values for list_* data types,!3630Issue #2815301 by Chi, DanielVeza, kostyashupenko, smustgrave: Allow to create...,!3600Issue #3344629: Passing null to parameter #1 ($haystack) of type string is deprecated,!3291Issue #3336463: Rewrite rules for gzipped CSS and JavaScript aggregates never match,!3102Issue #3164428 by DonAtt, longwave, sahil.goyal, Anchal_gupta, alexpott: Use...,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2334Issue #3228209: Add hasRole() method to AccountInterface,!2074Issue #2707689: NodeForm::actions() checks for delete access on new entities,!2062Issue #3246454: Add weekly granularity to views date sort,!1591Issue #3199697: Add JSON:API Translation experimental module,!1484Exposed filters get values from URL when Ajax is on,!1255Issue #3238922: Refactor (if feasible) uses of the jQuery serialize function to use vanillaJS,!1254Issue #3238915: Refactor (if feasible) uses of the jQuery ready function to use VanillaJS,!1162Issue #3100350: Unable to save '/' root path alias,!1105Issue #3025039: New non translatable field on translatable content throws error,!1073issue #3191727: Focus states on mobile second level navigation items fixed,!10223132456: Fix issue where views instances are emptied before an ajax request is complete,!957Added throwing of InvalidPluginDefinitionException from getDefinition().,!925Issue #2339235: Remove taxonomy hard dependency on node module,!877Issue #2708101: Default value for link text is not saved,!873Issue #2875228: Site install not using batch API service,!872Draft: Issue #3221319: Race condition when creating menu links and editing content deletes menu links,!844Resolve #3036010 "Updaters",!712Issue #2909128: Autocomplete intermittent on Chrome Android,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493,!485Sets the autocomplete attribute for username/password input field on login form.,!30Issue #3182188: Updates composer usage to point at ./vendor/bin/composer
......@@ -3,6 +3,7 @@
namespace Drupal\Core\EventSubscriber;
use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
......@@ -91,6 +92,14 @@ public function on404(ExceptionEvent $event) {
if ($fast_paths && preg_match($fast_paths, $request->getPathInfo())) {
$fast_404_html = strtr($config->get('fast_404.html'), ['@path' => Html::escape($request->getUri())]);
$response = new HtmlResponse($fast_404_html, Response::HTTP_NOT_FOUND);
// Some routes such as system.files conditionally throw a
// NotFoundHttpException depending on URL parameters instead of just the
// route and route parameters, so add the URL cache context to account
// for this.
$cacheable_metadata = new CacheableMetadata();
$cacheable_metadata->setCacheContexts(['url']);
$cacheable_metadata->addCacheTags(['4xx-response']);
$response->addCacheableDependency($cacheable_metadata);
$event->setResponse($response);
}
}
......
......@@ -2,6 +2,7 @@
namespace Drupal\file\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
......@@ -268,4 +269,20 @@ public static function getDefaultEntityOwner() {
return NULL;
}
/**
* {@inheritdoc}
*/
protected function invalidateTagsOnSave($update) {
$tags = $this->getListCacheTagsToInvalidate();
// Always invalidate the 404 or 403 response cache because while files do
// not have a canonical URL as such, they may be served via routes such as
// private files.
// Creating or updating an entity may change a cached 403 or 404 response.
$tags = Cache::mergeTags($tags, ['4xx-response']);
if ($update) {
$tags = Cache::mergeTags($tags, $this->getCacheTagsToInvalidate());
}
Cache::invalidateTags($tags);
}
}
......@@ -82,7 +82,7 @@ protected function doPrivateFileTransferTest() {
// Create a file.
$contents = $this->randomMachineName(8);
$file = $this->createFile(NULL, $contents, 'private');
$file = $this->createFile($contents . '.txt', $contents, 'private');
// Created private files without usage are by default not accessible
// for a user different from the owner, but createFile always uses uid 1
// as the owner of the files. Therefore make it permanent to allow access
......@@ -108,19 +108,33 @@ protected function doPrivateFileTransferTest() {
$this->assertSame($contents, $this->getSession()->getPage()->getContent(), 'Contents of the file are correct.');
$http_client = $this->getHttpClient();
// Deny access to all downloads via a -1 header.
file_test_set_return('download', -1);
$response = $http_client->head($url, ['http_errors' => FALSE]);
$this->assertSame(403, $response->getStatusCode(), 'Correctly denied access to a file when file_test sets the header to -1.');
// Try non-existent file.
file_test_reset();
$url = $this->fileUrlGenerator->generateAbsoluteString('private://' . $this->randomMachineName());
$response = $http_client->head($url, ['http_errors' => FALSE]);
$not_found_url = $this->fileUrlGenerator->generateAbsoluteString('private://' . $this->randomMachineName() . '.txt');
$response = $http_client->head($not_found_url, ['http_errors' => FALSE]);
$this->assertSame(404, $response->getStatusCode(), 'Correctly returned 404 response for a non-existent file.');
// Assert that hook_file_download is not called.
$this->assertEquals([], \Drupal::state()->get('file_test.results')['download']);
// Having tried a non-existent file, try the original file again to ensure
// it's returned instead of a 404 response.
// Set file_test access header to allow the download.
file_test_reset();
file_test_set_return('download', ['x-foo' => 'Bar']);
$this->drupalGet($url);
// Verify that header is set by file_test module on private download.
$this->assertSession()->responseHeaderEquals('x-foo', 'Bar');
// Verify that page cache is disabled on private file download.
$this->assertSession()->responseHeaderDoesNotExist('x-drupal-cache');
$this->assertSession()->statusCodeEquals(200);
// Test that the file transferred correctly.
$this->assertSame($contents, $this->getSession()->getPage()->getContent(), 'Contents of the file are correct.');
// Deny access to all downloads via a -1 header.
file_test_set_return('download', -1);
$response = $http_client->head($url, ['http_errors' => FALSE]);
$this->assertSame(403, $response->getStatusCode(), 'Correctly denied access to a file when file_test sets the header to -1.');
// Try requesting the private file url without a file specified.
file_test_reset();
$this->drupalGet('/system/files');
......
......@@ -2,6 +2,7 @@
namespace Drupal\FunctionalTests\EventSubscriber;
use Drupal\file\Entity\File;
use Drupal\Tests\BrowserTestBase;
/**
......@@ -18,6 +19,11 @@ class Fast404Test extends BrowserTestBase {
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = ['file'];
/**
* Tests the fast 404 functionality.
*/
......@@ -69,4 +75,33 @@ public function testFast404(): void {
$this->assertSession()->responseHeaderContains('X-Generator', 'Drupal');
}
/**
* Tests the fast 404 functionality.
*/
public function testFast404PrivateFiles(): void {
$admin = $this->createUser([], NULL, TRUE);
$this->drupalLogin($admin);
$file_url = 'system/files/test/private-file-test.txt';
$this->drupalGet($file_url);
$this->assertSession()->statusCodeEquals(404);
$this->drupalGet($file_url);
$this->assertSession()->statusCodeEquals(404);
// Create a private file for testing accessible by the admin user.
\Drupal::service('file_system')->mkdir($this->privateFilesDirectory . '/test');
$filepath = 'private://test/private-file-test.txt';
$contents = "file_put_contents() doesn't seem to appreciate empty strings so let's put in some data.";
file_put_contents($filepath, $contents);
$file = File::create([
'uri' => $filepath,
'uid' => $admin->id(),
]);
$file->save();
$this->drupalGet($file_url);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($contents);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment