Update to Drupal 8.0-dev-2015-11-17. Commits through da81cd220, Tue Nov 17 15:53:49 2015 +0000, Issue #2617224 by Wim Leers: Move around/fix some documentation.

This commit is contained in:
Pantheon Automation 2015-11-17 13:42:33 -08:00 committed by Greg Anderson
parent 4afb23bbd3
commit 7784f4c23d
929 changed files with 19798 additions and 5304 deletions

View file

@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains \Drupal\aggregator\Tests\Migrate\MigrateAggregatorStubTest.
*/
namespace Drupal\aggregator\Tests\Migrate;
use Drupal\migrate\MigrateException;
use Drupal\migrate_drupal\Tests\MigrateDrupalTestBase;
use Drupal\migrate_drupal\Tests\StubTestTrait;
/**
* Test stub creation for aggregator feeds and items.
*
* @group aggregator
*/
class MigrateAggregatorStubTest extends MigrateDrupalTestBase {
use StubTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['aggregator'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('aggregator_feed');
$this->installEntitySchema('aggregator_item');
}
/**
* Tests creation of aggregator feed stubs.
*/
public function testFeedStub() {
$this->performStubTest('aggregator_feed');
}
/**
* Tests creation of aggregator feed items.
*/
public function testItemStub() {
try {
// We expect an exception, because there's no feed to reference.
$this->performStubTest('aggregator_item');
$this->fail('Expected exception has not been thrown.');
}
catch (MigrateException $e) {
$this->assertIdentical($e->getMessage(),
'Stubbing failed, unable to generate value for field fid');
}
// The stub should pass when there's a feed to point to.
$this->createStub('aggregator_feed');
$this->performStubTest('aggregator_item');
}
}

View file

