Displaying referenced unpublished content

If you reference unpublished nodes, they will not be visible to anonymous users. If you want to show an unlinked title instead of hiding it, you there are several methods you can use.

First, let's look at the field formatter, to see how it is hiding the unpublished content.

The default Entity Reference field formatter is EntityReferenceLabelFormatter. It uses checkAccess() on each item:

protected function checkAccess(EntityInterface $entity) {
  return $entity->access('view label', NULL, TRUE);
}

checkAccess() calls $entity->access(), which uses the entity's EntityAccessControlHandler (see access() in ContentEntityBase).

In EntityAccessControlHandler, we find this helpful info:

/**
 * Allows to grant access to just the labels.
 *
 * By default, the "view label" operation falls back to "view". Set this to
 * TRUE to allow returning different access when just listing entity labels.
 *
 * @var bool
 */
protected $viewLabelOperation = FALSE;

Node uses NodeAccessControlHandler, which does not use $viewLabelOperation. If it did, we would see the unpublished reference labels.

Unfortunately there isn't a hook to override this - I tried hook_entity_access(), but that doesn't recognize the "view label" operation since it is not implemented in the access handler. Instead we can create a class that extends NodeAccessControlHandler, and set Node to use that class.

/**
 * Extends NodeAccessControlHandler to add 'view label' access check.
 */
class NodeUnpublishedViewLabelAccessControlHandler extends \Drupal\node\NodeAccessControlHandler {

  /**
   * {@inheritdoc}
   */
  protected $viewLabelOperation = TRUE;

  /**
   * {@inheritdoc}
   */
  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
    switch ($operation) {
      case 'view label':
        return AccessResult::allowedIfHasPermission($account, 'access content');
      default:
        return parent::checkAccess($entity, $operation, $account);
    }
  }
}

I found this code in ParagraphsTypeAccessControlHandler while searching for "view label" to see how other modules were using it. The only change I made was to extend NodeAccessControlHandler.

Now we need to tell Node to use this new class, using hook_entity_type_alter().

function treesearch_data_entity_type_alter(array &$entity_types) {
  /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
  $entity_types['node']->setHandlerClass('access', 'Drupal\treesearch_data\NodeUnpublishedViewLabelAccessControlHandler');
}

Now all nodes will allow access to the title of unpublished nodes. An anonymous user will now see referenced content as a linked label, even if it's unpublished.

The problem now is that the title should not be linked if it's unpublished. If a user clicks the link, they will see an access denied page, so let's just show an unlinked title for those.

We will have to create a field formatter that extends EntityReferenceLabelFormatter to override this.

/**
 * Plugin implementation of the 'entity reference label' formatter.
 *
 * @FieldFormatter(
 *   id = "entity_reference_unpublished_label",
 *   label = @Translation("Label unpublished"),
 *   description = @Translation("Display the label of the referenced entities, including unpublished."),
 *   field_types = {
 *     "entity_reference"
 *   }
 * )
 */
class EntityReferenceUnpublishedLabelFormatter extends EntityReferenceLabelFormatter {

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode) {
    foreach ($items->referencedEntities() as $item) {
      if ($item->isPublished()) {
        $elements[] = $item->toLink()->toRenderable();
      }
      else {
        $elements[] = ['#markup' => $item->label()];
      }
    }

    return $elements;
  }
}

Make sure your module is enabled and clear the cache. You can now select the new formatter for Entity Reference fields.

 

As a bonus here, you can actually skip all the access check and control handler setup on view label. Simply adjust the accessCheck function in this new formatter class. You can filter by entity type here too if you want.

/**
 * {@inheritdoc}
 */
protected function checkAccess(EntityInterface $entity) {
  return $entity->access('view label', NULL, TRUE);
}