Problem/Motivation
file_file_download() invokes file_get_file_references() to find out which fields contain the specified file. The function fails because it looks for references in default language but the file can be linked to the translated field in different language.
This completely breaks file_file_download()
, because file_get_file_references()
returns empty value. And since file_file_download()
finds no file references it refuses to download the file.
This issue also blocks completely downloading private files when they are translated, resulting in an access denied page.
Steps to reproduce
On a fresh D8 install:
- Enable content translation, language and field UI modules.
- Install additional language.
- Enable the private file system at Administration > Configuration > Media > File system
- Add a private file field to Basic page content type.
- Configure content type Basic page to be translatable (Administration >> Configuration >> Regional and language). Remember to make file field translatable.
- Add new Basic page and set its language to default.
- Translate the node to a second language and save. Remember to also upload different version of the file.
- Invoke
file_file_download('public://YOUR_TRANSLATED_FILE');
. I've used devel module's PHP block. If you provide path to the translated file thefile_file_download()
will return empty value. If you provide path to the original file version thefile_file_download()
will return array of headers.
Tested with language Detection and selection method set to session and browser.
It seems that the problem lies here (part of the file_get_file_references()
function):
if (!$match && ($items = $entity->get($field_name))) {
foreach ($items as $item) {
if ($file->id() == $item->{$field_column}) { // Possible cause?
$match = TRUE;
break;
}
}
}
$item->{$field_column}
returns ID of the untranslated file but $file->id()
returns ID of translated file.
D7 is also affected with similar issue #2112287: file_file_download() does not specify language when calling field_get_items(). I've already created patch for D7 but my knowledge of D8 is too limited for now to be able to understand why so much overhead is needed in D8 to achieve such a simple thing.
Proposed resolution
I see two possible solutions
- Fix File entity, to make it actually save language instead of using hardcoded 'und'. That might help and we could probably use the language code to load specific translation inside
file_get_file_references()
. - Iterate over all entity translations inside
file_get_file_references()
to find a match and allow file download.
Remaining tasks
Find out how to correctly identify file references.Create a patch.- Review the patch (solution 2)
- Discuss making File entity save language
User interface changes
No user interface changes.
API changes
No API changes.
Comments
Comment #1
SiliconMind CreditAttribution: SiliconMind commentedAdded some "code" tags to make the summary more readable.
(what happened to the "preview" button??)
Comment #2
matsbla CreditAttribution: matsbla commentedAdd "beta blocker" tag
It is important to fix this i18n bug
so file_file_download() function can manage such a simple thing as calling the correct translated file
Comment #3
xjmNot all important bugs are beta blockers; there's a specific set of criteria for beta blockers (critical issues that affect the data model or change the public entity, routing, plugin, caching, or configuration APIs). Also, the beta blocker tag should only be added by (or with) a core maintainer. Thanks!
Comment #4
Gábor HojtsyComment #5
SiliconMind CreditAttribution: SiliconMind commentedThis is what I came up with.
The problem is that this code checks only active entity language completely ignoring entity translations:
The fact that $file entity does not specify language (Maybe it should? See #2280675: Make file field save file language) does not help because we don't know which entity translation to use when comparing fids.
With this we have two options:
file_get_file_references()
.file_get_file_references()
to find a match and allow file download.Since first option is for now more complicated, here's a patch that deals with this issue as described in second option.
Comment #6
SiliconMind CreditAttribution: SiliconMind commentedForgot to update issue status.
Comment #9
SiliconMind CreditAttribution: SiliconMind commentedLooks like irrelevant tests failed. Resubmitting.
Comment #15
SiliconMind CreditAttribution: SiliconMind commentedDamn, it keeps failing due to some random mysql's "Too many connections" error.
Comment #17
SiliconMind CreditAttribution: SiliconMind commentedGabor, can we move on with this one? Or do you need something more?
Comment #18
Jose Reyero CreditAttribution: Jose Reyero commentedI've been testing this one and can confirm the issue. But it is actually much worse than the original report (which only talks about the file API not working as expected).
This issue also blocks completely downloading private files when they are translated, resulting in an access denied page.
To reproduce it:
- Follo steps 1 to 6 of the report, but making the field a private file.
- Visit the node page in the translated language, click the file download link -> Access denied.
I can confirm the patch, though not the nicest solution, works and fixes both issues, with private and public files.
A better fix would mean actually fixing the file API, getting the 'file_usage' table to also store the language version of the field where it is used, but that's way more complex...
I also think this one should be raised to critical, since downloading of private files is just broken.
Comment #19
Jose Reyero CreditAttribution: Jose Reyero commentedUpdated title and issue description
Comment #20
Devin Carlson CreditAttribution: Devin Carlson commentedA test to verify that access to private files attached to translated content is broken.
I'm also looking into the more advanced solution proposed in #18 but it would definitely be a larger change.
Comment #22
Devin Carlson CreditAttribution: Devin Carlson commentedComment #23
SiliconMind CreditAttribution: SiliconMind commented@Devin, I've already opened issue for that #2280675: Make file field save file language but it's really messy stuff.
Your test also touches very important issue which is #2280527: Unable to upload different language versions of images with image field.
Comment #24
Wim LeersThe code changes themselves look fine. Mostly nitpicks about the tests :)
s/than default/than the default/
Missing space after
foreach
.s/Definition of Drupal/Contains \/
The sentence makes more sense to me than the test name. Perhaps
PrivateFileOnTranslatedEntity
makes more sense?Broken sentence.
Missing
{@inheritdoc}
.… on page node.
Looks like the comment belongs above
$permissions
?s/field/file field/
Why not just use
$this->fieldName
everywhere?I'd say "default language" instead of "English", and I'd even try to omit the explicit
en
langcode.I'd understand this could be necessary when changing language settings. But here, this doesn't make any sense to me.
Node::load()
File::load()
Comment #25
xjmJust a minor note, @Devin Carlson -- attach your test-only patch first, and then the full patch second. Testbot automatically retests the last parch in the issue once it's RTBC, so we want to make sure that's the real thing. :) Also that avoids the System Message NW noise for an expected fail. Thanks! And thanks @Wim Leers for the review.
Comment #26
Devin Carlson CreditAttribution: Devin Carlson commentedAn updated patch including all of the changes from #24, except that I haven't found a way to remove
$this->rebuildContainer();
.This is also found in
PathLanguageTest
'stestAliasTranslation()
:Comment #27
Wim LeersCould you please provide an interdiff?
Comment #28
Devin Carlson CreditAttribution: Devin Carlson commentedInterdiff between #20 and #26.
Comment #29
Gábor HojtsyErm, should still end in Test. I see Wim suggested a specific name without test, but AFAIK it should still end in Test.
https://www.drupal.org/node/325974 The class name should end in "UnitTest" if it's a unit test (Unit or DrupalUnit), and just "Test" if it's a web-based test.
Comment #30
Wim LeersSorry about that :(
Comment #31
pfrenssenThis break is not sufficient, it only breaks out of the inner foreach, while it should also break out of the outer one.
This is overriding the parent property, so document it with {@inheritdoc}.
Use lowerCamelCase for properties. Also declare and document this property.
It's not the node that is private, but rather the file.
Comment #32
pfrenssenComment #33
pfrenssenFixed my remarks and Gábor's remark from #29 + some more small fixes:
Comment #35
Gábor HojtsyComment #36
Gábor HojtsyComment #37
Gábor HojtsyLooks good except the following things I found:
This looks odd at this point. Eg. this same code does not appear after the second file is uploaded. Should this happen after the new language is added? Not sure what cache is this meant to clean?
This is valid but looks odd. If there was no fr translation, getTranslation() returns a new one. I would do the getTranslation() after this assert to be in a logical order. (This does not change anything about the behaviour just how the code reads).
This does not ensure the two file target ids are different, that in fact this was a translated file download :)
Comment #38
Gábor HojtsyI don't think this is a lot of work to do, hopefully not a big setback?
Comment #39
Devin Carlson CreditAttribution: Devin Carlson commentedNeeded a few changes after #2304969: Port private files access bypass from SA-CORE-2014-003.
I still haven't managed to track down why the container rebuild is necessary (as is done in testAliasTranslation). I get a
InvalidArgumentException: Invalid translation language (fr) specified.
if it is removed.Moved.
I'm not sure I follow 100%. Should we verify that the default language and french nodes are using different files? If so, I added a check to the attached patch.
Comment #41
Gábor Hojtsy@Devin: yeah I had checking that the two files are different in mind, so that the upload of files worked and is indeed different per language. The test before that only ensured that the files could be downloaded but they could still be the same. Thanks for that update.
As for the container, I would reset the language manager instead as an attempt. \Drupal::languageManager()->reset(); I think that would be enough to help with the entity translation stuff. The reason that fails is the language list is cached in the language manager and is not reset for you since you use the API to access the translation but the UI to add the language. Probably better: use ConfigurableLanguage::save() to save a new language (after instantiated). That would clear the cache I guess.
Comment #42
Gábor HojtsyLooking at ConfigurableLanguage.php it does not seem like save() would clear the languageManager internal language list cache. BTW #1879930: Language selectors are not showing localized to the page language is removing that cache for good, so if that is committed before this one lands, then the cache clear may not be needed. So I would try with \Drupal::languageManager()->reset(); then instead of the whole container clearing. That may not be enough but worth a try to be less intrusive :)
Comment #43
Gábor Hojtsy#1879930: Language selectors are not showing localized to the page language did not end up removing the cache, in fact it added a per language cache. The clearing will be needed. Did you try with \Drupal::languageManager()->reset(); ?
Comment #44
Devin Carlson CreditAttribution: Devin Carlson commentedRetested #39 against the latest HEAD.
With only
languageManager()->reset()
, the test fails witharray_filter() expects parameter 1 to be array, null given in Drupal\content_translation\Controller\ContentTranslationController->overview()
.$entity->translation
is NULL instead ofComment #46
Gábor HojtsyI tried to run the patch locally but it segfaults my local setup (in its curently posted unmodified form):
So I could not try moving the kernel rebuilding around. I would have done it at the end of setUp() or right after adding French in setUp(). Not in the middle of the test function. There does not seem to be anything in that test function that necessitates a kernel rebuild, no?
Anyway, this is a minor nit compared to how important this bugfix and test is, so I am not going to keep blocking it. It looks great overall.
Comment #47
Gábor Hojtsy(BTW it is not just this test that segfaults for me, other existing file upload tests in core, eg. the responsive image field test does the same. So I am sure that is not the fault of this test, rather bugs in my environment).
Comment #48
alexpottI had a play with moving the container rebuild around. The most likely issue here is that the test running environment is getting out of sync with the child environment.
This issue addresses a critical bug and is allowed per #2350615: [policy, no patch] What changes can be accepted during the Drupal 8 beta phase?. Committed c6742dd and pushed to 8.0.x. Thanks!
Comment #50
Gábor HojtsyThanks all!
Comment #51
Gábor HojtsyBy the way, the Drupal 7 version of this patch is at #2112287: file_file_download() does not specify language when calling field_get_items(). Please help with the port there.