Update to Drupal 8.1.8. For more information, see https://www.drupal.org/project/drupal/releases/8.1.8

This commit is contained in:
Pantheon Automation 2016-08-03 13:22:33 -07:00 committed by Greg Anderson
parent e9f047ccf8
commit f9f23cdf38
312 changed files with 6751 additions and 1546 deletions

View file

@ -12,6 +12,12 @@
* or print a subset such as {{ content.field_example }}. Use
* {{ content|without('field_example') }} to temporarily suppress the printing
* of a given element.
* - title_attributes: Same as attributes, except applied to the main title
* tag that appears in the template.
* - title_prefix: Additional output populated by modules, intended to be
* displayed in front of the main title tag that appears in the template.
* - title_suffix: Additional output populated by modules, intended to be
* displayed after the main title tag that appears in the template.
*
* @see template_preprocess_aggregator_feed()
*

View file

@ -10,6 +10,11 @@
* or print a subset such as {{ content.field_example }}. Use
* {{ content|without('field_example') }} to temporarily suppress the printing
* of a given element.
* - attributes: HTML attributes for the wrapper.
* - title_prefix: Additional output populated by modules, intended to be
* displayed in front of the main title tag that appears in the template.
* - title_suffix: Additional output populated by modules, intended to be
* displayed after the main title tag that appears in the template.
*
* @see template_preprocess_aggregator_item()
*

View file

@ -1,6 +1,6 @@
<?php
namespace Drupal\tests\aggregator\Kernel\Migrate;
namespace Drupal\Tests\aggregator\Kernel\Migrate;
use Drupal\migrate\MigrateException;
use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;

View file