@ -89,7 +89,10 @@ function block_page_top(array &$page_top) {
*/
function block_themes_installed($theme_list) {
foreach ($theme_list as $theme) {
block_theme_initialize($theme);
// Don't initialize themes that are not displayed in the UI.
if (\Drupal::service('theme_handler')->hasUi($theme)) {
block_theme_initialize($theme);
}
}
}

View file

@ -11,6 +11,7 @@ use Drupal\Component\Utility\Html;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Controller routines for admin block routes.
@ -53,6 +54,10 @@ class BlockController extends ControllerBase {
* A #type 'page' render array containing the block region demo.
*/
public function demo($theme) {
if (!$this->themeHandler->hasUi($theme)) {
throw new NotFoundHttpException();
}
$page = [
'#title' => Html::escape($this->themeHandler->getName($theme)),
'#type' => 'page',

View file

@ -8,13 +8,42 @@
namespace Drupal\block\Controller;
use Drupal\Core\Entity\Controller\EntityListController;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Defines a controller to list blocks.
*/
class BlockListController extends EntityListController {
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* Constructs the BlockListController.
*
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(ThemeHandlerInterface $theme_handler) {
$this->themeHandler = $theme_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('theme_handler')
);
}
/**
* Shows the block administration page.
*
@ -28,6 +57,10 @@ class BlockListController extends EntityListController {
*/
public function listing($theme = NULL, Request $request = NULL) {
$theme = $theme ?: $this->config('system.theme')->get('default');
if (!$this->themeHandler->hasUi($theme)) {
throw new NotFoundHttpException();
}
return $this->entityManager()->getListBuilder('block')->render($theme, $request);
}

View file

@ -50,7 +50,7 @@ class ThemeLocalTask extends DeriverBase implements ContainerDeriverInterface {
$default_theme = $this->themeHandler->getDefault();
foreach ($this->themeHandler->listInfo() as $theme_name => $theme) {
if ($theme->status) {
if ($this->themeHandler->hasUi($theme_name)) {
$this->derivatives[$theme_name] = $base_plugin_definition;
$this->derivatives[$theme_name]['title'] = $theme->info['name'];
$this->derivatives[$theme_name]['route_parameters'] = array('theme' => $theme_name);

View file

@ -56,7 +56,9 @@ class BlockHiddenRegionTest extends WebTestBase {
// Install "block_test_theme" and set it as the default theme.
$theme = 'block_test_theme';
\Drupal::service('theme_handler')->install(array($theme));
// We need to install a non-hidden theme so that there is more than one
// local task.
\Drupal::service('theme_handler')->install(array($theme, 'stark'));
$this->config('system.theme')
->set('default', $theme)
->save();

View file

@ -197,9 +197,9 @@ class BlockTest extends BlockTestBase {
*/
public function testBlockThemeSelector() {
// Install all themes.
\Drupal::service('theme_handler')->install(array('bartik', 'seven'));
\Drupal::service('theme_handler')->install(['bartik', 'seven', 'stark']);
$theme_settings = $this->config('system.theme');
foreach (array('bartik', 'classy', 'seven') as $theme) {
foreach (['bartik', 'seven', 'stark'] as $theme) {
$this->drupalGet('admin/structure/block/list/' . $theme);
$this->assertTitle(t('Block layout') . ' | Drupal');
// Select the 'Powered by Drupal' block to be placed.

View file

@ -90,6 +90,10 @@ class BlockUiTest extends WebTestBase {
\Drupal::service('theme_handler')->install(array('test_theme'));
$this->drupalGet('admin/structure/block/demo/test_theme');
$this->assertEscaped('<strong>Test theme</strong>');
\Drupal::service('theme_handler')->install(['stable']);
$this->drupalGet('admin/structure/block/demo/stable');
$this->assertResponse(404, 'Hidden themes that are not the default theme are not supported by the block demo screen');
}
/**
@ -136,6 +140,28 @@ class BlockUiTest extends WebTestBase {
$this->drupalGet('admin/structure/block');
$element = $this->xpath('//tr[contains(@class, :class)]', [':class' => 'region-title-header']);
$this->assertTrue(!empty($element));
// Ensure hidden themes do not appear in the UI. Enable another non base
// theme and place the local tasks block.
$this->assertTrue(\Drupal::service('theme_handler')->themeExists('classy'), 'The classy base theme is enabled');
$this->drupalPlaceBlock('local_tasks_block', ['region' => 'header']);
\Drupal::service('theme_installer')->install(['stable', 'stark']);
$this->drupalGet('admin/structure/block');
$theme_handler = \Drupal::service('theme_handler');
$this->assertLink($theme_handler->getName('classy'));
$this->assertLink($theme_handler->getName('stark'));
$this->assertNoLink($theme_handler->getName('stable'));
$this->drupalGet('admin/structure/block/list/stable');
$this->assertResponse(404, 'Placing blocks through UI is not possible for a hidden base theme.');
\Drupal::configFactory()->getEditable('system.theme')->set('admin', 'stable')->save();
\Drupal::service('router.builder')->rebuildIfNeeded();
$this->drupalPlaceBlock('local_tasks_block', ['region' => 'header', 'theme' => 'stable']);
$this->drupalGet('admin/structure/block');
$this->assertLink($theme_handler->getName('stable'));
$this->drupalGet('admin/structure/block/list/stable');
$this->assertResponse(200, 'Placing blocks through UI is possible for a hidden base theme that is the admin theme.');
}
/**

View file

@ -65,6 +65,14 @@ class NewDefaultThemeBlocksTest extends WebTestBase {
unset($new_blocks[str_replace($default_theme . '_', $new_theme . '_', $default_block_name)]);
}
$this->assertTrue(empty($new_blocks), 'The new theme has exactly the same blocks as the previous default theme.');
// Install a hidden base theme and ensure blocks are not copied.
$base_theme = 'test_basetheme';
\Drupal::service('theme_handler')->install([$base_theme]);
$new_blocks = $this->container->get('entity.query')->get('block')
->condition('theme', $base_theme)
->execute();
$this->assertTrue(empty($new_blocks), 'Installing a hidden base theme does not copy blocks from the default theme.');
}
}

View file

@ -27,7 +27,11 @@ class BlockLocalTasksTest extends LocalTaskIntegrationTestBase {
$themes = array();
$themes['test_a'] = (object) array(
'status' => 0,
'status' => 1,
'info' => array(
'name' => 'test_a',
'hidden' => TRUE,
),
);
$themes['test_b'] = (object) array(
'status' => 1,
@ -45,6 +49,13 @@ class BlockLocalTasksTest extends LocalTaskIntegrationTestBase {
$theme_handler->expects($this->any())
->method('listInfo')
->will($this->returnValue($themes));
$theme_handler->expects($this->any())
->method('hasUi')
->willReturnMap([
['test_a', FALSE],
['test_b', TRUE],
['test_c', TRUE],
]);
$container = new ContainerBuilder();
$container->set('config.factory', $config_factory);

View file

@ -44,6 +44,7 @@ entity.block_content.canonical:
_admin_route: TRUE
requirements:
_entity_access: 'block_content.update'
block_content: \d+
entity.block_content.edit_form:
path: '/block/{block_content}'
@ -53,6 +54,7 @@ entity.block_content.edit_form:
_admin_route: TRUE
requirements:
_entity_access: 'block_content.update'
block_content: \d+
entity.block_content.delete_form:
path: '/block/{block_content}/delete'
@ -63,6 +65,7 @@ entity.block_content.delete_form:
_admin_route: TRUE
requirements:
_entity_access: 'block_content.delete'
block_content: \d+
block_content.type_add:
path: '/admin/structure/block/block-content/types/add'

View file

@ -185,17 +185,15 @@ class BlockContentTypeTest extends BlockContentTestBase {
->getStorage('block_content');
// Install all themes.
\Drupal::service('theme_handler')->install(array('bartik', 'seven'));
$themes = array('bartik', 'seven', 'classy');
\Drupal::service('theme_handler')->install(['bartik', 'seven', 'stark']);
$theme_settings = $this->config('system.theme');
foreach ($themes as $default_theme) {
foreach (['bartik', 'seven', 'stark'] as $default_theme) {
// Change the default theme.
$theme_settings->set('default', $default_theme)->save();
\Drupal::service('router.builder')->rebuild();
// For each installed theme, go to its block page and test the redirects.
$themes = array('bartik', 'classy', 'seven');
foreach ($themes as $theme) {
foreach (['bartik', 'seven', 'stark'] as $theme) {
// Test that adding a block from the 'place blocks' form sends you to the
// block configure form.
$path = $theme == $default_theme ? 'admin/structure/block' : "admin/structure/block/list/$theme";

View file

@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains \Drupal\block_content\Tests\Migrate\MigrateBlockContentStubTest.
*/
namespace Drupal\block_content\Tests\Migrate;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\migrate\MigrateException;
use Drupal\migrate_drupal\Tests\MigrateDrupalTestBase;
use Drupal\migrate_drupal\Tests\StubTestTrait;
/**
* Test stub creation for block_content entities.
*
* @group block_content
*/
class MigrateBlockContentStubTest extends MigrateDrupalTestBase {
use StubTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['block_content'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('block_content');
}
/**
* Tests creation of block content stubs with no block_content_type available.
*/
public function testStubFailure() {
$message = 'Expected MigrateException thrown when no bundles exist.';
try {
$this->createStub('block_content');
$this->fail($message);
}
catch (MigrateException $e) {
$this->pass($message);
$this->assertEqual('Stubbing failed, no bundles available for entity type: block_content', $e->getMessage());
}
}
/**
* Tests creation of block content stubs when there is a block_content_type.
*/
public function testStubSuccess() {
BlockContentType::create([
'id' => 'test_block_content_type',
'label' => 'Test block content type',
])->save();
$this->performStubTest('block_content');
}
}

View file

@ -29,6 +29,7 @@ book.export:
requirements:
_permission: 'access printer-friendly version'
_entity_access: 'node.view'
node: \d+
entity.node.book_outline_form:
path: '/node/{node}/outline'
@ -38,6 +39,7 @@ entity.node.book_outline_form:
requirements:
_permission: 'administer book outlines'
_entity_access: 'node.view'
node: \d+
options:
_node_operation_route: TRUE
@ -62,3 +64,4 @@ entity.node.book_remove_form:
_permission: 'administer book outlines'
_entity_access: 'node.view'
_access_book_removable: 'TRUE'
node: \d+

View file

@ -9,6 +9,27 @@
'use strict';
function parseAttributes(element) {
var parsedAttributes = {};
var domElement = element.$;
var attribute = null;
var attributeName;
for (var attrIndex = 0; attrIndex < domElement.attributes.length; attrIndex++) {
attribute = domElement.attributes.item(attrIndex);
attributeName = attribute.nodeName.toLowerCase();
// Don't consider data-cke-saved- attributes; they're just there to work
// around browser quirks.
if (attributeName.substring(0, 15) === 'data-cke-saved-') {
continue;
}
// Store the value for this attribute, unless there's a data-cke-saved-
// alternative for it, which will contain the quirk-free, original value.
parsedAttributes[attributeName] = element.data('cke-saved-' + attributeName) || attribute.nodeValue;
}
return parsedAttributes;
}
CKEDITOR.plugins.add('drupallink', {
init: function (editor) {
// Add the commands for link and unlink.
@ -16,8 +37,7 @@
allowedContent: {
a: {
attributes: {
'!href': true,
'target': true
'!href': true
},
classes: {}
}
@ -34,35 +54,16 @@
var drupalImageUtils = CKEDITOR.plugins.drupalimage;
var focusedImageWidget = drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
var linkElement = getSelectedLink(editor);
var linkDOMElement = null;
// Set existing values based on selected element.
var existingValues = {};
if (linkElement && linkElement.$) {
linkDOMElement = linkElement.$;
// Populate an array with the link's current attributes.
var attribute = null;
var attributeName;
for (var attrIndex = 0; attrIndex < linkDOMElement.attributes.length; attrIndex++) {
attribute = linkDOMElement.attributes.item(attrIndex);
attributeName = attribute.nodeName.toLowerCase();
// Don't consider data-cke-saved- attributes; they're just there
// to work around browser quirks.
if (attributeName.substring(0, 15) === 'data-cke-saved-') {
continue;
}
// Store the value for this attribute, unless there's a
// data-cke-saved- alternative for it, which will contain the
// quirk-free, original value.
existingValues[attributeName] = linkElement.data('cke-saved-' + attributeName) || attribute.nodeValue;
}
existingValues = parseAttributes(linkElement);
}
// Or, if an image widget is focused, we're editing a link wrapping
// an image widget.
else if (focusedImageWidget && focusedImageWidget.data.link) {
var url = focusedImageWidget.data.link.url;
existingValues.href = url.protocol + url.url;
existingValues = CKEDITOR.tools.clone(focusedImageWidget.data.link);
}
// Prepare a save callback to be used upon saving the dialog.
@ -70,14 +71,7 @@
// If an image widget is focused, we're not editing an independent
// link, but we're wrapping an image widget in a link.
if (focusedImageWidget) {
var urlMatch = returnValues.attributes.href.match(urlRegex);
focusedImageWidget.setData('link', {
type: 'url',
url: {
protocol: urlMatch[1],
url: urlMatch[2]
}
});
focusedImageWidget.setData('link', CKEDITOR.tools.extend(returnValues.attributes, focusedImageWidget.data.link));
editor.fire('saveSnapshot');
return;
}
@ -97,11 +91,6 @@
range.selectNodeContents(text);
}
// Ignore a disabled target attribute.
if (returnValues.attributes.target === 0) {
delete returnValues.attributes.target;
}
// Create the new link by applying a style to the new text.
var style = new CKEDITOR.style({element: 'a', attributes: returnValues.attributes});
style.type = CKEDITOR.STYLE_INLINE;
@ -150,8 +139,7 @@
allowedContent: {
a: {
attributes: {
'!href': true,
'target': true
'!href': true
}
}
},
@ -280,8 +268,6 @@
return null;
}
var urlRegex = /^((?:http|https):\/\/)?(.*)$/;
/**
* The image2 plugin is currently tightly coupled to the link plugin: it
* calls CKEDITOR.plugins.link.parseLinkAttributes().
@ -296,28 +282,20 @@
*/
CKEDITOR.plugins.link = CKEDITOR.plugins.link || {
parseLinkAttributes: function (editor, element) {
var href = (element && (element.data('cke-saved-href') || element.getAttribute('href'))) || '';
var urlMatch = href.match(urlRegex);
return {
type: 'url',
url: {
protocol: urlMatch[1],
url: urlMatch[2]
}
};
return parseAttributes(element);
},
getLinkAttributes: function (editor, data) {
var set = {};
var protocol = (data.url && typeof data.url.protocol !== 'undefined') ? data.url.protocol : 'http://';
var url = (data.url && CKEDITOR.tools.trim(data.url.url)) || '';
set['data-cke-saved-href'] = (url.indexOf('/') === 0) ? url : protocol + url;
// Browser need the "href" fro copy/paste link to work. (#6641)
if (set['data-cke-saved-href']) {
set.href = set['data-cke-saved-href'];
for (var attributeName in data) {
if (data.hasOwnProperty(attributeName)) {
set[attributeName] = data[attributeName];
}
}
// CKEditor tracks the *actual* saved href in a data-cke-saved-* attribute
// to work around browser quirks. We need to update it.
set['data-cke-saved-href'] = set.href;
// Remove all attributes which are not currently set.
var removed = {};
for (var s in set) {

View file

@ -23,6 +23,7 @@ entity.comment.edit_form:
_entity_form: 'comment.default'
requirements:
_entity_access: 'comment.update'
comment: \d+
comment.approve:
path: '/comment/{comment}/approve'
@ -33,6 +34,7 @@ comment.approve:
requirements:
_entity_access: 'comment.approve'
_csrf_token: 'TRUE'
comment: \d+
entity.comment.canonical:
path: '/comment/{comment}'
@ -41,6 +43,7 @@ entity.comment.canonical:
_controller: '\Drupal\comment\Controller\CommentController::commentPermalink'
requirements:
_entity_access: 'comment.view'
comment: \d+
entity.comment.delete_form:
path: '/comment/{comment}/delete'
@ -49,6 +52,7 @@ entity.comment.delete_form:
_entity_form: 'comment.delete'
requirements:
_entity_access: 'comment.delete'
comment: \d+
comment.reply:
path: '/comment/reply/{entity_type}/{entity}/{field_name}/{pid}'
@ -77,6 +81,7 @@ comment.node_redirect:
requirements:
_entity_access: 'node.view'
_module_dependencies: 'node'
node: \d+
entity.comment_type.collection:
path: '/admin/structure/comment'

View file

@ -39,6 +39,34 @@ class CommentSelection extends DefaultSelection {
return $query;
}
/**
* {@inheritdoc}
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$comment = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
// In order to create a referenceable comment, it needs to published.
/** @var \Drupal\comment\CommentInterface $comment */
$comment->setPublished(TRUE);
return $comment;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableNewEntities(array $entities) {
$entities = parent::validateReferenceableNewEntities($entities);
// Mirror the conditions checked in buildEntityQuery().
if (!$this->currentUser->hasPermission('administer comments')) {
$entities = array_filter($entities, function ($comment) {
/** @var \Drupal\comment\CommentInterface $comment */
return $comment->isPublished();
});
}
return $entities;
}
/**
* {@inheritdoc}
*/

View file

@ -10,6 +10,7 @@ namespace Drupal\comment\Plugin\migrate\destination;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\MigrateException;
@ -62,13 +63,15 @@ class EntityComment extends EntityContentBase {
* The list of bundles this entity type has.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager service.
* @param \Drupal\Core\State\StateInterface $state
* The state storage object.
* @param \Drupal\Core\Entity\Query\QueryFactory $entity_query
* The query object that can query the given entity type.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, StateInterface $state, QueryFactory $entity_query) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager);
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager, StateInterface $state, QueryFactory $entity_query) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager, $field_type_manager);
$this->state = $state;
$this->entityQuery = $entity_query;
}
@ -86,6 +89,7 @@ class EntityComment extends EntityContentBase {
$container->get('entity.manager')->getStorage($entity_type),
array_keys($container->get('entity.manager')->getBundleInfo($entity_type)),
$container->get('entity.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('state'),
$container->get('entity.query')
);
@ -110,32 +114,9 @@ class EntityComment extends EntityContentBase {
*/
protected function processStubRow(Row $row) {
parent::processStubRow($row);
$stub_commented_entity_type = $row->getDestinationProperty('entity_type');
// While parent::getEntity() fills the bundle property for stub entities
// if it's still empty, here we must also make sure entity_id/entity_type
// are filled (so $comment->getCommentedEntity() always returns a value).
if (empty($this->stubCommentedEntityIds[$stub_commented_entity_type])) {
// Fill stub entity id. Any id will do, as long as it exists.
$entity_type = $this->entityManager->getDefinition($stub_commented_entity_type);
$id_key = $entity_type->getKey('id');
$result = $this->entityQuery
->get($stub_commented_entity_type)
->range(0, 1)
->execute();
if ($result) {
$this->stubCommentedEntityIds[$stub_commented_entity_type] = array_pop($result);
$row->setSourceProperty($id_key, $this->stubCommentedEntityIds[$stub_commented_entity_type]);
}
else {
throw new MigrateException(t('Could not find parent entity to use for comment %id', ['%id' => implode(':', $row->getSourceIdValues())]), MigrationInterface::MESSAGE_ERROR);
}
}
$row->setDestinationProperty('entity_id', $this->stubCommentedEntityIds[$stub_commented_entity_type]);
$row->setDestinationProperty('entity_type', $stub_commented_entity_type);
$row->setDestinationProperty('created', REQUEST_TIME);
$row->setDestinationProperty('changed', REQUEST_TIME);
// Neither uid nor name is required in itself, but it is required to set one
// of them.
$row->setDestinationProperty('name', 'anonymous_stub');
}
}

View file

@ -39,7 +39,7 @@ class CommentValidationTest extends EntityUnitTestBase {
*/
public function testValidation() {
// Add a user.
$user = User::create(array('name' => 'test'));
$user = User::create(array('name' => 'test', 'status' => TRUE));
$user->save();
// Add comment type.

View file

@ -0,0 +1,78 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\Migrate\MigrateCommentStubTest.
*/
namespace Drupal\comment\Tests\Migrate;
use Drupal\comment\Entity\CommentType;
use Drupal\migrate\MigrateException;
use Drupal\migrate_drupal\Tests\MigrateDrupalTestBase;
use Drupal\migrate_drupal\Tests\StubTestTrait;
use Drupal\node\Entity\NodeType;
/**
* Test stub creation for comment entities.
*
* @group comment
*/
class MigrateCommentStubTest extends MigrateDrupalTestBase {
use StubTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['comment', 'node'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('comment');
$this->installEntitySchema('node');
// Make sure uid 0 is created (default uid for comments is 0).
$storage = \Drupal::entityManager()->getStorage('user');
// Insert a row for the anonymous user.
$storage
->create(array(
'uid' => 0,
'status' => 0,
'name' => '',
))
->save();
// Need at least one node type and comment type present.
NodeType::create([
'type' => 'testnodetype',
'name' => 'Test node type',
])->save();
CommentType::create([
'id' => 'testcommenttype',
'label' => 'Test comment type',
'target_entity_type_id' => 'node',
])->save();
}
/**
* Tests creation of comment stubs.
*/
public function testStub() {
try {
// We expect an exception, because there's no node to reference.
$this->performStubTest('comment');
$this->fail('Expected exception has not been thrown.');
}
catch (MigrateException $e) {
$this->assertIdentical($e->getMessage(),
'Stubbing failed, unable to generate value for field entity_id');
}
// The stub should pass when there's a node to point to.
$this->createStub('node');
$this->performStubTest('comment');
}
}

View file

@ -22,13 +22,7 @@ class MigrateCommentTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'comment',
// Directly testing that a stub comment's entity_id is populated upon
// importing is not straightforward, but RDF module serves as an implicit
// test - its hook_comment_storage_load() references a stubbed comment.
'rdf',
];
public static $modules = ['comment'];
/**
* {@inheritdoc}

View file

@ -67,6 +67,7 @@ class ConfigImportInstallProfileTest extends WebTestBase {
$core['module']['testing_config_import'] = 0;
unset($core['module']['syslog']);
unset($core['theme']['stark']);
$core['theme']['stable'] = 0;
$core['theme']['classy'] = 0;
$sync->write('core.extension', $core);
$sync->deleteAll('syslog.');

View file

@ -92,12 +92,12 @@ class ConfigInstallProfileOverrideTest extends WebTestBase {
}
// Install the config_test module and ensure that the override from the
// install profile is not used. Optional configuration can not override
// install profile is used. Optional configuration can override
// configuration in a modules config/install directory.
$this->container->get('module_installer')->install(['config_test']);
$this->rebuildContainer();
$config_test_storage = \Drupal::entityManager()->getStorage('config_test');
$this->assertEqual($config_test_storage->load('dotted.default')->label(), 'Default', 'The config_test entity is not overridden by the profile optional configuration.');
$this->assertEqual($config_test_storage->load('dotted.default')->label(), 'Default install profile override', 'The config_test entity is overridden by the profile optional configuration.');
// Test that override of optional configuration does work.
$this->assertEqual($config_test_storage->load('override')->label(), 'Override', 'The optional config_test entity is overridden by the profile optional configuration.');
// Test that override of optional configuration which introduces an unmet

View file

@ -95,6 +95,7 @@ class ConfigInstallProfileUnmetDependenciesTest extends InstallerTestBase {
else {
$this->fail('Expected Drupal\Core\Config\UnmetDependenciesException exception thrown');
}
$this->assertErrorLogged('Configuration objects (system.action.user_block_user_action) provided by user have unmet dependencies in');
}
}

View file

@ -54,3 +54,4 @@ entity.user.contact_form:
_controller: '\Drupal\contact\Controller\ContactController::contactPersonalPage'
requirements:
_access_contact_personal_tab: 'TRUE'
user: \d+

View file

@ -29,3 +29,19 @@ function content_translation_enable() {
$message = t('<a href=":settings_url">Enable translation</a> for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>accounts</em>, or any other element you wish to translate.', $t_args);
drupal_set_message($message, 'warning');
}
/**
* @addtogroup updates-8.0.0-rc
* @{
*/
/**
* Rebuild the routes as the content translation routes have now new names.
*/
function content_translation_update_8001() {
\Drupal::service('router.builder')->rebuild();
}
/**
* @} End of "addtogroup updates-8.0.0-rc".
*/

View file

@ -53,7 +53,7 @@ function content_translation_help($route_name, RouteMatchInterface $route_match)
*/
function content_translation_module_implements_alter(&$implementations, $hook) {
switch ($hook) {
// Move some of our hook implementations to the end of the list.
// Move our hook_entity_type_alter() implementation to the end of the list.
case 'entity_type_alter':
$group = $implementations['content_translation'];
unset($implementations['content_translation']);
@ -140,7 +140,11 @@ function content_translation_entity_type_alter(array &$entity_types) {
if ($entity_type->hasLinkTemplate('canonical')) {
// Provide default route names for the translation paths.
if (!$entity_type->hasLinkTemplate('drupal:content-translation-overview')) {
$entity_type->setLinkTemplate('drupal:content-translation-overview', $entity_type->getLinkTemplate('canonical') . '/translations');
$translations_path = $entity_type->getLinkTemplate('canonical') . '/translations';
$entity_type->setLinkTemplate('drupal:content-translation-overview', $translations_path);
$entity_type->setLinkTemplate('drupal:content-translation-add', $translations_path . '/add/{source}/{target}');
$entity_type->setLinkTemplate('drupal:content-translation-edit', $translations_path . '/edit/{language}');
$entity_type->setLinkTemplate('drupal:content-translation-delete', $translations_path . '/delete/{language}');
}
// @todo Remove this as soon as menu access checks rely on the
// controller. See https://www.drupal.org/node/2155787.

View file

@ -652,7 +652,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
$source = $form_state->getValue(array('source_langcode', 'source'));
$entity_type_id = $entity->getEntityTypeId();
$form_state->setRedirect('content_translation.translation_add_' . $entity_type_id, array(
$form_state->setRedirect("entity.$entity_type_id.content_translation_add", array(
$entity_type_id => $entity->id(),
'source' => $source,
'target' => $form_object->getFormLangcode($form_state),
@ -689,7 +689,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
$form_state->setRedirectUrl($entity->urlInfo('delete-form'));
}
else {
$form_state->setRedirect('content_translation.translation_delete_' . $entity_type_id, [
$form_state->setRedirect("entity.$entity_type_id.content_translation_delete", [
$entity_type_id => $entity->id(),
'language' => $form_object->getFormLangcode($form_state),
]);

View file

@ -127,7 +127,7 @@ class ContentTranslationController extends ControllerBase {
$langcode = $language->getId();
$add_url = new Url(
'content_translation.translation_add_' . $entity_type_id,
"entity.$entity_type_id.content_translation_add",
array(
'source' => $original,
'target' => $language->getId(),
@ -138,7 +138,7 @@ class ContentTranslationController extends ControllerBase {
)
);
$edit_url = new Url(
'content_translation.translation_edit_' . $entity_type_id,
"entity.$entity_type_id.content_translation_edit",
array(
'language' => $language->getId(),
$entity_type_id => $entity->id(),
@ -148,7 +148,7 @@ class ContentTranslationController extends ControllerBase {
)
);
$delete_url = new Url(
'content_translation.translation_delete_' . $entity_type_id,
"entity.$entity_type_id.content_translation_delete",
array(
'language' => $language->getId(),
$entity_type_id => $entity->id(),

View file

@ -112,7 +112,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
'_admin_route' => $is_admin,
)
);
$collection->add("content_translation.translation_add_$entity_type_id", $route);
$collection->add("entity.$entity_type_id.content_translation_add", $route);
$route = new Route(
$path . '/edit/{language}',
@ -137,7 +137,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
'_admin_route' => $is_admin,
)
);
$collection->add("content_translation.translation_edit_$entity_type_id", $route);
$collection->add("entity.$entity_type_id.content_translation_edit", $route);
$route = new Route(
$path . '/delete/{language}',
@ -162,7 +162,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
'_admin_route' => $is_admin,
)
);
$collection->add("content_translation.translation_delete_$entity_type_id", $route);
$collection->add("entity.$entity_type_id.content_translation_delete", $route);
}
}

View file

@ -19,7 +19,7 @@ class ContentTranslationEnableTest extends WebTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['entity_test', 'menu_link_content'];
public static $modules = ['entity_test', 'menu_link_content', 'node'];
/**
* Tests that entity schemas are up-to-date after enabling translation.
@ -39,6 +39,9 @@ class ContentTranslationEnableTest extends WebTestBase {
$requirement_value = $this->cssSelect("tr.system-status-report__entry th:contains('Entity/field definitions') + td");
$this->assertEqual(t('Up to date'), trim((string) $requirement_value[0]));
$this->drupalGet('admin/config/regional/content-language');
// The node entity type should not be an option because it has no bundles.
$this->assertNoRaw('entity_types[node]');
// Enable content translation on entity types that have will have a
// content_translation_uid.
$edit = [
@ -47,12 +50,23 @@ class ContentTranslationEnableTest extends WebTestBase {
'entity_types[entity_test_mul]' => TRUE,
'settings[entity_test_mul][entity_test_mul][translatable]' => TRUE,
];
$this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration'));
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
// No pending updates should be available.
$this->drupalGet('admin/reports/status');
$requirement_value = $this->cssSelect("tr.system-status-report__entry th:contains('Entity/field definitions') + td");
$this->assertEqual(t('Up to date'), trim((string) $requirement_value[0]));
// Create a node type and check the content translation settings are now
// available for nodes.
$edit = array(
'name' => 'foo',
'title_label' => 'title for foo',
'type' => 'foo',
);
$this->drupalPostForm('admin/structure/types/add', $edit, t('Save content type'));
$this->drupalGet('admin/config/regional/content-language');
$this->assertRaw('entity_types[node]');
}
}

View file

@ -107,7 +107,8 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$language = ConfigurableLanguage::load($langcode);
$values[$langcode] = $this->getNewEntityValues($langcode);
$add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
$entity_type_id = $entity->getEntityTypeId();
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
@ -167,7 +168,8 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$language = ConfigurableLanguage::load($langcode);
$source_langcode = 'it';
$edit = array('source_langcode[source]' => $source_langcode);
$add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
$entity_type_id = $entity->getEntityTypeId();
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
@ -180,7 +182,8 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
// Add another translation and mark the other ones as outdated.
$values[$langcode] = $this->getNewEntityValues($langcode);
$edit = $this->getEditValues($values, $langcode) + array('content_translation[retranslate]' => TRUE);
$add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
$entity_type_id = $entity->getEntityTypeId();
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $source_langcode,
'target' => $langcode
@ -207,13 +210,15 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
*/
protected function doTestTranslationOverview() {
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
$translate_url = $entity->urlInfo('drupal:content-translation-overview');
$this->drupalGet($translate_url);
$translate_url->setAbsolute(FALSE);
foreach ($this->langcodes as $langcode) {
if ($entity->hasTranslation($langcode)) {
$language = new Language(array('id' => $langcode));
$view_path = $entity->url('canonical', array('language' => $language));
$elements = $this->xpath('//table//a[@href=:href]', array(':href' => $view_path));
$view_url = $entity->url('canonical', ['language' => $language]);
$elements = $this->xpath('//table//a[@href=:href]', [':href' => $view_url]);
$this->assertEqual((string) $elements[0], $entity->getTranslation($langcode)->label(), format_string('Label correctly shown for %language translation.', array('%language' => $langcode)));
$edit_path = $entity->url('edit-form', array('language' => $language));
$elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a[@href=:href]', array(':href' => $edit_path));
@ -343,7 +348,7 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
// Check that the translator cannot delete the original translation.
$args = [$this->entityTypeId => $entity->id(), 'language' => 'en'];
$this->drupalGet(Url::fromRoute('content_translation.translation_delete_' . $this->entityTypeId, $args));
$this->drupalGet(Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", $args));
$this->assertResponse(403);
}

View file

@ -73,7 +73,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
// Create a translation.
$this->drupalLogin($this->translator);
$add_translation_url = Url::fromRoute('content_translation.translation_add_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $this->langcodes[2]]);
$add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $this->langcodes[2]]);
$this->drupalPostForm($add_translation_url, array(), t('Save'));
$this->rebuildContainer();
}
@ -175,7 +175,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
$this->assertResponse($expected_status['overview'], SafeMarkup::format('The @user_label has the expected translation overview access.', $args));
// Check whether the user is allowed to create a translation.
$add_translation_url = Url::fromRoute('content_translation.translation_add_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $langcode], $options);
$add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $langcode], $options);
if ($expected_status['add_translation'] == 200) {
$this->clickLink('Add');
$this->assertUrl($add_translation_url->toString(), [], 'The translation overview points to the translation form when creating translations.');
@ -193,7 +193,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
// Check whether the user is allowed to edit a translation.
$langcode = $this->langcodes[2];
$options['language'] = $languages[$langcode];
$edit_translation_url = Url::fromRoute('content_translation.translation_edit_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
$edit_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_edit", [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
if ($expected_status['edit_translation'] == 200) {
$this->drupalGet($translations_url);
$editor = $expected_status['edit'] == 200;
@ -221,7 +221,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
// Check whether the user is allowed to delete a translation.
$langcode = $this->langcodes[2];
$options['language'] = $languages[$langcode];
$delete_translation_url = Url::fromRoute('content_translation.translation_delete_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
$delete_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
if ($expected_status['delete_translation'] == 200) {
$this->drupalGet($translations_url);
$editor = $expected_status['delete'] == 200;

View file

@ -464,7 +464,11 @@ function _editor_get_file_uuids_by_field(EntityInterface $entity) {
$formatted_text_fields = _editor_get_formatted_text_fields($entity);
foreach ($formatted_text_fields as $formatted_text_field) {
$text = $entity->get($formatted_text_field)->value;
$text = '';
$field_items = $entity->get($formatted_text_field);
foreach ($field_items as $field_item) {
$text .= $field_item->value;
}
$uuids[$formatted_text_field] = _editor_parse_file_uuids($text);
}
return $uuids;

View file

@ -14,11 +14,43 @@ use Drupal\Component\Annotation\Plugin;
*
* Plugin Namespace: Plugin\Editor
*
* Text editor plugin implementations need to define a plugin definition array
* through annotation. These definition arrays may be altered through
* hook_editor_info_alter(). The definition includes the following keys:
*
* - id: The unique, system-wide identifier of the text editor. Typically named
* the same as the editor library.
* - label: The human-readable name of the text editor, translated.
* - supports_content_filtering: Whether the editor supports "allowed content
* only" filtering.
* - supports_inline_editing: Whether the editor supports the inline editing
* provided by the Edit module.
* - is_xss_safe: Whether this text editor is not vulnerable to XSS attacks.
* - supported_element_types: On which form element #types this text editor is
* capable of working.
*
* A complete sample plugin definition should be defined as in this example:
*
* @code
* @Editor(
* id = "myeditor",
* label = @Translation("My Editor"),
* supports_content_filtering = FALSE,
* supports_inline_editing = FALSE,
* is_xss_safe = FALSE,
* supported_element_types = {
* "textarea",
* "textfield",
* }
* )
* @endcode
*
* For a working example, see \Drupal\ckeditor\Plugin\Editor\CKEditor
*
* @see \Drupal\editor\Plugin\EditorPluginInterface
* @see \Drupal\editor\Plugin\EditorBase
* @see \Drupal\editor\Plugin\EditorManager
* @see hook_editor_info_alter()
* @see plugin_api
*
* @Annotation

View file

@ -60,6 +60,9 @@ class EditorController extends ControllerBase {
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JSON response containing the XSS-filtered value.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* Thrown if no value to filter is specified.
*
* @see editor_filter_xss()
*/
public function filterXss(Request $request, FilterFormatInterface $filter_format) {

View file

@ -17,30 +17,8 @@ use Drupal\editor\Entity\Editor;
* This class provides default implementations of the EditorPluginInterface so
* that classes extending this one do not need to implement every method.
*
* Plugins extending this class need to define a plugin definition array through
* annotation. These definition arrays may be altered through
* hook_editor_info_alter(). The definition includes the following keys:
*
* - id: The unique, system-wide identifier of the text editor. Typically named
* the same as the editor library.
* - label: The human-readable name of the text editor, translated.
* - supports_content_filtering: Whether the editor supports "allowed content
* only" filtering.
* - supports_inline_editing: Whether the editor supports the inline editing
* provided by the Edit module.
* - is_xss_safe: Whether this text editor is not vulnerable to XSS attacks.
*
* A complete sample plugin definition should be defined as in this example:
*
* @code
* @Editor(
* id = "myeditor",
* label = @Translation("My Editor"),
* supports_content_filtering = FALSE,
* supports_inline_editing = FALSE,
* is_xss_safe = FALSE
* )
* @endcode
* Plugins extending this class need to specify an annotation containing the
* plugin definition so the plugin can be discovered.
*
* @see \Drupal\editor\Annotation\Editor
* @see \Drupal\editor\Plugin\EditorPluginInterface

View file

@ -8,6 +8,8 @@
namespace Drupal\editor\Tests;
use Drupal\system\Tests\Entity\EntityUnitTestBase;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Tests tracking of file usage by the Text Editor module.
@ -39,6 +41,11 @@ class EditorFileUsageTest extends EntityUnitTestBase {
));
$filtered_html_format->save();
// Set cardinality for body field.
FieldStorageConfig::loadByName('node', 'body')
->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
->save();
// Set up text editor.
$editor = entity_create('editor', array(
'format' => 'filtered_html',
@ -56,33 +63,56 @@ class EditorFileUsageTest extends EntityUnitTestBase {
* Tests the configurable text editor manager.
*/
public function testEditorEntityHooks() {
$image = entity_create('file');
$image->setFileUri('core/misc/druplicon.png');
$image->setFilename(drupal_basename($image->getFileUri()));
$image->save();
$file_usage = $this->container->get('file.usage');
$this->assertIdentical(array(), $file_usage->listUsage($image), 'The image has zero usages.');
$image_paths = array(
0 => 'core/misc/druplicon.png',
1 => 'core/misc/tree.png',
2 => 'core/misc/help.png',
);
$image_entities = array();
foreach ($image_paths as $key => $image_path) {
$image = entity_create('file');
$image->setFileUri($image_path);
$image->setFilename(drupal_basename($image->getFileUri()));
$image->save();
$file_usage = $this->container->get('file.usage');
$this->assertIdentical(array(), $file_usage->listUsage($image), 'The image ' . $image_paths[$key] . ' has zero usages.');
$image_entities[] = $image;
}
$body = array();
foreach ($image_entities as $key => $image_entity) {
// Don't be rude, say hello.
$body_value = '<p>Hello, world!</p>';
// Test handling of a valid image entry.
$body_value .= '<img src="awesome-llama-' . $key . '.jpg" data-entity-type="file" data-entity-uuid="' . $image_entity->uuid() . '" />';
// Test handling of an invalid data-entity-uuid attribute.
$body_value .= '<img src="awesome-llama-' . $key . '.jpg" data-entity-type="file" data-entity-uuid="invalid-entity-uuid-value" />';
// Test handling of an invalid data-entity-type attribute.
$body_value .= '<img src="awesome-llama-' . $key . '.jpg" data-entity-type="invalid-entity-type-value" data-entity-uuid="' . $image_entity->uuid() . '" />';
// Test handling of a non-existing UUID.
$body_value .= '<img src="awesome-llama-' . $key . '.jpg" data-entity-type="file" data-entity-uuid="30aac704-ba2c-40fc-b609-9ed121aa90f4" />';
$body[] = array(
'value' => $body_value,
'format' => 'filtered_html',
);
}
$body_value = '<p>Hello, world!</p><img src="awesome-llama.jpg" data-entity-type="file" data-entity-uuid="' . $image->uuid() . '" />';
// Test handling of an invalid data-entity-uuid attribute.
$body_value .= '<img src="awesome-llama.jpg" data-entity-type="file" data-entity-uuid="invalid-entity-uuid-value" />';
// Test handling of an invalid data-entity-type attribute.
$body_value .= '<img src="awesome-llama.jpg" data-entity-type="invalid-entity-type-value" data-entity-uuid="' . $image->uuid() . '" />';
// Test handling of a non-existing UUID.
$body_value .= '<img src="awesome-llama.jpg" data-entity-type="file" data-entity-uuid="30aac704-ba2c-40fc-b609-9ed121aa90f4" />';
// Test editor_entity_insert(): increment.
$this->createUser();
$node = entity_create('node', array(
'type' => 'page',
'title' => 'test',
'body' => array(
'value' => $body_value,
'format' => 'filtered_html',
),
'body' => $body,
'uid' => 1,
));
$node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '1'))), $file_usage->listUsage($image), 'The image has 1 usage.');
foreach ($image_entities as $key => $image_entity) {
$this->assertIdentical(array('editor' => array('node' => array(1 => '1'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 1 usage.');
}
// Test editor_entity_update(): increment, twice, by creating new revisions.
$node->setNewRevision(TRUE);
@ -90,45 +120,68 @@ class EditorFileUsageTest extends EntityUnitTestBase {
$second_revision_id = $node->getRevisionId();
$node->setNewRevision(TRUE);
$node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image), 'The image has 3 usages.');
foreach ($image_entities as $key => $image_entity) {
$this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 3 usages.');
}
// Test hook_entity_update(): decrement, by modifying the last revision:
// remove the data-entity-type attribute from the body field.
$body = $node->get('body')->first()->get('value');
$original_value = $body->getValue();
$new_value = str_replace('data-entity-type', 'data-entity-type-modified', $original_value);
$body->setValue($new_value);
$original_values = array();
for ($i = 0; $i < count($image_entities); $i++) {
$original_value = $node->body[$i]->value;
$new_value = str_replace('data-entity-type', 'data-entity-type-modified', $original_value);
$node->body[$i]->value = $new_value;
$original_values[$i] = $original_value;
}
$node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image), 'The image has 2 usages.');
foreach ($image_entities as $key => $image_entity) {
$this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 2 usages.');
}
// Test editor_entity_update(): increment again by creating a new revision:
// read the data- attributes to the body field.
$node->setNewRevision(TRUE);
$node->get('body')->first()->get('value')->setValue($original_value);
foreach ($original_values as $key => $original_value) {
$node->body[$key]->value = $original_value;
}
$node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image), 'The image has 3 usages.');
foreach ($image_entities as $key => $image_entity) {
$this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 3 usages.');
}
// Test hook_entity_update(): decrement, by modifying the last revision:
// remove the data-entity-uuid attribute from the body field.
$body = $node->get('body')->first()->get('value');
$new_value = str_replace('data-entity-uuid', 'data-entity-uuid-modified', $original_value);
$body->setValue($new_value);
foreach ($original_values as $key => $original_value) {
$original_value = $node->body[$key]->value;
$new_value = str_replace('data-entity-type', 'data-entity-type-modified', $original_value);
$node->body[$key]->value = $new_value;
}
$node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image), 'The image has 2 usages.');
foreach ($image_entities as $key => $image_entity) {
$this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 2 usages.');
}
// Test hook_entity_update(): increment, by modifying the last revision:
// read the data- attributes to the body field.
$node->get('body')->first()->get('value')->setValue($original_value);
foreach ($original_values as $key => $original_value) {
$node->body[$key]->value = $original_value;
}
$node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image), 'The image has 3 usages.');
foreach ($image_entities as $key => $image_entity) {
$this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 3 usages.');
}
// Test editor_entity_revision_delete(): decrement, by deleting a revision.
entity_revision_delete('node', $second_revision_id);
$this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image), 'The image has 2 usages.');
foreach ($image_entities as $key => $image_entity) {
$this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 2 usages.');
}
// Test editor_entity_delete().
$node->delete();
$this->assertIdentical(array(), $file_usage->listUsage($image), 'The image has zero usages again.');
foreach ($image_entities as $key => $image_entity) {
$this->assertIdentical(array(), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has zero usages again.');
}
}
}

View file

@ -40,7 +40,7 @@ class FieldInstance extends DrupalSqlBase {
public function fields() {
return array(
'field_name' => $this->t('The machine name of field.'),
'type_name' => $this->t('Content type where is used this field.'),
'type_name' => $this->t('Content type where this field is in use.'),
'weight' => $this->t('Weight.'),
'label' => $this->t('A name to show.'),
'widget_type' => $this->t('Widget type.'),

View file

@ -7,6 +7,8 @@
namespace Drupal\field\Tests\EntityReference;
use Drupal\comment\Entity\Comment;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldItemInterface;
@ -17,8 +19,11 @@ use Drupal\entity_test\Entity\EntityTestStringId;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Tests\FieldUnitTestBase;
use Drupal\file\Entity\File;
use Drupal\node\Entity\Node;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\user\Entity\User;
/**
@ -35,7 +40,7 @@ class EntityReferenceItemTest extends FieldUnitTestBase {
*
* @var array
*/
public static $modules = ['taxonomy', 'text', 'filter', 'views', 'field'];
public static $modules = ['node', 'comment', 'file', 'taxonomy', 'text', 'filter', 'views', 'field'];
/**
* The taxonomy vocabulary to test with.
@ -66,6 +71,11 @@ class EntityReferenceItemTest extends FieldUnitTestBase {
$this->installEntitySchema('entity_test_string_id');
$this->installEntitySchema('taxonomy_term');
$this->installEntitySchema('node');
$this->installEntitySchema('comment');
$this->installEntitySchema('file');
$this->installSchema('comment', ['comment_entity_statistics']);
$this->vocabulary = entity_create('taxonomy_vocabulary', array(
'name' => $this->randomMachineName(),
@ -90,6 +100,10 @@ class EntityReferenceItemTest extends FieldUnitTestBase {
$this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_taxonomy_term', 'Test content entity reference', 'taxonomy_term');
$this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_entity_test_string_id', 'Test content entity reference with string ID', 'entity_test_string_id');
$this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_taxonomy_vocabulary', 'Test config entity reference', 'taxonomy_vocabulary');
$this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_node', 'Test node entity reference', 'node');
$this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_user', 'Test user entity reference', 'user');
$this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_comment', 'Test comment entity reference', 'comment');
$this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_file', 'Test file entity reference', 'file');
}
/**
@ -333,9 +347,9 @@ class EntityReferenceItemTest extends FieldUnitTestBase {
}
/**
* Tests validation constraint.
* Tests ValidReferenceConstraint with newly created and unsaved entities.
*/
public function testValidation() {
public function testAutocreateValidation() {
// The term entity is unsaved here.
$term = Term::create(array(
'name' => $this->randomMachineName(),
@ -367,6 +381,100 @@ class EntityReferenceItemTest extends FieldUnitTestBase {
$entity->save();
$errors = $entity->validate();
$this->assertEqual(0, count($errors));
// Test with an unpublished and unsaved node.
$title = $this->randomString();
$node = Node::create([
'title' => $title,
'type' => 'node',
'status' => NODE_NOT_PUBLISHED,
]);
$entity = EntityTest::create([
'field_test_node' => [
'entity' => $node,
],
]);
$errors = $entity->validate();
$this->assertEqual(1, count($errors));
$this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'node', '%label' => $title]));
$this->assertEqual($errors[0]->getPropertyPath(), 'field_test_node.0.entity');
// Publish the node and try again.
$node->setPublished(TRUE);
$errors = $entity->validate();
$this->assertEqual(0, count($errors));
// Test with an unpublished and unsaved comment.
$title = $this->randomString();
$comment = Comment::create([
'subject' => $title,
'comment_type' => 'comment',
'status' => 0,
]);
$entity = EntityTest::create([
'field_test_comment' => [
'entity' => $comment,
],
]);
$errors = $entity->validate();
$this->assertEqual(1, count($errors));
$this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'comment', '%label' => $title]));
$this->assertEqual($errors[0]->getPropertyPath(), 'field_test_comment.0.entity');
// Publish the comment and try again.
$comment->setPublished(TRUE);
$errors = $entity->validate();
$this->assertEqual(0, count($errors));
// Test with an inactive and unsaved user.
$name = $this->randomString();
$user = User::create([
'name' => $name,
'status' => 0,
]);
$entity = EntityTest::create([
'field_test_user' => [
'entity' => $user,
],
]);
$errors = $entity->validate();
$this->assertEqual(1, count($errors));
$this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'user', '%label' => $name]));
$this->assertEqual($errors[0]->getPropertyPath(), 'field_test_user.0.entity');
// Activate the user and try again.
$user->activate();
$errors = $entity->validate();
$this->assertEqual(0, count($errors));
// Test with a temporary and unsaved file.
$filename = $this->randomMachineName() . '.txt';
$file = File::create([
'filename' => $filename,
'status' => 0,
]);
$entity = EntityTest::create([
'field_test_file' => [
'entity' => $file,
],
]);
$errors = $entity->validate();
$this->assertEqual(1, count($errors));
$this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'file', '%label' => $filename]));
$this->assertEqual($errors[0]->getPropertyPath(), 'field_test_file.0.entity');
// Set the file as permanent and try again.
$file->setPermanent();
$errors = $entity->validate();
$this->assertEqual(0, count($errors));
}
}

View file

@ -68,6 +68,11 @@ class UriItemTest extends FieldUnitTestBase {
'type' => 'uri',
])
->save();
// Test the generateSampleValue() method.
$entity = entity_create('entity_test');
$entity->$field_name->generateSampleItems();
$this->entityValidateAndSave($entity);
}
}

View file

@ -5,3 +5,6 @@ migrate.destination.entity:file:
source_path_property:
type: string
label: 'Source path'
urlencode:
type: boolean
label: 'Whether to urlencode incoming file paths'

View file

@ -9,27 +9,6 @@ use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Render\Element;
/**
* Returns HTML for an individual file upload widget.
*
* Default template: file-widget.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: A render element representing the file.
*/
function template_preprocess_file_widget(&$variables) {
$element = $variables['element'];
if (!empty($element['fids']['#value'])) {
// Add the file size after the file name.
$file = reset($element['#files']);
$element['file_' . $file->id()]['filename']['#suffix'] = ' <span class="file-size">(' . format_size($file->getSize()) . ')</span> ';
}
$variables['element'] = $element;
// The "js-form-managed-file" class is required for proper Ajax functionality.
$variables['attributes'] = array('class' => array('file-widget', 'js-form-managed-file', 'form-managed-file', 'clearfix'));
}
/**
* Prepares variables for multi file form widget templates.
*

View file

@ -110,10 +110,10 @@
*/
Drupal.behaviors.filePreviewLinks = {
attach: function (context) {
$(context).find('div.js-form-managed-file .file a, .file-widget .file a').on('click', Drupal.file.openInNewWindow);
$(context).find('div.js-form-managed-file .file a').on('click', Drupal.file.openInNewWindow);
},
detach: function (context) {
$(context).find('div.js-form-managed-file .file a, .file-widget .file a').off('click', Drupal.file.openInNewWindow);
$(context).find('div.js-form-managed-file .file a').off('click', Drupal.file.openInNewWindow);
}
};

View file

@ -555,10 +555,6 @@ function file_theme() {
),
// From file.field.inc.
'file_widget' => array(
'render element' => 'element',
'file' => 'file.field.inc',
),
'file_widget_multiple' => array(
'render element' => 'element',
'file' => 'file.field.inc',

View file

@ -23,3 +23,4 @@ process:
uid: uid
destination:
plugin: entity:file
urlencode: true

View file

@ -23,3 +23,4 @@ process:
destination:
plugin: entity:file
source_path_property: filepath
urlencode: true

View file

@ -27,8 +27,41 @@ class FileSelection extends DefaultSelection {
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
$query->condition('status', FILE_STATUS_PERMANENT);
// Allow referencing :
// - files with status "permanent"
// - or files uploaded by the current user (since newly uploaded files only
// become "permanent" after the containing entity gets validated and
// saved.)
$query->condition($query->orConditionGroup()
->condition('status', FILE_STATUS_PERMANENT)
->condition('uid', $this->currentUser->id()));
return $query;
}
/**
* {@inheritdoc}
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$file = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
// In order to create a referenceable file, it needs to have a "permanent"
// status.
/** @var \Drupal\file\FileInterface $file */
$file->setPermanent();
return $file;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableNewEntities(array $entities) {
$entities = parent::validateReferenceableNewEntities($entities);
$entities = array_filter($entities, function ($file) {
/** @var \Drupal\file\FileInterface $file */
return $file->isPermanent() || $file->getOwnerId() === $this->currentUser->id();
});
return $entities;
}
}

View file

@ -28,7 +28,7 @@ use Drupal\Core\TypedData\DataDefinition;
* default_widget = "file_generic",
* default_formatter = "file_default",
* list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList",
* constraints = {"ValidReference" = {}, "ReferenceAccess" = {}}
* constraints = {"ReferenceAccess" = {}, "FileValidation" = {}}
* )
*/
class FileItem extends EntityReferenceItem {

View file

@ -18,8 +18,9 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\file\Element\ManagedFile;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\file\Entity\File;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* Plugin implementation of the 'file_generic' widget.
@ -369,11 +370,6 @@ class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
$item = $element['#value'];
$item['fids'] = $element['fids']['#value'];
// Prevent the file widget from overriding the image widget.
if (!isset($element['#theme'])) {
$element['#theme'] = 'file_widget';
}
// Add the display field if enabled.
if ($element['#display_field']) {
$element['display'] = array(
@ -575,4 +571,15 @@ class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
static::setWidgetState($parents, $field_name, $form_state, $field_state);
}
/**
* {@inheritdoc}
*/
public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
// Never flag validation errors for the remove button.
$clicked_button = end($form_state->getTriggeringElement()['#parents']);
if ($clicked_button !== 'remove_button') {
parent::flagErrors($items, $violations, $form, $form_state);
}
}
}

View file

@ -0,0 +1,22 @@
<?php
/**
* @file
* Contains \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraint.
*/
namespace Drupal\file\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Validation File constraint.
*
* @Constraint(
* id = "FileValidation",
* label = @Translation("File Validation", context = "Validation")
* )
*/
class FileValidationConstraint extends Constraint {
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraintValidator.
*/
namespace Drupal\file\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Checks that a file referenced in a file field is valid.
*/
class FileValidationConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
// Get the file to execute validators.
$file = $value->get('entity')->getTarget()->getValue();
// Get the validators.
$validators = $value->getUploadValidators();
// Checks that a file meets the criteria specified by the validators.
if ($errors = file_validate($file, $validators)) {
foreach ($errors as $error) {
$this->context->addViolation($error);
}
}
}
}

View file

@ -7,8 +7,11 @@
namespace Drupal\file\Plugin\migrate\destination;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\UriItem;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\StreamWrapper\LocalStream;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
@ -41,7 +44,7 @@ class EntityFile extends EntityContentBase {
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, StreamWrapperManagerInterface $stream_wrappers, FileSystemInterface $file_system) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager, StreamWrapperManagerInterface $stream_wrappers, FileSystemInterface $file_system) {
$configuration += array(
'source_base_path' => '',
'source_path_property' => 'filepath',
@ -49,7 +52,7 @@ class EntityFile extends EntityContentBase {
'move' => FALSE,
'urlencode' => FALSE,
);
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager);
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager, $field_type_manager);
$this->streamWrapperManager = $stream_wrappers;
$this->fileSystem = $file_system;
@ -68,6 +71,7 @@ class EntityFile extends EntityContentBase {
$container->get('entity.manager')->getStorage($entity_type),
array_keys($container->get('entity.manager')->getBundleInfo($entity_type)),
$container->get('entity.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('stream_wrapper_manager'),
$container->get('file_system')
);
@ -77,6 +81,12 @@ class EntityFile extends EntityContentBase {
* {@inheritdoc}
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
// For stub rows, there is no real file to deal with, let the stubbing
// process take its default path.
if ($row->isStub()) {
return parent::getEntity($row, $old_destination_id_values);
}
$destination = $row->getDestinationProperty($this->configuration['destination_path_property']);
$entity = $this->storage->loadByProperties(['uri' => $destination]);
if ($entity) {
@ -91,6 +101,12 @@ class EntityFile extends EntityContentBase {
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
// For stub rows, there is no real file to deal with, let the stubbing
// process create the stub entity.
if ($row->isStub()) {
return parent::import($row, $old_destination_id_values);
}
$file = $row->getSourceProperty($this->configuration['source_path_property']);
$destination = $row->getDestinationProperty($this->configuration['destination_path_property']);
$source = $this->configuration['source_base_path'] . $file;
@ -256,4 +272,30 @@ class EntityFile extends EntityContentBase {
return $filename;
}
/**
* {@inheritdoc}
*/
protected function processStubRow(Row $row) {
// We stub the uri value ourselves so we can create a real stub file for it.
if (!$row->getDestinationProperty('uri')) {
$field_definitions = $this->entityManager
->getFieldDefinitions($this->storage->getEntityTypeId(),
$this->getKey('bundle'));
$value = UriItem::generateSampleValue($field_definitions['uri']);
if (empty($value)) {
throw new MigrateException('Stubbing failed, unable to generate value for field uri');
}
// generateSampleValue() wraps the value in an array.
$value = reset($value);
// Make it into a proper public file uri, stripping off the existing
// scheme if present.
$value = 'public://' . preg_replace('|^[a-z]+://|i', '', $value);
$value = Unicode::substr($value, 0, $field_definitions['uri']->getSetting('max_length'));
// Create a real file, so File::preSave() can do filesize() on it.
touch($value);
$row->setDestinationProperty('uri', $value);
}
parent::processStubRow($row);
}
}

View file

@ -24,6 +24,11 @@ class FileUri extends ProcessPluginBase {
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// If we're stubbing a file entity, return a uri of NULL so it will get
// stubbed by the general process.
if ($row->isStub()) {
return NULL;
}
list($filepath, $file_directory_path, $temp_directory_path, $is_public) = $value;
// Specific handling using $temp_directory_path for temporary files.

View file

@ -159,4 +159,35 @@ class FileFieldValidateTest extends FileFieldTestBase {
$this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with extension checking.');
}
/**
* Checks that a file can always be removed if it does not pass validation.
*/
public function testFileRemoval() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$type_name = 'article';
$field_name = 'file_test';
$this->createFileField($field_name, 'node', $type_name);
$test_file = $this->getTestFile('image');
// Disable extension checking.
$this->updateFileField($field_name, $type_name, array('file_extensions' => ''));
// Check that the file can be uploaded with no extension checking.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$node_storage->resetCache(array($nid));
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
$this->assertFileExists($node_file, 'File exists after uploading a file with no extension checking.');
$this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with no extension checking.');
// Enable extension checking for text files.
$this->updateFileField($field_name, $type_name, array('file_extensions' => 'txt'));
// Check that the file can still be removed.
$this->removeNodeFile($nid);
$this->assertNoText('Only files with the following extensions are allowed: txt.');
$this->assertText('Article ' . $node->getTitle() . ' has been updated.');
}
}

View file

@ -0,0 +1,42 @@
<?php
/**
* @file
* Contains \Drupal\file\Tests\Migrate\MigrateFileStubTest.
*/
namespace Drupal\file\Tests\Migrate;
use Drupal\migrate_drupal\Tests\MigrateDrupalTestBase;
use Drupal\migrate_drupal\Tests\StubTestTrait;
/**
* Test stub creation for file entities.
*
* @group file
*/
class MigrateFileStubTest extends MigrateDrupalTestBase {
use StubTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['file'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('file');
}
/**
* Tests creation of file stubs.
*/
public function testStub() {
$this->performStubTest('file');
}
}

View file

@ -1,17 +0,0 @@
{#
/**
* @file
* Default theme implementation to display a file widget.
*
* Available variables:
* - element: Form element for the managed file.
* - attributes: Remaining HTML attributes for the containing element.
*
* @see template_preprocess_file_widget()
*
* @ingroup themeable
*/
#}
<div{{ attributes }}>
{{ element }}
</div>

View file

@ -0,0 +1,119 @@
<?php
/**
* @file
* Contains \Drupal\Tests\file\Kernel\FileItemValidationTest.
*/
namespace Drupal\Tests\file\Kernel;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\file\Entity\File;
use Drupal\KernelTests\KernelTestBase;
use Drupal\user\Entity\User;
use org\bovigo\vfs\vfsStream;
/**
* Tests that files referenced in file and image fields are always validated.
*
* @group file
*/
class FileItemValidationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['file', 'image', 'entity_test', 'field', 'user', 'system'];
/**
* A user.
*
* @var \Drupal\user\UserInterface
*/
protected $user;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('file');
$this->installSchema('file', 'file_usage');
$this->installSchema('system', 'sequences');
$this->user = User::create([
'name' => 'username',
'status' => 1,
]);
$this->user->save();
}
/**
* @covers \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraint
* @covers \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraintValidator
* @dataProvider getFileTypes
*/
public function testFileValidationConstraint($file_type) {
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_test_file',
'entity_type' => 'entity_test',
'type' => $file_type,
]);
$field_storage->save();
$field = FieldConfig::create([
'field_name' => 'field_test_file',
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'settings' => [
'max_filesize' => '2k',
'file_extensions' => 'jpg|png',
],
]);
$field->save();
vfsStream::setup('drupal_root');
vfsStream::create([
'sites' => [
'default' => [
'files' => [
'test.txt' => str_repeat('a', 3000),
]
]
]
]);
// Test for max filesize.
$file = File::create([
'uri' => 'vfs://drupal_root/sites/default/files/test.txt',
]);
$file->setPermanent();
$file->save();
$entity_test = EntityTest::create([
'uid' => $this->user->id(),
'field_test_file' => [
'target_id' => $file->id(),
]
]);
$result = $entity_test->validate();
$this->assertCount(2, $result);
$this->assertEquals('field_test_file.0', $result->get(0)->getPropertyPath());
$this->assertEquals('The file is <em class="placeholder">2.93 KB</em> exceeding the maximum file size of <em class="placeholder">2 KB</em>.', (string) $result->get(0)->getMessage());
$this->assertEquals('field_test_file.0', $result->get(1)->getPropertyPath());
$this->assertEquals('Only files with the following extensions are allowed: <em class="placeholder">jpg|png</em>.', (string) $result->get(1)->getMessage());
}
/**
* Provides a list of file types to test.
*/
public function getFileTypes() {
return [['file'], ['image']];
}
}

View file

@ -11,3 +11,4 @@ history.read_node:
_controller: '\Drupal\history\Controller\HistoryController::readNode'
requirements:
_entity_access: 'node.view'
node: \d+

View file

@ -81,8 +81,7 @@ class HistoryUserTimestamp extends FilterPluginBase {
$clause = '';
$clause2 = '';
if (\Drupal::moduleHandler()->moduleExists('comment')) {
$ces = $this->query->ensureTable('comment_entity_statistics', $this->relationship);
if ($ces = $this->query->ensureTable('comment_entity_statistics', $this->relationship)) {
$clause = ("OR $ces.last_comment_timestamp > (***CURRENT_TIME*** - $limit)");
$clause2 = "OR $field < $ces.last_comment_timestamp";
}

View file

@ -80,5 +80,14 @@ class HistoryTimestampTest extends ViewTestBase {
$this->executeView($view);
$this->assertEqual(count($view->result), 1);
$this->assertIdenticalResultset($view, array(array('nid' => $nodes[0]->id())), $column_map);
// Install Comment module and make sure that content types without comment
// field will not break the view.
// See \Drupal\history\Plugin\views\filter\HistoryUserTimestamp::query()
\Drupal::service('module_installer')->install(['comment']);
$view = Views::getView('test_history');
$view->setDisplay('page_2');
$this->executeView($view);
}
}

View file

@ -43,7 +43,7 @@ use Drupal\file\Plugin\Field\FieldType\FileItem;
* },
* },
* list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList",
* constraints = {"ValidReference" = {}, "ReferenceAccess" = {}}
* constraints = {"ReferenceAccess" = {}, "FileValidation" = {}}
* )
*/
class ImageItem extends FileItem {
@ -346,10 +346,10 @@ class ImageItem extends FileItem {
if ($path = $random->image(drupal_realpath($destination), $min_resolution, $max_resolution)) {
$image = File::create();
$image->setFileUri($path);
// $image->setOwner($account);
$image->setOwnerId(\Drupal::currentUser()->id());
$image->setMimeType('image/' . pathinfo($path, PATHINFO_EXTENSION));
$image->setFileName(drupal_basename($path));
$destination_dir = $settings['uri_scheme'] . '://' . $settings['file_directory'];
$destination_dir = static::doGetUploadLocation($settings);
file_prepare_directory($destination_dir, FILE_CREATE_DIRECTORY);
$destination = $destination_dir . '/' . basename($path);
$file = file_move($image, $destination, FILE_CREATE_DIRECTORY);

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Update functions for Language module.
*/
/**
* Rebuild the container as services changed.
*/
function language_update_8001() {
\Drupal::service('kernel')->invalidateContainer();
}

View file

@ -9,7 +9,7 @@ services:
- [initLanguageManager]
language.config_subscriber:
class: Drupal\language\EventSubscriber\ConfigSubscriber
arguments: ['@language_manager', '@language.default', '@config.factory']
arguments: ['@language_manager', '@language.default', '@config.factory', '@language_negotiator']
tags:
- { name: event_subscriber }
language.config_factory_override:

View file

@ -14,6 +14,8 @@ use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\language\ConfigurableLanguageManager;
use Drupal\language\HttpKernel\PathProcessorLanguage;
use Drupal\language\LanguageNegotiatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
@ -42,6 +44,20 @@ class ConfigSubscriber implements EventSubscriberInterface {
*/
protected $configFactory;
/**
* The language negotiator.
*
* @var \Drupal\language\LanguageNegotiatorInterface
*/
protected $languageNegotiator;
/**
* The language path processor.
*
* @var \Drupal\language\HttpKernel\PathProcessorLanguage
*/
protected $pathProcessorLanguage;
/**
* Constructs a new class object.
*
@ -51,11 +67,14 @@ class ConfigSubscriber implements EventSubscriberInterface {
* The default language.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\language\LanguageNegotiatorInterface $language_negotiator
* The language negotiator.
*/
public function __construct(LanguageManagerInterface $language_manager, LanguageDefault $language_default, ConfigFactoryInterface $config_factory) {
public function __construct(LanguageManagerInterface $language_manager, LanguageDefault $language_default, ConfigFactoryInterface $config_factory, LanguageNegotiatorInterface $language_negotiator) {
$this->languageManager = $language_manager;
$this->languageDefault = $language_default;
$this->configFactory = $config_factory;
$this->languageNegotiator = $language_negotiator;
}
/**
@ -102,6 +121,25 @@ class ConfigSubscriber implements EventSubscriberInterface {
// Trigger a container rebuild on the next request by invalidating it.
ConfigurableLanguageManager::rebuildServices();
}
elseif ($saved_config->getName() == 'language.types' && $event->isChanged('negotiation')) {
// If the negotiation configuration changed the language negotiator and
// the language path processor have to be reset so that they regenerate
// the method instances and also sort them accordingly to the new config.
$this->languageNegotiator->reset();
if (isset($this->pathProcessorLanguage)) {
$this->pathProcessorLanguage->reset();
}
}
}
/**
* Injects the language path processors on multilingual site configuration.
*
* @param \Drupal\language\HttpKernel\PathProcessorLanguage $path_processor_language
* The language path processor.
*/
public function setPathProcessorLanguage(PathProcessorLanguage $path_processor_language) {
$this->pathProcessorLanguage = $path_processor_language;
}
/**

View file

@ -63,7 +63,7 @@ class ContentLanguageSettingsForm extends FormBase {
$bundles = $this->entityManager->getAllBundleInfo();
$language_configuration = array();
foreach ($entity_types as $entity_type_id => $entity_type) {
if (!$entity_type instanceof ContentEntityTypeInterface || !$entity_type->hasKey('langcode')) {
if (!$entity_type instanceof ContentEntityTypeInterface || !$entity_type->hasKey('langcode') || !isset($bundles[$entity_type_id])) {
continue;
}
$labels[$entity_type_id] = $entity_type->getLabel() ?: $entity_type_id;

View file

@ -13,6 +13,7 @@ use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\language\ConfigurableLanguageManagerInterface;
use Drupal\language\EventSubscriber\ConfigSubscriber;
use Drupal\language\LanguageNegotiatorInterface;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Session\AccountInterface;
@ -57,6 +58,14 @@ class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPa
*/
protected $multilingual;
/**
* The language configuration event subscriber.
*
* @var \Drupal\language\EventSubscriber\ConfigSubscriber
*/
protected $configSubscriber;
/**
* Constructs a PathProcessorLanguage object.
*
@ -68,12 +77,15 @@ class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPa
* The language negotiator.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current active user.
* @param \Drupal\language\EventSubscriber\ConfigSubscriber $config_subscriber
* The language configuration event subscriber.
*/
public function __construct(ConfigFactoryInterface $config, ConfigurableLanguageManagerInterface $language_manager, LanguageNegotiatorInterface $negotiator, AccountInterface $current_user) {
public function __construct(ConfigFactoryInterface $config, ConfigurableLanguageManagerInterface $language_manager, LanguageNegotiatorInterface $negotiator, AccountInterface $current_user, ConfigSubscriber $config_subscriber) {
$this->config = $config;
$this->languageManager = $language_manager;
$this->negotiator = $negotiator;
$this->negotiator->setCurrentUser($current_user);
$this->configSubscriber = $config_subscriber;
}
/**
@ -152,4 +164,22 @@ class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPa
});
}
/**
* Initializes the injected event subscriber with the language path processor.
*
* The language path processor service is registered only on multilingual
* site configuration, thus we inject it in the event subscriber only when
* it is initialized.
*/
public function initConfigSubscriber() {
$this->configSubscriber->setPathProcessorLanguage($this);
}
/**
* Resets the collected processors instances.
*/
public function reset() {
$this->processors = array();
}
}

View file

@ -39,7 +39,9 @@ class LanguageServiceProvider extends ServiceProviderBase {
->addArgument(new Reference('config.factory'))
->addArgument(new Reference('language_manager'))
->addArgument(new Reference('language_negotiator'))
->addArgument(new Reference('current_user'));
->addArgument(new Reference('current_user'))
->addArgument(new Reference('language.config_subscriber'))
->addMethodCall('initConfigSubscriber');
}
}

View file

@ -0,0 +1,294 @@
<?php
/**
* @file
* Contains \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity.
*/
namespace Drupal\language\Plugin\LanguageNegotiation;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Url;
use Drupal\language\LanguageNegotiationMethodBase;
use Drupal\language\LanguageSwitcherInterface;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Class for identifying the content translation language.
*
* @LanguageNegotiation(
* id = Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity::METHOD_ID,
* types = {Drupal\Core\Language\LanguageInterface::TYPE_CONTENT},
* weight = -9,
* name = @Translation("Content language"),
* description = @Translation("Determines the content language from a request parameter."),
* )
*/
class LanguageNegotiationContentEntity extends LanguageNegotiationMethodBase implements OutboundPathProcessorInterface, LanguageSwitcherInterface, ContainerFactoryPluginInterface {
/**
* The language negotiation method ID.
*/
const METHOD_ID = 'language-content-entity';
/**
* The query string parameter.
*/
const QUERY_PARAMETER = 'language_content_entity';
/**
* A list of all the link paths of enabled content entities.
*
* @var array
*/
protected $contentEntityPaths;
/**
* Static cache for the language negotiation order check.
*
* @see \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity::hasLowerLanguageNegotiationWeight()
*
* @var bool
*/
protected $hasLowerLanguageNegotiationWeightResult;
/**
* Static cache of outbound route paths per request.
*
* @var \SplObjectStorage
*/
protected $paths;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a new LanguageNegotiationContentEntity instance.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entity_manager) {
$this->entityManager = $entity_manager;
$this->paths = new \SplObjectStorage();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($container->get('entity.manager'));
}
/**
* {@inheritdoc}
*/
public function getLangcode(Request $request = NULL) {
$langcode = $request->get(static::QUERY_PARAMETER);
$language_enabled = array_key_exists($langcode, $this->languageManager->getLanguages());
return $language_enabled ? $langcode : NULL;
}
/**
* {@inheritdoc}
*/
public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
// If appropriate, process outbound to add a query parameter to the url and
// remove the language option, so that url negotiator does not rewrite the
// url.
// First, check if processing conditions are met.
if (!($request && !empty($options['route']) && $this->hasLowerLanguageNegotiationWeight() && $this->meetsContentEntityRoutesCondition($options['route'], $request))) {
return $path;
}
if (isset($options['language']) || $langcode = $this->getLangcode($request)) {
// If the language option is set, unset it, so that the url language
// negotiator does not rewrite the url.
if (isset($options['language'])) {
$langcode = $options['language']->getId();
unset($options['language']);
}
if (isset($options['query']) && is_string($options['query'])) {
$query = [];
parse_str($options['query'], $query);
$options['query'] = $query;
}
else {
$options['query'] = [];
}
if (!isset($options['query'][static::QUERY_PARAMETER])) {
$query_addon = [static::QUERY_PARAMETER => $langcode];
$options['query'] += $query_addon;
// @todo Remove this once https://www.drupal.org/node/2507005 lands.
$path .= (strpos($path, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($query_addon);
}
if ($bubbleable_metadata) {
// Cached URLs that have been processed by this outbound path
// processor must be:
$bubbleable_metadata
// - varied by the content language query parameter.
->addCacheContexts(['url.query_args:' . static::QUERY_PARAMETER]);
}
}
return $path;
}
/**
* {@inheritdoc}
*/
public function getLanguageSwitchLinks(Request $request, $type, Url $url) {
$links = [];
$query = [];
parse_str($request->getQueryString(), $query);
foreach ($this->languageManager->getNativeLanguages() as $language) {
$langcode = $language->getId();
$query[static::QUERY_PARAMETER] = $langcode;
$links[$langcode] = [
'url' => $url,
'title' => $language->getName(),
'attributes' => ['class' => ['language-link']],
'query' => $query,
];
}
return $links;
}
/**
* Determines if content entity language negotiator has higher priority.
*
* The content entity language negotiator having higher priority than the url
* language negotiator, is a criteria in
* \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity::processOutbound().
*
* @return bool
* TRUE if the the content entity language negotiator has higher priority
* than the url language negotiator, FALSE otherwise.
*/
protected function hasLowerLanguageNegotiationWeight() {
if (!isset($this->hasLowerLanguageNegotiationWeightResult)) {
// Only run if the LanguageNegotiationContentEntity outbound function is
// being executed before the outbound function of LanguageNegotiationUrl.
$content_method_weights = $this->config->get('language.types')->get('negotiation.language_content.enabled') ?: [];
// Check if the content language is configured to be dependent on the
// url negotiator directly or indirectly over the interface negotiator.
if (isset($content_method_weights[LanguageNegotiationUrl::METHOD_ID]) && ($content_method_weights[static::METHOD_ID] > $content_method_weights[LanguageNegotiationUrl::METHOD_ID])) {
$this->hasLowerLanguageNegotiationWeightResult = FALSE;
}
else {
$check_interface_method = FALSE;
if (isset($content_method_weights[LanguageNegotiationUI::METHOD_ID])) {
$interface_method_weights = $this->config->get('language.types')->get('negotiation.language_interface.enabled') ?: [];
$check_interface_method = isset($interface_method_weights[LanguageNegotiationUrl::METHOD_ID]);
}
if ($check_interface_method) {
$max_weight = $content_method_weights[LanguageNegotiationUI::METHOD_ID];
$max_weight = isset($content_method_weights[LanguageNegotiationUrl::METHOD_ID]) ? max($max_weight, $content_method_weights[LanguageNegotiationUrl::METHOD_ID]) : $max_weight;
}
else {
$max_weight = isset($content_method_weights[LanguageNegotiationUrl::METHOD_ID]) ? $content_method_weights[LanguageNegotiationUrl::METHOD_ID] : PHP_INT_MAX;
}
$this->hasLowerLanguageNegotiationWeightResult = $content_method_weights[static::METHOD_ID] < $max_weight;
}
}
return $this->hasLowerLanguageNegotiationWeightResult;
}
/**
* Determines if content entity route condition is met.
*
* Requirements: currently being on an content entity route and processing
* outbound url pointing to the same content entity.
*
* @param \Symfony\Component\Routing\Route $outbound_route
* The route object for the current outbound url being processed.
* @param \Symfony\Component\HttpFoundation\Request $request
* The HttpRequest object representing the current request.
*
* @return bool
* TRUE if the content entity route condition is met, FALSE otherwise.
*/
protected function meetsContentEntityRoutesCondition(Route $outbound_route, Request $request) {
$outbound_path_pattern = $outbound_route->getPath();
$storage = isset($this->paths[$request]) ? $this->paths[$request] : [];
if (!isset($storage[$outbound_path_pattern])) {
$storage[$outbound_path_pattern] = FALSE;
// Check if the outbound route points to the current entity.
if ($content_entity_type_id_for_current_route = $this->getContentEntityTypeIdForCurrentRequest($request)) {
if (!empty($this->getContentEntityPaths()[$outbound_path_pattern]) && $content_entity_type_id_for_current_route == $this->getContentEntityPaths()[$outbound_path_pattern]) {
$storage[$outbound_path_pattern] = TRUE;
}
}
$this->paths[$request] = $storage;
}
return $storage[$outbound_path_pattern];
}
/**
* Returns the content entity type ID from the current request for the route.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The HttpRequest object representing the current request.
*
* @return string
* The entity type ID for the route from the request.
*/
protected function getContentEntityTypeIdForCurrentRequest(Request $request) {
$content_entity_type_id_for_current_route = '';
if ($current_route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) {
$current_route_path = $current_route->getPath();
$content_entity_type_id_for_current_route = isset($this->getContentEntityPaths()[$current_route_path]) ? $this->getContentEntityPaths()[$current_route_path] : '';
}
return $content_entity_type_id_for_current_route;
}
/**
* Returns the paths for the link templates of all content entities.
*
* @return array
* An array of all content entity type IDs, keyed by the corresponding link
* template paths.
*/
protected function getContentEntityPaths() {
if (!isset($this->contentEntityPaths)) {
$this->contentEntityPaths = [];
$entity_types = $this->entityManager->getDefinitions();
foreach ($entity_types as $entity_type_id => $entity_type) {
if ($entity_type->isSubclassOf(ContentEntityInterface::class)) {
$entity_paths = array_fill_keys($entity_type->getLinkTemplates(), $entity_type_id);
$this->contentEntityPaths = array_merge($this->contentEntityPaths, $entity_paths);
}
}
}
return $this->contentEntityPaths;
}
}

View file

@ -7,22 +7,34 @@
namespace Drupal\language\Tests;
use Drupal\Core\Language\LanguageInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\KernelTestBase;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Tests the language of entity URLs.
* @group language
*/
class EntityUrlLanguageTest extends KernelTestBase {
class EntityUrlLanguageTest extends LanguageTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['language', 'entity_test', 'user', 'system'];
public static $modules = ['entity_test', 'user'];
/**
* The entity being used for testing.
*
* @var \Drupal\Core\Entity\ContentEntityInterface
*/
protected $entity;
protected function setUp() {
parent::setUp();
@ -37,33 +49,93 @@ class EntityUrlLanguageTest extends KernelTestBase {
ConfigurableLanguage::create(['id' => 'es'])->save();
ConfigurableLanguage::create(['id' => 'fr'])->save();
$this->config('language.types')->setData([
'configurable' => ['language_interface'],
'negotiation' => ['language_interface' => ['enabled' => ['language-url' => 0]]],
])->save();
$this->config('language.negotiation')->setData([
'url' => [
'source' => 'path_prefix',
'prefixes' => ['en' => 'en', 'es' => 'es', 'fr' => 'fr']
],
])->save();
$config = $this->config('language.negotiation');
$config->set('url.prefixes', ['en' => 'en', 'es' => 'es', 'fr' => 'fr'])
->save();
$this->kernel->rebuildContainer();
$this->container = $this->kernel->getContainer();
\Drupal::setContainer($this->container);
$this->createTranslatableEntity();
}
/**
* Ensures that entity URLs in a language have the right language prefix.
*/
public function testEntityUrlLanguage() {
$entity = EntityTest::create();
$entity->addTranslation('es', ['name' => 'name spanish']);
$entity->addTranslation('fr', ['name' => 'name french']);
$entity->save();
$this->assertTrue(strpos($this->entity->urlInfo()->toString(), '/en/entity_test/' . $this->entity->id()) !== FALSE);
$this->assertTrue(strpos($this->entity->getTranslation('es')->urlInfo()->toString(), '/es/entity_test/' . $this->entity->id()) !== FALSE);
$this->assertTrue(strpos($this->entity->getTranslation('fr')->urlInfo()->toString(), '/fr/entity_test/' . $this->entity->id()) !== FALSE);
}
$this->assertTrue(strpos($entity->urlInfo()->toString(), '/en/entity_test/' . $entity->id()) !== FALSE);
$this->assertTrue(strpos($entity->getTranslation('es')->urlInfo()->toString(), '/es/entity_test/' . $entity->id()) !== FALSE);
$this->assertTrue(strpos($entity->getTranslation('fr')->urlInfo()->toString(), '/fr/entity_test/' . $entity->id()) !== FALSE);
/**
* Ensures correct entity URLs with the method language-content-entity enabled.
*
* Test case with the method language-content-entity enabled and configured
* with higher and also with lower priority than the method language-url.
*/
public function testEntityUrlLanguageWithLanguageContentEnabled() {
// Define the method language-content-entity with a higher priority than
// language-url.
$config = $this->config('language.types');
$config->set('configurable', [LanguageInterface::TYPE_INTERFACE, LanguageInterface::TYPE_CONTENT]);
$config->set('negotiation.language_content.enabled', [
LanguageNegotiationContentEntity::METHOD_ID => 0,
LanguageNegotiationUrl::METHOD_ID => 1
]);
$config->save();
// Without being on an content entity route the default entity URL tests
// should still pass.
$this->testEntityUrlLanguage();
// Now switching to an entity route, so that the URL links are generated
// while being on an entity route.
$this->setCurrentRequestForRoute('/entity_test/{entity_test}', 'entity.entity_test.canonical');
// The method language-content-entity should run before language-url and
// append query parameter for the content language and prevent language-url
// from overwriting the url.
$this->assertTrue(strpos($this->entity->urlInfo('canonical')->toString(), '/en/entity_test/' . $this->entity->id() . '?' . LanguageNegotiationContentEntity::QUERY_PARAMETER . '=en') !== FALSE);
$this->assertTrue(strpos($this->entity->getTranslation('es')->urlInfo('canonical')->toString(), '/en/entity_test/' . $this->entity->id() . '?' . LanguageNegotiationContentEntity::QUERY_PARAMETER . '=es') !== FALSE);
$this->assertTrue(strpos($this->entity->getTranslation('fr')->urlInfo('canonical')->toString(), '/en/entity_test/' . $this->entity->id() . '?' . LanguageNegotiationContentEntity::QUERY_PARAMETER . '=fr') !== FALSE);
// Define the method language-url with a higher priority than
// language-content-entity. This configuration should match the default one,
// where the language-content-entity is turned off.
$config->set('negotiation.language_content.enabled', [
LanguageNegotiationUrl::METHOD_ID => 0,
LanguageNegotiationContentEntity::METHOD_ID => 1
]);
$config->save();
// The default entity URL tests should pass again with the current
// configuration.
$this->testEntityUrlLanguage();
}
/**
* Creates a translated entity.
*/
protected function createTranslatableEntity() {
$this->entity = EntityTest::create();
$this->entity->addTranslation('es', ['name' => 'name spanish']);
$this->entity->addTranslation('fr', ['name' => 'name french']);
$this->entity->save();
}
/**
* Sets the current request to a specific path with the corresponding route.
*
* @param string $path
* The path for which the current request should be created.
* @param string $route_name
* The route name for which the route object for the request should be
* created.
*/
protected function setCurrentRequestForRoute($path, $route_name) {
$request = Request::create($path);
$request->attributes->set(RouteObjectInterface::ROUTE_NAME, $route_name);
$request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route($path));
$this->container->get('request_stack')->push($request);
}
}

View file

@ -0,0 +1,182 @@
<?php
/**
* @file
* Contains \Drupal\language\Tests\LanguageNegotiationLanguageContentEntityTest.
*/
namespace Drupal\language\Tests;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Language\LanguageInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
use Drupal\simpletest\WebTestBase;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Tests language negotiation with the language negotiator content entity.
*
* @group language
*/
class LanguageNegotiationContentEntityTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language', 'language_test', 'entity_test', 'system');
/**
* The entity being used for testing.
*
* @var \Drupal\Core\Entity\ContentEntityInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
ConfigurableLanguage::create(['id' => 'es'])->save();
ConfigurableLanguage::create(['id' => 'fr'])->save();
// In order to reflect the changes for a multilingual site in the container
// we have to rebuild it.
$this->rebuildContainer();
$this->createTranslatableEntity();
$user = $this->drupalCreateUser(array('view test entity'));
$this->drupalLogin($user);
}
/**
* Tests default with content language remaining same as interface language.
*/
public function testDefaultConfiguration() {
$translation = $this->entity;
$this->drupalGet($translation->urlInfo());
$last = $this->container->get('state')->get('language_test.language_negotiation_last');
$last_content_language = $last[LanguageInterface::TYPE_CONTENT];
$last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
$this->assertTrue(($last_interface_language == $last_content_language) && ($last_content_language == $translation->language()->getId()), new FormattableMarkup('Interface language %interface_language and Content language %content_language are the same as the translation language %translation_language of the entity.', ['%interface_language' => $last_interface_language, '%content_language' => $last_content_language, '%translation_language' => $translation->language()->getId()]));
$translation = $this->entity->getTranslation('es');
$this->drupalGet($translation->urlInfo());
$last = $this->container->get('state')->get('language_test.language_negotiation_last');
$last_content_language = $last[LanguageInterface::TYPE_CONTENT];
$last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
$this->assertTrue(($last_interface_language == $last_content_language) && ($last_content_language == $translation->language()->getId()), new FormattableMarkup('Interface language %interface_language and Content language %content_language are the same as the translation language %translation_language of the entity.', ['%interface_language' => $last_interface_language, '%content_language' => $last_content_language, '%translation_language' => $translation->language()->getId()]));
$translation = $this->entity->getTranslation('fr');
$this->drupalGet($translation->urlInfo());
$last = $this->container->get('state')->get('language_test.language_negotiation_last');
$last_content_language = $last[LanguageInterface::TYPE_CONTENT];
$last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
$this->assertTrue(($last_interface_language == $last_content_language) && ($last_content_language == $translation->language()->getId()), new FormattableMarkup('Interface language %interface_language and Content language %content_language are the same as the translation language %translation_language of the entity.', ['%interface_language' => $last_interface_language, '%content_language' => $last_content_language, '%translation_language' => $translation->language()->getId()]));
}
/**
* Tests enabling the language negotiator language_content_entity.
*/
public function testEnabledLanguageContentNegotiator() {
// Define the method language-url with a higher priority than
// language-content-entity. This configuration should match the default one,
// where the language-content-entity is turned off.
$config = $this->config('language.types');
$config->set('configurable', [LanguageInterface::TYPE_INTERFACE, LanguageInterface::TYPE_CONTENT]);
$config->set('negotiation.language_content.enabled', [
LanguageNegotiationUrl::METHOD_ID => 0,
LanguageNegotiationContentEntity::METHOD_ID => 1
]);
$config->save();
// In order to reflect the changes for a multilingual site in the container
// we have to rebuild it.
$this->rebuildContainer();
// The tests for the default configuration should still pass.
$this->testDefaultConfiguration();
// Define the method language-content-entity with a higher priority than
// language-url.
$config->set('negotiation.language_content.enabled', [
LanguageNegotiationContentEntity::METHOD_ID => 0,
LanguageNegotiationUrl::METHOD_ID => 1
]);
$config->save();
// In order to reflect the changes for a multilingual site in the container
// we have to rebuild it.
$this->rebuildContainer();
// The method language-content-entity should run before language-url and
// append query parameter for the content language and prevent language-url
// from overwriting the URL.
$default_site_langcode = $this->config('system.site')->get('default_langcode');
// Now switching to an entity route, so that the URL links are generated
// while being on an entity route.
$this->setCurrentRequestForRoute('/entity_test/{entity_test}', 'entity.entity_test.canonical');
$translation = $this->entity;
$this->drupalGet($translation->urlInfo());
$last = $this->container->get('state')->get('language_test.language_negotiation_last');
$last_content_language = $last[LanguageInterface::TYPE_CONTENT];
$last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
$this->assertTrue(($last_interface_language == $default_site_langcode) && ($last_interface_language == $last_content_language) && ($last_content_language == $translation->language()->getId()), 'Interface language and Content language are the same as the default translation language of the entity.');
$this->assertTrue($last_interface_language == $default_site_langcode, 'Interface language did not change from the default site language.');
$this->assertTrue($last_content_language == $translation->language()->getId(), 'Content language matches the current entity translation language.');
$translation = $this->entity->getTranslation('es');
$this->drupalGet($translation->urlInfo());
$last = $this->container->get('state')->get('language_test.language_negotiation_last');
$last_content_language = $last[LanguageInterface::TYPE_CONTENT];
$last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
$this->assertTrue($last_interface_language == $default_site_langcode, 'Interface language did not change from the default site language.');
$this->assertTrue($last_content_language == $translation->language()->getId(), 'Content language matches the current entity translation language.');
$translation = $this->entity->getTranslation('fr');
$this->drupalGet($translation->urlInfo());
$last = $this->container->get('state')->get('language_test.language_negotiation_last');
$last_content_language = $last[LanguageInterface::TYPE_CONTENT];
$last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
$this->assertTrue($last_interface_language == $default_site_langcode, 'Interface language did not change from the default site language.');
$this->assertTrue($last_content_language == $translation->language()->getId(), 'Content language matches the current entity translation language.');
}
/**
* Creates a translated entity.
*/
protected function createTranslatableEntity() {
$this->entity = EntityTest::create();
$this->entity->addTranslation('es', ['name' => 'name spanish']);
$this->entity->addTranslation('fr', ['name' => 'name french']);
$this->entity->save();
}
/**
* Sets the current request to a specific path with the corresponding route.
*
* @param string $path
* The path for which the current request should be created.
* @param string $route_name
* The route name for which the route object for the request should be
* created.
*/
protected function setCurrentRequestForRoute($path, $route_name) {
$request = Request::create($path);
$request->attributes->set(RouteObjectInterface::ROUTE_NAME, $route_name);
$request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route($path));
$this->container->get('request_stack')->push($request);
}
}

View file

@ -84,7 +84,7 @@ class LanguageSelectorTranslatableTest extends WebTestBase {
$this->drupalGet($path);
// Get en language from selector.
$elements = $this->xpath('//select[@id=:id]//option[@value=:option]', array(':id' => 'edit-settings-node-node-settings-language-langcode', ':option' => 'en'));
$elements = $this->xpath('//select[@id=:id]//option[@value=:option]', array(':id' => 'edit-settings-user-user-settings-language-langcode', ':option' => 'en'));
// Check that the language text is translated.
$this->assertEqual((string) $elements[0], $name_translation, 'Checking the option string English is translated to Spanish.');

View file

@ -221,8 +221,13 @@ class LanguageUILanguageNegotiationTest extends WebTestBase {
// Unknown language prefix should return 404.
$definitions = \Drupal::languageManager()->getNegotiator()->getNegotiationMethods();
// Enable only methods, which are either not limited to a specific language
// type or are supporting the interface language type.
$language_interface_method_definitions = array_filter($definitions, function ($method_definition) {
return !isset($method_definition['types']) || (isset($method_definition['types']) && in_array(LanguageInterface::TYPE_INTERFACE, $method_definition['types']));
});
$this->config('language.types')
->set('negotiation.' . LanguageInterface::TYPE_INTERFACE . '.enabled', array_flip(array_keys($definitions)))
->set('negotiation.' . LanguageInterface::TYPE_INTERFACE . '.enabled', array_flip(array_keys($language_interface_method_definitions)))
->save();
$this->drupalGet("$langcode_unknown/admin/config", array(), $http_header_browser_fallback);
$this->assertResponse(404, "Unknown language path prefix should return 404");

View file

@ -1,24 +1,25 @@
{#
/**
* @file
* Default theme implementation for a language negotiation configuration form.
*
* Available variables:
* - language_types: A list of language negotiation types. Each language type
* contains the following:
* - type: The machine name for the negotiation type.
* - title: The language negotiation type name.
* - description: A description for how the language negotiation type operates.
* - configurable: A radio element to toggle the table.
* - table: A draggable table for the language detection methods of this type.
* - children: Remaining form items for the group.
* - attributes: A list of HTML attributes for the wrapper element.
* - children: Remaining form items for all groups.
*
* @see template_preprocess_language_negotiation_configure_form()
*
* @ingroup themeable
*/
* @file
* Default theme implementation for a language negotiation configuration form.
*
* Available variables:
* - language_types: A list of language negotiation types. Each language type
* contains the following:
* - type: The machine name for the negotiation type.
* - title: The language negotiation type name.
* - description: A description for how the language negotiation type
* operates.
* - configurable: A radio element to toggle the table.
* - table: A draggable table for the language detection methods of this type.
* - children: Remaining form items for the group.
* - attributes: A list of HTML attributes for the wrapper element.
* - children: Remaining form items for all groups.
*
* @see template_preprocess_language_negotiation_configure_form()
*
* @ingroup themeable
*/
#}
{% for language_type in language_types %}
{%

View file

@ -121,25 +121,30 @@ class LinkItem extends FieldItemBase implements LinkItemInterface {
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
// Set of possible top-level domains.
$tlds = array('com', 'net', 'gov', 'org', 'edu', 'biz', 'info');
// Set random length for the domain name.
$domain_length = mt_rand(7, 15);
$random = new Random();
if ($field_definition->getItemDefinition()->getSetting('link_type') & LinkItemInterface::LINK_EXTERNAL) {
// Set of possible top-level domains.
$tlds = array('com', 'net', 'gov', 'org', 'edu', 'biz', 'info');
// Set random length for the domain name.
$domain_length = mt_rand(7, 15);
switch ($field_definition->getSetting('title')) {
case DRUPAL_DISABLED:
$values['title'] = '';
break;
case DRUPAL_REQUIRED:
$values['title'] = $random->sentences(4);
break;
case DRUPAL_OPTIONAL:
// In case of optional title, randomize its generation.
$values['title'] = mt_rand(0,1) ? $random->sentences(4) : '';
break;
switch ($field_definition->getSetting('title')) {
case DRUPAL_DISABLED:
$values['title'] = '';
break;
case DRUPAL_REQUIRED:
$values['title'] = $random->sentences(4);
break;
case DRUPAL_OPTIONAL:
// In case of optional title, randomize its generation.
$values['title'] = mt_rand(0, 1) ? $random->sentences(4) : '';
break;
}
$values['uri'] = 'http://www.' . $random->word($domain_length) . '.' . $tlds[mt_rand(0, (sizeof($tlds) - 1))];
}
else {
$values['uri'] = 'base:' . $random->name(mt_rand(1, 64));
}
$values['uri'] = 'http://www.' . $random->word($domain_length) . '.' . $tlds[mt_rand(0, (sizeof($tlds)-1))];
return $values;
}

View file

@ -7,6 +7,8 @@
namespace Drupal\link\Plugin\Validation\Constraint;
use Symfony\Component\Routing\Exception\InvalidParameterException;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidatorInterface;
@ -50,9 +52,17 @@ class LinkNotExistingInternalConstraintValidator implements ConstraintValidatorI
try {
$url->toString();
}
// The following exceptions are all possible during URL generation, and
// should be considered as disallowed URLs.
catch (RouteNotFoundException $e) {
$allowed = FALSE;
}
catch (InvalidParameterException $e) {
$allowed = FALSE;
}
catch (MissingMandatoryParametersException $e) {
$allowed = FALSE;
}
if (!$allowed) {
$this->context->addViolation($constraint->message, array('@uri' => $value->uri));
}

View file

@ -129,8 +129,6 @@ class LinkFieldTest extends WebTestBase {
'entity:user/1' => '- Restricted access - (1)',
// URI for an entity that doesn't exist, but with a valid ID.
'entity:user/999999' => 'entity:user/999999',
// URI for an entity that doesn't exist, with an invalid ID.
'entity:user/invalid-parameter' => 'entity:user/invalid-parameter',
);
// Define some invalid URLs.
@ -146,6 +144,8 @@ class LinkFieldTest extends WebTestBase {
$invalid_internal_entries = array(
'no-leading-slash' => $validation_error_2,
'entity:non_existing_entity_type/yar' => $validation_error_1,
// URI for an entity that doesn't exist, with an invalid ID.
'entity:user/invalid-parameter' => $validation_error_1,
);
// Test external and internal URLs for 'link_type' = LinkItemInterface::LINK_GENERIC.

View file

@ -10,7 +10,10 @@ namespace Drupal\link\Tests;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Tests\FieldUnitTestBase;
use Drupal\link\LinkItemInterface;
/**
* Tests the new entity API for the link field type.
@ -29,17 +32,40 @@ class LinkItemTest extends FieldUnitTestBase {
protected function setUp() {
parent::setUp();
// Create a link field for validation.
entity_create('field_storage_config', array(
// Create a generic, external, and internal link fields for validation.
FieldStorageConfig::create([
'field_name' => 'field_test',
'entity_type' => 'entity_test',
'type' => 'link',
))->save();
entity_create('field_config', array(
])->save();
FieldConfig::create([
'entity_type' => 'entity_test',
'field_name' => 'field_test',
'bundle' => 'entity_test',
))->save();
'settings' => ['link_type' => LinkItemInterface::LINK_GENERIC],
])->save();
FieldStorageConfig::create([
'field_name' => 'field_test_external',
'entity_type' => 'entity_test',
'type' => 'link',
])->save();
FieldConfig::create([
'entity_type' => 'entity_test',
'field_name' => 'field_test_external',
'bundle' => 'entity_test',
'settings' => ['link_type' => LinkItemInterface::LINK_EXTERNAL],
])->save();
FieldStorageConfig::create([
'field_name' => 'field_test_internal',
'entity_type' => 'entity_test',
'type' => 'link',
])->save();
FieldConfig::create([
'entity_type' => 'entity_test',
'field_name' => 'field_test_internal',
'bundle' => 'entity_test',
'settings' => ['link_type' => LinkItemInterface::LINK_INTERNAL],
])->save();
}
/**
@ -130,9 +156,12 @@ class LinkItemTest extends FieldUnitTestBase {
$entity->field_test[0] = NULL;
$this->assertNull($entity->field_test[0]->getValue());
// Test the generateSampleValue() method.
// Test the generateSampleValue() method for generic, external, and internal
// link types.
$entity = entity_create('entity_test');
$entity->field_test->generateSampleItems();
$entity->field_test_external->generateSampleItems();
$entity->field_test_internal->generateSampleItems();
$this->entityValidateAndSave($entity);
}

View file

@ -78,6 +78,10 @@ class LocaleConfigTranslationImportTest extends WebTestBase {
$this->container->get('module_installer')->install(['block', 'config_translation']);
$this->resetAll();
// The testing profile overrides locale.settings to disable translation
// import. Test that this override is in place.
$this->assertFalse($this->config('locale.settings')->get('translation.import_enabled'), 'Translations imports are disabled by default in the Testing profile.');
$admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'administer permissions', 'translate configuration'));
$this->drupalLogin($admin_user);

View file

@ -144,6 +144,66 @@ class LocalePluralFormatTest extends WebTestBase {
}
}
/**
* Tests plural editing of DateFormatter strings
*/
public function testPluralEditDateFormatter() {
// Import some .po files with formulas to set up the environment.
// These will also add the languages to the system.
$this->importPoFile($this->getPoFileWithSimplePlural(), array(
'langcode' => 'fr',
));
// Set French as the site default language.
$this->config('system.site')->set('default_langcode', 'fr')->save();
// Visit User Info page before updating translation strings.
$this->drupalGet('user');
// Member for time should be translated.
$this->assertText("seconde", "'Member for' text is translated.");
$path = 'admin/config/regional/translate/';
$search = array(
'langcode' => 'fr',
// Limit to only translated strings to ensure that database ordering does
// not break the test.
'translation' => 'translated',
);
$this->drupalPostForm($path, $search, t('Filter'));
// Plural values for the langcode fr.
$this->assertText('@count seconde');
$this->assertText('@count secondes');
// Inject a plural source string to the database. We need to use a specific
// langcode here because the language will be English by default and will
// not save our source string for performance optimization if we do not ask
// specifically for a language.
\Drupal::translation()->formatPlural(1, '1 second', '@count seconds', array(), array('langcode' => 'fr'))->render();
$lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", array(':source' => "1 second" . LOCALE_PLURAL_DELIMITER . "@count seconds"))->fetchField();
// Look up editing page for this plural string and check fields.
$search = array(
'string' => '1 second',
'langcode' => 'fr',
);
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
// Save complete translations for the string in langcode fr.
$edit = array(
"strings[$lid][translations][0]" => '1 seconde updated',
"strings[$lid][translations][1]" => '@count secondes updated',
);
$this->drupalPostForm($path, $edit, t('Save translations'));
// User interface input for translating seconds should not be duplicated
$this->assertUniqueText('@count seconds', 'Interface translation input for @count seconds only appears once.');
// Member for time should be translated.
$this->drupalGet('user');
$this->assertText("seconde", "'Member for' text is translated.");
}
/**
* Tests plural editing and export functionality.
*/
@ -308,6 +368,11 @@ msgid_plural "@count hours"
msgstr[0] "@count heure"
msgstr[1] "@count heures"
msgid "1 second"
msgid_plural "@count seconds"
msgstr[0] "@count seconde"
msgstr[1] "@count secondes"
msgid "Monday"
msgstr "lundi"
EOF;

View file

@ -0,0 +1,42 @@
<?php
/**
* @file
* Contains \Drupal\menu_link_content\Tests\Migrate\MigrateMenuLinkContentStubTest.
*/
namespace Drupal\menu_link_content\Tests\Migrate;
use Drupal\migrate_drupal\Tests\MigrateDrupalTestBase;
use Drupal\migrate_drupal\Tests\StubTestTrait;
/**
* Test stub creation for menu link content entities.
*
* @group menu_link_content
*/
class MigrateMenuLinkContentStubTest extends MigrateDrupalTestBase {
use StubTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['menu_link_content', 'link'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('menu_link_content');
}
/**
* Tests creation of menu link content stubs.
*/
public function testStub() {
$this->performStubTest('menu_link_content');
}
}

View file

@ -401,8 +401,8 @@ class MigrateExecutable implements MigrateExecutableInterface {
$multiple = $multiple || $plugin->multiple();
}
}
// No plugins means do not set.
if ($plugins) {
// No plugins or no value means do not set.
if ($plugins && !is_null($value)) {
$row->setDestinationProperty($destination, $value);
}
// Reset the value.

View file

@ -44,10 +44,14 @@ interface MigrateSourceInterface extends \Countable, \Iterator, PluginInspection
public function __toString();
/**
* Get the source ids.
* Defines the source fields uniquely identifying a source row. None of these
* fields should contain a NULL value - if necessary, use prepareRow() or
* hook_migrate_prepare_row() to rewrite NULL values to appropriate empty
* values (such as '' or 0).
*
* @return array
* The source ids.
* Array keyed by source field name, with values being a schema array
* describing the field (such as ['type' => 'string]).
*/
public function getIds();

View file

@ -136,19 +136,6 @@ abstract class Entity extends DestinationBase implements ContainerFactoryPluginI
return $row->getDestinationProperty($this->getKey('id'));
}
/**
* Process the stub values.
*
* @param \Drupal\migrate\Row $row
* The row of data.
*/
protected function processStubRow(Row $row) {
$bundle_key = $this->getKey('bundle');
if ($bundle_key && empty($row->getDestinationProperty($bundle_key))) {
$row->setDestinationProperty($bundle_key, reset($this->bundles));
}
}
/**
* Returns a specific entity key.
*

View file

@ -7,12 +7,17 @@
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Component\Utility\Random;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\link\LinkItemInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -29,6 +34,13 @@ class EntityContentBase extends Entity {
*/
protected $entityManager;
/**
* Field type plugin manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* Constructs a content entity.
*
@ -46,10 +58,13 @@ class EntityContentBase extends Entity {
* The list of bundles this entity type has.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles);
$this->entityManager = $entity_manager;
$this->fieldTypeManager = $field_type_manager;
}
/**
@ -64,7 +79,8 @@ class EntityContentBase extends Entity {
$migration,
$container->get('entity.manager')->getStorage($entity_type),
array_keys($container->get('entity.manager')->getBundleInfo($entity_type)),
$container->get('entity.manager')
$container->get('entity.manager'),
$container->get('plugin.manager.field.field_type')
);
}
@ -74,6 +90,9 @@ class EntityContentBase extends Entity {
public function import(Row $row, array $old_destination_id_values = array()) {
$this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
$entity = $this->getEntity($row, $old_destination_id_values);
if (!$entity) {
throw new MigrateException('Unable to get entity');
}
return $this->save($entity, $old_destination_id_values);
}
@ -132,4 +151,47 @@ class EntityContentBase extends Entity {
$this->setRollbackAction($row->getIdMap());
}
/**
* Do as much population of the stub row as we can.
*
* @param \Drupal\migrate\Row $row
* The row of data.
*/
protected function processStubRow(Row $row) {
$bundle_key = $this->getKey('bundle');
if ($bundle_key && empty($row->getDestinationProperty($bundle_key))) {
if (empty($this->bundles)) {
throw new MigrateException('Stubbing failed, no bundles available for entity type: ' . $this->storage->getEntityTypeId());
}
$row->setDestinationProperty($bundle_key, reset($this->bundles));
}
// Populate any required fields not already populated.
$fields = $this->entityManager
->getFieldDefinitions($this->storage->getEntityTypeId(), $bundle_key);
foreach ($fields as $field_name => $field_definition) {
if ($field_definition->isRequired() && is_null($row->getDestinationProperty($field_name))) {
// Use the configured default value for this specific field, if any.
if ($default_value = $field_definition->getDefaultValueLiteral()) {
$values[] = $default_value;
}
else {
// Otherwise, ask the field type to generate a sample value.
$field_type = $field_definition->getType();
/** @var \Drupal\Core\Field\FieldItemInterface $field_type_class */
$field_type_class = $this->fieldTypeManager
->getPluginClass($field_definition->getType());
$values = $field_type_class::generateSampleValue($field_definition);
if (is_null($values)) {
// Handle failure to generate a sample value.
throw new MigrateException('Stubbing failed, unable to generate value for field ' . $field_name);
break;
}
}
$row->setDestinationProperty($field_name, $values);
}
}
}
}

View file

@ -287,6 +287,7 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
foreach ($this->migration->getSourcePlugin()->getIds() as $id_definition) {
$mapkey = 'sourceid' . $count++;
$source_id_schema[$mapkey] = $this->getFieldSchema($id_definition);
$source_id_schema[$mapkey]['not null'] = TRUE;
// With InnoDB, utf8mb4-based primary keys can't be over 191 characters.
// Use ASCII-based primary keys instead.

View file

@ -136,6 +136,7 @@ abstract class SourcePluginBase extends PluginBase implements MigrateSourceInter
$this->skipCount = !empty($configuration['skip_count']);
$this->cacheKey = !empty($configuration['cache_key']) ? !empty($configuration['cache_key']) : NULL;
$this->trackChanges = !empty($configuration['track_changes']) ? $configuration['track_changes'] : FALSE;
$this->idMap = $this->migration->getIdMap();
// Pull out the current highwater mark if we have a highwater property.
if ($this->highWaterProperty = $this->migration->get('highWaterProperty')) {
@ -256,7 +257,6 @@ abstract class SourcePluginBase extends PluginBase implements MigrateSourceInter
* source records.
*/
public function rewind() {
$this->idMap = $this->migration->getIdMap();
$this->getIterator()->rewind();
$this->next();
}

View file

@ -73,7 +73,7 @@ class Row {
*
* @see getRawDestination()
*/
protected $rawDestination;
protected $rawDestination = [];
/**
* TRUE when this row is a stub.
@ -222,6 +222,17 @@ class Row {
NestedArray::setValue($this->destination, explode(static::PROPERTY_SEPARATOR, $property), $value, TRUE);
}
/**
* Removes destination property.
*
* @param string $property
* The name of the destination property.
*/
public function removeDestinationProperty($property) {
unset($this->rawDestination[$property]);
NestedArray::unsetValue($this->destination, explode(static::PROPERTY_SEPARATOR, $property));
}
/**
* Returns the whole destination array.
*

View file

@ -9,8 +9,12 @@ namespace Drupal\Tests\migrate\Unit;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
/**
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
@ -128,6 +132,7 @@ class MigrateSourceTest extends MigrateTestCase {
}
/**
* @covers ::__construct
* @expectedException \Drupal\migrate\MigrateException
*/
public function testHighwaterTrackChangesIncompatible() {
@ -138,6 +143,8 @@ class MigrateSourceTest extends MigrateTestCase {
/**
* Test that the source count is correct.
*
* @covers ::count
*/
public function testCount() {
// Mock the cache to validate set() receives appropriate arguments.
@ -221,6 +228,144 @@ class MigrateSourceTest extends MigrateTestCase {
$this->assertTrue(is_a($source->current(), 'Drupal\migrate\Row'), 'Incoming row timestamp is greater than current highwater mark so we have a row.');
}
/**
* Test basic row preparation.
*
* @covers ::prepareRow
*/
public function testPrepareRow() {
$this->migrationConfiguration['id'] = 'test_migration';
// Get a new migration with an id.
$migration = $this->getMigration();
$source = new StubSourcePlugin([], '', [], $migration);
$row = new Row([], []);
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
$module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$source->setModuleHandler($module_handler->reveal());
// Ensure we don't log this to the mapping table.
$this->idMap->expects($this->never())
->method('saveIdMapping');
$this->assertTrue($source->prepareRow($row));
// Track_changes...
$source = new StubSourcePlugin(['track_changes' => TRUE], '', [], $migration);
$row2 = $this->prophesize(Row::class);
$row2->rehash()
->shouldBeCalled();
$module_handler->invokeAll('migrate_prepare_row', [$row2, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row2, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$source->setModuleHandler($module_handler->reveal());
$this->assertTrue($source->prepareRow($row2->reveal()));
}
/**
* Test that global prepare hooks can skip rows.
*
* @covers ::prepareRow
*/
public function testPrepareRowGlobalPrepareSkip() {
$this->migrationConfiguration['id'] = 'test_migration';
$migration = $this->getMigration();
$source = new StubSourcePlugin([], '', [], $migration);
$row = new Row([], []);
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
// Return a failure from a prepare row hook.
$module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, FALSE, TRUE])
->shouldBeCalled();
$module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$source->setModuleHandler($module_handler->reveal());
$this->idMap->expects($this->once())
->method('saveIdMapping')
->with($row, [], MigrateIdMapInterface::STATUS_IGNORED);
$this->assertFalse($source->prepareRow($row));
}
/**
* Test that migrate specific prepare hooks can skip rows.
*
* @covers ::prepareRow
*/
public function testPrepareRowMigratePrepareSkip() {
$this->migrationConfiguration['id'] = 'test_migration';
$migration = $this->getMigration();
$source = new StubSourcePlugin([], '', [], $migration);
$row = new Row([], []);
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
// Return a failure from a prepare row hook.
$module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, FALSE, TRUE])
->shouldBeCalled();
$source->setModuleHandler($module_handler->reveal());
$this->idMap->expects($this->once())
->method('saveIdMapping')
->with($row, [], MigrateIdMapInterface::STATUS_IGNORED);
$this->assertFalse($source->prepareRow($row));
}
/**
* Test that a skip exception during prepare hooks correctly skips.
*
* @covers ::prepareRow
*/
public function testPrepareRowPrepareException() {
$this->migrationConfiguration['id'] = 'test_migration';
$migration = $this->getMigration();
$source = new StubSourcePlugin([], '', [], $migration);
$row = new Row([], []);
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
// Return a failure from a prepare row hook.
$module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
->willThrow(new MigrateSkipRowException())
->shouldBeCalled();
$source->setModuleHandler($module_handler->reveal());
// This will only be called on the first prepare because the second
// explicitly avoids it.
$this->idMap->expects($this->once())
->method('saveIdMapping')
->with($row, [], MigrateIdMapInterface::STATUS_IGNORED);
$this->assertFalse($source->prepareRow($row));
// Throw an exception the second time that avoids mapping.
$e = new MigrateSkipRowException('', FALSE);
$module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
->willThrow($e)
->shouldBeCalled();
$this->assertFalse($source->prepareRow($row));
}
/**
* Get a mock executable for the test.
*
@ -239,3 +384,46 @@ class MigrateSourceTest extends MigrateTestCase {
}
}
/**
* Stubbed source plugin for testing base class implementations.
*/
class StubSourcePlugin extends SourcePluginBase {
/**
* Helper for setting internal module handler implementation.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
*/
function setModuleHandler(ModuleHandlerInterface $module_handler) {
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [];
}
/**
* {@inheritdoc}
*/
public function __toString() {
return '';
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [];
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
return [];
}
}

View file

@ -19,6 +19,11 @@ abstract class MigrateTestCase extends UnitTestCase {
protected $migrationConfiguration = [];
/**
* @var \Drupal\migrate\Plugin\MigrateIdMapInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $idMap;
/**
* Local store for mocking setStatus()/getStatus().
*

View file

@ -0,0 +1,118 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\Plugin\migrate\destination\EntityContentBaseTest
*/
namespace Drupal\Tests\migrate\Unit\Plugin\migrate\destination;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
use Drupal\Tests\UnitTestCase;
/**
* Tests base entity migration destination functionality.
*
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\destination\EntityContentBase
* @group migrate
*/
class EntityContentBaseTest extends UnitTestCase {
/**
* @var \Drupal\migrate\Entity\MigrationInterface
*/
protected $migration;
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->migration = $this->prophesize(MigrationInterface::class);
$this->storage = $this->prophesize(EntityStorageInterface::class);
$this->entityManager = $this->prophesize(EntityManagerInterface::class);
}
/**
* Test basic entity save.
*
* @covers ::import
*/
public function testImport() {
$bundles = [];
$destination = new EntityTestDestination([], '', [],
$this->migration->reveal(),
$this->storage->reveal(),
$bundles,
$this->entityManager->reveal(),
$this->prophesize(FieldTypePluginManagerInterface::class)->reveal());
$entity = $this->prophesize(ContentEntityInterface::class);
// Assert that save is called.
$entity->save()
->shouldBeCalledTimes(1);
// Set an id for the entity
$entity->id()
->willReturn(5);
$destination->setEntity($entity->reveal());
// Ensure the id is saved entity id is returned from import.
$this->assertEquals([5], $destination->import(new Row([], [])));
// Assert that import set the rollback action.
$this->assertEquals(MigrateIdMapInterface::ROLLBACK_DELETE, $destination->rollbackAction());
}
/**
* Test row skipping when we can't get an entity to save.
*
* @covers ::import
* @expectedException \Drupal\migrate\MigrateException
* @expectedExceptionMessage Unable to get entity
*/
public function testImportEntityLoadFailure() {
$bundles = [];
$destination = new EntityTestDestination([], '', [],
$this->migration->reveal(),
$this->storage->reveal(),
$bundles,
$this->entityManager->reveal(),
$this->prophesize(FieldTypePluginManagerInterface::class)->reveal());
$destination->setEntity(FALSE);
$destination->import(new Row([], []));
}
}
/**
* Stub class for testing EntityContentBase methods.
*
* We want to test things without testing the base class implementations.
*/
class EntityTestDestination extends EntityContentBase {
private $entity = NULL;
public function setEntity($entity) {
$this->entity = $entity;
}
protected function getEntity(Row $row, array $old_destination_id_values) {
return $this->entity;
}
}

View file

@ -36,6 +36,11 @@ class EntityRevisionTest extends UnitTestCase {
*/
protected $entityManager;
/**
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
public function setUp() {
parent::setUp();
@ -43,6 +48,7 @@ class EntityRevisionTest extends UnitTestCase {
$this->migration = $this->prophesize('\Drupal\migrate\Entity\MigrationInterface');
$this->storage = $this->prophesize('\Drupal\Core\Entity\EntityStorageInterface');
$this->entityManager = $this->prophesize('\Drupal\Core\Entity\EntityManagerInterface');
$this->fieldTypeManager = $this->prophesize('\Drupal\Core\Field\FieldTypePluginManagerInterface');
}
/**
@ -183,7 +189,9 @@ class EntityRevisionTest extends UnitTestCase {
$this->migration->reveal(),
$this->storage->reveal(),
[],
$this->entityManager->reveal());
$this->entityManager->reveal(),
$this->fieldTypeManager->reveal()
);
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* @file
* Contains \Drupal\migrate_drupal\Tests\StubTestTrait.
*/
namespace Drupal\migrate_drupal\Tests;
use Drupal\migrate\Entity\Migration;
use Drupal\migrate\Row;
/**
* Provides common functionality for testing stubbing.
*/
trait StubTestTrait {
/**
* Test that creating a stub of the given entity type results in a valid
* entity.
*
* @param string $entity_type_id
* The entity type we are stubbing.
*/
protected function performStubTest($entity_type_id) {
$entity_id = $this->createStub($entity_type_id);
$this->assertTrue($entity_id, 'Stub successfully created');
if ($entity_id) {
$violations = $this->validateStub($entity_type_id, $entity_id);
if (!$this->assertIdentical(count($violations), 0, 'Stub is a valid entity')) {
foreach ($violations as $violation) {
$this->fail((string) $violation->getMessage());
}
}
}
}
/**
* Create a stub of the given entity type.
*
* @param string $entity_type_id
* The entity type we are stubbing.
*
* @return int
* ID of the created entity.
*/
protected function createStub($entity_type_id) {
// Create a dummy migration to pass to the destination plugin.
$config = [
'id' => 'dummy',
'migration_tags' => ['Stub test'],
'source' => ['plugin' => 'empty'],
'process' => [],
'destination' => ['plugin' => 'entity:' . $entity_type_id],
];
$migration = Migration::create($config);
$destination_plugin = $migration->getDestinationPlugin(TRUE);
$stub_row = new Row([], [], TRUE);
$destination_ids = $destination_plugin->import($stub_row);
return reset($destination_ids);
}
/**
* Perform validation on a stub entity.
*
* @param string $entity_type_id
* The entity type we are stubbing.
* @param string $entity_id
* ID of the stubbed entity to validate.
*
* @return \Drupal\Core\Entity\EntityConstraintViolationListInterface
* List of constraint violations identified.
*/
protected function validateStub($entity_type_id, $entity_id) {
$controller = \Drupal::entityManager()->getStorage($entity_type_id);
/** @var \Drupal\Core\Entity\ContentEntityInterface $stub_entity */
$stub_entity = $controller->load($entity_id);
return $stub_entity->validate();
}
}

View file

@ -41539,10 +41539,30 @@ $connection->insert('variable')
'name' => 'tracker_batch_size',
'value' => 'i:999;',
))
->values(array(
'name' => 'update_check_frequency',
'value' => 'i:1;',
))
->values(array(
'name' => 'update_fetch_url',
'value' => 's:23:"http://127.0.0.1/update";',
))
->values(array(
'name' => 'update_last_check',
'value' => 'i:1444944973;',
))
->values(array(
'name' => 'update_max_fetch_attempts',
'value' => 'i:3;',
))
->values(array(
'name' => 'update_notification_threshold',
'value' => 's:3:"all";',
))
->values(array(
'name' => 'update_notify_emails',
'value' => 'a:1:{i:0;s:19:"webmaster@127.0.0.1";}',
))
->values(array(
'name' => 'user_admin_role',
'value' => 's:1:"3";',

View file

@ -1064,6 +1064,14 @@ function node_query_node_access_alter(AlterableInterface $query) {
// Update the query for the given storage method.
\Drupal::service('node.grant_storage')->alterQuery($query, $tables, $op, $account, $base_table);
// Bubble the 'user.node_grants:$op' cache context to the current render
// context.
$renderer = \Drupal::service('renderer');
if ($renderer->hasRenderContext()) {
$build = ['#cache' => ['contexts' => ['user.node_grants:' . $op]]];
$renderer->render($build);
}
}
/**

View file

@ -47,6 +47,7 @@ entity.node.version_history:
_controller: '\Drupal\node\Controller\NodeController::revisionOverview'
requirements:
_access_node_revision: 'view'
node: \d+
options:
_node_operation_route: TRUE
@ -57,6 +58,7 @@ entity.node.revision:
_title_callback: '\Drupal\node\Controller\NodeController::revisionPageTitle'
requirements:
_access_node_revision: 'view'
node: \d+
node.revision_revert_confirm:
path: '/node/{node}/revisions/{node_revision}/revert'
@ -65,6 +67,7 @@ node.revision_revert_confirm:
_title: 'Revert to earlier revision'
requirements:
_access_node_revision: 'update'
node: \d+
options:
_node_operation_route: TRUE
@ -75,6 +78,7 @@ node.revision_revert_translation_confirm:
_title: 'Revert to earlier revision of a translation'
requirements:
_access_node_revision: 'update'
node: \d+
options:
_node_operation_route: TRUE
@ -85,6 +89,7 @@ node.revision_delete_confirm:
_title: 'Delete earlier revision'
requirements:
_access_node_revision: 'delete'
node: \d+
options:
_node_operation_route: TRUE

View file

@ -187,7 +187,7 @@ class NodeController extends ControllerBase implements ContainerInjectionInterfa
if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) {
$username = [
'#theme' => 'username',
'#account' => $revision->uid->entity,
'#account' => $revision->getRevisionAuthor(),
];
// Use revision link to link to revisions that are not active.

View file

@ -27,6 +27,7 @@ class NodeRouteProvider implements EntityRouteProviderInterface {
'_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);
@ -35,6 +36,7 @@ class NodeRouteProvider implements EntityRouteProviderInterface {
'_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);
@ -42,6 +44,7 @@ class NodeRouteProvider implements EntityRouteProviderInterface {
$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);

View file

@ -48,4 +48,32 @@ class NodeSelection extends DefaultSelection {
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(TRUE);
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;
}
}

View file

@ -199,7 +199,9 @@ class Node extends WizardPluginBase {
protected function buildFilters(&$form, FormStateInterface $form_state) {
parent::buildFilters($form, $form_state);
$selected_bundle = static::getSelected($form_state, array('show', 'type'), 'all', $form['displays']['show']['type']);
if (isset($form['displays']['show']['type'])) {
$selected_bundle = static::getSelected($form_state, array('show', 'type'), 'all', $form['displays']['show']['type']);
}
// Add the "tagged with" filter to the view.

Some files were not shown because too many files have changed in this diff Show more