Problem/Motivation

EntityOwnerTrait::ownerBaseFieldDefinitions() is a helper to define an 'owner' field for entity types.

The DB fields created for the owner field have NOT NULL set on the DB column. However, the entity field definition is not set to required.

This means that unless an entity implements custom logic to ensure the field always has a value set (.e.g. Node::preSave()), it is possible to attempt to save an entity with the owner field's value set to NULL, which then causes a database crash.

Steps to reproduce

1. go to the form to create a media entity
2. fill in everything that's required
3. clear the value in the 'Authored by form element
4. press save.

See #3260175: Saving media entity without an owner crashes.

Proposed resolution

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

joachim created an issue. See original summary.

joachim’s picture

tstoeckler’s picture

Thanks for opening the issue! Interesting, I would have thought that the default value callback that EntityOwnerTrait adds a value in every case. Are you doing anything particular to have the entity (attempt to) save without a value or am I just wrong about that?

Regardless, however, the underlying issue is valid, I think. A non-required field should not be NOT NULL in the DB. The underlying problem is that we make the assumption that every entity key should have a NOT NULL constraint. That is a problematic assumption for the reason stated here (among others) and is, theoretically, being fixed in #2841291: Fix NOT NULL handling in the entity storage and 'primary key' changes when updating the storage definition of an identifier field. So adding that as a related issue.

Since this is a concrete bug report, though, I think it makes sense to discuss this on its own merits. We already have an exception for the label and the revision translation affected keys to "opt" those "out" of the assumption mentioned above, this happens in \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::getSharedTableFieldSchema(). Adding the relevant snippet here, because I think the context is useful:

    // A shared table contains rows for entities where the field is empty
    // (since other fields stored in the same table might not be empty), thus
    // the only columns that can be 'not null' are those for required
    // properties of required fields. For now, we only hardcode 'not null' to a
    // few "entity keys", in order to keep their indexes optimized.
    // @todo Fix this in https://www.drupal.org/node/2841291.
    $not_null_keys = $this->entityType->getKeys();
    // Label and the 'revision_translation_affected' fields are not necessarily
    // required.
    unset($not_null_keys['label'], $not_null_keys['revision_translation_affected']);

So one thing we could do is just add the owner key to this list of exceptions and that would fix this concrete issue. Since for nodes the owner will in fact never be NULL, as is noted in the issue summary, this would be an issue if there are SQL queries that rely on the owner index (or node_field__uid__target_id index to be precise) on node_field_data, as those would suffer a performance decrease due to this. I am not sure, though, A) if there are any queries that use that index (from a cursory glance, nothing stuck out at me - at least nothing that would be run on a regular page - but I am a bit out of my depth here to be honest) and B) if there are any what the concrete performance implications on those queries are. I presume the index was not added "explicitly" either and just came "automatically" because the owner field is an entity reference field and that just adds an index automatically, but not sure...

joachim’s picture

Issue summary: View changes

> Interesting, I would have thought that the default value callback that EntityOwnerTrait adds a value in every case. Are you doing anything particular to have the entity (attempt to) save without a value or am I just wrong about that?

I hadn't spotted the default value callback, but AFAICT field default values are applied when the entity is created, not when it's being saved with empty values.

It's really easy to make this crash Drupal in the UI -- I've added the exact steps to reproduce to the IS.

> Since for nodes the owner will in fact never be NULL, as is noted in the issue summary, this would be an issue if there are SQL queries that rely on the owner index

Querying for entities based on owners is a fairly common use case.

I would say the fix needed here is to make the field required.

andregp’s picture

Assigned: Unassigned » andregp

I'll work on this

andregp’s picture

Assigned: andregp » Unassigned
Status: Active » Needs review
FileSize
187.63 KB
86.13 KB
186.65 KB
533 bytes

I've tried adding the owner key to this list of exceptions as mentioned on #3 but neither of the options bellow worked (you can see the errors on the images).

unset($not_null_keys['label'], $not_null_keys['revision_translation_affected'], $not_null_keys['owner']);
unset($not_null_keys['label'], $not_null_keys['revision_translation_affected'], $not_null_keys['uid']);

