Update to Drupal 8.0.5. For more information, see https://www.drupal.org/node/2679347

This commit is contained in:
Pantheon Automation 2016-03-02 12:40:24 -08:00 committed by Greg Anderson
parent 2a9f1f148d
commit fd3b12cf27
251 changed files with 5439 additions and 957 deletions

View file

@ -1,27 +0,0 @@
<?php
/**
* @file
* Hooks provided by the Actions module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Executes code after an action is deleted.
*
* @param $aid
* The action ID.
*/
function hook_action_delete($aid) {
db_delete('actions_assignments')
->condition('aid', $aid)
->execute();
}
/**
* @} End of "addtogroup hooks".
*/

View file

@ -88,7 +88,10 @@ class BookBreadcrumbBuilder implements BreadcrumbBuilderInterface {
$depth = 1;
while (!empty($book['p' . ($depth + 1)])) {
if (!empty($parent_books[$book['p' . $depth]]) && ($parent_book = $parent_books[$book['p' . $depth]])) {
if ($parent_book->access('view', $this->account)) {
$access = $parent_book->access('view', $this->account, TRUE);
$breadcrumb->addCacheableDependency($access);
if ($access->isAllowed()) {
$breadcrumb->addCacheableDependency($parent_book);
$links[] = Link::createFromRoute($parent_book->label(), 'entity.node.canonical', array('node' => $parent_book->id()));
}
}

View file

@ -397,7 +397,7 @@ class BookManager implements BookManagerInterface {
foreach ($tree as $data) {
if ($data['link']['depth'] > $depth_limit) {
// Don't iterate through any links on this level.
break;
return;
}
if (!in_array($data['link']['nid'], $exclude)) {
$nids[] = $data['link']['nid'];
@ -408,7 +408,8 @@ class BookManager implements BookManagerInterface {
foreach ($tree as $data) {
$nid = $data['link']['nid'];
if (in_array($nid, $exclude)) {
// Check for excluded or missing node.
if (empty($nodes[$nid])) {
continue;
}
$toc[$nid] = $indent . ' ' . Unicode::truncate($nodes[$nid]->label(), 30, TRUE, TRUE);

View file

@ -11,6 +11,7 @@ use Drupal\book\BookManagerInterface;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -91,7 +92,8 @@ class BookOutlineForm extends ContentEntityForm {
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->entity->book['original_bid'] ? $this->t('Update book outline') : $this->t('Add to book outline');
$actions['delete']['#value'] = $this->t('Remove from book outline');
$actions['delete']['#title'] = $this->t('Remove from book outline');
$actions['delete']['#url'] = new Url('entity.node.book_remove_form', ['node' => $this->entity->book['nid']]);
$actions['delete']['#access'] = $this->bookManager->checkNodeIsRemovable($this->entity);
return $actions;
}
@ -126,11 +128,4 @@ class BookOutlineForm extends ContentEntityForm {
}
}
/**
* {@inheritdoc}
*/
public function delete(array $form, FormStateInterface $form_state) {
$form_state->setRedirectUrl($this->entity->urlInfo('book-remove-form'));
}
}

View file

@ -0,0 +1,209 @@
<?php
/**
* @file
* Contains \Drupal\book\Tests\BookBreadcrumbTest.
*/
namespace Drupal\book\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Create a book, add pages, and test book interface.
*
* @group book
*/
class BookBreadcrumbTest extends WebTestBase {
/**
* Modules to install.
*
* @var array
*/
public static $modules = array('book', 'block', 'book_breadcrumb_test');
/**
* A book node.
*
* @var \Drupal\node\NodeInterface
*/
protected $book;
/**
* A user with permission to create and edit books.
*
* @var \Drupal\user\Entity\User
*/
protected $bookAuthor;
/**
* A user without the 'node test view' permission.
*
* @var \Drupal\user\UserInterface
*/
protected $webUserWithoutNodeAccess;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('page_title_block');
// Create users.
$this->bookAuthor = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books'));
$this->adminUser = $this->drupalCreateUser(array('create new books', 'create book content', 'edit any book content', 'delete any book content', 'add content to books', 'administer blocks', 'administer permissions', 'administer book outlines', 'administer content types', 'administer site configuration'));
}
/**
* Creates a new book with a page hierarchy.
*
* @return \Drupal\node\NodeInterface[]
* The created book nodes.
*/
protected function createBreadcrumbBook() {
// Create new book.
$this->drupalLogin($this->bookAuthor);
$this->book = $this->createBookNode('new');
$book = $this->book;
/*
* Add page hierarchy to book.
* Book
* |- Node 0
* |- Node 1
* |- Node 2
* |- Node 3
* |- Node 4
* |- Node 5
* |- Node 6
*/
$nodes = array();
$nodes[0] = $this->createBookNode($book->id());
$nodes[1] = $this->createBookNode($book->id(), $nodes[0]->id());
$nodes[2] = $this->createBookNode($book->id(), $nodes[0]->id());
$nodes[3] = $this->createBookNode($book->id(), $nodes[2]->id());
$nodes[4] = $this->createBookNode($book->id(), $nodes[3]->id());
$nodes[5] = $this->createBookNode($book->id(), $nodes[4]->id());
$nodes[6] = $this->createBookNode($book->id());
$this->drupalLogout();
return $nodes;
}
/**
* Creates a book node.
*
* @param int|string $book_nid
* A book node ID or set to 'new' to create a new book.
* @param int|null $parent
* (optional) Parent book reference ID. Defaults to NULL.
*
* @return \Drupal\node\NodeInterface
* The created node.
*/
protected function createBookNode($book_nid, $parent = NULL) {
// $number does not use drupal_static as it should not be reset since it
// uniquely identifies each call to createBookNode(). It is used to ensure
// that when sorted nodes stay in same order.
static $number = 0;
$edit = array();
$edit['title[0][value]'] = str_pad($number, 2, '0', STR_PAD_LEFT) . ' - SimpleTest test node ' . $this->randomMachineName(10);
$edit['body[0][value]'] = 'SimpleTest test body ' . $this->randomMachineName(32) . ' ' . $this->randomMachineName(32);
$edit['book[bid]'] = $book_nid;
if ($parent !== NULL) {
$this->drupalPostForm('node/add/book', $edit, t('Change book (update list of parents)'));
$edit['book[pid]'] = $parent;
$this->drupalPostForm(NULL, $edit, t('Save'));
// Make sure the parent was flagged as having children.
$parent_node = \Drupal::entityManager()->getStorage('node')->loadUnchanged($parent);
$this->assertFalse(empty($parent_node->book['has_children']), 'Parent node is marked as having children');
}
else {
$this->drupalPostForm('node/add/book', $edit, t('Save'));
}
// Check to make sure the book node was created.
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertNotNull(($node === FALSE ? NULL : $node), 'Book node found in database.');
$number++;
return $node;
}
/**
* Test that the breadcrumb is updated when book content changes.
*/
public function testBreadcrumbTitleUpdates() {
// Create a new book.
$nodes = $this->createBreadcrumbBook();
$book = $this->book;
$this->drupalLogin($this->bookAuthor);
$this->drupalGet($nodes[4]->toUrl());
// Fetch each node title in the current breadcrumb.
$links = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a');
$got_breadcrumb = array();
foreach ($links as $link) {
$got_breadcrumb[] = (string) $link;
}
// Home link and four parent book nodes should be in the breadcrumb.
$this->assertEqual(5, count($got_breadcrumb));
$this->assertEqual($nodes[3]->getTitle(), end($got_breadcrumb));
$edit = [
'title[0][value]' => 'Updated node5 title',
];
$this->drupalPostForm($nodes[3]->toUrl('edit-form'), $edit, 'Save');
$this->drupalGet($nodes[4]->toUrl());
// Fetch each node title in the current breadcrumb.
$links = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a');
$got_breadcrumb = array();
foreach ($links as $link) {
$got_breadcrumb[] = (string) $link;
}
$this->assertEqual(5, count($got_breadcrumb));
$this->assertEqual($edit['title[0][value]'], end($got_breadcrumb));
}
/**
* Test that the breadcrumb is updated when book access changes.
*/
public function testBreadcrumbAccessUpdates() {
// Create a new book.
$nodes = $this->createBreadcrumbBook();
$this->drupalLogin($this->bookAuthor);
$edit = [
'title[0][value]' => "you can't see me",
];
$this->drupalPostForm($nodes[3]->toUrl('edit-form'), $edit, 'Save');
$this->drupalGet($nodes[4]->toUrl());
$links = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a');
$got_breadcrumb = array();
foreach ($links as $link) {
$got_breadcrumb[] = (string) $link;
}
$this->assertEqual(5, count($got_breadcrumb));
$this->assertEqual($edit['title[0][value]'], end($got_breadcrumb));
$config = $this->container->get('config.factory')->getEditable('book_breadcrumb_test.settings');
$config->set('hide', TRUE)->save();
$this->drupalGet($nodes[4]->toUrl());
$links = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a');
$got_breadcrumb = array();
foreach ($links as $link) {
$got_breadcrumb[] = (string) $link;
}
$this->assertEqual(4, count($got_breadcrumb));
$this->assertEqual($nodes[2]->getTitle(), end($got_breadcrumb));
$this->drupalGet($nodes[3]->toUrl());
$this->assertResponse(403);
}
}

View file

@ -443,6 +443,60 @@ class BookTest extends WebTestBase {
$this->assertNoText($nodes[0]->label(), 'No links to individual book pages are displayed.');
}
/**
* Tests BookManager::getTableOfContents().
*/
public function testGetTableOfContents() {
// Create new book.
$nodes = $this->createBook();
$book = $this->book;
$this->drupalLogin($this->bookAuthor);
/*
* Add Node 5 under Node 2.
* Add Node 6, 7, 8, 9, 10, 11 under Node 3.
* Book
* |- Node 0
* |- Node 1
* |- Node 2
* |- Node 5
* |- Node 3
* |- Node 6
* |- Node 7
* |- Node 8
* |- Node 9
* |- Node 10
* |- Node 11
* |- Node 4
*/
foreach ([5 => 2, 6 => 3, 7 => 6, 8 => 7, 9 => 8, 10 => 9, 11 => 10] as $child => $parent) {
$nodes[$child] = $this->createBookNode($book->id(), $nodes[$parent]->id());
}
$this->drupalGet($nodes[0]->toUrl('edit-form'));
// Snice Node 0 has children 2 levels deep, nodes 10 and 11 should not
// appear in the selector.
$this->assertNoOption('edit-book-pid', $nodes[10]->id());
$this->assertNoOption('edit-book-pid', $nodes[11]->id());
// Node 9 should be available as an option.
$this->assertOption('edit-book-pid', $nodes[9]->id());
// Get a shallow set of options.
/** @var \Drupal\book\BookManagerInterface $manager */
$manager = $this->container->get('book.manager');
$options = $manager->getTableOfContents($book->id(), 3);
$expected_nids = [$book->id(), $nodes[0]->id(), $nodes[1]->id(), $nodes[2]->id(), $nodes[3]->id(), $nodes[6]->id(), $nodes[4]->id()];
$this->assertEqual(count($options), count($expected_nids));
$diff = array_diff($expected_nids, array_keys($options));
$this->assertTrue(empty($diff), 'Found all expected option keys');
// Exclude Node 3.
$options = $manager->getTableOfContents($book->id(), 3, array($nodes[3]->id()));
$expected_nids = array($book->id(), $nodes[0]->id(), $nodes[1]->id(), $nodes[2]->id(), $nodes[4]->id());
$this->assertEqual(count($options), count($expected_nids));
$diff = array_diff($expected_nids, array_keys($options));
$this->assertTrue(empty($diff), 'Found all expected option keys after excluding Node 3');
}
/**
* Tests the book navigation block when an access module is installed.
*/
@ -564,6 +618,7 @@ class BookTest extends WebTestBase {
$this->drupalGet('node/' . $empty_book->id() . '/outline');
$this->assertRaw(t('Book outline'));
$this->assertOptionSelected('edit-book-bid', 0, 'Node does not belong to a book');
$this->assertNoLink(t('Remove from book outline'));
$edit = array();
$edit['book[bid]'] = '1';
@ -583,6 +638,8 @@ class BookTest extends WebTestBase {
$this->drupalLogin($this->adminUser);
$this->drupalGet('node/' . $book->id() . '/outline');
$this->assertRaw(t('Book outline'));
$this->clickLink(t('Remove from book outline'));
$this->assertRaw(t('Are you sure you want to remove %title from the book hierarchy?', array('%title' => $book->label())));
// Create a new node and set the book after the node was created.
$node = $this->drupalCreateNode(array('type' => 'book'));

View file

@ -0,0 +1,6 @@
name: 'Book module breadcrumb tests'
type: module
description: 'Support module for book module breadcrumb testing.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,27 @@
<?php
/**
* @file
* Test module for testing the book module breadcrumb.
*/
use Drupal\Core\Access\AccessResultForbidden;
use Drupal\Core\Access\AccessResultNeutral;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
/**
* Implements hook_node_access().
*/
function book_breadcrumb_test_node_access(NodeInterface $node, $operation, AccountInterface $account) {
$config = \Drupal::config('book_breadcrumb_test.settings');
if ($config->get('hide') && $node->getTitle() == "you can't see me" && $operation == 'view') {
$access = new AccessResultForbidden();
}
else {
$access = new AccessResultNeutral();
}
$access->addCacheableDependency($config);
$access->addCacheableDependency($node);
return $access;
}

View file

@ -0,0 +1,9 @@
# Schema for the configuration files of the book_breadcrumb_test module.
book_breadcrumb_test.settings:
type: config_object
label: 'Book Breadcrumb Test module settings'
mapping:
hide:
type: boolean
label: 'Setting for hiding content'

View file

@ -18,15 +18,20 @@
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-') {
// Ignore data-cke-* attributes; they're CKEditor internals.
if (attributeName.indexOf('data-cke-') === 0) {
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;
}
// Remove any cke_* classes.
if (parsedAttributes.class) {
parsedAttributes.class = CKEDITOR.tools.trim(parsedAttributes.class.replace(/cke_\S+/, ''));
}
return parsedAttributes;
}

View file

@ -335,7 +335,7 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
// Collect languages included with CKEditor based on file listing.
$files = scandir('core/assets/vendor/ckeditor/lang');
foreach ($files as $file) {
if ($file[0] !== '.' && fnmatch('*.js', $file)) {
if ($file[0] !== '.' && preg_match('/\.js$/', $file)) {
$langcode = basename($file, '.js');
$langcodes[$langcode] = $langcode;
}

View file

@ -270,6 +270,7 @@ class CKEditorTest extends KernelTestBase {
$expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/base/elements.css'));
$expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/components/captions.css'));
$expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/components/table.css'));
$expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/components/text-formatted.css'));
$this->assertIdentical($expected, $this->ckeditor->buildContentsCssJSSetting($editor), '"contentsCss" configuration part of JS settings built correctly while a theme providing a CKEditor stylesheet exists.');
}

View file

@ -229,7 +229,7 @@ class ConfigDependencyTest extends EntityUnitTestBase {
// Set a more complicated test where dependencies will be fixed.
\Drupal::state()->set('config_test.fix_dependencies', array($entity1->getConfigDependencyName()));
\Drupal::state()->set('config_test.on_dependency_removal_called', []);
// Entity1 will be deleted because it depends on node.
$entity1 = $storage->create(
array(
@ -259,7 +259,8 @@ class ConfigDependencyTest extends EntityUnitTestBase {
$entity2->save();
// Entity3 will be unchanged because it is dependent on Entity2 which can
// be fixed.
// be fixed. The ConfigEntityInterface::onDependencyRemoval() method will
// not be called for this entity.
$entity3 = $storage->create(
array(
'id' => 'entity3',
@ -295,6 +296,10 @@ class ConfigDependencyTest extends EntityUnitTestBase {
$this->assertEqual($entity3->uuid(), reset($config_entities['unchanged'])->uuid(), 'Entity 3 is not changed.');
$this->assertEqual($entity4->uuid(), $config_entities['delete'][1]->uuid(), 'Entity 4 will be deleted.');
$called = \Drupal::state()->get('config_test.on_dependency_removal_called', []);
$this->assertFalse(in_array($entity3->id(), $called), 'ConfigEntityInterface::onDependencyRemoval() is not called for entity 3.');
$this->assertIdentical(['entity1', 'entity2', 'entity4'], $called, 'The most dependent entites have ConfigEntityInterface::onDependencyRemoval() called first.');
// Perform the uninstall.
$config_manager->uninstall('module', 'node');

View file

@ -70,6 +70,12 @@ class FileStorageTest extends ConfigStorageTestBase {
$config_files = $this->storage->listAll();
$this->assertIdentical($config_files, $expected_files, 'Relative path, two config files found.');
// @todo https://www.drupal.org/node/2666954 FileStorage::listAll() is
// case-sensitive. However, \Drupal\Core\Config\DatabaseStorage::listAll()
// is case-insensitive.
$this->assertIdentical(['system.performance'], $this->storage->listAll('system'), 'The FileStorage::listAll() with prefix works.');
$this->assertIdentical([], $this->storage->listAll('System'), 'The FileStorage::listAll() is case sensitive.');
// Initialize FileStorage with absolute file path.
$absolute_path = realpath($this->directory);
$storage_absolute_path = new FileStorage($absolute_path);

View file

@ -122,6 +122,11 @@ class ConfigTest extends ConfigEntityBase implements ConfigTestInterface {
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
// Record which entities have this method called on.
$called = \Drupal::state()->get('config_test.on_dependency_removal_called', []);
$called[] = $this->id();
\Drupal::state()->set('config_test.on_dependency_removal_called', $called);
$changed = parent::onDependencyRemoval($dependencies);
if (!isset($this->dependencies['enforced']['config'])) {
return $changed;

View file

@ -19,11 +19,14 @@ use Drupal\Core\Render\Element;
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field
* A field definition object.
* @param string $element_name
* (optional) The element name, which is added to drupalSettings so that
* javascript can manipulate the form element.
*
* @return array
* A form element to configure field synchronization.
*/
function content_translation_field_sync_widget(FieldDefinitionInterface $field) {
function content_translation_field_sync_widget(FieldDefinitionInterface $field, $element_name = 'third_party_settings[content_translation][translation_sync]') {
// No way to store field sync information on this field.
if (!($field instanceof ThirdPartySettingsInterface)) {
return array();
@ -33,15 +36,18 @@ function content_translation_field_sync_widget(FieldDefinitionInterface $field)
$definition = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field->getType());
$column_groups = $definition['column_groups'];
if (!empty($column_groups) && count($column_groups) > 1) {
$options = array();
$default = array();
$options = [];
$default = [];
$require_all_groups_for_translation = [];
foreach ($column_groups as $group => $info) {
$options[$group] = $info['label'];
$default[$group] = !empty($info['translatable']) ? $group : FALSE;
if (!empty($info['require_all_groups_for_translation'])) {
$require_all_groups_for_translation[] = $group;
}
}
$settings = array('dependent_selectors' => array('instance[third_party_settings][content_translation][translation_sync]' => array('file')));
$default = $field->getThirdPartySetting('content_translation', 'translation_sync', $default);
$element = array(
@ -49,15 +55,19 @@ function content_translation_field_sync_widget(FieldDefinitionInterface $field)
'#title' => t('Translatable elements'),
'#options' => $options,
'#default_value' => $default,
'#attached' => array(
'library' => array(
'content_translation/drupal.content_translation.admin',
),
'drupalSettings' => [
'contentTranslationDependentOptions' => $settings,
],
),
);
if ($require_all_groups_for_translation) {
// The actual checkboxes are sometimes rendered separately and the parent
// element is ignored. Attach to the first option to ensure that this
// does not get lost.
$element[key($options)]['#attached']['drupalSettings']['contentTranslationDependentOptions'] = [
'dependent_selectors' => [
$element_name => $require_all_groups_for_translation
],
];
$element[key($options)]['#attached']['library'][] = 'content_translation/drupal.content_translation.admin';
}
}
return $element;
@ -82,7 +92,6 @@ function _content_translation_form_language_content_settings_form_alter(array &$
$form['#attached']['library'][] = 'content_translation/drupal.content_translation.admin';
$dependent_options_settings = array();
$entity_manager = Drupal::entityManager();
foreach ($form['#labels'] as $entity_type_id => $label) {
$entity_type = $entity_manager->getDefinition($entity_type_id);
@ -110,13 +119,9 @@ function _content_translation_form_language_content_settings_form_alter(array &$
'#default_value' => $definition->isTranslatable(),
);
// Display the column translatability configuration widget.
$column_element = content_translation_field_sync_widget($definition);
$column_element = content_translation_field_sync_widget($definition, "settings[{$entity_type_id}][{$bundle}][columns][{$field_name}]");
if ($column_element) {
$form['settings'][$entity_type_id][$bundle]['columns'][$field_name] = $column_element;
// @todo This should not concern only files.
if (isset($column_element['#options']['file'])) {
$dependent_options_settings["settings[{$entity_type_id}][{$bundle}][columns][{$field_name}]"] = array('file');
}
}
}
}
@ -132,8 +137,6 @@ function _content_translation_form_language_content_settings_form_alter(array &$
}
}
$settings = array('dependent_selectors' => $dependent_options_settings);
$form['#attached']['drupalSettings']['contentTranslationDependentOptions'] = $settings;
$form['#validate'][] = 'content_translation_form_language_content_settings_validate';
$form['#submit'][] = 'content_translation_form_language_content_settings_submit';
}

View file

@ -31,7 +31,7 @@
// We're given a generic name to look for so we find all inputs containing
// that name and copy over the input values that require all columns to be
// translatable.
if (options.dependent_selectors) {
if (options && options.dependent_selectors) {
for (var field in options.dependent_selectors) {
if (options.dependent_selectors.hasOwnProperty(field)) {
$fields = $context.find('input[name^="' + field + '"]');

View file

@ -45,3 +45,19 @@ function content_translation_update_8001() {
/**
* @} End of "addtogroup updates-8.0.0-rc".
*/
/**
* @addtogroup updates-8.0.x
* @{
*/
/**
* Clear field type plugin caches to fix image field translatability.
*/
function content_translation_update_8002() {
\Drupal::service('plugin.manager.field.field_type')->clearCachedDefinitions();
}
/**
* @} End of "addtogroup updates-8.0.x".
*/

View file

@ -14,6 +14,7 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\field\FieldConfigInterface;
/**
* Implements hook_help().
@ -199,9 +200,17 @@ function content_translation_entity_base_field_info(EntityTypeInterface $entity_
* which columns should be synchronized across different translations and
* which are translatable. This is useful for instance to translate the
* "alt" and "title" textual elements of an image field, while keeping the
* same image on every translation.
* same image on every translation. Each group has the following keys:
* - title: Title of the column group.
* - translatable: (optional) If the column group should be translatable by
* default, defaults to FALSE.
* - columns: (optional) A list of columns of this group. Defaults to the
* name of he group as the single column.
* - require_all_groups_for_translation: (optional) Set to TRUE to enforce
* that making this column group translatable requires all others to be
* translatable too.
*
* @see Drupal\image\Plugin\Field\FieldType\imageItem.
* @see Drupal\image\Plugin\Field\FieldType\ImageItem
*/
function content_translation_field_info_alter(&$info) {
foreach ($info as $key => $settings) {

View file

@ -66,6 +66,17 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
// Retrieve all the untranslatable column groups and merge them into
// single list.
$groups = array_keys(array_diff($translation_sync, array_filter($translation_sync)));
// If a group was selected has the require_all_groups_for_translation
// flag set, there are no untranslatable columns. This is done because
// the UI adds Javascript that disables the other checkboxes, so their
// values are not saved.
foreach (array_filter($translation_sync) as $group) {
if (!empty($column_groups[$group]['require_all_groups_for_translation'])) {
$groups = [];
break;
}
}
if (!empty($groups)) {
$columns = array();
foreach ($groups as $group) {
@ -163,7 +174,16 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
continue;
}
// If a synchronized column has changed or has been created from
// scratch we need to override the full items array for all languages.
// scratch we need to replace the values for this language as a
// combination of the values that need to be synced from the source
// items and the other columns from the existing values. This only
// works if the delta exists in the language.
elseif ($created && !empty($original_field_values[$langcode][$delta])) {
$item_columns_to_sync = array_intersect_key($source_items[$delta], array_flip($columns));
$item_columns_to_keep = array_diff_key($original_field_values[$langcode][$delta], array_flip($columns));
$values[$langcode][$delta] = $item_columns_to_sync + $item_columns_to_keep;
}
// If the delta doesn't exist, copy from the source language.
elseif ($created) {
$values[$langcode][$delta] = $source_items[$delta];
}

View file

@ -201,7 +201,9 @@ class ContentTranslationSyncUnitTest extends KernelTestBase {
for ($delta = 0; $delta < $this->cardinality; $delta++) {
if ($delta_callback($delta)) {
foreach ($this->columns as $column) {
$field_values[$sync_langcode][$delta][$column] = $field_values[$sync_langcode][0][$column];
if (in_array($column, $this->synchronized)) {
$field_values[$sync_langcode][$delta][$column] = $field_values[$sync_langcode][0][$column];
}
}
}
}
@ -209,19 +211,18 @@ class ContentTranslationSyncUnitTest extends KernelTestBase {
$changed_items = $field_values[$sync_langcode];
$this->synchronizer->synchronizeItems($field_values, $unchanged_items, $sync_langcode, $this->langcodes, $this->synchronized);
$result = TRUE;
foreach ($this->unchangedFieldValues as $langcode => $unchanged_items) {
for ($delta = 0; $delta < $this->cardinality; $delta++) {
foreach ($this->columns as $column) {
// The first item is always unchanged hence it is retained by the
// synchronization process. The other ones are retained or synced
// depending on the logic implemented by the delta callback.
$value = $delta > 0 && $delta_callback($delta) ? $changed_items[0][$column] : $unchanged_items[$delta][$column];
$result = $result && ($field_values[$langcode][$delta][$column] == $value);
// depending on the logic implemented by the delta callback and
// whether it is a sync column or not.
$value = $delta > 0 && $delta_callback($delta) && in_array($column, $this->synchronized) ? $changed_items[0][$column] : $unchanged_items[$delta][$column];
$this->assertEqual($field_values[$langcode][$delta][$column], $value, "Item $delta column $column for langcode $langcode synced correctly");
}
}
}
$this->assertTrue($result, 'Multiple synced items have been correctly synchronized.');
}
}
@ -241,15 +242,16 @@ class ContentTranslationSyncUnitTest extends KernelTestBase {
$changed_items = $field_values[$sync_langcode];
$this->synchronizer->synchronizeItems($field_values, $unchanged_items, $sync_langcode, $this->langcodes, $this->synchronized);
$result = TRUE;
foreach ($this->unchangedFieldValues as $langcode => $unchanged_items) {
for ($delta = 0; $delta < $this->cardinality; $delta++) {
foreach ($this->columns as $column) {
$result = $result && ($field_values[$langcode][$delta][$column] == $changed_items[$delta][$column]);
// If the column is synchronized, the value should have been synced,
// for unsychronized columns, the value must not change.
$expected_value = in_array($column, $this->synchronized) ? $changed_items[$delta][$column] : $this->unchangedFieldValues[$langcode][$delta][$column];
$this->assertEqual($field_values[$langcode][$delta][$column], $expected_value, "Differing Item $delta column $column for langcode $langcode synced correctly");
}
}
}
$this->assertTrue($result, 'Differing synced columns have been correctly synchronized.');
}
}

View file

@ -9,7 +9,7 @@ namespace Drupal\dblog\Plugin\rest\resource;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
@ -36,7 +36,10 @@ class DBLogResource extends ResourceBase {
* @return \Drupal\rest\ResourceResponse
* The response containing the log entry.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* Thrown when the log entry was not found.
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Thrown when no log entry was provided.
*/
public function get($id = NULL) {
if ($id) {
@ -49,6 +52,6 @@ class DBLogResource extends ResourceBase {
throw new NotFoundHttpException(t('Log entry with ID @id was not found', array('@id' => $id)));
}
throw new HttpException(t('No log entry ID was provided'));
throw new BadRequestHttpException(t('No log entry ID was provided'));
}
}

View file

@ -59,5 +59,11 @@ class DbLogResourceTest extends RESTTestBase {
$this->assertResponse(404);
$decoded = Json::decode($response);
$this->assertEqual($decoded['error'], 'Log entry with ID 9999 was not found', 'Response message is correct.');
// Make a bad request (a true malformed request would never be a route match).
$response = $this->httpRequest(Url::fromRoute('rest.dblog.GET.' . $this->defaultFormat, ['id' => 0, '_format' => $this->defaultFormat]), 'GET');
$this->assertResponse(400);
$decoded = Json::decode($response);
$this->assertEqual($decoded['error'], 'No log entry ID was provided', 'Response message is correct.');
}
}

View file

@ -5,6 +5,7 @@
* Post update functions for Field module.
*/
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
@ -44,3 +45,34 @@ function field_post_update_entity_reference_handler_setting() {
/**
* @} End of "addtogroup updates-8.0.0-beta".
*/
/**
* @addtogroup updates-8.1.0
* @{
*/
/**
* Adds the 'size' setting for email widgets.
*/
function field_post_update_email_widget_size_setting() {
foreach (EntityFormDisplay::loadMultiple() as $entity_form_display) {
$changed = FALSE;
foreach ($entity_form_display->getComponents() as $name => $options) {
if (isset($options['type']) && $options['type'] === 'email_default') {
$options['settings']['size'] = '60';
$entity_form_display->setComponent($name, $options);
$changed = TRUE;
}
}
if ($changed) {
$entity_form_display->save();
}
}
return t('The new size setting for email widgets has been added.');
}
/**
* @} End of "addtogroup updates-8.1.0".
*/

View file

@ -51,14 +51,16 @@ class FieldInstanceSettings extends ProcessPluginBase {
case 'imagefield_widget':
$settings['file_extensions'] = $widget_settings['file_extensions'];
$settings['file_directory'] = 'public://';
$settings['file_directory'] = $widget_settings['file_path'];
$settings['max_filesize'] = $this->convertSizeUnit($widget_settings['max_filesize_per_file']);
$settings['alt_field'] = $widget_settings['alt'];
$settings['alt_field_required'] = $widget_settings['custom_alt'];
$settings['title_field'] = $widget_settings['title'];
$settings['title_field_required'] = $widget_settings['custom_title'];
$settings['max_resolution'] = $widget_settings['max_resolution'];
$settings['min_resolution'] = $widget_settings['min_resolution'];
// With nothing entered for min or max resolution in Drupal 6, zero is
// stored. For Drupal 8 this should be an empty string.
$settings['max_resolution'] = !empty($widget_settings['max_resolution']) ? $widget_settings['max_resolution'] : '';
$settings['min_resolution'] = !empty($widget_settings['min_resolution']) ? $widget_settings['min_resolution'] : '';
break;
}

View file

@ -62,6 +62,16 @@ class MigrateFieldInstanceTest extends MigrateDrupal6TestBase {
$this->assertIdentical('Email Field', $field->label());
$this->assertIdentical('benjy@example.com', $entity->field_test_email->value);
// Test image field.
$field = FieldConfig::load('node.story.field_test_imagefield');
$this->assertIdentical('Image Field', $field->label());
$field_settings = $field->getSettings();
$this->assertIdentical('', $field_settings['max_resolution']);
$this->assertIdentical('', $field_settings['min_resolution']);
$this->assertIdentical('', $field_settings['file_directory']);
$this->assertIdentical('png gif jpg jpeg', $field_settings['file_extensions']);
$this->assertIdentical('public', $field_settings['uri_scheme']);
// Test a filefield.
$field = FieldConfig::load('node.story.field_test_filefield');
$this->assertIdentical('File Field', $field->label());

View file

@ -56,6 +56,7 @@ class MigrateFieldWidgetSettingsTest extends MigrateDrupal6TestBase {
$component = $form_display->getComponent('field_test_email');
$expected['type'] = 'email_default';
$expected['weight'] = 6;
$expected['settings'] = array('placeholder' => '', 'size' => 60);
$this->assertIdentical($expected, $component);
// Link field.

View file

@ -0,0 +1,53 @@
<?php
/**
* @file
* Contains \Drupal\field\Tests\Update\EmailWidgetSizeSettingUpdateTest.
*/
namespace Drupal\field\Tests\Update;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
* Tests the update for the 'size' setting of the 'email_default' field widget.
*
* @group field
*/
class EmailWidgetSizeSettingUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../tests/fixtures/update/drupal-8.email_widget_size_setting-2578741.php',
];
}
/**
* Tests field_post_update_email_widget_size_setting().
*
* @see field_post_update_email_widget_size_setting()
*/
public function testFieldPostUpdateEmailWidgetSizeSetting() {
$configFactory = $this->container->get('config.factory');
// Load the 'node.article.default' entity form display and check that the
// widget for 'field_email_2578741' does not have a 'size' setting.
/** @var \Drupal\Core\Config\Config $config */
$config = $configFactory->get('core.entity_form_display.node.article.default');
$settings = $config->get('content.field_email_2578741.settings');
$this->assertTrue(!isset($settings['size']), 'The size setting does not exist prior to running the update functions.');
// Run updates.
$this->runUpdates();
// Reload the config and check that the 'size' setting has been populated.
$config = $configFactory->get('core.entity_form_display.node.article.default');
$settings = $config->get('content.field_email_2578741.settings');
$this->assertEqual($settings['size'], 60, 'The size setting exists and it has the correct default value.');
}
}

View file

@ -7,6 +7,8 @@
namespace Drupal\field\Tests\Views;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\views\Views;
/**
@ -112,4 +114,32 @@ class FieldUITest extends FieldTestBase {
$this->assertEqual($options, array('format', 'value'), 'The expected sort field options were found.');
}
/**
* Tests adding a boolean field filter handler.
*/
public function testBooleanFilterHandler() {
// Create a boolean field.
$field_name = 'field_boolean';
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => 'boolean',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'page',
]);
$field->save();
$url = "admin/structure/views/nojs/add-handler/test_view_fieldapi/default/filter";
$this->drupalPostForm($url, ['name[node__' . $field_name . '.' . $field_name . '_value]' => TRUE], t('Add and configure @handler', array('@handler' => t('filter criteria'))));
$this->assertResponse(200);
// Verify that using a boolean field as a filter also results in using the
// boolean plugin.
$option = $this->xpath('//label[@for="edit-options-value-1"]');
$this->assertEqual(t('True'), (string) $option[0]);
$option = $this->xpath('//label[@for="edit-options-value-0"]');
$this->assertEqual(t('False'), (string) $option[0]);
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* @file
* Contains SQL necessary to add a new component for an email field/widget to
* the 'node.article.default' entity form display.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$config = $connection->select('config', 'c')
->fields('c')
->condition('collection', '')
->condition('name', 'core.entity_form_display.node.article.default')
->execute()
->fetchAssoc();
$data = unserialize($config['data']);
// Manually add a new component that simulates an email field using the default
// email widget.
$data['content']['field_email_2578741'] = [
'weight' => 20,
'settings' => [
'placeholder' => '',
],
'third_party_settings' => [],
'type' => 'email_default',
];
$connection->update('config')
->fields(['data' => serialize($data)])
->condition('collection', '')
->condition('name', 'core.entity_form_display.node.article.default')
->execute();

View file

@ -2,7 +2,7 @@
/**
* @file
* Contains \Drupal\Tests\field\Unit\Plugin\migrate\process\d7\FieldInstanceSettingsTest.
* Contains \Drupal\Tests\field\Unit\Plugin\migrate\process\d7\FieldSettingsTest.
*/
namespace Drupal\Tests\field\Unit\Plugin\migrate\process\d7;

View file

@ -51,10 +51,12 @@ function file_help($route_name, RouteMatchInterface $route_match) {
/**
* Loads file entities from the database.
*
* @param array $fids
* (optional) An array of entity IDs. If omitted, all entities are loaded.
* @param array|null $fids
* (optional) An array of entity IDs. If omitted or NULL, all entities are
* loaded.
* @param bool $reset
* Whether to reset the internal file_load_multiple() cache.
* (optional) Whether to reset the internal file_load_multiple() cache.
* Defaults to FALSE.
*
* @return array
* An array of file entities, indexed by fid.
@ -80,9 +82,10 @@ function file_load_multiple(array $fids = NULL, $reset = FALSE) {
* @param int $fid
* A file ID.
* @param bool $reset
* Whether to reset the internal file_load_multiple() cache.
* (optional) Whether to reset the internal file_load_multiple() cache.
* Defaults to FALSE.
*
* @return \Drupal\file\FileInterface
* @return \Drupal\file\FileInterface|null
* A file entity or NULL if the file was not found.
*
* @deprecated in Drupal 8.x, will be removed before Drupal 9.0.
@ -117,19 +120,20 @@ function file_load($fid, $reset = FALSE) {
* @param \Drupal\file\FileInterface $source
* A file entity.
* @param string $destination
* A string containing the destination that $source should be copied to.
* This must be a stream wrapper URI.
* A string containing the destination that $source should be
* copied to. This must be a stream wrapper URI.
* @param int $replace
* Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
* the destination name exists then its database entry will be updated. If
* no database entry is found then a new one will be created.
* - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
* unique.
* - FILE_EXISTS_ERROR - Do nothing and return FALSE.
* (optional) Replace behavior when the destination file already exists.
* Possible values include:
* - FILE_EXISTS_REPLACE: Replace the existing file. If a managed file with
* the destination name exists, then its database entry will be updated. If
* no database entry is found, then a new one will be created.
* - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
* filename is unique.
* - FILE_EXISTS_ERROR: Do nothing and return FALSE.
*
* @return \Drupal\file\FileInterface|FALSE
* File object if the copy is successful, or FALSE in the event of an error.
* @return \Drupal\file\FileInterface|false
* File entity if the copy is successful, or FALSE in the event of an error.
*
* @see file_unmanaged_copy()
* @see hook_file_copy()
@ -192,20 +196,20 @@ function file_copy(FileInterface $source, $destination = NULL, $replace = FILE_E
* @param \Drupal\file\FileInterface $source
* A file entity.
* @param string $destination
* A string containing the destination that $source should be moved to.
* This must be a stream wrapper URI.
* A string containing the destination that $source should be moved
* to. This must be a stream wrapper URI.
* @param int $replace
* Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
* the destination name exists then its database entry will be updated and
* $source->delete() called after invoking hook_file_move().
* If no database entry is found then the source files record will be
* updated.
* - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
* unique.
* - FILE_EXISTS_ERROR - Do nothing and return FALSE.
* (optional) The replace behavior when the destination file already exists.
* Possible values include:
* - FILE_EXISTS_REPLACE: Replace the existing file. If a managed file with
* the destination name exists then its database entry will be updated and
* $source->delete() called after invoking hook_file_move(). If no database
* entry is found, then the source files record will be updated.
* - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
* filename is unique.
* - FILE_EXISTS_ERROR: Do nothing and return FALSE.
*
* @return \Drupal\file\FileInterface
* @return \Drupal\file\FileInterface|false
* Resulting file entity for success, or FALSE in the event of an error.
*
* @see file_unmanaged_move()
@ -268,12 +272,13 @@ function file_move(FileInterface $source, $destination = NULL, $replace = FILE_E
* @param \Drupal\file\FileInterface $file
* A file entity.
* @param array $validators
* An optional, associative array of callback functions used to validate the
* file. The keys are function names and the values arrays of callback
* parameters which will be passed in after the file entity. The
* functions should return an array of error messages; an empty array
* indicates that the file passed validation. The functions will be called in
* the order specified.
* (optional) An associative array of callback functions used to validate
* the file. The keys are function names and the values arrays of callback
* parameters which will be passed in after the file entity. The functions
* should return an array of error messages; an empty array indicates that
* the file passed validation. The callback functions will be called in the
* order specified in the array, then the hook hook_file_validate()
* will be invoked so other modules can validate the new file.
*
* @return array
* An array containing validation error messages.
@ -301,7 +306,8 @@ function file_validate(FileInterface $file, $validators = array()) {
* A file entity.
*
* @return array
* An array. If the file name is too long, it will contain an error message.
* An empty array if the file name length is smaller than the limit or an
* array containing an error message if it's not or is empty.
*/
function file_validate_name_length(FileInterface $file) {
$errors = array();
@ -324,8 +330,8 @@ function file_validate_name_length(FileInterface $file) {
* A string with a space separated list of allowed extensions.
*
* @return array
* An array. If the file extension is not allowed, it will contain an error
* message.
* An empty array if the file extension is allowed or an array containing an
* error message if it's not.
*
* @see hook_file_validate()
*/
@ -345,15 +351,15 @@ function file_validate_extensions(FileInterface $file, $extensions) {
* @param \Drupal\file\FileInterface $file
* A file entity.
* @param int $file_limit
* An integer specifying the maximum file size in bytes. Zero indicates that
* no limit should be enforced.
* (optional) The maximum file size in bytes. Zero (the default) indicates
* that no limit should be enforced.
* @param int $user_limit
* An integer specifying the maximum number of bytes the user is allowed.
* Zero indicates that no limit should be enforced.
* (optional) The maximum number of bytes the user is allowed. Zero (the
* default) indicates that no limit should be enforced.
*
* @return array
* An array. If the file size exceeds limits, it will contain an error
* message.
* An empty array if the file size is below limits or an array containing an
* error message if it's not.
*
* @see hook_file_validate()
*/
@ -380,7 +386,8 @@ function file_validate_size(FileInterface $file, $file_limit = 0, $user_limit =
* A file entity.
*
* @return array
* An array. If the file is not an image, it will contain an error message.
* An empty array if the file is a valid image or an array containing an error
* message if it's not.
*
* @see hook_file_validate()
*/
@ -405,18 +412,21 @@ function file_validate_is_image(FileInterface $file) {
*
* @param \Drupal\file\FileInterface $file
* A file entity. This function may resize the file affecting its size.
* @param string $maximum_dimensions
* An optional string in the form WIDTHxHEIGHT e.g. '640x480' or '85x85'. If
* an image toolkit is installed the image will be resized down to these
* dimensions. A value of 0 indicates no restriction on size, so resizing
* will be attempted.
* @param string $minimum_dimensions
* An optional string in the form WIDTHxHEIGHT. This will check that the
* image meets a minimum size. A value of 0 indicates no restriction.
* @param string|int $maximum_dimensions
* (optional) A string in the form WIDTHxHEIGHT; for example, '640x480' or
* '85x85'. If an image toolkit is installed, the image will be resized down
* to these dimensions. A value of zero (the default) indicates no restriction
* on size, so no resizing will be attempted.
* @param string|int $minimum_dimensions
* (optional) A string in the form WIDTHxHEIGHT. This will check that the
* image meets a minimum size. A value of zero (the default) indicates that
* there is no restriction on size.
*
* @return
* An array. If the file is an image and did not meet the requirements, it
* will contain an error message.
* @return array
* An empty array if the file meets the specified dimensions, was resized
* successfully to meet those requirements or is not an image. If the image
* does not meet the requirements or an attempt to resize it fails, an array
* containing the error message will be returned.
*
* @see hook_file_validate()
*/
@ -460,20 +470,22 @@ function file_validate_image_resolution(FileInterface $file, $maximum_dimensions
*
* @param string $data
* A string containing the contents of the file.
* @param string $destination
* A string containing the destination URI. This must be a stream wrapper URI.
* If no value is provided, a randomized name will be generated and the file
* will be saved using Drupal's default files scheme, usually "public://".
* @param string|null $destination
* (optional) A string containing the destination URI. This must be a stream
* wrapper URI. If no value or NULL is provided, a randomized name will be
* generated and the file will be saved using Drupal's default files scheme,
* usually "public://".
* @param int $replace
* Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
* the destination name exists then its database entry will be updated. If
* no database entry is found then a new one will be created.
* - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
* unique.
* - FILE_EXISTS_ERROR - Do nothing and return FALSE.
* (optional) The replace behavior when the destination file already exists.
* Possible values include:
* - FILE_EXISTS_REPLACE: Replace the existing file. If a managed file with
* the destination name exists, then its database entry will be updated. If
* no database entry is found, then a new one will be created.
* - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
* filename is unique.
* - FILE_EXISTS_ERROR: Do nothing and return FALSE.
*
* @return \Drupal\file\FileInterface
* @return \Drupal\file\FileInterface|false
* A file entity, or FALSE on error.
*
* @see file_unmanaged_save_data()
@ -666,39 +678,32 @@ function file_cron() {
* A string that is the associative array key of the upload form element in
* the form array.
* @param array $validators
* An optional, associative array of callback functions used to validate the
* (optional) An associative array of callback functions used to validate the
* file. See file_validate() for a full discussion of the array format.
* If no extension validator is provided it will default to a limited safe
* list of extensions which is as follows: "jpg jpeg gif png txt
* doc xls pdf ppt pps odt ods odp". To allow all extensions you must
* explicitly set the 'file_validate_extensions' validator to an empty array
* (Beware: this is not safe and should only be allowed for trusted users, if
* at all).
* @param string $destination
* A string containing the URI that the file should be copied to. This must
* be a stream wrapper URI. If this value is omitted, Drupal's temporary
* files scheme will be used ("temporary://").
* @param int $delta
* Delta of the file to save or NULL to save all files. Defaults to NULL.
* If the array is empty, it will be set up to call file_validate_extensions()
* with a safe list of extensions, as follows: "jpg jpeg gif png txt doc
* xls pdf ppt pps odt ods odp". To allow all extensions, you must explicitly
* set this array to ['file_validate_extensions' => '']. (Beware: this is not
* safe and should only be allowed for trusted users, if at all.)
* @param string|false $destination
* (optional) A string containing the URI that the file should be copied to.
* This must be a stream wrapper URI. If this value is omitted or set to
* FALSE, Drupal's temporary files scheme will be used ("temporary://").
* @param null|int $delta
* (optional) The delta of the file to return the file entity.
* Defaults to NULL.
* @param int $replace
* Replace behavior when the destination file already exists:
* (optional) The replace behavior when the destination file already exists.
* Possible values include:
* - FILE_EXISTS_REPLACE: Replace the existing file.
* - FILE_EXISTS_RENAME: Append _{incrementing number} until the filename is
* unique.
* - FILE_EXISTS_RENAME: (default) Append _{incrementing number} until the
* filename is unique.
* - FILE_EXISTS_ERROR: Do nothing and return FALSE.
*
* @return array
* Function returns array of files or a single file object if $delta
* != NULL. Each file object contains the file information if the
* upload succeeded or FALSE in the event of an error. Function
* returns NULL if no file was uploaded.
*
* The documentation for the "File interface" group, which you can find under
* Related topics, or the header at the top of this file, documents the
* components of a file entity. In addition to the standard components,
* this function adds:
* - source: Path to the file before it is moved.
* - destination: Path to the file after it is moved (same as 'uri').
* @return array|\Drupal\file\FileInterface|null|false
* An array of file entities or a single file entity if $delta != NULL. Each
* array element contains the file entity if the upload succeeded or FALSE if
* there was an error. Function returns NULL if no file was uploaded.
*/
function file_save_upload($form_field_name, $validators = array(), $destination = FALSE, $delta = NULL, $replace = FILE_EXISTS_RENAME) {
$user = \Drupal::currentUser();
@ -903,7 +908,7 @@ function file_save_upload($form_field_name, $validators = array(), $destination
/**
* Determines the preferred upload progress implementation.
*
* @return string|FALSE
* @return string|false
* A string indicating which upload progress system is available. Either "apc"
* or "uploadprogress". If neither are available, returns FALSE.
*/
@ -928,7 +933,7 @@ function file_progress_implementation() {
* Implements hook_ENTITY_TYPE_predelete() for file entities.
*/
function file_file_predelete(File $file) {
// TODO: Remove references to a file that is in-use.
// @todo Remove references to a file that is in-use.
}
/**
@ -1156,9 +1161,10 @@ function file_managed_file_submit($form, FormStateInterface $form_state) {
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* @return array|false
* An array of file entities for each file that was saved, keyed by its file
* ID, or FALSE if no files were saved.
* ID. Each array element contains a file entity. Function returns FALSE if
* upload directory could not be created or no files were uploaded.
*/
function file_managed_file_save_upload($element, FormStateInterface $form_state) {
$upload_name = implode('_', $element['#parents']);
@ -1437,20 +1443,20 @@ function file_icon_map($mime_type) {
*
* @param \Drupal\file\FileInterface $file
* A file entity.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field
* (optional) A field definition to be used for this check. If given, limits the
* reference check to the given field.
* @param \Drupal\Core\Field\FieldDefinitionInterface|null $field
* (optional) A field definition to be used for this check. If given,
* limits the reference check to the given field. Defaults to NULL.
* @param int $age
* (optional) A constant that specifies which references to count. Use
* EntityStorageInterface::FIELD_LOAD_REVISION to retrieve all
* EntityStorageInterface::FIELD_LOAD_REVISION (the default) to retrieve all
* references within all revisions or
* EntityStorageInterface::FIELD_LOAD_CURRENT to retrieve references
* only in the current revisions.
* EntityStorageInterface::FIELD_LOAD_CURRENT to retrieve references only in
* the current revisions of all entities that have references to this file.
* @param string $field_type
* (optional) The name of a field type. If given, limits the reference check
* to fields of the given type. If both $field and $field_type is given but
* to fields of the given type. If both $field and $field_type are given but
* $field is not the same type as $field_type, an empty array will be
* returned.
* returned. Defaults to 'file'.
*
* @return array
* A multidimensional array. The keys are field_name, entity_type,
@ -1526,10 +1532,12 @@ function file_get_file_references(FileInterface $file, FieldDefinitionInterface
/**
* Formats human-readable version of file status.
*
* @param int $choice
* integer Status code.
* @return string
* string Text-represented file status.
* @param int|null $choice
* (optional) An integer status code. If not set, all statuses are returned.
* Defaults to NULL.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup|\Drupal\Core\StringTranslation\TranslatableMarkup[]
* An array of file statuses or a specified status if $choice is set.
*/
function _views_file_status($choice = NULL) {
$status = array(

View file

@ -1,5 +1,5 @@
file.ajax_progress:
path: '/file/progress'
path: '/file/progress/{key}'
defaults:
_controller: '\Drupal\file\Controller\FileWidgetAjaxController::progress'
requirements:

View file

@ -283,7 +283,7 @@ class ManagedFile extends FormElement {
}
// Add the upload progress callback.
$element['upload_button']['#ajax']['progress']['url'] = Url::fromRoute('file.ajax_progress');
$element['upload_button']['#ajax']['progress']['url'] = Url::fromRoute('file.ajax_progress', ['key' => $upload_progress_key]);
}
// The file upload field itself.

View file

@ -23,7 +23,7 @@ class DownloadTest extends FileManagedTestBase {
* Test the public file transfer system.
*/
function testPublicFileTransfer() {
// Test generating an URL to a created file.
// Test generating a URL to a created file.
$file = $this->createFile();
$url = file_create_url($file->getFileUri());
// URLs can't contain characters outside the ASCII set so $filename has to be
@ -33,7 +33,7 @@ class DownloadTest extends FileManagedTestBase {
$this->drupalHead($url);
$this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the created file.');
// Test generating an URL to a shipped file (i.e. a file that is part of
// Test generating a URL to a shipped file (i.e. a file that is part of
// Drupal core, a module or a theme, for example a JavaScript file).
$filepath = 'core/assets/vendor/jquery/jquery.min.js';
$url = file_create_url($filepath);

View file

@ -480,6 +480,12 @@ class FileFieldWidgetTest extends FileFieldTestBase {
// If the field has at least a item, the table should be visible.
$this->assertIdentical(count($elements), 1);
// Test for AJAX error when using progress bar on file field widget
$key = $this->randomMachineName();
$this->drupalPost('file/progress/' . $key, 'application/json', []);
$this->assertNoResponse(500, t('No AJAX error when using progress bar on file field widget'));
$this->assertText('Starting upload...');
}
/**

View file

@ -13,7 +13,7 @@
/**
* Perform alterations on filter definitions.
*
* @param $info
* @param array $info
* Array of information on filters exposed by filter plugins.
*/
function hook_filter_info_alter(&$info) {
@ -48,7 +48,7 @@ function hook_filter_secure_image_alter(&$image) {
/**
* Perform actions when a text format has been disabled.
*
* @param $format
* @param \Drupal\filter\FilterFormatInterface $format
* The format object of the format being disabled.
*/
function hook_filter_format_disable($format) {

View file

@ -159,7 +159,7 @@ function filter_get_roles_by_format(FilterFormatInterface $format) {
/**
* Retrieves a list of text formats that are allowed for a given role.
*
* @param $rid
* @param string $rid
* The user role ID to retrieve text formats for.
*
* @return \Drupal\filter\FilterFormatInterface[]
@ -236,7 +236,7 @@ function filter_default_format(AccountInterface $account = NULL) {
* Any modules implementing a format deletion functionality must not delete this
* format.
*
* @return
* @return string|null
* The ID of the fallback text format.
*
* @see hook_filter_format_disable()
@ -635,10 +635,10 @@ function _filter_url_parse_partial_links($match) {
*
* Callback for preg_replace_callback() within _filter_url().
*
* @param $match
* @param array $match
* An array containing matches to replace from preg_replace_callback(),
* whereas $match[1] is expected to contain the content to be filtered.
* @param $escape
* @param bool|null $escape
* (optional) A Boolean indicating whether to escape (TRUE) or unescape
* comments (FALSE). Defaults to NULL, indicating neither. If TRUE, statically
* cached $comments are reset.

View file

@ -35,7 +35,7 @@ use Drupal\Core\Template\Attribute;
* public function process($text, $langcode) {
* // Modify $text.
*
* return new FilterProcess($text);
* return new FilterProcessResult($text);
* }
* @endcode
*
@ -44,7 +44,7 @@ use Drupal\Core\Template\Attribute;
* public function process($text, $langcode) {
* // Modify $text.
*
* $result = new FilterProcess($text);
* $result = new FilterProcessResult($text);
*
* // Associate assets to be attached.
* $result->setAttachments(array(

View file

@ -22,7 +22,7 @@ class FilterDefaultConfigTest extends KernelTestBase {
protected function setUp() {
parent::setUp();
// Drupal\filter\FilterPermissions::permissions() builds an URL to output
// Drupal\filter\FilterPermissions::permissions() builds a URL to output
// a link in the description.
$this->installSchema('system', 'url_alias');

View file

@ -862,7 +862,7 @@ www.example.com with a newline in comments -->
*
* @param FilterInterface $filter
* A input filter object.
* @param $tests
* @param array $tests
* An associative array, whereas each key is an arbitrary input string and
* each value is again an associative array whose keys are filter output
* strings and whose values are Booleans indicating whether the output is
@ -1141,13 +1141,13 @@ body {color:red}
* Note that this does not remove nulls, new lines and other characters that
* could be used to obscure a tag or an attribute name.
*
* @param $haystack
* @param string $haystack
* Text to look in.
* @param $needle
* @param string $needle
* Lowercase, plain text to look for.
* @param $message
* @param string $message
* (optional) Message to display if failed. Defaults to an empty string.
* @param $group
* @param string $group
* (optional) The group this message belongs to. Defaults to 'Other'.
*
* @return bool
@ -1166,13 +1166,13 @@ body {color:red}
* Note that this does not remove nulls, new lines, and other character that
* could be used to obscure a tag or an attribute name.
*
* @param $haystack
* @param string $haystack
* Text to look in.
* @param $needle
* @param string $needle
* Lowercase, plain text to look for.
* @param $message
* @param string $message
* (optional) Message to display if failed. Defaults to an empty string.
* @param $group
* @param string $group
* (optional) The group this message belongs to. Defaults to 'Other'.
*
* @return bool

View file

@ -1,6 +1,9 @@
langcode: en
status: true
dependencies: { }
dependencies:
enforced:
module:
- forum
id: comment_forum
label: Comment_forum
target_entity_type_id: node

View file

@ -119,6 +119,10 @@ class ForumUninstallTest extends WebTestBase {
$this->drupalPostForm(NULL, array(), t('Delete'));
$this->assertResponse(200);
$this->assertFalse((bool) NodeType::load('forum'), 'Node type with machine forum deleted.');
// Double check everything by reinstalling the forum module again.
$this->drupalPostForm('admin/modules', ['modules[Core][forum][enable]' => 1], 'Install');
$this->assertText('Module Forum has been enabled.');
}
/**

View file

@ -31,7 +31,8 @@ use Drupal\file\Plugin\Field\FieldType\FileItem;
* "label" = @Translation("File"),
* "columns" = {
* "target_id", "width", "height"
* }
* },
* "require_all_groups_for_translation" = TRUE
* },
* "alt" = {
* "label" = @Translation("Alt"),

View file

@ -244,7 +244,8 @@ class ImageAdminStylesTest extends ImageFieldTestBase {
$this->assertNoLinkByHref($style_path . '/effects/' . $uuids['image_crop'] . '/delete');
// Refresh the image style information and verify that the effect was
// actually deleted.
$style = entity_load_unchanged('image_style', $style->id());
$entity_type_manager = $this->container->get('entity_type.manager');
$style = $entity_type_manager->getStorage('image_style')->loadUnchanged($style->id());
$this->assertFalse($style->getEffects()->has($uuids['image_crop']), format_string(
'Effect with ID %uuid no longer found on image style %style',
array(
@ -260,7 +261,8 @@ class ImageAdminStylesTest extends ImageFieldTestBase {
);
$this->drupalPostForm($style_path, array('new' => 'image_rotate'), t('Add'));
$this->drupalPostForm(NULL, $edit, t('Add effect'));
$style = entity_load_unchanged('image_style', $style_name);
$entity_type_manager = $this->container->get('entity_type.manager');
$style = $entity_type_manager->getStorage('image_style')->loadUnchanged($style_name);
$this->assertEqual(count($style->getEffects()), 6, 'Rotate effect with transparent background was added.');
// Style deletion form.
@ -289,6 +291,47 @@ class ImageAdminStylesTest extends ImageFieldTestBase {
}
/**
* Tests editing Ajax-enabled image effect forms.
*/
public function testAjaxEnabledEffectForm() {
$admin_path = 'admin/config/media/image-styles';
// Setup a style to be created and effects to add to it.
$style_name = strtolower($this->randomMachineName(10));
$style_label = $this->randomString();
$style_path = $admin_path . '/manage/' . $style_name;
$effect_edit = [
'data[test_parameter]' => 100,
];
// Add style form.
$edit = [
'name' => $style_name,
'label' => $style_label,
];
$this->drupalPostForm($admin_path . '/add', $edit, t('Create new style'));
$this->assertRaw(t('Style %name was created.', ['%name' => $style_label]));
// Add two Ajax-enabled test effects.
$this->drupalPostForm($style_path, ['new' => 'image_module_test_ajax'], t('Add'));
$this->drupalPostForm(NULL, $effect_edit, t('Add effect'));
$this->drupalPostForm($style_path, ['new' => 'image_module_test_ajax'], t('Add'));
$this->drupalPostForm(NULL, $effect_edit, t('Add effect'));
// Load the saved image style.
$style = ImageStyle::load($style_name);
// Edit back the effects.
foreach ($style->getEffects() as $uuid => $effect) {
$effect_path = $admin_path . '/manage/' . $style_name . '/effects/' . $uuid;
$this->drupalGet($effect_path);
$this->drupalPostAjaxForm(NULL, $effect_edit, ['op' => t('Ajax refresh')]);
$this->drupalPostForm(NULL, $effect_edit, t('Update effect'));
}
}
/**
* Test deleting a style and choosing a replacement style.
*/

View file

@ -154,4 +154,11 @@ abstract class ImageFieldTestBase extends WebTestBase {
return isset($matches[1]) ? $matches[1] : FALSE;
}
/**
* Retrieves the fid of the last inserted file.
*/
protected function getLastFileId() {
return (int) db_query('SELECT MAX(fid) FROM {file_managed}')->fetchField();
}
}

View file

@ -0,0 +1,232 @@
<?php
/**
* @file
* Contains \Drupal\image\Tests\ImageOnTranslatedEntityTest.
*/
namespace Drupal\image\Tests;
use Drupal\file\Entity\File;
/**
* Uploads images to translated nodes.
*
* @group image
*/
class ImageOnTranslatedEntityTest extends ImageFieldTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('language', 'content_translation', 'field_ui');
/**
* The name of the image field used in the test.
*
* @var string
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create the "Basic page" node type.
$this->drupalCreateContentType(array('type' => 'basicpage', 'name' => 'Basic page'));
// Create a image field on the "Basic page" node type.
$this->fieldName = strtolower($this->randomMachineName());
$this->createImageField($this->fieldName, 'basicpage', [], ['title_field' => 1]);
// Create and login user.
$permissions = array(
'access administration pages',
'administer content translation',
'administer content types',
'administer languages',
'administer node fields',
'create content translations',
'create basicpage content',
'edit any basicpage content',
'translate any entity',
'delete any basicpage content',
);
$admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($admin_user);
// Add a second and third language.
$edit = array();
$edit['predefined_langcode'] = 'fr';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
$edit = array();
$edit['predefined_langcode'] = 'nl';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
}
/**
* Tests synced file fields on translated nodes.
*/
public function testSyncedImages() {
// Enable translation for "Basic page" nodes.
$edit = array(
'entity_types[node]' => 1,
'settings[node][basicpage][translatable]' => 1,
"settings[node][basicpage][fields][$this->fieldName]" => 1,
"settings[node][basicpage][columns][$this->fieldName][file]" => 1,
// Explicitly disable alt and title since the javascript disables the
// checkboxes on the form.
"settings[node][basicpage][columns][$this->fieldName][alt]" => FALSE,
"settings[node][basicpage][columns][$this->fieldName][title]" => FALSE,
);
$this->drupalPostForm('admin/config/regional/content-language', $edit, 'Save configuration');
// Verify that the image field on the "Basic basic" node type is
// translatable.
$definitions = \Drupal::entityManager()->getFieldDefinitions('node', 'basicpage');
$this->assertTrue($definitions[$this->fieldName]->isTranslatable(), 'Node image field is translatable.');
// Create a default language node.
$default_language_node = $this->drupalCreateNode(array('type' => 'basicpage', 'title' => 'Lost in translation'));
// Edit the node to upload a file.
$edit = array();
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('image')[0]->uri);
$this->drupalPostForm('node/' . $default_language_node->id() . '/edit', $edit, t('Save'));
$edit = [$this->fieldName . '[0][alt]' => 'Lost in translation image', $this->fieldName . '[0][title]' => 'Lost in translation image title'];
$this->drupalPostForm(NULL, $edit, t('Save'));
$first_fid = $this->getLastFileId();
// Translate the node into French: remove the existing file.
$this->drupalPostForm('node/' . $default_language_node->id() . '/translations/add/en/fr', array(), t('Remove'));
// Upload a different file.
$edit = array();
$edit['title[0][value]'] = 'Scarlett Johansson';
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('image')[1]->uri);
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$edit = [$this->fieldName . '[0][alt]' => 'Scarlett Johansson image', $this->fieldName . '[0][title]' => 'Scarlett Johansson image title'];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
// This inspects the HTML after the post of the translation, the image
// should be displayed on the original node.
$this->assertRaw('alt="Lost in translation image"');
$this->assertRaw('title="Lost in translation image title"');
$second_fid = $this->getLastFileId();
// View the translated node.
$this->drupalGet('fr/node/' . $default_language_node->id());
$this->assertRaw('alt="Scarlett Johansson image"');
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
/* @var $file \Drupal\file\FileInterface */
// Ensure the file status of the first file permanent.
$file = File::load($first_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the second file is permanent.
$file = File::load($second_fid);
$this->assertTrue($file->isPermanent());
// Translate the node into dutch: remove the existing file.
$this->drupalPostForm('node/' . $default_language_node->id() . '/translations/add/en/nl', array(), t('Remove'));
// Upload a different file.
$edit = array();
$edit['title[0][value]'] = 'Akiko Takeshita';
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('image')[2]->uri);
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$edit = [$this->fieldName . '[0][alt]' => 'Akiko Takeshita image', $this->fieldName . '[0][title]' => 'Akiko Takeshita image title'];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$third_fid = $this->getLastFileId();
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
// Ensure the first file is untouched.
$file = File::load($first_fid);
$this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.');
// This inspects the HTML after the post of the translation, the image
// should be displayed on the original node.
$this->assertRaw('alt="Lost in translation image"');
$this->assertRaw('title="Lost in translation image title"');
// View the translated node.
$this->drupalGet('nl/node/' . $default_language_node->id());
$this->assertRaw('alt="Akiko Takeshita image"');
$this->assertRaw('title="Akiko Takeshita image title"');
// Ensure the file status of the second file is permanent.
$file = File::load($second_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the third file is permanent.
$file = File::load($third_fid);
$this->assertTrue($file->isPermanent());
// Edit the second translation: remove the existing file.
$this->drupalPostForm('fr/node/' . $default_language_node->id() . '/edit', array(), t('Remove'));
// Upload a different file.
$edit = array();
$edit['title[0][value]'] = 'Giovanni Ribisi';
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('image')[3]->uri);
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$name = $this->fieldName . '[0][alt]';
$edit = [$name => 'Giovanni Ribisi image'];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$replaced_second_fid = $this->getLastFileId();
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
// Ensure the first and third files are untouched.
$file = File::load($first_fid);
$this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.');
$file = File::load($third_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the replaced second file is permanent.
$file = File::load($replaced_second_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the old second file is now temporary.
$file = File::load($second_fid);
$this->assertTrue($file->isTemporary());
// Delete the third translation.
$this->drupalPostForm('nl/node/' . $default_language_node->id() . '/delete', array(), t('Delete Dutch translation'));
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
// Ensure the first and replaced second files are untouched.
$file = File::load($first_fid);
$this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.');
$file = File::load($replaced_second_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the third file is now temporary.
$file = File::load($third_fid);
$this->assertTrue($file->isTemporary());
// Delete the all translations.
$this->drupalPostForm('node/' . $default_language_node->id() . '/delete', array(), t('Delete all translations'));
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
// Ensure the file status of the all files are now temporary.
$file = File::load($first_fid);
$this->assertTrue($file->isTemporary(), 'First file still exists and is temporary.');
$file = File::load($replaced_second_fid);
$this->assertTrue($file->isTemporary());
}
}

View file

@ -1,3 +1,11 @@
image.effect.image_module_test_ajax:
type: mapping
label: 'Ajax test'
mapping:
test_parameter:
type: integer
label: 'Test Parameter'
image.style.*.third_party.image_module_test:
type: mapping
label: 'Schema for image_module_test module additions to image_style entity'

View file

@ -0,0 +1,89 @@
<?php
/**
* @file
* Contains \Drupal\image_module_test\Plugin\ImageEffect\AjaxTestImageEffect.
*/
namespace Drupal\image_module_test\Plugin\ImageEffect;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageInterface;
use Drupal\image\ConfigurableImageEffectBase;
/**
* Provides a test effect using Ajax in the configuration form.
*
* @ImageEffect(
* id = "image_module_test_ajax",
* label = @Translation("Ajax test")
* )
*/
class AjaxTestImageEffect extends ConfigurableImageEffectBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'test_parameter' => 0,
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['test_parameter'] = [
'#type' => 'number',
'#title' => t('Test parameter'),
'#default_value' => $this->configuration['test_parameter'],
'#min' => 0,
];
$form['ajax_refresh'] = [
'#type' => 'button',
'#value' => $this->t('Ajax refresh'),
'#ajax' => ['callback' => [$this, 'ajaxCallback']],
];
$form['ajax_value'] = [
'#id' => 'ajax-value',
'#type' => 'item',
'#title' => $this->t('Ajax value'),
'#markup' => 'bar',
];
return $form;
}
/**
* AJAX callback.
*/
public function ajaxCallback($form, FormStateInterface $form_state) {
$item = [
'#type' => 'item',
'#title' => $this->t('Ajax value'),
'#markup' => microtime(),
];
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#ajax-value', $item));
return $response;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$this->configuration['test_parameter'] = $form_state->getValue('test_parameter');
}
/**
* {@inheritdoc}
*/
public function applyEffect(ImageInterface $image) {
return TRUE;
}
}

View file

@ -13,7 +13,7 @@
/**
* Define language types.
*
* @return
* @return array
* An associative array of language type definitions. The keys are the
* identifiers, which are also used as names for global variables representing
* the types in the bootstrap phase. The values are associative arrays that
@ -51,7 +51,7 @@ function hook_language_types_info() {
/**
* Perform alterations on language types.
*
* @param $language_types
* @param array $language_types
* Array of language type definitions.
*
* @see hook_language_types_info()
@ -66,7 +66,7 @@ function hook_language_types_info_alter(array &$language_types) {
/**
* Perform alterations on language negotiation methods.
*
* @param $negotiation_info
* @param array $negotiation_info
* Array of language negotiation method definitions.
*
* @ingroup language_negotiation

View file

@ -51,7 +51,7 @@ interface ConfigurableLanguageManagerInterface extends LanguageManagerInterface
/**
* Stores language types configuration.
*
* @param array
* @param array $config
* An indexed array with the following keys_
* - configurable: an array of configurable language type names.
* - all: an array of all the defined language type names.

View file

@ -54,7 +54,7 @@ class LanguageRequestSubscriber implements EventSubscriberInterface {
*
* @param \Drupal\language\ConfigurableLanguageManagerInterface $language_manager
* The language manager service.
* @param \Drupal\language\LanguageNegotiatorInterface
* @param \Drupal\language\LanguageNegotiatorInterface $negotiator
* The language negotiator.
* @param \Drupal\Core\StringTranslation\Translator\TranslatorInterface $translation;
* The translation service.

View file

@ -73,7 +73,7 @@ class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPa
* A config factory object for retrieving configuration settings.
* @param \Drupal\language\ConfigurableLanguageManagerInterface $language_manager
* The configurable language manager.
* @param \Drupal\language\LanguageNegotiatorInterface
* @param \Drupal\language\LanguageNegotiatorInterface $negotiator
* The language negotiator.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current active user.

View file

@ -62,7 +62,7 @@ class LanguageListBuilder extends DraggableListBuilder {
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage controller class.
* @param \Drupal\Core\Language\LanguageManagerInterface
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.

View file

@ -160,7 +160,7 @@ interface LanguageNegotiatorInterface {
/**
* Returns the ID of the language type's primary language negotiation method.
*
* @param $type
* @param string $type
* The language type.
*
* @return string
@ -172,9 +172,9 @@ interface LanguageNegotiatorInterface {
/**
* Checks whether a language negotiation method is enabled for a language type.
*
* @param $method_id
* @param string $method_id
* The language negotiation method ID.
* @param $type
* @param string $type
* (optional) The language type. If none is passed, all the configurable
* language types will be inspected.
*

View file

@ -100,9 +100,9 @@ class EntityDefaultLanguageTest extends KernelTestBase {
/**
* Creates a new node content type.
*
* @param name
* @param string $name
* The content type name.
* @param $langcode
* @param string $langcode
* Default language code of the nodes of this type.
*/
protected function createContentType($name, $langcode) {
@ -123,9 +123,9 @@ class EntityDefaultLanguageTest extends KernelTestBase {
/**
* Creates a new node of given type and language using Entity API.
*
* @param $type
* @param string $type
* The node content type.
* @param $langcode
* @param string $langcode
* (optional) Language code to pass to entity create.
*
* @return \Drupal\node\NodeInterface

View file

@ -2,7 +2,7 @@
/**
* @file
* Contains \Drupal\language\Tests\LanguageNegotiationLanguageContentEntityTest.
* Contains \Drupal\language\Tests\LanguageNegotiationContentEntityTest.
*/
namespace Drupal\language\Tests;

View file

@ -41,7 +41,7 @@ interface LinkItemInterface extends FieldItemInterface {
* Gets the URL object.
*
* @return \Drupal\Core\Url
* Returns an Url object.
* Returns a Url object.
*/
public function getUrl();

View file

@ -240,7 +240,7 @@ class LinkFormatter extends FormatterBase implements ContainerFactoryPluginInter
* The link field item being rendered.
*
* @return \Drupal\Core\Url
* An Url object.
* A Url object.
*/
protected function buildUrl(LinkItemInterface $item) {
$url = $item->getUrl() ?: Url::fromRoute('<none>');

View file

@ -44,7 +44,7 @@ process:
plugin: menu_link_parent
source:
- plid
- @menu_name
- '@menu_name'
- parent_link_path
changed: updated
destination:

View file

@ -139,12 +139,20 @@ function _menu_ui_node_save(NodeInterface $node, array $values) {
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
if (!empty($values['entity_id'])) {
$entity = MenuLinkContent::load($values['entity_id']);
if ($entity->isTranslatable()) {
if (!$entity->hasTranslation($node->language()->getId())) {
$entity = $entity->addTranslation($node->language()->getId(), $entity->toArray());
}
else {
$entity = $entity->getTranslation($node->language()->getId());
}
}
}
else {
// Create a new menu_link_content entity.
$entity = entity_create('menu_link_content', array(
'link' => ['uri' => 'entity:node/' . $node->id()],
'langcode' => $node->getUntranslated()->language()->getId(),
'langcode' => $node->language()->getId(),
));
$entity->enabled->value = 1;
}
@ -216,10 +224,12 @@ function menu_ui_get_menu_link_defaults(NodeInterface $node) {
}
if ($id) {
$menu_link = MenuLinkContent::load($id);
$menu_link = \Drupal::service('entity.repository')->getTranslationFromContext($menu_link);
$defaults = array(
'entity_id' => $menu_link->id(),
'id' => $menu_link->getPluginId(),
'title' => $menu_link->getTitle(),
'title_max_length' => $menu_link->getFieldDefinitions()['title']->getSetting('max_length'),
'description' => $menu_link->getDescription(),
'menu_name' => $menu_link->getMenuName(),
'parent' => $menu_link->getParentId(),
@ -229,10 +239,15 @@ function menu_ui_get_menu_link_defaults(NodeInterface $node) {
}
if (!$defaults) {
// Get the default max_length of a menu link title from the base field
// definition.
$field_definitions = \Drupal::entityManager()->getBaseFieldDefinitions('menu_link_content');
$max_length = $field_definitions['title']->getSetting('max_length');
$defaults = array(
'entity_id' => 0,
'id' => '',
'title' => '',
'title_max_length' => $max_length,
'description' => '',
'menu_name' => $menu_name,
'parent' => '',
@ -313,6 +328,7 @@ function menu_ui_form_node_form_alter(&$form, FormStateInterface $form_state) {
'#type' => 'textfield',
'#title' => t('Menu link title'),
'#default_value' => $defaults['title'],
'#maxlength' => $defaults['title_max_length'],
);
$form['menu']['link']['description'] = array(

View file

@ -8,7 +8,9 @@
namespace Drupal\menu_ui\Tests;
use Drupal\simpletest\WebTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\node\Entity\Node;
/**
* Add, edit, and delete a node with menu link.
@ -29,7 +31,7 @@ class MenuNodeTest extends WebTestBase {
*
* @var array
*/
public static $modules = array('menu_ui', 'test_page_test', 'node', 'block');
public static $modules = array('menu_ui', 'test_page_test', 'node', 'block', 'locale', 'language', 'content_translation');
protected function setUp() {
parent::setUp();
@ -46,6 +48,10 @@ class MenuNodeTest extends WebTestBase {
'create page content',
'edit any page content',
'delete any page content',
'create content translations',
'update content translations',
'delete content translations',
'translate any entity',
));
$this->drupalLogin($this->editor);
}
@ -61,6 +67,11 @@ class MenuNodeTest extends WebTestBase {
$this->drupalGet('admin/structure/types/manage/page');
$this->assertCacheContext('user.roles:authenticated');
// Verify that the menu link title has the correct maxlength.
$max_length = \Drupal::entityManager()->getBaseFieldDefinitions('menu_link_content')['title']->getSetting('max_length');
$this->drupalGet('node/add/page');
$this->assertPattern('/<input .* id="edit-menu-title" .* maxlength="' . $max_length . '" .* \/>/', 'Menu link title field has correct maxlength in node add form.');
// Disable the default main menu, so that no menus are enabled.
$edit = array(
'menu_options[main]' => FALSE,
@ -171,6 +182,7 @@ class MenuNodeTest extends WebTestBase {
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertFieldById('edit-menu-weight', 17, 'Menu weight correct in edit form');
$this->assertPattern('/<input .* id="edit-menu-title" .* maxlength="' . $max_length . '" .* \/>/', 'Menu link title field has correct maxlength in node edit form.');
// Disable the menu link, then edit the node--the link should stay disabled.
$link_id = menu_ui_get_menu_link_defaults($node)['entity_id'];
@ -229,4 +241,105 @@ class MenuNodeTest extends WebTestBase {
// Assert that unallowed Administration menu is not available in options.
$this->assertNoOption('edit-menu-menu-parent', 'admin:');
}
/**
* Testing correct loading and saving of menu links via node form widget in a multilingual environment.
*/
function testMultilingualMenuNodeFormWidget() {
// Setup languages.
$langcodes = array('de');
foreach ($langcodes as $langcode) {
ConfigurableLanguage::createFromLangcode($langcode)->save();
}
array_unshift($langcodes, \Drupal::languageManager()->getDefaultLanguage()->getId());
$config = \Drupal::service('config.factory')->getEditable('language.negotiation');
// Ensure path prefix is used to determine the language.
$config->set('url.source', 'path_prefix');
// Ensure that there's a path prefix set for english as well.
$config->set('url.prefixes.' . $langcodes[0], $langcodes[0]);
$config->save();
$this->rebuildContainer();
$languages = array();
foreach ($langcodes as $langcode) {
$languages[$langcode] = ConfigurableLanguage::load($langcode);
}
// Use a UI form submission to make the node type and menu link content entity translatable.
$this->drupalLogout();
$this->drupalLogin($this->rootUser);
$edit = array(
'entity_types[node]' => TRUE,
'entity_types[menu_link_content]' => TRUE,
'settings[node][page][settings][language][language_alterable]' => TRUE,
'settings[node][page][translatable]' => TRUE,
'settings[node][page][fields][title]' => TRUE,
'settings[menu_link_content][menu_link_content][translatable]' => TRUE,
);
$this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration'));
// Log out and back in as normal user.
$this->drupalLogout();
$this->drupalLogin($this->editor);
// Create a node.
$node_title = $this->randomMachineName(8);
$node = Node::create([
'type' => 'page',
'title' => $node_title,
'body' => $this->randomMachineName(16),
'uid' => $this->editor->id(),
'status' => 1,
'langcode' => $langcodes[0],
]);
$node->save();
// Create translation.
$translated_node_title = $this->randomMachineName(8);
$node->addTranslation($langcodes[1], ['title' => $translated_node_title, 'body' => $this->randomMachineName(16), 'status' => 1]);
$node->save();
// Edit the node and create a menu link.
$edit = array(
'menu[enabled]' => 1,
'menu[title]' => $node_title,
'menu[weight]' => 17,
);
$options = array('language' => $languages[$langcodes[0]]);
$url = $node->toUrl('edit-form', $options);
$this->drupalPostForm($url, $edit, t('Save') . ' ' . t('(this translation)'));
// Edit the node in a different language and translate the menu link.
$edit = array(
'menu[enabled]' => 1,
'menu[title]' => $translated_node_title,
'menu[weight]' => 17,
);
$options = array('language' => $languages[$langcodes[1]]);
$url = $node->toUrl('edit-form', $options);
$this->drupalPostForm($url, $edit, t('Save') . ' ' . t('(this translation)'));
// Assert that the original link exists in the frontend.
$this->drupalGet('node/' . $node->id(), array('language' => $languages[$langcodes[0]]));
$this->assertLink($node_title);
// Assert that the translated link exists in the frontend.
$this->drupalGet('node/' . $node->id(), array('language' => $languages[$langcodes[1]]));
$this->assertLink($translated_node_title);
// Revisit the edit page in original language, check the loaded menu item title and save.
$options = array('language' => $languages[$langcodes[0]]);
$url = $node->toUrl('edit-form', $options);
$this->drupalGet($url);
$this->assertFieldById('edit-menu-title', $node_title);
$this->drupalPostForm(NULL, [], t('Save') . ' ' . t('(this translation)'));
// Revisit the edit page of the translation and check the loaded menu item title.
$options = array('language' => $languages[$langcodes[1]]);
$url = $node->toUrl('edit-form', $options);
$this->drupalGet($url);
$this->assertFieldById('edit-menu-title', $translated_node_title);
}
}

View file

@ -40,6 +40,17 @@ migrate.process.dedupe_entity:
type: integer
label: 'Length'
migrate.process.explode:
type: migrate_process
label: 'Explode process'
mapping:
delimiter:
type: string
label: 'Delimiter'
limit:
type: integer
label: 'Limit'
migrate.process.extract:
type: migrate_process
label: 'Extract process'

View file

@ -74,7 +74,7 @@ class MigrateTemplateStorage implements MigrateTemplateStorageInterface {
if (file_exists($full_directory)) {
$files = scandir($full_directory);
foreach ($files as $file) {
if ($file[0] !== '.' && fnmatch('*.yml', $file)) {
if ($file[0] !== '.' && preg_match('/\.yml$/', $file)) {
$templates[basename($file, '.yml')] = Yaml::decode(file_get_contents("$full_directory/$file"));
}
}

View file

@ -112,7 +112,7 @@ abstract class Entity extends DestinationBase implements ContainerFactoryPluginI
* The entity we are importing into.
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
$entity_id = $old_destination_id_values ? reset($old_destination_id_values) : $this->getEntityId($row);
$entity_id = reset($old_destination_id_values) ?: $this->getEntityId($row);
if (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) {
$this->updateEntity($entity, $row);
}

View file

@ -33,6 +33,11 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
*/
class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryPluginInterface {
/**
* Column name of hashed source id values.
*/
const SOURCE_IDS_HASH = 'source_ids_hash';
/**
* An event dispatcher instance to use for map events.
*
@ -171,6 +176,34 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
);
}
/**
* Retrieves the hash of the source identifier values.
*
* It is public only for testing purposes.
*
* @param array $source_id_values
* The source identifiers
*
* @return string
* An hash containing the hashed values of the source identifiers.
*/
public function getSourceIDsHash(array $source_id_values) {
// When looking up the destination ID we require an array with both the
// source key and value, e.g. ['nid' => 41]. In this case, $source_id_values
// need to be ordered the same order as $this->sourceIdFields().
// However, the Migration process plugin doesn't currently have a way to get
// the source key so we presume the values have been passed through in the
// correct order.
if (!isset($source_id_values[0])) {
$source_id_values_keyed = [];
foreach ($this->sourceIdFields() as $field_name => $source_id) {
$source_id_values_keyed[] = $source_id_values[$field_name];
}
$source_id_values = $source_id_values_keyed;
}
return hash('sha256', serialize(array_map('strval', $source_id_values)));
}
/**
* The source ID fields.
*
@ -285,28 +318,25 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
// and map from the source field names to the map/msg field names.
$count = 1;
$source_id_schema = array();
$pks = array();
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.
if (isset($source_id_schema[$mapkey]['type']) && $source_id_schema[$mapkey]['type'] == 'varchar') {
$source_id_schema[$mapkey]['type'] = 'varchar_ascii';
}
$pks[] = $mapkey;
}
$fields = $source_id_schema;
$source_ids_hash[static::SOURCE_IDS_HASH] = array(
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
'description' => 'Hash of source ids. Used as primary key',
);
$fields = $source_ids_hash + $source_id_schema;
// Add destination identifiers to map table.
// @todo How do we discover the destination schema?
$count = 1;
foreach ($this->migration->getDestinationPlugin()->getIds() as $id_definition) {
// Allow dest identifier fields to be NULL (for IGNORED/FAILED
// cases).
// Allow dest identifier fields to be NULL (for IGNORED/FAILED cases).
$mapkey = 'destid' . $count++;
$fields[$mapkey] = $this->getFieldSchema($id_definition);
$fields[$mapkey]['not null'] = FALSE;
@ -343,10 +373,8 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
$schema = array(
'description' => 'Mappings from source identifier value(s) to destination identifier value(s).',
'fields' => $fields,
'primary key' => array(static::SOURCE_IDS_HASH),
);
if ($pks) {
$schema['primary key'] = $pks;
}
$this->getDatabase()->schema()->createTable($this->mapTableName, $schema);
// Now do the message table.
@ -357,7 +385,7 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
'unsigned' => TRUE,
'not null' => TRUE,
);
$fields += $source_id_schema;
$fields += $source_ids_hash;
$fields['level'] = array(
'type' => 'int',
@ -375,9 +403,6 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
'fields' => $fields,
'primary key' => array('msgid'),
);
if ($pks) {
$schema['indexes']['sourcekey'] = $pks;
}
$this->getDatabase()->schema()->createTable($this->messageTableName(), $schema);
}
}
@ -406,6 +431,14 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
)
);
}
if (!$this->getDatabase()->schema()->fieldExists($this->mapTableName, static::SOURCE_IDS_HASH)) {
$this->getDatabase()->schema()->addField($this->mapTableName, static::SOURCE_IDS_HASH, array(
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
'description' => 'Hash of source ids. Used as primary key',
));
}
}
}
@ -435,9 +468,7 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
public function getRowBySource(array $source_id_values) {
$query = $this->getDatabase()->select($this->mapTableName(), 'map')
->fields('map');
foreach ($this->sourceIdFields() as $field_name => $source_id) {
$query->condition("map.$source_id", $source_id_values[$field_name], '=');
}
$query->condition(static::SOURCE_IDS_HASH, $this->getSourceIDsHash($source_id_values));
$result = $query->execute();
return $result->fetchAssoc();
}
@ -497,14 +528,7 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
$query = $this->getDatabase()->select($this->mapTableName(), 'map')
->fields('map', $this->destinationIdFields());
// When looking up the destination ID we require an array with both the
// source key and value, e.g. ['nid' => 41]. However, the Migration process
// plugin doesn't currently have a way to get the source key so we presume
// the values have been passed through in the correct order.
$have_keys = !isset($source_id_values[0]);
foreach ($this->sourceIdFields() as $field_name => $source_id) {
$query->condition("map.$source_id", $have_keys ? $source_id_values[$field_name] : array_shift($source_id_values), '=');
}
$query->condition(static::SOURCE_IDS_HASH, $this->getSourceIDsHash($source_id_values));
$result = $query->execute();
$destination_id = $result->fetchAssoc();
return array_values($destination_id ?: array());
@ -517,19 +541,23 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
// Construct the source key.
$source_id_values = $row->getSourceIdValues();
// Construct the source key and initialize to empty variable keys.
$keys = array();
$fields = [];
foreach ($this->sourceIdFields() as $field_name => $key_name) {
// A NULL key value will fail.
// A NULL key value is usually an indication of a problem.
if (!isset($source_id_values[$field_name])) {
$this->message->display(t(
'Could not save to map table due to NULL value for key field @field',
$this->message->display($this->t(
'Did not save to map table due to NULL value for key field @field',
array('@field' => $field_name)), 'error');
return;
}
$keys[$key_name] = $source_id_values[$field_name];
$fields[$key_name] = $source_id_values[$field_name];
}
$fields = array(
if (!$fields) {
return;
}
$fields += array(
'source_row_status' => (int) $source_row_status,
'rollback_action' => (int) $rollback_action,
'hash' => $row->getHash(),
@ -545,14 +573,13 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
if ($this->migration->get('trackLastImported')) {
$fields['last_imported'] = time();
}
if ($keys) {
// Notify anyone listening of the map row we're about to save.
$this->eventDispatcher->dispatch(MigrateEvents::MAP_SAVE, new MigrateMapSaveEvent($this, $keys + $fields));
$this->getDatabase()->merge($this->mapTableName())
->key($keys)
->fields($fields)
->execute();
}
$keys = [static::SOURCE_IDS_HASH => $this->getSourceIDsHash($source_id_values)];
// Notify anyone listening of the map row we're about to save.
$this->eventDispatcher->dispatch(MigrateEvents::MAP_SAVE, new MigrateMapSaveEvent($this, $fields));
$this->getDatabase()->merge($this->mapTableName())
->key($keys)
->fields($fields)
->execute();
}
/**
@ -564,8 +591,8 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
if (!isset($source_id_values[$field_name])) {
return;
}
$fields[$source_id] = $source_id_values[$field_name];
}
$fields[static::SOURCE_IDS_HASH] = $this->getSourceIDsHash($source_id_values);
$fields['level'] = $level;
$fields['message'] = $message;
$this->getDatabase()->insert($this->messageTableName())
@ -583,10 +610,8 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
public function getMessageIterator(array $source_id_values = [], $level = NULL) {
$query = $this->getDatabase()->select($this->messageTableName(), 'msg')
->fields('msg');
foreach ($this->sourceIdFields() as $field_name => $source_id) {
if (isset($source_id_values[$field_name])) {
$query->condition($source_id, $source_id_values[$field_name]);
}
if ($source_id_values) {
$query->condition(static::SOURCE_IDS_HASH, $this->getSourceIDsHash($source_id_values));
}
if ($level) {
@ -672,22 +697,16 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
if (empty($source_id_values)) {
throw new MigrateException('Without source identifier values it is impossible to find the row to delete.');
}
if (!$messages_only) {
$map_query = $this->getDatabase()->delete($this->mapTableName());
}
$message_query = $this->getDatabase()->delete($this->messageTableName());
foreach ($this->sourceIdFields() as $field_name => $source_id) {
if (!$messages_only) {
$map_query->condition($source_id, $source_id_values[$field_name]);
}
$message_query->condition($source_id, $source_id_values[$field_name]);
}
if (!$messages_only) {
$map_query = $this->getDatabase()->delete($this->mapTableName());
$map_query->condition(static::SOURCE_IDS_HASH, $this->getSourceIDsHash($source_id_values));
// Notify anyone listening of the map row we're about to delete.
$this->eventDispatcher->dispatch(MigrateEvents::MAP_DELETE, new MigrateMapDeleteEvent($this, $source_id_values));
$map_query->execute();
}
$message_query = $this->getDatabase()->delete($this->messageTableName());
$message_query->condition(static::SOURCE_IDS_HASH, $this->getSourceIDsHash($source_id_values));
$message_query->execute();
}
@ -705,11 +724,8 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
// Notify anyone listening of the map row we're about to delete.
$this->eventDispatcher->dispatch(MigrateEvents::MAP_DELETE, new MigrateMapDeleteEvent($this, $source_id_values));
$map_query->execute();
$count = 1;
foreach ($this->sourceIdFields() as $field_name => $source_id) {
$message_query->condition($source_id, $source_id_values[$field_name]);
$count++;
}
$message_query->condition(static::SOURCE_IDS_HASH, $this->getSourceIDsHash($source_id_values));
$message_query->execute();
}
}
@ -762,6 +778,7 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
}
$this->result = $this->getDatabase()->select($this->mapTableName(), 'map')
->fields('map', $fields)
->orderBy('destid1')
->execute();
$this->next();
}

View file

@ -0,0 +1,49 @@
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\process\Explode.
*/
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* This plugin explodes a delimited string into an array of values.
*
* @MigrateProcessPlugin(
* id = "explode"
* )
*/
class Explode extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (is_string($value)) {
if (!empty($this->configuration['delimiter'])) {
$limit = isset($this->configuration['limit']) ? $this->configuration['limit'] : PHP_INT_MAX;
return explode($this->configuration['delimiter'], $value, $limit);
}
else {
throw new MigrateException('delimiter is empty');
}
}
else {
throw new MigrateException(sprintf('%s is not a string', var_export($value, TRUE)));
}
}
/**
* {@inheritdoc}
*/
public function multiple() {
return TRUE;
}
}

View file

@ -102,6 +102,10 @@ class Migration extends ProcessPluginBase implements ContainerFactoryPluginInter
}
}
if (!$destination_ids && !empty($this->configuration['no_stub'])) {
return NULL;
}
if (!$destination_ids && ($self || isset($this->configuration['stub_id']) || count($migrations) == 1)) {
// If the lookup didn't succeed, figure out which migration will do the
// stubbing.

View file

@ -75,7 +75,7 @@ abstract class SqlBase extends SourcePluginBase implements ContainerFactoryPlugi
* The query string.
*/
public function __toString() {
return (string) $this->query;
return (string) $this->query();
}
/**

View file

@ -29,6 +29,21 @@ class MigrateSqlIdMapEnsureTablesTest extends MigrateTestCase {
* Tests the ensureTables method when the tables do not exist.
*/
public function testEnsureTablesNotExist() {
$fields['source_ids_hash'] = Array(
'type' => 'varchar',
'length' => 64,
'not null' => 1,
'description' => 'Hash of source ids. Used as primary key'
);
$fields['sourceid1'] = array(
'type' => 'int',
'not null' => TRUE,
);
$fields['destid1'] = array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
);
$fields['source_row_status'] = array(
'type' => 'int',
'size' => 'tiny',
@ -58,19 +73,10 @@ class MigrateSqlIdMapEnsureTablesTest extends MigrateTestCase {
'not null' => FALSE,
'description' => 'Hash of source row data, for detecting changes',
);
$fields['sourceid1'] = array(
'type' => 'int',
'not null' => TRUE,
);
$fields['destid1'] = array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
);
$map_table_schema = array(
'description' => 'Mappings from source identifier value(s) to destination identifier value(s).',
'fields' => $fields,
'primary key' => array('sourceid1'),
'primary key' => array('source_ids_hash'),
);
$schema = $this->getMockBuilder('Drupal\Core\Database\Schema')
->disableOriginalConstructor()
@ -89,9 +95,11 @@ class MigrateSqlIdMapEnsureTablesTest extends MigrateTestCase {
'unsigned' => TRUE,
'not null' => TRUE,
);
$fields['sourceid1'] = array(
'type' => 'int',
'not null' => TRUE,
$fields['source_ids_hash'] = Array(
'type' => 'varchar',
'length' => 64,
'not null' => 1,
'description' => 'Hash of source ids. Used as primary key'
);
$fields['level'] = array(
'type' => 'int',
@ -109,7 +117,6 @@ class MigrateSqlIdMapEnsureTablesTest extends MigrateTestCase {
'fields' => $fields,
'primary key' => array('msgid'),
);
$table_schema['indexes']['sourcekey'] = array('sourceid1');
$schema->expects($this->at(2))
->method('tableExists')
@ -162,7 +169,20 @@ class MigrateSqlIdMapEnsureTablesTest extends MigrateTestCase {
$schema->expects($this->at(4))
->method('addField')
->with('migrate_map_sql_idmap_test', 'hash', $field_schema);
$schema->expects($this->exactly(5))
$schema->expects($this->at(5))
->method('fieldExists')
->with('migrate_map_sql_idmap_test', 'source_ids_hash')
->will($this->returnValue(FALSE));
$field_schema = array(
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
'description' => 'Hash of source ids. Used as primary key',
);
$schema->expects($this->at(6))
->method('addField')
->with('migrate_map_sql_idmap_test', 'source_ids_hash', $field_schema);
$schema->expects($this->exactly(7))
->method($this->anything());
$this->runEnsureTablesTest($schema);
}

View file

@ -171,6 +171,7 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
$expected_result = [
[
'sourceid1' => 'source_value',
'source_ids_hash' => $this->getIdMap()->getSourceIDsHash($source),
'destid1' => 2,
] + $this->idMapDefaults(),
];
@ -182,6 +183,7 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
$id_map->saveIdMapping($row, ['destination_id_property' => 3]);
$expected_result[] = [
'sourceid1' => 'source_value_1',
'source_ids_hash' => $this->getIdMap()->getSourceIDsHash($source),
'destid1' => 3,
] + $this->idMapDefaults();
$this->queryResultTest($this->getIdMapContents(), $expected_result);
@ -239,6 +241,7 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
$id_map->saveIdMapping($row, $destination, $status);
$expected_results[] = [
'sourceid1' => 'source_value_' . $status,
'source_ids_hash' => $this->getIdMap()->getSourceIDsHash($source),
'destid1' => 'destination_value_' . $status,
'source_row_status' => $status,
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
@ -296,20 +299,26 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
*/
public function testMessageSave() {
$message = 'Hello world.';
$expected_results = [
$original_values = [
1 => ['message' => $message, 'level' => MigrationInterface::MESSAGE_ERROR],
2 => ['message' => $message, 'level' => MigrationInterface::MESSAGE_WARNING],
3 => ['message' => $message, 'level' => MigrationInterface::MESSAGE_NOTICE],
4 => ['message' => $message, 'level' => MigrationInterface::MESSAGE_INFORMATIONAL],
];
$expected_results = [
'7ad742edb7e866caa78ced1e4455d2e9cbd8adb2074e7c323d21b4e67732e755' => ['message' => $message, 'level' => MigrationInterface::MESSAGE_ERROR],
'2d3ec2b0c547e819346e6ae03f881fd9f5c978ff3cbe29dfb807d40735e53703' => ['message' => $message, 'level' => MigrationInterface::MESSAGE_WARNING],
'12a042f72cad9a2a8c7715df0c7695d762975f0687d87f5d480725dae1432a6f' => ['message' => $message, 'level' => MigrationInterface::MESSAGE_NOTICE],
'd9d1fd27a2447ace48f47a2e9ff649673f67b446d9381a7963c949fc083f8791' => ['message' => $message, 'level' => MigrationInterface::MESSAGE_INFORMATIONAL],
];
$id_map = $this->getIdMap();
foreach ($expected_results as $key => $expected_result) {
$id_map->saveMessage(['source_id_property' => $key], $message, $expected_result['level']);
foreach ($original_values as $key => $original_value) {
$id_map->saveMessage(['source_id_property' => $key], $message, $original_value['level']);
}
foreach ($id_map->getMessageIterator() as $message_row) {
$key = $message_row->sourceid1;
$key = $message_row->source_ids_hash;
$this->assertEquals($expected_results[$key]['message'], $message_row->message);
$this->assertEquals($expected_results[$key]['level'], $message_row->level);
}
@ -344,12 +353,14 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
$row = [
'sourceid1' => 'source_id_value_1',
'sourceid2' => 'source_id_value_2',
'source_ids_hash' => $this->getIdMap()->getSourceIDsHash(['source_id_property' => 'source_id_value_1']),
'destid1' => 'destination_id_value_1',
] + $this->idMapDefaults();
$this->saveMap($row);
$row = [
'sourceid1' => 'source_id_value_3',
'sourceid2' => 'source_id_value_4',
'source_ids_hash' => $this->getIdMap()->getSourceIDsHash(['source_id_property' => 'source_id_value_3', 'sourceid2' => 'source_id_value_4']),
'destid1' => 'destination_id_value_2',
] + $this->idMapDefaults();
$this->saveMap($row);
@ -413,6 +424,7 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
$expected_result[] = "destination_id_value_$i";
$this->destinationIds["destination_id_property_$i"] = [];
}
$row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash($source_id_values);
$this->saveMap($row);
$id_map = $this->getIdMap();
// Test for a valid hit.
@ -430,12 +442,14 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
$row = [
'sourceid1' => 'source_id_value_1',
'sourceid2' => 'source_id_value_2',
'source_ids_hash' => $this->getIdMap()->getSourceIDsHash(['source_id_property' => 'source_id_value_1']),
'destid1' => 'destination_id_value_1',
] + $this->idMapDefaults();
$this->saveMap($row);
$row = [
'sourceid1' => 'source_id_value_3',
'sourceid2' => 'source_id_value_4',
'source_ids_hash' => $this->getIdMap()->getSourceIDsHash(['source_id_property' => 'source_id_value_3']),
'destid1' => 'destination_id_value_2',
] + $this->idMapDefaults();
$this->saveMap($row);
@ -487,9 +501,11 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
$this->sourceIds = [];
$this->destinationIds = [];
$row = $this->idMapDefaults();
$source_ids_values = [];
$expected_result = [];
for ($i = 1; $i <= $num_source_fields; $i++) {
$row["sourceid$i"] = "source_id_value_$i";
$source_ids_values = [$row["sourceid$i"]];
$expected_result["source_id_property_$i"] = "source_id_value_$i";
$this->sourceIds["source_id_property_$i"] = [];
}
@ -501,6 +517,7 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
$nonexistent_id_values["destination_id_property_$i"] = "nonexistent_destination_id_value_$i";
$this->destinationIds["destination_id_property_$i"] = [];
}
$row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash($source_ids_values);
$this->saveMap($row);
$id_map = $this->getIdMap();
// Test for a valid hit.
@ -607,6 +624,7 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
for ($i = 0; $i < 5; $i++) {
$row = $this->idMapDefaults();
$row['sourceid1'] = "source_id_value_$i";
$row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash(['source_id_property' => $row['sourceid1']]);
$row['destid1'] = "destination_id_value_$i";
$row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
$this->saveMap($row);
@ -614,6 +632,7 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
for (; $i < 5 + $num_update_rows; $i++) {
$row = $this->idMapDefaults();
$row['sourceid1'] = "source_id_value_$i";
$row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash(['source_id_property' => $row['sourceid1']]);
$row['destid1'] = "destination_id_value_$i";
$row['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
$this->saveMap($row);
@ -653,6 +672,7 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
for ($i = 0; $i < 5; $i++) {
$row = $this->idMapDefaults();
$row['sourceid1'] = "source_id_value_$i";
$row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash(['source_id_property' => $row['sourceid1']]);
$row['destid1'] = "destination_id_value_$i";
$row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
$this->saveMap($row);
@ -660,6 +680,7 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
for (; $i < 5 + $num_error_rows; $i++) {
$row = $this->idMapDefaults();
$row['sourceid1'] = "source_id_value_$i";
$row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash(['source_id_property' => $row['sourceid1']]);
$row['destid1'] = "destination_id_value_$i";
$row['source_row_status'] = MigrateIdMapInterface::STATUS_FAILED;
$this->saveMap($row);
@ -687,6 +708,7 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
$id_map->saveIdMapping($row, $destination, $status);
$expected_results[] = [
'sourceid1' => 'source_value_' . $status,
'source_ids_hash' => $this->getIdMap()->getSourceIDsHash($source),
'destid1' => 'destination_value_' . $status,
'source_row_status' => $status,
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
@ -815,6 +837,7 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
for ($i = 0; $i < 3; $i++) {
$row = $this->idMapDefaults();
$row['sourceid1'] = "source_id_value_$i";
$row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash(['source_id_property' => $row['sourceid1']]);
$row['destid1'] = "destination_id_value_$i";
$row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
$expected_results[serialize(['sourceid1' => $row['sourceid1']])] = ['destid1' => $row['destid1']];

View file

@ -0,0 +1,82 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\process\ExplodeTest.
*/
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\Plugin\migrate\process\Explode;
use Drupal\migrate\Plugin\migrate\process\Concat;
/**
* Tests the Explode process plugin.
*
* @group migrate
*/
class ExplodeTest extends MigrateProcessTestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
$configuration = [
'delimiter' => ',',
];
$this->plugin = new Explode($configuration, 'map', []);
parent::setUp();
}
/**
* Test explode transform process works.
*/
public function testTransform() {
$value = $this->plugin->transform('foo,bar,tik', $this->migrateExecutable, $this->row, 'destinationproperty');
$this->assertSame($value, ['foo', 'bar', 'tik']);
}
/**
* Test explode transform process works with a limit.
*/
public function testTransformLimit() {
$plugin = new Explode(['delimiter' => '_', 'limit' => 2], 'map', []);
$value = $plugin->transform('foo_bar_tik', $this->migrateExecutable, $this->row, 'destinationproperty');
$this->assertSame($value, ['foo', 'bar_tik']);
}
/**
* Test if the explode process can be chained with a handles_multiple process.
*/
public function testChainedTransform() {
$exploded = $this->plugin->transform('foo,bar,tik', $this->migrateExecutable, $this->row, 'destinationproperty');
$concat = new Concat([], 'map', []);
$concatenated = $concat->transform($exploded, $this->migrateExecutable, $this->row, 'destinationproperty');
$this->assertSame($concatenated, 'foobartik');
}
/**
* Test explode fails properly on non-strings.
*
* @expectedException \Drupal\migrate\MigrateException
*
* @expectedExceptionMessage is not a string
*/
public function testExplodeWithNonString() {
$this->plugin->transform(['foo'], $this->migrateExecutable, $this->row, 'destinationproperty');
}
/**
* Test explode fails with empty delimiter.
*
* @expectedException \Drupal\migrate\MigrateException
*
* @expectedExceptionMessage delimiter is empty
*/
public function testExplodeWithEmptyDelimiter() {
$plugin = new Explode(['delimiter' => ''], 'map', []);
$plugin->transform('foo,bar', $this->migrateExecutable, $this->row, 'destinationproperty');
}
}

View file

@ -0,0 +1,92 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\process\MigrationTest.
*/
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Plugin\migrate\process\Migration;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
use Drupal\migrate\Plugin\MigrateDestinationInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigratePluginManager;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\process\Migration
* @group migrate
*/
class MigrationTest extends MigrateProcessTestCase {
/**
* @covers ::transform
*/
public function testTransformWithStubSkipping() {
$migration_entity = $this->prophesize(MigrationInterface::class);
$migration_storage = $this->prophesize(EntityStorageInterface::class);
$process_plugin_manager = $this->prophesize(MigratePluginManager::class);
$destination_id_map = $this->prophesize(MigrateIdMapInterface::class);
$destination_migration = $this->prophesize(MigrationInterface::class);
$destination_migration->getIdMap()->willReturn($destination_id_map->reveal());
$migration_storage->loadMultiple(['destination_migration'])
->willReturn(['destination_migration' => $destination_migration->reveal()]);
$destination_id_map->lookupDestinationId([1])->willReturn(NULL);
$configuration = [
'no_stub' => TRUE,
'migration' => 'destination_migration',
];
$migration_entity->id()->willReturn('actual_migration');
$destination_migration->getDestinationPlugin(TRUE)->shouldNotBeCalled();
$migration = new Migration($configuration, '', [], $migration_entity->reveal(), $migration_storage->reveal(), $process_plugin_manager->reveal());
$result = $migration->transform(1, $this->migrateExecutable, $this->row, '');
$this->assertNull($result);
}
/**
* @covers ::transform
*/
public function testTransformWithStubbing() {
$migration_entity = $this->prophesize(MigrationInterface::class);
$migration_storage = $this->prophesize(EntityStorageInterface::class);
$process_plugin_manager = $this->prophesize(MigratePluginManager::class);
$destination_id_map = $this->prophesize(MigrateIdMapInterface::class);
$destination_migration = $this->prophesize(MigrationInterface::class);
$destination_migration->getIdMap()->willReturn($destination_id_map->reveal());
$migration_storage->loadMultiple(['destination_migration'])
->willReturn(['destination_migration' => $destination_migration->reveal()]);
$destination_id_map->lookupDestinationId([1])->willReturn(NULL);
$configuration = [
'no_stub' => FALSE,
'migration' => 'destination_migration',
];
$migration_entity->id()->willReturn('actual_migration');
$destination_migration->id()->willReturn('destination_migration');
$destination_migration->getDestinationPlugin(TRUE)->shouldBeCalled();
$destination_migration->get('process')->willReturn([]);
$destination_migration->get('source')->willReturn([]);
$source_plugin = $this->prophesize(MigrateSourceInterface::class);
$source_plugin->getIds()->willReturn(['nid']);
$destination_migration->getSourcePlugin()->willReturn($source_plugin->reveal());
$destination_plugin = $this->prophesize(MigrateDestinationInterface::class);
$destination_plugin->import(Argument::any())->willReturn([2]);
$destination_migration->getDestinationPlugin(TRUE)->willReturn($destination_plugin->reveal());
$migration = new Migration($configuration, '', [], $migration_entity->reveal(), $migration_storage->reveal(), $process_plugin_manager->reveal());
$result = $migration->transform(1, $this->migrateExecutable, $this->row, '');
$this->assertEquals(2, $result);
}
}

View file

@ -1,4 +1,5 @@
<?php
// @codingStandardsIgnoreFile
/**
* @file
* A database agnostic dump for testing purposes.
@ -32353,7 +32354,7 @@ $connection->insert('url_alias')
'pid' => '1',
'src' => 'node/1',
'dst' => 'alias-one',
'language' => 'en',
'language' => 'af',
))
->values(array(
'pid' => '2',
@ -32361,6 +32362,12 @@ $connection->insert('url_alias')
'dst' => 'alias-two',
'language' => 'en',
))
->values(array(
'pid' => '3',
'src' => 'node/3',
'dst' => 'alias-three',
'language' => '',
))
->execute();
$connection->schema()->createTable('users', array(

View file

@ -1,4 +1,5 @@
<?php
// @codingStandardsIgnoreFile
/**
* @file
* A database agnostic dump for testing purposes.

View file

@ -73,26 +73,6 @@ views.argument_default.node:
type: string
label: 'Nid'
views.argument_validator.node:
type: mapping
label: 'Content'
mapping:
types:
type: sequence
label: 'Content types'
sequence:
type: string
label: 'Type'
access:
type: boolean
label: 'Validate user has access to the content'
access_op:
type: boolean
label: 'Access operation to check'
nid_type:
type: string
label: 'Filter value format'
views.field.node:
type: views_field
label: 'Node'

View file

@ -148,10 +148,9 @@ function hook_node_grants(\Drupal\Core\Session\AccountInterface $account, $op) {
* @param \Drupal\node\NodeInterface $node
* The node that has just been saved.
*
* @return
* @return array
* An array of grants as defined above.
*
* @see node_access_write_grants()
* @see hook_node_access_records_alter()
* @ingroup node_access
*/

View file

@ -1155,6 +1155,11 @@ function node_access_rebuild($batch_mode = FALSE) {
// quickly.
$entity_query = \Drupal::entityQuery('node');
$entity_query->sort('nid', 'DESC');
// Disable access checking since all nodes must be processed even if the
// user does not have access. And unless the current user has the bypass
// node access permission, no nodes are accessible since the grants have
// just been deleted.
$entity_query->accessCheck(false);
$nids = $entity_query->execute();
foreach ($nids as $nid) {
$node_storage->resetCache(array($nid));
@ -1204,6 +1209,11 @@ function _node_access_rebuild_batch_operation(&$context) {
$nids = \Drupal::entityQuery('node')
->condition('nid', $context['sandbox']['current_node'], '>')
->sort('nid', 'ASC')
// Disable access checking since all nodes must be processed even if the
// user does not have access. And unless the current user has the bypass
// node access permission, no nodes are accessible since the grants have
// just been deleted.
->accessCheck(false)
->range(0, $limit)
->execute();
$node_storage->resetCache($nids);

View file

@ -54,9 +54,11 @@ class NodeViewsData extends EntityViewsData {
),
);
$data['node_field_data']['promote']['help'] = t('A boolean indicating whether the node is visible on the front page.');
$data['node_field_data']['promote']['filter']['label'] = t('Promoted to front page status');
$data['node_field_data']['promote']['filter']['type'] = 'yes-no';
$data['node_field_data']['sticky']['help'] = t('A boolean indicating whether the node should sort to the top of content lists.');
$data['node_field_data']['sticky']['filter']['label'] = t('Sticky status');
$data['node_field_data']['sticky']['filter']['type'] = 'yes-no';
$data['node_field_data']['sticky']['sort']['help'] = t('Whether or not the content is sticky. To list sticky content first, set this to descending.');
@ -256,6 +258,10 @@ class NodeViewsData extends EntityViewsData {
$data['node_field_revision']['status']['filter']['type'] = 'yes-no';
$data['node_field_revision']['status']['filter']['use_equal'] = TRUE;
$data['node_field_revision']['promote']['help'] = t('A boolean indicating whether the node is visible on the front page.');
$data['node_field_revision']['sticky']['help'] = t('A boolean indicating whether the node should sort to the top of content lists.');
$data['node_field_revision']['langcode']['help'] = t('The language of the content or translation.');
$data['node_field_revision']['link_to_revision'] = array(

View file

@ -9,6 +9,8 @@ namespace Drupal\node\Tests\Migrate\d6;
use Drupal\migrate\Entity\Migration;
use Drupal\Core\Database\Database;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\node\Entity\Node;
/**
@ -24,9 +26,6 @@ class MigrateNodeTest extends MigrateNodeTestBase {
protected function setUp() {
parent::setUp();
$this->executeMigrations(['d6_node:*']);
// This is required for the second import below.
\Drupal::database()->truncate(Migration::load('d6_node__story')->getIdMap()->mapTableName())->execute();
}
/**
@ -53,27 +52,61 @@ class MigrateNodeTest extends MigrateNodeTestBase {
// This is empty on the first revision.
$this->assertIdentical(NULL, $node_revision->revision_log->value);
$node = Node::load(2);
$this->assertIdentical('Test title rev 3', $node->getTitle());
$this->assertIdentical('test rev 3', $node->body->value);
$this->assertIdentical('filtered_html', $node->body->format);
// Test that we can re-import using the EntityContentBase destination.
$connection = Database::getConnection('default', 'migrate');
$connection->update('node_revisions')
$title = $this->rerunMigration();
$node = Node::load(2);
$this->assertIdentical($title, $node->getTitle());
// Test multi-column fields are correctly upgraded.
$this->assertIdentical('test rev 3', $node->body->value);
$this->assertIdentical('full_html', $node->body->format);
// Now insert a row indicating a failure and set to update later.
$title = $this->rerunMigration(array(
'sourceid1' => 2,
'destid1' => NULL,
'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
));
$node = Node::load(2);
$this->assertIdentical($title, $node->getTitle());
}
/**
* Execute the migration a second time.
*
* @param array $new_row
* An optional row to be inserted into the id map.
*
* @return string
* The new title in the source for vid 3.
*/
protected function rerunMigration($new_row = []) {
$title = $this->randomString();
$migration = Migration::load('d6_node__story');
$source_connection = Database::getConnection('default', 'migrate');
$source_connection->update('node_revisions')
->fields(array(
'title' => 'New node title',
'title' => $title,
'format' => 2,
))
->condition('vid', 1)
->condition('vid', 3)
->execute();
$connection->delete('content_field_test_two')
->condition('delta', 1)
->execute();
$migration = Migration::load('d6_node__story');
$table_name = $migration->getIdMap()->mapTableName();
$default_connection = \Drupal::database();
$default_connection->truncate($table_name)->execute();
if ($new_row) {
$hash = $migration->getIdMap()->getSourceIDsHash(['nid' => $new_row['sourceid1']]);
$new_row['source_ids_hash'] = $hash;
$default_connection->insert($table_name)
->fields($new_row)
->execute();
}
$this->executeMigration($migration);
$node = Node::load(1);
$this->assertIdentical('New node title', $node->getTitle());
// Test a multi-column fields are correctly upgraded.
$this->assertIdentical('test', $node->body->value);
$this->assertIdentical('full_html', $node->body->format);
return $title;
}
}

View file

@ -171,6 +171,22 @@ class NodeAccessBaseTableTest extends NodeTestBase {
// This user should be able to see all of the nodes on the relevant
// taxonomy pages.
$this->assertTaxonomyPage(TRUE);
// Rebuild the node access permissions, repeat the test. This is done to
// ensure that node access is rebuilt correctly even if the current user
// does not have the bypass node access permission.
node_access_rebuild();
foreach ($this->nodesByUser as $private_status) {
foreach ($private_status as $nid => $is_private) {
$this->drupalGet('node/' . $nid);
$this->assertResponse(200);
}
}
// This user should be able to see all of the nodes on the relevant
// taxonomy pages.
$this->assertTaxonomyPage(TRUE);
}
/**

View file

@ -0,0 +1,80 @@
<?php
/**
* @file
* Contains \Drupal\node\Tests\Views\RevisionCreateTimestampTest.
*/
namespace Drupal\node\Tests\Views;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\views\Tests\ViewKernelTestBase;
use Drupal\views\Tests\ViewTestData;
use Drupal\views\Views;
/**
* Ensures that the revision create time can be accessed in views.
*
* @group views
*/
class RevisionCreateTimestampTest extends ViewKernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['node_test_views', 'node', 'views', 'user'];
/**
* {@inheritdoc}
*/
public static $testViews = ['test_node_revision_timestamp'];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE) {
parent::setUp($import_test_views);
$this->installSchema('node', 'node_access');
$this->installEntitySchema('node');
$this->installEntitySchema('user');
if ($import_test_views) {
ViewTestData::createTestViews(get_class($this), ['node_test_views']);
}
}
public function testRevisionCreateTimestampView() {
$node_type = NodeType::create([
'type' => 'article',
'label' => 'Article',
]);
$node_type->save();
$node = Node::create([
'title' => 'Test node',
'type' => 'article',
'revision_timestamp' => 1000,
]);
$node->save();
$node->setRevisionCreationTime(1200);
$node->setNewRevision(TRUE);
$node->save();
$node->setRevisionCreationTime(1400);
$node->setNewRevision(TRUE);
$node->save();
$view = Views::getView('test_node_revision_timestamp');
$this->executeView($view);
$this->assertIdenticalResultset($view, [
['vid' => 3, 'revision_timestamp' => 1400],
['vid' => 2, 'revision_timestamp' => 1200],
['vid' => 1, 'revision_timestamp' => 1000],
], ['vid' => 'vid', 'revision_timestamp' => 'revision_timestamp']);
}
}

View file

@ -0,0 +1,44 @@
langcode: en
status: true
dependencies:
module:
- node
id: test_node_revision_timestamp
label: null
module: views
description: ''
tag: ''
base_table: node_field_revision
base_field: vid
core: '8'
display:
default:
display_options:
fields:
vid:
id: vid
table: node_field_revision
field: vid
plugin_id: field
entity_type: node
entity_field: vid
revision_timestamp:
id: revision_timestamp
table: node_revision
field: revision_timestamp
plugin_id: field
entity_type: node
entity_field: revision_timestamp
sorts:
revision_timestamp:
id: revision_timestamp
table: node_revision
field: revision_timestamp
order: DESC
plugin_id: field
entity_type: node
entity_field: revision_timestamp
display_plugin: default
display_title: Master
id: default
position: 0

View file

@ -17,6 +17,8 @@ process:
source:
- constants/slash
- dst
langcode: language
langcode:
plugin: d6_url_alias_language
source: language
destination:
plugin: url_alias

View file

@ -0,0 +1,32 @@
<?php
/**
* @file
* Contains \Drupal\path\Plugin\migrate\process\d6\UrlAliasLanguage.
*/
namespace Drupal\path\Plugin\migrate\process\d6;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Drupal\Core\Language\LanguageInterface;
/**
* Url alias language code process.
*
* @MigrateProcessPlugin(
* id = "d6_url_alias_language"
* )
*/
class UrlAliasLanguage extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$langcode = ($value === '') ? LanguageInterface::LANGCODE_NOT_SPECIFIED : $value;
return $langcode;
}
}

View file

@ -33,27 +33,44 @@ class MigrateUrlAliasTest extends MigrateDrupal6TestBase {
$this->executeMigration('d6_url_alias');
}
/**
* Assert a path.
*
* @param string pid
* The path id.
* @param array $conditions
* The path conditions.
* @param array $path
* The path.
*/
private function assertPath($pid, $conditions, $path) {
$this->assertTrue($path, "Path alias for " . $conditions['source'] . " successfully loaded.");
$this->assertIdentical($conditions['alias'], $path['alias']);
$this->assertIdentical($conditions['langcode'], $path['langcode']);
$this->assertIdentical($conditions['source'], $path['source']);
}
/**
* Test the url alias migration.
*/
public function testUrlAlias() {
$id_map = Migration::load('d6_url_alias')->getIdMap();
// Test that the field exists.
$conditions = array(
'source' => '/node/1',
'alias' => '/alias-one',
'langcode' => 'en',
'langcode' => 'af',
);
$path = \Drupal::service('path.alias_storage')->load($conditions);
$this->assertNotNull($path, "Path alias for node/1 successfully loaded.");
$this->assertPath('1', $conditions, $path);
$this->assertIdentical($id_map->lookupDestinationID(array($path['pid'])), array('1'), "Test IdMap");
$conditions = array(
'source' => '/node/2',
'alias' => '/alias-two',
'langcode' => 'en',
);
$path = \Drupal::service('path.alias_storage')->load($conditions);
$this->assertNotNull($path, "Path alias for node/2 successfully loaded.");
$this->assertPath('2', $conditions, $path);
// Test that we can re-import using the UrlAlias destination.
Database::getConnection('default', 'migrate')
@ -72,7 +89,16 @@ class MigrateUrlAliasTest extends MigrateDrupal6TestBase {
$this->executeMigration($migration);
$path = \Drupal::service('path.alias_storage')->load(array('pid' => $path['pid']));
$this->assertIdentical('/new-url-alias', $path['alias']);
$conditions['alias'] = '/new-url-alias';
$this->assertPath('2', $conditions, $path);
$conditions = array(
'source' => '/node/3',
'alias' => '/alias-three',
'langcode' => 'und',
);
$path = \Drupal::service('path.alias_storage')->load($conditions);
$this->assertPath('3', $conditions, $path);
}
}

View file

@ -45,19 +45,24 @@ class EntityResource extends ResourceBase {
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function get(EntityInterface $entity) {
if (!$entity->access('view')) {
$entity_access = $entity->access('view', NULL, TRUE);
if (!$entity_access->isAllowed()) {
throw new AccessDeniedHttpException();
}
foreach ($entity as $field_name => $field) {
if (!$field->access('view')) {
unset($entity->{$field_name});
}
}
$response = new ResourceResponse($entity, 200);
// Make the response use the entity's cacheability metadata.
// @todo include access cacheability metadata, for the access checks above.
$response->addCacheableDependency($entity);
$response->addCacheableDependency($entity_access);
foreach ($entity as $field_name => $field) {
/** @var \Drupal\Core\Field\FieldItemListInterface $field */
$field_access = $field->access('view', NULL, TRUE);
$response->addCacheableDependency($field_access);
if (!$field_access->isAllowed()) {
$entity->set($field_name, NULL);
}
}
return $response;
}
@ -110,7 +115,8 @@ class EntityResource extends ResourceBase {
// 201 Created responses have an empty body.
$url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE);
$response = new ResourceResponse(NULL, 201, ['Location' => $url->getGeneratedUrl()]);
$response->addCacheableDependency($url);
// Responses after creating an entity are not cacheable, so we add no
// cacheability metadata here.
return $response;
}
catch (EntityStorageException $e) {

View file

@ -135,7 +135,7 @@ class Serializer extends StylePluginBase implements CacheableDependencyInterface
else {
$content_type = !empty($this->options['formats']) ? reset($this->options['formats']) : 'json';
}
return $this->serializer->serialize($rows, $content_type);
return $this->serializer->serialize($rows, $content_type, ['views_style_plugin' => $this]);
}
/**

View file

@ -102,8 +102,8 @@ class RequestHandler implements ContainerAwareInterface {
return new Response($content, $e->getStatusCode(), $headers);
}
// Serialize the outgoing data for the response, if available.
if ($response instanceof ResourceResponse && $data = $response->getResponseData()) {
if ($response instanceof ResourceResponse) {
$data = $response->getResponseData();
// Serialization can invoke rendering (e.g., generating URLs), but the
// serialization API does not provide a mechanism to collect the
// bubbleable metadata associated with that (e.g., language and other

View file

@ -64,7 +64,17 @@ class ResourceRoutes extends RouteSubscriberBase {
*/
protected function alterRoutes(RouteCollection $collection) {
$routes = array();
$enabled_resources = $this->config->get('rest.settings')->get('resources') ?: array();
// Silently ignore resources that are in the settings but are not defined on
// the plugin manager currently. That avoids exceptions when REST module is
// enabled before another module that provides the resource plugin specified
// in the settings.
// @todo Remove in https://www.drupal.org/node/2308745
$resources = $this->config->get('rest.settings')->get('resources') ?: array();
$enabled_resources = array_intersect_key($resources, $this->manager->getDefinitions());
if (count($resources) != count($enabled_resources)) {
trigger_error('rest.settings lists resources relying on the following missing plugins: ' . implode(', ', array_keys(array_diff_key($resources, $enabled_resources))));
}
// Iterate over all enabled resource plugins.
foreach ($enabled_resources as $id => $enabled_methods) {

View file

@ -74,7 +74,7 @@ class AuthTest extends RESTTestBase {
* set curl settings for basic authentication.
*
* @param \Drupal\Core\Url $url
* An Url object.
* A Url object.
* @param string $username
* The user name to authenticate with.
* @param string $password

View file

@ -199,8 +199,8 @@ class CreateTest extends RESTTestBase {
}
else {
// Changed and revision_timestamp fields can never be added.
unset($entity->changed);
unset($entity->revision_timestamp);
$entity->set('changed', NULL);
$entity->set('revision_timestamp', NULL);
}
$serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]);
@ -299,7 +299,7 @@ class CreateTest extends RESTTestBase {
}
// Changed field can never be added.
unset($entity->changed);
$entity->set('changed', NULL);
$serialized = $this->serializer->serialize($entity, $this->defaultFormat, ['account' => $account]);

View file

@ -33,6 +33,7 @@ class PageCacheTest extends RESTTestBase {
// Create an entity programmatically.
$entity = $this->entityCreate('entity_test');
$entity->set('field_test_text', 'custom cache tag value');
$entity->save();
// Read it over the REST API.
$this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET', NULL, $this->defaultMimeType);
@ -40,6 +41,7 @@ class PageCacheTest extends RESTTestBase {
$this->assertHeader('x-drupal-cache', 'MISS');
$this->assertCacheTag('config:rest.settings');
$this->assertCacheTag('entity_test:1');
$this->assertCacheTag('entity_test_access:field_test_text');
// Read it again, should be page-cached now.
$this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET', NULL, $this->defaultMimeType);
@ -47,6 +49,7 @@ class PageCacheTest extends RESTTestBase {
$this->assertHeader('x-drupal-cache', 'HIT');
$this->assertCacheTag('config:rest.settings');
$this->assertCacheTag('entity_test:1');
$this->assertCacheTag('entity_test_access:field_test_text');
// Trigger a config save which should clear the page cache, so we should get
// a cache miss now for the same request.
@ -56,6 +59,7 @@ class PageCacheTest extends RESTTestBase {
$this->assertHeader('x-drupal-cache', 'MISS');
$this->assertCacheTag('config:rest.settings');
$this->assertCacheTag('entity_test:1');
$this->assertCacheTag('entity_test_access:field_test_text');
}
}

View file

@ -6,6 +6,8 @@
*/
namespace Drupal\rest\Tests;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\Entity\Role;
@ -121,4 +123,30 @@ class ResourceTest extends RESTTestBase {
}
}
/**
* Tests that a resource with a missing plugin does not cause an exception.
*/
public function testMissingPlugin() {
$settings = array(
'entity:nonexisting' => array(
'GET' => array(
'supported_formats' => array(
'hal_json',
),
),
),
);
try {
// Attempt to enable the resource.
$this->config->set('resources', $settings);
$this->config->save();
$this->rebuildCache();
$this->pass('rest.settings referencing a missing REST resource plugin does not cause an exception.');
}
catch (PluginNotFoundException $e) {
$this->fail('rest.settings referencing a missing REST resource plugin caused an exception.');
}
}
}

View file

@ -52,7 +52,7 @@ class UpdateTest extends RESTTestBase {
));
$patch_entity = entity_create($entity_type, $patch_values);
// We don't want to overwrite the UUID.
unset($patch_entity->uuid);
$patch_entity->set('uuid', NULL);
$serialized = $serializer->serialize($patch_entity, $this->defaultFormat, $context);
// Update the entity over the REST API.

View file

@ -0,0 +1,135 @@
<?php
/**
* @file
* Contains \Drupal\Tests\rest\Kernel\RequestHandlerTest.
*/
namespace Drupal\Tests\rest\Kernel;
use Drupal\Core\Routing\RouteMatch;
use Drupal\KernelTests\KernelTestBase;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\Plugin\Type\ResourcePluginManager;
use Drupal\rest\RequestHandler;
use Drupal\rest\ResourceResponse;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Test REST RequestHandler controller logic.
*
* @group rest
* @coversDefaultClass \Drupal\rest\RequestHandler
*/
class RequestHandlerTest extends KernelTestBase {
/**
* @var \Drupal\rest\RequestHandler
*/
protected $requestHandler;
public static $modules = ['serialization', 'rest'];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->requestHandler = new RequestHandler();
$this->requestHandler->setContainer($this->container);
}
/**
* Assert some basic handler method logic.
*
* @covers ::handle
*/
public function testBaseHandler() {
$request = new Request();
$route_match = new RouteMatch('test', new Route('/rest/test', ['_plugin' => 'restplugin', '_format' => 'json']));
$resource = $this->prophesize(StubRequestHandlerResourcePlugin::class);
$resource->get(NULL, $request)
->shouldBeCalled();
// Setup stub plugin manager that will return our plugin.
$stub = $this->prophesize(ResourcePluginManager::class);
$stub->getInstance(['id' => 'restplugin'])
->willReturn($resource->reveal());
$this->container->set('plugin.manager.rest', $stub->reveal());
// Response returns NULL this time because response from plugin is not
// a ResourceResponse so it is passed through directly.
$response = $this->requestHandler->handle($route_match, $request);
$this->assertEquals(NULL, $response);
// Response will return a ResourceResponse this time.
$response = new ResourceResponse([]);
$resource->get(NULL, $request)
->willReturn($response);
$handler_response = $this->requestHandler->handle($route_match, $request);
$this->assertEquals($response, $handler_response);
// We will call the patch method this time.
$request->setMethod('PATCH');
$response = new ResourceResponse([]);
$resource->patch(NULL, $request)
->shouldBeCalledTimes(1)
->willReturn($response);
$handler_response = $this->requestHandler->handle($route_match, $request);
$this->assertEquals($response, $handler_response);
}
/**
* Test that given structured data, the request handler will serialize it.
*
* @dataProvider providerTestSerialization
* @covers ::handle
*/
public function testSerialization($data) {
$request = new Request();
$route_match = new RouteMatch('test', new Route('/rest/test', ['_plugin' => 'restplugin', '_format' => 'json']));
$resource = $this->prophesize(StubRequestHandlerResourcePlugin::class);
// Setup stub plugin manager that will return our plugin.
$stub = $this->prophesize(ResourcePluginManager::class);
$stub->getInstance(['id' => 'restplugin'])
->willReturn($resource->reveal());
$this->container->set('plugin.manager.rest', $stub->reveal());
$response = new ResourceResponse($data);
$resource->get(NULL, $request)
->willReturn($response);
$handler_response = $this->requestHandler->handle($route_match, $request);
// Content is a serialized version of the data we provided.
$this->assertEquals(json_encode($data), $handler_response->getContent());
}
public function providerTestSerialization() {
return [
[NULL],
[''],
['string'],
['Complex \ string $%^&@ with unicode ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ'],
[[]],
[['test']],
[['test' => 'foobar']],
[TRUE],
[FALSE],
// @todo Not supported. https://www.drupal.org/node/2427811
// [new \stdClass()],
// [(object) ['test' => 'foobar']],
];
}
}
class StubRequestHandlerResourcePlugin extends ResourceBase {
/** stub methods so they can be prophesied. */
function get() {}
function patch() {}
}

View file

@ -0,0 +1,83 @@
<?php
/**
* @file
* Contains \Drupal\Tests\rest\Unit\Plugin\views\style\SerializerTest.
*/
namespace Drupal\Tests\rest\Unit\Plugin\views\style;
use Drupal\rest\Plugin\views\display\RestExport;
use Drupal\rest\Plugin\views\style\Serializer;
use Drupal\Tests\UnitTestCase;
use Drupal\views\ViewExecutable;
use Prophecy\Argument;
use Symfony\Component\Serializer\SerializerInterface;
/**
* @coversDefaultClass \Drupal\rest\Plugin\views\style\Serializer
* @group rest
*/
class SerializerTest extends UnitTestCase {
/**
* The View instance.
*
* @var \Drupal\views\ViewExecutable|\PHPUnit_Framework_MockObject_MockObject
*/
protected $view;
/**
* The RestExport display handler.
*
* @var \Drupal\rest\Plugin\views\display\RestExport|\PHPUnit_Framework_MockObject_MockObject
*/
protected $displayHandler;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->view = $this->getMockBuilder(ViewExecutable::class)
->disableOriginalConstructor()
->getMock();
// Make the view result empty so we don't have to mock the row plugin render
// call.
$this->view->result = [];
$this->displayHandler = $this->getMockBuilder(RestExport::class)
->disableOriginalConstructor()
->getMock();
$this->displayHandler->expects($this->any())
->method('getContentType')
->willReturn('json');
}
/**
* Tests that the symfony serializer receives style plugin from the render() method.
*
* @covers ::render
*/
public function testSerializerReceivesOptions() {
$mock_serializer = $this->prophesize(SerializerInterface::class);
// This is the main expectation of the test. We want to make sure the
// serializer options are passed to the SerializerInterface object.
$mock_serializer->serialize([], 'json', Argument::that(function ($argument) {
return isset($argument['views_style_plugin']) && $argument['views_style_plugin'] instanceof Serializer;
}))
->willReturn()
->shouldBeCalled();
$view_serializer_style = new Serializer([], 'dummy_serializer', [], $mock_serializer->reveal(), ['json', 'xml']);
$view_serializer_style->options = ['formats' => ['xml', 'json']];
$view_serializer_style->view = $this->view;
$view_serializer_style->displayHandler = $this->displayHandler;
$view_serializer_style->render();
}
}

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