a few more base modules

This commit is contained in:
Bachir Soussi Chiadmi
2016-09-06 16:05:07 +02:00
parent c6cff46234
commit 027aa99b32
455 changed files with 43606 additions and 0 deletions
@@ -0,0 +1,159 @@
<?php
namespace Drupal\entity\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Route;
/**
* Checks access to a entity revision.
*/
class EntityRevisionRouteAccessChecker implements AccessInterface {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Stores calculated access check results.
*
* @var array
*/
protected $accessCache = array();
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Creates a new EntityRevisionRouteAccessChecker instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, RequestStack $request_stack) {
$this->entityTypeManager = $entity_type_manager;
$this->requestStack = $request_stack;
}
/**
* {@inheritdoc}
*/
public function access(Route $route, AccountInterface $account, Request $request = NULL) {
if (empty($request)) {
$request = $this->requestStack->getCurrentRequest();
}
$operation = $route->getRequirement('_entity_access_revision');
list(, $operation) = explode('.', $operation, 2);
if ($operation === 'list') {
$_entity = $request->attributes->get('_entity', $request->attributes->get($route->getOption('entity_type_id')));
return AccessResult::allowedIf($this->checkAccess($_entity, $account, $operation))->cachePerPermissions();
}
else {
$_entity_revision = $request->attributes->get('_entity_revision');
return AccessResult::allowedIf($_entity_revision && $this->checkAccess($_entity_revision, $account, $operation))->cachePerPermissions();
}
}
protected function checkAccess(ContentEntityInterface $entity, AccountInterface $account, $operation = 'view') {
$entity_type = $entity->getEntityType();
$entity_type_id = $entity->getEntityTypeId();
$entity_access = $this->entityTypeManager->getAccessControlHandler($entity_type_id);
/** @var \Drupal\Core\Entity\EntityStorageInterface $entity_storage */
$entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
$map = [
'view' => "view all $entity_type_id revisions",
'list' => "view all $entity_type_id revisions",
'update' => "revert all $entity_type_id revisions",
'delete' => "delete all $entity_type_id revisions",
];
$bundle = $entity->bundle();
$type_map = [
'view' => "view $entity_type_id $bundle revisions",
'list' => "view $entity_type_id $bundle revisions",
'update' => "revert $entity_type_id $bundle revisions",
'delete' => "delete $entity_type_id $bundle revisions",
];
if (!$entity || !isset($map[$operation]) || !isset($type_map[$operation])) {
// If there was no node to check against, or the $op was not one of the
// supported ones, we return access denied.
return FALSE;
}
// Statically cache access by revision ID, language code, user account ID,
// and operation.
$langcode = $entity->language()->getId();
$cid = $entity->getRevisionId() . ':' . $langcode . ':' . $account->id() . ':' . $operation;
if (!isset($this->accessCache[$cid])) {
// Perform basic permission checks first.
if (!$account->hasPermission($map[$operation]) && !$account->hasPermission($type_map[$operation]) && !$account->hasPermission('administer nodes')) {
$this->accessCache[$cid] = FALSE;
return FALSE;
}
if (($admin_permission = $entity_type->getAdminPermission()) && $account->hasPermission($admin_permission)) {
$this->accessCache[$cid] = TRUE;
}
else {
// First check the access to the default revision and finally, if the
// node passed in is not the default revision then access to that, too.
$this->accessCache[$cid] = $entity_access->access($entity_storage->load($entity->id()), $operation, $account) && ($entity->isDefaultRevision() || $entity_access->access($entity, $operation, $account));
}
}
return $this->accessCache[$cid];
}
/**
* Counts the number of revisions in the default language.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity.
* @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
* The entity storage.
*
* @return int
* The number of revisions in the default language.
*/
protected function countDefaultLanguageRevisions(ContentEntityInterface $entity, EntityStorageInterface $entity_storage) {
$entity_type = $entity->getEntityType();
$count = $entity_storage->getQuery()
->allRevisions()
->condition($entity_type->getKey('id'), $entity->id())
->condition($entity_type->getKey('default_langcode'), 1)
->count()
->execute();
return $count;
}
/**
* Resets the access cache.
*
* @return $this
*/
public function resetAccessCache() {
$this->accessCache = [];
return $this;
}
}
@@ -0,0 +1,200 @@
<?php
namespace Drupal\entity\Controller;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines a trait for common revision UI functionality.
*/
trait RevisionControllerTrait {
/**
* Returns the entity type manager.
*
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
*/
abstract protected function entityTypeManager();
/**
* Returns the langauge manager.
*
* @return \Drupal\Core\Language\LanguageManagerInterface
*/
public abstract function languageManager();
/**
* Determines if the user has permission to revert revisions.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check revert access for.
*
* @return bool
* TRUE if the user has revert access.
*/
abstract protected function hasRevertRevisionAccess(EntityInterface $entity);
/**
* Determines if the user has permission to delete revisions.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check delete revision access for.
*
* @return bool
* TRUE if the user has delete revision access.
*/
abstract protected function hasDeleteRevisionAccess(EntityInterface $entity);
/**
* Builds a link to revert an entity revision.
*
* @param \Drupal\Core\Entity\EntityInterface $entity_revision
* The entity to build a revert revision link for.
*
* @return array
* A link render array.
*
*/
abstract protected function buildRevertRevisionLink(EntityInterface $entity_revision);
/**
* Builds a link to delete an entity revision.
*
* @param \Drupal\Core\Entity\EntityInterface $entity_revision
* The entity to build a delete revision link for.
*
* @return array
* A link render array.
*/
abstract protected function buildDeleteRevisionLink(EntityInterface $entity_revision);
/**
* Returns a string providing details of the revision.
*
* E.g. Node describes its revisions using {date} by {username}. For the
* non-current revision, it also provides a link to view that revision.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $revision
* The entity revision.
* @param bool $is_current
* TRUE if the revision is the current revision.
*
* @return string
* Returns a string to provide the details of the revision.
*/
abstract protected function getRevisionDescription(ContentEntityInterface $revision, $is_current = FALSE);
/**
* Loads all revision IDs of an entity sorted by revision ID descending.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity.
*
* @return mixed[]
*/
protected function revisionIds(ContentEntityInterface $entity) {
$entity_type = $entity->getEntityType();
$result = $this->entityTypeManager()->getStorage($entity_type->id())->getQuery()
->allRevisions()
->condition($entity_type->getKey('id'), $entity->id())
->sort($entity_type->getKey('revision'), 'DESC')
->execute();
return array_keys($result);
}
/**
* Generates an overview table of older revisions of an entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* An entity object.
*
* @return array
* A render array.
*/
protected function revisionOverview(ContentEntityInterface $entity) {
$langcode = $this->languageManager()
->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)
->getId();
$entity_storage = $this->entityTypeManager()
->getStorage($entity->getEntityTypeId());
$header = [$this->t('Revision'), $this->t('Operations')];
$rows = [];
$revision_ids = $this->revisionIds($entity);
// @todo Expand the entity storage to load multiple revisions.
$entity_revisions = array_combine($revision_ids, array_map(function($vid) use ($entity_storage) {
return $entity_storage->loadRevision($vid);
}, $revision_ids));
foreach ($entity_revisions as $revision) {
$row = [];
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)
->isRevisionTranslationAffected()
) {
$row[] = $this->getRevisionDescription($revision, $revision->isDefaultRevision());
if ($revision->isDefaultRevision()) {
$row[] = [
'data' => [
'#prefix' => '<em>',
'#markup' => $this->t('Current revision'),
'#suffix' => '</em>',
],
];
foreach ($row as &$current) {
$current['class'] = ['revision-current'];
}
}
else {
$links = $this->getOperationLinks($revision);
$row[] = [
'data' => [
'#type' => 'operations',
'#links' => $links,
],
];
}
}
$rows[] = $row;
}
$build[$entity->getEntityTypeId() . '_revisions_table'] = [
'#theme' => 'table',
'#rows' => $rows,
'#header' => $header,
];
// We have no clue about caching yet.
$build['#cache']['max-age'] = 0;
return $build;
}
/**
* Get the links of the operations for an entity revision.
*
* @param \Drupal\Core\Entity\EntityInterface $entity_revision
* The entity to build the revision links for.
*
* @return array
* The operation links.
*/
protected function getOperationLinks(EntityInterface $entity_revision) {
$links = [];
if ($this->hasRevertRevisionAccess($entity_revision)) {
$links['revert'] = $this->buildRevertRevisionLink($entity_revision);
}
if ($this->hasDeleteRevisionAccess($entity_revision)) {
$links['delete'] = $this->buildDeleteRevisionLink($entity_revision);
}
return array_filter($links);
}
}
@@ -0,0 +1,161 @@
<?php
namespace Drupal\entity\Controller;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\user\EntityOwnerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a controller which shows the revision history.
*
* This controller leverages the revision controller trait, which is agnostic to
* any entity type, by using \Drupal\Core\Entity\RevisionLogInterface.
*/
class RevisionOverviewController extends ControllerBase {
use RevisionControllerTrait;
/**
* The date formatter.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Creates a new RevisionOverviewController instance.
*
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter.
*/
public function __construct(DateFormatterInterface $date_formatter, RendererInterface $renderer) {
$this->dateFormatter = $date_formatter;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('date.formatter'), $container->get('renderer'));
}
/**
* {@inheritdoc}
*/
protected function hasDeleteRevisionAccess(EntityInterface $entity) {
return $this->currentUser()->hasPermission("delete all {$entity->id()} revisions");
}
/**
* {@inheritdoc}
*/
protected function buildRevertRevisionLink(EntityInterface $entity_revision) {
if ($entity_revision->hasLinkTemplate('revision-revert-form')) {
return [
'title' => t('Revert'),
'url' => $entity_revision->toUrl('revision-revert-form'),
];
}
}
/**
* {@inheritdoc}
*/
protected function buildDeleteRevisionLink(EntityInterface $entity_revision) {
if ($entity_revision->hasLinkTemplate('revision-delete-form')) {
return [
'title' => t('Delete'),
'url' => $entity_revision->toUrl('revision-delete-form'),
];
}
}
/**
* Generates an overview table of older revisions of an entity.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*
* @return array
* A render array.
*/
public function revisionOverviewController(RouteMatchInterface $route_match) {
return $this->revisionOverview($route_match->getParameter($route_match->getRouteObject()->getOption('entity_type_id')));
}
/**
* {@inheritdoc}
*/
protected function getRevisionDescription(ContentEntityInterface $revision, $is_default = FALSE) {
/** @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\user\EntityOwnerInterface|\Drupal\Core\Entity\RevisionLogInterface $revision */
if ($revision instanceof RevisionLogInterface) {
// Use revision link to link to revisions that are not active.
$date = $this->dateFormatter->format($revision->getRevisionCreationTime(), 'short');
$link = $revision->toLink($date, 'revision');
// @todo: Simplify this when https://www.drupal.org/node/2334319 lands.
$username = [
'#theme' => 'username',
'#account' => $revision->getRevisionUser(),
];
$username = $this->renderer->render($username);
}
else {
$link = $revision->toLink($revision->label(), 'revision');
$username = '';
}
$markup = '';
if ($revision instanceof RevisionLogInterface) {
$markup = $revision->getRevisionLogMessage();
}
if ($username) {
$template = '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}';
}
else {
$template = '{% trans %} {{ date }} {% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}';
}
$column = [
'data' => [
'#type' => 'inline_template',
'#template' => $template,
'#context' => [
'date' => $link->toString(),
'username' => $username,
'message' => ['#markup' => $markup, '#allowed_tags' => Xss::getHtmlTagList()],
],
],
];
return $column;
}
/**
* {@inheritdoc}
*/
protected function hasRevertRevisionAccess(EntityInterface $entity) {
return AccessResult::allowedIfHasPermission($this->currentUser(), "revert all {$entity->getEntityTypeId()} revisions")->orIf(
AccessResult::allowedIfHasPermission($this->currentUser(), "revert {$entity->bundle()} {$entity->getEntityTypeId()} revisions")
);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Drupal\entity\Entity;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
interface RevisionableEntityBundleInterface extends ConfigEntityInterface {
/**
* Returns whether a new revision should be created by default.
*
* @return bool
* TRUE if a new revision should be created by default.
*/
public function shouldCreateNewRevision();
}
@@ -0,0 +1,37 @@
<?php
namespace Drupal\entity;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityViewBuilder as CoreEntityViewBuilder;
/**
* Provides a entity view builder with contextual links support
*/
class EntityViewBuilder extends CoreEntityViewBuilder {
/**
* {@inheritdoc}
*/
protected function alterBuild(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
$entity_type_id = $entity->getEntityTypeId();
if (($entity instanceof ContentEntityInterface && $entity->isDefaultRevision()) || !$entity->getEntityType()->isRevisionable()) {
$build['#contextual_links'][$entity_type_id] = [
'route_parameters' => [
$entity_type_id => $entity->id()
],
];
}
else {
$build['#contextual_links'][$entity_type_id . '_revision'] = [
'route_parameters' => [
$entity_type_id => $entity->id(),
$entity_type_id . '_revision' => $entity->getRevisionId(),
],
];
}
}
}
@@ -0,0 +1,226 @@
<?php
namespace Drupal\entity\Form;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Url;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\PrivateTempStoreFactory;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides an entities deletion confirmation form.
*/
class DeleteMultiple extends ConfirmFormBase {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The tempstore.
*
* @var \Drupal\user\SharedTempStore
*/
protected $tempStore;
/**
* The entity type id.
*
* @var string
*/
protected $entityTypeId;
/**
* The selection, in the entity_id => langcodes format.
*
* @var array
*/
protected $selection = [];
/**
* Constructs a new DeleteMultiple object.
*
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
*/
public function __construct(AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory) {
$this->currentUser = $current_user;
$this->entityTypeManager = $entity_type_manager;
$this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('current_user'),
$container->get('entity_type.manager'),
$container->get('user.private_tempstore')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'entity_delete_multiple_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->formatPlural(count($this->selection), 'Are you sure you want to delete this item?', 'Are you sure you want to delete these items?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.' . $this->entityTypeId . '.collection');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*
* @param string $entity_type_id
* The entity type id.
*/
public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL) {
$this->entityTypeId = $entity_type_id;
$this->selection = $this->tempStore->get($this->currentUser->id());
if (empty($this->entityTypeId) || empty($this->selection)) {
return new RedirectResponse($this->getCancelUrl()->setAbsolute()->toString());
}
$storage = $this->entityTypeManager->getStorage($this->entityTypeId);
/** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
$entities = $storage->loadMultiple(array_keys($this->selection));
$items = [];
foreach ($this->selection as $id => $langcodes) {
foreach ($langcodes as $langcode) {
$entity = $entities[$id]->getTranslation($langcode);
$key = $id . ':' . $langcode;
$default_key = $id . ':' . $entity->getUntranslated()->language()->getId();
// If we have a translated entity we build a nested list of translations
// that will be deleted.
$languages = $entity->getTranslationLanguages();
if (count($languages) > 1 && $entity->isDefaultTranslation()) {
$names = [];
foreach ($languages as $translation_langcode => $language) {
$names[] = $language->getName();
unset($items[$id . ':' . $translation_langcode]);
}
$items[$default_key] = [
'label' => [
'#markup' => $this->t('@label (Original translation) - <em>The following translations will be deleted:</em>', ['@label' => $entity->label()]),
],
'deleted_translations' => [
'#theme' => 'item_list',
'#items' => $names,
],
];
}
elseif (!isset($items[$default_key])) {
$items[$key] = $entity->label();
}
}
}
$form['entities'] = [
'#theme' => 'item_list',
'#items' => $items,
];
$form = parent::buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$total_count = 0;
$delete_entities = [];
$delete_translations = [];
$storage = $this->entityTypeManager->getStorage($this->entityTypeId);
/** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
$entities = $storage->loadMultiple(array_keys($this->selection));
foreach ($this->selection as $id => $langcodes) {
foreach ($langcodes as $langcode) {
$entity = $entities[$id]->getTranslation($langcode);
if ($entity->isDefaultTranslation()) {
$delete_entities[$id] = $entity;
unset($delete_translations[$id]);
$total_count += count($entity->getTranslationLanguages());
}
elseif (!isset($delete_entities[$id])) {
$delete_translations[$id][] = $entity;
}
}
}
if ($delete_entities) {
$storage->delete($delete_entities);
$this->logger('content')->notice('Deleted @count @entity_type items.', [
'@count' => count($delete_entities),
'@entity_type' => $this->entityTypeId,
]);
}
if ($delete_translations) {
$count = 0;
/** @var \Drupal\Core\Entity\ContentEntityInterface[][] $delete_translations */
foreach ($delete_translations as $id => $translations) {
$entity = $entities[$id]->getUntranslated();
foreach ($translations as $translation) {
$entity->removeTranslation($translation->language()->getId());
}
$entity->save();
$count += count($translations);
}
if ($count) {
$total_count += $count;
$this->logger('content')->notice('Deleted @count @entity_type translations.', [
'@count' => $count,
'@entity_type' => $this->entityTypeId,
]);
}
}
if ($total_count) {
drupal_set_message($this->formatPlural($total_count, 'Deleted 1 item.', 'Deleted @count items.'));
}
$this->tempStore->delete($this->currentUser->id());
$form_state->setRedirectUrl($this->getCancelUrl());
}
}
@@ -0,0 +1,167 @@
<?php
namespace Drupal\entity\Form;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\RevisionLogInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
class RevisionRevertForm extends ConfirmFormBase {
/**
* The entity revision.
*
* @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\RevisionLogInterface
*/
protected $revision;
/**
* The date formatter.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The entity bundle information.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInformation;
/**
* Creates a new RevisionRevertForm instance.
*
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_information
* The bundle information.
*/
public function __construct(DateFormatterInterface $date_formatter, EntityTypeBundleInfoInterface $bundle_information) {
$this->dateFormatter = $date_formatter;
$this->bundleInformation = $bundle_information;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('date.formatter'),
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'entity_revision_revert_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
if ($this->revision instanceof RevisionLogInterface) {
return $this->t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]);
}
return $this->t('Are you sure you want to revert the revision?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
if ($this->revision->getEntityType()->hasLinkTemplate('version-history')) {
return $this->revision->toUrl('version-history');
}
return $this->revision->toUrl();
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return t('Revert');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $_entity_revision = NULL, Request $request = NULL) {
$this->revision = $_entity_revision;
$form = parent::buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// The revision timestamp will be updated when the revision is saved. Keep
// the original one for the confirmation message.
$this->revision = $this->prepareRevision($this->revision);
if ($this->revision instanceof RevisionLogInterface) {
$original_revision_timestamp = $this->revision->getRevisionCreationTime();
$this->revision->setRevisionLogMessage($this->t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)]));
drupal_set_message(t('@type %title has been reverted to the revision from %revision-date.', ['@type' => $this->getBundleLabel($this->revision), '%title' => $this->revision->label(), '%revision-date' => $this->dateFormatter->format($original_revision_timestamp)]));
}
else {
drupal_set_message(t('@type %title has been reverted', ['@type' => $this->getBundleLabel($this->revision), '%title' => $this->revision->label()]));
}
$this->revision->save();
$this->logger('content')->notice('@type: reverted %title revision %revision.', ['@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]);
$form_state->setRedirect(
"entity.{$this->revision->getEntityTypeId()}.version_history",
[$this->revision->getEntityTypeId() => $this->revision->id()]
);
}
/**
* Prepares a revision to be reverted.
*
* @param \Drupal\Core\Entity\RevisionableInterface $revision
* The revision to be reverted.
*
* @return \Drupal\Core\Entity\RevisionableInterface
* The prepared revision ready to be stored.
*/
protected function prepareRevision(RevisionableInterface $revision) {
$revision->setNewRevision();
$revision->isDefaultRevision(TRUE);
return $revision;
}
/**
* Returns a bundle label.
*
* @param \Drupal\Core\Entity\RevisionableInterface $revision
* The entity revision.
*
* @return string
*/
protected function getBundleLabel(RevisionableInterface $revision) {
/** @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface $revision */
$bundle_info = $this->bundleInformation->getBundleInfo($revision->getEntityTypeId());
return $bundle_info[$revision->bundle()]['label'];
}
}
@@ -0,0 +1,159 @@
<?php
namespace Drupal\entity\Form;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\entity\Entity\RevisionableEntityBundleInterface;
/**
* Extends the base entity form with revision support in the UI.
*/
class RevisionableContentEntityForm extends ContentEntityForm {
/**
* The entity being used by this form.
*
* @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface|\Drupal\entity\Revision\EntityRevisionLogInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function prepareEntity() {
parent::prepareEntity();
$bundle_entity = $this->getBundleEntity();
// Set up default values, if required.
if (!$this->entity->isNew()) {
$this->entity->setRevisionLogMessage(NULL);
}
if ($bundle_entity instanceof RevisionableEntityBundleInterface) {
// Always use the default revision setting.
$this->entity->setNewRevision($bundle_entity && $bundle_entity->shouldCreateNewRevision());
}
}
/**
* Returns the bundle entity of the entity, or NULL if there is none.
*
* @return \Drupal\Core\Entity\EntityInterface|null
*/
protected function getBundleEntity() {
if ($bundle_key = $this->entity->getEntityType()->getKey('bundle')) {
return $this->entity->{$bundle_key}->referencedEntities()[0];
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$entity_type = $this->entity->getEntityType();
$bundle_entity = $this->getBundleEntity();
$account = $this->currentUser();
if ($this->operation == 'edit') {
$form['#title'] = $this->t('Edit %bundle_label @label', [
'%bundle_label' => $bundle_entity ? $bundle_entity->label() : '',
'@label' => $this->entity->label(),
]);
}
$form['advanced'] = [
'#type' => 'vertical_tabs',
'#weight' => 99,
];
// Add a log field if the "Create new revision" option is checked, or if the
// current user has the ability to check that option.
// @todo Could we autogenerate this form by using some widget on the
// revision info field.
$form['revision_information'] = [
'#type' => 'details',
'#title' => $this->t('Revision information'),
// Open by default when "Create new revision" is checked.
'#open' => $this->entity->isNewRevision(),
'#group' => 'advanced',
'#weight' => 20,
'#access' => $this->entity->isNewRevision() || $account->hasPermission($entity_type->get('admin_permission')),
];
$form['revision_information']['revision'] = [
'#type' => 'checkbox',
'#title' => $this->t('Create new revision'),
'#default_value' => $this->entity->isNewRevision(),
'#access' => $account->hasPermission($entity_type->get('admin_permission')),
];
// Check the revision log checkbox when the log textarea is filled in.
// This must not happen if "Create new revision" is enabled by default,
// since the state would auto-disable the checkbox otherwise.
if (!$this->entity->isNewRevision()) {
$form['revision_information']['revision']['#states'] = [
'checked' => [
'textarea[name="revision_log"]' => ['empty' => FALSE],
],
];
}
$form['revision_information']['revision_log'] = [
'#type' => 'textarea',
'#title' => $this->t('Revision log message'),
'#rows' => 4,
'#default_value' => $this->entity->getRevisionLogMessage(),
'#description' => $this->t('Briefly describe the changes you have made.'),
];
return parent::form($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
// Save as a new revision if requested to do so.
if (!$form_state->isValueEmpty('revision')) {
$this->entity->setNewRevision();
}
$insert = $this->entity->isNew();
$this->entity->save();
$context = ['@type' => $this->entity->bundle(), '%info' => $this->entity->label()];
$logger = $this->logger($this->entity->id());
$bundle_entity = $this->getBundleEntity();
$t_args = ['@type' => $bundle_entity ? $bundle_entity->label() : 'None', '%info' => $this->entity->label()];
if ($insert) {
$logger->notice('@type: added %info.', $context);
drupal_set_message($this->t('@type %info has been created.', $t_args));
}
else {
$logger->notice('@type: updated %info.', $context);
drupal_set_message($this->t('@type %info has been updated.', $t_args));
}
if ($this->entity->id()) {
$form_state->setValue('id', $this->entity->id());
$form_state->set('id', $this->entity->id());
if ($this->entity->getEntityType()->hasLinkTemplate('collection')) {
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
}
else {
$form_state->setRedirectUrl($this->entity->toUrl('canonical'));
}
}
else {
// In the unlikely case something went wrong on save, the entity will be
// rebuilt and entity form redisplayed.
drupal_set_message($this->t('The entity could not be saved.'), 'error');
$form_state->setRebuild();
}
}
}
@@ -0,0 +1,97 @@
<?php
namespace Drupal\entity\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Redirects to an entity deletion form.
*
* @Action(
* id = "entity_delete_action",
* label = @Translation("Delete entity"),
* deriver = "Drupal\entity\Plugin\Action\Derivative\DeleteActionDeriver",
* )
*/
class DeleteAction extends ActionBase implements ContainerFactoryPluginInterface {
/**
* The tempstore object.
*
* @var \Drupal\user\SharedTempStore
*/
protected $tempStore;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new DeleteAction object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param AccountInterface $current_user
* Current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
$this->currentUser = $current_user;
$this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm');
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('user.private_tempstore'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function executeMultiple(array $entities) {
/** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
$selection = [];
foreach ($entities as $entity) {
$langcode = $entity->language()->getId();
$selection[$entity->id()][$langcode] = $langcode;
}
$this->tempStore->set($this->currentUser->id(), $selection);
}
/**
* {@inheritdoc}
*/
public function execute($object = NULL) {
$this->executeMultiple([$object]);
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
return $object->access('delete', $account, $return_as_object);
}
}
@@ -0,0 +1,78 @@
<?php
namespace Drupal\entity\Plugin\Action\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a delete action for each content entity type.
*/
class DeleteActionDeriver extends DeriverBase implements ContainerDeriverInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new DeleteActionDeriver object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static($container->get('entity_type.manager'));
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
if (empty($this->derivatives)) {
$definitions = [];
foreach ($this->getParticipatingEntityTypes() as $entity_type_id => $entity_type) {
$definition = $base_plugin_definition;
$definition['label'] = t('Delete @entity_type', ['@entity_type' => $entity_type->getLowercaseLabel()]);
$definition['type'] = $entity_type_id;
$definition['confirm_form_route_name'] = 'entity.' . $entity_type_id . '.delete_multiple_form';
$definitions[$entity_type_id] = $definition;
}
$this->derivatives = $definitions;
}
return parent::getDerivativeDefinitions($base_plugin_definition);
}
/**
* Gets a list of participating entity types.
*
* The list consists of all content entity types with a delete-multiple-form
* link template.
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
* The participating entity types, keyed by entity type id.
*/
protected function getParticipatingEntityTypes() {
$entity_types = $this->entityTypeManager->getDefinitions();
$entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) {
return $entity_type->isSubclassOf(ContentEntityInterface::class) && $entity_type->hasLinkTemplate('delete-multiple-form');
});
return $entity_types;
}
}
@@ -0,0 +1,67 @@
<?php
namespace Drupal\entity\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides local tasks for the revision overview.
*/
class RevisionsOverviewDeriver extends DeriverBase implements ContainerDeriverInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates a new RevisionsOverviewDeriver instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(\Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager) {
$this->entityTypeManager = $entityTypeManager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$exclude = ['node'];
$this->derivatives = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if (in_array($entity_type_id, $exclude)) {
continue;
}
if (!$entity_type->hasLinkTemplate('version-history')) {
continue;
}
$this->derivatives[$entity_type_id] = [
'route_name' => "entity.$entity_type_id.version_history",
'title' => 'Revisions',
'base_route' => "entity.$entity_type_id.canonical",
'weight' => 20,
] + $base_plugin_definition;
}
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}
@@ -0,0 +1,30 @@
<?php
namespace Drupal\entity\Revision;
use Drupal\Core\Entity\RevisionableContentEntityBase as BaseRevisionableContentEntityBase;
use Drupal\Core\Entity\ContentEntityBase;
/**
* Improves the url route handling of core's revisionable content entity base.
*/
abstract class RevisionableContentEntityBase extends BaseRevisionableContentEntityBase {
/**
* {@inheritdoc}
*/
protected function urlRouteParameters($rel) {
$uri_route_parameters = [];
if ($rel != 'collection') {
// The entity ID is needed as a route parameter.
$uri_route_parameters[$this->getEntityTypeId()] = $this->id();
}
if (strpos($this->getEntityType()->getLinkTemplate($rel), $this->getEntityTypeId() . '_revision') !== FALSE) {
$uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId();
}
return $uri_route_parameters;
}
}
@@ -0,0 +1,47 @@
<?php
namespace Drupal\entity\Routing;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides the HTML route for deleting multiple entities.
*/
class DeleteMultipleRouteProvider implements EntityRouteProviderInterface {
/**
* {@inheritdoc}
*/
public function getRoutes(EntityTypeInterface $entity_type) {
$routes = new RouteCollection();
if ($route = $this->deleteMultipleFormRoute($entity_type)) {
$routes->add('entity.' . $entity_type->id() . '.delete_multiple_form', $route);
}
return $routes;
}
/**
* Returns the delete multiple form route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function deleteMultipleFormRoute(EntityTypeInterface $entity_type) {
if ($entity_type->hasLinkTemplate('delete-multiple-form')) {
$route = new Route($entity_type->getLinkTemplate('delete-multiple-form'));
$route->setDefault('_form', '\Drupal\entity\Form\DeleteMultiple');
$route->setDefault('entity_type_id', $entity_type->id());
$route->setRequirement('_permission', $entity_type->getAdminPermission());
return $route;
}
}
}
@@ -0,0 +1,127 @@
<?php
namespace Drupal\entity\Routing;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides revision routes.
*/
class RevisionRouteProvider implements EntityRouteProviderInterface {
/**
* {@inheritdoc}
*/
public function getRoutes(EntityTypeInterface $entity_type) {
$collection = new RouteCollection();
$entity_type_id = $entity_type->id();
if ($view_route = $this->getRevisionViewRoute($entity_type)) {
$collection->add("entity.$entity_type_id.revision", $view_route);
}
if ($view_route = $this->getRevisionRevertRoute($entity_type)) {
$collection->add("entity.$entity_type_id.revision_revert_form", $view_route);
}
if ($view_route = $this->getRevisionHistoryRoute($entity_type)) {
$collection->add("entity.$entity_type_id.version_history", $view_route);
}
return $collection;
}
/**
* Gets the entity revision view route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getRevisionViewRoute(EntityTypeInterface $entity_type) {
if ($entity_type->hasLinkTemplate('revision')) {
$entity_type_id = $entity_type->id();
$route = new Route($entity_type->getLinkTemplate('revision'));
$route->addDefaults([
'_controller' => '\Drupal\Core\Entity\Controller\EntityViewController::viewRevision',
'_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title',
]);
$route->addRequirements([
'_entity_access_revision' => "$entity_type_id.view",
]);
$route->setOption('parameters', [
$entity_type->id() => [
'type' => 'entity:' . $entity_type->id(),
],
$entity_type->id() . '_revision' => [
'type' => 'entity_revision:' . $entity_type->id(),
],
]);
return $route;
}
}
/**
* Gets the entity revision revert route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getRevisionRevertRoute(EntityTypeInterface $entity_type) {
if ($entity_type->hasLinkTemplate('revision-revert-form')) {
$entity_type_id = $entity_type->id();
$route = new Route($entity_type->getLinkTemplate('revision-revert-form'));
$route->addDefaults([
'_form' => '\Drupal\entity\Form\RevisionRevertForm',
'title' => 'Revert to earlier revision',
]);
$route->addRequirements([
'_entity_access_revision' => "$entity_type_id.update",
]);
$route->setOption('parameters', [
$entity_type->id() => [
'type' => 'entity:' . $entity_type->id(),
],
$entity_type->id() . '_revision' => [
'type' => 'entity_revision:' . $entity_type->id(),
],
]);
return $route;
}
}
/**
* Gets the entity revision version history route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getRevisionHistoryRoute($entity_type) {
if ($entity_type->hasLinkTemplate('version-history')) {
$entity_type_id = $entity_type->id();
$route = new Route($entity_type->getLinkTemplate('version-history'));
$route->addDefaults([
'_controller' => '\Drupal\entity\Controller\RevisionOverviewController::revisionOverviewController',
'_title' => 'Revisions',
]);
$route->setRequirement('_entity_access_revision', "$entity_type_id.list");
$route->setOption('entity_type_id', $entity_type->id());
$route->setOption('parameters', [
$entity_type->id() => [
'type' => 'entity:' . $entity_type->id(),
],
]);
return $route;
}
}
}