@ -44,7 +44,8 @@ class BanIpManager implements BanIpManagerInterface {
* {@inheritdoc}
*/
public function banIp($ip) {
$this->connection->insert('ban_ip')
$this->connection->merge('ban_ip')
->key(array('ip' => $ip))
->fields(array('ip' => $ip))
->execute();
}

View file

@ -3,6 +3,8 @@
namespace Drupal\Tests\ban\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Core\Database\Database;
use Drupal\ban\BanIpManager;
/**
* Tests IP address banning.
@ -29,7 +31,7 @@ class IpAddressBlockingTest extends BrowserTestBase {
// Ban a valid IP address.
$edit = array();
$edit['ip'] = '192.168.1.1';
$edit['ip'] = '1.2.3.3';
$this->drupalPostForm('admin/config/people/ban', $edit, t('Add'));
$ip = db_query("SELECT iid from {ban_ip} WHERE ip = :ip", array(':ip' => $edit['ip']))->fetchField();
$this->assertTrue($ip, 'IP address found in database.');
@ -37,7 +39,7 @@ class IpAddressBlockingTest extends BrowserTestBase {
// Try to block an IP address that's already blocked.
$edit = array();
$edit['ip'] = '192.168.1.1';
$edit['ip'] = '1.2.3.3';
$this->drupalPostForm('admin/config/people/ban', $edit, t('Add'));
$this->assertText(t('This IP address is already banned.'));
@ -73,6 +75,28 @@ class IpAddressBlockingTest extends BrowserTestBase {
// $edit['ip'] = \Drupal::request()->getClientIP();
// $this->drupalPostForm('admin/config/people/ban', $edit, t('Save'));
// $this->assertText(t('You may not ban your own IP address.'));
// Test duplicate ip address are not present in the 'blocked_ips' table.
// when they are entered programmatically.
$connection = Database::getConnection();
$banIp = new BanIpManager($connection);
$ip = '1.0.0.0';
$banIp->banIp($ip);
$banIp->banIp($ip);
$banIp->banIp($ip);
$query = db_select('ban_ip', 'bip');
$query->fields('bip', array('iid'));
$query->condition('bip.ip', $ip);
$ip_count = $query->execute()->fetchAll();
$this->assertEqual(1, count($ip_count));
$ip = '';
$banIp->banIp($ip);
$banIp->banIp($ip);
$query = db_select('ban_ip', 'bip');
$query->fields('bip', array('iid'));
$query->condition('bip.ip', $ip);
$ip_count = $query->execute()->fetchAll();
$this->assertEqual(1, count($ip_count));
}
}

View file

@ -89,7 +89,10 @@
}, interval);
}
var interval = 200;
// The frequency with which to check for newly arrived BigPipe placeholders.
// Hence 50 ms means we check 20 times per second. Setting this to 100 ms or
// more would cause the user to see content appear noticeably slower.
var interval = drupalSettings.bigPipeInterval || 50;
// The internal ID to contain the watcher service.
var timeoutID;

View file

@ -126,9 +126,15 @@ class BigPipe implements BigPipeInterface {
// Reopen it for the duration that we are rendering placeholders.
$this->session->start();
list($pre_body, $post_body) = explode('</body>', $content, 2);
// Find the closing </body> tag and get the strings before and after. But be
// careful to use the latest occurrence of the string "</body>", to ensure
// that strings in inline JavaScript or CDATA sections aren't used instead.
$parts = explode('</body>', $content);
$post_body = array_pop($parts);
$pre_body = implode('', $parts);
$this->sendPreBody($pre_body, $nojs_placeholders, $cumulative_assets);
$this->sendPlaceholders($placeholders, $this->getPlaceholderOrder($pre_body), $cumulative_assets);
$this->sendPlaceholders($placeholders, $this->getPlaceholderOrder($pre_body, $placeholders), $cumulative_assets);
$this->sendPostBody($post_body);
// Close the session again.
@ -528,6 +534,9 @@ EOF;
*
* @param string $html
* HTML markup.
* @param array $placeholders
* Associative array; the BigPipe placeholders. Keys are the BigPipe
* placeholder IDs.
*
* @return array
* Indexed array; the order in which the BigPipe placeholders must be sent.
@ -535,18 +544,45 @@ EOF;
* placeholders are kept: if the same placeholder occurs multiple times, we
* only keep the first occurrence.
*/
protected function getPlaceholderOrder($html) {
protected function getPlaceholderOrder($html, $placeholders) {
$fragments = explode('<div data-big-pipe-placeholder-id="', $html);
array_shift($fragments);
$order = [];
$placeholder_ids = [];
foreach ($fragments as $fragment) {
$t = explode('"></div>', $fragment, 2);
$placeholder = $t[0];
$order[] = $placeholder;
$placeholder_id = $t[0];
$placeholder_ids[] = $placeholder_id;
}
$placeholder_ids = array_unique($placeholder_ids);
// The 'status messages' placeholder needs to be special cased, because it
// depends on global state that can be modified when other placeholders are
// being rendered: any code can add messages to render.
// This violates the principle that each lazy builder must be able to render
// itself in isolation, and therefore in any order. However, we cannot
// change the way drupal_set_message() works in the Drupal 8 cycle. So we
// have to accommodate its special needs.
// Allowing placeholders to be rendered in a particular order (in this case:
// last) would violate this isolation principle. Thus a monopoly is granted
// to this one special case, with this hard-coded solution.
// @see \Drupal\Core\Render\Element\StatusMessages
// @see \Drupal\Core\Render\Renderer::replacePlaceholders()
// @see https://www.drupal.org/node/2712935#comment-11368923
$message_placeholder_ids = [];
foreach ($placeholders as $placeholder_id => $placeholder_element) {
if (isset($placeholder_element['#lazy_builder']) && $placeholder_element['#lazy_builder'][0] === 'Drupal\Core\Render\Element\StatusMessages::renderMessages') {
$message_placeholder_ids[] = $placeholder_id;
}
}
return array_unique($order);
// Return placeholder IDs in DOM order, but with the 'status messages'
// placeholders at the end, if they are present.
$ordered_placeholder_ids = array_merge(
array_diff($placeholder_ids, $message_placeholder_ids),
array_intersect($placeholder_ids, $message_placeholder_ids)
);
return $ordered_placeholder_ids;
}
}

View file

@ -50,7 +50,7 @@ class BigPipePlaceholderTestCases {
// 1. Real-world example of HTML placeholder.
$status_messages = new BigPipePlaceholderTestCase(
[], //['#type' => 'status_messages'],
['#type' => 'status_messages'],
'<drupal-render-placeholder callback="Drupal\Core\Render\Element\StatusMessages::renderMessages" arguments="0" token="a8c34b5e"></drupal-render-placeholder>',
[
'#lazy_builder' => [
@ -110,7 +110,7 @@ class BigPipePlaceholderTestCases {
'command' => 'insert',
'method' => 'replaceWith',
'selector' => '[data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e"]',
'data' => "\n" . ' <div role="contentinfo" aria-label="Status message" class="messages messages--status">' . "\n" . ' <h2 class="visually-hidden">Status message</h2>' . "\n" . ' Hello from BigPipe!' . "\n" . ' </div>' . "\n \n",
'data' => "\n" . ' <div role="contentinfo" aria-label="Status message" class="messages messages--status">' . "\n" . ' <h2 class="visually-hidden">Status message</h2>' . "\n" . ' Hello from BigPipe!' . "\n" . ' </div>' . "\n ",
'settings' => NULL,
],
];

View file

@ -170,6 +170,11 @@ class BigPipeTest extends WebTestBase {
$cases['edge_case__html_non_lazy_builder']->bigPipePlaceholderId => Json::encode($cases['edge_case__html_non_lazy_builder']->embeddedAjaxResponseCommands),
$cases['exception__lazy_builder']->bigPipePlaceholderId => NULL,
$cases['exception__embedded_response']->bigPipePlaceholderId => NULL,
], [
0 => $cases['edge_case__html_non_lazy_builder']->bigPipePlaceholderId,
// The 'html' case contains the 'status messages' placeholder, which is
// always rendered last.
1 => $cases['html']->bigPipePlaceholderId,
]);
$this->assertRaw('</body>', 'Closing body tag present.');
@ -183,7 +188,7 @@ class BigPipeTest extends WebTestBase {
$records = db_query('SELECT * FROM {watchdog} ORDER BY wid DESC LIMIT 2')->fetchAll();
$this->assertEqual(RfcLogLevel::ERROR, $records[0]->severity);
$this->assertTrue(FALSE !== strpos((string) unserialize($records[0]->variables)['@message'], 'Oh noes!'));
$this->assertEqual(RfcLogLevel::ERROR, $records[0]->severity);
$this->assertEqual(RfcLogLevel::ERROR, $records[1]->severity);
$this->assertTrue(FALSE !== strpos((string) unserialize($records[1]->variables)['@message'], 'You are not allowed to say llamas are not cool!'));
// Verify that 4xx responses work fine. (4xx responses are handled by
@ -250,7 +255,7 @@ class BigPipeTest extends WebTestBase {
$this->assertNoRaw(BigPipe::STOP_SIGNAL, 'BigPipe stop signal absent.');
$this->pass('Verifying BigPipe assets are absent…', 'Debug');
$this->assertFalse(empty($this->getDrupalSettings()), 'drupalSettings and BigPipe asset library absent.');
$this->assertTrue(!isset($this->getDrupalSettings()['bigPipePlaceholderIds']) && empty($this->getDrupalSettings()['ajaxPageState']), 'BigPipe drupalSettings and BigPipe asset library absent.');
$this->assertRaw('</body>', 'Closing body tag present.');
// Verify that 4xx responses work fine. (4xx responses are handled by
@ -336,8 +341,11 @@ class BigPipeTest extends WebTestBase {
*
* @param array $expected_big_pipe_placeholders
* Keys: BigPipe placeholder IDs. Values: expected AJAX response.
* @param array $expected_big_pipe_placeholder_stream_order
* Keys: BigPipe placeholder IDs. Values: expected AJAX response. Keys are
* defined in the order that they are expected to be rendered & streamed.
*/
protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholders) {
protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholders, array $expected_big_pipe_placeholder_stream_order) {
$this->pass('Verifying BigPipe placeholders & replacements…', 'Debug');
$this->assertSetsEqual(array_keys($expected_big_pipe_placeholders), explode(' ', $this->drupalGetHeader('BigPipe-Test-Placeholders')));
$placeholder_positions = [];
@ -364,9 +372,14 @@ class BigPipeTest extends WebTestBase {
}
ksort($placeholder_positions, SORT_NUMERIC);
$this->assertEqual(array_keys($expected_big_pipe_placeholders), array_values($placeholder_positions));
$this->assertEqual(count($expected_big_pipe_placeholders), preg_match_all('/' . preg_quote('<div data-big-pipe-placeholder-id="', '/') . '/', $this->getRawContent()));
$expected_big_pipe_placeholders_with_replacements = array_filter($expected_big_pipe_placeholders);
$this->assertEqual(array_keys($expected_big_pipe_placeholders_with_replacements), array_values($placeholder_replacement_positions));
$placeholders = array_map(function(\SimpleXMLElement $element) { return (string) $element['data-big-pipe-placeholder-id']; }, $this->cssSelect('[data-big-pipe-placeholder-id]'));
$this->assertEqual(count($expected_big_pipe_placeholders), count(array_unique($placeholders)));
$expected_big_pipe_placeholders_with_replacements = [];
foreach ($expected_big_pipe_placeholder_stream_order as $big_pipe_placeholder_id) {
$expected_big_pipe_placeholders_with_replacements[$big_pipe_placeholder_id] = $expected_big_pipe_placeholders[$big_pipe_placeholder_id];
}
$this->assertEqual($expected_big_pipe_placeholders_with_replacements, array_filter($expected_big_pipe_placeholders));
$this->assertSetsEqual(array_keys($expected_big_pipe_placeholders_with_replacements), array_values($placeholder_replacement_positions));
$this->assertEqual(count($expected_big_pipe_placeholders_with_replacements), preg_match_all('/' . preg_quote('<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="', '/') . '/', $this->getRawContent()));
$this->pass('Verifying BigPipe start/stop signals…', 'Debug');

View file

@ -0,0 +1,6 @@
name: 'BigPipe regression test'
type: module
description: 'Support module for BigPipe regression testing.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,6 @@
big_pipe_regression_test.2678662:
path: '/big_pipe_regression_test/2678662'
defaults:
_controller: '\Drupal\big_pipe_regression_test\BigPipeRegressionTestController::regression2678662'
requirements:
_access: 'TRUE'

View file

@ -0,0 +1,20 @@
<?php
namespace Drupal\big_pipe_regression_test;
use Drupal\big_pipe\Render\BigPipeMarkup;
class BigPipeRegressionTestController {
const MARKER_2678662 = '<script>var hitsTheFloor = "</body>";</script>';
/**
* @see \Drupal\Tests\big_pipe\FunctionalJavascript\BigPipeRegressionTest::testMultipleBodies_2678662()
*/
public function regression2678662() {
return [
'#markup' => BigPipeMarkup::create(self::MARKER_2678662),
];
}
}

View file

@ -2,10 +2,13 @@
namespace Drupal\Tests\big_pipe\FunctionalJavascript;
use Drupal\big_pipe\Render\BigPipe;
use Drupal\big_pipe_regression_test\BigPipeRegressionTestController;
use Drupal\comment\CommentInterface;
use Drupal\comment\Entity\Comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Core\Url;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
@ -27,13 +30,8 @@ class BigPipeRegressionTest extends JavascriptTestBase {
* {@inheritdoc}
*/
public static $modules = [
'node',
'comment',
'big_pipe',
'history',
'editor',
'ckeditor',
'filter',
'big_pipe_regression_test',
];
/**
@ -53,6 +51,8 @@ class BigPipeRegressionTest extends JavascriptTestBase {
* @see https://www.drupal.org/node/2698811
*/
public function testCommentForm_2698811() {
$this->assertTrue($this->container->get('module_installer')->install(['comment', 'history', 'ckeditor'], TRUE), 'Installed modules.');
// Ensure an `article` node type exists.
$this->createContentType(['type' => 'article']);
$this->addDefaultCommentField('node', 'article');
@ -114,4 +114,74 @@ JS;
$this->assertJsCondition($javascript);
}
/**
* Ensure BigPipe works despite inline JS containing the string "</body>".
*
* @see https://www.drupal.org/node/2678662
*/
public function testMultipleClosingBodies_2678662() {
$this->assertTrue($this->container->get('module_installer')->install(['render_placeholder_message_test'], TRUE), 'Installed modules.');
$this->drupalLogin($this->drupalCreateUser());
$this->drupalGet(Url::fromRoute('big_pipe_regression_test.2678662'));
// Confirm that AJAX behaviors were instantiated, if not, this points to a
// JavaScript syntax error.
$javascript = <<<JS
(function(){
return Object.keys(Drupal.ajax.instances).length > 0;
}());
JS;
$this->assertJsCondition($javascript);
// Besides verifying there is no JavaScript syntax error, also verify the
// HTML structure.
$this->assertSession()
->responseContains(BigPipe::STOP_SIGNAL . "\n\n\n</body></html>", 'The BigPipe stop signal is present just before the closing </body> and </html> tags.');
$js_code_until_closing_body_tag = substr(BigPipeRegressionTestController::MARKER_2678662, 0, strpos(BigPipeRegressionTestController::MARKER_2678662, '</body>'));
$this->assertSession()
->responseNotContains($js_code_until_closing_body_tag . "\n" . BigPipe::START_SIGNAL, 'The BigPipe start signal does NOT start at the closing </body> tag string in an inline script.');
}
/**
* Ensure messages set in placeholders always appear.
*
* @see https://www.drupal.org/node/2712935
*/
public function testMessages_2712935() {
$this->assertTrue($this->container->get('module_installer')->install(['render_placeholder_message_test'], TRUE), 'Installed modules.');
$this->drupalLogin($this->drupalCreateUser());
$messages_markup = '<div role="contentinfo" aria-label="Status message"';
$test_routes = [
// Messages placeholder rendered first.
'render_placeholder_message_test.first',
// Messages placeholder rendered after one, before another.
'render_placeholder_message_test.middle',
// Messages placeholder rendered last.
'render_placeholder_message_test.last',
];
$assert = $this->assertSession();
foreach ($test_routes as $route) {
// Verify that we start off with zero messages queued.
$this->drupalGet(Url::fromRoute('render_placeholder_message_test.queued'));
$assert->responseNotContains($messages_markup);
// Verify the test case at this route behaves as expected.
$this->drupalGet(Url::fromRoute($route));
$assert->elementContains('css', 'p.logged-message:nth-of-type(1)', 'Message: P1');
$assert->elementContains('css', 'p.logged-message:nth-of-type(2)', 'Message: P2');
$assert->responseContains($messages_markup);
$assert->elementExists('css', 'div[aria-label="Status message"] ul');
$assert->elementContains('css', 'div[aria-label="Status message"] ul li:nth-of-type(1)', 'P1');
$assert->elementContains('css', 'div[aria-label="Status message"] ul li:nth-of-type(2)', 'P2');
// Verify that we end with all messages printed, hence again zero queued.
$this->drupalGet(Url::fromRoute('render_placeholder_message_test.queued'));
$assert->responseNotContains($messages_markup);
}
}
}

View file

@ -198,6 +198,10 @@
far too wide focus indicator. This fixes that. */
overflow: hidden;
}
.ckeditor-buttons .cke_button_icon img {
width: 16px;
height: 16px;
}
.ckeditor-buttons li .cke_ltr {
direction: ltr;
}

View file

@ -483,7 +483,7 @@
* A HTML string for the button to toggle group names.
*/
Drupal.theme.ckeditorButtonGroupNamesToggle = function () {
return '<a class="ckeditor-groupnames-toggle" role="button" aria-pressed="false"></a>';
return '<button class="link ckeditor-groupnames-toggle" aria-pressed="false"></button>';
};
/**

View file

@ -96,9 +96,16 @@ class StylesCombo extends CKEditorPluginBase implements CKEditorPluginConfigurab
* #element_validate handler for the "styles" element in settingsForm().
*/
public function validateStylesValue(array $element, FormStateInterface $form_state) {
if ($this->generateStylesSetSetting($element['#value']) === FALSE) {
$styles_setting = $this->generateStylesSetSetting($element['#value']);
if ($styles_setting === FALSE) {
$form_state->setError($element, t('The provided list of styles is syntactically incorrect.'));
}
else {
$style_names = array_map(function ($style) { return $style['name']; }, $styles_setting);
if (count($style_names) !== count(array_unique($style_names))) {
$form_state->setError($element, t('Each style must have a unique label.'));
}
}
}
/**

View file

@ -0,0 +1,99 @@
<?php
namespace Drupal\ckeditor\Tests;
use Drupal\editor\Entity\Editor;
use Drupal\simpletest\WebTestBase;
use Drupal\filter\Entity\FilterFormat;
/**
* Tests administration of the CKEditor StylesCombo plugin.
*
* @group ckeditor
*/
class CKEditorStylesComboAdminTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['filter', 'editor', 'ckeditor'];
/**
* A user with the 'administer filters' permission.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A random generated format machine name.
*
* @var string
*/
protected $format;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->format = strtolower($this->randomMachineName());
$filter_format = FilterFormat::create([
'format' => $this->format,
'name' => $this->randomString(),
'filters' => [],
]);
$filter_format->save();
$editor = Editor::create([
'format' => $this->format,
'editor' => 'ckeditor',
]);
$editor->save();
$this->adminUser = $this->drupalCreateUser(['administer filters']);
}
/**
* Tests StylesCombo settings for an existing text format.
*/
function testExistingFormat() {
$ckeditor = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
$default_settings = $ckeditor->getDefaultSettings();
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/config/content/formats/manage/' . $this->format);
// Ensure an Editor config entity exists, with the proper settings.
$expected_settings = $default_settings;
$editor = Editor::load($this->format);
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
// Case 1: Configure the Styles plugin with different labels for each style,
// and ensure the updated settings are saved.
$this->drupalGet('admin/config/content/formats/manage/' . $this->format);
$edit = [
'editor[settings][plugins][stylescombo][styles]' => "h1.title|Title\np.callout|Callout\n\n",
];
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$expected_settings['plugins']['stylescombo']['styles'] = "h1.title|Title\np.callout|Callout\n\n";
$editor = Editor::load($this->format);
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
// Case 2: Configure the Styles plugin with same labels for each style, and
// ensure that an error is displayed and that the updated settings are not
// saved.
$this->drupalGet('admin/config/content/formats/manage/' . $this->format);
$edit = [
'editor[settings][plugins][stylescombo][styles]' => "h1.title|Title\np.callout|Title\n\n",
];
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$this->assertRaw(t('Each style must have a unique label.'));
$expected_settings['plugins']['stylescombo']['styles'] = "h1.title|Title\np.callout|Callout\n\n";
$editor = Editor::load($this->format);
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
}
}

View file

@ -0,0 +1,31 @@
<?php
/**
* @file
* Install, update and uninstall functions for the config module.
*/
/**
* Implements hook_requirements().
*/
function config_requirements($phase) {
$requirements = [];
try {
$directory = config_get_config_directory(CONFIG_SYNC_DIRECTORY);
}
catch (\Exception $e) {
// system_requirements() guarantees that the CONFIG_SYNC_DIRECTORY exists
// as the config.storage.staging service relies on it.
$directory = FALSE;
}
// Ensure the configuration sync directory is writable. This is only a warning
// because only configuration import from a tarball requires the folder to be
// web writable.
if ($phase !== 'install' && !is_writable($directory)) {
$requirements['config directory ' . CONFIG_SYNC_DIRECTORY] = [
'title' => t('The directory %directory is not writable.', ['%directory' => $directory]),
'severity' => REQUIREMENT_WARNING,
];
}
return $requirements;
}

View file

@ -50,6 +50,11 @@ class ConfigImportForm extends FormBase {
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$directory = config_get_config_directory(CONFIG_SYNC_DIRECTORY);
$directory_is_writable = is_writable($directory);
if (!$directory_is_writable) {
drupal_set_message($this->t('The directory %directory is not writable.', ['%directory' => $directory]), 'error');
}
$form['import_tarball'] = array(
'#type' => 'file',
'#title' => $this->t('Configuration archive'),
@ -59,6 +64,7 @@ class ConfigImportForm extends FormBase {
$form['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Upload'),
'#disabled' => !$directory_is_writable,
);
return $form;
}

View file

@ -45,6 +45,16 @@ class ConfigImportUploadTest extends WebTestBase {
$edit = array('files[import_tarball]' => drupal_realpath($text_file->uri));
$this->drupalPostForm('admin/config/development/configuration/full/import', $edit, t('Upload'));
$this->assertText(t('Could not extract the contents of the tar file'));
// Make the sync directory read-only.
$directory = config_get_config_directory(CONFIG_SYNC_DIRECTORY);
\Drupal::service('file_system')->chmod($directory, 0555);
$this->drupalGet('admin/config/development/configuration/full/import');
$this->assertRaw(t('The directory %directory is not writable.', ['%directory' => $directory]));
// Ensure submit button for \Drupal\config\Form\ConfigImportForm is
// disabled.
$submit_is_disabled = $this->cssSelect('form.config-import-form input[type="submit"]:disabled');
$this->assertTrue(count($submit_is_disabled) === 1, 'The submit button is disabled.');
}
}

View file

@ -26,7 +26,7 @@ class ConfigInstallWebTest extends WebTestBase {
protected function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(array('administer modules', 'administer themes'));
$this->adminUser = $this->drupalCreateUser(array('administer modules', 'administer themes', 'administer site configuration'));
// Ensure the global variable being asserted by this test does not exist;
// a previous test executed in this request/process might have set it.
@ -188,4 +188,22 @@ class ConfigInstallWebTest extends WebTestBase {
$this->assertTrue(entity_load('config_test', 'other_module_test_with_dependency'), 'The config_test.dynamic.other_module_test_with_dependency configuration has been created during install.');
}
/**
* Tests config_requirements().
*/
public function testConfigModuleRequirements() {
$this->drupalLogin($this->adminUser);
$this->drupalPostForm('admin/modules', array('modules[Core][config][enable]' => TRUE), t('Install'));
$directory = config_get_config_directory(CONFIG_SYNC_DIRECTORY);
file_unmanaged_delete_recursive($directory);
$this->drupalGet('/admin/reports/status');
$this->assertRaw(t('The directory %directory does not exist.', array('%directory' => $directory)));
file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
\Drupal::service('file_system')->chmod($directory, 0555);
$this->drupalGet('/admin/reports/status');
$this->assertRaw(t('The directory %directory is not writable.', ['%directory' => $directory]));
}
}

View file

@ -21,7 +21,7 @@ entity.config_test.edit_form:
requirements:
_permission: 'administer site configuration'
entity.config_test.edit_form_config_test_no_status:
entity.config_test_no_status.edit_form:
path: '/admin/structure/config_test/manage/{config_test_no_status}'
defaults:
_entity_form: config_test_no_status

View file

@ -2,6 +2,8 @@
namespace Drupal\config_translation\Access;
use Drupal\config_translation\ConfigMapperInterface;
use Drupal\config_translation\Exception\ConfigMapperLanguageException;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
@ -12,27 +14,68 @@ use Drupal\Core\Session\AccountInterface;
class ConfigTranslationFormAccess extends ConfigTranslationOverviewAccess {
/**
* {@inheritdoc}
* Checks access to the overview based on permissions and translatability.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route_match to check against.
* @param \Drupal\Core\Session\AccountInterface $account
* The account to check access for.
* @param string $langcode
* The language code of the target language.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(RouteMatchInterface $route_match, AccountInterface $account, $langcode = NULL) {
// For the translation forms we have a target language, so we need some
// checks in addition to the checks performed for the translation overview.
$base_access = parent::access($route_match, $account);
if ($base_access->isAllowed()) {
$mapper = $this->getMapperFromRouteMatch($route_match);
try {
$source_langcode = $mapper->getLangcode();
$source_language = $this->languageManager->getLanguage($source_langcode);
$target_language = $this->languageManager->getLanguage($langcode);
// Make sure that the target language is not locked, and that the target
// language is not the original submission language. Although technically
// configuration can be overlaid with translations in the same language,
// that is logically not a good idea.
$access =
!empty($target_language) &&
!$target_language->isLocked() &&
(empty($this->sourceLanguage) || ($target_language->getId() != $this->sourceLanguage->getId()));
return $base_access->andIf(AccessResult::allowedIf($access));
return $this->doCheckAccess($account, $mapper, $source_language, $target_language);
}
return $base_access;
catch (ConfigMapperLanguageException $exception) {
return AccessResult::forbidden();
}
}
/**
* Checks access given an account, configuration mapper, and source language.
*
* In addition to the checks performed by
* ConfigTranslationOverviewAccess::doCheckAccess() this makes sure the target
* language is not locked and the target language is not the source language.
*
* Although technically configuration can be overlaid with translations in the
* same language, that is logically not a good idea.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The account to check access for.
* @param \Drupal\config_translation\ConfigMapperInterface $mapper
* The configuration mapper to check access for.
* @param \Drupal\Core\Language\LanguageInterface|null $source_language
* The source language to check for, if any.
* @param \Drupal\Core\Language\LanguageInterface|null $target_language
* The target language to check for, if any.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The result of the access check.
*
* @see \Drupal\config_translation\Access\ConfigTranslationOverviewAccess::doCheckAccess()
*/
protected function doCheckAccess(AccountInterface $account, ConfigMapperInterface $mapper, $source_language = NULL, $target_language = NULL) {
$base_access_result = parent::doCheckAccess($account, $mapper, $source_language);
$access =
$target_language &&
!$target_language->isLocked() &&
(!$source_language || ($target_language->getId() !== $source_language->getId()));
return $base_access_result->andIf(AccessResult::allowedIf($access));
}
}

View file

@ -2,6 +2,8 @@
namespace Drupal\config_translation\Access;
use Drupal\config_translation\ConfigMapperInterface;
use Drupal\config_translation\Exception\ConfigMapperLanguageException;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\config_translation\ConfigMapperManagerInterface;
use Drupal\Core\Access\AccessResult;
@ -28,13 +30,6 @@ class ConfigTranslationOverviewAccess implements AccessInterface {
*/
protected $languageManager;
/**
* The source language.
*
* @var \Drupal\Core\Language\LanguageInterface
*/
protected $sourceLanguage;
/**
* Constructs a ConfigTranslationOverviewAccess object.
*
@ -54,28 +49,66 @@ class ConfigTranslationOverviewAccess implements AccessInterface {
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route_match to check against.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
* The account to check access for.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(RouteMatchInterface $route_match, AccountInterface $account) {
$route = $route_match->getRouteObject();
$mapper = $this->getMapperFromRouteMatch($route_match);
/** @var \Drupal\config_translation\ConfigMapperInterface $mapper */
$mapper = $this->configMapperManager->createInstance($route->getDefault('plugin_id'));
try {
$langcode = $mapper->getLangcode();
}
catch (ConfigMapperLanguageException $exception) {
// ConfigTranslationController shows a helpful message if the language
// codes do not match, so do not let that prevent granting access.
$langcode = 'en';
}
$source_language = $this->languageManager->getLanguage($langcode);
return $this->doCheckAccess($account, $mapper, $source_language);
}
/**
* Gets a configuration mapper using a route match.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match to populate the mapper with.
*
* @return \Drupal\config_translation\ConfigMapperInterface
* The configuration mapper.
*/
protected function getMapperFromRouteMatch(RouteMatchInterface $route_match) {
$mapper = $this->configMapperManager->createInstance($route_match->getRouteObject()
->getDefault('plugin_id'));
$mapper->populateFromRouteMatch($route_match);
$this->sourceLanguage = $this->languageManager->getLanguage($mapper->getLangcode());
return $mapper;
}
// Allow access to the translation overview if the proper permission is
// granted, the configuration has translatable pieces, and the source
// language is not locked if it is present.
$source_language_access = is_null($this->sourceLanguage) || !$this->sourceLanguage->isLocked();
/**
* Checks access given an account, configuration mapper, and source language.
*
* Grants access if the proper permission is granted to the account, the
* configuration has translatable pieces, and the source language is not
* locked given it is present.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The account to check access for.
* @param \Drupal\config_translation\ConfigMapperInterface $mapper
* The configuration mapper to check access for.
* @param \Drupal\Core\Language\LanguageInterface|null $source_language
* The source language to check for, if any.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The result of the access check.
*/
protected function doCheckAccess(AccountInterface $account, ConfigMapperInterface $mapper, $source_language = NULL) {
$access =
$account->hasPermission('translate configuration') &&
$mapper->hasSchema() &&
$mapper->hasTranslatable() &&
$source_language_access;
(!$source_language || !$source_language->isLocked());
return AccessResult::allowedIf($access)->cachePerPermissions();
}

View file

@ -202,6 +202,17 @@ interface ConfigMapperInterface {
*/
public function getLangcode();
/**
* Returns the language code of a configuration object given its name.
*
* @param string $config_name
* The name of the configuration object.
*
* @return string
* The language code of the configuration object.
*/
public function getLangcodeFromConfig($config_name);
/**
* Sets the original language code.
*

View file

@ -2,6 +2,7 @@
namespace Drupal\config_translation;
use Drupal\config_translation\Exception\ConfigMapperLanguageException;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Language\LanguageInterface;
@ -380,22 +381,26 @@ class ConfigNamesMapper extends PluginBase implements ConfigMapperInterface, Con
* {@inheritdoc}
*/
public function getLangcode() {
$config_factory = $this->configFactory;
$langcodes = array_map(function($name) use ($config_factory) {
// Default to English if no language code was provided in the file.
// Although it is a best practice to include a language code, if the
// developer did not think about a multilingual use-case, we fall back
// on assuming the file is English.
return $config_factory->get($name)->get('langcode') ?: 'en';
}, $this->getConfigNames());
$langcodes = array_map([$this, 'getLangcodeFromConfig'], $this->getConfigNames());
if (count(array_unique($langcodes)) > 1) {
throw new \RuntimeException('A config mapper can only contain configuration for a single language.');
throw new ConfigMapperLanguageException('A config mapper can only contain configuration for a single language.');
}
return reset($langcodes);
}
/**
* {@inheritdoc}
*/
public function getLangcodeFromConfig($config_name) {
// Default to English if no language code was provided in the file.
// Although it is a best practice to include a language code, if the
// developer did not think about a multilingual use case, we fall back
// on assuming the file is English.
return $this->configFactory->get($config_name)->get('langcode') ?: 'en';
}
/**
* {@inheritdoc}
*/

View file

@ -3,11 +3,14 @@
namespace Drupal\config_translation\Controller;
use Drupal\config_translation\ConfigMapperManagerInterface;
use Drupal\config_translation\Exception\ConfigMapperLanguageException;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
@ -63,6 +66,13 @@ class ConfigTranslationController extends ControllerBase {
*/
protected $languageManager;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a ConfigTranslationController.
*
@ -78,14 +88,17 @@ class ConfigTranslationController extends ControllerBase {
* The current user.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(ConfigMapperManagerInterface $config_mapper_manager, AccessManagerInterface $access_manager, RequestMatcherInterface $router, InboundPathProcessorInterface $path_processor, AccountInterface $account, LanguageManagerInterface $language_manager) {
public function __construct(ConfigMapperManagerInterface $config_mapper_manager, AccessManagerInterface $access_manager, RequestMatcherInterface $router, InboundPathProcessorInterface $path_processor, AccountInterface $account, LanguageManagerInterface $language_manager, RendererInterface $renderer) {
$this->configMapperManager = $config_mapper_manager;
$this->accessManager = $access_manager;
$this->router = $router;
$this->pathProcessor = $path_processor;
$this->account = $account;
$this->languageManager = $language_manager;
$this->renderer = $renderer;
}
/**
@ -98,7 +111,8 @@ class ConfigTranslationController extends ControllerBase {
$container->get('router'),
$container->get('path_processor_manager'),
$container->get('current_user'),
$container->get('language_manager')
$container->get('language_manager'),
$container->get('renderer')
);
}
@ -127,7 +141,33 @@ class ConfigTranslationController extends ControllerBase {
if (count($languages) == 1) {
drupal_set_message($this->t('In order to translate configuration, the website must have at least two <a href=":url">languages</a>.', array(':url' => $this->url('entity.configurable_language.collection'))), 'warning');
}
$original_langcode = $mapper->getLangcode();
try {
$original_langcode = $mapper->getLangcode();
$operations_access = TRUE;
}
catch (ConfigMapperLanguageException $exception) {
$items = [];
foreach ($mapper->getConfigNames() as $config_name) {
$langcode = $mapper->getLangcodeFromConfig($config_name);
$items[] = $this->t('@name: @langcode', [
'@name' => $config_name,
'@langcode' => $langcode,
]);
}
$message = [
'message' => ['#markup' => $this->t('The configuration objects have different language codes so they cannot be translated:')],
'items' => [
'#theme' => 'item_list',
'#items' => $items,
],
];
drupal_set_message($this->renderer->renderPlain($message), 'warning');
$original_langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
$operations_access = FALSE;
}
if (!isset($languages[$original_langcode])) {
// If the language is not configured on the site, create a dummy language
// object for this listing only to ensure the user gets useful info.
@ -205,6 +245,9 @@ class ConfigTranslationController extends ControllerBase {
$page['languages'][$langcode]['operations'] = array(
'#type' => 'operations',
'#links' => $operations,
// Even if the mapper contains multiple language codes, the source
// configuration can still be edited.
'#access' => ($langcode == $original_langcode) || $operations_access,
);
}
return $page;

View file

@ -0,0 +1,9 @@
<?php
namespace Drupal\config_translation\Exception;
/**
* Provides an exception for configuration mappers with multiple languages.
*/
class ConfigMapperLanguageException extends \RuntimeException {
}

View file

@ -138,7 +138,12 @@ abstract class ConfigTranslationFormBase extends FormBase implements BaseFormIdI
$this->mapper = $mapper;
$this->language = $language;
$this->sourceLanguage = $this->languageManager->getLanguage($this->mapper->getLangcode());
// ConfigTranslationFormAccess will not grant access if this raises an
// exception, so we can call this without a try-catch block here.
$langcode = $this->mapper->getLangcode();
$this->sourceLanguage = $this->languageManager->getLanguage($langcode);
// Get base language configuration to display in the form before setting the
// language to use for the form. This avoids repetitively settings and

View file

@ -2,10 +2,13 @@
namespace Drupal\content_translation;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateImportEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
@ -74,11 +77,27 @@ class ContentTranslationUpdatesManager implements EventSubscriberInterface {
$this->updateDefinitions($entity_types);
}
/**
* Listener for migration imports.
*/
public function onMigrateImport(MigrateImportEvent $event) {
$migration = $event->getMigration();
$configuration = $migration->getDestinationConfiguration();
$entity_types = NestedArray::getValue($configuration, ['content_translation_update_definitions']);
if ($entity_types) {
$entity_types = array_intersect_key($this->entityManager->getDefinitions(), array_flip($entity_types));
$this->updateDefinitions($entity_types);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[ConfigEvents::IMPORT][] = ['onConfigImporterImport', 60];
if (class_exists('\Drupal\migrate\Event\MigrateEvents')) {
$events[MigrateEvents::POST_IMPORT][] = ['onMigrateImport'];
}
return $events;
}

View file

@ -124,8 +124,8 @@ class DynamicPageCacheSubscriber implements EventSubscriberInterface {
*/
public function onRouteMatch(GetResponseEvent $event) {
// Don't cache the response if the Dynamic Page Cache request policies are
// not met. Store the result in a request attribute, so that onResponse()
// does not have to redo the request policy check.
// not met. Store the result in a static keyed by current request, so that
// onResponse() does not have to redo the request policy check.
$request = $event->getRequest();
$request_policy_result = $this->requestPolicy->check($request);
$this->requestPolicyResults[$request] = $request_policy_result;

View file

@ -14,6 +14,7 @@ process:
label:
plugin: static_map
source: view_mode
bypass: true
map:
search_index: "Search index"
search_result: "Search result"

View file

@ -28,8 +28,7 @@ class FieldInstancePerFormDisplay extends DrupalSqlBase {
->fields('fc', array(
'type',
'module',
))
->condition('fci.entity_type', 'node');
));
$query->join('field_config', 'fc', 'fci.field_id = fc.id');
return $query;
}

View file

@ -201,8 +201,18 @@ class EntityReferenceFormatterTest extends EntityKernelTestBase {
$this->assertEqual($build[0]['#cache']['tags'], $expected_cache_tags, format_string('The @formatter formatter has the expected cache tags.', array('@formatter' => $formatter)));
// Test the second field item.
$expected_rendered_name_field_2 = '
<div class="field field--name-name field--type-string field--label-hidden field__item">' . $this->unsavedReferencedEntity->label() . '</div>
';
$expected_rendered_body_field_2 = '
<div class="clearfix text-formatted field field--name-body field--type-text field--label-above">
<div class="field__label">Body</div>
<div class="field__item"><p>Hello, unsaved world!</p></div>
</div>
';
$renderer->renderRoot($build[1]);
$this->assertEqual($build[1]['#markup'], $this->unsavedReferencedEntity->label(), sprintf('The markup returned by the %s formatter is correct for an item with a unsaved entity.', $formatter));
$this->assertEqual($build[1]['#markup'], 'default | ' . $this->unsavedReferencedEntity->label() . $expected_rendered_name_field_2 . $expected_rendered_body_field_2, sprintf('The markup returned by the %s formatter is correct for an item with a unsaved entity.', $formatter));
}
/**
@ -271,6 +281,7 @@ class EntityReferenceFormatterTest extends EntityKernelTestBase {
* Tests the label formatter.
*/
public function testLabelFormatter() {
$this->installEntitySchema('entity_test_label');
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = $this->container->get('renderer');
$formatter = 'entity_reference_label';

View file

@ -6,6 +6,7 @@ use Drupal\comment\Entity\CommentType;
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
use Drupal\node\Entity\NodeType;
@ -88,6 +89,8 @@ class MigrateFieldFormatterSettingsTest extends MigrateDrupal7TestBase {
'label' => $this->randomMachineName(),
])->save();
Vocabulary::create(['vid' => 'test_vocabulary'])->save();
// Give one unfortunate field instance invalid display settings to ensure
// that the migration provides an empty array as a default (thus avoiding
// an "unsupported operand types" fatal).

View file

@ -5,6 +5,7 @@ namespace Drupal\Tests\field\Kernel\Migrate\d7;
use Drupal\comment\Entity\CommentType;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\FieldConfigInterface;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
use Drupal\node\Entity\NodeType;
@ -45,6 +46,7 @@ class MigrateFieldInstanceTest extends MigrateDrupal7TestBase {
$this->createType('book');
$this->createType('forum');
$this->createType('test_content_type');
Vocabulary::create(['vid' => 'test_vocabulary'])->save();
$this->executeMigrations(['d7_field', 'd7_field_instance']);
}

View file

@ -45,6 +45,7 @@ class MigrateFieldInstanceWidgetSettingsTest extends MigrateDrupal7TestBase {
$this->executeMigrations([
'd7_node_type',
'd7_comment_type',
'd7_taxonomy_vocabulary',
'd7_field',
'd7_field_instance',
'd7_field_instance_widget_settings',
@ -126,6 +127,16 @@ class MigrateFieldInstanceWidgetSettingsTest extends MigrateDrupal7TestBase {
$this->assertComponent('node.test_content_type.default', 'field_term_reference', 'entity_reference_autocomplete', 14);
$this->assertComponent('node.test_content_type.default', 'field_text', 'text_textfield', 15);
$this->assertComponent('node.test_content_type.default', 'field_text_list', 'options_select', 11);
$this->assertEntity('user.user.default', 'user', 'user');
$this->assertComponent('user.user.default', 'field_file', 'file_generic', 8);
$this->assertEntity('comment.comment_node_test_content_type.default', 'comment', 'comment_node_test_content_type');
$this->assertComponent('comment.comment_node_test_content_type.default', 'comment_body', 'text_textarea', 0);
$this->assertComponent('comment.comment_node_test_content_type.default', 'field_integer', 'number', 2);
$this->assertEntity('taxonomy_term.test_vocabulary.default', 'taxonomy_term', 'test_vocabulary');
$this->assertComponent('comment.comment_node_test_content_type.default', 'field_integer', 'number', 2);
}
}

View file

@ -13,7 +13,7 @@ use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
*/
class MigrateViewModesTest extends MigrateDrupal7TestBase {
public static $modules = ['comment', 'node'];
public static $modules = ['comment', 'node', 'taxonomy'];
/**
* {@inheritdoc}
@ -52,6 +52,7 @@ class MigrateViewModesTest extends MigrateDrupal7TestBase {
$this->assertEntity('comment.full', 'Full', 'comment');
$this->assertEntity('node.teaser', 'Teaser', 'node');
$this->assertEntity('node.full', 'Full', 'node');
$this->assertEntity('node.custom', 'custom', 'node');
$this->assertEntity('user.full', 'Full', 'user');
}

View file

@ -35,6 +35,42 @@ class FieldInstancePerFormDisplayTest extends MigrateSqlSourceTestCase {
'required' => FALSE,
'global_settings' => array(),
),
array(
'field_name' => 'field_file',
'entity_type' => 'user',
'bundle' => 'user',
'widget_settings' => array(
),
'display_settings' => array(
),
'description' => '',
'required' => FALSE,
'global_settings' => array(),
),
array(
'field_name' => 'field_integer',
'entity_type' => 'comment',
'bundle' => 'comment_node_test_content_type',
'widget_settings' => array(
),
'display_settings' => array(
),
'description' => '',
'required' => FALSE,
'global_settings' => array(),
),
array(
'field_name' => 'field_link',
'entity_type' => 'taxonomy_term',
'bundle' => 'test_vocabulary',
'widget_settings' => array(
),
'display_settings' => array(
),
'description' => '',
'required' => FALSE,
'global_settings' => array(),
),
);
/**
@ -51,6 +87,33 @@ class FieldInstancePerFormDisplayTest extends MigrateSqlSourceTestCase {
'data' => 'a:6:{s:5:"label";s:4:"Body";s:6:"widget";a:4:{s:4:"type";s:26:"text_textarea_with_summary";s:8:"settings";a:2:{s:4:"rows";i:20;s:12:"summary_rows";i:5;}s:6:"weight";i:-4;s:6:"module";s:4:"text";}s:8:"settings";a:3:{s:15:"display_summary";b:1;s:15:"text_processing";i:1;s:18:"user_register_form";b:0;}s:7:"display";a:2:{s:7:"default";a:5:{s:5:"label";s:6:"hidden";s:4:"type";s:12:"text_default";s:8:"settings";a:0:{}s:6:"module";s:4:"text";s:6:"weight";i:0;}s:6:"teaser";a:5:{s:5:"label";s:6:"hidden";s:4:"type";s:23:"text_summary_or_trimmed";s:8:"settings";a:1:{s:11:"trim_length";i:600;}s:6:"module";s:4:"text";s:6:"weight";i:0;}}s:8:"required";b:0;s:11:"description";s:0:"";}',
'deleted' => '0',
),
array(
'id' => '33',
'field_id' => '11',
'field_name' => 'field_file',
'entity_type' => 'user',
'bundle' => 'user',
'data' => 'a:6:{s:5:"label";s:4:"File";s:6:"widget";a:5:{s:6:"weight";s:1:"8";s:4:"type";s:12:"file_generic";s:6:"module";s:4:"file";s:6:"active";i:1;s:8:"settings";a:1:{s:18:"progress_indicator";s:8:"throbber";}}s:8:"settings";a:5:{s:14:"file_directory";s:0:"";s:15:"file_extensions";s:3:"txt";s:12:"max_filesize";s:0:"";s:17:"description_field";i:0;s:18:"user_register_form";i:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:12:"file_default";s:8:"settings";a:0:{}s:6:"module";s:4:"file";s:6:"weight";i:0;}}s:8:"required";i:0;s:11:"description";s:0:"";}',
'deleted' => '0',
),
array(
'id' => '32',
'field_id' => '14',
'field_name' => 'field_integer',
'entity_type' => 'comment',
'bundle' => 'comment_node_test_content_type',
'data' => 'a:7:{s:5:"label";s:7:"Integer";s:6:"widget";a:5:{s:6:"weight";s:1:"2";s:4:"type";s:6:"number";s:6:"module";s:6:"number";s:6:"active";i:0;s:8:"settings";a:0:{}}s:8:"settings";a:5:{s:3:"min";s:0:"";s:3:"max";s:0:"";s:6:"prefix";s:0:"";s:6:"suffix";s:0:"";s:18:"user_register_form";b:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:14:"number_integer";s:8:"settings";a:4:{s:18:"thousand_separator";s:1:" ";s:17:"decimal_separator";s:1:".";s:5:"scale";i:0;s:13:"prefix_suffix";b:1;}s:6:"module";s:6:"number";s:6:"weight";i:1;}}s:8:"required";i:0;s:11:"description";s:0:"";s:13:"default_value";N;}',
'deleted' => '0',
),
array(
'id' => '25',
'field_id' => '15',
'field_name' => 'field_link',
'entity_type' => 'taxonomy_term',
'bundle' => 'test_vocabulary',
'data' => 'a:7:{s:5:"label";s:4:"Link";s:6:"widget";a:5:{s:6:"weight";s:2:"10";s:4:"type";s:10:"link_field";s:6:"module";s:4:"link";s:6:"active";i:0;s:8:"settings";a:0:{}}s:8:"settings";a:12:{s:12:"absolute_url";i:1;s:12:"validate_url";i:1;s:3:"url";i:0;s:5:"title";s:8:"optional";s:11:"title_value";s:19:"Unused Static Title";s:27:"title_label_use_field_label";i:0;s:15:"title_maxlength";s:3:"128";s:7:"display";a:1:{s:10:"url_cutoff";s:2:"81";}s:10:"attributes";a:6:{s:6:"target";s:6:"_blank";s:3:"rel";s:8:"nofollow";s:18:"configurable_class";i:0;s:5:"class";s:7:"classes";s:18:"configurable_title";i:1;s:5:"title";s:0:"";}s:10:"rel_remove";s:19:"rel_remove_external";s:13:"enable_tokens";i:1;s:18:"user_register_form";b:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:12:"link_default";s:6:"weight";s:1:"9";s:8:"settings";a:0:{}s:6:"module";s:4:"link";}}s:8:"required";i:0;s:11:"description";s:0:"";s:13:"default_value";N;}',
'deleted' => '0',
),
);
$this->databaseContents['field_config'] = array(
array(
@ -68,6 +131,51 @@ class FieldInstancePerFormDisplayTest extends MigrateSqlSourceTestCase {
'translatable' => '0',
'deleted' => '0',
),
array(
'id' => '11',
'field_name' => 'field_file',
'type' => 'file',
'module' => 'file',
'active' => '1',
'storage_type' => 'field_sql_storage',
'storage_module' => 'field_sql_storage',
'storage_active' => '1',
'locked' => '0',
'data' => 'a:7:{s:12:"translatable";s:1:"0";s:12:"entity_types";a:0:{}s:8:"settings";a:3:{s:13:"display_field";i:0;s:15:"display_default";i:0;s:10:"uri_scheme";s:6:"public";}s:7:"storage";a:5:{s:4:"type";s:17:"field_sql_storage";s:8:"settings";a:0:{}s:6:"module";s:17:"field_sql_storage";s:6:"active";s:1:"1";s:7:"details";a:1:{s:3:"sql";a:2:{s:18:"FIELD_LOAD_CURRENT";a:1:{s:21:"field_data_field_file";a:3:{s:3:"fid";s:14:"field_file_fid";s:7:"display";s:18:"field_file_display";s:11:"description";s:22:"field_file_description";}}s:19:"FIELD_LOAD_REVISION";a:1:{s:25:"field_revision_field_file";a:3:{s:3:"fid";s:14:"field_file_fid";s:7:"display";s:18:"field_file_display";s:11:"description";s:22:"field_file_description";}}}}}s:12:"foreign keys";a:1:{s:3:"fid";a:2:{s:5:"table";s:12:"file_managed";s:7:"columns";a:1:{s:3:"fid";s:3:"fid";}}}s:7:"indexes";a:1:{s:3:"fid";a:1:{i:0;s:3:"fid";}}s:2:"id";s:2:"11";}',
'cardinality' => '1',
'translatable' => '0',
'deleted' => '0',
),
array(
'id' => '14',
'field_name' => 'field_integer',
'type' => 'number_integer',
'module' => 'number',
'active' => '1',
'storage_type' => 'field_sql_storage',
'storage_module' => 'field_sql_storage',
'storage_active' => '1',
'locked' => '0',
'data' => 'a:7:{s:12:"translatable";s:1:"0";s:12:"entity_types";a:0:{}s:8:"settings";a:0:{}s:7:"storage";a:5:{s:4:"type";s:17:"field_sql_storage";s:8:"settings";a:0:{}s:6:"module";s:17:"field_sql_storage";s:6:"active";s:1:"1";s:7:"details";a:1:{s:3:"sql";a:2:{s:18:"FIELD_LOAD_CURRENT";a:1:{s:24:"field_data_field_integer";a:1:{s:5:"value";s:19:"field_integer_value";}}s:19:"FIELD_LOAD_REVISION";a:1:{s:28:"field_revision_field_integer";a:1:{s:5:"value";s:19:"field_integer_value";}}}}}s:12:"foreign keys";a:0:{}s:7:"indexes";a:0:{}s:2:"id";s:2:"14";}',
'cardinality' => '1',
'translatable' => '0',
'deleted' => '0',
),
array(
'id' => '15',
'field_name' => 'field_link',
'type' => 'link_field',
'module' => 'link',
'active' => '1',
'storage_type' => 'field_sql_storage',
'storage_module' => 'field_sql_storage',
'storage_active' => '1',
'locked' => '0',
'data' => 'a:7:{s:12:"translatable";s:1:"0";s:12:"entity_types";a:0:{}s:8:"settings";a:7:{s:10:"attributes";a:3:{s:6:"target";s:7:"default";s:5:"class";s:0:"";s:3:"rel";s:0:"";}s:3:"url";i:0;s:5:"title";s:8:"optional";s:11:"title_value";s:0:"";s:15:"title_maxlength";i:128;s:13:"enable_tokens";i:1;s:7:"display";a:1:{s:10:"url_cutoff";i:80;}}s:7:"storage";a:5:{s:4:"type";s:17:"field_sql_storage";s:8:"settings";a:0:{}s:6:"module";s:17:"field_sql_storage";s:6:"active";s:1:"1";s:7:"details";a:1:{s:3:"sql";a:2:{s:18:"FIELD_LOAD_CURRENT";a:1:{s:21:"field_data_field_link";a:3:{s:3:"url";s:14:"field_link_url";s:5:"title";s:16:"field_link_title";s:10:"attributes";s:21:"field_link_attributes";}}s:19:"FIELD_LOAD_REVISION";a:1:{s:25:"field_revision_field_link";a:3:{s:3:"url";s:14:"field_link_url";s:5:"title";s:16:"field_link_title";s:10:"attributes";s:21:"field_link_attributes";}}}}}s:12:"foreign keys";a:0:{}s:7:"indexes";a:0:{}s:2:"id";s:2:"15";}',
'cardinality' => '1',
'translatable' => '0',
'deleted' => '0',
),
);
parent::setUp();

View file

@ -29,6 +29,10 @@ class ViewModeTest extends MigrateSqlSourceTestCase {
'entity_type' => 'node',
'view_mode' => 'teaser',
),
array(
'entity_type' => 'node',
'view_mode' => 'custom',
),
array(
'entity_type' => 'user',
'view_mode' => 'default',
@ -50,7 +54,7 @@ class ViewModeTest extends MigrateSqlSourceTestCase {
'field_name' => 'body',
'entity_type' => 'node',
'bundle' => 'forum',
'data' => 'a:6:{s:5:"label";s:4:"Body";s:6:"widget";a:4:{s:4:"type";s:26:"text_textarea_with_summary";s:8:"settings";a:2:{s:4:"rows";i:20;s:12:"summary_rows";i:5;}s:6:"weight";i:1;s:6:"module";s:4:"text";}s:8:"settings";a:3:{s:15:"display_summary";b:1;s:15:"text_processing";i:1;s:18:"user_register_form";b:0;}s:7:"display";a:2:{s:7:"default";a:5:{s:5:"label";s:6:"hidden";s:4:"type";s:12:"text_default";s:8:"settings";a:0:{}s:6:"module";s:4:"text";s:6:"weight";i:11;}s:6:"teaser";a:5:{s:5:"label";s:6:"hidden";s:4:"type";s:23:"text_summary_or_trimmed";s:8:"settings";a:1:{s:11:"trim_length";i:600;}s:6:"module";s:4:"text";s:6:"weight";i:11;}}s:8:"required";b:0;s:11:"description";s:0:"";}',
'data' => 'a:6:{s:5:"label";s:4:"Body";s:6:"widget";a:4:{s:4:"type";s:26:"text_textarea_with_summary";s:8:"settings";a:2:{s:4:"rows";i:20;s:12:"summary_rows";i:5;}s:6:"weight";i:1;s:6:"module";s:4:"text";}s:8:"settings";a:3:{s:15:"display_summary";b:1;s:15:"text_processing";i:1;s:18:"user_register_form";b:0;}s:7:"display";a:3:{s:7:"default";a:5:{s:5:"label";s:6:"hidden";s:4:"type";s:12:"text_default";s:8:"settings";a:0:{}s:6:"module";s:4:"text";s:6:"weight";i:11;}s:6:"teaser";a:5:{s:5:"label";s:6:"hidden";s:4:"type";s:23:"text_summary_or_trimmed";s:8:"settings";a:1:{s:11:"trim_length";i:600;}s:6:"module";s:4:"text";s:6:"weight";i:11;}s:6:"custom";a:5:{s:5:"label";s:6:"hidden";s:4:"type";s:23:"text_summary_or_trimmed";s:8:"settings";a:0:{}s:6:"module";s:4:"text";s:6:"weight";i:11;}}s:8:"required";b:0;s:11:"description";s:0:"";}',
'deleted' => '0',
),
array(

View file

@ -325,25 +325,30 @@ class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
* This validator is used only when cardinality not set to 1 or unlimited.
*/
public static function validateMultipleCount($element, FormStateInterface $form_state, $form) {
$parents = $element['#parents'];
$values = NestedArray::getValue($form_state->getValues(), $parents);
$values = NestedArray::getValue($form_state->getValues(), $element['#parents']);
array_pop($parents);
$current = count(Element::children(NestedArray::getValue($form, $parents))) - 1;
$array_parents = $element['#array_parents'];
array_pop($array_parents);
$previously_uploaded_count = count(Element::children(NestedArray::getValue($form, $array_parents))) - 1;
$field_storage_definitions = \Drupal::entityManager()->getFieldStorageDefinitions($element['#entity_type']);
$field_storage = $field_storage_definitions[$element['#field_name']];
$uploaded = count($values['fids']);
$count = $uploaded + $current;
if ($count > $field_storage->getCardinality()) {
$keep = $uploaded - $count + $field_storage->getCardinality();
$newly_uploaded_count = count($values['fids']);
$total_uploaded_count = $newly_uploaded_count + $previously_uploaded_count;
if ($total_uploaded_count > $field_storage->getCardinality()) {
$keep = $newly_uploaded_count - $total_uploaded_count + $field_storage->getCardinality();
$removed_files = array_slice($values['fids'], $keep);
$removed_names = array();
foreach ($removed_files as $fid) {
$file = File::load($fid);
$removed_names[] = $file->getFilename();
}
$args = array('%field' => $field_storage->getName(), '@max' => $field_storage->getCardinality(), '@count' => $uploaded, '%list' => implode(', ', $removed_names));
$args = [
'%field' => $field_storage->getName(),
'@max' => $field_storage->getCardinality(),
'@count' => $total_uploaded_count,
'%list' => implode(', ', $removed_names),
];
$message = t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args);
drupal_set_message($message, 'warning');
$values['fids'] = array_slice($values['fids'], 0, $keep);

View file

@ -56,13 +56,13 @@ class FileFieldRSSContentTest extends FileFieldTestBase {
// Check that the RSS enclosure appears in the RSS feed.
$this->drupalGet('rss.xml');
$uploaded_filename = str_replace('public://', '', $node_file->getFileUri());
$test_element = sprintf(
'<enclosure url="%s" length="%s" type="%s" />',
$selector = sprintf(
'enclosure[url="%s"][length="%s"][type="%s"]',
file_create_url("public://$uploaded_filename", array('absolute' => TRUE)),
$node_file->getSize(),
$node_file->getMimeType()
);
$this->assertRaw($test_element, 'File field RSS enclosure is displayed when viewing the RSS feed.');
$this->assertTrue(!empty($this->cssSelect($selector)), 'File field RSS enclosure is displayed when viewing the RSS feed.');
}
}

View file

@ -36,6 +36,8 @@ abstract class FileFieldTestBase extends WebTestBase {
/**
* Retrieves a sample file of the specified type.
*
* @return \Drupal\file\FileInterface
*/
function getTestFile($type_name, $size = NULL) {
// Get a file to upload.

View file

@ -247,32 +247,39 @@ class FileFieldWidgetTest extends FileFieldTestBase {
$this->assertTrue(empty($node->{$field_name}->target_id), 'Node was successfully saved without any files.');
}
$upload_files = array($test_file, $test_file);
$upload_files_node_creation = array($test_file, $test_file);
// Try to upload multiple files, but fewer than the maximum.
$nid = $this->uploadNodeFiles($upload_files, $field_name, $type_name);
$nid = $this->uploadNodeFiles($upload_files_node_creation, $field_name, $type_name);
$node_storage->resetCache(array($nid));
$node = $node_storage->load($nid);
$this->assertEqual(count($node->{$field_name}), count($upload_files), 'Node was successfully saved with mulitple files.');
$this->assertEqual(count($node->{$field_name}), count($upload_files_node_creation), 'Node was successfully saved with mulitple files.');
// Try to upload more files than allowed on revision.
$this->uploadNodeFiles($upload_files, $field_name, $nid, 1);
$args = array(
$upload_files_node_revision = array($test_file, $test_file, $test_file, $test_file);
$this->uploadNodeFiles($upload_files_node_revision, $field_name, $nid, 1);
$args = [
'%field' => $field_name,
'@count' => $cardinality
);
$this->assertRaw(t('%field: this field cannot hold more than @count values.', $args));
'@max' => $cardinality,
'@count' => count($upload_files_node_creation) + count($upload_files_node_revision),
'%list' => implode(', ', array_fill(0, 3, $test_file->getFilename())),
];
$this->assertRaw(t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args));
$node_storage->resetCache(array($nid));
$node = $node_storage->load($nid);
$this->assertEqual(count($node->{$field_name}), count($upload_files), 'More files than allowed could not be saved to node.');
$this->assertEqual(count($node->{$field_name}), $cardinality, 'More files than allowed could not be saved to node.');
// Try to upload exactly the allowed number of files on revision.
$this->uploadNodeFile($test_file, $field_name, $nid, 1);
// Try to upload exactly the allowed number of files on revision. Create an
// empty node first, to fill it in its first revision.
$node = $this->drupalCreateNode([
'type' => $type_name
]);
$this->uploadNodeFile($test_file, $field_name, $node->id(), 1);
$node_storage->resetCache(array($nid));
$node = $node_storage->load($nid);
$this->assertEqual(count($node->{$field_name}), $cardinality, 'Node was successfully revised to maximum number of files.');
// Try to upload exactly the allowed number of files, new node.
$upload_files[] = $test_file;
$upload_files = array_fill(0, $cardinality, $test_file);
$nid = $this->uploadNodeFiles($upload_files, $field_name, $type_name);
$node_storage->resetCache(array($nid));
$node = $node_storage->load($nid);

View file

@ -1,5 +1,6 @@
id: d6_language_content_settings
label: Drupal 6 language content settings
migration_tags:
- Drupal 6
source:
@ -39,6 +40,8 @@ process:
2: true
destination:
plugin: entity:language_content_settings
content_translation_update_definitions:
- node
migration_dependencies:
required:
- d6_node_type

View file

@ -39,6 +39,8 @@ process:
2: true
destination:
plugin: entity:language_content_settings
content_translation_update_definitions:
- node
migration_dependencies:
required:
- d7_node_type

View file

@ -276,8 +276,8 @@ class LanguageNegotiator implements LanguageNegotiatorInterface {
*/
function purgeConfiguration() {
// Ensure that we are getting the defined language negotiation information.
// An invocation of \Drupal\Core\Extension\ModuleHandler::install() or
// \Drupal\Core\Extension\ModuleHandler::uninstall() could invalidate the
// An invocation of \Drupal\Core\Extension\ModuleInstaller::install() or
// \Drupal\Core\Extension\ModuleInstaller::uninstall() could invalidate the
// cached information.
$this->negotiatorManager->clearCachedDefinitions();
$this->languageManager->reset();
@ -291,8 +291,8 @@ class LanguageNegotiator implements LanguageNegotiatorInterface {
*/
function updateConfiguration(array $types) {
// Ensure that we are getting the defined language negotiation information.
// An invocation of \Drupal\Core\Extension\ModuleHandler::install() or
// \Drupal\Core\Extension\ModuleHandler::uninstall() could invalidate the
// An invocation of \Drupal\Core\Extension\ModuleInstaller::install() or
// \Drupal\Core\Extension\ModuleInstaller::uninstall() could invalidate the
// cached information.
$this->negotiatorManager->clearCachedDefinitions();
$this->languageManager->reset();

View file

@ -169,7 +169,7 @@ class LinkItem extends FieldItemBase implements LinkItemInterface {
* {@inheritdoc}
*/
public function getUrl() {
return Url::fromUri($this->uri);
return Url::fromUri($this->uri, $this->options);
}
/**

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\link\Plugin\migrate\cckfield\d7;
use Drupal\link\Plugin\migrate\cckfield\LinkField as D6LinkField;
/**
* @MigrateCckField(
* id = "link_field",
* core = {7},
* type_map = {
* "link_field" = "link"
* }
* )
*
* This plugin provides the exact same functionality as the Drupal 6 "link"
* plugin with the exception that the plugin ID "link_field" is used in the
* field type map.
*/
class LinkField extends D6LinkField {
/**
* {@inheritdoc}
*/
public function getFieldWidgetMap() {
// By default, use the plugin ID for the widget types.
return ['link_field' => 'link_default'];
}
}

View file

@ -78,6 +78,13 @@ class LinkItemTest extends FieldKernelTestBase {
$entity->field_test->title = $title;
$entity->field_test->first()->get('options')->set('query', $parsed_url['query']);
$entity->field_test->first()->get('options')->set('attributes', array('class' => $class));
$this->assertEquals([
'query' => $parsed_url['query'],
'attributes' => [
'class' => $class,
],
'external' => TRUE,
], $entity->field_test->first()->getUrl()->getOptions());
$entity->name->value = $this->randomMachineName();
$entity->save();

View file

@ -12,6 +12,7 @@ process:
menu_name:
-
plugin: migration
# The menu migration is in the system module.
migration: menu
source: menu_name
-
@ -20,7 +21,7 @@ process:
management: admin
bypass: true
'link/uri':
plugin: internal_uri
plugin: link_uri
source:
- link_path
'link/options': options

View file

@ -1,30 +0,0 @@
<?php
namespace Drupal\menu_link_content\Plugin\migrate\process\d6;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Process a path into an 'internal:' URI.
*
* @MigrateProcessPlugin(
* id = "internal_uri"
* )
*/
class InternalUri extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
list($path) = $value;
if (parse_url($path, PHP_URL_SCHEME) === NULL) {
return 'internal:/' . $path;
}
return $path;
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Drupal\menu_link_content\Plugin\migrate\process\d6;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Processes a link path into an 'internal:' or 'entity:' URI.
*
* @MigrateProcessPlugin(
* id = "link_uri"
* )
*/
class LinkUri extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The entity type manager, used to fetch entity link templates.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a LinkUri object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager, used to fetch entity link templates.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
list($path) = $value;
$path = ltrim($path, '/');
if (parse_url($path, PHP_URL_SCHEME) === NULL) {
$path = 'internal:/' . $path;
// Convert entity URIs to the entity scheme, if the path matches a route
// of the form "entity.$entity_type_id.canonical".
// @see \Drupal\Core\Url::fromEntityUri()
$url = Url::fromUri($path);
if ($url->isRouted()) {
$route_name = $url->getRouteName();
foreach (array_keys($this->entityTypeManager->getDefinitions()) as $entity_type_id) {
if ($route_name == "entity.$entity_type_id.canonical" && isset($url->getRouteParameters()[$entity_type_id])) {
return "entity:$entity_type_id/" . $url->getRouteParameters()[$entity_type_id];
}
}
}
}
return $path;
}
}

View file

@ -0,0 +1,136 @@
<?php
namespace Drupal\Tests\menu_link_content\Unit\Plugin\migrate\process\d6;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Url;
use Drupal\menu_link_content\Plugin\migrate\process\d6\LinkUri;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Path\PathValidator;
/**
* Tests \Drupal\menu_link_content\Plugin\migrate\process\d6\LinkUri.
*
* @group menu_link_content
*
* @coversDefaultClass \Drupal\menu_link_content\Plugin\migrate\process\d6\LinkUri
*/
class LinkUriTest extends UnitTestCase {
/**
* The entity type manager prophecy used in the test.
*
* @var \Prophecy\Prophecy\ProphecyInterface|\Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The 'link_uri' process plugin being tested.
*
* @var \Drupal\menu_link_content\Plugin\migrate\process\d6\LinkUri
*/
protected $processPlugin;
/**
* The path validator prophecy used in the test.
*
* @var \Drupal\Core\Path\PathValidator
*/
protected $pathValidator;
/**
* The fake entity type ID used in the test.
*
* @var string
*/
protected $entityTypeId = 'the_entity_type_id';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
$this->entityTypeManager->getDefinitions()->willReturn([$this->entityTypeId => '']);
$this->processPlugin = new LinkUri([], 'link_uri', [], $this->entityTypeManager->reveal());
// Url::fromInternalUri() accesses the path validator from the global
// container.
// @see \Drupal\Core\Url::fromInternalUri()
$this->pathValidator = $this->prophesize(PathValidator::class);
$container = new ContainerBuilder();
$container->set('path.validator', $this->pathValidator->reveal());
\Drupal::setContainer($container);
}
/**
* Tests LinkUri::transform().
*
* @param array $value
* The value to pass to LinkUri::transform().
* @param string $expected
* The expected return value of LinkUri::transform().
* @param \Drupal\Core\Url $url
* (optional) The URL that the path validator prophecy will return.
*
* @dataProvider providerTestTransform
*
* @covers ::transform
*/
public function testTransform(array $value, $expected, Url $url = NULL) {
$migrate_executable = $this->prophesize(MigrateExecutableInterface::class);
$row = $this->prophesize(Row::class);
if ($url) {
$this->pathValidator->getUrlIfValidWithoutAccessCheck(reset($value))->willReturn($url);
}
$actual = $this->processPlugin->transform($value, $migrate_executable->reveal(), $row->reveal(), 'link/uri');
$this->assertEquals($expected, $actual);
}
/**
* Provides test cases for LinkUriTest::testTransform().
*
* @return array
* An array of test cases, each which the following values:
* - The value array to pass to LinkUri::transform().
* - The expected path returned by LinkUri::transform().
* - (optional) A URL object that the path validator prophecy will return.
*/
public function providerTestTransform() {
$tests = [];
$value = ['http://example.com'];
$expected = 'http://example.com';
$tests['with_scheme'] = [$value, $expected];
$value = ['/test'];
$expected = 'internal:/test';
$tests['leading_slash'] = [$value, $expected];
$value = ['test'];
$expected = 'internal:/test';
$tests['without_scheme'] = [$value, $expected];
$url = Url::fromRoute('route_name');
$tests['with_route'] = [$value, $expected, $url];
$url = Url::fromRoute('entity.not_an_entity_type_id.canonical');
$tests['without_entity_type'] = [$value, $expected, $url];
$url = Url::fromRoute('entity.the_entity_type_id.canonical');
$tests['without_route_parameter'] = [$value, $expected, $url];
$url = Url::fromRoute('entity.the_entity_type_id.canonical', ['the_entity_type_id' => 'the_entity_id']);
$expected = 'entity:the_entity_type_id/the_entity_id';
$tests['entity_path'] = [$value, $expected, $url];
return $tests;
}
}

View file

@ -1,3 +1,4 @@
# The menu migration is in the system module and the menu_links migration is in the menu_link_content module.
id: menu_settings
label: Menu UI configuration
migration_tags:

View file

@ -94,8 +94,10 @@ abstract class DestinationBase extends PluginBase implements MigrateDestinationI
*
* @param array $id_map
* The map row data for the item.
* @param int $update_action
* The rollback action to take if we are updating an existing item.
*/
protected function setRollbackAction(array $id_map) {
protected function setRollbackAction(array $id_map, $update_action = MigrateIdMapInterface::ROLLBACK_PRESERVE) {
// If the entity we're updating was previously migrated by us, preserve the
// existing rollback action.
if (isset($id_map['sourceid1'])) {
@ -104,7 +106,7 @@ abstract class DestinationBase extends PluginBase implements MigrateDestinationI
// Otherwise, we're updating an entity which already existed on the
// destination and want to make sure we do not delete it on rollback.
else {
$this->rollbackAction = MigrateIdMapInterface::ROLLBACK_PRESERVE;
$this->rollbackAction = $update_action;
}
}

View file

@ -124,7 +124,8 @@ abstract class Entity extends DestinationBase implements ContainerFactoryPluginI
protected function getEntity(Row $row, array $old_destination_id_values) {
$entity_id = reset($old_destination_id_values) ?: $this->getEntityId($row);
if (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) {
$this->updateEntity($entity, $row);
// Allow updateEntity() to change the entity.
$entity = $this->updateEntity($entity, $row) ?: $entity;
}
else {
// Attempt to ensure we always have a bundle.

View file

@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateException;
@ -85,7 +86,12 @@ class EntityContentBase extends Entity {
if (!$entity) {
throw new MigrateException('Unable to get entity');
}
return $this->save($entity, $old_destination_id_values);
$ids = $this->save($entity, $old_destination_id_values);
if (!empty($this->configuration['translations'])) {
$ids[] = $entity->language()->getId();
}
return $ids;
}
/**
@ -104,12 +110,32 @@ class EntityContentBase extends Entity {
return array($entity->id());
}
/**
* Get whether this destination is for translations.
*
* @return bool
* Whether this destination is for translations.
*/
protected function isTranslationDestination() {
return !empty($this->configuration['translations']);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$id_key = $this->getKey('id');
$ids[$id_key]['type'] = 'integer';
if ($this->isTranslationDestination()) {
if ($key = $this->getKey('langcode')) {
$ids[$key]['type'] = 'string';
}
else {
throw new MigrateException('This entity type does not support translation.');
}
}
return $ids;
}
@ -120,8 +146,29 @@ class EntityContentBase extends Entity {
* The entity to update.
* @param \Drupal\migrate\Row $row
* The row object to update from.
*
* @return NULL|\Drupal\Core\Entity\EntityInterface
* An updated entity, or NULL if it's the same as the one passed in.
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
// By default, an update will be preserved.
$rollback_action = MigrateIdMapInterface::ROLLBACK_PRESERVE;
// Make sure we have the right translation.
if ($this->isTranslationDestination()) {
$property = $this->storage->getEntityType()->getKey('langcode');
if ($row->hasDestinationProperty($property)) {
$language = $row->getDestinationProperty($property);
if (!$entity->hasTranslation($language)) {
$entity->addTranslation($language);
// We're adding a translation, so delete it on rollback.
$rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE;
}
$entity = $entity->getTranslation($language);
}
}
// If the migration has specified a list of properties to be overwritten,
// clone the row with an empty set of destination values, and re-add only
// the specified properties.
@ -140,7 +187,10 @@ class EntityContentBase extends Entity {
}
}
$this->setRollbackAction($row->getIdMap());
$this->setRollbackAction($row->getIdMap(), $rollback_action);
// We might have a different (translated) entity, so return it.
return $entity;
}
/**
@ -185,4 +235,32 @@ class EntityContentBase extends Entity {
}
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
if ($this->isTranslationDestination()) {
// Attempt to remove the translation.
$entity = $this->storage->load(reset($destination_identifier));
if ($entity && $entity instanceof TranslatableInterface) {
if ($key = $this->getKey('langcode')) {
if (isset($destination_identifier[$key])) {
$langcode = $destination_identifier[$key];
if ($entity->hasTranslation($langcode)) {
// Make sure we don't remove the default translation.
$translation = $entity->getTranslation($langcode);
if (!$translation->isDefaultTranslation()) {
$entity->removeTranslation($langcode);
$entity->save();
}
}
}
}
}
}
else {
parent::rollback($destination_identifier);
}
}
}

View file

@ -93,7 +93,7 @@ class Migration extends ProcessPluginBase implements ContainerFactoryPluginInter
$source_id_values[$migration_id] = $value;
}
// Break out of the loop as soon as a destination ID is found.
if ($destination_ids = $migration->getIdMap()->lookupDestinationID($source_id_values[$migration_id])) {
if ($destination_ids = $migration->getIdMap()->lookupDestinationId($source_id_values[$migration_id])) {
break;
}
}
@ -162,7 +162,7 @@ class Migration extends ProcessPluginBase implements ContainerFactoryPluginInter
*
* @throws \Drupal\migrate\MigrateSkipProcessException
*/
protected function skipOnEmpty($value) {
protected function skipOnEmpty(array $value) {
if (!array_filter($value)) {
throw new MigrateSkipProcessException();
}

View file

@ -0,0 +1,41 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateException;
use Drupal\Component\Utility\Unicode;
/**
* This plugin returns a substring of the current value.
*
* @MigrateProcessPlugin(
* id = "substr"
* )
*/
class Substr extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$start = isset($this->configuration['start']) ? $this->configuration['start'] : 0;
if (!is_int($start)) {
throw new MigrateException('The start position configuration value should be an integer. Omit this key to capture from the beginning of the string.');
}
$length = isset($this->configuration['length']) ? $this->configuration['length'] : NULL;
if (!is_null($length) && !is_int($length)) {
throw new MigrateException('The character length configuration value should be an integer. Omit this key to capture from the start position to the end of the string.');
}
if (!is_string($value)) {
throw new MigrateException('The input value must be a string.');
}
// Use optional start or length to return a portion of $value.
$new_value = Unicode::substr($value, $start, $length);
return $new_value;
}
}

View file

@ -0,0 +1,8 @@
name: 'Migration external translated test'
type: module
package: Testing
version: VERSION
core: 8.x
dependencies:
- node
- migrate

View file

@ -0,0 +1,19 @@
id: external_translated_test_node
label: External translated content
source:
plugin: migrate_external_translated_test
default_lang: true
constants:
type: external_test
process:
type: constants/type
title: title
langcode:
plugin: static_map
source: lang
map:
English: en
French: fr
Spanish: es
destination:
plugin: entity:node

View file

@ -0,0 +1,27 @@
id: external_translated_test_node_translation
label: External translated content translations
source:
plugin: migrate_external_translated_test
default_lang: false
constants:
type: external_test
process:
nid:
plugin: migration
source: name
migration: external_translated_test_node
type: constants/type
title: title
langcode:
plugin: static_map
source: lang
map:
English: en
French: fr
Spanish: es
destination:
plugin: entity:node
translations: true
migration_dependencies:
required:
- external_translated_test_node

View file

@ -0,0 +1,77 @@
<?php
namespace Drupal\migrate_external_translated_test\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
/**
* A simple migrate source for our tests.
*
* @MigrateSource(
* id = "migrate_external_translated_test"
* )
*/
class MigrateExternalTranslatedTestSource extends SourcePluginBase {
/**
* The data to import.
*
* @var array
*/
protected $import = [
['name' => 'cat', 'title' => 'Cat', 'lang' => 'English'],
['name' => 'cat', 'title' => 'Chat', 'lang' => 'French'],
['name' => 'cat', 'title' => 'Gato', 'lang' => 'Spanish'],
['name' => 'dog', 'title' => 'Dog', 'lang' => 'English'],
['name' => 'dog', 'title' => 'Chien', 'lang' => 'French'],
['name' => 'monkey', 'title' => 'Monkey', 'lang' => 'English'],
];
/**
* {@inheritdoc}
*/
public function fields() {
return [
'name' => $this->t('Unique name'),
'title' => $this->t('Title'),
'lang' => $this->t('Language'),
];
}
/**
* {@inheritdoc}
*/
public function __toString() {
return '';
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['name']['type'] = 'string';
if (!$this->configuration['default_lang']) {
$ids['lang']['type'] = 'string';
}
return $ids;
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$data = [];
// Keep the rows with the right languages.
$want_default = $this->configuration['default_lang'];
foreach ($this->import as $row) {
$is_english = $row['lang'] == 'English';
if ($want_default == $is_english) {
$data[] = $row;
}
}
return new \ArrayIterator($data);
}
}

View file

@ -0,0 +1,159 @@
<?php
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* Tests the EntityContentBase destination.
*
* @group migrate
*/
class MigrateEntityContentBaseTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['migrate', 'user', 'language', 'entity_test'];
/**
* The storage for entity_test_mul.
*
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
*/
protected $storage;
/**
* A content migrate destination.
*
* @var \Drupal\migrate\Plugin\MigrateDestinationInterface
*/
protected $destination;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_mul');
ConfigurableLanguage::createFromLangcode('en')->save();
ConfigurableLanguage::createFromLangcode('fr')->save();
$this->storage = $this->container->get('entity.manager')->getStorage('entity_test_mul');
}
/**
* Check the existing translations of an entity.
*
* @param int $id
* The entity ID.
* @param string $default
* The expected default translation language code.
* @param string[] $others
* The expected other translation language codes.
*/
protected function assertTranslations($id, $default, $others = []) {
$entity = $this->storage->load($id);
$this->assertTrue($entity, "Entity exists");
$this->assertEquals($default, $entity->language()->getId(), "Entity default translation");
$translations = array_keys($entity->getTranslationLanguages(FALSE));
sort($others);
sort($translations);
$this->assertEquals($others, $translations, "Entity translations");
}
/**
* Create the destination plugin to test.
*
* @param array $configuration
* The plugin configuration.
*/
protected function createDestination(array $configuration) {
$this->destination = new EntityContentBase(
$configuration,
'fake_plugin_id',
[],
$this->getMock(MigrationInterface::class),
$this->storage,
[],
$this->container->get('entity.manager'),
$this->container->get('plugin.manager.field.field_type')
);
}
/**
* Test importing and rolling back translated entities.
*/
public function testTranslated() {
// Create a destination.
$this->createDestination(['translations' => TRUE]);
// Create some pre-existing entities.
$this->storage->create(['id' => 1, 'langcode' => 'en'])->save();
$this->storage->create(['id' => 2, 'langcode' => 'fr'])->save();
$translated = $this->storage->create(['id' => 3, 'langcode' => 'en']);
$translated->save();
$translated->addTranslation('fr')->save();
// Pre-assert that things are as expected.
$this->assertTranslations(1, 'en');
$this->assertTranslations(2, 'fr');
$this->assertTranslations(3, 'en', ['fr']);
$this->assertFalse($this->storage->load(4));
$destination_rows = [
// Existing default translation.
['id' => 1, 'langcode' => 'en', 'action' => MigrateIdMapInterface::ROLLBACK_PRESERVE],
// New translation.
['id' => 2, 'langcode' => 'en', 'action' => MigrateIdMapInterface::ROLLBACK_DELETE],
// Existing non-default translation.
['id' => 3, 'langcode' => 'fr', 'action' => MigrateIdMapInterface::ROLLBACK_PRESERVE],
// Brand new row.
['id' => 4, 'langcode' => 'fr', 'action' => MigrateIdMapInterface::ROLLBACK_DELETE],
];
$rollback_actions = [];
// Import some rows.
foreach ($destination_rows as $idx => $destination_row) {
$row = new Row([], []);
foreach ($destination_row as $key => $value) {
$row->setDestinationProperty($key, $value);
}
$this->destination->import($row);
// Check that the rollback action is correct, and save it.
$this->assertEquals($destination_row['action'], $this->destination->rollbackAction());
$rollback_actions[$idx] = $this->destination->rollbackAction();
}
$this->assertTranslations(1, 'en');
$this->assertTranslations(2, 'fr', ['en']);
$this->assertTranslations(3, 'en', ['fr']);
$this->assertTranslations(4, 'fr');
// Rollback the rows.
foreach ($destination_rows as $idx => $destination_row) {
if ($rollback_actions[$idx] == MigrateIdMapInterface::ROLLBACK_DELETE) {
$this->destination->rollback($destination_row);
}
}
// No change, update of existing translation.
$this->assertTranslations(1, 'en');
// Remove added translation.
$this->assertTranslations(2, 'fr');
// No change, update of existing translation.
$this->assertTranslations(3, 'en', ['fr']);
// No change, can't remove default translation.
$this->assertTranslations(4, 'fr');
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Drupal\Tests\migrate\Kernel;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\migrate\MigrateExecutable;
use Drupal\node\Entity\NodeType;
/**
* Tests migrating non-Drupal translated content.
*
* Ensure it's possible to migrate in translations, even if there's no nid or
* tnid property on the source.
*
* @group migrate
*/
class MigrateExternalTranslatedTest extends MigrateTestBase {
/**
* {@inheritdoc}
*
* @todo: Remove migrate_drupal when https://www.drupal.org/node/2560795 is
* fixed.
*/
public static $modules = ['system', 'user', 'language', 'node', 'field', 'migrate_drupal', 'migrate_external_translated_test'];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->installSchema('system', ['sequences']);
$this->installSchema('node', array('node_access'));
$this->installEntitySchema('user');
$this->installEntitySchema('node');
// Create some languages.
ConfigurableLanguage::createFromLangcode('en')->save();
ConfigurableLanguage::createFromLangcode('fr')->save();
ConfigurableLanguage::createFromLangcode('es')->save();
// Create a content type.
NodeType::create([
'type' => 'external_test',
'name' => 'Test node type',
])->save();
}
/**
* Test importing and rolling back our data.
*/
public function testMigrations() {
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->container->get('entity.manager')->getStorage('node');
$this->assertEquals(0, count($storage->loadMultiple()));
// Run the migrations.
$migration_ids = ['external_translated_test_node', 'external_translated_test_node_translation'];
$this->executeMigrations($migration_ids);
$this->assertEquals(3, count($storage->loadMultiple()));
$node = $storage->load(1);
$this->assertEquals('en', $node->language()->getId());
$this->assertEquals('Cat', $node->title->value);
$this->assertEquals('Chat', $node->getTranslation('fr')->title->value);
$this->assertEquals('Gato', $node->getTranslation('es')->title->value);
$node = $storage->load(2);
$this->assertEquals('en', $node->language()->getId());
$this->assertEquals('Dog', $node->title->value);
$this->assertEquals('Chien', $node->getTranslation('fr')->title->value);
$this->assertFalse($node->hasTranslation('es'), "No spanish translation for node 2");
$node = $storage->load(3);
$this->assertEquals('en', $node->language()->getId());
$this->assertEquals('Monkey', $node->title->value);
$this->assertFalse($node->hasTranslation('fr'), "No french translation for node 3");
$this->assertFalse($node->hasTranslation('es'), "No spanish translation for node 3");
$this->assertNull($storage->load(4), "No node 4 migrated");
// Roll back the migrations.
foreach ($migration_ids as $migration_id) {
$migration = $this->getMigration($migration_id);
$executable = new MigrateExecutable($migration, $this);
$executable->rollback();
}
$this->assertEquals(0, count($storage->loadMultiple()));
}
}

View file

@ -8,6 +8,7 @@
namespace Drupal\Tests\migrate\Unit\Plugin\migrate\destination;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityType;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
@ -97,6 +98,33 @@ class EntityContentBaseTest extends UnitTestCase {
$destination->import(new Row([], []));
}
/**
* Test that translation destination fails for untranslatable entities.
*
* @expectedException \Drupal\migrate\MigrateException
* @expectedExceptionMessage This entity type does not support translation
*/
public function testUntranslatable() {
// An entity type without a language.
$entity_type = $this->prophesize(ContentEntityType::class);
$entity_type->getKey('langcode')->willReturn('');
$entity_type->getKey('id')->willReturn('id');
$this->storage->getEntityType()->willReturn($entity_type->reveal());
$destination = new EntityTestDestination(
[ 'translations' => TRUE ],
'',
[],
$this->migration->reveal(),
$this->storage->reveal(),
[],
$this->entityManager->reveal(),
$this->prophesize(FieldTypePluginManagerInterface::class)->reveal()
);
$destination->getIds();
}
}
/**

View file

@ -2,6 +2,7 @@
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\process\Migration;
use Drupal\migrate\Plugin\MigrateDestinationInterface;
@ -86,4 +87,51 @@ class MigrationTest extends MigrateProcessTestCase {
$this->assertEquals(2, $result);
}
/**
* Tests that processing is skipped when the input value is empty.
*
* @expectedException \Drupal\migrate\MigrateSkipProcessException
*/
public function testSkipOnEmpty() {
$migration_plugin = $this->prophesize(MigrationInterface::class);
$migration_plugin_manager = $this->prophesize(MigrationPluginManagerInterface::class);
$process_plugin_manager = $this->prophesize(MigratePluginManager::class);
$configuration = [
'migration' => 'foobaz',
];
$migration_plugin->id()->willReturn(uniqid());
$migration = new Migration($configuration, 'migration', [], $migration_plugin->reveal(), $migration_plugin_manager->reveal(), $process_plugin_manager->reveal());
$migration->transform(0, $this->migrateExecutable, $this->row, 'foo');
}
/**
* Tests a successful lookup.
*/
public function testSuccessfulLookup() {
$migration_plugin = $this->prophesize(MigrationInterface::class);
$migration_plugin_manager = $this->prophesize(MigrationPluginManagerInterface::class);
$process_plugin_manager = $this->prophesize(MigratePluginManager::class);
$configuration = [
'migration' => 'foobaz',
];
$migration_plugin->id()->willReturn(uniqid());
$id_map = $this->prophesize(MigrateIdMapInterface::class);
$id_map->lookupDestinationId([1])->willReturn([3]);
$migration_plugin->getIdMap()->willReturn($id_map->reveal());
$migration_plugin_manager->createInstances(['foobaz'])
->willReturn(['foobaz' => $migration_plugin->reveal()]);
$migrationStorage = $this->prophesize(EntityStorageInterface::class);
$migrationStorage
->loadMultiple(['foobaz'])
->willReturn([$migration_plugin->reveal()]);
$migration = new Migration($configuration, 'migration', [], $migration_plugin->reveal(), $migration_plugin_manager->reveal(), $process_plugin_manager->reveal());
$this->assertSame(3, $migration->transform(1, $this->migrateExecutable, $this->row, 'foo'));
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\Plugin\migrate\process\Substr;
/**
* Tests the substr plugin.
*
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\process\Substr
*
* @group migrate
*/
class SubstrTest extends MigrateProcessTestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
}
/**
* Tests Substr plugin based on providerTestSubstr() values.
*
* @dataProvider providerTestSubstr
*/
public function testSubstr($start = NULL, $length = NULL, $expected = NULL) {
$configuration['start'] = $start;
$configuration['length'] = $length;
$this->plugin = new Substr($configuration, 'map', []);
$value = $this->plugin->transform('Captain Janeway', $this->migrateExecutable, $this->row, 'destinationproperty');
$this->assertSame($expected, $value);
}
/**
* Data provider for testSubstr().
*/
public function providerTestSubstr() {
return [
// Tests with valid start and length values.
[0, 7, 'Captain'],
// Tests with valid start > 0 and valid length.
[6, 3, 'n J'],
// Tests with valid start < 0 and valid length.
[-7, 4, 'Jane'],
// Tests without start value and valid length value.
[NULL, 7, 'Captain'],
// Tests with valid start value and no length value.
[1, NULL, 'aptain Janeway'],
// Tests without both start and length values.
[NULL, NULL, 'Captain Janeway'],
];
}
/**
* Tests invalid input type.
*
* @expectedException \Drupal\migrate\MigrateException
* @expectedExceptionMessage The input value must be a string.
*/
public function testSubstrFail() {
$configuration = [];
$this->plugin = new Substr($configuration, 'map', []);
$this->plugin->transform(['Captain Janeway'], $this->migrateExecutable, $this->row, 'destinationproperty');
}
/**
* Tests that the start parameter is an integer.
*
* @expectedException \Drupal\migrate\MigrateException
* @expectedExceptionMessage The start position configuration value should be an integer. Omit this key to capture from the beginning of the string.
*/
public function testStartIsString() {
$configuration['start'] = '2';
$this->plugin = new Substr($configuration, 'map', []);
$this->plugin->transform(['foo'], $this->migrateExecutable, $this->row, 'destinationproperty');
}
/**
* Tests that the length parameter is an integer.
*
* @expectedException \Drupal\migrate\MigrateException
* @expectedExceptionMessage The character length configuration value should be an integer. Omit this key to capture from the start position to the end of the string.
*/
public function testLengthIsString() {
$configuration['length'] = '1';
$this->plugin = new Substr($configuration, 'map', []);
$this->plugin->transform(['foo'], $this->migrateExecutable, $this->row, 'destinationproperty');
}
}

View file

@ -37,6 +37,13 @@ class CckMigration extends Migration implements ContainerFactoryPluginInterface
*/
protected $cckPluginCache;
/**
* The cckfield plugin manager.
*
* @var \Drupal\migrate\Plugin\MigratePluginManager
*/
protected $cckPluginManager;
/**
* Constructs a CckMigration.
*

View file

@ -8034,7 +8034,17 @@ $connection->insert('history')
->values(array(
'uid' => '1',
'nid' => '9',
'timestamp' => '1457655127',
'timestamp' => '1468384961',
))
->values(array(
'uid' => '1',
'nid' => '12',
'timestamp' => '1468384823',
))
->values(array(
'uid' => '1',
'nid' => '13',
'timestamp' => '1468384931',
))
->execute();
@ -34709,7 +34719,7 @@ $connection->insert('menu_router')
'access_callback' => 'user_access',
'access_arguments' => 'a:1:{i:0;s:24:"administer content types";}',
'page_callback' => 'drupal_get_form',
'page_arguments' => 'a:2:{i:0;s:14:"node_type_form";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:2:"20";s:6:"custom";s:1:"0";s:8:"modified";s:1:"0";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}',
'page_arguments' => 'a:2:{i:0;s:14:"node_type_form";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:1:"0";s:6:"custom";s:1:"0";s:8:"modified";s:1:"1";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}',
'fit' => '15',
'number_parts' => '4',
'tab_parent' => '',
@ -34731,7 +34741,7 @@ $connection->insert('menu_router')
'access_callback' => 'user_access',
'access_arguments' => 'a:1:{i:0;s:24:"administer content types";}',
'page_callback' => 'drupal_get_form',
'page_arguments' => 'a:2:{i:0;s:24:"node_type_delete_confirm";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:2:"20";s:6:"custom";s:1:"0";s:8:"modified";s:1:"0";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}',
'page_arguments' => 'a:2:{i:0;s:24:"node_type_delete_confirm";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:1:"0";s:6:"custom";s:1:"0";s:8:"modified";s:1:"1";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}',
'fit' => '31',
'number_parts' => '5',
'tab_parent' => '',
@ -34841,7 +34851,7 @@ $connection->insert('menu_router')
'access_callback' => 'user_access',
'access_arguments' => 'a:1:{i:0;s:24:"administer content types";}',
'page_callback' => 'drupal_get_form',
'page_arguments' => 'a:2:{i:0;s:14:"node_type_form";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:2:"20";s:6:"custom";s:1:"0";s:8:"modified";s:1:"0";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}',
'page_arguments' => 'a:2:{i:0;s:14:"node_type_form";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:1:"0";s:6:"custom";s:1:"0";s:8:"modified";s:1:"1";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}',
'fit' => '31',
'number_parts' => '5',
'tab_parent' => 'admin/content/node-type/company',
@ -41334,6 +41344,40 @@ $connection->insert('node')
'tnid' => '0',
'translate' => '0',
))
->values(array(
'nid' => '10',
'vid' => '13',
'type' => 'page',
'language' => 'en',
'title' => 'The Real McCoy',
'uid' => '1',
'status' => '1',
'created' => '1444238800',
'changed' => '1444238808',
'comment' => '2',
'promote' => '1',
'moderate' => '0',
'sticky' => '0',
'tnid' => '10',
'translate' => '0',
))
->values(array(
'nid' => '11',
'vid' => '14',
'type' => 'page',
'language' => 'fr',
'title' => 'Le Vrai McCoy',
'uid' => '1',
'status' => '1',
'created' => '1444239050',
'changed' => '1444239050',
'comment' => '2',
'promote' => '1',
'moderate' => '0',
'sticky' => '0',
'tnid' => '10',
'translate' => '0',
))
->execute();
$connection->schema()->createTable('node_access', array(
@ -41463,6 +41507,13 @@ $connection->insert('node_comment_statistics')
'last_comment_uid',
'comment_count',
))
->values(array(
'nid' => '0',
'last_comment_timestamp' => '1468384735',
'last_comment_name' => NULL,
'last_comment_uid' => '1',
'comment_count' => '0',
))
->values(array(
'nid' => '1',
'last_comment_timestamp' => '1388271197',
@ -41479,7 +41530,14 @@ $connection->insert('node_comment_statistics')
))
->values(array(
'nid' => '9',
'last_comment_timestamp' => '1444671588',
'last_comment_timestamp' => '1444238800',
'last_comment_name' => NULL,
'last_comment_uid' => '1',
'comment_count' => '0',
))
->values(array(
'nid' => '10',
'last_comment_timestamp' => '1444239050',
'last_comment_name' => NULL,
'last_comment_uid' => '1',
'comment_count' => '0',
@ -41727,6 +41785,28 @@ $connection->insert('node_revisions')
'timestamp' => '1444671588',
'format' => '1',
))
->values(array(
'nid' => '10',
'vid' => '13',
'uid' => '1',
'title' => 'The Real McCoy',
'body' => "In the original, Queen's English.",
'teaser' => "In the original, Queen's English.",
'log' => '',
'timestamp' => '1444238808',
'format' => '1',
))
->values(array(
'nid' => '11',
'vid' => '14',
'uid' => '1',
'title' => 'Le Vrai McCoy',
'body' => 'Ooh là là!',
'teaser' => 'Ooh là là!',
'log' => '',
'timestamp' => '1444239050',
'format' => '1',
))
->execute();
$connection->schema()->createTable('node_type', array(
@ -41861,9 +41941,9 @@ $connection->insert('node_type')
'title_label' => 'Name',
'has_body' => '1',
'body_label' => 'Description',
'min_word_count' => '20',
'min_word_count' => '0',
'custom' => '0',
'modified' => '0',
'modified' => '1',
'locked' => '0',
'orig_type' => 'company',
))
@ -44465,8 +44545,8 @@ $connection->insert('users')
'signature' => '',
'signature_format' => '0',
'created' => '0',
'access' => '1458198052',
'login' => '1458193160',
'access' => '1468384823',
'login' => '1468384420',
'status' => '1',
'timezone' => NULL,
'language' => '',
@ -44809,13 +44889,17 @@ $connection->insert('variable')
'name' => 'comment_article',
'value' => 's:1:"2";',
))
->values(array(
'name' => 'comment_company',
'value' => 's:1:"2";',
))
->values(array(
'name' => 'comment_controls_article',
'value' => 'i:3;',
))
->values(array(
'name' => 'comment_controls_company',
'value' => 'i:3;',
'value' => 's:1:"3";',
))
->values(array(
'name' => 'comment_controls_employee',
@ -44855,7 +44939,7 @@ $connection->insert('variable')
))
->values(array(
'name' => 'comment_default_mode_company',
'value' => 'i:4;',
'value' => 's:1:"4";',
))
->values(array(
'name' => 'comment_default_mode_employee',
@ -44895,7 +44979,7 @@ $connection->insert('variable')
))
->values(array(
'name' => 'comment_default_order_company',
'value' => 'i:1;',
'value' => 's:1:"1";',
))
->values(array(
'name' => 'comment_default_order_employee',
@ -44935,7 +45019,7 @@ $connection->insert('variable')
))
->values(array(
'name' => 'comment_default_per_page_company',
'value' => 'i:50;',
'value' => 's:2:"50";',
))
->values(array(
'name' => 'comment_default_per_page_employee',
@ -44975,7 +45059,7 @@ $connection->insert('variable')
))
->values(array(
'name' => 'comment_form_location_company',
'value' => 'i:0;',
'value' => 's:1:"0";',
))
->values(array(
'name' => 'comment_form_location_employee',
@ -45019,7 +45103,7 @@ $connection->insert('variable')
))
->values(array(
'name' => 'comment_preview_company',
'value' => 'i:1;',
'value' => 's:1:"1";',
))
->values(array(
'name' => 'comment_preview_employee',
@ -45063,7 +45147,7 @@ $connection->insert('variable')
))
->values(array(
'name' => 'comment_subject_field_company',
'value' => 'i:1;',
'value' => 's:1:"1";',
))
->values(array(
'name' => 'comment_subject_field_employee',
@ -45425,6 +45509,10 @@ $connection->insert('variable')
'name' => 'event_nodeapi_article',
'value' => 's:5:"never";',
))
->values(array(
'name' => 'event_nodeapi_company',
'value' => 's:5:"never";',
))
->values(array(
'name' => 'event_nodeapi_event',
'value' => 's:3:"all";',
@ -45487,7 +45575,11 @@ $connection->insert('variable')
))
->values(array(
'name' => 'form_build_id_article',
'value' => 's:48:"form-mXZfFJxcCFGB80PPYtNOuwYbho6-xKTvrRLb3TAMkic";',
'value' => 's:48:"form-t2zKJflpBD4rpYoGQH33ckjjWAYdo5lF3Hl1O_YnWyE";',
))
->values(array(
'name' => 'form_build_id_company',
'value' => 's:48:"form-jFw2agRukPxjG5dG-N6joZLyoxXmCoxTzua0HUciqK0";',
))
->values(array(
'name' => 'forum_block_num_0',
@ -45589,6 +45681,10 @@ $connection->insert('variable')
'name' => 'node_options_book',
'value' => 'a:1:{i:0;s:6:"status";}',
))
->values(array(
'name' => 'node_options_company',
'value' => 'a:2:{i:0;s:6:"status";i:1;s:7:"promote";}',
))
->values(array(
'name' => 'node_options_forum',
'value' => 'a:1:{i:0;s:6:"status";}',
@ -45785,6 +45881,10 @@ $connection->insert('variable')
'name' => 'upload_article',
'value' => 'b:0;',
))
->values(array(
'name' => 'upload_company',
'value' => 's:1:"1";',
))
->values(array(
'name' => 'upload_page',
'value' => 'b:1;',

View file

@ -3474,7 +3474,7 @@ $connection->insert('field_config_instance')
'field_name' => 'body',
'entity_type' => 'node',
'bundle' => 'article',
'data' => 'a:6:{s:5:"label";s:4:"Body";s:6:"widget";a:4:{s:4:"type";s:26:"text_textarea_with_summary";s:8:"settings";a:2:{s:4:"rows";i:20;s:12:"summary_rows";i:5;}s:6:"weight";i:-4;s:6:"module";s:4:"text";}s:8:"settings";a:3:{s:15:"display_summary";b:1;s:15:"text_processing";i:1;s:18:"user_register_form";b:0;}s:7:"display";a:2:{s:7:"default";a:5:{s:5:"label";s:6:"hidden";s:4:"type";s:12:"text_default";s:8:"settings";a:0:{}s:6:"module";s:4:"text";s:6:"weight";i:0;}s:6:"teaser";a:5:{s:5:"label";s:6:"hidden";s:4:"type";s:23:"text_summary_or_trimmed";s:8:"settings";a:1:{s:11:"trim_length";i:600;}s:6:"module";s:4:"text";s:6:"weight";i:0;}}s:8:"required";b:0;s:11:"description";s:0:"";}',
'data' => 'a:6:{s:5:"label";s:4:"Body";s:6:"widget";a:4:{s:4:"type";s:26:"text_textarea_with_summary";s:8:"settings";a:2:{s:4:"rows";i:20;s:12:"summary_rows";i:5;}s:6:"weight";i:-4;s:6:"module";s:4:"text";}s:8:"settings";a:3:{s:15:"display_summary";b:1;s:15:"text_processing";i:1;s:18:"user_register_form";b:0;}s:7:"display";a:3:{s:7:"default";a:5:{s:5:"label";s:6:"hidden";s:4:"type";s:12:"text_default";s:8:"settings";a:0:{}s:6:"module";s:4:"text";s:6:"weight";i:0;}s:6:"teaser";a:5:{s:5:"label";s:6:"hidden";s:4:"type";s:23:"text_summary_or_trimmed";s:8:"settings";a:1:{s:11:"trim_length";i:600;}s:6:"module";s:4:"text";s:6:"weight";i:0;}s:6:"custom";a:5:{s:5:"label";s:6:"hidden";s:4:"type";s:23:"text_summary_or_trimmed";s:8:"settings";a:0:{}s:6:"module";s:4:"text";s:6:"weight";i:11;}}s:8:"required";b:0;s:11:"description";s:0:"";}',
'deleted' => '0',
))
->values(array(
@ -3720,6 +3720,24 @@ $connection->insert('field_config_instance')
'data' => 'a:6:{s:5:"label";s:4:"File";s:6:"widget";a:5:{s:6:"weight";s:1:"8";s:4:"type";s:12:"file_generic";s:6:"module";s:4:"file";s:6:"active";i:1;s:8:"settings";a:1:{s:18:"progress_indicator";s:8:"throbber";}}s:8:"settings";a:5:{s:14:"file_directory";s:0:"";s:15:"file_extensions";s:3:"txt";s:12:"max_filesize";s:0:"";s:17:"description_field";i:0;s:18:"user_register_form";i:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:12:"file_default";s:8:"settings";a:0:{}s:6:"module";s:4:"file";s:6:"weight";i:0;}}s:8:"required";i:0;s:11:"description";s:0:"";}',
'deleted' => '0',
))
->values(array(
'id' => '34',
'field_id' => '15',
'field_name' => 'field_link',
'entity_type' => 'node',
'bundle' => 'article',
'data' => 'a:7:{s:5:"label";s:4:"Link";s:6:"widget";a:5:{s:6:"weight";s:2:"10";s:4:"type";s:10:"link_field";s:6:"module";s:4:"link";s:6:"active";i:0;s:8:"settings";a:0:{}}s:8:"settings";a:12:{s:12:"absolute_url";i:1;s:12:"validate_url";i:1;s:3:"url";i:0;s:5:"title";s:8:"optional";s:11:"title_value";s:19:"Unused Static Title";s:27:"title_label_use_field_label";i:0;s:15:"title_maxlength";s:3:"128";s:7:"display";a:1:{s:10:"url_cutoff";s:2:"81";}s:10:"attributes";a:6:{s:6:"target";s:6:"_blank";s:3:"rel";s:8:"nofollow";s:18:"configurable_class";i:0;s:5:"class";s:7:"classes";s:18:"configurable_title";i:1;s:5:"title";s:0:"";}s:10:"rel_remove";s:19:"rel_remove_external";s:13:"enable_tokens";i:1;s:18:"user_register_form";b:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:12:"link_default";s:6:"weight";s:1:"9";s:8:"settings";a:0:{}s:6:"module";s:4:"link";}}s:8:"required";i:0;s:11:"description";s:0:"";s:13:"default_value";N;}',
'deleted' => '0',
))
->values(array(
'id' => '35',
'field_id' => '14',
'field_name' => 'field_integer',
'entity_type' => 'taxonomy_term',
'bundle' => 'test_vocabulary',
'data' => 'a:7:{s:5:"label";s:7:"Integer";s:6:"widget";a:5:{s:6:"weight";s:1:"2";s:4:"type";s:6:"number";s:6:"module";s:6:"number";s:6:"active";i:0;s:8:"settings";a:0:{}}s:8:"settings";a:5:{s:3:"min";s:0:"";s:3:"max";s:0:"";s:6:"prefix";s:0:"";s:6:"suffix";s:0:"";s:18:"user_register_form";b:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:14:"number_integer";s:8:"settings";a:4:{s:18:"thousand_separator";s:0:"";s:17:"decimal_separator";s:1:".";s:5:"scale";i:0;s:13:"prefix_suffix";b:1;}s:6:"module";s:6:"number";s:6:"weight";i:0;}}s:8:"required";i:0;s:11:"description";s:0:"";s:13:"default_value";N;}',
'deleted' => '0',
))
->execute();
$connection->schema()->createTable('field_data_body', array(
@ -4900,6 +4918,18 @@ $connection->insert('field_data_field_link')
'field_link_title' => 'Click Here',
'field_link_attributes' => 'a:1:{s:5:"title";s:10:"Click Here";}',
))
->values(array(
'entity_type' => 'node',
'bundle' => 'article',
'deleted' => '0',
'entity_id' => '2',
'revision_id' => '2',
'language' => 'und',
'delta' => '0',
'field_link_url' => '<front>',
'field_link_title' => 'Home',
'field_link_attributes' => 'a:0:{}',
))
->execute();
$connection->schema()->createTable('field_data_field_long_text', array(
@ -6668,6 +6698,18 @@ $connection->insert('field_revision_field_link')
'field_link_title' => 'Click Here',
'field_link_attributes' => 'a:1:{s:5:"title";s:10:"Click Here";}',
))
->values(array(
'entity_type' => 'node',
'bundle' => 'article',
'deleted' => '0',
'entity_id' => '2',
'revision_id' => '2',
'language' => 'und',
'delta' => '0',
'field_link_url' => '<front>',
'field_link_title' => 'Home',
'field_link_attributes' => 'a:0:{}',
))
->execute();
$connection->schema()->createTable('field_revision_field_long_text', array(

View file

@ -4,7 +4,10 @@ namespace Drupal\Tests\migrate_drupal\Kernel\d6;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\user\Entity\User;
use Prophecy\Argument;
/**
* @group migrate_drupal
@ -85,4 +88,40 @@ class EntityContentBaseTest extends MigrateDrupal6TestBase {
$this->assertIdentical('proto@zo.an', $account->getInitialEmail());
}
/**
* Test that translation destination fails for untranslatable entities.
*/
public function testUntranslatable() {
$this->enableModules(['language_test']);
$this->installEntitySchema('no_language_entity_test');
/** @var MigrationInterface $migration */
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration([
'source' => [
'plugin' => 'embedded_data',
'ids' => ['id' => ['type' => 'integer']],
'data_rows' => [['id' => 1]],
],
'process' => [
'id' => 'id',
],
'destination' => [
'plugin' => 'entity:no_language_entity_test',
'translations' => TRUE,
],
]);
$message = $this->prophesize(MigrateMessageInterface::class);
// Match the expected message. Can't use default argument types, because
// we need to convert to string from TranslatableMarkup.
$argument = Argument::that(function($msg) {
return strpos((string) $msg, "This entity type does not support translation") !== FALSE;
});
$message->display($argument, Argument::any())
->shouldBeCalled();
$executable = new MigrateExecutable($migration, $message->reveal());
$executable->import();
}
}

View file

@ -89,17 +89,24 @@ abstract class MigrateDrupal6TestBase extends MigrateDrupalTestBase {
/**
* Executes all content migrations.
*
* @param bool $include_revisions
* If TRUE, migrates node revisions.
* @param array $include
* Extra things to include as part of the migrations. Values may be
* 'revisions' or 'translations'.
*/
protected function migrateContent($include_revisions = FALSE) {
protected function migrateContent($include = []) {
if (in_array('translations', $include)) {
$this->executeMigrations(['language']);
}
$this->migrateUsers(FALSE);
$this->migrateFields();
$this->installEntitySchema('node');
$this->executeMigrations(['d6_node_settings', 'd6_node']);
if ($include_revisions) {
if (in_array('translations', $include)) {
$this->executeMigrations(['translations']);
}
if (in_array('revisions', $include)) {
$this->executeMigrations(['d6_node_revision']);
}
}

View file

@ -266,6 +266,14 @@ class MigrateUpgradeForm extends ConfirmFormBase {
'source_module' => 'image',
'destination_module' => 'image',
],
'd6_language_content_settings' => [
'source_module' => 'locale',
'destination_module' => 'language',
],
'd7_language_content_settings' => [
'source_module' => 'locale',
'destination_module' => 'language',
],
'd7_language_negotiation_settings' => [
'source_module' => 'locale',
'destination_module' => 'language',
@ -290,6 +298,10 @@ class MigrateUpgradeForm extends ConfirmFormBase {
'source_module' => 'node',
'destination_module' => 'node',
],
'd6_node_translation' => [
'source_module' => 'node',
'destination_module' => 'node',
],
'd6_node_revision' => [
'source_module' => 'node',
'destination_module' => 'node',

View file

@ -30,7 +30,7 @@ abstract class MigrateUpgradeTestBase extends WebTestBase {
*
* @var array
*/
public static $modules = ['migrate_drupal_ui', 'telephone'];
public static $modules = ['language', 'content_translation', 'migrate_drupal_ui', 'telephone'];
/**
* {@inheritdoc}

View file

@ -40,14 +40,16 @@ class MigrateUpgrade6Test extends MigrateUpgradeTestBase {
'comment' => 3,
'comment_type' => 2,
'contact_form' => 5,
'configurable_language' => 5,
'editor' => 2,
'field_config' => 62,
'field_config' => 63,
'field_storage_config' => 43,
'file' => 7,
'filter_format' => 7,
'image_style' => 5,
'language_content_settings' => 2,
'migration' => 105,
'node' => 9,
'node' => 10,
'node_type' => 11,
'rdf_mapping' => 5,
'search_page' => 2,
@ -57,7 +59,7 @@ class MigrateUpgrade6Test extends MigrateUpgradeTestBase {
'menu' => 8,
'taxonomy_term' => 6,
'taxonomy_vocabulary' => 6,
'tour' => 1,
'tour' => 4,
'user' => 7,
'user_role' => 6,
'menu_link_content' => 4,

View file

@ -39,13 +39,16 @@ class MigrateUpgrade7Test extends MigrateUpgradeTestBase {
'block_content_type' => 1,
'comment' => 1,
'comment_type' => 7,
// Module 'language' comes with 'en', 'und', 'zxx'. Migration adds 'is'.
'configurable_language' => 4,
'contact_form' => 3,
'editor' => 2,
'field_config' => 41,
'field_storage_config' => 31,
'field_config' => 43,
'field_storage_config' => 32,
'file' => 1,
'filter_format' => 7,
'image_style' => 6,
'language_content_settings' => 1,
'migration' => 59,
'node' => 2,
'node_type' => 6,
@ -57,16 +60,16 @@ class MigrateUpgrade7Test extends MigrateUpgradeTestBase {
'menu' => 10,
'taxonomy_term' => 18,
'taxonomy_vocabulary' => 3,
'tour' => 1,
'tour' => 4,
'user' => 3,
'user_role' => 4,
'menu_link_content' => 9,
'view' => 12,
'date_format' => 11,
'entity_form_display' => 15,
'entity_form_display' => 16,
'entity_form_mode' => 1,
'entity_view_display' => 22,
'entity_view_mode' => 10,
'entity_view_display' => 24,
'entity_view_mode' => 11,
'base_field_override' => 7,
];
}

View file

@ -6,7 +6,10 @@ deriver: Drupal\node\Plugin\migrate\D6NodeDeriver
source:
plugin: d6_node
process:
nid: nid
# In D6, nodes always have a tnid, but it's zero for untranslated nodes.
# We normalize it to equal the nid in that case.
# @see \Drupal\node\Plugin\migrate\source\d6\Node::prepareRow().
nid: tnid
vid: vid
langcode:
plugin: default_value

View file

@ -0,0 +1,52 @@
id: d6_node_translation
label: Node translations
migration_tags:
- Drupal 6
deriver: Drupal\node\Plugin\migrate\D6NodeDeriver
source:
plugin: d6_node
translations: true
process:
nid: tnid
type: type
langcode:
plugin: default_value
source: language
default_value: "und"
title: title
uid: node_uid
status: status
created: created
changed: changed
promote: promote
sticky: sticky
'body/format':
plugin: migration
migration: d6_filter_format
source: format
'body/value': body
'body/summary': teaser
revision_uid: revision_uid
revision_log: log
revision_timestamp: timestamp
# unmapped d6 fields.
# translate
# moderate
# comment
destination:
plugin: entity:node
translations: true
migration_dependencies:
required:
- d6_user
- d6_node_type
- d6_node_settings
- d6_filter_format
- language
optional:
- d6_field_instance_widget_settings
- d6_field_formatter_settings
- d6_upload_field_instance
provider: migrate_drupal

View file

@ -8,6 +8,7 @@ use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\node\NodeStorageInterface;
use Drupal\node\NodeTypeInterface;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -125,6 +126,7 @@ class NodeController extends ControllerBase implements ContainerInjectionInterfa
*/
public function revisionShow($node_revision) {
$node = $this->entityManager()->getStorage('node')->loadRevision($node_revision);
$node = $this->entityManager()->getTranslationFromContext($node);
$node_view_controller = new NodeViewController($this->entityManager, $this->renderer);
$page = $node_view_controller->view($node);
unset($page['nodes'][$node->id()]['#cache']);
@ -170,12 +172,9 @@ class NodeController extends ControllerBase implements ContainerInjectionInterfa
$delete_permission = (($account->hasPermission("delete $type revisions") || $account->hasPermission('delete all revisions') || $account->hasPermission('administer nodes')) && $node->access('delete'));
$rows = array();
$vids = $node_storage->revisionIds($node);
$latest_revision = TRUE;
foreach (array_reverse($vids) as $vid) {
foreach ($this->_getRevisionIds($node, $node_storage) as $vid) {
/** @var \Drupal\node\NodeInterface $revision */
$revision = $node_storage->loadRevision($vid);
// Only show revisions that are affected by the language that is being
@ -263,6 +262,8 @@ class NodeController extends ControllerBase implements ContainerInjectionInterfa
),
);
$build['pager'] = array('#type' => 'pager');
return $build;
}
@ -279,4 +280,25 @@ class NodeController extends ControllerBase implements ContainerInjectionInterfa
return $this->t('Create @name', array('@name' => $node_type->label()));
}
/**
* Gets a list of node revision IDs for a specific node.
*
* @param \Drupal\node\NodeInterface
* The node entity.
* @param \Drupal\node\NodeStorageInterface $node_storage
* The node storage handler.
*
* @return int[]
* Node revision IDs (in descending order).
*/
protected function _getRevisionIds(NodeInterface $node, NodeStorageInterface $node_storage) {
$result = $node_storage->getQuery()
->allRevisions()
->condition($node->getEntityType()->getKey('id'), $node->id())
->sort($node->getEntityType()->getKey('revision'), 'DESC')
->pager(50)
->execute();
return array_keys($result);
}
}

View file

@ -146,10 +146,21 @@ class NodeViewBuilder extends EntityViewBuilder {
/** @var \Drupal\node\NodeInterface $entity */
parent::alterBuild($build, $entity, $display, $view_mode);
if ($entity->id()) {
$build['#contextual_links']['node'] = array(
'route_parameters' => array('node' => $entity->id()),
'metadata' => array('changed' => $entity->getChangedTime()),
);
if ($entity->isDefaultRevision()) {
$build['#contextual_links']['node'] = [
'route_parameters' => ['node' => $entity->id()],
'metadata' => ['changed' => $entity->getChangedTime()],
];
}
else {
$build['#contextual_links']['node_revision'] = [
'route_parameters' => [
'node' => $entity->id(),
'node_revision' => $entity->getRevisionId(),
],
'metadata' => ['changed' => $entity->getChangedTime()],
];
}
}
}

View file

@ -37,6 +37,13 @@ class D6NodeDeriver extends DeriverBase implements ContainerDeriverInterface {
*/
protected $cckPluginManager;
/**
* Whether or not to include translations.
*
* @var bool
*/
protected $includeTranslations;
/**
* D6NodeDeriver constructor.
*
@ -44,19 +51,24 @@ class D6NodeDeriver extends DeriverBase implements ContainerDeriverInterface {
* The base plugin ID for the plugin ID.
* @param \Drupal\Component\Plugin\PluginManagerInterface $cck_manager
* The CCK plugin manager.
* @param bool $translations
* Whether or not to include translations.
*/
public function __construct($base_plugin_id, PluginManagerInterface $cck_manager) {
public function __construct($base_plugin_id, PluginManagerInterface $cck_manager, $translations) {
$this->basePluginId = $base_plugin_id;
$this->cckPluginManager = $cck_manager;
$this->includeTranslations = $translations;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
// Translations don't make sense unless we have content_translation.
return new static(
$base_plugin_id,
$container->get('plugin.manager.migrate.cckfield')
$container->get('plugin.manager.migrate.cckfield'),
$container->get('module_handler')->moduleExists('content_translation')
);
}
@ -72,6 +84,11 @@ class D6NodeDeriver extends DeriverBase implements ContainerDeriverInterface {
* @see \Drupal\Component\Plugin\Derivative\DeriverBase::getDerivativeDefinition()
*/
public function getDerivativeDefinitions($base_plugin_definition) {
if ($base_plugin_definition['id'] == 'd6_node_translation' && !$this->includeTranslations) {
// Refuse to generate anything.
return $this->derivatives;
}
// Read all CCK field instance definitions in the source database.
$fields = array();
try {
@ -100,9 +117,10 @@ class D6NodeDeriver extends DeriverBase implements ContainerDeriverInterface {
$values['source']['node_type'] = $node_type;
$values['destination']['default_bundle'] = $node_type;
// If this migration is based on the d6_node_revision migration, it
// should explicitly depend on the corresponding d6_node variant.
if ($base_plugin_definition['id'] == 'd6_node_revision') {
// If this migration is based on the d6_node_revision migration or
// is for translations of nodes, it should explicitly depend on the
// corresponding d6_node variant.
if (in_array($base_plugin_definition['id'], ['d6_node_revision', 'd6_node_translation'])) {
$values['migration_dependencies']['required'][] = 'd6_node:' . $node_type;
}

View file

@ -2,6 +2,7 @@
namespace Drupal\node\Plugin\migrate\source\d6;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
@ -37,9 +38,11 @@ class Node extends DrupalSqlBase {
* {@inheritdoc}
*/
public function query() {
// Select node in its last revision.
$query = $this->select('node_revisions', 'nr')
->fields('n', array(
$query = $this->select('node_revisions', 'nr');
$query->innerJoin('node', 'n', static::JOIN);
$this->handleTranslations($query);
$query->fields('n', array(
'nid',
'type',
'language',
@ -54,17 +57,16 @@ class Node extends DrupalSqlBase {
'translate',
))
->fields('nr', array(
'vid',
'title',
'body',
'teaser',
'log',
'timestamp',
'format',
'vid',
));
$query->addField('n', 'uid', 'node_uid');
$query->addField('nr', 'uid', 'revision_uid');
$query->innerJoin('node', 'n', static::JOIN);
if (isset($this->configuration['node_type'])) {
$query->condition('n.type', $this->configuration['node_type']);
@ -123,6 +125,11 @@ class Node extends DrupalSqlBase {
}
}
// Make sure we always have a translation set.
if ($row->getSourceProperty('tnid') == 0) {
$row->setSourceProperty('tnid', $row->getSourceProperty('nid'));
}
return parent::prepareRow($row);
}
@ -251,4 +258,22 @@ class Node extends DrupalSqlBase {
return $ids;
}
/**
* Adapt our query for translations.
*
* @param \Drupal\Core\Database\Query\SelectInterface
* The generated query.
*/
protected function handleTranslations(SelectInterface $query) {
// Check whether or not we want translations.
if (empty($this->configuration['translations'])) {
// No translations: Yield untranslated nodes, or default translations.
$query->where('n.tnid = 0 OR n.tnid = n.nid');
}
else {
// Translations: Yield only non-default translations.
$query->where('n.tnid <> 0 AND n.tnid <> n.nid');
}
}
}

View file

@ -1,6 +1,7 @@
<?php
namespace Drupal\node\Plugin\migrate\source\d6;
use Drupal\Core\Database\Query\SelectInterface;
/**
* Drupal 6 node revision source from database.
@ -37,4 +38,11 @@ class NodeRevision extends Node {
return $ids;
}
/**
* {@inheritdoc}
*/
protected function handleTranslations(SelectInterface $query) {
// @todo in https://www.drupal.org/node/2746541
}
}

View file

@ -63,7 +63,9 @@ class Path extends FieldPluginBase {
*/
public function render(ResultRow $values) {
$nid = $this->getValue($values, 'nid');
return \Drupal::url('entity.node.canonical', ['node' => $nid], ['absolute' => $this->options['absolute']]);
return array(
'#markup' => \Drupal::url('entity.node.canonical', ['node' => $nid], ['absolute' => $this->options['absolute']]),
);
}
}

View file

@ -10,6 +10,11 @@ use Drupal\Tests\node\Kernel\Migrate\d6\MigrateNodeTestBase;
*/
class MigrateNodeRevisionTest extends MigrateNodeTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['language', 'content_translation'];
/**
* {@inheritdoc}
*/

View file

@ -2,6 +2,8 @@
namespace Drupal\node\Tests;
use Drupal\filter\Entity\FilterFormat;
/**
* Ensures that data added to nodes by other modules appears in RSS feeds.
*
@ -60,4 +62,47 @@ class NodeRSSContentTest extends NodeTestBase {
$this->assertNoText($rss_only_content, 'Node content designed for RSS does not appear when viewing node.');
}
/**
* Tests relative, root-relative, protocol-relative and absolute URLs.
*/
public function testUrlHandling() {
// Only the plain_text text format is available by default, which escapes
// all HTML.
FilterFormat::create([
'format' => 'full_html',
'name' => 'Full HTML',
'filters' => [],
])->save();
$defaults = [
'type' => 'article',
'promote' => 1,
];
$this->drupalCreateNode($defaults + [
'body' => [
'value' => '<p><a href="' . file_url_transform_relative(file_create_url('public://root-relative')) . '">Root-relative URL</a></p>',
'format' => 'full_html',
],
]);
$protocol_relative_url = substr(file_create_url('public://protocol-relative'), strlen(\Drupal::request()->getScheme() . ':'));
$this->drupalCreateNode($defaults + [
'body' => [
'value' => '<p><a href="' . $protocol_relative_url . '">Protocol-relative URL</a></p>',
'format' => 'full_html',
],
]);
$absolute_url = file_create_url('public://absolute');
$this->drupalCreateNode($defaults + [
'body' => [
'value' => '<p><a href="' . $absolute_url . '">Absolute URL</a></p>',
'format' => 'full_html',
],
]);
$this->drupalGet('rss.xml');
$this->assertRaw(file_create_url('public://root-relative'), 'Root-relative URL is transformed to absolute.');
$this->assertRaw($protocol_relative_url, 'Protocol-relative URL is left untouched.');
$this->assertRaw($absolute_url, 'Absolute URL is left untouched.');
}
}

View file

@ -2,6 +2,8 @@
namespace Drupal\node\Tests;
use Drupal\node\NodeInterface;
/**
* Create a node with revisions and test viewing, saving, reverting, and
* deleting revisions for user with access to all.
@ -15,7 +17,7 @@ class NodeRevisionsAllTest extends NodeTestBase {
protected function setUp() {
parent::setUp();
$node_storage = $this->container->get('entity.manager')->getStorage('node');
// Create and log in user.
$web_user = $this->drupalCreateUser(
array(
@ -45,17 +47,7 @@ class NodeRevisionsAllTest extends NodeTestBase {
for ($i = 0; $i < $revision_count; $i++) {
$logs[] = $node->revision_log = $this->randomMachineName(32);
// Create revision with a random title and body and update variables.
$node->title = $this->randomMachineName();
$node->body = array(
'value' => $this->randomMachineName(32),
'format' => filter_default_format(),
);
$node->setNewRevision();
$node->save();
$node_storage->resetCache(array($node->id()));
$node = $node_storage->load($node->id()); // Make sure we get revision information.
$node = $this->createNodeRevision($node);
$nodes[] = clone $node;
}
@ -63,6 +55,28 @@ class NodeRevisionsAllTest extends NodeTestBase {
$this->revisionLogs = $logs;
}
/**
* Creates a new revision for a given node.
*
* @param \Drupal\node\NodeInterface $node
* A node object.
*
* @return \Drupal\node\NodeInterface
* A node object with up to date revision information.
*/
protected function createNodeRevision(NodeInterface $node) {
// Create revision with a random title and body and update variables.
$node->title = $this->randomMachineName();
$node->body = array(
'value' => $this->randomMachineName(32),
'format' => filter_default_format(),
);
$node->setNewRevision();
$node->save();
return $node;
}
/**
* Checks node revision operations.
*/
@ -145,6 +159,29 @@ class NodeRevisionsAllTest extends NodeTestBase {
'%title' => $nodes[2]->getTitle(),
'%revision-date' => format_date($old_revision_date),
)));
// Create 50 more revisions in order to trigger paging on the revisions
// overview screen.
$node = $nodes[0];
for ($i = 0; $i < 50; $i++) {
$logs[] = $node->revision_log = $this->randomMachineName(32);
$node = $this->createNodeRevision($node);
$nodes[] = clone $node;
}
$this->drupalGet('node/' . $node->id() . '/revisions');
// Check that the pager exists.
$this->assertRaw('page=1');
// Check that the last revision is displayed on the first page.
$this->assertText(end($logs));
// Go to the second page and check that one of the initial three revisions
// is displayed.
$this->clickLink(t('Page 2'));
$this->assertText($logs[2]);
}
}

View file

@ -8,6 +8,7 @@ use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\Component\Serialization\Json;
/**
* Create a node with revisions and test viewing, saving, reverting, and
@ -34,7 +35,7 @@ class NodeRevisionsTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('node', 'datetime', 'language', 'content_translation');
public static $modules = ['node', 'contextual', 'datetime', 'language', 'content_translation'];
/**
* {@inheritdoc}
@ -71,6 +72,7 @@ class NodeRevisionsTest extends NodeTestBase {
'delete page revisions',
'edit any page content',
'delete any page content',
'access contextual links',
'translate any entity',
'administer content types',
)
@ -152,6 +154,18 @@ class NodeRevisionsTest extends NodeTestBase {
// Confirm that this is the default revision.
$this->assertTrue($node->isDefaultRevision(), 'Third node revision is the default one.');
// Confirm that the "Edit" and "Delete" contextual links appear for the
// default revision.
$ids = ['node:node=' . $node->id() . ':changed=' . $node->getChangedTime()];
$json = $this->renderContextualLinks($ids, 'node/' . $node->id());
$this->verbose($json[$ids[0]]);
$expected = '<li class="entitynodeedit-form"><a href="' . base_path() . 'node/' . $node->id() . '/edit">Edit</a></li>';
$this->assertTrue(strstr($json[$ids[0]], $expected), 'The "Edit" contextual link is shown for the default revision.');
$expected = '<li class="entitynodedelete-form"><a href="' . base_path() . 'node/' . $node->id() . '/delete">Delete</a></li>';
$this->assertTrue(strstr($json[$ids[0]], $expected), 'The "Delete" contextual link is shown for the default revision.');
// Confirm that revisions revert properly.
$this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionid() . "/revert", array(), t('Revert'));
$this->assertRaw(t('@type %title has been reverted to the revision from %revision-date.',
@ -165,6 +179,16 @@ class NodeRevisionsTest extends NodeTestBase {
$node = node_revision_load($node->getRevisionId());
$this->assertFalse($node->isDefaultRevision(), 'Third node revision is not the default one.');
// Confirm that "Edit" and "Delete" contextual links don't appear for
// non-default revision.
$ids = ['node_revision::node=' . $node->id() . '&node_revision=' . $node->getRevisionId() . ':'];
$json = $this->renderContextualLinks($ids, 'node/' . $node->id() . '/revisions/' . $node->getRevisionId() . '/view');
$this->verbose($json[$ids[0]]);
$this->assertFalse(strstr($json[$ids[0]], '<li class="entitynodeedit-form">'), 'The "Edit" contextual link is not shown for a non-default revision.');
$this->assertFalse(strstr($json[$ids[0]], '<li class="entitynodedelete-form">'), 'The "Delete" contextual link is not shown for a non-default revision.');
// Confirm revisions delete properly.
$this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionId() . "/delete", array(), t('Delete'));
$this->assertRaw(t('Revision from %revision-date of @type %title has been deleted.',
@ -317,6 +341,27 @@ class NodeRevisionsTest extends NodeTestBase {
$this->assertTrue(empty($node_revision->revision_log->value), 'After a new node revision is saved with an empty log message, the log message for the node is empty.');
}
/**
* Gets server-rendered contextual links for the given contextual links IDs.
*
* @param string[] $ids
* An array of contextual link IDs.
* @param string $current_path
* The Drupal path for the page for which the contextual links are rendered.
*
* @return string
* The decoded JSON response body.
*/
protected function renderContextualLinks(array $ids, $current_path) {
$post = array();
for ($i = 0; $i < count($ids); $i++) {
$post['ids[' . $i . ']'] = $ids[$i];
}
$response = $this->drupalPost('contextual/render', 'application/json', $post, ['query' => ['destination' => $current_path]]);
return Json::decode($response);
}
/**
* Tests the revision translations are correctly reverted.
*/

View file

@ -445,4 +445,53 @@ class NodeTranslationUITest extends ContentTranslationUITestBase {
}
}
/**
* Tests that revision translations are rendered properly.
*/
public function testRevisionTranslationRendering() {
$storage = \Drupal::entityTypeManager()->getStorage('node');
// Create a node.
$nid = $this->createEntity(['title' => 'First rev en title'], 'en');
$node = $storage->load($nid);
$original_revision_id = $node->getRevisionId();
// Add a French translation.
$translation = $node->addTranslation('fr');
$translation->title = 'First rev fr title';
$translation->setNewRevision(FALSE);
$translation->save();
// Create a new revision.
$node->title = 'Second rev en title';
$node->setNewRevision(TRUE);
$node->save();
// Get an English view of this revision.
$original_revision = $storage->loadRevision($original_revision_id);
$original_revision_url = $original_revision->toUrl('revision')->toString();
// Should be different from regular node URL.
$this->assertNotIdentical($original_revision_url, $original_revision->toUrl()->toString());
$this->drupalGet($original_revision_url);
$this->assertResponse(200);
// Contents should be in English, of correct revision.
$this->assertText('First rev en title');
$this->assertNoText('First rev fr title');
// Get a French view.
$url_fr = $original_revision->getTranslation('fr')->toUrl('revision')->toString();
// Should have different URL from English.
$this->assertNotIdentical($url_fr, $original_revision->toUrl()->toString());
$this->assertNotIdentical($url_fr, $original_revision_url);
$this->drupalGet($url_fr);
$this->assertResponse(200);
// Contents should be in French, of correct revision.
$this->assertText('First rev fr title');
$this->assertNoText('First rev en title');
}
}

View file

@ -23,7 +23,9 @@ class NodeTypeTranslationTest extends WebTestBase {
* @var array
*/
public static $modules = array(
'block',
'config_translation',
'field_ui',
'node',
);
@ -53,6 +55,8 @@ class NodeTypeTranslationTest extends WebTestBase {
$admin_permissions = array(
'administer content types',
'administer node fields',
'administer languages',
'administer site configuration',
'administer themes',
'translate configuration',
@ -144,6 +148,29 @@ class NodeTypeTranslationTest extends WebTestBase {
$this->assertText('Edited title');
$this->drupalGet("$langcode/node/add/$type");
$this->assertText('Translated title');
// Add an e-mail field.
$this->drupalPostForm("admin/structure/types/manage/$type/fields/add-field", array('new_storage_type' => 'email', 'label' => 'Email', 'field_name' => 'email'), 'Save and continue');
$this->drupalPostForm(NULL, array(), 'Save field settings');
$this->drupalPostForm(NULL, array(), 'Save settings');
$type = Unicode::strtolower($this->randomMachineName(16));
$name = $this->randomString();
$this->drupalCreateContentType(array('type' => $type, 'name' => $name));
// Set tabs.
$this->drupalPlaceBlock('local_tasks_block', array('primary' => TRUE));
// Change default language.
$this->drupalPostForm('admin/config/regional/language', array('site_default_language' => 'es'), 'Save configuration');
// Try re-using the email field.
$this->drupalGet("es/admin/structure/types/manage/$type/fields/add-field");
$this->drupalPostForm(NULL, array('existing_storage_name' => 'field_email', 'existing_storage_label' => 'Email'), 'Save and continue');
$this->assertResponse(200);
$this->drupalGet("es/admin/structure/types/manage/$type/fields/node.$type.field_email/translate");
$this->assertResponse(200);
$this->assertText("The configuration objects have different language codes so they cannot be translated");
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Drupal\node\Tests\Views;
use Drupal\views\Views;
/**
* Tests the node row plugin.
*
* @group node
*/
class PathPluginTest extends NodeTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node');
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_node_path_plugin');
/**
* Contains all nodes used by this test.
*
* @var Node[]
*/
protected $nodes;
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'article'));
// Create two nodes.
for ($i = 0; $i < 2; $i++) {
$this->nodes[] = $this->drupalCreateNode(
array(
'type' => 'article',
'body' => array(
array(
'value' => $this->randomMachineName(42),
'format' => filter_default_format(),
'summary' => $this->randomMachineName(),
),
),
)
);
}
}
/**
* Tests the node path plugin.
*/
public function testPathPlugin() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = $this->container->get('renderer');
$view = Views::getView('test_node_path_plugin');
$view->initDisplay();
$view->setDisplay('page_1');
$view->initStyle();
$view->rowPlugin->options['view_mode'] = 'full';
// Test with view_mode full.
$output = $view->preview();
$output = $renderer->renderRoot($output);
foreach ($this->nodes as $node) {
$this->assertTrue(strpos($output, 'This is <strong>not escaped</strong> and this is ' . $node->link('the link')) !== FALSE, 'Make sure path field rewriting is not escaped.');
}
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Drupal\node\Tests\Views;
/**
* Tests the different revision link handlers.
*
* @group node
*
* @see \Drupal\node\Plugin\views\field\RevisionLink
* @see \Drupal\node\Plugin\views\field\RevisionLinkDelete
* @see \Drupal\node\Plugin\views\field\RevisionLinkRevert
*/
class RevisionLinkTest extends NodeTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_node_revision_links'];
/**
* Tests revision links.
*/
public function testRevisionLinks() {
// Create one user which can view/revert and delete and one which can only
// do one of them.
$this->drupalCreateContentType(['name' => 'page', 'type' => 'page']);
$account = $this->drupalCreateUser(['revert all revisions', 'view all revisions', 'delete all revisions', 'edit any page content', 'delete any page content']);
$this->drupalLogin($account);
// Create two nodes, one without an additional revision and one with a
// revision.
$nodes = [
$this->drupalCreateNode(),
$this->drupalCreateNode(),
];
$first_revision = $nodes[1]->getRevisionId();
// Create revision of the node.
$nodes[1]->setNewRevision();
$nodes[1]->save();
$second_revision = $nodes[1]->getRevisionId();
$this->drupalGet('test-node-revision-links');
$this->assertResponse(200, 'Test view can be accessed in the path expected');
// The first node revision should link to the node directly as you get an
// access denied if you link to the revision.
$url = $nodes[0]->urlInfo()->toString();
$this->assertLinkByHref($url);
$this->assertNoLinkByHref($url . '/revisions/' . $nodes[0]->getRevisionId() . '/view');
$this->assertNoLinkByHref($url . '/revisions/' . $nodes[0]->getRevisionId() . '/delete');
$this->assertNoLinkByHref($url . '/revisions/' . $nodes[0]->getRevisionId() . '/revert');
// For the second node the current revision got set to the last revision, so
// the first one should also link to the node page itself.
$url = $nodes[1]->urlInfo()->toString();
$this->assertLinkByHref($url);
$this->assertLinkByHref($url . '/revisions/' . $first_revision . '/view');
$this->assertLinkByHref($url . '/revisions/' . $first_revision . '/delete');
$this->assertLinkByHref($url . '/revisions/' . $first_revision . '/revert');
$this->assertNoLinkByHref($url . '/revisions/' . $second_revision . '/view');
$this->assertNoLinkByHref($url . '/revisions/' . $second_revision . '/delete');
$this->assertNoLinkByHref($url . '/revisions/' . $second_revision . '/revert');
$accounts = [
'view' => $this->drupalCreateUser(['view all revisions']),
'revert' => $this->drupalCreateUser(['revert all revisions', 'edit any page content']),
'delete' => $this->drupalCreateUser(['delete all revisions', 'delete any page content']),
];
$url = $nodes[1]->urlInfo()->toString();
// Render the view with users which can only delete/revert revisions.
foreach ($accounts as $allowed_operation => $account) {
$this->drupalLogin($account);
$this->drupalGet('test-node-revision-links');
// Check expected links.
foreach (['revert', 'delete'] as $operation) {
if ($operation == $allowed_operation) {
$this->assertLinkByHref($url . '/revisions/' . $first_revision . '/' . $operation);
}
else {
$this->assertNoLinkByHref($url . '/revisions/' . $first_revision . '/' . $operation);
}
}
}
}
}

View file

@ -0,0 +1,182 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_node_path_plugin
label: test_node_path_plugin
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' Previous'
next: 'Next '
first: '« First'
last: 'Last »'
quantity: 9
style:
type: default
row:
type: fields
fields:
path:
id: path
table: node
field: path
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: true
text: 'This is <strong>not escaped</strong> and this is <a href="{{ path }}" hreflang="en">the link</a>.'
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
absolute: false
entity_type: node
plugin_id: node_path
filters:
status:
value: true
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
group: 1
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
title: test_node_path_plugin
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test-node-path-plugin
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }

View file

@ -0,0 +1,224 @@
langcode: en
status: true
dependencies:
module:
- node
id: test_node_revision_links
label: test_node_revision_links
module: views
description: ''
tag: ''
base_table: node_field_revision
base_field: vid
core: '8'
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: none
options: { }
cache:
type: none
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
items_per_page: 0
offset: 0
style:
type: default
row:
type: fields
fields:
link_to_revision:
id: link_to_revision
table: node_field_revision
field: link_to_revision
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
text: 'Link to revision'
entity_type: node
plugin_id: node_revision_link
delete_revision:
id: delete_revision
table: node_field_revision
field: delete_revision
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
text: 'Link to delete revision'
entity_type: node
plugin_id: node_revision_link_delete
revert_revision:
id: revert_revision
table: node_field_revision
field: revert_revision
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
text: 'Link to delete revision'
entity_type: node
plugin_id: node_revision_link_revert
filters: { }
sorts: { }
title: test_node_revision_links
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test-node-revision-links

View file

@ -0,0 +1,50 @@
<?php
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Test D6NodeDeriver.
*
* @group migrate_drupal_6
*/
class MigrateNodeDeriverTest extends MigrateDrupal6TestBase {
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $pluginManager;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->pluginManager = $this->container->get('plugin.manager.migration');
}
/**
* Test node translation migrations with translation disabled.
*/
public function testNoTranslations() {
// Without content_translation, there should be no translation migrations.
$migrations = $this->pluginManager->createInstances('d6_node_translation');
$this->assertSame([], $migrations,
"No node translation migrations without content_translation");
}
/**
* Test node translation migrations with translation enabled.
*/
public function testTranslations() {
// With content_translation, there should be translation migrations for
// each content type.
$this->enableModules(['language', 'content_translation']);
$migrations = $this->pluginManager->createInstances('d6_node_translation');
$this->assertArrayHasKey('d6_node_translation:story', $migrations,
"Node translation migrations exist after content_translation installed");
}
}

View file

@ -16,6 +16,11 @@ class MigrateNodeTest extends MigrateNodeTestBase {
use FileMigrationTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['language', 'content_translation'];
/**
* {@inheritdoc}
*/
@ -23,7 +28,7 @@ class MigrateNodeTest extends MigrateNodeTestBase {
parent::setUp();
$this->setUpMigratedFiles();
$this->installSchema('file', ['file_usage']);
$this->executeMigrations(['d6_node']);
$this->executeMigrations(['language', 'd6_node', 'd6_node_translation']);
}
/**
@ -85,6 +90,15 @@ class MigrateNodeTest extends MigrateNodeTestBase {
$this->assertSame('Buy it now', $node->field_test_link->title);
$this->assertSame(['attributes' => ['target' => '_blank']], $node->field_test_link->options);
// Test that translations are working.
$node = Node::load(10);
$this->assertIdentical('en', $node->langcode->value);
$this->assertIdentical('The Real McCoy', $node->title->value);
$this->assertTrue($node->hasTranslation('fr'), "Node 10 has french translation");
// Node 11 is a translation of node 10, and should not be imported separately.
$this->assertNull(Node::load(11), "Node 11 doesn't exist in D8, it was a translation");
// Rerun migration with two source database changes.
// 1. Add an invalid link attributes and a different URL and
// title. If only the attributes are changed the error does not occur.

View file

@ -44,6 +44,7 @@ class MigrateNodeTest extends MigrateDrupal7TestBase {
'd7_user',
'd7_node_type',
'd7_comment_type',
'd7_taxonomy_vocabulary',
'd7_field',
'd7_field_instance',
'd7_node:test_content_type',
@ -139,9 +140,13 @@ class MigrateNodeTest extends MigrateDrupal7TestBase {
$this->assertIdentical('title text', $node->field_images->title);
$this->assertIdentical('93', $node->field_images->width);
$this->assertIdentical('93', $node->field_images->height);
$this->assertIdentical('http://google.com', $node->field_link->uri);
$this->assertIdentical('Click Here', $node->field_link->title);
$node = Node::load(2);
$this->assertIdentical("...is that it's the absolute best show ever. Trust me, I would know.", $node->body->value);
$this->assertIdentical('internal:/', $node->field_link->uri);
$this->assertIdentical('Home', $node->field_link->title);
}
}

View file

@ -40,7 +40,7 @@ class NodeByNodeTypeTest extends MigrateSqlSourceTestCase {
'promote' => 1,
'moderate' => 0,
'sticky' => 0,
'tnid' => 0,
'tnid' => 1,
'translate' => 0,
// Node revision fields.
'body' => 'body for node 1',
@ -64,7 +64,7 @@ class NodeByNodeTypeTest extends MigrateSqlSourceTestCase {
'promote' => 1,
'moderate' => 0,
'sticky' => 0,
'tnid' => 0,
'tnid' => 2,
'translate' => 0,
// Node revision fields.
'body' => 'body for node 2',

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