So I changed "Authored by" field to be required.

tstoeckler’s picture

Fair enough @joachim. From the issue summary I had - apparently wrongly - assumed that you had a legitimate use-case for saving an entity with an empty owner. Absolutely fine with making it required since it effectively is required already. Does that mean, though, that we can remove the respective code in Node::preSave()?

joachim’s picture

> Does that mean, though, that we can remove the respective code in Node::preSave()?

Probably, yes, I think?

- If you're using $node_storage->create() then the default value callback has acted.
- If you're saving a node add form, then the default value callback acted when the entity was created to set into the form's initial values

joachim’s picture

Status: Needs review » Needs work

Thanks for the patch!

+++ b/core/modules/media/src/Entity/Media.php
@@ -454,6 +454,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+      ->setRequired(TRUE)

This is the right fix, but in the wrong place -- it should be in EntityOwnerTrait.

andregp’s picture

Assigned: Unassigned » andregp

Okay, sorry, I'll change it :)

andregp’s picture

So I made a patch with the fix on the right file and made a second patch that also removes the code mentioned on comment #7-#8 in case you want to commit this too.

The last submitted patch, 11: 3260173-11.patch, failed testing. View results

andregp’s picture

Now the patch is failing because of the NodeEditFormTest::checkVariousAuthoredByValues test.
In the code bellow (line 274-285) the test tries to make the "authored by" field empty.

    // Change the authored by field to an empty string, which should assign
    // authorship to the anonymous user (uid 0).
    $edit[$form_element_name] = '';
    $this->drupalGet('node/' . $node->id() . '/edit');
    $this->submitForm($edit, 'Save');
    $this->nodeStorage->resetCache([$node->id()]);
    $node = $this->nodeStorage->load($node->id());
    $uid = $node->getOwnerId();
    // Most SQL database drivers stringify fetches but entities are not
    // necessarily stored in a SQL database. At the same time, NULL/FALSE/""
    // won't do.
    $this->assertTrue($uid === 0 || $uid === '0', 'Node authored by anonymous user.');
andregp’s picture

Status: Needs review » Needs work

@joachim @tstoeckler What do you believe is the best approach then? Should we change/remove this part of the test or try another solution?

joachim’s picture

    // Change the authored by field to an empty string, which should assign
    // authorship to the anonymous user (uid 0).

It looks like the intention is that you can clear the field to change the owner of a node from a specific user to anonymous. That's a useful feature.

In which case, we should keep the code in preSave(), and the test.

(And as a follow-up, we should make this behaviour consistent on all user reference fields.)

andregp’s picture

So I changed the preSave() code in the Media entity to keep consistent with the Node entity. Now you can save the media entities with no Author and it will default to Anonymous.

Is there any other user reference fields I am missing?

andregp’s picture

Status: Needs work » Needs review
joachim’s picture

Status: Needs review » Reviewed & tested by the community

@andregp patch #16 looks good, but it belongs over at #3260175: Saving media entity without an owner crashes. (Though it would be nice if it could be handled at the field level, so all entity types benefit from it.)

I think here, patch #6 is good to go. Should it be re-uploaded so it's the most recent patch, to make it clear to commiters?

andregp’s picture

Oh sorry, I may have got confused.

andregp’s picture

Re-upload of patch #6
It seems to be the best approach for this issue since changing the EntityOwnerTrait.php to have Authoring required triggers the NodeEditFormTest::checkVariousAuthoredByValues test, which tests if the entity receives a Anonymous author when its author is omitted.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 20: 3260173-6.patch, failed testing. View results

andregp’s picture

Status: Needs work » Needs review

The failed test was layout_builder\FunctionalJavascript\ContentPreviewToggleTest::testContentPreviewToggle. Seems unrelated to this issue so sending back to needs review.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

nod_’s picture

Status: Needs review » Reviewed & tested by the community

Not sure the patch is addressing the issue title directly.

Patch prevents drupal from crashing when a media entity doesn't have an author assigned

catch’s picture

Title: EntityOwnerTrait does not define the owner field as required, but the DB field is NOT NULL » Media should set the owner field to anonymous if no explicit owner is set
catch’s picture

