first commit

This commit is contained in:
2020-06-08 23:57:36 +02:00
commit 6277454f7a
16057 changed files with 1715382 additions and 0 deletions
@@ -0,0 +1,74 @@
<?php
namespace Drupal\node\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeTypeInterface;
/**
* Determines access to for node add pages.
*
* @ingroup node_access
*/
class NodeAddAccessCheck implements AccessInterface {
use DeprecatedServicePropertyTrait;
/**
* {@inheritdoc}
*/
protected $deprecatedProperties = ['entityManager' => 'entity.manager'];
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a EntityCreateAccessCheck 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;
}
/**
* Checks access to the node add page for the node type.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
* @param \Drupal\node\NodeTypeInterface $node_type
* (optional) The node type. If not specified, access is allowed if there
* exists at least one node type for which the user may create a node.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(AccountInterface $account, NodeTypeInterface $node_type = NULL) {
$access_control_handler = $this->entityTypeManager->getAccessControlHandler('node');
// If checking whether a node of a particular type may be created.
if ($account->hasPermission('administer content types')) {
return AccessResult::allowed()->cachePerPermissions();
}
if ($node_type) {
return $access_control_handler->createAccess($node_type->id(), $account, [], TRUE);
}
// If checking whether a node of any type may be created.
foreach ($this->entityTypeManager->getStorage('node_type')->loadMultiple() as $node_type) {
if (($access = $access_control_handler->createAccess($node_type->id(), $account, [], TRUE)) && $access->isAllowed()) {
return $access;
}
}
// No opinion.
return AccessResult::neutral();
}
}
@@ -0,0 +1,62 @@
<?php
namespace Drupal\node\Access;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
/**
* Determines access to node previews.
*
* @ingroup node_access
*/
class NodePreviewAccessCheck implements AccessInterface {
use DeprecatedServicePropertyTrait;
/**
* {@inheritdoc}
*/
protected $deprecatedProperties = ['entityManager' => 'entity.manager'];
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a EntityCreateAccessCheck object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* Checks access to the node preview page.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
* @param \Drupal\node\NodeInterface $node_preview
* The node that is being previewed.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(AccountInterface $account, NodeInterface $node_preview) {
if ($node_preview->isNew()) {
$access_controller = $this->entityTypeManager->getAccessControlHandler('node');
return $access_controller->createAccess($node_preview->bundle(), $account, [], TRUE);
}
else {
return $node_preview->access('update', $account, TRUE);
}
}
}
@@ -0,0 +1,153 @@
<?php
namespace Drupal\node\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
use Symfony\Component\Routing\Route;
/**
* Provides an access checker for node revisions.
*
* @ingroup node_access
*/
class NodeRevisionAccessCheck implements AccessInterface {
/**
* The node storage.
*
* @var \Drupal\node\NodeStorageInterface
*/
protected $nodeStorage;
/**
* The node access control handler.
*
* @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
*/
protected $nodeAccess;
/**
* A static cache of access checks.
*
* @var array
*/
protected $access = [];
/**
* Constructs a new NodeRevisionAccessCheck.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->nodeStorage = $entity_type_manager->getStorage('node');
$this->nodeAccess = $entity_type_manager->getAccessControlHandler('node');
}
/**
* Checks routing access for the node revision.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
* @param int $node_revision
* (optional) The node revision ID. If not specified, but $node is, access
* is checked for that object's revision.
* @param \Drupal\node\NodeInterface $node
* (optional) A node object. Used for checking access to a node's default
* revision when $node_revision is unspecified. Ignored when $node_revision
* is specified. If neither $node_revision nor $node are specified, then
* access is denied.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, AccountInterface $account, $node_revision = NULL, NodeInterface $node = NULL) {
if ($node_revision) {
$node = $this->nodeStorage->loadRevision($node_revision);
}
$operation = $route->getRequirement('_access_node_revision');
return AccessResult::allowedIf($node && $this->checkAccess($node, $account, $operation))->cachePerPermissions()->addCacheableDependency($node);
}
/**
* Checks node revision access.
*
* @param \Drupal\node\NodeInterface $node
* The node to check.
* @param \Drupal\Core\Session\AccountInterface $account
* A user object representing the user for whom the operation is to be
* performed.
* @param string $op
* (optional) The specific operation being checked. Defaults to 'view.'
*
* @return bool
* TRUE if the operation may be performed, FALSE otherwise.
*/
public function checkAccess(NodeInterface $node, AccountInterface $account, $op = 'view') {
$map = [
'view' => 'view all revisions',
'update' => 'revert all revisions',
'delete' => 'delete all revisions',
];
$bundle = $node->bundle();
$type_map = [
'view' => "view $bundle revisions",
'update' => "revert $bundle revisions",
'delete' => "delete $bundle revisions",
];
if (!$node || !isset($map[$op]) || !isset($type_map[$op])) {
// 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 = $node->language()->getId();
$cid = $node->getRevisionId() . ':' . $langcode . ':' . $account->id() . ':' . $op;
if (!isset($this->access[$cid])) {
// Perform basic permission checks first.
if (!$account->hasPermission($map[$op]) && !$account->hasPermission($type_map[$op]) && !$account->hasPermission('administer nodes')) {
$this->access[$cid] = FALSE;
return FALSE;
}
// If the revisions checkbox is selected for the content type, display the
// revisions tab.
$bundle_entity_type = $node->getEntityType()->getBundleEntityType();
$bundle_entity = \Drupal::entityTypeManager()->getStorage($bundle_entity_type)->load($bundle);
if ($bundle_entity->shouldCreateNewRevision() && $op === 'view') {
$this->access[$cid] = TRUE;
}
else {
// There should be at least two revisions. If the vid of the given node
// and the vid of the default revision differ, then we already have two
// different revisions so there is no need for a separate database
// check. Also, if you try to revert to or delete the default revision,
// that's not good.
if ($node->isDefaultRevision() && ($this->nodeStorage->countDefaultLanguageRevisions($node) == 1 || $op === 'update' || $op === 'delete')) {
$this->access[$cid] = FALSE;
}
elseif ($account->hasPermission('administer nodes')) {
$this->access[$cid] = TRUE;
}
else {
// First check the access to the default revision and finally, if the
// node passed in is not the default revision then check access to
// that, too.
$this->access[$cid] = $this->nodeAccess->access($this->nodeStorage->load($node->id()), $op, $account) && ($node->isDefaultRevision() || $this->nodeAccess->access($node, $op, $account));
}
}
}
return $this->access[$cid];
}
}
@@ -0,0 +1,101 @@
<?php
namespace Drupal\node\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\Context\CalculatedCacheContextInterface;
use Drupal\Core\Cache\Context\UserCacheContextBase;
/**
* Defines the node access view cache context service.
*
* Cache context ID: 'user.node_grants' (to vary by all operations' grants).
* Calculated cache context ID: 'user.node_grants:%operation', e.g.
* 'user.node_grants:view' (to vary by the view operation's grants).
*
* This allows for node access grants-sensitive caching when listing nodes.
*
* @see node_query_node_access_alter()
* @ingroup node_access
*/
class NodeAccessGrantsCacheContext extends UserCacheContextBase implements CalculatedCacheContextInterface {
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t("Content access view grants");
}
/**
* {@inheritdoc}
*/
public function getContext($operation = NULL) {
// If the current user either can bypass node access then we don't need to
// determine the exact node grants for the current user.
if ($this->user->hasPermission('bypass node access')) {
return 'all';
}
// When no specific operation is specified, check the grants for all three
// possible operations.
if ($operation === NULL) {
$result = [];
foreach (['view', 'update', 'delete'] as $op) {
$result[] = $this->checkNodeGrants($op);
}
return implode('-', $result);
}
else {
return $this->checkNodeGrants($operation);
}
}
/**
* Checks the node grants for the given operation.
*
* @param string $operation
* The operation to check the node grants for.
*
* @return string
* The string representation of the cache context.
*/
protected function checkNodeGrants($operation) {
// When checking the grants for the 'view' operation and the current user
// has a global view grant (i.e. a view grant for node ID 0) — note that
// this is automatically the case if no node access modules exist (no
// hook_node_grants() implementations) then we don't need to determine the
// exact node view grants for the current user.
if ($operation === 'view' && node_access_view_all_nodes($this->user)) {
return 'view.all';
}
$grants = node_access_grants($operation, $this->user);
$grants_context_parts = [];
foreach ($grants as $realm => $gids) {
$grants_context_parts[] = $realm . ':' . implode(',', $gids);
}
return $operation . '.' . implode(';', $grants_context_parts);
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata($operation = NULL) {
$cacheable_metadata = new CacheableMetadata();
if (!\Drupal::moduleHandler()->getImplementations('node_grants')) {
return $cacheable_metadata;
}
// The node grants may change if the user is updated. (The max-age is set to
// zero below, but sites may override this cache context, and change it to a
// non-zero value. In such cases, this cache tag is needed for correctness.)
$cacheable_metadata->setCacheTags(['user:' . $this->user->id()]);
// If the site is using node grants, this cache context can not be
// optimized.
return $cacheable_metadata->setCacheMaxAge(0);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Drupal\node\ConfigTranslation;
use Drupal\config_translation\ConfigEntityMapper;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides a configuration mapper for node types.
*/
class NodeTypeMapper extends ConfigEntityMapper {
/**
* {@inheritdoc}
*/
public function setEntity(ConfigEntityInterface $entity) {
parent::setEntity($entity);
// Adds the title label to the translation form.
$node_type = $entity->id();
$config = $this->configFactory->get("core.base_field_override.node.$node_type.title");
if (!$config->isNew()) {
$this->addConfigName($config->getName());
}
}
}
@@ -0,0 +1,73 @@
<?php
namespace Drupal\node\ContextProvider;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\node\Entity\Node;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Sets the current node as a context on node routes.
*/
class NodeRouteContext implements ContextProviderInterface {
use StringTranslationTrait;
/**
* The route match object.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a new NodeRouteContext.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
*/
public function __construct(RouteMatchInterface $route_match) {
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public function getRuntimeContexts(array $unqualified_context_ids) {
$result = [];
$context_definition = EntityContextDefinition::create('node')->setRequired(FALSE);
$value = NULL;
if (($route_object = $this->routeMatch->getRouteObject()) && ($route_contexts = $route_object->getOption('parameters')) && isset($route_contexts['node'])) {
if ($node = $this->routeMatch->getParameter('node')) {
$value = $node;
}
}
elseif ($this->routeMatch->getRouteName() == 'node.add') {
$node_type = $this->routeMatch->getParameter('node_type');
$value = Node::create(['type' => $node_type->id()]);
}
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['route']);
$context = new Context($context_definition, $value);
$context->addCacheableDependency($cacheability);
$result['node'] = $context;
return $result;
}
/**
* {@inheritdoc}
*/
public function getAvailableContexts() {
$context = EntityContext::fromEntityTypeId('node', $this->t('Node from URL'));
return ['node' => $context];
}
}
@@ -0,0 +1,336 @@
<?php
namespace Drupal\node\Controller;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\node\NodeStorageInterface;
use Drupal\node\NodeTypeInterface;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for Node routes.
*/
class NodeController extends ControllerBase implements ContainerInjectionInterface {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The entity repository service.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* Constructs a NodeController object.
*
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(DateFormatterInterface $date_formatter, RendererInterface $renderer, EntityRepositoryInterface $entity_repository = NULL) {
$this->dateFormatter = $date_formatter;
$this->renderer = $renderer;
if (!$entity_repository) {
@trigger_error('The entity.repository service must be passed to NodeController::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
$entity_repository = \Drupal::service('entity.repository');
}
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('date.formatter'),
$container->get('renderer'),
$container->get('entity.repository')
);
}
/**
* Displays add content links for available content types.
*
* Redirects to node/add/[type] if only one content type is available.
*
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
* A render array for a list of the node types that can be added; however,
* if there is only one node type defined for the site, the function
* will return a RedirectResponse to the node add page for that one node
* type.
*/
public function addPage() {
$build = [
'#theme' => 'node_add_list',
'#cache' => [
'tags' => $this->entityTypeManager()->getDefinition('node_type')->getListCacheTags(),
],
];
$content = [];
// Only use node types the user has access to.
foreach ($this->entityTypeManager()->getStorage('node_type')->loadMultiple() as $type) {
$access = $this->entityTypeManager()->getAccessControlHandler('node')->createAccess($type->id(), NULL, [], TRUE);
if ($access->isAllowed()) {
$content[$type->id()] = $type;
}
$this->renderer->addCacheableDependency($build, $access);
}
// Bypass the node/add listing if only one content type is available.
if (count($content) == 1) {
$type = array_shift($content);
return $this->redirect('node.add', ['node_type' => $type->id()]);
}
$build['#content'] = $content;
return $build;
}
/**
* Provides the node submission form.
*
* @param \Drupal\node\NodeTypeInterface $node_type
* The node type entity for the node.
*
* @return array
* A node submission form.
*
* @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Define
* entity form routes through the _entity_form instead through the
* _controller directive.
*/
public function add(NodeTypeInterface $node_type) {
@trigger_error(__METHOD__ . ' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Define entity form routes through the _entity_form instead through the _controller directive. See https://www.drupal.org/node/3084856', E_USER_DEPRECATED);
$node = $this->entityTypeManager()->getStorage('node')->create([
'type' => $node_type->id(),
]);
$form = $this->entityFormBuilder()->getForm($node);
return $form;
}
/**
* Displays a node revision.
*
* @param int $node_revision
* The node revision ID.
*
* @return array
* An array suitable for \Drupal\Core\Render\RendererInterface::render().
*/
public function revisionShow($node_revision) {
$node = $this->entityTypeManager()->getStorage('node')->loadRevision($node_revision);
$node = $this->entityRepository->getTranslationFromContext($node);
$node_view_controller = new NodeViewController($this->entityTypeManager(), $this->renderer, $this->currentUser(), $this->entityRepository);
$page = $node_view_controller->view($node);
unset($page['nodes'][$node->id()]['#cache']);
return $page;
}
/**
* Page title callback for a node revision.
*
* @param int $node_revision
* The node revision ID.
*
* @return string
* The page title.
*/
public function revisionPageTitle($node_revision) {
$node = $this->entityTypeManager()->getStorage('node')->loadRevision($node_revision);
return $this->t('Revision of %title from %date', ['%title' => $node->label(), '%date' => $this->dateFormatter->format($node->getRevisionCreationTime())]);
}
/**
* Generates an overview table of older revisions of a node.
*
* @param \Drupal\node\NodeInterface $node
* A node object.
*
* @return array
* An array as expected by \Drupal\Core\Render\RendererInterface::render().
*/
public function revisionOverview(NodeInterface $node) {
$account = $this->currentUser();
$langcode = $node->language()->getId();
$langname = $node->language()->getName();
$languages = $node->getTranslationLanguages();
$has_translations = (count($languages) > 1);
$node_storage = $this->entityTypeManager()->getStorage('node');
$type = $node->getType();
$build['#title'] = $has_translations ? $this->t('@langname revisions for %title', ['@langname' => $langname, '%title' => $node->label()]) : $this->t('Revisions for %title', ['%title' => $node->label()]);
$header = [$this->t('Revision'), $this->t('Operations')];
$revert_permission = (($account->hasPermission("revert $type revisions") || $account->hasPermission('revert all revisions') || $account->hasPermission('administer nodes')) && $node->access('update'));
$delete_permission = (($account->hasPermission("delete $type revisions") || $account->hasPermission('delete all revisions') || $account->hasPermission('administer nodes')) && $node->access('delete'));
$rows = [];
$default_revision = $node->getRevisionId();
$current_revision_displayed = FALSE;
foreach ($this->getRevisionIds($node, $node_storage) as $vid) {
/** @var \Drupal\node\NodeInterface $revision */
$revision = $node_storage->loadRevision($vid);
// Only show revisions that are affected by the language that is being
// displayed.
if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) {
$username = [
'#theme' => 'username',
'#account' => $revision->getRevisionUser(),
];
// Use revision link to link to revisions that are not active.
$date = $this->dateFormatter->format($revision->revision_timestamp->value, 'short');
// We treat also the latest translation-affecting revision as current
// revision, if it was the default revision, as its values for the
// current language will be the same of the current default revision in
// this case.
$is_current_revision = $vid == $default_revision || (!$current_revision_displayed && $revision->wasDefaultRevision());
if (!$is_current_revision) {
$link = Link::fromTextAndUrl($date, new Url('entity.node.revision', ['node' => $node->id(), 'node_revision' => $vid]))->toString();
}
else {
$link = $node->toLink($date)->toString();
$current_revision_displayed = TRUE;
}
$row = [];
$column = [
'data' => [
'#type' => 'inline_template',
'#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}',
'#context' => [
'date' => $link,
'username' => $this->renderer->renderPlain($username),
'message' => ['#markup' => $revision->revision_log->value, '#allowed_tags' => Xss::getHtmlTagList()],
],
],
];
// @todo Simplify once https://www.drupal.org/node/2334319 lands.
$this->renderer->addCacheableDependency($column['data'], $username);
$row[] = $column;
if ($is_current_revision) {
$row[] = [
'data' => [
'#prefix' => '<em>',
'#markup' => $this->t('Current revision'),
'#suffix' => '</em>',
],
];
$rows[] = [
'data' => $row,
'class' => ['revision-current'],
];
}
else {
$links = [];
if ($revert_permission) {
$links['revert'] = [
'title' => $vid < $node->getRevisionId() ? $this->t('Revert') : $this->t('Set as current revision'),
'url' => $has_translations ?
Url::fromRoute('node.revision_revert_translation_confirm', ['node' => $node->id(), 'node_revision' => $vid, 'langcode' => $langcode]) :
Url::fromRoute('node.revision_revert_confirm', ['node' => $node->id(), 'node_revision' => $vid]),
];
}
if ($delete_permission) {
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => Url::fromRoute('node.revision_delete_confirm', ['node' => $node->id(), 'node_revision' => $vid]),
];
}
$row[] = [
'data' => [
'#type' => 'operations',
'#links' => $links,
],
];
$rows[] = $row;
}
}
}
$build['node_revisions_table'] = [
'#theme' => 'table',
'#rows' => $rows,
'#header' => $header,
'#attached' => [
'library' => ['node/drupal.node.admin'],
],
'#attributes' => ['class' => 'node-revision-table'],
];
$build['pager'] = ['#type' => 'pager'];
return $build;
}
/**
* The _title_callback for the node.add route.
*
* @param \Drupal\node\NodeTypeInterface $node_type
* The current node.
*
* @return string
* The page title.
*/
public function addPageTitle(NodeTypeInterface $node_type) {
return $this->t('Create @name', ['@name' => $node_type->label()]);
}
/**
* Gets a list of node revision IDs for a specific node.
*
* @param \Drupal\node\NodeInterface $node
* The node entity.
* @param \Drupal\node\NodeStorageInterface $node_storage
* The node storage handler.
*
* @return int[]
* Node revision IDs (in descending order).
*/
protected function getRevisionIds(NodeInterface $node, NodeStorageInterface $node_storage) {
$result = $node_storage->getQuery()
->allRevisions()
->condition($node->getEntityType()->getKey('id'), $node->id())
->sort($node->getEntityType()->getKey('revision'), 'DESC')
->pager(50)
->execute();
return array_keys($result);
}
}
@@ -0,0 +1,82 @@
<?php
namespace Drupal\node\Controller;
use Drupal\Core\Entity\Controller\EntityViewController;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a controller to render a single node in preview.
*/
class NodePreviewController extends EntityViewController {
/**
* The entity repository service.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* Creates an NodeViewController object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, EntityRepositoryInterface $entity_repository = NULL) {
parent::__construct($entity_type_manager, $renderer);
if (!$entity_repository) {
@trigger_error('The entity.repository service must be passed to NodePreviewController::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
$entity_repository = \Drupal::service('entity.repository');
}
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('renderer'),
$container->get('entity.repository')
);
}
/**
* {@inheritdoc}
*/
public function view(EntityInterface $node_preview, $view_mode_id = 'full', $langcode = NULL) {
$node_preview->preview_view_mode = $view_mode_id;
$build = parent::view($node_preview, $view_mode_id);
$build['#attached']['library'][] = 'node/drupal.node.preview';
// Don't render cache previews.
unset($build['#cache']);
return $build;
}
/**
* The _title_callback for the page that renders a single node in preview.
*
* @param \Drupal\Core\Entity\EntityInterface $node_preview
* The current node.
*
* @return string
* The page title.
*/
public function title(EntityInterface $node_preview) {
return $this->entityRepository->getTranslationFromContext($node_preview)->label();
}
}
@@ -0,0 +1,131 @@
<?php
namespace Drupal\node\Controller;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Controller\EntityViewController;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a controller to render a single node.
*/
class NodeViewController extends EntityViewController {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The entity repository service.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* Creates an NodeViewController object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user. For backwards compatibility this is optional, however
* this will be removed before Drupal 9.0.0.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, AccountInterface $current_user = NULL, EntityRepositoryInterface $entity_repository = NULL) {
parent::__construct($entity_type_manager, $renderer);
$this->currentUser = $current_user ?: \Drupal::currentUser();
if (!$entity_repository) {
@trigger_error('The entity.repository service must be passed to NodeViewController::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
$entity_repository = \Drupal::service('entity.repository');
}
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('renderer'),
$container->get('current_user'),
$container->get('entity.repository')
);
}
/**
* {@inheritdoc}
*/
public function view(EntityInterface $node, $view_mode = 'full', $langcode = NULL) {
$build = parent::view($node, $view_mode, $langcode);
foreach ($node->uriRelationships() as $rel) {
$url = $node->toUrl($rel)->setAbsolute(TRUE);
// Add link relationships if the user is authenticated or if the anonymous
// user has access. Access checking must be done for anonymous users to
// avoid traffic to inaccessible pages from web crawlers. For
// authenticated users, showing the links in HTML head does not impact
// user experience or security, since the routes are access checked when
// visited and only visible via view source. This prevents doing
// potentially expensive and hard to cache access checks on every request.
// This means that the page will vary by user.permissions. We also rely on
// the access checking fallback to ensure the correct cacheability
// metadata if we have to check access.
if ($this->currentUser->isAuthenticated() || $url->access($this->currentUser)) {
// Set the node path as the canonical URL to prevent duplicate content.
$build['#attached']['html_head_link'][] = [
[
'rel' => $rel,
'href' => $url->toString(),
],
TRUE,
];
}
if ($rel == 'canonical') {
// Set the non-aliased canonical path as a default shortlink.
$build['#attached']['html_head_link'][] = [
[
'rel' => 'shortlink',
'href' => $url->setOption('alias', TRUE)->toString(),
],
TRUE,
];
}
}
// Since this generates absolute URLs, it can only be cached "per site".
$build['#cache']['contexts'][] = 'url.site';
// Given this varies by $this->currentUser->isAuthenticated(), add a cache
// context based on the anonymous role.
$build['#cache']['contexts'][] = 'user.roles:anonymous';
return $build;
}
/**
* The _title_callback for the page that renders a single node.
*
* @param \Drupal\Core\Entity\EntityInterface $node
* The current node.
*
* @return string
* The page title.
*/
public function title(EntityInterface $node) {
return $this->entityRepository->getTranslationFromContext($node)->label();
}
}
+397
View File
@@ -0,0 +1,397 @@
<?php
namespace Drupal\node\Entity;
use Drupal\Core\Entity\EditorialContentEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
use Drupal\user\EntityOwnerTrait;
/**
* Defines the node entity class.
*
* @ContentEntityType(
* id = "node",
* label = @Translation("Content"),
* label_collection = @Translation("Content"),
* label_singular = @Translation("content item"),
* label_plural = @Translation("content items"),
* label_count = @PluralTranslation(
* singular = "@count content item",
* plural = "@count content items"
* ),
* bundle_label = @Translation("Content type"),
* handlers = {
* "storage" = "Drupal\node\NodeStorage",
* "storage_schema" = "Drupal\node\NodeStorageSchema",
* "view_builder" = "Drupal\node\NodeViewBuilder",
* "access" = "Drupal\node\NodeAccessControlHandler",
* "views_data" = "Drupal\node\NodeViewsData",
* "form" = {
* "default" = "Drupal\node\NodeForm",
* "delete" = "Drupal\node\Form\NodeDeleteForm",
* "edit" = "Drupal\node\NodeForm",
* "delete-multiple-confirm" = "Drupal\node\Form\DeleteMultiple"
* },
* "route_provider" = {
* "html" = "Drupal\node\Entity\NodeRouteProvider",
* },
* "list_builder" = "Drupal\node\NodeListBuilder",
* "translation" = "Drupal\node\NodeTranslationHandler"
* },
* base_table = "node",
* data_table = "node_field_data",
* revision_table = "node_revision",
* revision_data_table = "node_field_revision",
* show_revision_ui = TRUE,
* translatable = TRUE,
* list_cache_contexts = { "user.node_grants:view" },
* entity_keys = {
* "id" = "nid",
* "revision" = "vid",
* "bundle" = "type",
* "label" = "title",
* "langcode" = "langcode",
* "uuid" = "uuid",
* "status" = "status",
* "published" = "status",
* "uid" = "uid",
* "owner" = "uid",
* },
* revision_metadata_keys = {
* "revision_user" = "revision_uid",
* "revision_created" = "revision_timestamp",
* "revision_log_message" = "revision_log"
* },
* bundle_entity_type = "node_type",
* field_ui_base_route = "entity.node_type.edit_form",
* common_reference_target = TRUE,
* permission_granularity = "bundle",
* links = {
* "canonical" = "/node/{node}",
* "delete-form" = "/node/{node}/delete",
* "delete-multiple-form" = "/admin/content/node/delete",
* "edit-form" = "/node/{node}/edit",
* "version-history" = "/node/{node}/revisions",
* "revision" = "/node/{node}/revisions/{node_revision}/view",
* "create" = "/node",
* }
* )
*/
class Node extends EditorialContentEntityBase implements NodeInterface {
use EntityOwnerTrait;
/**
* Whether the node is being previewed or not.
*
* The variable is set to public as it will give a considerable performance
* improvement. See https://www.drupal.org/node/2498919.
*
* @var true|null
* TRUE if the node is being previewed and NULL if it is not.
*/
public $in_preview = NULL;
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
foreach (array_keys($this->getTranslationLanguages()) as $langcode) {
$translation = $this->getTranslation($langcode);
// If no owner has been set explicitly, make the anonymous user the owner.
if (!$translation->getOwner()) {
$translation->setOwnerId(0);
}
}
// If no revision author has been set explicitly, make the node owner the
// revision author.
if (!$this->getRevisionUser()) {
$this->setRevisionUserId($this->getOwnerId());
}
}
/**
* {@inheritdoc}
*/
public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
parent::preSaveRevision($storage, $record);
if (!$this->isNewRevision() && isset($this->original) && (!isset($record->revision_log) || $record->revision_log === '')) {
// If we are updating an existing node without adding a new revision, we
// need to make sure $entity->revision_log is reset whenever it is empty.
// Therefore, this code allows us to avoid clobbering an existing log
// entry with an empty one.
$record->revision_log = $this->original->revision_log->value;
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
// Update the node access table for this node, but only if it is the
// default revision. There's no need to delete existing records if the node
// is new.
if ($this->isDefaultRevision()) {
/** @var \Drupal\node\NodeAccessControlHandlerInterface $access_control_handler */
$access_control_handler = \Drupal::entityTypeManager()->getAccessControlHandler('node');
$grants = $access_control_handler->acquireGrants($this);
\Drupal::service('node.grant_storage')->write($this, $grants, NULL, $update);
}
// Reindex the node when it is updated. The node is automatically indexed
// when it is added, simply by being added to the node table.
if ($update) {
node_reindex_node_search($this->id());
}
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
// Ensure that all nodes deleted are removed from the search index.
if (\Drupal::hasService('search.index')) {
/** @var \Drupal\search\SearchIndexInterface $search_index */
$search_index = \Drupal::service('search.index');
foreach ($entities as $entity) {
$search_index->clear('node_search', $entity->nid->value);
}
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $nodes) {
parent::postDelete($storage, $nodes);
\Drupal::service('node.grant_storage')->deleteNodeRecords(array_keys($nodes));
}
/**
* {@inheritdoc}
*/
public function getType() {
return $this->bundle();
}
/**
* {@inheritdoc}
*/
public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE) {
// This override exists to set the operation to the default value "view".
return parent::access($operation, $account, $return_as_object);
}
/**
* {@inheritdoc}
*/
public function getTitle() {
return $this->get('title')->value;
}
/**
* {@inheritdoc}
*/
public function setTitle($title) {
$this->set('title', $title);
return $this;
}
/**
* {@inheritdoc}
*/
public function getCreatedTime() {
return $this->get('created')->value;
}
/**
* {@inheritdoc}
*/
public function setCreatedTime($timestamp) {
$this->set('created', $timestamp);
return $this;
}
/**
* {@inheritdoc}
*/
public function isPromoted() {
return (bool) $this->get('promote')->value;
}
/**
* {@inheritdoc}
*/
public function setPromoted($promoted) {
$this->set('promote', $promoted ? NodeInterface::PROMOTED : NodeInterface::NOT_PROMOTED);
return $this;
}
/**
* {@inheritdoc}
*/
public function isSticky() {
return (bool) $this->get('sticky')->value;
}
/**
* {@inheritdoc}
*/
public function setSticky($sticky) {
$this->set('sticky', $sticky ? NodeInterface::STICKY : NodeInterface::NOT_STICKY);
return $this;
}
/**
* {@inheritdoc}
*/
public function getRevisionAuthor() {
@trigger_error(__NAMESPACE__ . '\Node::getRevisionAuthor is deprecated in drupal:8.2.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Entity\RevisionLogInterface::getRevisionUser() instead. See https://www.drupal.org/node/3069750', E_USER_DEPRECATED);
return $this->getRevisionUser();
}
/**
* {@inheritdoc}
*/
public function setRevisionAuthorId($uid) {
@trigger_error(__NAMESPACE__ . '\Node::setRevisionAuthorId is deprecated in drupal:8.2.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Entity\RevisionLogInterface::setRevisionUserId() instead. See https://www.drupal.org/node/3069750', E_USER_DEPRECATED);
return $this->setRevisionUserId($uid);
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields += static::ownerBaseFieldDefinitions($entity_type);
$fields['title'] = BaseFieldDefinition::create('string')
->setLabel(t('Title'))
->setRequired(TRUE)
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setSetting('max_length', 255)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
])
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -5,
])
->setDisplayConfigurable('form', TRUE);
$fields['uid']
->setLabel(t('Authored by'))
->setDescription(t('The username of the content author.'))
->setRevisionable(TRUE)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'author',
'weight' => 0,
])
->setDisplayOptions('form', [
'type' => 'entity_reference_autocomplete',
'weight' => 5,
'settings' => [
'match_operator' => 'CONTAINS',
'size' => '60',
'placeholder' => '',
],
])
->setDisplayConfigurable('form', TRUE);
$fields['status']
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
'settings' => [
'display_label' => TRUE,
],
'weight' => 120,
])
->setDisplayConfigurable('form', TRUE);
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Authored on'))
->setDescription(t('The time that the node was created.'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'timestamp',
'weight' => 0,
])
->setDisplayOptions('form', [
'type' => 'datetime_timestamp',
'weight' => 10,
])
->setDisplayConfigurable('form', TRUE);
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the node was last edited.'))
->setRevisionable(TRUE)
->setTranslatable(TRUE);
$fields['promote'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Promoted to front page'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setDefaultValue(TRUE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
'settings' => [
'display_label' => TRUE,
],
'weight' => 15,
])
->setDisplayConfigurable('form', TRUE);
$fields['sticky'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Sticky at top of lists'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setDefaultValue(FALSE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
'settings' => [
'display_label' => TRUE,
],
'weight' => 16,
])
->setDisplayConfigurable('form', TRUE);
return $fields;
}
/**
* Default value callback for 'uid' base field definition.
*
* @see ::baseFieldDefinitions()
*
* @deprecated The ::getCurrentUserId method is deprecated in 8.6.x and will
* be removed before 9.0.0.
*
* @return array
* An array of default values.
*/
public static function getCurrentUserId() {
@trigger_error('The ::getCurrentUserId method is deprecated in 8.6.x and will be removed before 9.0.0.', E_USER_DEPRECATED);
return [\Drupal::currentUser()->id()];
}
}
@@ -0,0 +1,49 @@
<?php
namespace Drupal\node\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides routes for nodes.
*/
class NodeRouteProvider implements EntityRouteProviderInterface {
/**
* {@inheritdoc}
*/
public function getRoutes(EntityTypeInterface $entity_type) {
$route_collection = new RouteCollection();
$route = (new Route('/node/{node}'))
->addDefaults([
'_controller' => '\Drupal\node\Controller\NodeViewController::view',
'_title_callback' => '\Drupal\node\Controller\NodeViewController::title',
])
->setRequirement('node', '\d+')
->setRequirement('_entity_access', 'node.view');
$route_collection->add('entity.node.canonical', $route);
$route = (new Route('/node/{node}/delete'))
->addDefaults([
'_entity_form' => 'node.delete',
'_title' => 'Delete',
])
->setRequirement('node', '\d+')
->setRequirement('_entity_access', 'node.delete')
->setOption('_node_operation_route', TRUE);
$route_collection->add('entity.node.delete_form', $route);
$route = (new Route('/node/{node}/edit'))
->setDefault('_entity_form', 'node.edit')
->setRequirement('_entity_access', 'node.update')
->setRequirement('node', '\d+')
->setOption('_node_operation_route', TRUE);
$route_collection->add('entity.node.edit_form', $route);
return $route_collection;
}
}
@@ -0,0 +1,223 @@
<?php
namespace Drupal\node\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\node\NodeTypeInterface;
/**
* Defines the Node type configuration entity.
*
* @ConfigEntityType(
* id = "node_type",
* label = @Translation("Content type"),
* label_collection = @Translation("Content types"),
* label_singular = @Translation("content type"),
* label_plural = @Translation("content types"),
* label_count = @PluralTranslation(
* singular = "@count content type",
* plural = "@count content types",
* ),
* handlers = {
* "access" = "Drupal\node\NodeTypeAccessControlHandler",
* "form" = {
* "add" = "Drupal\node\NodeTypeForm",
* "edit" = "Drupal\node\NodeTypeForm",
* "delete" = "Drupal\node\Form\NodeTypeDeleteConfirm"
* },
* "list_builder" = "Drupal\node\NodeTypeListBuilder",
* },
* admin_permission = "administer content types",
* config_prefix = "type",
* bundle_of = "node",
* entity_keys = {
* "id" = "type",
* "label" = "name"
* },
* links = {
* "edit-form" = "/admin/structure/types/manage/{node_type}",
* "delete-form" = "/admin/structure/types/manage/{node_type}/delete",
* "collection" = "/admin/structure/types",
* },
* config_export = {
* "name",
* "type",
* "description",
* "help",
* "new_revision",
* "preview_mode",
* "display_submitted",
* }
* )
*/
class NodeType extends ConfigEntityBundleBase implements NodeTypeInterface {
/**
* The machine name of this node type.
*
* @var string
*
* @todo Rename to $id.
*/
protected $type;
/**
* The human-readable name of the node type.
*
* @var string
*
* @todo Rename to $label.
*/
protected $name;
/**
* A brief description of this node type.
*
* @var string
*/
protected $description;
/**
* Help information shown to the user when creating a Node of this type.
*
* @var string
*/
protected $help;
/**
* Default value of the 'Create new revision' checkbox of this node type.
*
* @var bool
*/
protected $new_revision = TRUE;
/**
* The preview mode.
*
* @var int
*/
protected $preview_mode = DRUPAL_OPTIONAL;
/**
* Display setting for author and date Submitted by post information.
*
* @var bool
*/
protected $display_submitted = TRUE;
/**
* {@inheritdoc}
*/
public function id() {
return $this->type;
}
/**
* {@inheritdoc}
*/
public function isLocked() {
$locked = \Drupal::state()->get('node.type.locked');
return isset($locked[$this->id()]) ? $locked[$this->id()] : FALSE;
}
/**
* {@inheritdoc}
*/
public function isNewRevision() {
@trigger_error('NodeType::isNewRevision is deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use Drupal\Core\Entity\RevisionableEntityBundleInterface::shouldCreateNewRevision() instead. See https://www.drupal.org/node/3067365', E_USER_DEPRECATED);
return $this->shouldCreateNewRevision();
}
/**
* {@inheritdoc}
*/
public function setNewRevision($new_revision) {
$this->new_revision = $new_revision;
}
/**
* {@inheritdoc}
*/
public function displaySubmitted() {
return $this->display_submitted;
}
/**
* {@inheritdoc}
*/
public function setDisplaySubmitted($display_submitted) {
$this->display_submitted = $display_submitted;
}
/**
* {@inheritdoc}
*/
public function getPreviewMode() {
return $this->preview_mode;
}
/**
* {@inheritdoc}
*/
public function setPreviewMode($preview_mode) {
$this->preview_mode = $preview_mode;
}
/**
* {@inheritdoc}
*/
public function getHelp() {
return $this->help;
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
if ($update && $this->getOriginalId() != $this->id()) {
$update_count = node_type_update_nodes($this->getOriginalId(), $this->id());
if ($update_count) {
\Drupal::messenger()->addStatus(\Drupal::translation()->formatPlural($update_count,
'Changed the content type of 1 post from %old-type to %type.',
'Changed the content type of @count posts from %old-type to %type.',
[
'%old-type' => $this->getOriginalId(),
'%type' => $this->id(),
]));
}
}
if ($update) {
// Clear the cached field definitions as some settings affect the field
// definitions.
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
// Clear the node type cache to reflect the removal.
$storage->resetCache(array_keys($entities));
}
/**
* {@inheritdoc}
*/
public function shouldCreateNewRevision() {
return $this->new_revision;
}
}
@@ -0,0 +1,77 @@
<?php
namespace Drupal\node\EventSubscriber;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Routing\RouteBuilderInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
/**
* Sets the _admin_route for specific node-related routes.
*/
class NodeAdminRouteSubscriber extends RouteSubscriberBase {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The router builder.
*
* @var \Drupal\Core\Routing\RouteBuilderInterface
*/
protected $routerBuilder;
/**
* Constructs a new NodeAdminRouteSubscriber.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Routing\RouteBuilderInterface $router_builder
* The router builder service.
*/
public function __construct(ConfigFactoryInterface $config_factory, RouteBuilderInterface $router_builder) {
$this->configFactory = $config_factory;
$this->routerBuilder = $router_builder;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
if ($this->configFactory->get('node.settings')->get('use_admin_theme')) {
foreach ($collection->all() as $route) {
if ($route->hasOption('_node_operation_route')) {
$route->setOption('_admin_route', TRUE);
}
}
}
}
/**
* Rebuilds the router when node.settings:use_admin_theme is changed.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
*/
public function onConfigSave(ConfigCrudEvent $event) {
if ($event->getConfig()->getName() === 'node.settings' && $event->isChanged('use_admin_theme')) {
$this->routerBuilder->setRebuildNeeded();
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = parent::getSubscribedEvents();
$events[ConfigEvents::SAVE][] = ['onConfigSave', 0];
return $events;
}
}
@@ -0,0 +1,128 @@
<?php
namespace Drupal\node\EventSubscriber;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\State\StateInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Redirect node translations that have been consolidated by migration.
*
* If we migrated node translations from Drupal 6 or 7, these nodes are now
* combined with their source language node. Since there still might be
* references to the URLs of these now consolidated nodes, this service catches
* the 404s and try to redirect them to the right node in the right language.
*
* The mapping of the old nids to the new ones is made by the
* NodeTranslationMigrateSubscriber class during the migration and is stored
* in the "node_translation_redirect" key/value collection.
*
* @see \Drupal\node\NodeServiceProvider
* @see \Drupal\node\EventSubscriber\NodeTranslationMigrateSubscriber
*/
class NodeTranslationExceptionSubscriber implements EventSubscriberInterface {
/**
* The key value factory.
*
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
*/
protected $keyValue;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The URL generator.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs the NodeTranslationExceptionSubscriber.
*
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
* The key value factory.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The URL generator.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(KeyValueFactoryInterface $key_value, LanguageManagerInterface $language_manager, UrlGeneratorInterface $url_generator, StateInterface $state) {
$this->keyValue = $key_value;
$this->languageManager = $language_manager;
$this->urlGenerator = $url_generator;
$this->state = $state;
}
/**
* Redirects not found node translations using the key value collection.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
* The exception event.
*/
public function onException(GetResponseForExceptionEvent $event) {
$exception = $event->getException();
// If this is not a 404, we don't need to check for a redirection.
if (!($exception instanceof NotFoundHttpException)) {
return;
}
$previous_exception = $exception->getPrevious();
if ($previous_exception instanceof ParamNotConvertedException) {
$route_name = $previous_exception->getRouteName();
$parameters = $previous_exception->getRawParameters();
if ($route_name === 'entity.node.canonical' && isset($parameters['node'])) {
// If the node_translation_redirect state is not set, we don't need to check
// for a redirection.
if (!$this->state->get('node_translation_redirect')) {
return;
}
$old_nid = $parameters['node'];
$collection = $this->keyValue->get('node_translation_redirect');
if ($old_nid && $value = $collection->get($old_nid)) {
list($nid, $langcode) = $value;
$language = $this->languageManager->getLanguage($langcode);
$url = $this->urlGenerator->generateFromRoute('entity.node.canonical', ['node' => $nid], ['language' => $language]);
$response = new RedirectResponse($url, 301);
$event->setResponse($response);
}
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = [];
$events[KernelEvents::EXCEPTION] = ['onException'];
return $events;
}
}
@@ -0,0 +1,113 @@
<?php
namespace Drupal\node\EventSubscriber;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Event\EventBase;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Creates a key value collection for migrated node translation redirections.
*
* If we are migrating node translations from Drupal 6 or 7, these nodes will be
* combined with their source node. Since there still might be references to the
* URLs of these now consolidated nodes, this service saves the mapping between
* the old nids to the new ones to be able to redirect them to the right node in
* the right language.
*
* The mapping is stored in the "node_translation_redirect" key/value collection
* and the redirection is made by the NodeTranslationExceptionSubscriber class.
*
* @see \Drupal\node\NodeServiceProvider
* @see \Drupal\node\EventSubscriber\NodeTranslationExceptionSubscriber
*/
class NodeTranslationMigrateSubscriber implements EventSubscriberInterface {
/**
* The key value factory.
*
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
*/
protected $keyValue;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs the NodeTranslationMigrateSubscriber.
*
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
* The key value factory.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(KeyValueFactoryInterface $key_value, StateInterface $state) {
$this->keyValue = $key_value;
$this->state = $state;
}
/**
* Helper method to check if we are migrating translated nodes.
*
* @param \Drupal\migrate\Event\EventBase $event
* The migrate event.
*
* @return bool
* True if we are migrating translated nodes, false otherwise.
*/
protected function isNodeTranslationsMigration(EventBase $event) {
$migration = $event->getMigration();
$source_configuration = $migration->getSourceConfiguration();
$destination_configuration = $migration->getDestinationConfiguration();
return !empty($source_configuration['translations']) && $destination_configuration['plugin'] === 'entity:node';
}
/**
* Maps the old nid to the new one in the key value collection.
*
* @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event
* The migrate post row save event.
*/
public function onPostRowSave(MigratePostRowSaveEvent $event) {
if ($this->isNodeTranslationsMigration($event)) {
$row = $event->getRow();
$source = $row->getSource();
$destination = $row->getDestination();
$collection = $this->keyValue->get('node_translation_redirect');
$collection->set($source['nid'], [$destination['nid'], $destination['langcode']]);
}
}
/**
* Set the node_translation_redirect state to enable the redirections.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The migrate import event.
*/
public function onPostImport(MigrateImportEvent $event) {
if ($this->isNodeTranslationsMigration($event)) {
$this->state->set('node_translation_redirect', TRUE);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = [];
$events[MigrateEvents::POST_ROW_SAVE] = ['onPostRowSave'];
$events[MigrateEvents::POST_IMPORT] = ['onPostImport'];
return $events;
}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\node\Form;
use Drupal\Core\Entity\Form\DeleteMultipleForm as EntityDeleteMultipleForm;
use Drupal\Core\Url;
/**
* Provides a node deletion confirmation form.
*
* @internal
*/
class DeleteMultiple extends EntityDeleteMultipleForm {
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('system.admin_content');
}
/**
* {@inheritdoc}
*/
protected function getDeletedMessage($count) {
return $this->formatPlural($count, 'Deleted @count content item.', 'Deleted @count content items.');
}
/**
* {@inheritdoc}
*/
protected function getInaccessibleMessage($count) {
return $this->formatPlural($count, "@count content item has not been deleted because you do not have the necessary permissions.", "@count content items have not been deleted because you do not have the necessary permissions.");
}
}
@@ -0,0 +1,47 @@
<?php
namespace Drupal\node\Form;
use Drupal\Core\Entity\ContentEntityDeleteForm;
/**
* Provides a form for deleting a node.
*
* @internal
*/
class NodeDeleteForm extends ContentEntityDeleteForm {
/**
* {@inheritdoc}
*/
protected function getDeletionMessage() {
/** @var \Drupal\node\NodeInterface $entity */
$entity = $this->getEntity();
$node_type_storage = $this->entityTypeManager->getStorage('node_type');
$node_type = $node_type_storage->load($entity->bundle())->label();
if (!$entity->isDefaultTranslation()) {
return $this->t('@language translation of the @type %label has been deleted.', [
'@language' => $entity->language()->getName(),
'@type' => $node_type,
'%label' => $entity->label(),
]);
}
return $this->t('The @type %title has been deleted.', [
'@type' => $node_type,
'%title' => $this->getEntity()->label(),
]);
}
/**
* {@inheritdoc}
*/
protected function logDeletionMessage() {
/** @var \Drupal\node\NodeInterface $entity */
$entity = $this->getEntity();
$this->logger('content')->notice('@type: deleted %title.', ['@type' => $entity->getType(), '%title' => $entity->label()]);
}
}
@@ -0,0 +1,154 @@
<?php
namespace Drupal\node\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Contains a form for switching the view mode of a node during preview.
*
* @internal
*/
class NodePreviewForm extends FormBase {
use DeprecatedServicePropertyTrait;
/**
* {@inheritdoc}
*/
protected $deprecatedProperties = [
'entityManager' => 'entity.manager',
];
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_display.repository'),
$container->get('config.factory')
);
}
/**
* Constructs a new NodePreviewForm.
*
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
*/
public function __construct(EntityDisplayRepositoryInterface $entity_display_repository, ConfigFactoryInterface $config_factory) {
$this->entityDisplayRepository = $entity_display_repository;
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'node_preview_form_select';
}
/**
* Form constructor.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\Core\Entity\EntityInterface $node
* The node being previews
*
* @return array
* The form structure.
*/
public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $node = NULL) {
$view_mode = $node->preview_view_mode;
$query_options = ['query' => ['uuid' => $node->uuid()]];
$query = $this->getRequest()->query;
if ($query->has('destination')) {
$query_options['query']['destination'] = $query->get('destination');
}
$form['backlink'] = [
'#type' => 'link',
'#title' => $this->t('Back to content editing'),
'#url' => $node->isNew() ? Url::fromRoute('node.add', ['node_type' => $node->bundle()]) : $node->toUrl('edit-form'),
'#options' => ['attributes' => ['class' => ['node-preview-backlink']]] + $query_options,
];
// Always show full as an option, even if the display is not enabled.
$view_mode_options = ['full' => $this->t('Full')] + $this->entityDisplayRepository->getViewModeOptionsByBundle('node', $node->bundle());
// Unset view modes that are not used in the front end.
unset($view_mode_options['default']);
unset($view_mode_options['rss']);
unset($view_mode_options['search_index']);
$form['uuid'] = [
'#type' => 'value',
'#value' => $node->uuid(),
];
$form['view_mode'] = [
'#type' => 'select',
'#title' => $this->t('View mode'),
'#options' => $view_mode_options,
'#default_value' => $view_mode,
'#attributes' => [
'data-drupal-autosubmit' => TRUE,
],
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Switch'),
'#attributes' => [
'class' => ['js-hide'],
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$route_parameters = [
'node_preview' => $form_state->getValue('uuid'),
'view_mode_id' => $form_state->getValue('view_mode'),
];
$options = [];
$query = $this->getRequest()->query;
if ($query->has('destination')) {
$options['query']['destination'] = $query->get('destination');
$query->remove('destination');
}
$form_state->setRedirect('entity.node.preview', $route_parameters, $options);
}
}
@@ -0,0 +1,153 @@
<?php
namespace Drupal\node\Form;
use Drupal\Core\Database\Connection;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for deleting a node revision.
*
* @internal
*/
class NodeRevisionDeleteForm extends ConfirmFormBase {
/**
* The node revision.
*
* @var \Drupal\node\NodeInterface
*/
protected $revision;
/**
* The node storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeStorage;
/**
* The node type storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeTypeStorage;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* Constructs a new NodeRevisionDeleteForm.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $node_storage
* The node storage.
* @param \Drupal\Core\Entity\EntityStorageInterface $node_type_storage
* The node type storage.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
*/
public function __construct(EntityStorageInterface $node_storage, EntityStorageInterface $node_type_storage, Connection $connection, DateFormatterInterface $date_formatter) {
$this->nodeStorage = $node_storage;
$this->nodeTypeStorage = $node_type_storage;
$this->connection = $connection;
$this->dateFormatter = $date_formatter;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$entity_type_manager = $container->get('entity_type.manager');
return new static(
$entity_type_manager->getStorage('node'),
$entity_type_manager->getStorage('node_type'),
$container->get('database'),
$container->get('date.formatter')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'node_revision_delete_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return t('Are you sure you want to delete the revision from %revision-date?', [
'%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime()),
]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.node.version_history', ['node' => $this->revision->id()]);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return t('Delete');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $node_revision = NULL) {
$this->revision = $this->nodeStorage->loadRevision($node_revision);
$form = parent::buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->nodeStorage->deleteRevision($this->revision->getRevisionId());
$this->logger('content')->notice('@type: deleted %title revision %revision.', ['@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]);
$node_type = $this->nodeTypeStorage->load($this->revision->bundle())->label();
$this->messenger()
->addStatus($this->t('Revision from %revision-date of @type %title has been deleted.', [
'%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime()),
'@type' => $node_type,
'%title' => $this->revision->label(),
]));
$form_state->setRedirect(
'entity.node.canonical',
['node' => $this->revision->id()]
);
if ($this->connection->query('SELECT COUNT(DISTINCT vid) FROM {node_field_revision} WHERE nid = :nid', [':nid' => $this->revision->id()])->fetchField() > 1) {
$form_state->setRedirect(
'entity.node.version_history',
['node' => $this->revision->id()]
);
}
}
}
@@ -0,0 +1,167 @@
<?php
namespace Drupal\node\Form;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for reverting a node revision.
*
* @internal
*/
class NodeRevisionRevertForm extends ConfirmFormBase {
/**
* The node revision.
*
* @var \Drupal\node\NodeInterface
*/
protected $revision;
/**
* The node storage.
*
* @var \Drupal\node\NodeStorageInterface
*/
protected $nodeStorage;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The time service.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* Constructs a new NodeRevisionRevertForm.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $node_storage
* The node storage.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
*/
public function __construct(EntityStorageInterface $node_storage, DateFormatterInterface $date_formatter, TimeInterface $time) {
$this->nodeStorage = $node_storage;
$this->dateFormatter = $date_formatter;
$this->time = $time;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('node'),
$container->get('date.formatter'),
$container->get('datetime.time')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'node_revision_revert_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.node.version_history', ['node' => $this->revision->id()]);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return t('Revert');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $node_revision = NULL) {
$this->revision = $this->nodeStorage->loadRevision($node_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.
$original_revision_timestamp = $this->revision->getRevisionCreationTime();
$this->revision = $this->prepareRevertedRevision($this->revision, $form_state);
$this->revision->revision_log = t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)]);
$this->revision->setRevisionUserId($this->currentUser()->id());
$this->revision->setRevisionCreationTime($this->time->getRequestTime());
$this->revision->setChangedTime($this->time->getRequestTime());
$this->revision->save();
$this->logger('content')->notice('@type: reverted %title revision %revision.', ['@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]);
$this->messenger()
->addStatus($this->t('@type %title has been reverted to the revision from %revision-date.', [
'@type' => node_get_type_label($this->revision),
'%title' => $this->revision->label(),
'%revision-date' => $this->dateFormatter->format($original_revision_timestamp),
]));
$form_state->setRedirect(
'entity.node.version_history',
['node' => $this->revision->id()]
);
}
/**
* Prepares a revision to be reverted.
*
* @param \Drupal\node\NodeInterface $revision
* The revision to be reverted.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return \Drupal\node\NodeInterface
* The prepared revision ready to be stored.
*/
protected function prepareRevertedRevision(NodeInterface $revision, FormStateInterface $form_state) {
$revision->setNewRevision();
$revision->isDefaultRevision(TRUE);
return $revision;
}
}
@@ -0,0 +1,114 @@
<?php
namespace Drupal\node\Form;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for reverting a node revision for a single translation.
*
* @internal
*/
class NodeRevisionRevertTranslationForm extends NodeRevisionRevertForm {
/**
* The language to be reverted.
*
* @var string
*/
protected $langcode;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a new NodeRevisionRevertTranslationForm.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $node_storage
* The node storage.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
*/
public function __construct(EntityStorageInterface $node_storage, DateFormatterInterface $date_formatter, LanguageManagerInterface $language_manager, TimeInterface $time) {
parent::__construct($node_storage, $date_formatter, $time);
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('node'),
$container->get('date.formatter'),
$container->get('language_manager'),
$container->get('datetime.time')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'node_revision_revert_translation_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return t('Are you sure you want to revert @language translation to the revision from %revision-date?', ['@language' => $this->languageManager->getLanguageName($this->langcode), '%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $node_revision = NULL, $langcode = NULL) {
$this->langcode = $langcode;
$form = parent::buildForm($form, $form_state, $node_revision);
// Unless untranslatable fields are configured to affect only the default
// translation, we need to ask the user whether they should be included in
// the revert process.
$default_translation_affected = $this->revision->isDefaultTranslationAffectedOnly();
$form['revert_untranslated_fields'] = [
'#type' => 'checkbox',
'#title' => $this->t('Revert content shared among translations'),
'#default_value' => $default_translation_affected && $this->revision->getTranslation($this->langcode)->isDefaultTranslation(),
'#access' => !$default_translation_affected,
];
return $form;
}
/**
* {@inheritdoc}
*/
protected function prepareRevertedRevision(NodeInterface $revision, FormStateInterface $form_state) {
$revert_untranslated_fields = (bool) $form_state->getValue('revert_untranslated_fields');
$translation = $revision->getTranslation($this->langcode);
return $this->nodeStorage->createRevision($translation, TRUE, $revert_untranslated_fields);
}
}
@@ -0,0 +1,33 @@
<?php
namespace Drupal\node\Form;
use Drupal\Core\Entity\EntityDeleteForm;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a form for content type deletion.
*
* @internal
*/
class NodeTypeDeleteConfirm extends EntityDeleteForm {
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$num_nodes = $this->entityTypeManager->getStorage('node')->getQuery()
->condition('type', $this->entity->id())
->count()
->execute();
if ($num_nodes) {
$caption = '<p>' . $this->formatPlural($num_nodes, '%type is used by 1 piece of content on your site. You can not remove this content type until you have removed all of the %type content.', '%type is used by @count pieces of content on your site. You may not remove %type until you have removed all of the %type content.', ['%type' => $this->entity->label()]) . '</p>';
$form['#title'] = $this->getQuestion();
$form['description'] = ['#markup' => $caption];
return $form;
}
return parent::buildForm($form, $form_state);
}
}
@@ -0,0 +1,59 @@
<?php
namespace Drupal\node\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Form for rebuilding permissions.
*
* @internal
*/
class RebuildPermissionsForm extends ConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'node_configure_rebuild_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return t('Are you sure you want to rebuild the permissions on site content?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('system.status');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return t('Rebuild permissions');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return t('This action rebuilds all permissions on site content, and may be a lengthy process. This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
node_access_rebuild(TRUE);
$form_state->setRedirectUrl($this->getCancelUrl());
}
}
@@ -0,0 +1,205 @@
<?php
namespace Drupal\node;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the access control handler for the node entity type.
*
* @see \Drupal\node\Entity\Node
* @ingroup node_access
*/
class NodeAccessControlHandler extends EntityAccessControlHandler implements NodeAccessControlHandlerInterface, EntityHandlerInterface {
/**
* The node grant storage.
*
* @var \Drupal\node\NodeGrantDatabaseStorageInterface
*/
protected $grantStorage;
/**
* Constructs a NodeAccessControlHandler object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\node\NodeGrantDatabaseStorageInterface $grant_storage
* The node grant storage.
*/
public function __construct(EntityTypeInterface $entity_type, NodeGrantDatabaseStorageInterface $grant_storage) {
parent::__construct($entity_type);
$this->grantStorage = $grant_storage;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('node.grant_storage')
);
}
/**
* {@inheritdoc}
*/
public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
$account = $this->prepareUser($account);
if ($account->hasPermission('bypass node access')) {
$result = AccessResult::allowed()->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
if (!$account->hasPermission('access content')) {
$result = AccessResult::forbidden("The 'access content' permission is required.")->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
$result = parent::access($entity, $operation, $account, TRUE)->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) {
$account = $this->prepareUser($account);
if ($account->hasPermission('bypass node access')) {
$result = AccessResult::allowed()->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
if (!$account->hasPermission('access content')) {
$result = AccessResult::forbidden("The 'access content' permission is required.")->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
$result = parent::createAccess($entity_bundle, $account, $context, TRUE)->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $node, $operation, AccountInterface $account) {
/** @var \Drupal\node\NodeInterface $node */
// Fetch information from the node object if possible.
$status = $node->isPublished();
$uid = $node->getOwnerId();
// Check if authors can view their own unpublished nodes.
if ($operation === 'view' && !$status && $account->hasPermission('view own unpublished content') && $account->isAuthenticated() && $account->id() == $uid) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($node);
}
// Evaluate node grants.
$access_result = $this->grantStorage->access($node, $operation, $account);
if ($operation === 'view' && $access_result instanceof RefinableCacheableDependencyInterface) {
// Node variations can affect the access to the node. For instance, the
// access result cache varies on the node's published status. Only the
// 'view' node grant can currently be cached. The 'update' and 'delete'
// grants are already marked as uncacheable in the node grant storage.
// @see \Drupal\node\NodeGrantDatabaseStorage::access()
$access_result->addCacheableDependency($node);
}
return $access_result;
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
return AccessResult::allowedIf($account->hasPermission('create ' . $entity_bundle . ' content'))->cachePerPermissions();
}
/**
* {@inheritdoc}
*/
protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
// Only users with the administer nodes permission can edit administrative
// fields.
$administrative_fields = ['uid', 'status', 'created', 'promote', 'sticky'];
if ($operation == 'edit' && in_array($field_definition->getName(), $administrative_fields, TRUE)) {
return AccessResult::allowedIfHasPermission($account, 'administer nodes');
}
// No user can change read only fields.
$read_only_fields = ['revision_timestamp', 'revision_uid'];
if ($operation == 'edit' && in_array($field_definition->getName(), $read_only_fields, TRUE)) {
return AccessResult::forbidden();
}
// Users have access to the revision_log field either if they have
// administrative permissions or if the new revision option is enabled.
if ($operation == 'edit' && $field_definition->getName() == 'revision_log') {
if ($account->hasPermission('administer nodes')) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::allowedIf($items->getEntity()->type->entity->shouldCreateNewRevision())->cachePerPermissions();
}
return parent::checkFieldAccess($operation, $field_definition, $account, $items);
}
/**
* {@inheritdoc}
*/
public function acquireGrants(NodeInterface $node) {
$grants = $this->moduleHandler->invokeAll('node_access_records', [$node]);
// Let modules alter the grants.
$this->moduleHandler->alter('node_access_records', $grants, $node);
// If no grants are set and the node is published, then use the default grant.
if (empty($grants) && $node->isPublished()) {
$grants[] = ['realm' => 'all', 'gid' => 0, 'grant_view' => 1, 'grant_update' => 0, 'grant_delete' => 0];
}
return $grants;
}
/**
* {@inheritdoc}
*/
public function writeGrants(NodeInterface $node, $delete = TRUE) {
$grants = $this->acquireGrants($node);
$this->grantStorage->write($node, $grants, NULL, $delete);
}
/**
* {@inheritdoc}
*/
public function writeDefaultGrant() {
$this->grantStorage->writeDefault();
}
/**
* {@inheritdoc}
*/
public function deleteGrants() {
$this->grantStorage->delete();
}
/**
* {@inheritdoc}
*/
public function countGrants() {
return $this->grantStorage->count();
}
/**
* {@inheritdoc}
*/
public function checkAllGrants(AccountInterface $account) {
return $this->grantStorage->checkAll($account);
}
}
@@ -0,0 +1,82 @@
<?php
namespace Drupal\node;
use Drupal\Core\Session\AccountInterface;
/**
* Node specific entity access control methods.
*
* @ingroup node_access
*/
interface NodeAccessControlHandlerInterface {
/**
* Gets the list of node access grants.
*
* This function is called to check the access grants for a node. It collects
* all node access grants for the node from hook_node_access_records()
* implementations, allows these grants to be altered via
* hook_node_access_records_alter() implementations, and returns the grants to
* the caller.
*
* @param \Drupal\node\NodeInterface $node
* The $node to acquire grants for.
*
* @return array
* The access rules for the node.
*/
public function acquireGrants(NodeInterface $node);
/**
* Writes a list of grants to the database, deleting any previously saved ones.
*
* Modules that use node access can use this function when doing mass updates
* due to widespread permission changes.
*
* Note: Don't call this function directly from a contributed module. Call
* \Drupal\node\NodeAccessControlHandlerInterface::acquireGrants() instead.
*
* @param \Drupal\node\NodeInterface $node
* The node whose grants are being written.
* @param $delete
* (optional) If false, does not delete records. This is only for optimization
* purposes, and assumes the caller has already performed a mass delete of
* some form. Defaults to TRUE.
*
* @deprecated in drupal:8.0.0 and is removed from drupal:9.0.0.
* Use \Drupal\node\NodeAccessControlHandlerInterface::acquireGrants().
*/
public function writeGrants(NodeInterface $node, $delete = TRUE);
/**
* Creates the default node access grant entry on the grant storage.
*/
public function writeDefaultGrant();
/**
* Deletes all node access entries.
*/
public function deleteGrants();
/**
* Counts available node grants.
*
* @return int
* Returns the amount of node grants.
*/
public function countGrants();
/**
* Checks all grants for a given account.
*
* @param \Drupal\Core\Session\AccountInterface $account
* A user object representing the user for whom the operation is to be
* performed.
*
* @return int
* Status of the access check.
*/
public function checkAllGrants(AccountInterface $account);
}
+333
View File
@@ -0,0 +1,333 @@
<?php
namespace Drupal\node;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form handler for the node edit forms.
*
* @internal
*/
class NodeForm extends ContentEntityForm {
/**
* The tempstore factory.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempStoreFactory;
/**
* The Current User object.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* Constructs a NodeForm object.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The factory for the temp store object.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
*/
public function __construct(EntityRepositoryInterface $entity_repository, PrivateTempStoreFactory $temp_store_factory, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL, AccountInterface $current_user, DateFormatterInterface $date_formatter) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$this->tempStoreFactory = $temp_store_factory;
$this->currentUser = $current_user;
$this->dateFormatter = $date_formatter;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('tempstore.private'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time'),
$container->get('current_user'),
$container->get('date.formatter')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
// Try to restore from temp store, this must be done before calling
// parent::form().
$store = $this->tempStoreFactory->get('node_preview');
// Attempt to load from preview when the uuid is present unless we are
// rebuilding the form.
$request_uuid = \Drupal::request()->query->get('uuid');
if (!$form_state->isRebuilding() && $request_uuid && $preview = $store->get($request_uuid)) {
/** @var $preview \Drupal\Core\Form\FormStateInterface */
$form_state->setStorage($preview->getStorage());
$form_state->setUserInput($preview->getUserInput());
// Rebuild the form.
$form_state->setRebuild();
// The combination of having user input and rebuilding the form means
// that it will attempt to cache the form state which will fail if it is
// a GET request.
$form_state->setRequestMethod('POST');
$this->entity = $preview->getFormObject()->getEntity();
$this->entity->in_preview = NULL;
$form_state->set('has_been_previewed', TRUE);
}
/** @var \Drupal\node\NodeInterface $node */
$node = $this->entity;
if ($this->operation == 'edit') {
$form['#title'] = $this->t('<em>Edit @type</em> @title', [
'@type' => node_get_type_label($node),
'@title' => $node->label(),
]);
}
// Changed must be sent to the client, for later overwrite error checking.
$form['changed'] = [
'#type' => 'hidden',
'#default_value' => $node->getChangedTime(),
];
$form = parent::form($form, $form_state);
$form['advanced']['#attributes']['class'][] = 'entity-meta';
$form['meta'] = [
'#type' => 'details',
'#group' => 'advanced',
'#weight' => -10,
'#title' => $this->t('Status'),
'#attributes' => ['class' => ['entity-meta__header']],
'#tree' => TRUE,
'#access' => $this->currentUser->hasPermission('administer nodes'),
];
$form['meta']['published'] = [
'#type' => 'item',
'#markup' => $node->isPublished() ? $this->t('Published') : $this->t('Not published'),
'#access' => !$node->isNew(),
'#wrapper_attributes' => ['class' => ['entity-meta__title']],
];
$form['meta']['changed'] = [
'#type' => 'item',
'#title' => $this->t('Last saved'),
'#markup' => !$node->isNew() ? $this->dateFormatter->format($node->getChangedTime(), 'short') : $this->t('Not saved yet'),
'#wrapper_attributes' => ['class' => ['entity-meta__last-saved']],
];
$form['meta']['author'] = [
'#type' => 'item',
'#title' => $this->t('Author'),
'#markup' => $node->getOwner()->getAccountName(),
'#wrapper_attributes' => ['class' => ['entity-meta__author']],
];
$form['status']['#group'] = 'footer';
// Node author information for administrators.
$form['author'] = [
'#type' => 'details',
'#title' => t('Authoring information'),
'#group' => 'advanced',
'#attributes' => [
'class' => ['node-form-author'],
],
'#attached' => [
'library' => ['node/drupal.node'],
],
'#weight' => 90,
'#optional' => TRUE,
];
if (isset($form['uid'])) {
$form['uid']['#group'] = 'author';
}
if (isset($form['created'])) {
$form['created']['#group'] = 'author';
}
// Node options for administrators.
$form['options'] = [
'#type' => 'details',
'#title' => t('Promotion options'),
'#group' => 'advanced',
'#attributes' => [
'class' => ['node-form-options'],
],
'#attached' => [
'library' => ['node/drupal.node'],
],
'#weight' => 95,
'#optional' => TRUE,
];
if (isset($form['promote'])) {
$form['promote']['#group'] = 'options';
}
if (isset($form['sticky'])) {
$form['sticky']['#group'] = 'options';
}
$form['#attached']['library'][] = 'node/form';
return $form;
}
/**
* Entity builder updating the node status with the submitted value.
*
* @param string $entity_type_id
* The entity type identifier.
* @param \Drupal\node\NodeInterface $node
* The node updated with the submitted values.
* @param array $form
* The complete form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @see \Drupal\node\NodeForm::form()
*
* @deprecated in drupal:8.4.0 and is removed from drupal:9.0.0.
* The "Publish" button was removed.
*/
public function updateStatus($entity_type_id, NodeInterface $node, array $form, FormStateInterface $form_state) {
$element = $form_state->getTriggeringElement();
if (isset($element['#published_status'])) {
$element['#published_status'] ? $node->setPublished() : $node->setUnpublished();
}
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$element = parent::actions($form, $form_state);
$node = $this->entity;
$preview_mode = $node->type->entity->getPreviewMode();
$element['submit']['#access'] = $preview_mode != DRUPAL_REQUIRED || $form_state->get('has_been_previewed');
$element['preview'] = [
'#type' => 'submit',
'#access' => $preview_mode != DRUPAL_DISABLED && ($node->access('create') || $node->access('update')),
'#value' => t('Preview'),
'#weight' => 20,
'#submit' => ['::submitForm', '::preview'],
];
if (array_key_exists('delete', $element)) {
$element['delete']['#weight'] = 100;
}
return $element;
}
/**
* Form submission handler for the 'preview' action.
*
* @param $form
* An associative array containing the structure of the form.
* @param $form_state
* The current state of the form.
*/
public function preview(array $form, FormStateInterface $form_state) {
$store = $this->tempStoreFactory->get('node_preview');
$this->entity->in_preview = TRUE;
$store->set($this->entity->uuid(), $form_state);
$route_parameters = [
'node_preview' => $this->entity->uuid(),
'view_mode_id' => 'full',
];
$options = [];
$query = $this->getRequest()->query;
if ($query->has('destination')) {
$options['query']['destination'] = $query->get('destination');
$query->remove('destination');
}
$form_state->setRedirect('entity.node.preview', $route_parameters, $options);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$node = $this->entity;
$insert = $node->isNew();
$node->save();
$node_link = $node->toLink($this->t('View'))->toString();
$context = ['@type' => $node->getType(), '%title' => $node->label(), 'link' => $node_link];
$t_args = ['@type' => node_get_type_label($node), '%title' => $node->toLink()->toString()];
if ($insert) {
$this->logger('content')->notice('@type: added %title.', $context);
$this->messenger()->addStatus($this->t('@type %title has been created.', $t_args));
}
else {
$this->logger('content')->notice('@type: updated %title.', $context);
$this->messenger()->addStatus($this->t('@type %title has been updated.', $t_args));
}
if ($node->id()) {
$form_state->setValue('nid', $node->id());
$form_state->set('nid', $node->id());
if ($node->access('view')) {
$form_state->setRedirect(
'entity.node.canonical',
['node' => $node->id()]
);
}
else {
$form_state->setRedirect('<front>');
}
// Remove the preview entry from the temp store, if any.
$store = $this->tempStoreFactory->get('node_preview');
$store->delete($node->uuid());
}
else {
// In the unlikely case something went wrong on save, the node will be
// rebuilt and node form redisplayed the same way as in preview.
$this->messenger()->addError($this->t('The post could not be saved.'));
$form_state->setRebuild();
}
}
}
@@ -0,0 +1,311 @@
<?php
namespace Drupal\node;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines a storage handler class that handles the node grants system.
*
* This is used to build node query access.
*
* @ingroup node_access
*/
class NodeGrantDatabaseStorage implements NodeGrantDatabaseStorageInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a NodeGrantDatabaseStorage object.
*
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(Connection $database, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager) {
$this->database = $database;
$this->moduleHandler = $module_handler;
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public function access(NodeInterface $node, $operation, AccountInterface $account) {
// Grants only support these operations.
if (!in_array($operation, ['view', 'update', 'delete'])) {
return AccessResult::neutral();
}
// If no module implements the hook or the node does not have an id there is
// no point in querying the database for access grants.
if (!$this->moduleHandler->getImplementations('node_grants') || !$node->id()) {
// Return the equivalent of the default grant, defined by
// self::writeDefault().
if ($operation === 'view') {
return AccessResult::allowedIf($node->isPublished());
}
else {
return AccessResult::neutral();
}
}
// Check the database for potential access grants.
$query = $this->database->select('node_access');
$query->addExpression('1');
// Only interested for granting in the current operation.
$query->condition('grant_' . $operation, 1, '>=');
// Check for grants for this node and the correct langcode.
$nids = $query->andConditionGroup()
->condition('nid', $node->id())
->condition('langcode', $node->language()->getId());
// If the node is published, also take the default grant into account. The
// default is saved with a node ID of 0.
$status = $node->isPublished();
if ($status) {
$nids = $query->orConditionGroup()
->condition($nids)
->condition('nid', 0);
}
$query->condition($nids);
$query->range(0, 1);
$grants = static::buildGrantsQueryCondition(node_access_grants($operation, $account));
if (count($grants) > 0) {
$query->condition($grants);
}
// Only the 'view' node grant can currently be cached; the others currently
// don't have any cacheability metadata. Hopefully, we can add that in the
// future, which would allow this access check result to be cacheable in all
// cases. For now, this must remain marked as uncacheable, even when it is
// theoretically cacheable, because we don't have the necessary metadata to
// know it for a fact.
$set_cacheability = function (AccessResult $access_result) use ($operation) {
$access_result->addCacheContexts(['user.node_grants:' . $operation]);
if ($operation !== 'view') {
$access_result->setCacheMaxAge(0);
}
return $access_result;
};
if ($query->execute()->fetchField()) {
return $set_cacheability(AccessResult::allowed());
}
else {
return $set_cacheability(AccessResult::neutral());
}
}
/**
* {@inheritdoc}
*/
public function checkAll(AccountInterface $account) {
$query = $this->database->select('node_access');
$query->addExpression('COUNT(*)');
$query
->condition('nid', 0)
->condition('grant_view', 1, '>=');
$grants = static::buildGrantsQueryCondition(node_access_grants('view', $account));
if (count($grants) > 0) {
$query->condition($grants);
}
return $query->execute()->fetchField();
}
/**
* {@inheritdoc}
*/
public function alterQuery($query, array $tables, $op, AccountInterface $account, $base_table) {
if (!$langcode = $query->getMetaData('langcode')) {
$langcode = FALSE;
}
// Find all instances of the base table being joined -- could appear
// more than once in the query, and could be aliased. Join each one to
// the node_access table.
$grants = node_access_grants($op, $account);
// If any grant exists for the specified user, then user has access to the
// node for the specified operation.
$grant_conditions = static::buildGrantsQueryCondition($grants);
$grants_exist = count($grant_conditions->conditions()) > 0;
$is_multilingual = \Drupal::languageManager()->isMultilingual();
foreach ($tables as $nalias => $tableinfo) {
$table = $tableinfo['table'];
if (!($table instanceof SelectInterface) && $table == $base_table) {
// Set the subquery.
$subquery = $this->database->select('node_access', 'na')
->fields('na', ['nid']);
// Attach conditions to the sub-query for nodes.
if ($grants_exist) {
$subquery->condition($grant_conditions);
}
$subquery->condition('na.grant_' . $op, 1, '>=');
// Add langcode-based filtering if this is a multilingual site.
if ($is_multilingual) {
// If no specific langcode to check for is given, use the grant entry
// which is set as a fallback.
// If a specific langcode is given, use the grant entry for it.
if ($langcode === FALSE) {
$subquery->condition('na.fallback', 1, '=');
}
else {
$subquery->condition('na.langcode', $langcode, '=');
}
}
$field = 'nid';
// Now handle entities.
$subquery->where("$nalias.$field = na.nid");
$query->exists($subquery);
}
}
}
/**
* {@inheritdoc}
*/
public function write(NodeInterface $node, array $grants, $realm = NULL, $delete = TRUE) {
if ($delete) {
$query = $this->database->delete('node_access')->condition('nid', $node->id());
if ($realm) {
$query->condition('realm', [$realm, 'all'], 'IN');
}
$query->execute();
}
// Only perform work when node_access modules are active.
if (!empty($grants) && count($this->moduleHandler->getImplementations('node_grants'))) {
$query = $this->database->insert('node_access')->fields(['nid', 'langcode', 'fallback', 'realm', 'gid', 'grant_view', 'grant_update', 'grant_delete']);
// If we have defined a granted langcode, use it. But if not, add a grant
// for every language this node is translated to.
$fallback_langcode = $node->getUntranslated()->language()->getId();
foreach ($grants as $grant) {
if ($realm && $realm != $grant['realm']) {
continue;
}
if (isset($grant['langcode'])) {
$grant_languages = [$grant['langcode'] => $this->languageManager->getLanguage($grant['langcode'])];
}
else {
$grant_languages = $node->getTranslationLanguages(TRUE);
}
foreach ($grant_languages as $grant_langcode => $grant_language) {
// Only write grants; denies are implicit.
if ($grant['grant_view'] || $grant['grant_update'] || $grant['grant_delete']) {
$grant['nid'] = $node->id();
$grant['langcode'] = $grant_langcode;
// The record with the original langcode is used as the fallback.
if ($grant['langcode'] == $fallback_langcode) {
$grant['fallback'] = 1;
}
else {
$grant['fallback'] = 0;
}
$query->values($grant);
}
}
}
$query->execute();
}
}
/**
* {@inheritdoc}
*/
public function delete() {
$this->database->truncate('node_access')->execute();
}
/**
* {@inheritdoc}
*/
public function writeDefault() {
$this->database->insert('node_access')
->fields([
'nid' => 0,
'realm' => 'all',
'gid' => 0,
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
])
->execute();
}
/**
* {@inheritdoc}
*/
public function count() {
return $this->database->query('SELECT COUNT(*) FROM {node_access}')->fetchField();
}
/**
* {@inheritdoc}
*/
public function deleteNodeRecords(array $nids) {
$this->database->delete('node_access')
->condition('nid', $nids, 'IN')
->execute();
}
/**
* Creates a query condition from an array of node access grants.
*
* @param array $node_access_grants
* An array of grants, as returned by node_access_grants().
* @return \Drupal\Core\Database\Query\Condition
* A condition object to be passed to $query->condition().
*
* @see node_access_grants()
*/
protected static function buildGrantsQueryCondition(array $node_access_grants) {
$grants = new Condition("OR");
foreach ($node_access_grants as $realm => $gids) {
if (!empty($gids)) {
$and = new Condition('AND');
$grants->condition($and
->condition('gid', $gids, 'IN')
->condition('realm', $realm)
);
}
}
return $grants;
}
}
@@ -0,0 +1,127 @@
<?php
namespace Drupal\node;
use Drupal\Core\Session\AccountInterface;
/**
* Provides an interface for node access grant storage.
*
* @ingroup node_access
*/
interface NodeGrantDatabaseStorageInterface {
/**
* Checks all grants for a given account.
*
* @param \Drupal\Core\Session\AccountInterface $account
* A user object representing the user for whom the operation is to be
* performed.
*
* @return int
* Status of the access check.
*/
public function checkAll(AccountInterface $account);
/**
* Alters a query when node access is required.
*
* @param mixed $query
* Query that is being altered.
* @param array $tables
* A list of tables that need to be part of the alter.
* @param string $op
* The operation to be performed on the node. Possible values are:
* - "view"
* - "update"
* - "delete"
* - "create"
* @param \Drupal\Core\Session\AccountInterface $account
* A user object representing the user for whom the operation is to be
* performed.
* @param string $base_table
* The base table of the query.
*
* @return int
* Status of the access check.
*/
public function alterQuery($query, array $tables, $op, AccountInterface $account, $base_table);
/**
* Writes a list of grants to the database, deleting previously saved ones.
*
* If a realm is provided, it will only delete grants from that realm, but
* it will always delete a grant from the 'all' realm. Modules that use
* node access can use this method when doing mass updates due to widespread
* permission changes.
*
* Note: Don't call this method directly from a contributed module. Call
* \Drupal\node\NodeAccessControlHandlerInterface::acquireGrants() instead.
*
* @param \Drupal\node\NodeInterface $node
* The node whose grants are being written.
* @param array $grants
* A list of grants to write. Each grant is an array that must contain the
* following keys: realm, gid, grant_view, grant_update, grant_delete.
* The realm is specified by a particular module; the gid is as well, and
* is a module-defined id to define grant privileges. each grant_* field
* is a boolean value.
* @param string $realm
* (optional) If provided, read/write grants for that realm only. Defaults to
* NULL.
* @param bool $delete
* (optional) If false, does not delete records. This is only for optimization
* purposes, and assumes the caller has already performed a mass delete of
* some form. Defaults to TRUE.
*/
public function write(NodeInterface $node, array $grants, $realm = NULL, $delete = TRUE);
/**
* Deletes all node access entries.
*/
public function delete();
/**
* Creates the default node access grant entry.
*/
public function writeDefault();
/**
* Determines access to nodes based on node grants.
*
* @param \Drupal\node\NodeInterface $node
* The entity for which to check 'create' access.
* @param string $operation
* The entity operation. Usually one of 'view', 'edit', 'create' or
* 'delete'.
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to check access.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result, either allowed or neutral. If there are no node
* grants, the default grant defined by writeDefault() is applied.
*
* @see hook_node_grants()
* @see hook_node_access_records()
* @see \Drupal\node\NodeGrantDatabaseStorageInterface::writeDefault()
*/
public function access(NodeInterface $node, $operation, AccountInterface $account);
/**
* Counts available node grants.
*
* @return int
* Returns the amount of node grants.
*/
public function count();
/**
* Remove the access records belonging to certain nodes.
*
* @param array $nids
* A list of node IDs. The grant records belonging to these nodes will be
* deleted.
*/
public function deleteNodeRecords(array $nids);
}
+178
View File
@@ -0,0 +1,178 @@
<?php
namespace Drupal\node;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\user\EntityOwnerInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\ContentEntityInterface;
/**
* Provides an interface defining a node entity.
*/
interface NodeInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface, RevisionLogInterface, EntityPublishedInterface {
/**
* Denotes that the node is not published.
*/
const NOT_PUBLISHED = 0;
/**
* Denotes that the node is published.
*/
const PUBLISHED = 1;
/**
* Denotes that the node is not promoted to the front page.
*/
const NOT_PROMOTED = 0;
/**
* Denotes that the node is promoted to the front page.
*/
const PROMOTED = 1;
/**
* Denotes that the node is not sticky at the top of the page.
*/
const NOT_STICKY = 0;
/**
* Denotes that the node is sticky at the top of the page.
*/
const STICKY = 1;
/**
* Gets the node type.
*
* @return string
* The node type.
*/
public function getType();
/**
* Gets the node title.
*
* @return string
* Title of the node.
*/
public function getTitle();
/**
* Sets the node title.
*
* @param string $title
* The node title.
*
* @return $this
* The called node entity.
*/
public function setTitle($title);
/**
* Gets the node creation timestamp.
*
* @return int
* Creation timestamp of the node.
*/
public function getCreatedTime();
/**
* Sets the node creation timestamp.
*
* @param int $timestamp
* The node creation timestamp.
*
* @return $this
* The called node entity.
*/
public function setCreatedTime($timestamp);
/**
* Returns the node promotion status.
*
* @return bool
* TRUE if the node is promoted.
*/
public function isPromoted();
/**
* Sets the node promoted status.
*
* @param bool $promoted
* TRUE to set this node to promoted, FALSE to set it to not promoted.
*
* @return $this
* The called node entity.
*/
public function setPromoted($promoted);
/**
* Returns the node sticky status.
*
* @return bool
* TRUE if the node is sticky.
*/
public function isSticky();
/**
* Sets the node sticky status.
*
* @param bool $sticky
* TRUE to set this node to sticky, FALSE to set it to not sticky.
*
* @return $this
* The called node entity.
*/
public function setSticky($sticky);
/**
* Gets the node revision creation timestamp.
*
* @return int
* The UNIX timestamp of when this revision was created.
*/
public function getRevisionCreationTime();
/**
* Sets the node revision creation timestamp.
*
* @param int $timestamp
* The UNIX timestamp of when this revision was created.
*
* @return $this
* The called node entity.
*/
public function setRevisionCreationTime($timestamp);
/**
* Gets the node revision author.
*
* @return \Drupal\user\UserInterface
* The user entity for the revision author.
*
* @deprecated in drupal:8.2.0 and is removed from drupal:9.0.0. Use
* \Drupal\Core\Entity\RevisionLogInterface::getRevisionUser() instead.
*
* @see https://www.drupal.org/node/3069750
*/
public function getRevisionAuthor();
/**
* Sets the node revision author.
*
* @param int $uid
* The user ID of the revision author.
*
* @return $this
* The called node entity.
*
* @deprecated in drupal:8.2.0 and is removed from drupal:9.0.0. Use
* \Drupal\Core\Entity\RevisionLogInterface::setRevisionUserId() instead.
*
* @see https://www.drupal.org/node/3069750
*/
public function setRevisionAuthorId($uid);
}
@@ -0,0 +1,124 @@
<?php
namespace Drupal\node;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class to build a listing of node entities.
*
* @see \Drupal\node\Entity\Node
*/
class NodeListBuilder extends EntityListBuilder {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* Constructs a new NodeListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination
* The redirect destination service.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, DateFormatterInterface $date_formatter, RedirectDestinationInterface $redirect_destination) {
parent::__construct($entity_type, $storage);
$this->dateFormatter = $date_formatter;
$this->redirectDestination = $redirect_destination;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('date.formatter'),
$container->get('redirect.destination')
);
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
// Enable language column and filter if multiple languages are added.
$header = [
'title' => $this->t('Title'),
'type' => [
'data' => $this->t('Content type'),
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
],
'author' => [
'data' => $this->t('Author'),
'class' => [RESPONSIVE_PRIORITY_LOW],
],
'status' => $this->t('Status'),
'changed' => [
'data' => $this->t('Updated'),
'class' => [RESPONSIVE_PRIORITY_LOW],
],
];
if (\Drupal::languageManager()->isMultilingual()) {
$header['language_name'] = [
'data' => $this->t('Language'),
'class' => [RESPONSIVE_PRIORITY_LOW],
];
}
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/** @var \Drupal\node\NodeInterface $entity */
$mark = [
'#theme' => 'mark',
'#mark_type' => node_mark($entity->id(), $entity->getChangedTime()),
];
$langcode = $entity->language()->getId();
$uri = $entity->toUrl();
$options = $uri->getOptions();
$options += ($langcode != LanguageInterface::LANGCODE_NOT_SPECIFIED && isset($languages[$langcode]) ? ['language' => $languages[$langcode]] : []);
$uri->setOptions($options);
$row['title']['data'] = [
'#type' => 'link',
'#title' => $entity->label(),
'#suffix' => ' ' . \Drupal::service('renderer')->render($mark),
'#url' => $uri,
];
$row['type'] = node_get_type_label($entity);
$row['author']['data'] = [
'#theme' => 'username',
'#account' => $entity->getOwner(),
];
$row['status'] = $entity->isPublished() ? $this->t('published') : $this->t('not published');
$row['changed'] = $this->dateFormatter->format($entity->getChangedTime(), 'short');
$language_manager = \Drupal::languageManager();
if ($language_manager->isMultilingual()) {
$row['language_name'] = $language_manager->getLanguageName($langcode);
}
$row['operations']['data'] = $this->buildOperations($entity);
return $row + parent::buildRow($entity);
}
}
@@ -0,0 +1,76 @@
<?php
namespace Drupal\node;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\node\Entity\NodeType;
/**
* Provides dynamic permissions for nodes of different types.
*/
class NodePermissions {
use StringTranslationTrait;
/**
* Returns an array of node type permissions.
*
* @return array
* The node type permissions.
* @see \Drupal\user\PermissionHandlerInterface::getPermissions()
*/
public function nodeTypePermissions() {
$perms = [];
// Generate node permissions for all node types.
foreach (NodeType::loadMultiple() as $type) {
$perms += $this->buildPermissions($type);
}
return $perms;
}
/**
* Returns a list of node permissions for a given node type.
*
* @param \Drupal\node\Entity\NodeType $type
* The node type.
*
* @return array
* An associative array of permission names and descriptions.
*/
protected function buildPermissions(NodeType $type) {
$type_id = $type->id();
$type_params = ['%type_name' => $type->label()];
return [
"create $type_id content" => [
'title' => $this->t('%type_name: Create new content', $type_params),
],
"edit own $type_id content" => [
'title' => $this->t('%type_name: Edit own content', $type_params),
],
"edit any $type_id content" => [
'title' => $this->t('%type_name: Edit any content', $type_params),
],
"delete own $type_id content" => [
'title' => $this->t('%type_name: Delete own content', $type_params),
],
"delete any $type_id content" => [
'title' => $this->t('%type_name: Delete any content', $type_params),
],
"view $type_id revisions" => [
'title' => $this->t('%type_name: View revisions', $type_params),
'description' => t('To view a revision, you also need permission to view the content item.'),
],
"revert $type_id revisions" => [
'title' => $this->t('%type_name: Revert revisions', $type_params),
'description' => t('To revert a revision, you also need permission to edit the content item.'),
],
"delete $type_id revisions" => [
'title' => $this->t('%type_name: Delete revisions', $type_params),
'description' => $this->t('To delete a revision, you also need permission to delete the content item.'),
],
];
}
}
@@ -0,0 +1,42 @@
<?php
namespace Drupal\node;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\node\EventSubscriber\NodeTranslationExceptionSubscriber;
use Drupal\node\EventSubscriber\NodeTranslationMigrateSubscriber;
use Symfony\Component\DependencyInjection\Reference;
/**
* Registers services in the container.
*/
class NodeServiceProvider implements ServiceProviderInterface {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
// Register the node.node_translation_migrate service in the container if
// the migrate and language modules are enabled.
$modules = $container->getParameter('container.modules');
if (isset($modules['migrate']) && isset($modules['language'])) {
$container->register('node.node_translation_migrate', NodeTranslationMigrateSubscriber::class)
->addTag('event_subscriber')
->addArgument(new Reference('keyvalue'))
->addArgument(new Reference('state'));
}
// Register the node.node_translation_exception service in the container if
// the language module is enabled.
if (isset($modules['language'])) {
$container->register('node.node_translation_exception', NodeTranslationExceptionSubscriber::class)
->addTag('event_subscriber')
->addArgument(new Reference('keyvalue'))
->addArgument(new Reference('language_manager'))
->addArgument(new Reference('url_generator'))
->addArgument(new Reference('state'));
}
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace Drupal\node;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Language\LanguageInterface;
/**
* Defines the storage handler class for nodes.
*
* This extends the base storage class, adding required special handling for
* node entities.
*/
class NodeStorage extends SqlContentEntityStorage implements NodeStorageInterface {
/**
* {@inheritdoc}
*/
public function revisionIds(NodeInterface $node) {
return $this->database->query(
'SELECT vid FROM {' . $this->getRevisionTable() . '} WHERE nid=:nid ORDER BY vid',
[':nid' => $node->id()]
)->fetchCol();
}
/**
* {@inheritdoc}
*/
public function userRevisionIds(AccountInterface $account) {
return $this->database->query(
'SELECT vid FROM {' . $this->getRevisionDataTable() . '} WHERE uid = :uid ORDER BY vid',
[':uid' => $account->id()]
)->fetchCol();
}
/**
* {@inheritdoc}
*/
public function countDefaultLanguageRevisions(NodeInterface $node) {
return $this->database->query('SELECT COUNT(*) FROM {' . $this->getRevisionDataTable() . '} WHERE nid = :nid AND default_langcode = 1', [':nid' => $node->id()])->fetchField();
}
/**
* {@inheritdoc}
*/
public function updateType($old_type, $new_type) {
return $this->database->update($this->getBaseTable())
->fields(['type' => $new_type])
->condition('type', $old_type)
->execute();
}
/**
* {@inheritdoc}
*/
public function clearRevisionsLanguage(LanguageInterface $language) {
return $this->database->update($this->getRevisionTable())
->fields(['langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED])
->condition('langcode', $language->getId())
->execute();
}
}
@@ -0,0 +1,68 @@
<?php
namespace Drupal\node;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines an interface for node entity storage classes.
*/
interface NodeStorageInterface extends ContentEntityStorageInterface {
/**
* Gets a list of node revision IDs for a specific node.
*
* @param \Drupal\node\NodeInterface $node
* The node entity.
*
* @return int[]
* Node revision IDs (in ascending order).
*/
public function revisionIds(NodeInterface $node);
/**
* Gets a list of revision IDs having a given user as node author.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The user entity.
*
* @return int[]
* Node revision IDs (in ascending order).
*/
public function userRevisionIds(AccountInterface $account);
/**
* Counts the number of revisions in the default language.
*
* @param \Drupal\node\NodeInterface $node
* The node entity.
*
* @return int
* The number of revisions in the default language.
*/
public function countDefaultLanguageRevisions(NodeInterface $node);
/**
* Updates all nodes of one type to be of another type.
*
* @param string $old_type
* The current node type of the nodes.
* @param string $new_type
* The new node type of the nodes.
*
* @return int
* The number of nodes whose node type field was modified.
*/
public function updateType($old_type, $new_type);
/**
* Unsets the language for all nodes with the given language.
*
* @param \Drupal\Core\Language\LanguageInterface $language
* The language object.
*/
public function clearRevisionsLanguage(LanguageInterface $language);
}
@@ -0,0 +1,72 @@
<?php
namespace Drupal\node;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Defines the node schema handler.
*/
class NodeStorageSchema extends SqlContentEntityStorageSchema {
/**
* {@inheritdoc}
*/
protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
$schema = parent::getEntitySchema($entity_type, $reset);
if ($data_table = $this->storage->getDataTable()) {
$schema[$data_table]['indexes'] += [
'node__frontpage' => ['promote', 'status', 'sticky', 'created'],
'node__title_type' => ['title', ['type', 4]],
];
}
return $schema;
}
/**
* {@inheritdoc}
*/
protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
$schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
$field_name = $storage_definition->getName();
if ($table_name == 'node_revision') {
switch ($field_name) {
case 'langcode':
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
break;
case 'revision_uid':
$this->addSharedTableFieldForeignKey($storage_definition, $schema, 'users', 'uid');
break;
}
}
if ($table_name == 'node_field_data') {
switch ($field_name) {
case 'promote':
case 'status':
case 'sticky':
case 'title':
// Improves the performance of the indexes defined
// in getEntitySchema().
$schema['fields'][$field_name]['not null'] = TRUE;
break;
case 'changed':
case 'created':
// @todo Revisit index definitions:
// https://www.drupal.org/node/2015277.
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
break;
}
}
return $schema;
}
}
@@ -0,0 +1,71 @@
<?php
namespace Drupal\node;
use Drupal\content_translation\ContentTranslationHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines the translation handler for nodes.
*/
class NodeTranslationHandler extends ContentTranslationHandler {
/**
* {@inheritdoc}
*/
public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity) {
parent::entityFormAlter($form, $form_state, $entity);
if (isset($form['content_translation'])) {
// We do not need to show these values on node forms: they inherit the
// basic node property values.
$form['content_translation']['status']['#access'] = FALSE;
$form['content_translation']['name']['#access'] = FALSE;
$form['content_translation']['created']['#access'] = FALSE;
}
$form_object = $form_state->getFormObject();
$form_langcode = $form_object->getFormLangcode($form_state);
$translations = $entity->getTranslationLanguages();
$status_translatable = NULL;
// Change the submit button labels if there was a status field they affect
// in which case their publishing / unpublishing may or may not apply
// to all translations.
if (!$entity->isNew() && (!isset($translations[$form_langcode]) || count($translations) > 1)) {
foreach ($entity->getFieldDefinitions() as $property_name => $definition) {
if ($property_name == 'status') {
$status_translatable = $definition->isTranslatable();
}
}
if (isset($status_translatable)) {
if (isset($form['actions']['submit'])) {
$form['actions']['submit']['#value'] .= ' ' . ($status_translatable ? t('(this translation)') : t('(all translations)'));
}
}
}
}
/**
* {@inheritdoc}
*/
protected function entityFormTitle(EntityInterface $entity) {
$type_name = node_get_type_label($entity);
return t('<em>Edit @type</em> @title', ['@type' => $type_name, '@title' => $entity->label()]);
}
/**
* {@inheritdoc}
*/
public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, FormStateInterface $form_state) {
if ($form_state->hasValue('content_translation')) {
$translation = &$form_state->getValue('content_translation');
$translation['status'] = $entity->isPublished();
$account = $entity->uid->entity;
$translation['uid'] = $account ? $account->id() : 0;
$translation['created'] = $this->dateFormatter->format($entity->created->value, 'custom', 'Y-m-d H:i:s O');
}
parent::entityFormEntityBuild($entity_type, $entity, $form, $form_state);
}
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\node;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the node type entity type.
*
* @see \Drupal\node\Entity\NodeType
*/
class NodeTypeAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
switch ($operation) {
case 'view':
return AccessResult::allowedIfHasPermission($account, 'access content');
case 'delete':
if ($entity->isLocked()) {
return AccessResult::forbidden()->addCacheableDependency($entity);
}
else {
return parent::checkAccess($entity, $operation, $account)->addCacheableDependency($entity);
}
break;
default:
return parent::checkAccess($entity, $operation, $account);
}
}
}
+267
View File
@@ -0,0 +1,267 @@
<?php
namespace Drupal\node;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Entity\BundleEntityFormBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\language\Entity\ContentLanguageSettings;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form handler for node type forms.
*
* @internal
*/
class NodeTypeForm extends BundleEntityFormBase {
use DeprecatedServicePropertyTrait;
/**
* {@inheritdoc}
*/
protected $deprecatedProperties = [
'entityManager' => 'entity.manager',
];
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* Constructs the NodeTypeForm object.
*
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
*/
public function __construct(EntityFieldManagerInterface $entity_field_manager) {
$this->entityFieldManager = $entity_field_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_field.manager')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$type = $this->entity;
if ($this->operation == 'add') {
$form['#title'] = $this->t('Add content type');
$fields = $this->entityFieldManager->getBaseFieldDefinitions('node');
// Create a node with a fake bundle using the type's UUID so that we can
// get the default values for workflow settings.
// @todo Make it possible to get default values without an entity.
// https://www.drupal.org/node/2318187
$node = $this->entityTypeManager->getStorage('node')->create(['type' => $type->uuid()]);
}
else {
$form['#title'] = $this->t('Edit %label content type', ['%label' => $type->label()]);
$fields = $this->entityFieldManager->getFieldDefinitions('node', $type->id());
// Create a node to get the current values for workflow settings fields.
$node = $this->entityTypeManager->getStorage('node')->create(['type' => $type->id()]);
}
$form['name'] = [
'#title' => t('Name'),
'#type' => 'textfield',
'#default_value' => $type->label(),
'#description' => t('The human-readable name of this content type. This text will be displayed as part of the list on the <em>Add content</em> page. This name must be unique.'),
'#required' => TRUE,
'#size' => 30,
];
$form['type'] = [
'#type' => 'machine_name',
'#default_value' => $type->id(),
'#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
'#disabled' => $type->isLocked(),
'#machine_name' => [
'exists' => ['Drupal\node\Entity\NodeType', 'load'],
'source' => ['name'],
],
'#description' => t('A unique machine-readable name for this content type. It must only contain lowercase letters, numbers, and underscores. This name will be used for constructing the URL of the %node-add page, in which underscores will be converted into hyphens.', [
'%node-add' => t('Add content'),
]),
];
$form['description'] = [
'#title' => t('Description'),
'#type' => 'textarea',
'#default_value' => $type->getDescription(),
'#description' => t('This text will be displayed on the <em>Add new content</em> page.'),
];
$form['additional_settings'] = [
'#type' => 'vertical_tabs',
'#attached' => [
'library' => ['node/drupal.content_types'],
],
];
$form['submission'] = [
'#type' => 'details',
'#title' => t('Submission form settings'),
'#group' => 'additional_settings',
'#open' => TRUE,
];
$form['submission']['title_label'] = [
'#title' => t('Title field label'),
'#type' => 'textfield',
'#default_value' => $fields['title']->getLabel(),
'#required' => TRUE,
];
$form['submission']['preview_mode'] = [
'#type' => 'radios',
'#title' => t('Preview before submitting'),
'#default_value' => $type->getPreviewMode(),
'#options' => [
DRUPAL_DISABLED => t('Disabled'),
DRUPAL_OPTIONAL => t('Optional'),
DRUPAL_REQUIRED => t('Required'),
],
];
$form['submission']['help'] = [
'#type' => 'textarea',
'#title' => t('Explanation or submission guidelines'),
'#default_value' => $type->getHelp(),
'#description' => t('This text will be displayed at the top of the page when creating or editing content of this type.'),
];
$form['workflow'] = [
'#type' => 'details',
'#title' => t('Publishing options'),
'#group' => 'additional_settings',
];
$workflow_options = [
'status' => $node->status->value,
'promote' => $node->promote->value,
'sticky' => $node->sticky->value,
'revision' => $type->shouldCreateNewRevision(),
];
// Prepare workflow options to be used for 'checkboxes' form element.
$keys = array_keys(array_filter($workflow_options));
$workflow_options = array_combine($keys, $keys);
$form['workflow']['options'] = [
'#type' => 'checkboxes',
'#title' => t('Default options'),
'#default_value' => $workflow_options,
'#options' => [
'status' => t('Published'),
'promote' => t('Promoted to front page'),
'sticky' => t('Sticky at top of lists'),
'revision' => t('Create new revision'),
],
'#description' => t('Users with sufficient access rights will be able to override these options.'),
];
if ($this->moduleHandler->moduleExists('language')) {
$form['language'] = [
'#type' => 'details',
'#title' => t('Language settings'),
'#group' => 'additional_settings',
];
$language_configuration = ContentLanguageSettings::loadByEntityTypeBundle('node', $type->id());
$form['language']['language_configuration'] = [
'#type' => 'language_configuration',
'#entity_information' => [
'entity_type' => 'node',
'bundle' => $type->id(),
],
'#default_value' => $language_configuration,
];
}
$form['display'] = [
'#type' => 'details',
'#title' => t('Display settings'),
'#group' => 'additional_settings',
];
$form['display']['display_submitted'] = [
'#type' => 'checkbox',
'#title' => t('Display author and date information'),
'#default_value' => $type->displaySubmitted(),
'#description' => t('Author username and publish date will be displayed.'),
];
return $this->protectBundleIdElement($form);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = t('Save content type');
return $actions;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$id = trim($form_state->getValue('type'));
// '0' is invalid, since elsewhere we check it using empty().
if ($id == '0') {
$form_state->setErrorByName('type', $this->t("Invalid machine-readable name. Enter a name other than %invalid.", ['%invalid' => $id]));
}
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$type = $this->entity;
$type->setNewRevision($form_state->getValue(['options', 'revision']));
$type->set('type', trim($type->id()));
$type->set('name', trim($type->label()));
$status = $type->save();
$t_args = ['%name' => $type->label()];
if ($status == SAVED_UPDATED) {
$this->messenger()->addStatus($this->t('The content type %name has been updated.', $t_args));
}
elseif ($status == SAVED_NEW) {
node_add_body_field($type);
$this->messenger()->addStatus($this->t('The content type %name has been added.', $t_args));
$context = array_merge($t_args, ['link' => $type->toLink($this->t('View'), 'collection')->toString()]);
$this->logger('node')->notice('Added content type %name.', $context);
}
$fields = $this->entityFieldManager->getFieldDefinitions('node', $type->id());
// Update title field definition.
$title_field = $fields['title'];
$title_label = $form_state->getValue('title_label');
if ($title_field->getLabel() != $title_label) {
$title_field->getConfig($type->id())->setLabel($title_label)->save();
}
// Update workflow options.
// @todo Make it possible to get default values without an entity.
// https://www.drupal.org/node/2318187
$node = $this->entityTypeManager->getStorage('node')->create(['type' => $type->id()]);
foreach (['status', 'promote', 'sticky'] as $field_name) {
$value = (bool) $form_state->getValue(['options', $field_name]);
if ($node->$field_name->value != $value) {
$fields[$field_name]->getConfig($type->id())->setDefaultValue($value)->save();
}
}
$this->entityFieldManager->clearCachedFieldDefinitions();
$form_state->setRedirectUrl($type->toUrl('collection'));
}
}
@@ -0,0 +1,91 @@
<?php
namespace Drupal\node;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\RevisionableEntityBundleInterface;
/**
* Provides an interface defining a node type entity.
*/
interface NodeTypeInterface extends ConfigEntityInterface, RevisionableEntityBundleInterface {
/**
* Determines whether the node type is locked.
*
* @return string|false
* The module name that locks the type or FALSE.
*/
public function isLocked();
/**
* Gets whether a new revision should be created by default.
*
* @return bool
* TRUE if a new revision should be created by default.
*
* @deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use
* Drupal\Core\Entity\RevisionableEntityBundleInterface::shouldCreateNewRevision()
* instead.
*
* @see https://www.drupal.org/node/3067365
*/
public function isNewRevision();
/**
* Sets whether a new revision should be created by default.
*
* @param bool $new_revision
* TRUE if a new revision should be created by default.
*/
public function setNewRevision($new_revision);
/**
* Gets whether 'Submitted by' information should be shown.
*
* @return bool
* TRUE if the submitted by information should be shown.
*/
public function displaySubmitted();
/**
* Sets whether 'Submitted by' information should be shown.
*
* @param bool $display_submitted
* TRUE if the submitted by information should be shown.
*/
public function setDisplaySubmitted($display_submitted);
/**
* Gets the preview mode.
*
* @return int
* DRUPAL_DISABLED, DRUPAL_OPTIONAL or DRUPAL_REQUIRED.
*/
public function getPreviewMode();
/**
* Sets the preview mode.
*
* @param int $preview_mode
* DRUPAL_DISABLED, DRUPAL_OPTIONAL or DRUPAL_REQUIRED.
*/
public function setPreviewMode($preview_mode);
/**
* Gets the help information.
*
* @return string
* The help information of this node type.
*/
public function getHelp();
/**
* Gets the description.
*
* @return string
* The description of this node type.
*/
public function getDescription();
}
@@ -0,0 +1,64 @@
<?php
namespace Drupal\node;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Url;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines a class to build a listing of node type entities.
*
* @see \Drupal\node\Entity\NodeType
*/
class NodeTypeListBuilder extends ConfigEntityListBuilder {
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['title'] = t('Name');
$header['description'] = [
'data' => t('Description'),
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
];
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['title'] = [
'data' => $entity->label(),
'class' => ['menu-label'],
];
$row['description']['data'] = ['#markup' => $entity->getDescription()];
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
// Place the edit operation after the operations added by field_ui.module
// which have the weights 15, 20, 25.
if (isset($operations['edit'])) {
$operations['edit']['weight'] = 30;
}
return $operations;
}
/**
* {@inheritdoc}
*/
public function render() {
$build = parent::render();
$build['table']['#empty'] = $this->t('No content types available. <a href=":link">Add content type</a>.', [
':link' => Url::fromRoute('node.type_add')->toString(),
]);
return $build;
}
}
@@ -0,0 +1,160 @@
<?php
namespace Drupal\node;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityViewBuilder;
use Drupal\Core\Render\Element\Link;
use Drupal\Core\Security\TrustedCallbackInterface;
/**
* View builder handler for nodes.
*/
class NodeViewBuilder extends EntityViewBuilder implements TrustedCallbackInterface {
/**
* {@inheritdoc}
*/
public function buildComponents(array &$build, array $entities, array $displays, $view_mode) {
/** @var \Drupal\node\NodeInterface[] $entities */
if (empty($entities)) {
return;
}
parent::buildComponents($build, $entities, $displays, $view_mode);
foreach ($entities as $id => $entity) {
$bundle = $entity->bundle();
$display = $displays[$bundle];
if ($display->getComponent('links')) {
$build[$id]['links'] = [
'#lazy_builder' => [
get_called_class() . '::renderLinks', [
$entity->id(),
$view_mode,
$entity->language()->getId(),
!empty($entity->in_preview),
$entity->isDefaultRevision() ? NULL : $entity->getLoadedRevisionId(),
],
],
];
}
// Add Language field text element to node render array.
if ($display->getComponent('langcode')) {
$build[$id]['langcode'] = [
'#type' => 'item',
'#title' => t('Language'),
'#markup' => $entity->language()->getName(),
'#prefix' => '<div id="field-language-display">',
'#suffix' => '</div>',
];
}
}
}
/**
* {@inheritdoc}
*/
protected function getBuildDefaults(EntityInterface $entity, $view_mode) {
$defaults = parent::getBuildDefaults($entity, $view_mode);
// Don't cache nodes that are in 'preview' mode.
if (isset($defaults['#cache']) && isset($entity->in_preview)) {
unset($defaults['#cache']);
}
return $defaults;
}
/**
* #lazy_builder callback; builds a node's links.
*
* @param string $node_entity_id
* The node entity ID.
* @param string $view_mode
* The view mode in which the node entity is being viewed.
* @param string $langcode
* The language in which the node entity is being viewed.
* @param bool $is_in_preview
* Whether the node is currently being previewed.
* @param $revision_id
* (optional) The identifier of the node revision to be loaded. If none
* is provided, the default revision will be loaded.
*
* @return array
* A renderable array representing the node links.
*/
public static function renderLinks($node_entity_id, $view_mode, $langcode, $is_in_preview, $revision_id = NULL) {
$links = [
'#theme' => 'links__node',
'#pre_render' => [[Link::class, 'preRenderLinks']],
'#attributes' => ['class' => ['links', 'inline']],
];
if (!$is_in_preview) {
$storage = \Drupal::entityTypeManager()->getStorage('node');
/** @var \Drupal\node\NodeInterface $revision */
$revision = !isset($revision_id) ? $storage->load($node_entity_id) : $storage->loadRevision($revision_id);
$entity = $revision->getTranslation($langcode);
$links['node'] = static::buildLinks($entity, $view_mode);
// Allow other modules to alter the node links.
$hook_context = [
'view_mode' => $view_mode,
'langcode' => $langcode,
];
\Drupal::moduleHandler()->alter('node_links', $links, $entity, $hook_context);
}
return $links;
}
/**
* Build the default links (Read more) for a node.
*
* @param \Drupal\node\NodeInterface $entity
* The node object.
* @param string $view_mode
* A view mode identifier.
*
* @return array
* An array that can be processed by drupal_pre_render_links().
*/
protected static function buildLinks(NodeInterface $entity, $view_mode) {
$links = [];
// Always display a read more link on teasers because we have no way
// to know when a teaser view is different than a full view.
if ($view_mode == 'teaser') {
$node_title_stripped = strip_tags($entity->label());
$links['node-readmore'] = [
'title' => t('Read more<span class="visually-hidden"> about @title</span>', [
'@title' => $node_title_stripped,
]),
'url' => $entity->toUrl(),
'language' => $entity->language(),
'attributes' => [
'rel' => 'tag',
'title' => $node_title_stripped,
],
];
}
return [
'#theme' => 'links__node__node',
'#links' => $links,
'#attributes' => ['class' => ['links', 'inline']],
];
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
$callbacks = parent::trustedCallbacks();
$callbacks[] = 'renderLinks';
return $callbacks;
}
}
+392
View File
@@ -0,0 +1,392 @@
<?php
namespace Drupal\node;
use Drupal\views\EntityViewsData;
/**
* Provides the views data for the node entity type.
*/
class NodeViewsData extends EntityViewsData {
/**
* {@inheritdoc}
*/
public function getViewsData() {
$data = parent::getViewsData();
$data['node_field_data']['table']['base']['weight'] = -10;
$data['node_field_data']['table']['base']['access query tag'] = 'node_access';
$data['node_field_data']['table']['wizard_id'] = 'node';
$data['node_field_data']['nid']['argument'] = [
'id' => 'node_nid',
'name field' => 'title',
'numeric' => TRUE,
'validate type' => 'nid',
];
$data['node_field_data']['title']['field']['default_formatter_settings'] = ['link_to_entity' => TRUE];
$data['node_field_data']['title']['field']['link_to_node default'] = TRUE;
$data['node_field_data']['type']['argument']['id'] = 'node_type';
$data['node_field_data']['langcode']['help'] = $this->t('The language of the content or translation.');
$data['node_field_data']['status']['filter']['label'] = $this->t('Published status');
$data['node_field_data']['status']['filter']['type'] = 'yes-no';
// Use status = 1 instead of status <> 0 in WHERE statement.
$data['node_field_data']['status']['filter']['use_equal'] = TRUE;
$data['node_field_data']['status_extra'] = [
'title' => $this->t('Published status or admin user'),
'help' => $this->t('Filters out unpublished content if the current user cannot view it.'),
'filter' => [
'field' => 'status',
'id' => 'node_status',
'label' => $this->t('Published status or admin user'),
],
];
$data['node_field_data']['promote']['help'] = $this->t('A boolean indicating whether the node is visible on the front page.');
$data['node_field_data']['promote']['filter']['label'] = $this->t('Promoted to front page status');
$data['node_field_data']['promote']['filter']['type'] = 'yes-no';
$data['node_field_data']['sticky']['help'] = $this->t('A boolean indicating whether the node should sort to the top of content lists.');
$data['node_field_data']['sticky']['filter']['label'] = $this->t('Sticky status');
$data['node_field_data']['sticky']['filter']['type'] = 'yes-no';
$data['node_field_data']['sticky']['sort']['help'] = $this->t('Whether or not the content is sticky. To list sticky content first, set this to descending.');
$data['node']['node_bulk_form'] = [
'title' => $this->t('Node operations bulk form'),
'help' => $this->t('Add a form element that lets you run operations on multiple nodes.'),
'field' => [
'id' => 'node_bulk_form',
],
];
// Bogus fields for aliasing purposes.
// @todo Add similar support to any date field
// @see https://www.drupal.org/node/2337507
$data['node_field_data']['created_fulldate'] = [
'title' => $this->t('Created date'),
'help' => $this->t('Date in the form of CCYYMMDD.'),
'argument' => [
'field' => 'created',
'id' => 'date_fulldate',
],
];
$data['node_field_data']['created_year_month'] = [
'title' => $this->t('Created year + month'),
'help' => $this->t('Date in the form of YYYYMM.'),
'argument' => [
'field' => 'created',
'id' => 'date_year_month',
],
];
$data['node_field_data']['created_year'] = [
'title' => $this->t('Created year'),
'help' => $this->t('Date in the form of YYYY.'),
'argument' => [
'field' => 'created',
'id' => 'date_year',
],
];
$data['node_field_data']['created_month'] = [
'title' => $this->t('Created month'),
'help' => $this->t('Date in the form of MM (01 - 12).'),
'argument' => [
'field' => 'created',
'id' => 'date_month',
],
];
$data['node_field_data']['created_day'] = [
'title' => $this->t('Created day'),
'help' => $this->t('Date in the form of DD (01 - 31).'),
'argument' => [
'field' => 'created',
'id' => 'date_day',
],
];
$data['node_field_data']['created_week'] = [
'title' => $this->t('Created week'),
'help' => $this->t('Date in the form of WW (01 - 53).'),
'argument' => [
'field' => 'created',
'id' => 'date_week',
],
];
$data['node_field_data']['changed_fulldate'] = [
'title' => $this->t('Updated date'),
'help' => $this->t('Date in the form of CCYYMMDD.'),
'argument' => [
'field' => 'changed',
'id' => 'date_fulldate',
],
];
$data['node_field_data']['changed_year_month'] = [
'title' => $this->t('Updated year + month'),
'help' => $this->t('Date in the form of YYYYMM.'),
'argument' => [
'field' => 'changed',
'id' => 'date_year_month',
],
];
$data['node_field_data']['changed_year'] = [
'title' => $this->t('Updated year'),
'help' => $this->t('Date in the form of YYYY.'),
'argument' => [
'field' => 'changed',
'id' => 'date_year',
],
];
$data['node_field_data']['changed_month'] = [
'title' => $this->t('Updated month'),
'help' => $this->t('Date in the form of MM (01 - 12).'),
'argument' => [
'field' => 'changed',
'id' => 'date_month',
],
];
$data['node_field_data']['changed_day'] = [
'title' => $this->t('Updated day'),
'help' => $this->t('Date in the form of DD (01 - 31).'),
'argument' => [
'field' => 'changed',
'id' => 'date_day',
],
];
$data['node_field_data']['changed_week'] = [
'title' => $this->t('Updated week'),
'help' => $this->t('Date in the form of WW (01 - 53).'),
'argument' => [
'field' => 'changed',
'id' => 'date_week',
],
];
$data['node_field_data']['uid']['help'] = $this->t('The user authoring the content. If you need more fields than the uid add the content: author relationship');
$data['node_field_data']['uid']['filter']['id'] = 'user_name';
$data['node_field_data']['uid']['relationship']['title'] = $this->t('Content author');
$data['node_field_data']['uid']['relationship']['help'] = $this->t('Relate content to the user who created it.');
$data['node_field_data']['uid']['relationship']['label'] = $this->t('author');
$data['node']['node_listing_empty'] = [
'title' => $this->t('Empty Node Frontpage behavior'),
'help' => $this->t('Provides a link to the node add overview page.'),
'area' => [
'id' => 'node_listing_empty',
],
];
$data['node_field_data']['uid_revision']['title'] = $this->t('User has a revision');
$data['node_field_data']['uid_revision']['help'] = $this->t('All nodes where a certain user has a revision');
$data['node_field_data']['uid_revision']['real field'] = 'nid';
$data['node_field_data']['uid_revision']['filter']['id'] = 'node_uid_revision';
$data['node_field_data']['uid_revision']['argument']['id'] = 'node_uid_revision';
$data['node_field_revision']['table']['wizard_id'] = 'node_revision';
// Advertise this table as a possible base table.
$data['node_field_revision']['table']['base']['help'] = $this->t('Content revision is a history of changes to content.');
$data['node_field_revision']['table']['base']['defaults']['title'] = 'title';
$data['node_field_revision']['nid']['argument'] = [
'id' => 'node_nid',
'numeric' => TRUE,
];
// @todo the NID field needs different behavior on revision/non-revision
// tables. It would be neat if this could be encoded in the base field
// definition.
$data['node_field_revision']['nid']['relationship']['id'] = 'standard';
$data['node_field_revision']['nid']['relationship']['base'] = 'node_field_data';
$data['node_field_revision']['nid']['relationship']['base field'] = 'nid';
$data['node_field_revision']['nid']['relationship']['title'] = $this->t('Content');
$data['node_field_revision']['nid']['relationship']['label'] = $this->t('Get the actual content from a content revision.');
$data['node_field_revision']['nid']['relationship']['extra'][] = [
'field' => 'langcode',
'left_field' => 'langcode',
];
$data['node_field_revision']['vid'] = [
'argument' => [
'id' => 'node_vid',
'numeric' => TRUE,
],
'relationship' => [
'id' => 'standard',
'base' => 'node_field_data',
'base field' => 'vid',
'title' => $this->t('Content'),
'label' => $this->t('Get the actual content from a content revision.'),
'extra' => [
[
'field' => 'langcode',
'left_field' => 'langcode',
],
],
],
] + $data['node_field_revision']['vid'];
$data['node_field_revision']['langcode']['help'] = $this->t('The language the original content is in.');
$data['node_revision']['revision_uid']['help'] = $this->t('The user who created the revision.');
$data['node_revision']['revision_uid']['relationship']['label'] = $this->t('revision user');
$data['node_revision']['revision_uid']['filter']['id'] = 'user_name';
$data['node_revision']['table']['join']['node_field_data']['left_field'] = 'vid';
$data['node_revision']['table']['join']['node_field_data']['field'] = 'vid';
$data['node_field_revision']['table']['wizard_id'] = 'node_field_revision';
$data['node_field_revision']['status']['filter']['label'] = $this->t('Published');
$data['node_field_revision']['status']['filter']['type'] = 'yes-no';
$data['node_field_revision']['status']['filter']['use_equal'] = TRUE;
$data['node_field_revision']['promote']['help'] = $this->t('A boolean indicating whether the node is visible on the front page.');
$data['node_field_revision']['sticky']['help'] = $this->t('A boolean indicating whether the node should sort to the top of content lists.');
$data['node_field_revision']['langcode']['help'] = $this->t('The language of the content or translation.');
$data['node_field_revision']['link_to_revision'] = [
'field' => [
'title' => $this->t('Link to revision'),
'help' => $this->t('Provide a simple link to the revision.'),
'id' => 'node_revision_link',
'click sortable' => FALSE,
],
];
$data['node_field_revision']['revert_revision'] = [
'field' => [
'title' => $this->t('Link to revert revision'),
'help' => $this->t('Provide a simple link to revert to the revision.'),
'id' => 'node_revision_link_revert',
'click sortable' => FALSE,
],
];
$data['node_field_revision']['delete_revision'] = [
'field' => [
'title' => $this->t('Link to delete revision'),
'help' => $this->t('Provide a simple link to delete the content revision.'),
'id' => 'node_revision_link_delete',
'click sortable' => FALSE,
],
];
// Define the base group of this table. Fields that don't have a group defined
// will go into this field by default.
$data['node_access']['table']['group'] = $this->t('Content access');
// For other base tables, explain how we join.
$data['node_access']['table']['join'] = [
'node_field_data' => [
'left_field' => 'nid',
'field' => 'nid',
],
];
$data['node_access']['nid'] = [
'title' => $this->t('Access'),
'help' => $this->t('Filter by access.'),
'filter' => [
'id' => 'node_access',
'help' => $this->t('Filter for content by view access. <strong>Not necessary if you are using node as your base table.</strong>'),
],
];
// Add search table, fields, filters, etc., but only if a page using the
// node_search plugin is enabled.
if (\Drupal::moduleHandler()->moduleExists('search')) {
$enabled = FALSE;
$search_page_repository = \Drupal::service('search.search_page_repository');
foreach ($search_page_repository->getActiveSearchpages() as $page) {
if ($page->getPlugin()->getPluginId() == 'node_search') {
$enabled = TRUE;
break;
}
}
if ($enabled) {
$data['node_search_index']['table']['group'] = $this->t('Search');
// Automatically join to the node table (or actually, node_field_data).
// Use a Views table alias to allow other modules to use this table too,
// if they use the search index.
$data['node_search_index']['table']['join'] = [
'node_field_data' => [
'left_field' => 'nid',
'field' => 'sid',
'table' => 'search_index',
'extra' => "node_search_index.type = 'node_search' AND node_search_index.langcode = node_field_data.langcode",
],
];
$data['node_search_total']['table']['join'] = [
'node_search_index' => [
'left_field' => 'word',
'field' => 'word',
],
];
$data['node_search_dataset']['table']['join'] = [
'node_field_data' => [
'left_field' => 'sid',
'left_table' => 'node_search_index',
'field' => 'sid',
'table' => 'search_dataset',
'extra' => 'node_search_index.type = node_search_dataset.type AND node_search_index.langcode = node_search_dataset.langcode',
'type' => 'INNER',
],
];
$data['node_search_index']['score'] = [
'title' => $this->t('Score'),
'help' => $this->t('The score of the search item. This will not be used if the search filter is not also present.'),
'field' => [
'id' => 'search_score',
'float' => TRUE,
'no group by' => TRUE,
],
'sort' => [
'id' => 'search_score',
'no group by' => TRUE,
],
];
$data['node_search_index']['keys'] = [
'title' => $this->t('Search Keywords'),
'help' => $this->t('The keywords to search for.'),
'filter' => [
'id' => 'search_keywords',
'no group by' => TRUE,
'search_type' => 'node_search',
],
'argument' => [
'id' => 'search',
'no group by' => TRUE,
'search_type' => 'node_search',
],
];
}
}
return $data;
}
}
@@ -0,0 +1,44 @@
<?php
namespace Drupal\node\PageCache;
use Drupal\Core\PageCache\ResponsePolicyInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Cache policy for node preview page.
*
* This policy rule denies caching of responses generated by the
* entity.node.preview route.
*/
class DenyNodePreview implements ResponsePolicyInterface {
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a deny node preview page cache policy.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function __construct(RouteMatchInterface $route_match) {
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public function check(Response $response, Request $request) {
if ($this->routeMatch->getRouteName() === 'entity.node.preview') {
return static::DENY;
}
}
}
@@ -0,0 +1,51 @@
<?php
namespace Drupal\node\ParamConverter;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\Routing\Route;
use Drupal\Core\ParamConverter\ParamConverterInterface;
/**
* Provides upcasting for a node entity in preview.
*/
class NodePreviewConverter implements ParamConverterInterface {
/**
* Stores the tempstore factory.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempStoreFactory;
/**
* Constructs a new NodePreviewConverter.
*
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The factory for the temp store object.
*/
public function __construct(PrivateTempStoreFactory $temp_store_factory) {
$this->tempStoreFactory = $temp_store_factory;
}
/**
* {@inheritdoc}
*/
public function convert($value, $definition, $name, array $defaults) {
$store = $this->tempStoreFactory->get('node_preview');
if ($form_state = $store->get($value)) {
return $form_state->getFormObject()->getEntity();
}
}
/**
* {@inheritdoc}
*/
public function applies($definition, $name, Route $route) {
if (!empty($definition['type']) && $definition['type'] == 'node_preview') {
return TRUE;
}
return FALSE;
}
}
@@ -0,0 +1,144 @@
<?php
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Database\Connection;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Assigns ownership of a node to a user.
*
* @Action(
* id = "node_assign_owner_action",
* label = @Translation("Change the author of content"),
* type = "node"
* )
*/
class AssignOwnerNode extends ConfigurableActionBase implements ContainerFactoryPluginInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* Constructs a new AssignOwnerNode action.
*
* @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\Core\Database\Connection $connection
* The database connection.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $connection) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->connection = $connection;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition,
$container->get('database')
);
}
/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
/** @var \Drupal\node\NodeInterface $entity */
$entity->setOwnerId($this->configuration['owner_uid'])->save();
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'owner_uid' => '',
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$description = t('The username of the user to which you would like to assign ownership.');
$count = $this->connection->query("SELECT COUNT(*) FROM {users}")->fetchField();
// Use dropdown for fewer than 200 users; textbox for more than that.
if (intval($count) < 200) {
$options = [];
$result = $this->connection->query("SELECT uid, name FROM {users_field_data} WHERE uid > 0 AND default_langcode = 1 ORDER BY name");
foreach ($result as $data) {
$options[$data->uid] = $data->name;
}
$form['owner_uid'] = [
'#type' => 'select',
'#title' => t('Username'),
'#default_value' => $this->configuration['owner_uid'],
'#options' => $options,
'#description' => $description,
];
}
else {
$form['owner_uid'] = [
'#type' => 'entity_autocomplete',
'#title' => t('Username'),
'#target_type' => 'user',
'#selection_setttings' => [
'include_anonymous' => FALSE,
],
'#default_value' => User::load($this->configuration['owner_uid']),
// Validation is done in static::validateConfigurationForm().
'#validate_reference' => FALSE,
'#size' => '6',
'#maxlength' => '60',
'#description' => $description,
];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
$exists = (bool) $this->connection->queryRange('SELECT 1 FROM {users_field_data} WHERE uid = :uid AND default_langcode = 1', 0, 1, [':uid' => $form_state->getValue('owner_uid')])->fetchField();
if (!$exists) {
$form_state->setErrorByName('owner_uid', t('Enter a valid username.'));
}
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['owner_uid'] = $form_state->getValue('owner_uid');
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
$result = $object->access('update', $account, TRUE)
->andIf($object->getOwner()->access('edit', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}
@@ -0,0 +1,34 @@
<?php
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Action\Plugin\Action\DeleteAction;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
/**
* Redirects to a node deletion form.
*
* @deprecated in drupal:8.6.0 and is removed from drupal:9.0.0.
* Use \Drupal\Core\Action\Plugin\Action\DeleteAction instead.
*
* @see \Drupal\Core\Action\Plugin\Action\DeleteAction
* @see https://www.drupal.org/node/2934349
*
* @Action(
* id = "node_delete_action",
* label = @Translation("Delete content")
* )
*/
class DeleteNode extends DeleteAction {
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $temp_store_factory, $current_user);
@trigger_error(__NAMESPACE__ . '\DeleteNode is deprecated in Drupal 8.6.x, will be removed before Drupal 9.0.0. Use \Drupal\Core\Action\Plugin\Action\DeleteAction instead. See https://www.drupal.org/node/2934349.', E_USER_DEPRECATED);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Field\FieldUpdateActionBase;
use Drupal\node\NodeInterface;
/**
* Demotes a node.
*
* @Action(
* id = "node_unpromote_action",
* label = @Translation("Demote selected content from front page"),
* type = "node"
* )
*/
class DemoteNode extends FieldUpdateActionBase {
/**
* {@inheritdoc}
*/
protected function getFieldsToUpdate() {
return ['promote' => NodeInterface::NOT_PROMOTED];
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Field\FieldUpdateActionBase;
use Drupal\node\NodeInterface;
/**
* Promotes a node.
*
* @Action(
* id = "node_promote_action",
* label = @Translation("Promote selected content to front page"),
* type = "node"
* )
*/
class PromoteNode extends FieldUpdateActionBase {
/**
* {@inheritdoc}
*/
protected function getFieldsToUpdate() {
return ['promote' => NodeInterface::PROMOTED];
}
}
@@ -0,0 +1,33 @@
<?php
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Action\Plugin\Action\PublishAction;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Publishes a node.
*
* @deprecated in drupal:8.5.0 and is removed from drupal:9.0.0.
* Use \Drupal\Core\Action\Plugin\Action\PublishAction instead.
*
* @see \Drupal\Core\Action\Plugin\Action\PublishAction
* @see https://www.drupal.org/node/2919303
*
* @Action(
* id = "node_publish_action",
* label = @Translation("Publish selected content"),
* type = "node"
* )
*/
class PublishNode extends PublishAction {
/**
* {@inheritdoc}
*/
public function __construct($configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
@trigger_error(__NAMESPACE__ . '\PublishNode is deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use \Drupal\Core\Action\Plugin\Action\PublishAction instead. See https://www.drupal.org/node/2919303.', E_USER_DEPRECATED);
}
}
@@ -0,0 +1,34 @@
<?php
namespace Drupal\node\Plugin\Action;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Action\Plugin\Action\SaveAction;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Provides an action that can save any entity.
*
* @deprecated in drupal:8.5.0 and is removed from drupal:9.0.0.
* Use \Drupal\Core\Action\Plugin\Action\SaveAction instead.
*
* @see \Drupal\Core\Action\Plugin\Action\SaveAction
* @see https://www.drupal.org/node/2919303
*
* @Action(
* id = "node_save_action",
* label = @Translation("Save content"),
* type = "node"
* )
*/
class SaveNode extends SaveAction {
/**
* {@inheritdoc}
*/
public function __construct($configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, TimeInterface $time) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $time);
@trigger_error(__NAMESPACE__ . '\SaveNode is deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use \Drupal\Core\Action\Plugin\Action\SaveAction instead. See https://www.drupal.org/node/2919303.', E_USER_DEPRECATED);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Field\FieldUpdateActionBase;
use Drupal\node\NodeInterface;
/**
* Makes a node sticky.
*
* @Action(
* id = "node_make_sticky_action",
* label = @Translation("Make selected content sticky"),
* type = "node"
* )
*/
class StickyNode extends FieldUpdateActionBase {
/**
* {@inheritdoc}
*/
protected function getFieldsToUpdate() {
return ['sticky' => NodeInterface::STICKY];
}
}
@@ -0,0 +1,78 @@
<?php
namespace Drupal\node\Plugin\Action;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Unpublishes a node containing certain keywords.
*
* @Action(
* id = "node_unpublish_by_keyword_action",
* label = @Translation("Unpublish content containing keyword(s)"),
* type = "node"
* )
*/
class UnpublishByKeywordNode extends ConfigurableActionBase {
/**
* {@inheritdoc}
*/
public function execute($node = NULL) {
$elements = \Drupal::entityTypeManager()
->getViewBuilder('node')
->view(clone $node);
$render = \Drupal::service('renderer')->render($elements);
foreach ($this->configuration['keywords'] as $keyword) {
if (strpos($render, $keyword) !== FALSE || strpos($node->label(), $keyword) !== FALSE) {
$node->setUnpublished();
$node->save();
break;
}
}
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'keywords' => [],
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['keywords'] = [
'#title' => t('Keywords'),
'#type' => 'textarea',
'#description' => t('The content will be unpublished if it contains any of the phrases above. Use a case-sensitive, comma-separated list of phrases. Example: funny, bungee jumping, "Company, Inc."'),
'#default_value' => Tags::implode($this->configuration['keywords']),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['keywords'] = Tags::explode($form_state->getValue('keywords'));
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
$access = $object->access('update', $account, TRUE)
->andIf($object->status->access('edit', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}
@@ -0,0 +1,33 @@
<?php
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Action\Plugin\Action\UnpublishAction;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Unpublishes a node.
*
* @deprecated in drupal:8.5.0 and is removed from drupal:9.0.0.
* Use \Drupal\Core\Action\Plugin\Action\UnpublishAction instead.
*
* @see \Drupal\Core\Action\Plugin\Action\UnpublishAction
* @see https://www.drupal.org/node/2919303
*
* @Action(
* id = "node_unpublish_action",
* label = @Translation("Unpublish selected content"),
* type = "node"
* )
*/
class UnpublishNode extends UnpublishAction {
/**
* {@inheritdoc}
*/
public function __construct($configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
@trigger_error(__NAMESPACE__ . '\UnpublishNode is deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use \Drupal\Core\Action\Plugin\Action\UnpublishAction instead. See https://www.drupal.org/node/2919303.', E_USER_DEPRECATED);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Field\FieldUpdateActionBase;
use Drupal\node\NodeInterface;
/**
* Makes a node not sticky.
*
* @Action(
* id = "node_make_unsticky_action",
* label = @Translation("Make selected content not sticky"),
* type = "node"
* )
*/
class UnstickyNode extends FieldUpdateActionBase {
/**
* {@inheritdoc}
*/
protected function getFieldsToUpdate() {
return ['sticky' => NodeInterface::NOT_STICKY];
}
}
@@ -0,0 +1,46 @@
<?php
namespace Drupal\node\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Session\AccountInterface;
/**
* Provides a 'Syndicate' block that links to the site's RSS feed.
*
* @Block(
* id = "node_syndicate_block",
* admin_label = @Translation("Syndicate"),
* category = @Translation("System")
* )
*/
class SyndicateBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'block_count' => 10,
];
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
return AccessResult::allowedIfHasPermission($account, 'access content');
}
/**
* {@inheritdoc}
*/
public function build() {
return [
'#theme' => 'feed_icon',
'#url' => 'rss.xml',
];
}
}
@@ -0,0 +1,121 @@
<?php
namespace Drupal\node\Plugin\Condition;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'Node Type' condition.
*
* @Condition(
* id = "node_type",
* label = @Translation("Node Bundle"),
* context_definitions = {
* "node" = @ContextDefinition("entity:node", label = @Translation("Node"))
* }
* )
*/
class NodeType extends ConditionPluginBase implements ContainerFactoryPluginInterface {
/**
* The entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $entityStorage;
/**
* Creates a new NodeType instance.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
* The entity storage.
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
*/
public function __construct(EntityStorageInterface $entity_storage, array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityStorage = $entity_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('entity_type.manager')->getStorage('node_type'),
$configuration,
$plugin_id,
$plugin_definition
);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$options = [];
$node_types = $this->entityStorage->loadMultiple();
foreach ($node_types as $type) {
$options[$type->id()] = $type->label();
}
$form['bundles'] = [
'#title' => $this->t('Node types'),
'#type' => 'checkboxes',
'#options' => $options,
'#default_value' => $this->configuration['bundles'],
];
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['bundles'] = array_filter($form_state->getValue('bundles'));
parent::submitConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function summary() {
if (count($this->configuration['bundles']) > 1) {
$bundles = $this->configuration['bundles'];
$last = array_pop($bundles);
$bundles = implode(', ', $bundles);
return $this->t('The node bundle is @bundles or @last', ['@bundles' => $bundles, '@last' => $last]);
}
$bundle = reset($this->configuration['bundles']);
return $this->t('The node bundle is @bundle', ['@bundle' => $bundle]);
}
/**
* {@inheritdoc}
*/
public function evaluate() {
if (empty($this->configuration['bundles']) && !$this->isNegated()) {
return TRUE;
}
$node = $this->getContextValue('node');
return !empty($this->configuration['bundles'][$node->getType()]);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return ['bundles' => []] + parent::defaultConfiguration();
}
}
@@ -0,0 +1,65 @@
<?php
namespace Drupal\node\Plugin\EntityReferenceSelection;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\node\NodeInterface;
/**
* Provides specific access control for the node entity type.
*
* @EntityReferenceSelection(
* id = "default:node",
* label = @Translation("Node selection"),
* entity_types = {"node"},
* group = "default",
* weight = 1
* )
*/
class NodeSelection extends DefaultSelection {
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
// Adding the 'node_access' tag is sadly insufficient for nodes: core
// requires us to also know about the concept of 'published' and
// 'unpublished'. We need to do that as long as there are no access control
// modules in use on the site. As long as one access control module is there,
// it is supposed to handle this check.
if (!$this->currentUser->hasPermission('bypass node access') && !count($this->moduleHandler->getImplementations('node_grants'))) {
$query->condition('status', NodeInterface::PUBLISHED);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$node = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
// In order to create a referenceable node, it needs to published.
/** @var \Drupal\node\NodeInterface $node */
$node->setPublished();
return $node;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableNewEntities(array $entities) {
$entities = parent::validateReferenceableNewEntities($entities);
// Mirror the conditions checked in buildEntityQuery().
if (!$this->currentUser->hasPermission('bypass node access') && !count($this->moduleHandler->getImplementations('node_grants'))) {
$entities = array_filter($entities, function ($node) {
/** @var \Drupal\node\NodeInterface $node */
return $node->isPublished();
});
}
return $entities;
}
}
@@ -0,0 +1,888 @@
<?php
namespace Drupal\node\Plugin\Search;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\Config;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\Query\SelectExtender;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
use Drupal\search\Plugin\ConfigurableSearchPluginBase;
use Drupal\search\Plugin\SearchIndexingInterface;
use Drupal\search\SearchIndexInterface;
use Drupal\Search\SearchQuery;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Handles searching for node entities using the Search module index.
*
* @SearchPlugin(
* id = "node_search",
* title = @Translation("Content")
* )
*/
class NodeSearch extends ConfigurableSearchPluginBase implements AccessibleInterface, SearchIndexingInterface, TrustedCallbackInterface {
use DeprecatedServicePropertyTrait;
/**
* {@inheritdoc}
*/
protected $deprecatedProperties = ['entityManager' => 'entity.manager'];
/**
* The current database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The replica database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $databaseReplica;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* A module manager object.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* A config object for 'search.settings'.
*
* @var \Drupal\Core\Config\Config
*/
protected $searchSettings;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The Drupal account to use for checking for access to advanced search.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The Renderer service to format the username and node.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The search index.
*
* @var \Drupal\search\SearchIndexInterface
*/
protected $searchIndex;
/**
* An array of additional rankings from hook_ranking().
*
* @var array
*/
protected $rankings;
/**
* The list of options and info for advanced search filters.
*
* Each entry in the array has the option as the key and for its value, an
* array that determines how the value is matched in the database query. The
* possible keys in that array are:
* - column: (required) Name of the database column to match against.
* - join: (optional) Information on a table to join. By default the data is
* matched against the {node_field_data} table.
* - operator: (optional) OR or AND, defaults to OR.
*
* @var array
*/
protected $advanced = [
'type' => ['column' => 'n.type'],
'language' => ['column' => 'i.langcode'],
'author' => ['column' => 'n.uid'],
'term' => ['column' => 'ti.tid', 'join' => ['table' => 'taxonomy_index', 'alias' => 'ti', 'condition' => 'n.nid = ti.nid']],
];
/**
* A constant for setting and checking the query string.
*/
const ADVANCED_FORM = 'advanced-form';
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('database'),
$container->get('entity_type.manager'),
$container->get('module_handler'),
$container->get('config.factory')->get('search.settings'),
$container->get('language_manager'),
$container->get('renderer'),
$container->get('messenger'),
$container->get('current_user'),
$container->get('database.replica'),
$container->get('search.index')
);
}
/**
* Constructs a \Drupal\node\Plugin\Search\NodeSearch 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\Core\Database\Connection $database
* The current database connection.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* A module manager object.
* @param \Drupal\Core\Config\Config $search_settings
* A config object for 'search.settings'.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
* @param \Drupal\Core\Session\AccountInterface $account
* The $account object to use for checking for access to advanced search.
* @param \Drupal\Core\Database\Connection|null $database_replica
* (Optional) the replica database connection.
* @param \Drupal\search\SearchIndexInterface $search_index
* The search index.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, Config $search_settings, LanguageManagerInterface $language_manager, RendererInterface $renderer, MessengerInterface $messenger, AccountInterface $account = NULL, Connection $database_replica = NULL, SearchIndexInterface $search_index = NULL) {
$this->database = $database;
$this->databaseReplica = $database_replica ?: $database;
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
$this->searchSettings = $search_settings;
$this->languageManager = $language_manager;
$this->renderer = $renderer;
$this->messenger = $messenger;
$this->account = $account;
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->addCacheTags(['node_list']);
if (!$search_index) {
@trigger_error('Calling NodeSearch::__construct() without the $search_index argument is deprecated in drupal:8.8.0 and is required in drupal:9.0.0. See https://www.drupal.org/node/3075696', E_USER_DEPRECATED);
$search_index = \Drupal::service('search.index');
}
$this->searchIndex = $search_index;
}
/**
* {@inheritdoc}
*/
public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowedIfHasPermission($account, 'access content');
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
public function isSearchExecutable() {
// Node search is executable if we have keywords or an advanced parameter.
// At least, we should parse out the parameters and see if there are any
// keyword matches in that case, rather than just printing out the
// "Please enter keywords" message.
return !empty($this->keywords) || (isset($this->searchParameters['f']) && count($this->searchParameters['f']));
}
/**
* {@inheritdoc}
*/
public function getType() {
return $this->getPluginId();
}
/**
* {@inheritdoc}
*/
public function execute() {
if ($this->isSearchExecutable()) {
$results = $this->findResults();
if ($results) {
return $this->prepareResults($results);
}
}
return [];
}
/**
* Queries to find search results, and sets status messages.
*
* This method can assume that $this->isSearchExecutable() has already been
* checked and returned TRUE.
*
* @return \Drupal\Core\Database\StatementInterface|null
* Results from search query execute() method, or NULL if the search
* failed.
*/
protected function findResults() {
$keys = $this->keywords;
// Build matching conditions.
$query = $this->databaseReplica
->select('search_index', 'i')
->extend('Drupal\search\SearchQuery')
->extend('Drupal\Core\Database\Query\PagerSelectExtender');
$query->join('node_field_data', 'n', 'n.nid = i.sid AND n.langcode = i.langcode');
$query->condition('n.status', 1)
->addTag('node_access')
->searchExpression($keys, $this->getPluginId());
// Handle advanced search filters in the f query string.
// \Drupal::request()->query->get('f') is an array that looks like this in
// the URL: ?f[]=type:page&f[]=term:27&f[]=term:13&f[]=langcode:en
// So $parameters['f'] looks like:
// array('type:page', 'term:27', 'term:13', 'langcode:en');
// We need to parse this out into query conditions, some of which go into
// the keywords string, and some of which are separate conditions.
$parameters = $this->getParameters();
if (!empty($parameters['f']) && is_array($parameters['f'])) {
$filters = [];
// Match any query value that is an expected option and a value
// separated by ':' like 'term:27'.
$pattern = '/^(' . implode('|', array_keys($this->advanced)) . '):([^ ]*)/i';
foreach ($parameters['f'] as $item) {
if (preg_match($pattern, $item, $m)) {
// Use the matched value as the array key to eliminate duplicates.
$filters[$m[1]][$m[2]] = $m[2];
}
}
// Now turn these into query conditions. This assumes that everything in
// $filters is a known type of advanced search.
foreach ($filters as $option => $matched) {
$info = $this->advanced[$option];
// Insert additional conditions. By default, all use the OR operator.
$operator = empty($info['operator']) ? 'OR' : $info['operator'];
$where = new Condition($operator);
foreach ($matched as $value) {
$where->condition($info['column'], $value);
}
$query->condition($where);
if (!empty($info['join'])) {
$query->join($info['join']['table'], $info['join']['alias'], $info['join']['condition']);
}
}
}
// Add the ranking expressions.
$this->addNodeRankings($query);
// Run the query.
$find = $query
// Add the language code of the indexed item to the result of the query,
// since the node will be rendered using the respective language.
->fields('i', ['langcode'])
// And since SearchQuery makes these into GROUP BY queries, if we add
// a field, for PostgreSQL we also need to make it an aggregate or a
// GROUP BY. In this case, we want GROUP BY.
->groupBy('i.langcode')
->limit(10)
->execute();
// Check query status and set messages if needed.
$status = $query->getStatus();
if ($status & SearchQuery::EXPRESSIONS_IGNORED) {
$this->messenger->addWarning($this->t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', ['@count' => $this->searchSettings->get('and_or_limit')]));
}
if ($status & SearchQuery::LOWER_CASE_OR) {
$this->messenger->addWarning($this->t('Search for either of the two terms with uppercase <strong>OR</strong>. For example, <strong>cats OR dogs</strong>.'));
}
if ($status & SearchQuery::NO_POSITIVE_KEYWORDS) {
$this->messenger->addWarning($this->formatPlural($this->searchSettings->get('index.minimum_word_size'), 'You must include at least one keyword to match in the content, and punctuation is ignored.', 'You must include at least one keyword to match in the content. Keywords must be at least @count characters, and punctuation is ignored.'));
}
return $find;
}
/**
* Prepares search results for rendering.
*
* @param \Drupal\Core\Database\StatementInterface $found
* Results found from a successful search query execute() method.
*
* @return array
* Array of search result item render arrays (empty array if no results).
*/
protected function prepareResults(StatementInterface $found) {
$results = [];
$node_storage = $this->entityTypeManager->getStorage('node');
$node_render = $this->entityTypeManager->getViewBuilder('node');
$keys = $this->keywords;
foreach ($found as $item) {
// Render the node.
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->load($item->sid)->getTranslation($item->langcode);
$build = $node_render->view($node, 'search_result', $item->langcode);
/** @var \Drupal\node\NodeTypeInterface $type*/
$type = $this->entityTypeManager->getStorage('node_type')->load($node->bundle());
unset($build['#theme']);
$build['#pre_render'][] = [$this, 'removeSubmittedInfo'];
// Fetch comments for snippet.
$rendered = $this->renderer->renderPlain($build);
$this->addCacheableDependency(CacheableMetadata::createFromRenderArray($build));
$rendered .= ' ' . $this->moduleHandler->invoke('comment', 'node_update_index', [$node]);
$extra = $this->moduleHandler->invokeAll('node_search_result', [$node]);
$username = [
'#theme' => 'username',
'#account' => $node->getOwner(),
];
$result = [
'link' => $node->toUrl('canonical', ['absolute' => TRUE])->toString(),
'type' => $type->label(),
'title' => $node->label(),
'node' => $node,
'extra' => $extra,
'score' => $item->calculated_score,
'snippet' => search_excerpt($keys, $rendered, $item->langcode),
'langcode' => $node->language()->getId(),
];
$this->addCacheableDependency($node);
// We have to separately add the node owner's cache tags because search
// module doesn't use the rendering system, it does its own rendering
// without taking cacheability metadata into account. So we have to do it
// explicitly here.
$this->addCacheableDependency($node->getOwner());
if ($type->displaySubmitted()) {
$result += [
'user' => $this->renderer->renderPlain($username),
'date' => $node->getChangedTime(),
];
}
$results[] = $result;
}
return $results;
}
/**
* Removes the submitted by information from the build array.
*
* This information is being removed from the rendered node that is used to
* build the search result snippet. It just doesn't make sense to have it
* displayed in the snippet.
*
* @param array $build
* The build array.
*
* @return array
* The modified build array.
*/
public function removeSubmittedInfo(array $build) {
unset($build['created']);
unset($build['uid']);
return $build;
}
/**
* Adds the configured rankings to the search query.
*
* @param $query
* A query object that has been extended with the Search DB Extender.
*/
protected function addNodeRankings(SelectExtender $query) {
if ($ranking = $this->getRankings()) {
$tables = &$query->getTables();
foreach ($ranking as $rank => $values) {
if (isset($this->configuration['rankings'][$rank]) && !empty($this->configuration['rankings'][$rank])) {
$node_rank = $this->configuration['rankings'][$rank];
// If the table defined in the ranking isn't already joined, then add it.
if (isset($values['join']) && !isset($tables[$values['join']['alias']])) {
$query->addJoin($values['join']['type'], $values['join']['table'], $values['join']['alias'], $values['join']['on']);
}
$arguments = isset($values['arguments']) ? $values['arguments'] : [];
$query->addScore($values['score'], $arguments, $node_rank);
}
}
}
}
/**
* {@inheritdoc}
*/
public function updateIndex() {
// Interpret the cron limit setting as the maximum number of nodes to index
// per cron run.
$limit = (int) $this->searchSettings->get('index.cron_limit');
$query = $this->databaseReplica->select('node', 'n');
$query->addField('n', 'nid');
$query->leftJoin('search_dataset', 'sd', 'sd.sid = n.nid AND sd.type = :type', [':type' => $this->getPluginId()]);
$query->addExpression('CASE MAX(sd.reindex) WHEN NULL THEN 0 ELSE 1 END', 'ex');
$query->addExpression('MAX(sd.reindex)', 'ex2');
$query->condition(
$query->orConditionGroup()
->where('sd.sid IS NULL')
->condition('sd.reindex', 0, '<>')
);
$query->orderBy('ex', 'DESC')
->orderBy('ex2')
->orderBy('n.nid')
->groupBy('n.nid')
->range(0, $limit);
$nids = $query->execute()->fetchCol();
if (!$nids) {
return;
}
$node_storage = $this->entityTypeManager->getStorage('node');
$words = [];
try {
foreach ($node_storage->loadMultiple($nids) as $node) {
$words += $this->indexNode($node);
}
}
finally {
$this->searchIndex->updateWordWeights($words);
}
}
/**
* Indexes a single node.
*
* @param \Drupal\node\NodeInterface $node
* The node to index.
*
* @return array
* An array of words to update after indexing.
*/
protected function indexNode(NodeInterface $node) {
$words = [];
$languages = $node->getTranslationLanguages();
$node_render = $this->entityTypeManager->getViewBuilder('node');
foreach ($languages as $language) {
$node = $node->getTranslation($language->getId());
// Render the node.
$build = $node_render->view($node, 'search_index', $language->getId());
unset($build['#theme']);
// Add the title to text so it is searchable.
$build['search_title'] = [
'#prefix' => '<h1>',
'#plain_text' => $node->label(),
'#suffix' => '</h1>',
'#weight' => -1000,
];
$text = $this->renderer->renderPlain($build);
// Fetch extra data normally not visible.
$extra = $this->moduleHandler->invokeAll('node_update_index', [$node]);
foreach ($extra as $t) {
$text .= $t;
}
// Update index, using search index "type" equal to the plugin ID.
$words += $this->searchIndex->index($this->getPluginId(), $node->id(), $language->getId(), $text, FALSE);
}
return $words;
}
/**
* {@inheritdoc}
*/
public function indexClear() {
// All NodeSearch pages share a common search index "type" equal to
// the plugin ID.
$this->searchIndex->clear($this->getPluginId());
}
/**
* {@inheritdoc}
*/
public function markForReindex() {
// All NodeSearch pages share a common search index "type" equal to
// the plugin ID.
$this->searchIndex->markForReindex($this->getPluginId());
}
/**
* {@inheritdoc}
*/
public function indexStatus() {
$total = $this->database->query('SELECT COUNT(*) FROM {node}')->fetchField();
$remaining = $this->database->query("SELECT COUNT(DISTINCT n.nid) FROM {node} n LEFT JOIN {search_dataset} sd ON sd.sid = n.nid AND sd.type = :type WHERE sd.sid IS NULL OR sd.reindex <> 0", [':type' => $this->getPluginId()])->fetchField();
return ['remaining' => $remaining, 'total' => $total];
}
/**
* {@inheritdoc}
*/
public function searchFormAlter(array &$form, FormStateInterface $form_state) {
$parameters = $this->getParameters();
$keys = $this->getKeywords();
$used_advanced = !empty($parameters[self::ADVANCED_FORM]);
if ($used_advanced) {
$f = isset($parameters['f']) ? (array) $parameters['f'] : [];
$defaults = $this->parseAdvancedDefaults($f, $keys);
}
else {
$defaults = ['keys' => $keys];
}
$form['basic']['keys']['#default_value'] = $defaults['keys'];
// Add advanced search keyword-related boxes.
$form['advanced'] = [
'#type' => 'details',
'#title' => t('Advanced search'),
'#attributes' => ['class' => ['search-advanced']],
'#access' => $this->account && $this->account->hasPermission('use advanced search'),
'#open' => $used_advanced,
];
$form['advanced']['keywords-fieldset'] = [
'#type' => 'fieldset',
'#title' => t('Keywords'),
];
$form['advanced']['keywords'] = [
'#prefix' => '<div class="criterion">',
'#suffix' => '</div>',
];
$form['advanced']['keywords-fieldset']['keywords']['or'] = [
'#type' => 'textfield',
'#title' => t('Containing any of the words'),
'#size' => 30,
'#maxlength' => 255,
'#default_value' => isset($defaults['or']) ? $defaults['or'] : '',
];
$form['advanced']['keywords-fieldset']['keywords']['phrase'] = [
'#type' => 'textfield',
'#title' => t('Containing the phrase'),
'#size' => 30,
'#maxlength' => 255,
'#default_value' => isset($defaults['phrase']) ? $defaults['phrase'] : '',
];
$form['advanced']['keywords-fieldset']['keywords']['negative'] = [
'#type' => 'textfield',
'#title' => t('Containing none of the words'),
'#size' => 30,
'#maxlength' => 255,
'#default_value' => isset($defaults['negative']) ? $defaults['negative'] : '',
];
// Add node types.
$types = array_map(['\Drupal\Component\Utility\Html', 'escape'], node_type_get_names());
$form['advanced']['types-fieldset'] = [
'#type' => 'fieldset',
'#title' => t('Types'),
];
$form['advanced']['types-fieldset']['type'] = [
'#type' => 'checkboxes',
'#title' => t('Only of the type(s)'),
'#prefix' => '<div class="criterion">',
'#suffix' => '</div>',
'#options' => $types,
'#default_value' => isset($defaults['type']) ? $defaults['type'] : [],
];
$form['advanced']['submit'] = [
'#type' => 'submit',
'#value' => t('Advanced search'),
'#prefix' => '<div class="action">',
'#suffix' => '</div>',
'#weight' => 100,
];
// Add languages.
$language_options = [];
$language_list = $this->languageManager->getLanguages(LanguageInterface::STATE_ALL);
foreach ($language_list as $langcode => $language) {
// Make locked languages appear special in the list.
$language_options[$langcode] = $language->isLocked() ? t('- @name -', ['@name' => $language->getName()]) : $language->getName();
}
if (count($language_options) > 1) {
$form['advanced']['lang-fieldset'] = [
'#type' => 'fieldset',
'#title' => t('Languages'),
];
$form['advanced']['lang-fieldset']['language'] = [
'#type' => 'checkboxes',
'#title' => t('Languages'),
'#prefix' => '<div class="criterion">',
'#suffix' => '</div>',
'#options' => $language_options,
'#default_value' => isset($defaults['language']) ? $defaults['language'] : [],
];
}
}
/**
* {@inheritdoc}
*/
public function buildSearchUrlQuery(FormStateInterface $form_state) {
// Read keyword and advanced search information from the form values,
// and put these into the GET parameters.
$keys = trim($form_state->getValue('keys'));
$advanced = FALSE;
// Collect extra filters.
$filters = [];
if ($form_state->hasValue('type') && is_array($form_state->getValue('type'))) {
// Retrieve selected types - Form API sets the value of unselected
// checkboxes to 0.
foreach ($form_state->getValue('type') as $type) {
if ($type) {
$advanced = TRUE;
$filters[] = 'type:' . $type;
}
}
}
if ($form_state->hasValue('term') && is_array($form_state->getValue('term'))) {
foreach ($form_state->getValue('term') as $term) {
$filters[] = 'term:' . $term;
$advanced = TRUE;
}
}
if ($form_state->hasValue('language') && is_array($form_state->getValue('language'))) {
foreach ($form_state->getValue('language') as $language) {
if ($language) {
$advanced = TRUE;
$filters[] = 'language:' . $language;
}
}
}
if ($form_state->getValue('or') != '') {
if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state->getValue('or'), $matches)) {
$keys .= ' ' . implode(' OR ', $matches[1]);
$advanced = TRUE;
}
}
if ($form_state->getValue('negative') != '') {
if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state->getValue('negative'), $matches)) {
$keys .= ' -' . implode(' -', $matches[1]);
$advanced = TRUE;
}
}
if ($form_state->getValue('phrase') != '') {
$keys .= ' "' . str_replace('"', ' ', $form_state->getValue('phrase')) . '"';
$advanced = TRUE;
}
$keys = trim($keys);
// Put the keywords and advanced parameters into GET parameters. Make sure
// to put keywords into the query even if it is empty, because the page
// controller uses that to decide it's time to check for search results.
$query = ['keys' => $keys];
if ($filters) {
$query['f'] = $filters;
}
// Record that the person used the advanced search form, if they did.
if ($advanced) {
$query[self::ADVANCED_FORM] = '1';
}
return $query;
}
/**
* Parses the advanced search form default values.
*
* @param array $f
* The 'f' query parameter set up in self::buildUrlSearchQuery(), which
* contains the advanced query values.
* @param string $keys
* The search keywords string, which contains some information from the
* advanced search form.
*
* @return array
* Array of default form values for the advanced search form, including
* a modified 'keys' element for the bare search keywords.
*/
protected function parseAdvancedDefaults($f, $keys) {
$defaults = [];
// Split out the advanced search parameters.
foreach ($f as $advanced) {
list($key, $value) = explode(':', $advanced, 2);
if (!isset($defaults[$key])) {
$defaults[$key] = [];
}
$defaults[$key][] = $value;
}
// Split out the negative, phrase, and OR parts of keywords.
// For phrases, the form only supports one phrase.
$matches = [];
$keys = ' ' . $keys . ' ';
if (preg_match('/ "([^"]+)" /', $keys, $matches)) {
$keys = str_replace($matches[0], ' ', $keys);
$defaults['phrase'] = $matches[1];
}
// Negative keywords: pull all of them out.
if (preg_match_all('/ -([^ ]+)/', $keys, $matches)) {
$keys = str_replace($matches[0], ' ', $keys);
$defaults['negative'] = implode(' ', $matches[1]);
}
// OR keywords: pull up to one set of them out of the query.
if (preg_match('/ [^ ]+( OR [^ ]+)+ /', $keys, $matches)) {
$keys = str_replace($matches[0], ' ', $keys);
$words = explode(' OR ', trim($matches[0]));
$defaults['or'] = implode(' ', $words);
}
// Put remaining keywords string back into keywords.
$defaults['keys'] = trim($keys);
return $defaults;
}
/**
* Gathers ranking definitions from hook_ranking().
*
* @return array
* An array of ranking definitions.
*/
protected function getRankings() {
if (!$this->rankings) {
$this->rankings = $this->moduleHandler->invokeAll('ranking');
}
return $this->rankings;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
$configuration = [
'rankings' => [],
];
return $configuration;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
// Output form for defining rank factor weights.
$form['content_ranking'] = [
'#type' => 'details',
'#title' => t('Content ranking'),
'#open' => TRUE,
];
$form['content_ranking']['info'] = [
'#markup' => '<p><em>' . $this->t('Influence is a numeric multiplier used in ordering search results. A higher number means the corresponding factor has more influence on search results; zero means the factor is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '</em></p>',
];
// Prepare table.
$header = [$this->t('Factor'), $this->t('Influence')];
$form['content_ranking']['rankings'] = [
'#type' => 'table',
'#header' => $header,
];
// Note: reversed to reflect that higher number = higher ranking.
$range = range(0, 10);
$options = array_combine($range, $range);
foreach ($this->getRankings() as $var => $values) {
$form['content_ranking']['rankings'][$var]['name'] = [
'#markup' => $values['title'],
];
$form['content_ranking']['rankings'][$var]['value'] = [
'#type' => 'select',
'#options' => $options,
'#attributes' => ['aria-label' => $this->t("Influence of '@title'", ['@title' => $values['title']])],
'#default_value' => isset($this->configuration['rankings'][$var]) ? $this->configuration['rankings'][$var] : 0,
];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
foreach ($this->getRankings() as $var => $values) {
if (!$form_state->isValueEmpty(['rankings', $var, 'value'])) {
$this->configuration['rankings'][$var] = $form_state->getValue(['rankings', $var, 'value']);
}
else {
unset($this->configuration['rankings'][$var]);
}
}
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['removeSubmittedInfo'];
}
}
@@ -0,0 +1,122 @@
<?php
namespace Drupal\node\Plugin\migrate;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrationDeriverTrait;
use Drupal\migrate_drupal\FieldDiscoveryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Deriver for Drupal 6 node and node revision migrations based on node types.
*/
class D6NodeDeriver extends DeriverBase implements ContainerDeriverInterface {
use MigrationDeriverTrait;
/**
* The base plugin ID this derivative is for.
*
* @var string
*/
protected $basePluginId;
/**
* Whether or not to include translations.
*
* @var bool
*/
protected $includeTranslations;
/**
* The migration field discovery service.
*
* @var \Drupal\migrate_drupal\FieldDiscoveryInterface
*/
protected $fieldDiscovery;
/**
* D6NodeDeriver constructor.
*
* @param string $base_plugin_id
* The base plugin ID for the plugin ID.
* @param bool $translations
* Whether or not to include translations.
* @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
* The migration field discovery service.
*/
public function __construct($base_plugin_id, $translations, FieldDiscoveryInterface $field_discovery) {
$this->basePluginId = $base_plugin_id;
$this->includeTranslations = $translations;
$this->fieldDiscovery = $field_discovery;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
// Translations don't make sense unless we have content_translation.
return new static(
$base_plugin_id,
$container->get('module_handler')->moduleExists('content_translation'),
$container->get('migrate_drupal.field_discovery')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
if ($base_plugin_definition['id'] == 'd6_node_translation' && !$this->includeTranslations) {
// Refuse to generate anything.
return $this->derivatives;
}
$node_types = static::getSourcePlugin('d6_node_type');
try {
$node_types->checkRequirements();
}
catch (RequirementsException $e) {
// If the d6_node_type requirements failed, that means we do not have a
// Drupal source database configured - there is nothing to generate.
return $this->derivatives;
}
try {
foreach ($node_types as $row) {
$node_type = $row->getSourceProperty('type');
$values = $base_plugin_definition;
$values['label'] = t("@label (@type)", [
'@label' => $values['label'],
'@type' => $node_type,
]);
$values['source']['node_type'] = $node_type;
$values['destination']['default_bundle'] = $node_type;
// If this migration is based on the d6_node_revision migration or
// is for translations of nodes, it should explicitly depend on the
// corresponding d6_node variant.
if (in_array($base_plugin_definition['id'], ['d6_node_revision', 'd6_node_translation'])) {
$values['migration_dependencies']['required'][] = 'd6_node:' . $node_type;
}
/** @var \Drupal\migrate\Plugin\Migration $migration */
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($values);
$this->fieldDiscovery->addBundleFieldProcesses($migration, 'node', $node_type);
$this->derivatives[$node_type] = $migration->getPluginDefinition();
}
}
catch (DatabaseExceptionWrapper $e) {
// Once we begin iterating the source plugin it is possible that the
// source tables will not exist. This can happen when the
// MigrationPluginManager gathers up the migration definitions but we do
// not actually have a Drupal 6 source database.
}
return $this->derivatives;
}
}
@@ -0,0 +1,21 @@
<?php
namespace Drupal\node\Plugin\migrate;
use Drupal\migrate\Plugin\Migration;
use Drupal\migrate_drupal\Plugin\MigrationWithFollowUpInterface;
/**
* Migration plugin for the Drupal 6 node translations.
*/
class D6NodeTranslation extends Migration implements MigrationWithFollowUpInterface {
/**
* {@inheritdoc}
*/
public function generateFollowUpMigrations() {
$this->migrationPluginManager->clearCachedDefinitions();
return $this->migrationPluginManager->createInstances('d6_entity_reference_translation');
}
}
@@ -0,0 +1,131 @@
<?php
namespace Drupal\node\Plugin\migrate;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrationDeriverTrait;
use Drupal\migrate_drupal\FieldDiscoveryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Deriver for Drupal 7 node and node revision migrations based on node types.
*/
class D7NodeDeriver extends DeriverBase implements ContainerDeriverInterface {
use MigrationDeriverTrait;
/**
* The base plugin ID this derivative is for.
*
* @var string
*/
protected $basePluginId;
/**
* Whether or not to include translations.
*
* @var bool
*/
protected $includeTranslations;
/**
* The migration field discovery service.
*
* @var \Drupal\migrate_drupal\FieldDiscoveryInterface
*/
protected $fieldDiscovery;
/**
* D7NodeDeriver constructor.
*
* @param string $base_plugin_id
* The base plugin ID for the plugin ID.
* @param bool $translations
* Whether or not to include translations.
* @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
* The migration field discovery service.
*/
public function __construct($base_plugin_id, $translations, FieldDiscoveryInterface $field_discovery) {
$this->basePluginId = $base_plugin_id;
$this->includeTranslations = $translations;
$this->fieldDiscovery = $field_discovery;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
// Translations don't make sense unless we have content_translation.
return new static(
$base_plugin_id,
$container->get('module_handler')->moduleExists('content_translation'),
$container->get('migrate_drupal.field_discovery')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
if (in_array('translation', $base_plugin_definition['migration_tags']) && !$this->includeTranslations) {
// Refuse to generate anything.
return $this->derivatives;
}
$node_types = static::getSourcePlugin('d7_node_type');
try {
$node_types->checkRequirements();
}
catch (RequirementsException $e) {
// If the d7_node_type requirements failed, that means we do not have a
// Drupal source database configured - there is nothing to generate.
return $this->derivatives;
}
try {
foreach ($node_types as $row) {
$node_type = $row->getSourceProperty('type');
$values = $base_plugin_definition;
$values['label'] = t('@label (@type)', [
'@label' => $values['label'],
'@type' => $row->getSourceProperty('name'),
]);
$values['source']['node_type'] = $node_type;
$values['destination']['default_bundle'] = $node_type;
// Comment status must be mapped to correct comment type.
// Comment type migration creates a separate comment type for each
// node type except for Forum which uses 'comment_forum'.
$comment_type = 'comment_node_' . $node_type;
if ($node_type == 'forum') {
$comment_type = 'comment_forum';
}
$nested_key = $comment_type . '/0/status';
$values['process'][$nested_key] = 'comment';
// If this migration is based on the d7_node_revision migration or
// is for translations of nodes, it should explicitly depend on the
// corresponding d7_node variant.
if ($base_plugin_definition['id'] == ['d7_node_revision'] || in_array('translation', $base_plugin_definition['migration_tags'])) {
$values['migration_dependencies']['required'][] = 'd7_node:' . $node_type;
}
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($values);
$this->fieldDiscovery->addBundleFieldProcesses($migration, 'node', $node_type);
$this->derivatives[$node_type] = $migration->getPluginDefinition();
}
}
catch (DatabaseExceptionWrapper $e) {
// Once we begin iterating the source plugin it is possible that the
// source tables will not exist. This can happen when the
// MigrationPluginManager gathers up the migration definitions but we do
// not actually have a Drupal 7 source database.
}
return $this->derivatives;
}
}
@@ -0,0 +1,21 @@
<?php
namespace Drupal\node\Plugin\migrate;
use Drupal\migrate\Plugin\Migration;
use Drupal\migrate_drupal\Plugin\MigrationWithFollowUpInterface;
/**
* Migration plugin for the Drupal 7 node translations.
*/
class D7NodeTranslation extends Migration implements MigrationWithFollowUpInterface {
/**
* {@inheritdoc}
*/
public function generateFollowUpMigrations() {
$this->migrationPluginManager->clearCachedDefinitions();
return $this->migrationPluginManager->createInstances('d7_entity_reference_translation');
}
}
@@ -0,0 +1,27 @@
<?php
namespace Drupal\node\Plugin\migrate\destination;
use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "entity:node_type"
* )
*/
class EntityNodeType extends EntityConfigBase {
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
$entity_ids = parent::import($row, $old_destination_id_values);
if ($row->getDestinationProperty('create_body')) {
$node_type = $this->storage->load(reset($entity_ids));
node_add_body_field($node_type, $row->getDestinationProperty('create_body_label'));
}
return $entity_ids;
}
}
@@ -0,0 +1,30 @@
<?php
namespace Drupal\node\Plugin\migrate\process\d6;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Split the 'administer nodes' permission from 'access content overview'.
*
* @MigrateProcessPlugin(
* id = "node_update_7008"
* )
*/
class NodeUpdate7008 extends ProcessPluginBase {
/**
* {@inheritdoc}
*
* Split the 'administer nodes' permission from 'access content overview'.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if ($value === 'administer nodes') {
return [$value, 'access content overview'];
}
return $value;
}
}
@@ -0,0 +1,340 @@
<?php
namespace Drupal\node\Plugin\migrate\source\d6;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Drupal 6 node source from database.
*
* @MigrateSource(
* id = "d6_node",
* source_module = "node"
*
* )
*/
class Node extends DrupalSqlBase {
/**
* The join options between the node and the node_revisions table.
*/
const JOIN = 'n.vid = nr.vid';
/**
* The default filter format.
*
* @var string
*/
protected $filterDefaultFormat;
/**
* Cached field and field instance definitions.
*
* @var array
*/
protected $fieldInfo;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandler
*/
protected $moduleHandler;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, EntityTypeManagerInterface $entity_type_manager, ModuleHandler $module_handler) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state, $entity_type_manager);
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('state'),
$container->get('entity_type.manager'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('node_revisions', 'nr');
$query->innerJoin('node', 'n', static::JOIN);
$this->handleTranslations($query);
$query->fields('n', [
'nid',
'type',
'language',
'status',
'created',
'changed',
'comment',
'promote',
'moderate',
'sticky',
'tnid',
'translate',
])
->fields('nr', [
'title',
'body',
'teaser',
'log',
'timestamp',
'format',
'vid',
]);
$query->addField('n', 'uid', 'node_uid');
$query->addField('nr', 'uid', 'revision_uid');
// If the content_translation module is enabled, get the source langcode
// to fill the content_translation_source field.
if ($this->moduleHandler->moduleExists('content_translation')) {
$query->leftJoin('node', 'nt', 'n.tnid = nt.nid');
$query->addField('nt', 'language', 'source_langcode');
}
if (isset($this->configuration['node_type'])) {
$query->condition('n.type', $this->configuration['node_type']);
}
return $query;
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$this->filterDefaultFormat = $this->variableGet('filter_default_format', '1');
return parent::initializeIterator();
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'nid' => $this->t('Node ID'),
'type' => $this->t('Type'),
'title' => $this->t('Title'),
'body' => $this->t('Body'),
'format' => $this->t('Format'),
'teaser' => $this->t('Teaser'),
'node_uid' => $this->t('Node authored by (uid)'),
'revision_uid' => $this->t('Revision authored by (uid)'),
'created' => $this->t('Created timestamp'),
'changed' => $this->t('Modified timestamp'),
'status' => $this->t('Published'),
'promote' => $this->t('Promoted to front page'),
'sticky' => $this->t('Sticky at top of lists'),
'revision' => $this->t('Create new revision'),
'language' => $this->t('Language (fr, en, ...)'),
'tnid' => $this->t('The translation set id for this node'),
'timestamp' => $this->t('The timestamp the latest revision of this node was created.'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// format = 0 can happen when the body field is hidden. Set the format to 1
// to avoid migration map issues (since the body field isn't used anyway).
if ($row->getSourceProperty('format') === '0') {
$row->setSourceProperty('format', $this->filterDefaultFormat);
}
if ($this->moduleExists('content') && $this->getModuleSchemaVersion('content') >= 6001) {
foreach ($this->getFieldValues($row) as $field => $values) {
$row->setSourceProperty($field, $values);
}
}
// Make sure we always have a translation set.
if ($row->getSourceProperty('tnid') == 0) {
$row->setSourceProperty('tnid', $row->getSourceProperty('nid'));
}
return parent::prepareRow($row);
}
/**
* Gets field values for a node.
*
* @param \Drupal\migrate\Row $node
* The node.
*
* @return array
* Field values, keyed by field name.
*/
protected function getFieldValues(Row $node) {
$values = [];
foreach ($this->getFieldInfo($node->getSourceProperty('type')) as $field => $info) {
$values[$field] = $this->getFieldData($info, $node);
}
return $values;
}
/**
* Gets field and instance definitions from the database.
*
* @param string $node_type
* The node type for which to get field info.
*
* @return array
* Field and instance information for the node type, keyed by field name.
*/
protected function getFieldInfo($node_type) {
if (!isset($this->fieldInfo)) {
$this->fieldInfo = [];
// Query the database directly for all field info.
$query = $this->select('content_node_field_instance', 'cnfi');
$query->join('content_node_field', 'cnf', 'cnf.field_name = cnfi.field_name');
$query->fields('cnfi');
$query->fields('cnf');
foreach ($query->execute() as $field) {
$this->fieldInfo[$field['type_name']][$field['field_name']] = $field;
}
foreach ($this->fieldInfo as $type => $fields) {
foreach ($fields as $field => $info) {
foreach ($info as $property => $value) {
if ($property == 'db_columns' || preg_match('/_settings$/', $property)) {
$this->fieldInfo[$type][$field][$property] = unserialize($value);
}
}
}
}
}
return isset($this->fieldInfo[$node_type]) ? $this->fieldInfo[$node_type] : [];
}
/**
* Retrieves raw field data for a node.
*
* @param array $field
* A field and instance definition from getFieldInfo().
* @param \Drupal\migrate\Row $node
* The node.
*
* @return array
* The field values, keyed by delta.
*/
protected function getFieldData(array $field, Row $node) {
$field_table = 'content_' . $field['field_name'];
$node_table = 'content_type_' . $node->getSourceProperty('type');
/** @var \Drupal\Core\Database\Schema $db */
$db = $this->getDatabase()->schema();
if ($db->tableExists($field_table)) {
$query = $this->select($field_table, 't');
// If the delta column does not exist, add it as an expression to
// normalize the query results.
if ($db->fieldExists($field_table, 'delta')) {
$query->addField('t', 'delta');
}
else {
$query->addExpression(0, 'delta');
}
}
elseif ($db->tableExists($node_table)) {
$query = $this->select($node_table, 't');
// Every row should have a delta of 0.
$query->addExpression(0, 'delta');
}
if (isset($query)) {
$columns = array_keys($field['db_columns']);
// Add every column in the field's schema.
foreach ($columns as $column) {
$query->addField('t', $field['field_name'] . '_' . $column, $column);
}
return $query
// This call to isNotNull() is a kludge which relies on the convention
// that field schemas usually define their most important column first.
// A better way would be to allow field plugins to alter the query
// directly before it's run, but this will do for the time being.
->isNotNull($field['field_name'] . '_' . $columns[0])
->condition('nid', $node->getSourceProperty('nid'))
->condition('vid', $node->getSourceProperty('vid'))
->execute()
->fetchAllAssoc('delta');
}
else {
return [];
}
}
/**
* Retrieves raw field data for a node.
*
* @deprecated in drupal:8.2.0 and is removed from drupal:9.0.0. Use
* getFieldData() instead.
*
* @param array $field
* A field and instance definition from getFieldInfo().
* @param \Drupal\migrate\Row $node
* The node.
*
* @return array
* The field values, keyed by delta.
*/
protected function getCckData(array $field, Row $node) {
return $this->getFieldData($field, $node);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['nid']['type'] = 'integer';
$ids['nid']['alias'] = 'n';
return $ids;
}
/**
* Adapt our query for translations.
*
* @param \Drupal\Core\Database\Query\SelectInterface $query
* The generated query.
*/
protected function handleTranslations(SelectInterface $query) {
// Check whether or not we want translations.
if (empty($this->configuration['translations'])) {
// No translations: Yield untranslated nodes, or default translations.
$query->where('n.tnid = 0 OR n.tnid = n.nid');
}
else {
// Translations: Yield only non-default translations.
$query->where('n.tnid <> 0 AND n.tnid <> n.nid');
}
}
}
@@ -0,0 +1,49 @@
<?php
namespace Drupal\node\Plugin\migrate\source\d6;
/**
* Gets all node revisions from the source, including translation revisions.
*
* @MigrateSource(
* id = "d6_node_complete",
* source_module = "node"
* )
*/
class NodeComplete extends NodeRevision {
/**
* The join options between the node and the node_revisions_table.
*/
const JOIN = 'n.nid = nr.nid';
/**
* {@inheritdoc}
*/
public function query() {
$query = parent::query();
$query->orderBy('nr.vid');
return $query;
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'nid' => [
'type' => 'integer',
'alias' => 'n',
],
'vid' => [
'type' => 'integer',
'alias' => 'nr',
],
'language' => [
'type' => 'string',
'alias' => 'n',
],
];
}
}
@@ -0,0 +1,50 @@
<?php
namespace Drupal\node\Plugin\migrate\source\d6;
use Drupal\Core\Database\Query\SelectInterface;
/**
* Drupal 6 node revision source from database.
*
* @MigrateSource(
* id = "d6_node_revision",
* source_module = "node"
* )
*/
class NodeRevision extends Node {
/**
* The join options between the node and the node_revisions_table.
*/
const JOIN = 'n.nid = nr.nid AND n.vid <> nr.vid';
/**
* {@inheritdoc}
*/
public function fields() {
// Use all the node fields plus the vid that identifies the version.
return parent::fields() + [
'vid' => t('The primary identifier for this version.'),
'log' => $this->t('Revision Log message'),
'timestamp' => $this->t('Revision timestamp'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
$ids['vid']['alias'] = 'nr';
return $ids;
}
/**
* {@inheritdoc}
*/
protected function handleTranslations(SelectInterface $query) {
// @todo in https://www.drupal.org/node/2746541
}
}
@@ -0,0 +1,155 @@
<?php
namespace Drupal\node\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 Node types source from database.
*
* @MigrateSource(
* id = "d6_node_type",
* source_module = "node"
* )
*/
class NodeType extends DrupalSqlBase {
/**
* The teaser length
*
* @var int
*/
protected $teaserLength;
/**
* Node preview optional / required.
*
* @var int
*/
protected $nodePreview;
/**
* An array of theme settings.
*
* @var array
*/
protected $themeSettings;
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('node_type', 't')
->fields('t', [
'type',
'name',
'module',
'description',
'help',
'title_label',
'has_body',
'body_label',
'min_word_count',
'custom',
'modified',
'locked',
'orig_type',
])
->orderBy('t.type');
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'type' => $this->t('Machine name of the node type.'),
'name' => $this->t('Human name of the node type.'),
'module' => $this->t('The module providing the node type.'),
'description' => $this->t('Description of the node type.'),
'help' => $this->t('Help text for the node type.'),
'title_label' => $this->t('Title label.'),
'has_body' => $this->t('Flag indicating the node type has a body field.'),
'body_label' => $this->t('Body label.'),
'min_word_count' => $this->t('Minimum word count for the body field.'),
'custom' => $this->t('Flag.'),
'modified' => $this->t('Flag.'),
'locked' => $this->t('Flag.'),
'orig_type' => $this->t('The original type.'),
'teaser_length' => $this->t('Teaser length'),
];
if ($this->moduleExists('comment')) {
$fields += $this->getCommentFields();
}
return $fields;
}
/**
* Returns the fields containing comment settings for each node type.
*
* @return string[]
* An associative array of field descriptions, keyed by field.
*/
protected function getCommentFields() {
return [
'comment' => $this->t('Default comment setting'),
'comment_default_mode' => $this->t('Default display mode'),
'comment_default_per_page' => $this->t('Default comments per page'),
'comment_anonymous' => $this->t('Anonymous commenting'),
'comment_subject_field' => $this->t('Comment subject field'),
'comment_preview' => $this->t('Preview comment'),
'comment_form_location' => $this->t('Location of comment submission form'),
];
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$this->teaserLength = $this->variableGet('teaser_length', 600);
$this->nodePreview = $this->variableGet('node_preview', 0);
$this->themeSettings = $this->variableGet('theme_settings', []);
return parent::initializeIterator();
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$row->setSourceProperty('teaser_length', $this->teaserLength);
$row->setSourceProperty('node_preview', $this->nodePreview);
$type = $row->getSourceProperty('type');
$source_options = $this->variableGet('node_options_' . $type, ['promote', 'sticky']);
$options = [];
foreach (['promote', 'sticky', 'status', 'revision'] as $item) {
$options[$item] = in_array($item, $source_options);
}
$row->setSourceProperty('options', $options);
$submitted = isset($this->themeSettings['toggle_node_info_' . $type]) ? $this->themeSettings['toggle_node_info_' . $type] : FALSE;
$row->setSourceProperty('display_submitted', $submitted);
if ($default_node_menu = $this->variableGet('menu_default_node_menu', NULL)) {
$row->setSourceProperty('available_menus', [$default_node_menu]);
$row->setSourceProperty('parent', $default_node_menu . ':');
}
if ($this->moduleExists('comment')) {
foreach (array_keys($this->getCommentFields()) as $field) {
$row->setSourceProperty($field, $this->variableGet($field . '_' . $type, NULL));
}
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['type']['type'] = 'string';
return $ids;
}
}
@@ -0,0 +1,79 @@
<?php
namespace Drupal\node\Plugin\migrate\source\d6;
/**
* The view mode source.
*
* @MigrateSource(
* id = "d6_view_mode",
* source_module = "content"
* )
*/
class ViewMode extends ViewModeBase {
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$rows = [];
$result = $this->prepareQuery()->execute();
while ($field_row = $result->fetchAssoc()) {
$field_row['display_settings'] = unserialize($field_row['display_settings']);
foreach ($this->getViewModes() as $view_mode) {
// Append to the return value if the row has display settings for this
// view mode and the view mode is neither hidden nor excluded.
// @see \Drupal\field\Plugin\migrate\source\d6\FieldInstancePerViewMode::initializeIterator()
if (isset($field_row['display_settings'][$view_mode]) && $field_row['display_settings'][$view_mode]['format'] != 'hidden' && empty($field_row['display_settings'][$view_mode]['exclude'])) {
if (!isset($rows[$view_mode])) {
$rows[$view_mode]['entity_type'] = 'node';
$rows[$view_mode]['view_mode'] = $view_mode;
}
}
}
}
return new \ArrayIterator($rows);
}
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('content_node_field_instance', 'cnfi')
->fields('cnfi', [
'display_settings',
]);
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'display_settings' => $this->t('Serialize data with display settings.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['view_mode']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$this->dependencies = parent::calculateDependencies();
if (isset($this->configuration['constants']['targetEntityType'])) {
$this->addDependency('module', $this->entityTypeManager->getDefinition($this->configuration['constants']['targetEntityType'])->getProvider());
}
return $this->dependencies;
}
}
@@ -0,0 +1,48 @@
<?php
namespace Drupal\node\Plugin\migrate\source\d6;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* A base class for migrations that require view mode info.
*/
abstract class ViewModeBase extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function count($refresh = FALSE) {
return count($this->initializeIterator());
}
/**
* Get a list of D6 view modes.
*
* Drupal 6 supported the following view modes.
* NODE_BUILD_NORMAL = 0
* NODE_BUILD_PREVIEW = 1
* NODE_BUILD_SEARCH_INDEX = 2
* NODE_BUILD_SEARCH_RESULT = 3
* NODE_BUILD_RSS = 4
* NODE_BUILD_PRINT = 5
* teaser
* full
*
* @return array
* The view mode names.
*/
public function getViewModes() {
return [
0,
1,
2,
3,
4,
5,
'teaser',
'full',
];
}
}
@@ -0,0 +1,200 @@
<?php
namespace Drupal\node\Plugin\migrate\source\d7;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Drupal 7 node source from database.
*
* @MigrateSource(
* id = "d7_node",
* source_module = "node"
* )
*/
class Node extends FieldableEntity {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state, $entity_type_manager);
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('state'),
$container->get('entity_type.manager'),
$container->get('module_handler')
);
}
/**
* The join options between the node and the node_revisions table.
*/
const JOIN = 'n.vid = nr.vid';
/**
* {@inheritdoc}
*/
public function query() {
// Select node in its last revision.
$query = $this->select('node_revision', 'nr')
->fields('n', [
'nid',
'type',
'language',
'status',
'created',
'changed',
'comment',
'promote',
'sticky',
'tnid',
'translate',
])
->fields('nr', [
'vid',
'title',
'log',
'timestamp',
]);
$query->addField('n', 'uid', 'node_uid');
$query->addField('nr', 'uid', 'revision_uid');
$query->innerJoin('node', 'n', static::JOIN);
// If the content_translation module is enabled, get the source langcode
// to fill the content_translation_source field.
if ($this->moduleHandler->moduleExists('content_translation')) {
$query->leftJoin('node', 'nt', 'n.tnid = nt.nid');
$query->addField('nt', 'language', 'source_langcode');
}
$this->handleTranslations($query);
if (isset($this->configuration['node_type'])) {
$query->condition('n.type', $this->configuration['node_type']);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$nid = $row->getSourceProperty('nid');
$vid = $row->getSourceProperty('vid');
$type = $row->getSourceProperty('type');
// If this entity was translated using Entity Translation, we need to get
// its source language to get the field values in the right language.
// The translations will be migrated by the d7_node_entity_translation
// migration.
$entity_translatable = $this->isEntityTranslatable('node') && (int) $this->variableGet('language_content_type_' . $type, 0) === 4;
$source_language = $this->getEntityTranslationSourceLanguage('node', $nid);
$language = $entity_translatable && $source_language ? $source_language : $row->getSourceProperty('language');
// If this is using d7_node_complete source plugin and this is a node
// using entity translation then set the language of this revision to the
// entity translation language.
if ($row->getSourceProperty('etr_created')) {
$language = $row->getSourceProperty('language');
}
// Get Field API field values.
foreach ($this->getFields('node', $type) as $field_name => $field) {
// Ensure we're using the right language if the entity and the field are
// translatable.
$field_language = $entity_translatable && $field['translatable'] ? $language : NULL;
$row->setSourceProperty($field_name, $this->getFieldValues('node', $field_name, $nid, $vid, $field_language));
}
// Make sure we always have a translation set.
if ($row->getSourceProperty('tnid') == 0) {
$row->setSourceProperty('tnid', $row->getSourceProperty('nid'));
}
// If the node title was replaced by a real field using the Drupal 7 Title
// module, use the field value instead of the node title.
if ($this->moduleExists('title')) {
$title_field = $row->getSourceProperty('title_field');
if (isset($title_field[0]['value'])) {
$row->setSourceProperty('title', $title_field[0]['value']);
}
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'nid' => $this->t('Node ID'),
'type' => $this->t('Type'),
'title' => $this->t('Title'),
'node_uid' => $this->t('Node authored by (uid)'),
'revision_uid' => $this->t('Revision authored by (uid)'),
'created' => $this->t('Created timestamp'),
'changed' => $this->t('Modified timestamp'),
'status' => $this->t('Published'),
'promote' => $this->t('Promoted to front page'),
'sticky' => $this->t('Sticky at top of lists'),
'revision' => $this->t('Create new revision'),
'language' => $this->t('Language (fr, en, ...)'),
'tnid' => $this->t('The translation set id for this node'),
'timestamp' => $this->t('The timestamp the latest revision of this node was created.'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['nid']['type'] = 'integer';
$ids['nid']['alias'] = 'n';
return $ids;
}
/**
* Adapt our query for translations.
*
* @param \Drupal\Core\Database\Query\SelectInterface $query
* The generated query.
*/
protected function handleTranslations(SelectInterface $query) {
// Check whether or not we want translations.
if (empty($this->configuration['translations'])) {
// No translations: Yield untranslated nodes, or default translations.
$query->where('n.tnid = 0 OR n.tnid = n.nid');
}
else {
// Translations: Yield only non-default translations.
$query->where('n.tnid <> 0 AND n.tnid <> n.nid');
}
}
}
@@ -0,0 +1,98 @@
<?php
namespace Drupal\node\Plugin\migrate\source\d7;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\migrate\Row;
/**
* Gets all node revisions from the source, including translation revisions.
*
* @MigrateSource(
* id = "d7_node_complete",
* source_module = "node"
* )
*/
class NodeComplete extends NodeRevision {
/**
* The join options between the node and the node_revisions_table.
*/
const JOIN = 'n.nid = nr.nid';
/**
* {@inheritdoc}
*/
public function query() {
$query = parent::query();
$query->orderBy('nr.vid');
// Get any entity translation revision data.
if ($this->getDatabase()->schema()
->tableExists('entity_translation_revision')) {
$query->leftJoin('entity_translation_revision', 'etr', 'nr.nid = etr.entity_id AND nr.vid=etr.revision_id');
$query->fields('etr', [
'entity_type',
'entity_id',
'revision_id',
'source',
'translate',
]);
$conditions = $query->orConditionGroup();
$conditions->condition('etr.entity_type', 'node');
$conditions->isNull('etr.entity_type');
$query->condition($conditions);
$query->addExpression("COALESCE(etr.language, n.language)", 'language');
$query->addField('etr', 'uid', 'etr_uid');
$query->addField('etr', 'status', 'etr_status');
$query->addField('etr', 'created', 'etr_created');
$query->addField('etr', 'changed', 'etr_changed');
$query->orderBy('etr.revision_id');
$query->orderBy('etr.language');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Override properties when this is an entity translation revision. The tnid
// will be set in d7_node source plugin to the value of 'nid'.
if ($row->getSourceProperty('etr_created')) {
$row->setSourceProperty('vid', $row->getSourceProperty('revision_id'));
$row->setSourceProperty('created', $row->getSourceProperty('etr_created'));
$row->setSourceProperty('timestamp', $row->getSourceProperty('etr_changed'));
$row->setSourceProperty('revision_uid', $row->getSourceProperty('etr_uid'));
$row->setSourceProperty('source_langcode', $row->getSourceProperty('source'));
}
parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'nid' => [
'type' => 'integer',
'alias' => 'n',
],
'vid' => [
'type' => 'integer',
'alias' => 'nr',
],
'language' => [
'type' => 'string',
'alias' => 'n',
],
];
}
/**
* {@inheritdoc}
*/
protected function handleTranslations(SelectInterface $query) {}
}
@@ -0,0 +1,118 @@
<?php
namespace Drupal\node\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
/**
* Provides Drupal 7 node entity translations source plugin.
*
* @MigrateSource(
* id = "d7_node_entity_translation",
* source_module = "entity_translation"
* )
*/
class NodeEntityTranslation extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('entity_translation', 'et')
->fields('et')
->fields('n', [
'title',
'type',
'promote',
'sticky',
])
->fields('nr', [
'log',
'timestamp',
])
->condition('et.entity_type', 'node')
->condition('et.source', '', '<>');
$query->addField('nr', 'uid', 'revision_uid');
$query->innerJoin('node', 'n', 'n.nid = et.entity_id');
$query->innerJoin('node_revision', 'nr', 'nr.vid = et.revision_id');
if (isset($this->configuration['node_type'])) {
$query->condition('n.type', $this->configuration['node_type']);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$nid = $row->getSourceProperty('entity_id');
$vid = $row->getSourceProperty('revision_id');
$type = $row->getSourceProperty('type');
$language = $row->getSourceProperty('language');
// Get Field API field values.
foreach ($this->getFields('node', $type) as $field_name => $field) {
// Ensure we're using the right language if the entity is translatable.
$field_language = $field['translatable'] ? $language : NULL;
$row->setSourceProperty($field_name, $this->getFieldValues('node', $field_name, $nid, $vid, $field_language));
}
// If the node title was replaced by a real field using the Drupal 7 Title
// module, use the field value instead of the node title.
if ($this->moduleExists('title')) {
$title_field = $row->getSourceProperty('title_field');
if (isset($title_field[0]['value'])) {
$row->setSourceProperty('title', $title_field[0]['value']);
}
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'entity_type' => $this->t('The entity type this translation relates to'),
'entity_id' => $this->t('The entity ID this translation relates to'),
'revision_id' => $this->t('The entity revision ID this translation relates to'),
'language' => $this->t('The target language for this translation.'),
'source' => $this->t('The source language from which this translation was created.'),
'uid' => $this->t('The author of this translation.'),
'status' => $this->t('Boolean indicating whether the translation is published (visible to non-administrators).'),
'translate' => $this->t('A boolean indicating whether this translation needs to be updated.'),
'created' => $this->t('The Unix timestamp when the translation was created.'),
'changed' => $this->t('The Unix timestamp when the translation was most recently saved.'),
'title' => $this->t('Node title'),
'type' => $this->t('Node type'),
'promote' => $this->t('Promoted to front page'),
'sticky' => $this->t('Sticky at top of lists'),
'log' => $this->t('Revision log'),
'timestamp' => $this->t('The timestamp the latest revision of this node was created.'),
'revision_uid' => $this->t('Revision authored by (uid)'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'entity_id' => [
'type' => 'integer',
'alias' => 'et',
],
'language' => [
'type' => 'string',
'alias' => 'et',
],
];
}
}
@@ -0,0 +1,41 @@
<?php
namespace Drupal\node\Plugin\migrate\source\d7;
/**
* Drupal 7 node revision source from database.
*
* @MigrateSource(
* id = "d7_node_revision",
* source_module = "node"
* )
*/
class NodeRevision extends Node {
/**
* The join options between the node and the node_revisions_table.
*/
const JOIN = 'n.nid = nr.nid AND n.vid <> nr.vid';
/**
* {@inheritdoc}
*/
public function fields() {
// Use all the node fields plus the vid that identifies the version.
return parent::fields() + [
'vid' => t('The primary identifier for this version.'),
'log' => $this->t('Revision Log message'),
'timestamp' => $this->t('Revision timestamp'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
$ids['vid']['alias'] = 'nr';
return $ids;
}
}
@@ -0,0 +1,150 @@
<?php
namespace Drupal\node\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 Node types source from database.
*
* @MigrateSource(
* id = "d7_node_type",
* source_module = "node"
* )
*/
class NodeType extends DrupalSqlBase {
/**
* The teaser length
*
* @var int
*/
protected $teaserLength;
/**
* Node preview optional / required.
*
* @var int
*/
protected $nodePreview;
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('node_type', 't')->fields('t');
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'type' => $this->t('Machine name of the node type.'),
'name' => $this->t('Human name of the node type.'),
'description' => $this->t('Description of the node type.'),
'help' => $this->t('Help text for the node type.'),
'title_label' => $this->t('Title label.'),
'disabled' => $this->t('Flag indicating the node type is enable'),
'base' => $this->t('base node.'),
'custom' => $this->t('Flag.'),
'modified' => $this->t('Flag.'),
'locked' => $this->t('Flag.'),
'orig_type' => $this->t('The original type.'),
'teaser_length' => $this->t('Teaser length'),
];
if ($this->moduleExists('comment')) {
$fields += $this->getCommentFields();
}
return $fields;
}
/**
* Returns the fields containing comment settings for each node type.
*
* @return string[]
* An associative array of field descriptions, keyed by field.
*/
protected function getCommentFields() {
return [
'comment' => $this->t('Default comment setting'),
'comment_default_mode' => $this->t('Default display mode'),
'comment_default_per_page' => $this->t('Default comments per page'),
'comment_anonymous' => $this->t('Anonymous commenting'),
'comment_subject_field' => $this->t('Comment subject field'),
'comment_preview' => $this->t('Preview comment'),
'comment_form_location' => $this->t('Location of comment submission form'),
];
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$this->teaserLength = $this->variableGet('teaser_length', 600);
$this->nodePreview = $this->variableGet('node_preview', 0);
return parent::initializeIterator();
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$row->setSourceProperty('teaser_length', $this->teaserLength);
$row->setSourceProperty('node_preview', $this->nodePreview);
$type = $row->getSourceProperty('type');
$source_options = $this->variableGet('node_options_' . $type, ['promote', 'sticky']);
$options = [];
foreach (['promote', 'sticky', 'status', 'revision'] as $item) {
$options[$item] = in_array($item, $source_options);
}
$row->setSourceProperty('options', $options);
// Don't create a body field until we prove that this node type has one.
$row->setSourceProperty('create_body', FALSE);
if ($this->moduleExists('field')) {
// Find body field for this node type.
$body = $this->select('field_config_instance', 'fci')
->fields('fci', ['data'])
->condition('entity_type', 'node')
->condition('bundle', $row->getSourceProperty('type'))
->condition('field_name', 'body')
->execute()
->fetchAssoc();
if ($body) {
$row->setSourceProperty('create_body', TRUE);
$body['data'] = unserialize($body['data']);
$row->setSourceProperty('body_label', $body['data']['label']);
}
}
$row->setSourceProperty('display_submitted', $this->variableGet('node_submitted_' . $type, TRUE));
if ($menu_options = $this->variableGet('menu_options_' . $type, NULL)) {
$row->setSourceProperty('available_menus', $menu_options);
}
if ($parent = $this->variableGet('menu_parent_' . $type, NULL)) {
$row->setSourceProperty('parent', $parent . ':');
}
if ($this->moduleExists('comment')) {
foreach (array_keys($this->getCommentFields()) as $field) {
$row->setSourceProperty($field, $this->variableGet($field . '_' . $type, NULL));
}
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['type']['type'] = 'string';
return $ids;
}
}
@@ -0,0 +1,77 @@
<?php
namespace Drupal\node\Plugin\views\area;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Url;
use Drupal\views\Plugin\views\area\AreaPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines an area plugin to display a node/add link.
*
* @ingroup views_area_handlers
*
* @ViewsArea("node_listing_empty")
*/
class ListingEmpty extends AreaPluginBase {
/**
* The access manager.
*
* @var \Drupal\Core\Access\AccessManagerInterface
*/
protected $accessManager;
/**
* Constructs a new ListingEmpty.
*
* @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\Core\Access\AccessManagerInterface $access_manager
* The access manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, AccessManagerInterface $access_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->accessManager = $access_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('access_manager')
);
}
/**
* {@inheritdoc}
*/
public function render($empty = FALSE) {
$account = \Drupal::currentUser();
if (!$empty || !empty($this->options['empty'])) {
$element = [
'#theme' => 'links',
'#links' => [
[
'url' => Url::fromRoute('node.add_page'),
'title' => $this->t('Add content'),
],
],
'#access' => $this->accessManager->checkNamedRoute('node.add_page', [], $account),
];
return $element;
}
return [];
}
}
@@ -0,0 +1,64 @@
<?php
namespace Drupal\node\Plugin\views\argument;
use Drupal\node\NodeStorageInterface;
use Drupal\views\Plugin\views\argument\NumericArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Argument handler to accept a node id.
*
* @ViewsArgument("node_nid")
*/
class Nid extends NumericArgument {
/**
* The node storage.
*
* @var \Drupal\node\NodeStorageInterface
*/
protected $nodeStorage;
/**
* Constructs the Nid 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\node\NodeStorageInterface $node_storage
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, NodeStorageInterface $node_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->nodeStorage = $node_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('node')
);
}
/**
* Override the behavior of title(). Get the title of the node.
*/
public function titleQuery() {
$titles = [];
$nodes = $this->nodeStorage->loadMultiple($this->value);
foreach ($nodes as $node) {
$titles[] = $node->label();
}
return $titles;
}
}
@@ -0,0 +1,76 @@
<?php
namespace Drupal\node\Plugin\views\argument;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\views\Plugin\views\argument\StringArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Argument handler to accept a node type.
*
* @ViewsArgument("node_type")
*/
class Type extends StringArgument {
/**
* NodeType storage handler.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeTypeStorage;
/**
* Constructs a new Node Type 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\Core\Entity\EntityStorageInterface $node_type_storage
* The entity storage class.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $node_type_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->nodeTypeStorage = $node_type_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$entity_type_manager = $container->get('entity_type.manager');
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$entity_type_manager->getStorage('node_type')
);
}
/**
* Override the behavior of summaryName(). Get the user friendly version
* of the node type.
*/
public function summaryName($data) {
return $this->node_type($data->{$this->name_alias});
}
/**
* Override the behavior of title(). Get the user friendly version of the
* node type.
*/
public function title() {
return $this->node_type($this->argument);
}
public function node_type($type_name) {
$type = $this->nodeTypeStorage->load($type_name);
$output = $type ? $type->label() : $this->t('Unknown content type');
return $output;
}
}
@@ -0,0 +1,21 @@
<?php
namespace Drupal\node\Plugin\views\argument;
use Drupal\user\Plugin\views\argument\Uid;
/**
* Filter handler to accept a user id to check for nodes that
* user posted or created a revision on.
*
* @ViewsArgument("node_uid_revision")
*/
class UidRevision extends Uid {
public function query($group_by = FALSE) {
$this->ensureMyTable();
$placeholder = $this->placeholder();
$this->query->addWhereExpression(0, "$this->tableAlias.uid = $placeholder OR ((SELECT COUNT(DISTINCT vid) FROM {node_revision} nr WHERE nr.revision_uid = $placeholder AND nr.nid = $this->tableAlias.nid) > 0)", [$placeholder => $this->argument]);
}
}
@@ -0,0 +1,87 @@
<?php
namespace Drupal\node\Plugin\views\argument;
use Drupal\Core\Database\Connection;
use Drupal\views\Plugin\views\argument\NumericArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\node\NodeStorageInterface;
/**
* Argument handler to accept a node revision id.
*
* @ViewsArgument("node_vid")
*/
class Vid extends NumericArgument {
/**
* Database Service Object.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The node storage.
*
* @var \Drupal\node\NodeStorageInterface
*/
protected $nodeStorage;
/**
* Constructs a \Drupal\node\Plugin\views\argument\Vid 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\Core\Database\Connection $database
* Database Service Object.
* @param \Drupal\node\NodeStorageInterface $node_storage
* The node storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, NodeStorageInterface $node_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->database = $database;
$this->nodeStorage = $node_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('database'),
$container->get('entity_type.manager')->getStorage('node')
);
}
/**
* Override the behavior of title(). Get the title of the revision.
*/
public function titleQuery() {
$titles = [];
$results = $this->database->query('SELECT nr.vid, nr.nid, npr.title FROM {node_revision} nr WHERE nr.vid IN ( :vids[] )', [':vids[]' => $this->value])->fetchAllAssoc('vid', PDO::FETCH_ASSOC);
$nids = [];
foreach ($results as $result) {
$nids[] = $result['nid'];
}
$nodes = $this->nodeStorage->loadMultiple(array_unique($nids));
foreach ($results as $result) {
$nodes[$result['nid']]->set('title', $result['title']);
$titles[] = $nodes[$result['nid']]->label();
}
return $titles;
}
}
@@ -0,0 +1,82 @@
<?php
namespace Drupal\node\Plugin\views\argument_default;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Default argument plugin to extract a node.
*
* @ViewsArgumentDefault(
* id = "node",
* title = @Translation("Content ID from URL")
* )
*/
class Node extends ArgumentDefaultPluginBase implements CacheableDependencyInterface {
/**
* The route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a new Node instance.
*
* @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\Core\Routing\RouteMatchInterface $route_match
* The route match.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('current_route_match')
);
}
/**
* {@inheritdoc}
*/
public function getArgument() {
if (($node = $this->routeMatch->getParameter('node')) && $node instanceof NodeInterface) {
return $node->id();
}
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['url'];
}
}
@@ -0,0 +1,100 @@
<?php
namespace Drupal\node\Plugin\views\field;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\field\FieldPluginBase;
/**
* Field handler to provide simple renderer that allows linking to a node.
* Definition terms:
* - link_to_node default: Should this field have the checkbox "link to node" enabled by default.
*
* @ingroup views_field_handlers
*
* @ViewsField("node")
*/
class Node extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
// Don't add the additional fields to groupby
if (!empty($this->options['link_to_node'])) {
$this->additional_fields['nid'] = ['table' => 'node_field_data', 'field' => 'nid'];
}
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['link_to_node'] = ['default' => isset($this->definition['link_to_node default']) ? $this->definition['link_to_node default'] : FALSE];
return $options;
}
/**
* Provide link to node option
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['link_to_node'] = [
'#title' => $this->t('Link this field to the original piece of content'),
'#description' => $this->t("Enable to override this field's links."),
'#type' => 'checkbox',
'#default_value' => !empty($this->options['link_to_node']),
];
parent::buildOptionsForm($form, $form_state);
}
/**
* Prepares link to the node.
*
* @param string $data
* The XSS safe string for the link text.
* @param \Drupal\views\ResultRow $values
* The values retrieved from a single row of a view's query result.
*
* @return string
* Returns a string for the link text.
*/
protected function renderLink($data, ResultRow $values) {
if (!empty($this->options['link_to_node']) && !empty($this->additional_fields['nid'])) {
if ($data !== NULL && $data !== '') {
$this->options['alter']['make_link'] = TRUE;
$this->options['alter']['url'] = Url::fromRoute('entity.node.canonical', ['node' => $this->getValue($values, 'nid')]);
if (isset($this->aliases['langcode'])) {
$languages = \Drupal::languageManager()->getLanguages();
$langcode = $this->getValue($values, 'langcode');
if (isset($languages[$langcode])) {
$this->options['alter']['language'] = $languages[$langcode];
}
else {
unset($this->options['alter']['language']);
}
}
}
else {
$this->options['alter']['make_link'] = FALSE;
}
}
return $data;
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
$value = $this->getValue($values);
return $this->renderLink($this->sanitizeValue($value), $values);
}
}
@@ -0,0 +1,21 @@
<?php
namespace Drupal\node\Plugin\views\field;
use Drupal\views\Plugin\views\field\BulkForm;
/**
* Defines a node operations bulk form element.
*
* @ViewsField("node_bulk_form")
*/
class NodeBulkForm extends BulkForm {
/**
* {@inheritdoc}
*/
protected function emptySelectedMessage() {
return $this->t('No content selected.');
}
}
@@ -0,0 +1,77 @@
<?php
namespace Drupal\node\Plugin\views\field;
@trigger_error('Drupal\node\Plugin\views\field\Path is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use @ViewsField("entity_link") with \'output_url_as_text\' set.', E_USER_DEPRECATED);
use Drupal\Core\Url;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* Field handler to present the path to the node.
*
* @ingroup views_field_handlers
*
* @ViewsField("node_path")
*
* @deprecated in drupal:8.5.0 and is removed from drupal:9.0.0.
* Use @ViewsField("entity_link") with 'output_url_as_text' set.
*/
class Path extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
$this->additional_fields['nid'] = 'nid';
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['absolute'] = ['default' => FALSE];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$form['absolute'] = [
'#type' => 'checkbox',
'#title' => $this->t('Use absolute link (begins with "http://")'),
'#default_value' => $this->options['absolute'],
'#description' => $this->t('Enable this option to output an absolute link. Required if you want to use the path as a link destination (as in "output this field as a link" above).'),
'#fieldset' => 'alter',
];
}
/**
* {@inheritdoc}
*/
public function query() {
$this->ensureMyTable();
$this->addAdditionalFields();
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
$nid = $this->getValue($values, 'nid');
return [
'#markup' => Url::fromRoute('entity.node.canonical', ['node' => $nid], ['absolute' => $this->options['absolute']])->toString(),
];
}
}
@@ -0,0 +1,51 @@
<?php
namespace Drupal\node\Plugin\views\field;
use Drupal\Core\Url;
use Drupal\views\Plugin\views\field\LinkBase;
use Drupal\views\ResultRow;
/**
* Field handler to present a link to a node revision.
*
* @ingroup views_field_handlers
*
* @ViewsField("node_revision_link")
*/
class RevisionLink extends LinkBase {
/**
* {@inheritdoc}
*/
protected function getUrlInfo(ResultRow $row) {
/** @var \Drupal\node\NodeInterface $node */
$node = $this->getEntity($row);
// Current revision uses the node view path.
return !$node->isDefaultRevision() ?
Url::fromRoute('entity.node.revision', ['node' => $node->id(), 'node_revision' => $node->getRevisionId()]) :
$node->toUrl();
}
/**
* {@inheritdoc}
*/
protected function renderLink(ResultRow $row) {
/** @var \Drupal\node\NodeInterface $node */
$node = $this->getEntity($row);
if (!$node->getRevisionid()) {
return '';
}
$text = parent::renderLink($row);
$this->options['alter']['query'] = $this->getDestinationArray();
return $text;
}
/**
* {@inheritdoc}
*/
protected function getDefaultLabel() {
return $this->t('View');
}
}
@@ -0,0 +1,33 @@
<?php
namespace Drupal\node\Plugin\views\field;
use Drupal\Core\Url;
use Drupal\views\ResultRow;
/**
* Field handler to present link to delete a node revision.
*
* @ingroup views_field_handlers
*
* @ViewsField("node_revision_link_delete")
*/
class RevisionLinkDelete extends RevisionLink {
/**
* {@inheritdoc}
*/
protected function getUrlInfo(ResultRow $row) {
/** @var \Drupal\node\NodeInterface $node */
$node = $this->getEntity($row);
return Url::fromRoute('node.revision_delete_confirm', ['node' => $node->id(), 'node_revision' => $node->getRevisionId()]);
}
/**
* {@inheritdoc}
*/
protected function getDefaultLabel() {
return $this->t('Delete');
}
}
@@ -0,0 +1,33 @@
<?php
namespace Drupal\node\Plugin\views\field;
use Drupal\Core\Url;
use Drupal\views\ResultRow;
/**
* Field handler to present a link to revert a node to a revision.
*
* @ingroup views_field_handlers
*
* @ViewsField("node_revision_link_revert")
*/
class RevisionLinkRevert extends RevisionLink {
/**
* {@inheritdoc}
*/
protected function getUrlInfo(ResultRow $row) {
/** @var \Drupal\node\NodeInterface $node */
$node = $this->getEntity($row);
return Url::fromRoute('node.revision_revert_confirm', ['node' => $node->id(), 'node_revision' => $node->getRevisionId()]);
}
/**
* {@inheritdoc}
*/
protected function getDefaultLabel() {
return $this->t('Revert');
}
}
@@ -0,0 +1,59 @@
<?php
namespace Drupal\node\Plugin\views\filter;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\filter\FilterPluginBase;
/**
* Filter by node_access records.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("node_access")
*/
class Access extends FilterPluginBase {
public function adminSummary() {}
protected function operatorForm(&$form, FormStateInterface $form_state) {}
public function canExpose() {
return FALSE;
}
/**
* See _node_access_where_sql() for a non-views query based implementation.
*/
public function query() {
$account = $this->view->getUser();
if (!$account->hasPermission('bypass node access')) {
$table = $this->ensureMyTable();
$grants = new Condition('OR');
foreach (node_access_grants('view', $account) as $realm => $gids) {
foreach ($gids as $gid) {
$grants->condition((new Condition('AND'))
->condition($table . '.gid', $gid)
->condition($table . '.realm', $realm)
);
}
}
$this->query->addWhere('AND', $grants);
$this->query->addWhere('AND', $table . '.grant_view', 1, '>=');
}
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
$contexts = parent::getCacheContexts();
$contexts[] = 'user.node_grants:view';
return $contexts;
}
}
@@ -0,0 +1,45 @@
<?php
namespace Drupal\node\Plugin\views\filter;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\filter\FilterPluginBase;
/**
* Filter by published status.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("node_status")
*/
class Status extends FilterPluginBase {
public function adminSummary() {}
protected function operatorForm(&$form, FormStateInterface $form_state) {}
public function canExpose() {
return FALSE;
}
public function query() {
$table = $this->ensureMyTable();
$snippet = "$table.status = 1 OR ($table.uid = ***CURRENT_USER*** AND ***CURRENT_USER*** <> 0 AND ***VIEW_OWN_UNPUBLISHED_NODES*** = 1) OR ***BYPASS_NODE_ACCESS*** = 1";
if ($this->moduleHandler->moduleExists('content_moderation')) {
$snippet .= ' OR ***VIEW_ANY_UNPUBLISHED_NODES*** = 1';
}
$this->query->addWhereExpression($this->options['group'], $snippet);
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
$contexts = parent::getCacheContexts();
$contexts[] = 'user';
return $contexts;
}
}
@@ -0,0 +1,28 @@
<?php
namespace Drupal\node\Plugin\views\filter;
use Drupal\user\Plugin\views\filter\Name;
/**
* Filter handler to check for revisions a certain user has created.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("node_uid_revision")
*/
class UidRevision extends Name {
public function query($group_by = FALSE) {
$this->ensureMyTable();
$placeholder = $this->placeholder() . '[]';
$args = array_values($this->value);
$this->query->addWhereExpression($this->options['group'], "$this->tableAlias.uid IN($placeholder) OR
((SELECT COUNT(DISTINCT vid) FROM {node_revision} nr WHERE nr.revision_uid IN ($placeholder) AND nr.nid = $this->tableAlias.nid) > 0)", [$placeholder => $args],
$args);
}
}
@@ -0,0 +1,31 @@
<?php
namespace Drupal\node\Plugin\views\row;
use Drupal\views\Plugin\views\row\EntityRow;
/**
* Plugin which performs a node_view on the resulting object.
*
* Most of the code on this object is in the theme function.
*
* @ingroup views_row_plugins
*
* @ViewsRow(
* id = "entity:node",
* )
*/
class NodeRow extends EntityRow {
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['view_mode']['default'] = 'teaser';
return $options;
}
}
@@ -0,0 +1,171 @@
<?php
namespace Drupal\node\Plugin\views\row;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\views\Plugin\views\row\RssPluginBase;
/**
* Plugin which performs a node_view on the resulting object
* and formats it as an RSS item.
*
* @ViewsRow(
* id = "node_rss",
* title = @Translation("Content"),
* help = @Translation("Display the content with standard node view."),
* theme = "views_view_row_rss",
* register_theme = FALSE,
* base = {"node_field_data"},
* display_types = {"feed"}
* )
*/
class Rss extends RssPluginBase {
// Basic properties that let the row style follow relationships.
public $base_table = 'node_field_data';
public $base_field = 'nid';
// Stores the nodes loaded with preRender.
public $nodes = [];
/**
* {@inheritdoc}
*/
protected $entityTypeId = 'node';
/**
* The node storage
*
* @var \Drupal\node\NodeStorageInterface
*/
protected $nodeStorage;
/**
* Constructs the Rss 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\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_display_repository);
$this->nodeStorage = $entity_type_manager->getStorage('node');
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm_summary_options() {
$options = parent::buildOptionsForm_summary_options();
$options['title'] = $this->t('Title only');
$options['default'] = $this->t('Use site default RSS settings');
return $options;
}
public function summaryTitle() {
$options = $this->buildOptionsForm_summary_options();
return $options[$this->options['view_mode']];
}
public function preRender($values) {
$nids = [];
foreach ($values as $row) {
$nids[] = $row->{$this->field_alias};
}
if (!empty($nids)) {
$this->nodes = $this->nodeStorage->loadMultiple($nids);
}
}
public function render($row) {
global $base_url;
$nid = $row->{$this->field_alias};
if (!is_numeric($nid)) {
return;
}
$display_mode = $this->options['view_mode'];
if ($display_mode == 'default') {
$display_mode = \Drupal::config('system.rss')->get('items.view_mode');
}
// Load the specified node:
/** @var \Drupal\node\NodeInterface $node */
$node = $this->nodes[$nid];
if (empty($node)) {
return;
}
$node->link = $node->toUrl('canonical', ['absolute' => TRUE])->toString();
$node->rss_namespaces = [];
$node->rss_elements = [
[
'key' => 'pubDate',
'value' => gmdate('r', $node->getCreatedTime()),
],
[
'key' => 'dc:creator',
'value' => $node->getOwner()->getDisplayName(),
],
[
'key' => 'guid',
'value' => $node->id() . ' at ' . $base_url,
'attributes' => ['isPermaLink' => 'false'],
],
];
// The node gets built and modules add to or modify $node->rss_elements
// and $node->rss_namespaces.
$build_mode = $display_mode;
$build = \Drupal::entityTypeManager()
->getViewBuilder('node')
->view($node, $build_mode);
unset($build['#theme']);
if (!empty($node->rss_namespaces)) {
$this->view->style_plugin->namespaces = array_merge($this->view->style_plugin->namespaces, $node->rss_namespaces);
}
elseif (function_exists('rdf_get_namespaces')) {
// Merge RDF namespaces in the XML namespaces in case they are used
// further in the RSS content.
$xml_rdf_namespaces = [];
foreach (rdf_get_namespaces() as $prefix => $uri) {
$xml_rdf_namespaces['xmlns:' . $prefix] = $uri;
}
$this->view->style_plugin->namespaces += $xml_rdf_namespaces;
}
$item = new \stdClass();
if ($display_mode != 'title') {
// We render node contents.
$item->description = $build;
}
$item->title = $node->label();
$item->link = $node->link;
// Provide a reference so that the render call in
// template_preprocess_views_view_row_rss() can still access it.
$item->elements = &$node->rss_elements;
$item->nid = $node->id();
$build = [
'#theme' => $this->themeFunctions(),
'#view' => $this->view,
'#options' => $this->options,
'#row' => $item,
];
return $build;
}
}
@@ -0,0 +1,322 @@
<?php
namespace Drupal\node\Plugin\views\wizard;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\wizard\WizardPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @todo: replace numbers with constants.
*/
/**
* Tests creating node views with the wizard.
*
* @ViewsWizard(
* id = "node",
* base_table = "node_field_data",
* title = @Translation("Content")
* )
*/
class Node extends WizardPluginBase {
/**
* Set the created column.
*
* @var string
*/
protected $createdColumn = 'node_field_data-created';
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* Node constructor.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info_service
* The entity bundle info service.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeBundleInfoInterface $bundle_info_service, EntityDisplayRepositoryInterface $entity_display_repository = NULL, EntityFieldManagerInterface $entity_field_manager = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $bundle_info_service);
if (!$entity_display_repository) {
@trigger_error('The entity_display.repository service must be passed to ' . __METHOD__ . ', it is required before Drupal 9.0.0. See https://www.drupal.org/node/2835616.', E_USER_DEPRECATED);
$entity_display_repository = \Drupal::service('entity_display.repository');
}
$this->entityDisplayRepository = $entity_display_repository;
if (!$entity_field_manager) {
@trigger_error('The entity_field.manager service must be passed to ' . __METHOD__ . ', it is required before Drupal 9.0.0. See https://www.drupal.org/node/2835616.', E_USER_DEPRECATED);
$entity_field_manager = \Drupal::service('entity_field.manager');
}
$this->entityFieldManager = $entity_field_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.bundle.info'),
$container->get('entity_display.repository'),
$container->get('entity_field.manager')
);
}
/**
* Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::getAvailableSorts().
*
* @return array
* An array whose keys are the available sort options and whose
* corresponding values are human readable labels.
*/
public function getAvailableSorts() {
// You can't execute functions in properties, so override the method
return [
'node_field_data-title:ASC' => $this->t('Title'),
];
}
/**
* {@inheritdoc}
*/
protected function rowStyleOptions() {
$options = [];
$options['teasers'] = $this->t('teasers');
$options['full_posts'] = $this->t('full posts');
$options['titles'] = $this->t('titles');
$options['titles_linked'] = $this->t('titles (linked)');
$options['fields'] = $this->t('fields');
return $options;
}
/**
* {@inheritdoc}
*/
protected function defaultDisplayOptions() {
$display_options = parent::defaultDisplayOptions();
// Add permission-based access control.
$display_options['access']['type'] = 'perm';
$display_options['access']['options']['perm'] = 'access content';
// Remove the default fields, since we are customizing them here.
unset($display_options['fields']);
// Add the title field, so that the display has content if the user switches
// to a row style that uses fields.
/* Field: Content: Title */
$display_options['fields']['title']['id'] = 'title';
$display_options['fields']['title']['table'] = 'node_field_data';
$display_options['fields']['title']['field'] = 'title';
$display_options['fields']['title']['entity_type'] = 'node';
$display_options['fields']['title']['entity_field'] = 'title';
$display_options['fields']['title']['label'] = '';
$display_options['fields']['title']['alter']['alter_text'] = 0;
$display_options['fields']['title']['alter']['make_link'] = 0;
$display_options['fields']['title']['alter']['absolute'] = 0;
$display_options['fields']['title']['alter']['trim'] = 0;
$display_options['fields']['title']['alter']['word_boundary'] = 0;
$display_options['fields']['title']['alter']['ellipsis'] = 0;
$display_options['fields']['title']['alter']['strip_tags'] = 0;
$display_options['fields']['title']['alter']['html'] = 0;
$display_options['fields']['title']['hide_empty'] = 0;
$display_options['fields']['title']['empty_zero'] = 0;
$display_options['fields']['title']['settings']['link_to_entity'] = 1;
$display_options['fields']['title']['plugin_id'] = 'field';
return $display_options;
}
/**
* {@inheritdoc}
*/
protected function defaultDisplayFiltersUser(array $form, FormStateInterface $form_state) {
$filters = parent::defaultDisplayFiltersUser($form, $form_state);
$tids = [];
if ($values = $form_state->getValue(['show', 'tagged_with'])) {
foreach ($values as $value) {
$tids[] = $value['target_id'];
}
}
if (!empty($tids)) {
$vid = reset($form['displays']['show']['tagged_with']['#selection_settings']['target_bundles']);
$filters['tid'] = [
'id' => 'tid',
'table' => 'taxonomy_index',
'field' => 'tid',
'value' => $tids,
'vid' => $vid,
'plugin_id' => 'taxonomy_index_tid',
];
// If the user entered more than one valid term in the autocomplete
// field, they probably intended both of them to be applied.
if (count($tids) > 1) {
$filters['tid']['operator'] = 'and';
// Sort the terms so the filter will be displayed as it normally would
// on the edit screen.
sort($filters['tid']['value']);
}
}
return $filters;
}
/**
* {@inheritdoc}
*/
protected function pageDisplayOptions(array $form, FormStateInterface $form_state) {
$display_options = parent::pageDisplayOptions($form, $form_state);
$row_plugin = $form_state->getValue(['page', 'style', 'row_plugin']);
$row_options = $form_state->getValue(['page', 'style', 'row_options'], []);
$this->display_options_row($display_options, $row_plugin, $row_options);
return $display_options;
}
/**
* {@inheritdoc}
*/
protected function blockDisplayOptions(array $form, FormStateInterface $form_state) {
$display_options = parent::blockDisplayOptions($form, $form_state);
$row_plugin = $form_state->getValue(['block', 'style', 'row_plugin']);
$row_options = $form_state->getValue(['block', 'style', 'row_options'], []);
$this->display_options_row($display_options, $row_plugin, $row_options);
return $display_options;
}
/**
* Set the row style and row style plugins to the display_options.
*/
protected function display_options_row(&$display_options, $row_plugin, $row_options) {
switch ($row_plugin) {
case 'full_posts':
$display_options['row']['type'] = 'entity:node';
$display_options['row']['options']['view_mode'] = 'full';
break;
case 'teasers':
$display_options['row']['type'] = 'entity:node';
$display_options['row']['options']['view_mode'] = 'teaser';
break;
case 'titles_linked':
case 'titles':
$display_options['row']['type'] = 'fields';
$display_options['fields']['title']['id'] = 'title';
$display_options['fields']['title']['table'] = 'node_field_data';
$display_options['fields']['title']['field'] = 'title';
$display_options['fields']['title']['settings']['link_to_entity'] = $row_plugin === 'titles_linked';
$display_options['fields']['title']['plugin_id'] = 'field';
break;
}
}
/**
* Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::buildFilters().
*
* Add some options for filter by taxonomy terms.
*/
protected function buildFilters(&$form, FormStateInterface $form_state) {
parent::buildFilters($form, $form_state);
if (isset($form['displays']['show']['type'])) {
$selected_bundle = static::getSelected($form_state, ['show', 'type'], 'all', $form['displays']['show']['type']);
}
// Add the "tagged with" filter to the view.
// We construct this filter using taxonomy_index.tid (which limits the
// filtering to a specific vocabulary) rather than
// taxonomy_term_field_data.name (which matches terms in any vocabulary).
// This is because it is a more commonly-used filter that works better with
// the autocomplete UI, and also to avoid confusion with other vocabularies
// on the site that may have terms with the same name but are not used for
// free tagging.
// The downside is that if there *is* more than one vocabulary on the site
// that is used for free tagging, the wizard will only be able to make the
// "tagged with" filter apply to one of them (see below for the method it
// uses to choose).
// Find all "tag-like" taxonomy fields associated with the view's
// entities. If a particular entity type (i.e., bundle) has been
// selected above, then we only search for taxonomy fields associated
// with that bundle. Otherwise, we use all bundles.
$bundles = array_keys($this->bundleInfoService->getBundleInfo($this->entityTypeId));
// Double check that this is a real bundle before using it (since above
// we added a dummy option 'all' to the bundle list on the form).
if (isset($selected_bundle) && in_array($selected_bundle, $bundles)) {
$bundles = [$selected_bundle];
}
$tag_fields = [];
foreach ($bundles as $bundle) {
$display = $this->entityDisplayRepository->getFormDisplay($this->entityTypeId, $bundle);
$taxonomy_fields = array_filter($this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $bundle), function (FieldDefinitionInterface $field_definition) {
return $field_definition->getType() == 'entity_reference' && $field_definition->getSetting('target_type') == 'taxonomy_term';
});
foreach ($taxonomy_fields as $field_name => $field) {
$widget = $display->getComponent($field_name);
// We define "tag-like" taxonomy fields as ones that use the
// "Autocomplete (Tags style)" widget.
if ($widget['type'] == 'entity_reference_autocomplete_tags') {
$tag_fields[$field_name] = $field;
}
}
}
if (!empty($tag_fields)) {
// If there is more than one "tag-like" taxonomy field available to
// the view, we can only make our filter apply to one of them (as
// described above). We choose 'field_tags' if it is available, since
// that is created by the Standard install profile in core and also
// commonly used by contrib modules; thus, it is most likely to be
// associated with the "main" free-tagging vocabulary on the site.
if (array_key_exists('field_tags', $tag_fields)) {
$tag_field_name = 'field_tags';
}
else {
$tag_field_name = key($tag_fields);
}
// Add the autocomplete textfield to the wizard.
$target_bundles = $tag_fields[$tag_field_name]->getSetting('handler_settings')['target_bundles'];
$form['displays']['show']['tagged_with'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('tagged with'),
'#target_type' => 'taxonomy_term',
'#selection_settings' => ['target_bundles' => $target_bundles],
'#tags' => TRUE,
'#size' => 30,
'#maxlength' => 1024,
];
}
}
}
@@ -0,0 +1,143 @@
<?php
namespace Drupal\node\Plugin\views\wizard;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\wizard\WizardPluginBase;
/**
* @todo: replace numbers with constants.
*/
/**
* Tests creating node revision views with the wizard.
*
* @ViewsWizard(
* id = "node_revision",
* base_table = "node_field_revision",
* title = @Translation("Content revisions")
* )
*/
class NodeRevision extends WizardPluginBase {
/**
* Set the created column.
*
* @var string
*/
protected $createdColumn = 'changed';
/**
* Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::rowStyleOptions().
*
* Node revisions do not support full posts or teasers, so remove them.
*/
protected function rowStyleOptions() {
$options = parent::rowStyleOptions();
unset($options['teasers']);
unset($options['full_posts']);
return $options;
}
/**
* {@inheritdoc}
*/
protected function defaultDisplayOptions() {
$display_options = parent::defaultDisplayOptions();
// Add permission-based access control.
$display_options['access']['type'] = 'perm';
$display_options['access']['options']['perm'] = 'view all revisions';
// Remove the default fields, since we are customizing them here.
unset($display_options['fields']);
/* Field: Content revision: Created date */
$display_options['fields']['changed']['id'] = 'changed';
$display_options['fields']['changed']['table'] = 'node_field_revision';
$display_options['fields']['changed']['field'] = 'changed';
$display_options['fields']['changed']['entity_type'] = 'node';
$display_options['fields']['changed']['entity_field'] = 'changed';
$display_options['fields']['changed']['alter']['alter_text'] = FALSE;
$display_options['fields']['changed']['alter']['make_link'] = FALSE;
$display_options['fields']['changed']['alter']['absolute'] = FALSE;
$display_options['fields']['changed']['alter']['trim'] = FALSE;
$display_options['fields']['changed']['alter']['word_boundary'] = FALSE;
$display_options['fields']['changed']['alter']['ellipsis'] = FALSE;
$display_options['fields']['changed']['alter']['strip_tags'] = FALSE;
$display_options['fields']['changed']['alter']['html'] = FALSE;
$display_options['fields']['changed']['hide_empty'] = FALSE;
$display_options['fields']['changed']['empty_zero'] = FALSE;
$display_options['fields']['changed']['plugin_id'] = 'field';
$display_options['fields']['changed']['type'] = 'timestamp';
$display_options['fields']['changed']['settings']['date_format'] = 'medium';
$display_options['fields']['changed']['settings']['custom_date_format'] = '';
$display_options['fields']['changed']['settings']['timezone'] = '';
/* Field: Content revision: Title */
$display_options['fields']['title']['id'] = 'title';
$display_options['fields']['title']['table'] = 'node_field_revision';
$display_options['fields']['title']['field'] = 'title';
$display_options['fields']['title']['entity_type'] = 'node';
$display_options['fields']['title']['entity_field'] = 'title';
$display_options['fields']['title']['label'] = '';
$display_options['fields']['title']['alter']['alter_text'] = 0;
$display_options['fields']['title']['alter']['make_link'] = 0;
$display_options['fields']['title']['alter']['absolute'] = 0;
$display_options['fields']['title']['alter']['trim'] = 0;
$display_options['fields']['title']['alter']['word_boundary'] = 0;
$display_options['fields']['title']['alter']['ellipsis'] = 0;
$display_options['fields']['title']['alter']['strip_tags'] = 0;
$display_options['fields']['title']['alter']['html'] = 0;
$display_options['fields']['title']['hide_empty'] = 0;
$display_options['fields']['title']['empty_zero'] = 0;
$display_options['fields']['title']['settings']['link_to_entity'] = 0;
$display_options['fields']['title']['plugin_id'] = 'field';
return $display_options;
}
/**
* {@inheritdoc}
*/
protected function defaultDisplayFiltersUser(array $form, FormStateInterface $form_state) {
$filters = [];
$type = $form_state->getValue(['show', 'type']);
if ($type && $type != 'all') {
$filters['type'] = [
'id' => 'type',
'table' => 'node_field_data',
'field' => 'type',
'relationship' => 'nid',
'value' => [$type => $type],
'entity_type' => 'node',
'entity_field' => 'type',
'plugin_id' => 'bundle',
];
}
return $filters;
}
/**
* {@inheritdoc}
*/
protected function buildDisplayOptions($form, FormStateInterface $form_state) {
$display_options = parent::buildDisplayOptions($form, $form_state);
if (isset($display_options['default']['filters']['type'])) {
$display_options['default']['relationships']['nid'] = [
'id' => 'nid',
'table' => 'node_field_revision',
'field' => 'nid',
'relationship' => 'none',
'group_type' => 'group',
'admin_label' => 'Get the actual content from a content revision.',
'required' => 'true',
'entity_type' => 'node',
'entity_field' => 'nid',
'plugin_id' => 'standard',
];
}
return $display_options;
}
}
@@ -0,0 +1,88 @@
<?php
// @codingStandardsIgnoreFile
/**
* This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\node\ParamConverter\NodePreviewConverter' "core/modules/node/src".
*/
namespace Drupal\node\ProxyClass\ParamConverter {
/**
* Provides a proxy class for \Drupal\node\ParamConverter\NodePreviewConverter.
*
* @see \Drupal\Component\ProxyBuilder
*/
class NodePreviewConverter implements \Drupal\Core\ParamConverter\ParamConverterInterface
{
use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
* The id of the original proxied service.
*
* @var string
*/
protected $drupalProxyOriginalServiceId;
/**
* The real proxied service, after it was lazy loaded.
*
* @var \Drupal\node\ParamConverter\NodePreviewConverter
*/
protected $service;
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* Constructs a ProxyClass Drupal proxy object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
* @param string $drupal_proxy_original_service_id
* The service ID of the original service.
*/
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
{
$this->container = $container;
$this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
}
/**
* Lazy loads the real service from the container.
*
* @return object
* Returns the constructed real service.
*/
protected function lazyLoadItself()
{
if (!isset($this->service)) {
$this->service = $this->container->get($this->drupalProxyOriginalServiceId);
}
return $this->service;
}
/**
* {@inheritdoc}
*/
public function convert($value, $definition, $name, array $defaults)
{
return $this->lazyLoadItself()->convert($value, $definition, $name, $defaults);
}
/**
* {@inheritdoc}
*/
public function applies($definition, $name, \Symfony\Component\Routing\Route $route)
{
return $this->lazyLoadItself()->applies($definition, $name, $route);
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace Drupal\node\Routing;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
/**
* Listens to the dynamic route events.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
// As nodes are the primary type of content, the node listing should be
// easily available. In order to do that, override admin/content to show
// a node listing instead of the path's child links.
$route = $collection->get('system.admin_content');
if ($route) {
$route->setDefaults([
'_title' => 'Content',
'_entity_list' => 'node',
]);
$route->setRequirements([
'_permission' => 'access content overview',
]);
}
}
}
@@ -0,0 +1,53 @@
<?php
namespace Drupal\node\Tests;
@trigger_error('\Drupal\Tests\node\Functional\AssertButtonsTrait is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Instead, use \Drupal\Tests\node\Functional\AssertButtonsTrait', E_USER_DEPRECATED);
/**
* Asserts that buttons are present on a page.
*
* @deprecated in drupal:8.?.? and is removed from drupal:9.0.0.
* Use \Drupal\Tests\node\Functional\AssertButtonsTrait instead.
*/
trait AssertButtonsTrait {
/**
* Assert method to verify the buttons in the dropdown element.
*
* @param array $buttons
* A collection of buttons to assert for on the page.
* @param bool $dropbutton
* Whether to check if the buttons are in a dropbutton widget or not.
*/
public function assertButtons($buttons, $dropbutton = TRUE) {
// Try to find a Save button.
$save_button = $this->xpath('//input[@type="submit"][@value="Save"]');
// Verify that the number of buttons passed as parameters is
// available in the dropbutton widget.
if ($dropbutton) {
$i = 0;
$count = count($buttons);
// Assert there is no save button.
$this->assertTrue(empty($save_button));
// Dropbutton elements.
$elements = $this->xpath('//div[@class="dropbutton-wrapper"]//input[@type="submit"]');
$this->assertEqual($count, count($elements));
foreach ($elements as $element) {
$value = isset($element['value']) ? (string) $element['value'] : '';
$this->assertEqual($buttons[$i], $value);
$i++;
}
}
else {
// Assert there is a save button.
$this->assertTrue(!empty($save_button));
$this->assertNoRaw('dropbutton-wrapper');
}
}
}
@@ -0,0 +1,118 @@
<?php
namespace Drupal\node\Tests;
@trigger_error(__NAMESPACE__ . '\NodeTestBase is deprecated for removal before Drupal 9.0.0. Use Drupal\Tests\node\Functional\NodeTestBase instead. See https://www.drupal.org/node/2999939', E_USER_DEPRECATED);
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
use Drupal\simpletest\WebTestBase;
/**
* Sets up page and article content types.
*
* @deprecated in drupal:8.?.? and is removed from drupal:9.0.0.
* Use \Drupal\Tests\node\Functional\NodeTestBase instead.
*
* @see https://www.drupal.org/node/2999939
*/
abstract class NodeTestBase extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'datetime'];
/**
* The node access control handler.
*
* @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
*/
protected $accessHandler;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create Basic page and Article node types.
if ($this->profile != 'standard') {
$this->drupalCreateContentType([
'type' => 'page',
'name' => 'Basic page',
'display_submitted' => FALSE,
]);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
}
$this->accessHandler = \Drupal::entityTypeManager()->getAccessControlHandler('node');
}
/**
* Asserts that node access correctly grants or denies access.
*
* @param array $ops
* An associative array of the expected node access grants for the node
* and account, with each key as the name of an operation (e.g. 'view',
* 'delete') and each value a Boolean indicating whether access to that
* operation should be granted.
* @param \Drupal\node\NodeInterface $node
* The node object to check.
* @param \Drupal\Core\Session\AccountInterface $account
* The user account for which to check access.
*/
public function assertNodeAccess(array $ops, NodeInterface $node, AccountInterface $account) {
foreach ($ops as $op => $result) {
$this->assertEqual($result, $this->accessHandler->access($node, $op, $account), $this->nodeAccessAssertMessage($op, $result, $node->language()->getId()));
}
}
/**
* Asserts that node create access correctly grants or denies access.
*
* @param string $bundle
* The node bundle to check access to.
* @param bool $result
* Whether access should be granted or not.
* @param \Drupal\Core\Session\AccountInterface $account
* The user account for which to check access.
* @param string|null $langcode
* (optional) The language code indicating which translation of the node
* to check. If NULL, the untranslated (fallback) access is checked.
*/
public function assertNodeCreateAccess($bundle, $result, AccountInterface $account, $langcode = NULL) {
$this->assertEqual($result, $this->accessHandler->createAccess($bundle, $account, [
'langcode' => $langcode,
]), $this->nodeAccessAssertMessage('create', $result, $langcode));
}
/**
* Constructs an assert message to display which node access was tested.
*
* @param string $operation
* The operation to check access for.
* @param bool $result
* Whether access should be granted or not.
* @param string|null $langcode
* (optional) The language code indicating which translation of the node
* to check. If NULL, the untranslated (fallback) access is checked.
*
* @return string
* An assert message string which contains information in plain English
* about the node access permission test that was performed.
*/
public function nodeAccessAssertMessage($operation, $result, $langcode = NULL) {
return new FormattableMarkup(
'Node access returns @result with operation %op, language code %langcode.',
[
'@result' => $result ? 'true' : 'false',
'%op' => $operation,
'%langcode' => !empty($langcode) ? $langcode : 'empty',
]
);
}
}
@@ -0,0 +1,33 @@
<?php
namespace Drupal\node\Tests\Views;
@trigger_error('\Drupal\node\Tests\Views\NodeTestBase is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Instead, use \Drupal\Tests\node\Functional\Views\NodeTestBase', E_USER_DEPRECATED);
use Drupal\views\Tests\ViewTestBase;
use Drupal\views\Tests\ViewTestData;
/**
* Base class for all node tests.
*
* @deprecated in drupal:8.?.? and is removed from drupal:9.0.0.
* Use \Drupal\Tests\node\Functional\Views\NodeTestBase instead.
*/
abstract class NodeTestBase extends ViewTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node_test_views'];
protected function setUp($import_test_views = TRUE) {
parent::setUp($import_test_views);
if ($import_test_views) {
ViewTestData::createTestViews(get_class($this), ['node_test_views']);
}
}
}