Version: 10.1.x-dev » 9.4.x-dev
Status: Reviewed & tested by the community » Fixed

Committed/pushed to 10.1.x, cherry-picked back through to 9.4.x, thanks!

  • catch committed a2d3222 on 10.0.x
    Issue #3260173 by andregp, joachim, tstoeckler, nod_: Media should set...
  • catch committed 7aa35b3 on 10.1.x
    Issue #3260173 by andregp, joachim, tstoeckler, nod_: Media should set...
  • catch committed 49f5e5d on 9.4.x
    Issue #3260173 by andregp, joachim, tstoeckler, nod_: Media should set...
  • catch committed 24edc58 on 9.5.x
    Issue #3260173 by andregp, joachim, tstoeckler, nod_: Media should set...
catch’s picture

joachim’s picture

> Not sure the patch is addressing the issue title directly.

It's not. The patch was uploaded to the WRONG issue. It should have been for #3260175: Saving media entity without an owner crashes.

This issue was MEANT to be fixing the underlying problem so entity classes don't need this fiddly workaround. It was MEANT to be titled 'EntityOwnerTrait does not define the owner field as required, but the DB field is NOT NULL'.

So now the commit log and the issues are all messed up. What now?

joachim’s picture

Status: Fixed » Needs work

The commits need to be reverted --

1. they're for the wrong issue!
2. The change creates a UX discrepancy, because the field should NOT be required. With nodes, you submit the field EMPTY to mean 'anon user'. If the field on Media is required, then that's not possible. The patch on the other issue DOES make that possible

  • catch committed a3d87f7 on 10.0.x
    Revert "Issue #3260173 by andregp, joachim, tstoeckler, nod_: Media...
  • catch committed 6415ee9 on 10.1.x
    Revert "Issue #3260173 by andregp, joachim, tstoeckler, nod_: Media...
  • catch committed b417810 on 9.4.x
    Revert "Issue #3260173 by andregp, joachim, tstoeckler, nod_: Media...
  • catch committed 40226b6 on 9.5.x
    Revert "Issue #3260173 by andregp, joachim, tstoeckler, nod_: Media...
catch’s picture

Title: Media should set the owner field to anonymous if no explicit owner is set » EntityOwnerTrait does not define the owner field as required, but the DB field is NOT NULL
Issue tags: +Needs issue summary update

The commit isn't for the wrong issue, it was patch #20 from this issue, which was based on patch #6, which was RTBCed by @joachim in #18 and again by nod_ in #25. However it was committed with the wrong title, and I think the commit on #3260175: Saving media entity without an owner crashes brings things into line with Node, if not for other modules using the trait.

I think there's a potential field-level fix so that node and media don't have to have the code from #3260175: Saving media entity without an owner crashes, but that felt like follow-up material to me, possibly it should be a new issue and we should just close this one?

joachim’s picture

> I think the commit on #3260175: Saving media entity without an owner crashes things into line with Node, if not for other modules using the trait.

Yes, that's correct. That issue brings Media in line with Node: a slightly hacky fix in the Entity class. But it's not fixing it for contrib or custom code using the trait, and it's fiddly that this is needed.

> I think there's a potential field-level fix so that node and media don't have to have the code from

Yes, that was my thinking too. And that should probably be a separate issue.

However, this issue is specifically about this:

> The DB fields created for the owner field have NOT NULL set on the DB column. However, the entity field definition is not set to required.

To rephrase: EntityOwnerTrait defines a base field that is NOT set to required. Normally, if a field isn't required, the DB column is *NOT* set to 'NOT NULL', because it's not required, so it's allowed to be NULL.

But the owner field from EntityOwnerTrait gets its DB column set to 'NOT NULL'.

That seems wrong, and I can't figure out how it's happening.

catch’s picture

joachim’s picture

Status: Needs work » Closed (duplicate)

Ah!

In which case, yes, this issue is a duplicate of (or rather a specific case of) #2841291: Fix NOT NULL handling in the entity storage and 'primary key' changes when updating the storage definition of an identifier field.

And #3260175: Saving media entity without an owner crashes needs a follow-up for a more comprehensive fix.