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

This commit is contained in:
Pantheon Automation 2017-01-04 16:50:53 -08:00 committed by Greg Anderson
parent 8544b60b39
commit db56c09587
86 changed files with 2413 additions and 488 deletions

View file

@ -123,7 +123,6 @@ Color
- ?
Comment
- Dick Olsson 'dixon_' https://www.drupal.org/u/dixon_
- Lee Rowlands 'larowlan' https://www.drupal.org/u/larowlan
- Andrey Postnikov 'andypost' https://www.drupal.org/u/andypost

View file

@ -74,7 +74,7 @@ function drupal_get_schema_versions($module) {
* module is not installed.
*/
function drupal_get_installed_schema_version($module, $reset = FALSE, $array = FALSE) {
static $versions = array();
$versions = &drupal_static(__FUNCTION__, array());
if ($reset) {
$versions = array();

View file

@ -81,7 +81,7 @@ class Drupal {
/**
* The current system version.
*/
const VERSION = '8.2.4';
const VERSION = '8.2.5';
/**
* Core API compatibility.
@ -556,8 +556,7 @@ class Drupal {
* Renders a link with a given link text and Url object.
*
* This method is a convenience wrapper for the link generator service's
* generate() method. For detailed documentation, see
* \Drupal\Core\Routing\LinkGeneratorInterface::generate().
* generate() method.
*
* @param string $text
* The link text for the anchor tag.

View file

@ -11,6 +11,7 @@ use Drupal\Core\Render\Element;
* Properties:
* - #default_value: An array with the keys: 'year', 'month', and 'day'.
* Defaults to the current date if no value is supplied.
* - #size: The size of the input element in characters.
*
* @code
* $form['expiration'] = array(

View file

@ -10,6 +10,7 @@ use Drupal\Core\Render\Element;
*
* Properties:
* - #default_value: An RFC-compliant email address.
* - #size: The size of the input element in characters.
*
* Example usage:
* @code

View file

@ -16,6 +16,7 @@ use Drupal\Component\Utility\Number as NumberUtility;
* - #step: Ensures that the number is an even multiple of step, offset by #min
* if specified. A #min of 1 and a #step of 2 would allow values of 1, 3, 5,
* etc.
* - #size: The size of the input element in characters.
*
* Usage example:
* @code

View file

@ -8,6 +8,9 @@ use Drupal\Core\Render\Element;
/**
* Provides a form element for entering a password, with hidden text.
*
* Properties:
* - #size: The size of the input element in characters.
*
* Usage example:
* @code
* $form['pass'] = array(

View file

@ -10,6 +10,9 @@ use Drupal\Core\Form\FormStateInterface;
* Formats as a pair of password fields, which do not validate unless the two
* entered passwords match.
*
* Properties:
* - #size: The size of the input element in characters.
*
* Usage example:
* @code
* $form['pass'] = array(

View file

@ -14,8 +14,31 @@ use Drupal\Core\Render\Element;
* list. If a value is an array, it will be rendered similarly, but as an
* optgroup. The key of the sub-array will be used as the label for the
* optgroup. Nesting optgroups is not allowed.
* - #empty_option: The label that will be displayed to denote no selection.
* - #empty_value: The value of the option that is used to denote no selection.
* - #empty_option: (optional) The label to show for the first default option.
* By default, the label is automatically set to "- Select -" for a required
* field and "- None -" for an optional field.
* - #empty_value: (optional) The value for the first default option, which is
* used to determine whether the user submitted a value or not.
* - If #required is TRUE, this defaults to '' (an empty string).
* - If #required is not TRUE and this value isn't set, then no extra option
* is added to the select control, leaving the control in a slightly
* illogical state, because there's no way for the user to select nothing,
* since all user agents automatically preselect the first available
* option. But people are used to this being the behavior of select
* controls.
* @todo Address the above issue in Drupal 8.
* - If #required is not TRUE and this value is set (most commonly to an
* empty string), then an extra option (see #empty_option above)
* representing a "non-selection" is added with this as its value.
* - #multiple: (optional) Indicates whether one or more options can be
* selected. Defaults to FALSE.
* - #default_value: Must be NULL or not set in case there is no value for the
* element yet, in which case a first default option is inserted by default.
* Whether this first option is a valid option depends on whether the field
* is #required or not.
* - #required: (optional) Whether the user needs to select an option (TRUE)
* or not (FALSE). Defaults to FALSE.
* - #size: The size of the input element in characters.
*
* Usage example:
* @code
@ -66,31 +89,7 @@ class Select extends FormElement {
* select lists.
*
* @param array $element
* The form element to process. Properties used:
* - #multiple: (optional) Indicates whether one or more options can be
* selected. Defaults to FALSE.
* - #default_value: Must be NULL or not set in case there is no value for the
* element yet, in which case a first default option is inserted by default.
* Whether this first option is a valid option depends on whether the field
* is #required or not.
* - #required: (optional) Whether the user needs to select an option (TRUE)
* or not (FALSE). Defaults to FALSE.
* - #empty_option: (optional) The label to show for the first default option.
* By default, the label is automatically set to "- Select -" for a required
* field and "- None -" for an optional field.
* - #empty_value: (optional) The value for the first default option, which is
* used to determine whether the user submitted a value or not.
* - If #required is TRUE, this defaults to '' (an empty string).
* - If #required is not TRUE and this value isn't set, then no extra option
* is added to the select control, leaving the control in a slightly
* illogical state, because there's no way for the user to select nothing,
* since all user agents automatically preselect the first available
* option. But people are used to this being the behavior of select
* controls.
* @todo Address the above issue in Drupal 8.
* - If #required is not TRUE and this value is set (most commonly to an
* empty string), then an extra option (see #empty_option above)
* representing a "non-selection" is added with this as its value.
* The form element to process.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form

View file

@ -24,6 +24,7 @@ use Drupal\Component\Utility\Html as HtmlUtility;
* providing responsive tables. Defaults to TRUE.
* - #sticky: Indicates whether to add the drupal.tableheader library that makes
* table headers always visible at the top of the page. Defaults to FALSE.
* - #size: The size of the input element in characters.
*
* Usage example:
* @code

View file

@ -10,6 +10,9 @@ use Drupal\Core\Render\Element;
* Provides an HTML5 input element with type of "tel". It provides no special
* validation.
*
* Properties:
* - #size: The size of the input element in characters.
*
* Usage example:
* @code
* $form['phone'] = array(

View file

@ -11,6 +11,7 @@ use Drupal\Core\Render\Element;
*
* Properties:
* - #default_value: A valid URL string.
* - #size: The size of the input element in characters.
*
* Usage example:
* @code

View file

@ -254,13 +254,13 @@
* form array, which specifies the form elements for an HTML form; see the
* @link form_api Form generation topic @endlink for more information on forms.
*
* Render arrays (at each level in the hierarchy) will usually have one of the
* following three properties defined:
* Render arrays (at any level of the hierarchy) will usually have one of the
* following properties defined:
* - #type: Specifies that the array contains data and options for a particular
* type of "render element" (examples: 'form', for an HTML form; 'textfield',
* 'submit', and other HTML form element types; 'table', for a table with
* rows, columns, and headers). See @ref elements below for more on render
* element types.
* type of "render element" (for example, 'form', for an HTML form;
* 'textfield', 'submit', for HTML form element types; 'table', for a table
* with rows, columns, and headers). See @ref elements below for more on
* render element types.
* - #theme: Specifies that the array contains data to be themed by a particular
* theme hook. Modules define theme hooks by implementing hook_theme(), which
* specifies the input "variables" used to provide data and options; if a
@ -277,30 +277,29 @@
* can customize the markup. Note that the value is passed through
* \Drupal\Component\Utility\Xss::filterAdmin(), which strips known XSS
* vectors while allowing a permissive list of HTML tags that are not XSS
* vectors. (I.e, <script> and <style> are not allowed.) See
* \Drupal\Component\Utility\Xss::$adminTags for the list of tags that will
* be allowed. If your markup needs any of the tags that are not in this
* whitelist, then you can implement a theme hook and template file and/or
* an asset library. Aternatively, you can use the render array key
* #allowed_tags to alter which tags are filtered.
* vectors. (For example, <script> and <style> are not allowed.) See
* \Drupal\Component\Utility\Xss::$adminTags for the list of allowed tags. If
* your markup needs any of the tags not in this whitelist, then you can
* implement a theme hook and/or an asset library. Alternatively, you can use
* the key #allowed_tags to alter which tags are filtered.
* - #plain_text: Specifies that the array provides text that needs to be
* escaped. This value takes precedence over #markup if present.
* - #allowed_tags: If #markup is supplied this can be used to change which tags
* are using to filter the markup. The value should be an array of tags that
* Xss::filter() would accept. If #plain_text is set this value is ignored.
* escaped. This value takes precedence over #markup.
* - #allowed_tags: If #markup is supplied, this can be used to change which
* tags are allowed in the markup. The value is an array of tags that
* Xss::filter() would accept. If #plain_text is set, this value is ignored.
*
* Usage example:
* @code
* $output['admin_filtered_string'] = array(
* $output['admin_filtered_string'] = [
* '#markup' => '<em>This is filtered using the admin tag list</em>',
* );
* $output['filtered_string'] = array(
* '#markup' => '<em>This is filtered</em>',
* '#allowed_tags' => ['strong'],
* );
* $output['escaped_string'] = array(
* ];
* $output['filtered_string'] = [
* '#markup' => '<video><source src="v.webm" type="video/webm"></video>',
* '#allowed_tags' => ['video', 'source'],
* ];
* $output['escaped_string'] = [
* '#plain_text' => '<em>This is escaped</em>',
* );
* ];
* @endcode
*
* @see core.libraries.yml

View file

@ -138,6 +138,12 @@ interface BigPipeInterface {
* The HTML response content to send.
* @param array $attachments
* The HTML response's attachments.
*
* @internal
* This method should only be invoked by
* \Drupal\big_pipe\Render\BigPipeResponse, which is itself an internal
* class. Furthermore, the signature of this method will change in
* https://www.drupal.org/node/2657684.
*/
public function sendContent($content, array $attachments);

View file

@ -13,7 +13,10 @@ use Drupal\Core\Render\HtmlResponse;
*
* @see \Drupal\big_pipe\Render\BigPipeInterface
*
* @todo Will become obsolete with https://www.drupal.org/node/2577631
* @internal
* This is a temporary solution until a generic response emitter interface is
* created in https://www.drupal.org/node/2577631. Only code internal to
* BigPipe should instantiate or type hint to this class.
*/
class BigPipeResponse extends HtmlResponse {

View file

@ -273,6 +273,20 @@
}
});
// Redirect on hash change when the original hash has an associated CKEditor.
function redirectTextareaFragmentToCKEditorInstance() {
var hash = location.hash.substr(1);
var element = document.getElementById(hash);
if (element) {
var editor = CKEDITOR.dom.element.get(element).getEditor();
if (editor) {
var id = editor.container.getAttribute('id');
location.replace('#' + id);
}
}
}
$(window).on('hashchange.ckeditor', redirectTextareaFragmentToCKEditorInstance);
// Set the CKEditor cache-busting string to the same value as Drupal.
CKEDITOR.timestamp = drupalSettings.ckeditor.timestamp;

View file

@ -0,0 +1,121 @@
<?php
namespace Drupal\Tests\ckeditor\FunctionalJavascript;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\editor\Entity\Editor;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\filter\Entity\FilterFormat;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
use Drupal\node\Entity\NodeType;
/**
* Tests the integration of CKEditor.
*
* @group ckeditor
*/
class CKEditorIntegrationTest extends JavascriptTestBase {
/**
* The account.
*
* @var \Drupal\user\UserInterface
*/
protected $account;
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'ckeditor', 'filter'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create a text format and associate CKEditor.
$filtered_html_format = FilterFormat::create([
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
]);
$filtered_html_format->save();
Editor::create([
'format' => 'filtered_html',
'editor' => 'ckeditor',
])->save();
// Create a node type for testing.
NodeType::create(['type' => 'page', 'name' => 'page'])->save();
$field_storage = FieldStorageConfig::loadByName('node', 'body');
// Create a body field instance for the 'page' node type.
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'page',
'label' => 'Body',
'settings' => ['display_summary' => TRUE],
'required' => TRUE,
])->save();
// Assign widget settings for the 'default' form mode.
EntityFormDisplay::create([
'targetEntityType' => 'node',
'bundle' => 'page',
'mode' => 'default',
'status' => TRUE,
])->setComponent('body', ['type' => 'text_textarea_with_summary'])
->save();
$this->account = $this->drupalCreateUser([
'administer nodes',
'create page content',
'use text format filtered_html',
]);
$this->drupalLogin($this->account);
}
/**
* Tests if the fragment link to a textarea works with CKEditor enabled.
*/
public function testFragmentLink() {
$session = $this->getSession();
$web_assert = $this->assertSession();
$ckeditor_id = '#cke_edit-body-0-value';
$this->drupalGet('node/add/page');
$session->getPage();
// Add a bottom margin to the title field to be sure the body field is not
// visible. PhantomJS runs with a resolution of 1024x768px.
$session->executeScript("document.getElementById('edit-title-0-value').style.marginBottom = '800px';");
// Check that the CKEditor-enabled body field is currently not visible in
// the viewport.
$web_assert->assertNotVisibleInViewport('css', $ckeditor_id, 'topLeft', 'CKEditor-enabled body field is visible.');
$before_url = $session->getCurrentUrl();
// Trigger a hash change with as target the hidden textarea.
$session->executeScript("location.hash = '#edit-body-0-value';");
// Check that the CKEditor-enabled body field is visible in the viewport.
$web_assert->assertVisibleInViewport('css', $ckeditor_id, 'topLeft', 'CKEditor-enabled body field is not visible.');
// Use JavaScript to go back in the history instead of
// \Behat\Mink\Session::back() because that function doesn't work after a
// hash change.
$session->executeScript("history.back();");
$after_url = $session->getCurrentUrl();
// Check that going back in the history worked.
self::assertEquals($before_url, $after_url, 'History back works.');
}
}

View file

@ -36,3 +36,4 @@ migration_dependencies:
optional:
- d7_node_type
- d7_comment_type
- d7_taxonomy_vocabulary

View file

@ -96,13 +96,9 @@ function file_requirements($phase) {
$value = t('Not enabled');
$description = t('Your server is not capable of displaying file upload progress. File upload progress requires PHP be run with mod_php or PHP-FPM and not as FastCGI.');
}
elseif (!$implementation && extension_loaded('apcu')) {
$value = t('Not enabled');
$description = t('Your server is capable of displaying file upload progress through APC, but it is not enabled. Add <code>apc.rfc1867 = 1</code> to your php.ini configuration. Alternatively, it is recommended to use <a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress</a>, which supports more than one simultaneous upload.');
}
elseif (!$implementation) {
$value = t('Not enabled');
$description = t('Your server is capable of displaying file upload progress, but does not have the required libraries. It is recommended to install the <a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress library</a> (preferred) or to install <a href="http://php.net/apcu">APC</a>.');
$description = t('Your server is capable of displaying file upload progress, but does not have the required libraries. It is recommended to install the <a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress library</a>.');
}
elseif ($implementation == 'apc') {
$value = t('Enabled (<a href="http://php.net/manual/apcu.configuration.php#ini.apcu.rfc1867">APC RFC1867</a>)');

View file

@ -929,7 +929,7 @@ function file_progress_implementation() {
if (extension_loaded('uploadprogress')) {
$implementation = 'uploadprogress';
}
elseif (extension_loaded('apc') && ini_get('apc.rfc1867')) {
elseif (version_compare(PHP_VERSION, '7', '<') && extension_loaded('apc') && ini_get('apc.rfc1867')) {
$implementation = 'apc';
}
}

View file

@ -291,6 +291,21 @@ class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
return $new_values;
}
/**
* {@inheritdoc}
*/
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
parent::extractFormValues($items, $form, $form_state);
// Update reference to 'items' stored during upload to take into account
// changes to values like 'alt' etc.
// @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::submit()
$field_name = $this->fieldDefinition->getName();
$field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
$field_state['items'] = $items->getValue();
static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
}
/**
* Form API callback. Retrieves the value for the file_generic field element.
*

View file

@ -0,0 +1,13 @@
id: d7_filter_settings
label: Drupal 7 filter settings
migration_tags:
- Drupal 7
source:
plugin: variable
variables:
- filter_fallback_format
process:
fallback_format: filter_fallback_format
destination:
plugin: config
config_name: filter.settings

View file

@ -0,0 +1,32 @@
<?php
namespace Drupal\Tests\filter\Kernel\Migrate\d7;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests migration of Filter's settings to configuration.
*
* @group filter
*/
class MigrateFilterSettingsTest extends MigrateDrupal7TestBase {
public static $modules = ['filter'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installConfig(static::$modules);
$this->executeMigration('d7_filter_settings');
}
/**
* Tests migration of Filter variables to configuration.
*/
public function testFilterSettings() {
$this->assertSame('plain_text', $this->config('filter.settings')->get('fallback_format'));
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace Drupal\Tests\inline_form_errors\FunctionalJavascript;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\editor\Entity\Editor;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\filter\Entity\FilterFormat;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
use Drupal\node\Entity\NodeType;
/**
* Tests the inline errors fragment link to a CKEditor-enabled textarea.
*
* @group ckeditor
*/
class FormErrorHandlerCKEditorTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'ckeditor', 'inline_form_errors', 'filter'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create a text format and associate CKEditor.
$filtered_html_format = FilterFormat::create([
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
]);
$filtered_html_format->save();
Editor::create([
'format' => 'filtered_html',
'editor' => 'ckeditor',
])->save();
// Create a node type for testing.
NodeType::create(['type' => 'page', 'name' => 'page'])->save();
$field_storage = FieldStorageConfig::loadByName('node', 'body');
// Create a body field instance for the 'page' node type.
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'page',
'label' => 'Body',
'settings' => ['display_summary' => TRUE],
'required' => TRUE,
])->save();
// Assign widget settings for the 'default' form mode.
EntityFormDisplay::create([
'targetEntityType' => 'node',
'bundle' => 'page',
'mode' => 'default',
'status' => TRUE,
])->setComponent('body', ['type' => 'text_textarea_with_summary'])
->save();
$account = $this->drupalCreateUser([
'administer nodes',
'create page content',
'use text format filtered_html',
]);
$this->drupalLogin($account);
}
/**
* Tests if the fragment link to a textarea works with CKEditor enabled.
*/
public function testFragmentLink() {
$session = $this->getSession();
$web_assert = $this->assertSession();
$ckeditor_id = '#cke_edit-body-0-value';
$this->drupalGet('node/add/page');
$page = $this->getSession()->getPage();
// Only enter a title in the node add form and leave the body field empty.
$edit = ['edit-title-0-value' => 'Test inline form error with CKEditor'];
$this->submitForm($edit, 'Save and publish');
// Add a bottom margin to the title field to be sure the body field is not
// visible. PhantomJS runs with a resolution of 1024x768px.
$session->executeScript("document.getElementById('edit-title-0-value').style.marginBottom = '800px';");
// Check that the CKEditor-enabled body field is currently not visible in
// the viewport.
$web_assert->assertNotVisibleInViewport('css', $ckeditor_id, 'topLeft', 'CKEditor-enabled body field is not visible.');
// Check if we can find the error fragment link within the errors summary
// message.
$errors_link = $page->find('css', '.messages--error a[href=\#edit-body-0-value]');
$this->assertTrue($errors_link->isVisible(), 'Error fragment link is visible.');
$errors_link->click();
// Check that the CKEditor-enabled body field is visible in the viewport.
$web_assert->assertVisibleInViewport('css', $ckeditor_id, 'topLeft', 'CKEditor-enabled body field is visible.');
}
}

View file

@ -20,7 +20,7 @@ process:
plugin: skip_on_empty
method: row
'link/uri':
plugin: d7_internal_uri
plugin: link_uri
source:
- link_path
'link/options': options

View file

@ -63,6 +63,9 @@ class LinkUri extends ProcessPluginBase implements ContainerFactoryPluginInterfa
$path = ltrim($path, '/');
if (parse_url($path, PHP_URL_SCHEME) === NULL) {
if ($path == '<front>') {
$path = '';
}
$path = 'internal:/' . $path;
// Convert entity URIs to the entity scheme, if the path matches a route

View file

@ -2,42 +2,12 @@
namespace Drupal\menu_link_content\Plugin\migrate\process\d7;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Drupal\menu_link_content\Plugin\migrate\process\LinkUri;
/**
* Process a path into an 'internal:' URI.
* Processes an internal uri into an 'internal:' or 'entity:' URI.
*
* @MigrateProcessPlugin(
* id = "d7_internal_uri"
* )
* @deprecated in Drupal 8.2.0, will be removed before Drupal 9.0.0. Use
* \Drupal\menu_link_content\Plugin\migrate\process\LinkUri instead.
*/
class InternalUri extends ProcessPluginBase {
/**
* {@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) {
// If $path is the node page (i.e. node/[nid]) then return entity path.
if (preg_match('/^node\/\d+$/', $path)) {
// "entity: URI"s enable the menu link to appear in the Menu Settings
// section on the node edit page. Other entities (e.g. taxonomy terms,
// users) do not have the Menu Settings section.
return 'entity:' . $path;
}
elseif ($path == '<front>') {
return 'internal:/';
}
else {
return 'internal:/' . $path;
}
}
return $path;
}
}
class InternalUri extends LinkUri {}

View file

@ -5,6 +5,7 @@ namespace Drupal\Tests\menu_link_content\Kernel\Migrate\d7;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\menu_link_content\MenuLinkContentInterface;
use Drupal\node\Entity\Node;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
@ -18,7 +19,7 @@ class MigrateMenuLinkTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('link', 'menu_ui', 'menu_link_content');
public static $modules = array('link', 'menu_ui', 'menu_link_content', 'node');
/**
* {@inheritdoc}
@ -26,6 +27,13 @@ class MigrateMenuLinkTest extends MigrateDrupal7TestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('menu_link_content');
$this->installEntitySchema('node');
$node = Node::create([
'nid' => 3,
'title' => 'node link test',
'type' => 'article',
]);
$node->save();
$this->executeMigrations(['d7_menu', 'd7_menu_links']);
\Drupal::service('router.builder')->rebuild();
}

View file

@ -117,6 +117,10 @@ class LinkUriTest extends UnitTestCase {
$expected = 'internal:/test';
$tests['without_scheme'] = [$value, $expected];
$value = ['<front>'];
$expected = 'internal:/';
$tests['front'] = [$value, $expected];
$url = Url::fromRoute('route_name');
$tests['with_route'] = [$value, $expected, $url];

View file

@ -1,73 +0,0 @@
<?php
namespace Drupal\Tests\menu_link_content\Unit\Plugin\migrate\process\d7;
use Drupal\menu_link_content\Plugin\migrate\process\d7\InternalUri;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\Tests\UnitTestCase;
/**
* Tests \Drupal\menu_link_content\Plugin\migrate\process\d7\InternalUri.
*
* @group menu_link_content
*
* @coversDefaultClass \Drupal\menu_link_content\Plugin\migrate\process\d7\InternalUri
*/
class InternalUriTest extends UnitTestCase {
/**
* The 'd7_internal_uri' process plugin being tested.
*
* @var \Drupal\menu_link_content\Plugin\migrate\process\d7\InternalUri
*/
protected $processPlugin;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->processPlugin = new InternalUri([], 'd7_internal_uri', []);
}
/**
* Tests InternalUri::transform().
*
* @param array $value
* The value to pass to InternalUri::transform().
* @param string $expected
* The expected return value of InternalUri::transform().
*
* @dataProvider providerTestTransform
*
* @covers ::transform
*/
public function testTransform(array $value, $expected) {
$migrate_executable = $this->prophesize(MigrateExecutableInterface::class);
$row = $this->prophesize(Row::class);
$actual = $this->processPlugin->transform($value, $migrate_executable->reveal(), $row->reveal(), 'link/uri');
$this->assertEquals($expected, $actual);
}
/**
* Provides test cases for InternalUriTest::testTransform().
*
* @return array
* An array of test cases, each which the following values:
* - The value array to pass to InternalUri::transform().
* - The expected path returned by InternalUri::transform().
*/
public function providerTestTransform() {
$tests = [];
$tests['with_scheme'] = [['http://example.com'], 'http://example.com'];
$tests['leading_slash'] = [['/test'], 'internal:/test'];
$tests['without_scheme'] = [['test'], 'internal:/test'];
$tests['front'] = [['<front>'], 'internal:/'];
$tests['node'] = [['node/27'], 'entity:node/27'];
return $tests;
}
}

View file

@ -108,8 +108,13 @@ class Download extends ProcessPluginBase implements ContainerFactoryPluginInterf
// Stream the request body directly to the final destination stream.
$this->configuration['guzzle_options']['sink'] = $destination_stream;
// Make the request. Guzzle throws an exception for anything other than 200.
$this->httpClient->get($source, $this->configuration['guzzle_options']);
try {
// Make the request. Guzzle throws an exception for anything but 200.
$this->httpClient->get($source, $this->configuration['guzzle_options']);
}
catch (\Exception $e) {
throw new MigrateException("{$e->getMessage()} ($source)");
}
return $final_destination;
}

View file

@ -112,20 +112,17 @@ class FileCopy extends ProcessPluginBase implements ContainerFactoryPluginInterf
return $destination;
}
$replace = $this->getOverwriteMode();
// We attempt the copy/move first to avoid calling file_prepare_directory()
// any more than absolutely necessary.
$final_destination = $this->writeFile($source, $destination, $replace);
if ($final_destination) {
return $final_destination;
}
// If writeFile didn't work, make sure there's a writable directory in
// place.
// Check if a writable directory exists, and if not try to create it.
$dir = $this->getDirectory($destination);
if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
throw new MigrateException("Could not create or write to directory '$dir'");
// If the directory exists and is writable, avoid file_prepare_directory()
// call and write the file to destination.
if (!is_dir($dir) || !is_writable($dir)) {
if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
throw new MigrateException("Could not create or write to directory '$dir'");
}
}
$final_destination = $this->writeFile($source, $destination, $replace);
$final_destination = $this->writeFile($source, $destination, $this->getOverwriteMode());
if ($final_destination) {
return $final_destination;
}

View file

@ -22,15 +22,17 @@ class Iterator extends ProcessPluginBase {
* Runs a process pipeline on each destination property per list item.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$return = array();
foreach ($value as $key => $new_value) {
$new_row = new Row($new_value, array());
$migrate_executable->processRow($new_row, $this->configuration['process']);
$destination = $new_row->getDestination();
if (array_key_exists('key', $this->configuration)) {
$key = $this->transformKey($key, $migrate_executable, $new_row);
$return = [];
if (!is_null($value)) {
foreach ($value as $key => $new_value) {
$new_row = new Row($new_value, []);
$migrate_executable->processRow($new_row, $this->configuration['process']);
$destination = $new_row->getDestination();
if (array_key_exists('key', $this->configuration)) {
$key = $this->transformKey($key, $migrate_executable, $new_row);
}
$return[$key] = $destination;
}
$return[$key] = $destination;
}
return $return;
}

View file

@ -310,13 +310,13 @@ abstract class SourcePluginBase extends PluginBase implements MigrateSourceInter
while (!isset($this->currentRow) && $this->getIterator()->valid()) {
$row_data = $this->getIterator()->current() + $this->configuration;
$this->getIterator()->next();
$this->fetchNextRow();
$row = new Row($row_data, $this->migration->getSourcePlugin()->getIds(), $this->migration->getDestinationIds());
// Populate the source key for this row.
$this->currentSourceIds = $row->getSourceIdValues();
// Pick up the existing map row, if any, unless getNextRow() did it.
// Pick up the existing map row, if any, unless fetchNextRow() did it.
if (!$this->mapRowAdded && ($id_map = $this->idMap->getRowBySource($this->currentSourceIds))) {
$row->setIdMap($id_map);
}
@ -348,7 +348,14 @@ abstract class SourcePluginBase extends PluginBase implements MigrateSourceInter
}
/**
* Checks if the incoming data is newer than what we've previously imported.
* Position the iterator to the following row.
*/
protected function fetchNextRow() {
$this->getIterator()->next();
}
/**
* Check if the incoming data is newer than what we've previously imported.
*
* @param \Drupal\migrate\Row $row
* The row we're importing.

View file

@ -5,6 +5,7 @@ namespace Drupal\migrate\Plugin\migrate\source;
use Drupal\Core\Database\Database;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\id_map\Sql;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
@ -42,6 +43,22 @@ abstract class SqlBase extends SourcePluginBase implements ContainerFactoryPlugi
*/
protected $state;
/**
* The count of the number of batches run.
*
* @var int
*/
protected $batch = 0;
/**
* Number of records to fetch from the database during each batch.
*
* A value of zero indicates no batching is to be done.
*
* @var int
*/
protected $batchSize = 0;
/**
* {@inheritdoc}
*/
@ -160,70 +177,110 @@ abstract class SqlBase extends SourcePluginBase implements ContainerFactoryPlugi
* we will take advantage of the PDO-based API to optimize the query up-front.
*/
protected function initializeIterator() {
$this->prepareQuery();
// Initialize the batch size.
if ($this->batchSize == 0 && isset($this->configuration['batch_size'])) {
// Valid batch sizes are integers >= 0.
if (is_int($this->configuration['batch_size']) && ($this->configuration['batch_size']) >= 0) {
$this->batchSize = $this->configuration['batch_size'];
}
else {
throw new MigrateException("batch_size must be greater than or equal to zero");
}
}
// Get the key values, for potential use in joining to the map table.
$keys = array();
// If a batch has run the query is already setup.
if ($this->batch == 0) {
$this->prepareQuery();
// The rules for determining what conditions to add to the query are as
// follows (applying first applicable rule):
// 1. If the map is joinable, join it. We will want to accept all rows
// which are either not in the map, or marked in the map as NEEDS_UPDATE.
// Note that if high water fields are in play, we want to accept all rows
// above the high water mark in addition to those selected by the map
// conditions, so we need to OR them together (but AND with any existing
// conditions in the query). So, ultimately the SQL condition will look
// like (original conditions) AND (map IS NULL OR map needs update
// OR above high water).
$conditions = $this->query->orConditionGroup();
$condition_added = FALSE;
if (empty($this->configuration['ignore_map']) && $this->mapJoinable()) {
// Build the join to the map table. Because the source key could have
// multiple fields, we need to build things up.
$count = 1;
$map_join = '';
$delimiter = '';
foreach ($this->getIds() as $field_name => $field_schema) {
if (isset($field_schema['alias'])) {
$field_name = $field_schema['alias'] . '.' . $this->query->escapeField($field_name);
// Get the key values, for potential use in joining to the map table.
$keys = array();
// The rules for determining what conditions to add to the query are as
// follows (applying first applicable rule):
// 1. If the map is joinable, join it. We will want to accept all rows
// which are either not in the map, or marked in the map as NEEDS_UPDATE.
// Note that if high water fields are in play, we want to accept all rows
// above the high water mark in addition to those selected by the map
// conditions, so we need to OR them together (but AND with any existing
// conditions in the query). So, ultimately the SQL condition will look
// like (original conditions) AND (map IS NULL OR map needs update
// OR above high water).
$conditions = $this->query->orConditionGroup();
$condition_added = FALSE;
if (empty($this->configuration['ignore_map']) && $this->mapJoinable()) {
// Build the join to the map table. Because the source key could have
// multiple fields, we need to build things up.
$count = 1;
$map_join = '';
$delimiter = '';
foreach ($this->getIds() as $field_name => $field_schema) {
if (isset($field_schema['alias'])) {
$field_name = $field_schema['alias'] . '.' . $this->query->escapeField($field_name);
}
$map_join .= "$delimiter$field_name = map.sourceid" . $count++;
$delimiter = ' AND ';
}
$map_join .= "$delimiter$field_name = map.sourceid" . $count++;
$delimiter = ' AND ';
}
$alias = $this->query->leftJoin($this->migration->getIdMap()->getQualifiedMapTableName(), 'map', $map_join);
$conditions->isNull($alias . '.sourceid1');
$conditions->condition($alias . '.source_row_status', MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
$condition_added = TRUE;
$alias = $this->query->leftJoin($this->migration->getIdMap()
->getQualifiedMapTableName(), 'map', $map_join);
$conditions->isNull($alias . '.sourceid1');
$conditions->condition($alias . '.source_row_status', MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
$condition_added = TRUE;
// And as long as we have the map table, add its data to the row.
$n = count($this->getIds());
for ($count = 1; $count <= $n; $count++) {
$map_key = 'sourceid' . $count;
$this->query->addField($alias, $map_key, "migrate_map_$map_key");
}
if ($n = count($this->migration->getDestinationIds())) {
// And as long as we have the map table, add its data to the row.
$n = count($this->getIds());
for ($count = 1; $count <= $n; $count++) {
$map_key = 'destid' . $count++;
$map_key = 'sourceid' . $count;
$this->query->addField($alias, $map_key, "migrate_map_$map_key");
}
if ($n = count($this->migration->getDestinationIds())) {
for ($count = 1; $count <= $n; $count++) {
$map_key = 'destid' . $count++;
$this->query->addField($alias, $map_key, "migrate_map_$map_key");
}
}
$this->query->addField($alias, 'source_row_status', 'migrate_map_source_row_status');
}
// 2. If we are using high water marks, also include rows above the mark.
// But, include all rows if the high water mark is not set.
if ($this->getHighWaterProperty() && ($high_water = $this->getHighWater()) !== '') {
$high_water_field = $this->getHighWaterField();
$conditions->condition($high_water_field, $high_water, '>');
$this->query->orderBy($high_water_field);
}
if ($condition_added) {
$this->query->condition($conditions);
}
$this->query->addField($alias, 'source_row_status', 'migrate_map_source_row_status');
}
// 2. If we are using high water marks, also include rows above the mark.
// But, include all rows if the high water mark is not set.
if ($this->getHighWaterProperty() && ($high_water = $this->getHighWater()) !== '') {
$high_water_field = $this->getHighWaterField();
$conditions->condition($high_water_field, $high_water, '>');
$this->query->orderBy($high_water_field);
}
if ($condition_added) {
$this->query->condition($conditions);
}
// Download data in batches for performance.
if (($this->batchSize > 0)) {
$this->query->range($this->batch * $this->batchSize, $this->batchSize);
}
return new \IteratorIterator($this->query->execute());
}
/**
* Position the iterator to the following row.
*/
protected function fetchNextRow() {
$this->getIterator()->next();
// We might be out of data entirely, or just out of data in the current
// batch. Attempt to fetch the next batch and see.
if ($this->batchSize > 0 && !$this->getIterator()->valid()) {
$this->fetchNextBatch();
}
}
/**
* Prepares query for the next set of data from the source database.
*/
protected function fetchNextBatch() {
$this->batch++;
unset($this->iterator);
$this->getIterator()->rewind();
}
/**
* @return \Drupal\Core\Database\Query\SelectInterface
*/
@ -249,6 +306,14 @@ abstract class SqlBase extends SourcePluginBase implements ContainerFactoryPlugi
if (!$this->getIds()) {
return FALSE;
}
// With batching, we want a later batch to return the same rows that would
// have been returned at the same point within a monolithic query. If we
// join to the map table, the first batch is writing to the map table and
// thus affecting the results of subsequent batches. To be safe, we avoid
// joining to the map table when batching.
if ($this->batchSize > 0) {
return FALSE;
}
$id_map = $this->migration->getIdMap();
if (!$id_map instanceof Sql) {
return FALSE;

View file

@ -0,0 +1,7 @@
type: module
name: Migrate query batch Source test
description: 'Provides a database table and records for SQL import with batch testing.'
package: Testing
core: 8.x
dependencies:
- migrate

View file

@ -0,0 +1,45 @@
<?php
namespace Drupal\migrate_query_batch_test\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
/**
* Source plugin for migration high water tests.
*
* @MigrateSource(
* id = "query_batch_test"
* )
*/
class QueryBatchTest extends SqlBase {
/**
* {@inheritdoc}
*/
public function query() {
return ($this->select('query_batch_test', 'q')->fields('q'));
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'id' => $this->t('Id'),
'data' => $this->t('data'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'id' => [
'type' => 'integer',
],
];
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace Drupal\Tests\migrate\Functional\process;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\MigrateMessage;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the 'download' process plugin.
*
* @group migrate
*/
class DownloadFunctionalTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['migrate', 'file'];
/**
* Tests that an exception is thrown bu migration continues with the next row.
*/
public function testExceptionThrow() {
$invalid_url = "{$this->baseUrl}/not-existent-404";
$valid_url = "{$this->baseUrl}/core/misc/favicon.ico";
$definition = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['url' => $invalid_url, 'uri' => 'public://first.txt'],
['url' => $valid_url, 'uri' => 'public://second.ico'],
],
'ids' => [
'url' => ['type' => 'string'],
],
],
'process' => [
'uri' => [
'plugin' => 'download',
'source' => ['url', 'uri'],
]
],
'destination' => [
'plugin' => 'entity:file',
],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration, new MigrateMessage());
$result = $executable->import();
// Check that the migration has completed.
$this->assertEquals($result, MigrationInterface::RESULT_COMPLETED);
/** @var \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map_plugin */
$id_map_plugin = $migration->getIdMap();
// Check that the first row was marked as failed in the id map table.
$map_row = $id_map_plugin->getRowBySource(['url' => $invalid_url]);
$this->assertEquals(MigrateIdMapInterface::STATUS_FAILED, $map_row['source_row_status']);
$this->assertNull($map_row['destid1']);
// Check that a message with the thrown exception has been logged.
$messages = $id_map_plugin->getMessageIterator(['url' => $invalid_url])->fetchAll();
$this->assertCount(1, $messages);
$message = reset($messages);
$this->assertEquals("Cannot read from non-readable stream ($invalid_url)", $message->message);
$this->assertEquals(MigrationInterface::MESSAGE_ERROR, $message->level);
// Check that the second row was migrated successfully.
$map_row = $id_map_plugin->getRowBySource(['url' => $valid_url]);
$this->assertEquals(MigrateIdMapInterface::STATUS_IMPORTED, $map_row['source_row_status']);
$this->assertEquals(1, $map_row['destid1']);
}
}

View file

@ -0,0 +1,261 @@
<?php
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Core\Database\Driver\sqlite\Connection;
/**
* Tests query batching.
*
* @covers \Drupal\migrate_query_batch_test\Plugin\migrate\source\QueryBatchTest
* @group migrate
*/
class QueryBatchTest extends KernelTestBase {
/**
* The mocked migration.
*
* @var MigrationInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $migration;
/**
* {@inheritdoc}
*/
public static $modules = [
'migrate',
'migrate_query_batch_test',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create a mock migration. This will be injected into the source plugin
// under test.
$this->migration = $this->prophesize(MigrationInterface::class);
$this->migration->id()->willReturn(
$this->randomMachineName(16)
);
// Prophesize a useless ID map plugin and an empty set of destination IDs.
// Calling code can override these prophecies later and set up different
// behaviors.
$this->migration->getIdMap()->willReturn(
$this->prophesize(MigrateIdMapInterface::class)->reveal()
);
$this->migration->getDestinationIds()->willReturn([]);
}
/**
* Tests a negative batch size throws an exception.
*/
public function testBatchSizeNegative() {
$this->setExpectedException(MigrateException::class, 'batch_size must be greater than or equal to zero');
$plugin = $this->getPlugin(['batch_size' => -1]);
$plugin->next();
}
/**
* Tests a non integer batch size throws an exception.
*/
public function testBatchSizeNonInteger() {
$this->setExpectedException(MigrateException::class, 'batch_size must be greater than or equal to zero');
$plugin = $this->getPlugin(['batch_size' => '1']);
$plugin->next();
}
/**
* {@inheritdoc}
*/
public function queryDataProvider() {
// Define the parameters for building the data array. The first element is
// the number of source data rows, the second is the batch size to set on
// the plugin configuration.
$test_parameters = [
// Test when batch size is 0.
[200, 0],
// Test when rows mod batch size is 0.
[200, 20],
// Test when rows mod batch size is > 0.
[200, 30],
// Test when batch size = row count.
[200, 200],
// Test when batch size > row count.
[200, 300],
];
// Build the data provider array. The provider array consists of the source
// data rows, the expected result data, the expected count, the plugin
// configuration, the expected batch size and the expected batch count.
$table = 'query_batch_test';
$tests = [];
$data_set = 0;
foreach ($test_parameters as $data) {
list($num_rows, $batch_size) = $data;
for ($i = 0; $i < $num_rows; $i++) {
$tests[$data_set]['source_data'][$table][] = [
'id' => $i,
'data' => $this->randomString(),
];
}
$tests[$data_set]['expected_data'] = $tests[$data_set]['source_data'][$table];
$tests[$data_set][2] = $num_rows;
// Plugin configuration array.
$tests[$data_set][3] = ['batch_size' => $batch_size];
// Expected batch size.
$tests[$data_set][4] = $batch_size;
// Expected batch count is 0 unless a batch size is set.
$expected_batch_count = 0;
if ($batch_size > 0) {
$expected_batch_count = (int) ($num_rows / $batch_size);
if ($num_rows % $batch_size) {
// If there is a remainder an extra batch is needed to get the
// remaining rows.
$expected_batch_count++;
}
}
$tests[$data_set][5] = $expected_batch_count;
$data_set++;
}
return $tests;
}
/**
* Tests query batch size.
*
* @param array $source_data
* The source data, keyed by table name. Each table is an array containing
* the rows in that table.
* @param array $expected_data
* The result rows the plugin is expected to return.
* @param int $num_rows
* How many rows the source plugin is expected to return.
* @param array $configuration
* Configuration for the source plugin specifying the batch size.
* @param int $expected_batch_size
* The expected batch size, will be set to zero for invalid batch sizes.
* @param int $expected_batch_count
* The total number of batches.
*
* @dataProvider queryDataProvider
*/
public function testQueryBatch($source_data, $expected_data, $num_rows, $configuration, $expected_batch_size, $expected_batch_count) {
$plugin = $this->getPlugin($configuration);
// Since we don't yet inject the database connection, we need to use a
// reflection hack to set it in the plugin instance.
$reflector = new \ReflectionObject($plugin);
$property = $reflector->getProperty('database');
$property->setAccessible(TRUE);
$connection = $this->getDatabase($source_data);
$property->setValue($plugin, $connection);
// Test the results.
$i = 0;
/** @var \Drupal\migrate\Row $row */
foreach ($plugin as $row) {
$expected = $expected_data[$i++];
$actual = $row->getSource();
foreach ($expected as $key => $value) {
$this->assertArrayHasKey($key, $actual);
$this->assertSame((string) $value, (string) $actual[$key]);
}
}
// Test that all rows were retrieved.
self::assertSame($num_rows, $i);
// Test the batch size.
if (is_null($expected_batch_size)) {
$expected_batch_size = $configuration['batch_size'];
}
$property = $reflector->getProperty('batchSize');
$property->setAccessible(TRUE);
self::assertSame($expected_batch_size, $property->getValue($plugin));
// Test the batch count.
if (is_null($expected_batch_count)) {
$expected_batch_count = intdiv($num_rows, $expected_batch_size);
if ($num_rows % $configuration['batch_size']) {
$expected_batch_count++;
}
}
$property = $reflector->getProperty('batch');
$property->setAccessible(TRUE);
self::assertSame($expected_batch_count, $property->getValue($plugin));
}
/**
* Instantiates the source plugin under test.
*
* @param array $configuration
* The source plugin configuration.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface|object
* The fully configured source plugin.
*/
protected function getPlugin($configuration) {
/** @var \Drupal\migrate\Plugin\MigratePluginManager $plugin_manager */
$plugin_manager = $this->container->get('plugin.manager.migrate.source');
$plugin = $plugin_manager->createInstance('query_batch_test', $configuration, $this->migration->reveal());
$this->migration
->getSourcePlugin()
->willReturn($plugin);
return $plugin;
}
/**
* Builds an in-memory SQLite database from a set of source data.
*
* @param array $source_data
* The source data, keyed by table name. Each table is an array containing
* the rows in that table.
*
* @return \Drupal\Core\Database\Driver\sqlite\Connection
* The SQLite database connection.
*/
protected function getDatabase(array $source_data) {
// Create an in-memory SQLite database. Plugins can interact with it like
// any other database, and it will cease to exist when the connection is
// closed.
$connection_options = ['database' => ':memory:'];
$pdo = Connection::open($connection_options);
$connection = new Connection($pdo, $connection_options);
// Create the tables and fill them with data.
foreach ($source_data as $table => $rows) {
// Use the biggest row to build the table schema.
$counts = array_map('count', $rows);
asort($counts);
end($counts);
$pilot = $rows[key($counts)];
$connection->schema()
->createTable($table, [
// SQLite uses loose affinity typing, so it's OK for every field to
// be a text field.
'fields' => array_map(function () {
return ['type' => 'text'];
}, $pilot),
]);
$fields = array_keys($pilot);
$insert = $connection->insert($table)->fields($fields);
array_walk($rows, [$insert, 'values']);
$insert->execute();
}
return $connection;
}
}

View file

@ -4,6 +4,7 @@ namespace Drupal\Tests\migrate\Kernel\process;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\KernelTests\Core\File\FileTestBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\process\FileCopy;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Plugin\MigrateProcessInterface;
@ -12,6 +13,8 @@ use Drupal\migrate\Row;
/**
* Tests the file_copy process plugin.
*
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\process\FileCopy
*
* @group migrate
*/
class FileCopyTest extends FileTestBase {
@ -120,6 +123,32 @@ class FileCopyTest extends FileTestBase {
$this->doTransform($source, 'public://wontmatter.jpg');
}
/**
* Tests that non-writable destination throw an exception.
*
* @covers ::transform
*/
public function testNonWritableDestination() {
$source = $this->createUri('file.txt', NULL, 'temporary');
// Create the parent location.
$this->createDirectory('public://dir');
// Copy the file under public://dir/subdir1/.
$this->doTransform($source, 'public://dir/subdir1/file.txt');
// Check that 'subdir1' was created and the file was successfully migrated.
$this->assertFileExists('public://dir/subdir1/file.txt');
// Remove all permissions from public://dir to trigger a failure when
// trying to create a subdirectory 'subdir2' inside public://dir.
$this->fileSystem->chmod('public://dir', 0);
// Check that the proper exception is raised.
$this->setExpectedException(MigrateException::class, "Could not create or write to directory 'public://dir/subdir2'");
$this->doTransform($source, 'public://dir/subdir2/file.txt');
}
/**
* Test the 'rename' overwrite mode.
*/

View file

@ -3828,6 +3828,15 @@ $connection->insert('field_config_instance')
'data' => 'a:7:{s:5:"label";s:21:"Term Entity Reference";s:6:"widget";a:5:{s:6:"weight";s:2:"18";s:4:"type";s:33:"entityreference_autocomplete_tags";s:6:"module";s:15:"entityreference";s:6:"active";i:1;s:8:"settings";a:3:{s:14:"match_operator";s:8:"CONTAINS";s:4:"size";s:2:"60";s:4:"path";s:0:"";}}s:8:"settings";a:2:{s:9:"behaviors";a:1:{s:14:"taxonomy-index";a:1:{s:6:"status";b: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:21:"entityreference_label";s:8:"settings";a:1:{s:4:"link";b:0;}s:6:"module";s:15:"entityreference";s:6:"weight";i:17;}}s:8:"required";i:0;s:11:"description";s:0:"";s:13:"default_value";N;}',
'deleted' => '0',
))
->values(array(
'id' => '41',
'field_id' => '20',
'field_name' => 'field_term_reference',
'entity_type' => 'taxonomy_term',
'bundle' => 'test_vocabulary',
'data' => 'a:7:{s:5:"label";s:14:"Term Reference";s:6:"widget";a:5:{s:6:"weight";s:2:"14";s:4:"type";s:21:"taxonomy_autocomplete";s:6:"module";s:8:"taxonomy";s:6:"active";i:0;s:8:"settings";a:2:{s:4:"size";i:60;s:17:"autocomplete_path";s:21:"taxonomy/autocomplete";}}s:8:"settings";a:1:{s:18:"user_register_form";b:0;}s:7:"display";a:1:{s:7:"default";a:4:{s:5:"label";s:5:"above";s:4:"type";s:6:"hidden";s:6:"weight";s:2:"13";s:8:"settings";a: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(
@ -4850,6 +4859,16 @@ $connection->insert('field_data_field_integer')
'delta' => '0',
'field_integer_value' => '99',
))
->values(array(
'entity_type' => 'taxonomy_term',
'bundle' => 'test_vocabulary',
'deleted' => '0',
'entity_id' => '4',
'revision_id' => '4',
'language' => 'und',
'delta' => '0',
'field_integer_value' => '6',
))
->execute();
$connection->schema()->createTable('field_data_field_integer_list', array(
@ -5649,6 +5668,16 @@ $connection->insert('field_data_field_term_reference')
'delta' => '0',
'field_term_reference_tid' => '4',
))
->values(array(
'entity_type' => 'taxonomy_term',
'bundle' => 'test_vocabulary',
'deleted' => '0',
'entity_id' => '2',
'revision_id' => '2',
'language' => 'und',
'delta' => '0',
'field_term_reference_tid' => '3',
))
->execute();
$connection->schema()->createTable('field_data_field_text', array(
@ -7023,6 +7052,16 @@ $connection->insert('field_revision_field_integer')
'delta' => '0',
'field_integer_value' => '99',
))
->values(array(
'entity_type' => 'taxonomy_term',
'bundle' => 'test_vocabulary',
'deleted' => '0',
'entity_id' => '4',
'revision_id' => '4',
'language' => 'und',
'delta' => '0',
'field_integer_value' => '6',
))
->execute();
$connection->schema()->createTable('field_revision_field_integer_list', array(
@ -7830,6 +7869,16 @@ $connection->insert('field_revision_field_term_reference')
'delta' => '0',
'field_term_reference_tid' => '4',
))
->values(array(
'entity_type' => 'taxonomy_term',
'bundle' => 'test_vocabulary',
'deleted' => '0',
'entity_id' => '2',
'revision_id' => '2',
'language' => 'und',
'delta' => '0',
'field_term_reference_tid' => '3',
))
->execute();
$connection->schema()->createTable('field_revision_field_text', array(

View file

@ -250,6 +250,10 @@ class MigrateUpgradeForm extends ConfirmFormBase {
'source_module' => 'filter',
'destination_module' => 'filter',
],
'd7_filter_settings' => [
'source_module' => 'filter',
'destination_module' => 'filter',
],
'd6_forum_settings' => [
'source_module' => 'forum',
'destination_module' => 'forum',

View file

@ -43,8 +43,8 @@ class MigrateUpgrade7Test extends MigrateUpgradeTestBase {
'configurable_language' => 4,
'contact_form' => 3,
'editor' => 2,
'field_config' => 48,
'field_storage_config' => 36,
'field_config' => 49,
'field_storage_config' => 37,
'file' => 1,
'filter_format' => 7,
'image_style' => 6,

View file

@ -72,7 +72,7 @@ class NodePreviewForm extends FormBase {
public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $node = NULL) {
$view_mode = $node->preview_view_mode;
$query_options = $node->isNew() ? array('query' => array('uuid' => $node->uuid())) : array();
$query_options = array('query' => array('uuid' => $node->uuid()));
$form['backlink'] = array(
'#type' => 'link',
'#title' => $this->t('Back to content editing'),

View file

@ -67,31 +67,28 @@ class NodeForm extends ContentEntityForm {
public function form(array $form, FormStateInterface $form_state) {
// Try to restore from temp store, this must be done before calling
// parent::form().
$uuid = $this->entity->uuid();
$store = $this->tempStoreFactory->get('node_preview');
// If the user is creating a new node, the UUID is passed in the request.
if ($request_uuid = \Drupal::request()->query->get('uuid')) {
$uuid = $request_uuid;
}
if ($preview = $store->get($uuid)) {
// Attempt to load from preview when the uuid is present unless we are
// rebuilding the form.
$request_uuid = \Drupal::request()->query->get('uuid');
if (!$form_state->isRebuilding() && $request_uuid && $preview = $store->get($request_uuid)) {
/** @var $preview \Drupal\Core\Form\FormStateInterface */
foreach ($preview->getValues() as $name => $value) {
$form_state->setValue($name, $value);
}
$form_state->setStorage($preview->getStorage());
$form_state->setUserInput($preview->getUserInput());
// Rebuild the form.
$form_state->setRebuild();
// The combination of having user input and rebuilding the form means
// that it will attempt to cache the form state which will fail if it is
// a GET request.
$form_state->setRequestMethod('POST');
$this->entity = $preview->getFormObject()->getEntity();
$this->entity->in_preview = NULL;
// Remove the stale temp store entry for existing nodes.
if (!$this->entity->isNew()) {
$store->delete($uuid);
}
$this->hasBeenPreviewed = TRUE;
}

View file

@ -28,7 +28,7 @@ class PagePreviewTest extends NodeTestBase {
*
* @var array
*/
public static $modules = array('node', 'taxonomy', 'comment', 'image', 'file');
public static $modules = array('node', 'taxonomy', 'comment', 'image', 'file', 'text', 'node_test', 'menu_ui');
/**
* The name of the created field.
@ -41,7 +41,7 @@ class PagePreviewTest extends NodeTestBase {
parent::setUp();
$this->addDefaultCommentField('node', 'page');
$web_user = $this->drupalCreateUser(array('edit own page content', 'create page content'));
$web_user = $this->drupalCreateUser(array('edit own page content', 'create page content', 'administer menu'));
$this->drupalLogin($web_user);
// Add a vocabulary so we can test different view modes.
@ -124,6 +124,34 @@ class PagePreviewTest extends NodeTestBase {
entity_get_display('node', 'page', 'default')
->setComponent('field_image')
->save();
// Create a multi-value text field.
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_test_multi',
'entity_type' => 'node',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'type' => 'text',
'settings' => [
'max_length' => 50,
]
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'page',
])->save();
entity_get_form_display('node', 'page', 'default')
->setComponent('field_test_multi', array(
'type' => 'text_textfield',
))
->save();
entity_get_display('node', 'page', 'default')
->setComponent('field_test_multi', array(
'type' => 'string',
))
->save();
}
/**
@ -176,8 +204,11 @@ class PagePreviewTest extends NodeTestBase {
$this->clickLink(t('Back to content editing'));
$this->assertFieldByName($title_key, $edit[$title_key], 'Title field displayed.');
$this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.');
$this->assertFieldByName($term_key, $edit[$term_key] . ' (' . $this->term->id() . ')', 'Term field displayed.');
$this->assertFieldByName($term_key, $edit[$term_key], 'Term field displayed.');
$this->assertFieldByName('field_image[0][alt]', 'Picture of llamas');
$this->drupalPostAjaxForm(NULL, array(), array('field_test_multi_add_more' => t('Add another item')), NULL, array(), array(), 'node-page-form');
$this->assertFieldByName('field_test_multi[0][value]');
$this->assertFieldByName('field_test_multi[1][value]');
// Return to page preview to check everything is as expected.
$this->drupalPostForm(NULL, array(), t('Preview'));
@ -191,7 +222,7 @@ class PagePreviewTest extends NodeTestBase {
$this->drupalGet('node/add/page', array('query' => array('uuid' => $uuid)));
$this->assertFieldByName($title_key, $edit[$title_key], 'Title field displayed.');
$this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.');
$this->assertFieldByName($term_key, $edit[$term_key] . ' (' . $this->term->id() . ')', 'Term field displayed.');
$this->assertFieldByName($term_key, $edit[$term_key], 'Term field displayed.');
// Save the node - this is a new POST, so we need to upload the image.
$this->drupalPostForm('node/add/page', $edit, t('Upload'));
@ -260,6 +291,80 @@ class PagePreviewTest extends NodeTestBase {
$this->drupalPostForm('node/add/page', array($title_key => 'Preview'), t('Preview'));
$this->clickLink(t('Back to content editing'));
$this->assertRaw('edit-submit');
// Assert multiple items can be added and are not lost when previewing.
$test_image_1 = current($this->drupalGetTestFiles('image', 39325));
$edit_image_1['files[field_image_0][]'] = drupal_realpath($test_image_1->uri);
$test_image_2 = current($this->drupalGetTestFiles('image', 39325));
$edit_image_2['files[field_image_1][]'] = drupal_realpath($test_image_2->uri);
$edit['field_image[0][alt]'] = 'Alt 1';
$this->drupalPostForm('node/add/page', $edit_image_1, t('Upload'));
$this->drupalPostForm(NULL, $edit, t('Preview'));
$this->clickLink(t('Back to content editing'));
$this->assertFieldByName('files[field_image_1][]');
$this->drupalPostForm(NULL, $edit_image_2, t('Upload'));
$this->assertNoFieldByName('files[field_image_1][]');
$title = 'node_test_title';
$example_text_1 = 'example_text_preview_1';
$example_text_2 = 'example_text_preview_2';
$example_text_3 = 'example_text_preview_3';
$this->drupalGet('node/add/page');
$edit = [
'title[0][value]' => $title,
'field_test_multi[0][value]' => $example_text_1,
];
$this->assertRaw('Storage is not set');
$this->drupalPostForm(NULL, $edit, t('Preview'));
$this->clickLink(t('Back to content editing'));
$this->assertRaw('Storage is set');
$this->assertFieldByName('field_test_multi[0][value]');
$this->drupalPostForm(NULL, [], t('Save'));
$this->assertText('Basic page ' . $title . ' has been created.');
$node = $this->drupalGetNodeByTitle($title);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->drupalPostAjaxForm(NULL, [], array('field_test_multi_add_more' => t('Add another item')));
$this->drupalPostAjaxForm(NULL, [], array('field_test_multi_add_more' => t('Add another item')));
$edit = [
'field_test_multi[1][value]' => $example_text_2,
'field_test_multi[2][value]' => $example_text_3,
];
$this->drupalPostForm(NULL, $edit, t('Preview'));
$this->clickLink(t('Back to content editing'));
$this->drupalPostForm(NULL, $edit, t('Preview'));
$this->clickLink(t('Back to content editing'));
$this->assertFieldByName('field_test_multi[0][value]', $example_text_1);
$this->assertFieldByName('field_test_multi[1][value]', $example_text_2);
$this->assertFieldByName('field_test_multi[2][value]', $example_text_3);
// Now save the node and make sure all values got saved.
$this->drupalPostForm(NULL, [], t('Save'));
$this->assertText($example_text_1);
$this->assertText($example_text_2);
$this->assertText($example_text_3);
// Edit again, change the menu_ui settings and click on preview.
$this->drupalGet('node/' . $node->id() . '/edit');
$edit = [
'menu[enabled]' => TRUE,
'menu[title]' => 'Changed title',
];
$this->drupalPostForm(NULL, $edit, t('Preview'));
$this->clickLink(t('Back to content editing'));
$this->assertFieldChecked('edit-menu-enabled', 'Menu option is still checked');
$this->assertFieldByName('menu[title]', 'Changed title', 'Menu link title is correct after preview');
// Save, change the title while saving and make sure that it is correctly
// saved.
$edit = [
'menu[enabled]' => TRUE,
'menu[title]' => 'Second title change',
];
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertFieldByName('menu[title]', 'Second title change', 'Menu link title is correct after saving');
}
/**

View file

@ -10,9 +10,11 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
/**
* Implements hook_ENTITY_TYPE_view() for node entities.
*/
@ -175,3 +177,16 @@ function node_test_node_insert(NodeInterface $node) {
$node->save();
}
}
/**
* Implements hook_form_alter().
*/
function node_test_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if (!$form_state->get('node_test_form_alter')) {
drupal_set_message('Storage is not set');
$form_state->set('node_test_form_alter', TRUE);
}
else {
drupal_set_message('Storage is set');
}
}

View file

@ -49,93 +49,104 @@ class OutsideInBlockFormTest extends OutsideInJavascriptTestBase {
/**
* Tests opening Offcanvas tray by click blocks and elements in the blocks.
*
* @dataProvider providerTestBlocks
*/
public function testBlocks() {
// @todo: re-enable once https://www.drupal.org/node/2830485 is resolved.
$this->markTestSkipped('Test skipped due to random failures in DrupalCI, see https://www.drupal.org/node/2830485');
public function testBlocks($block_id, $new_page_text, $element_selector, $label_selector, $button_text, $toolbar_item) {
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
$block_selector = '#' . $block_id;
$this->drupalGet('user');
if (isset($toolbar_item)) {
// Check that you can open a toolbar tray and it will be closed after
// entering edit mode.
if ($element = $page->find('css', "#toolbar-administration a.is-active")) {
// If a tray was open from page load close it.
$element->click();
$this->waitForNoElement("#toolbar-administration a.is-active");
}
$page->find('css', $toolbar_item)->click();
$this->waitForElement("{$toolbar_item}.is-active");
}
$this->toggleEditingMode();
if (isset($toolbar_item)) {
$this->waitForNoElement("{$toolbar_item}.is-active");
}
$this->openBlockForm($block_selector);
switch ($block_id) {
case 'block-powered':
// Fill out form, save the form.
$page->fillField('settings[label]', $new_page_text);
$page->checkField('settings[label_display]');
break;
case 'block-branding':
// Fill out form, save the form.
$page->fillField('settings[site_information][site_name]', $new_page_text);
break;
}
if (isset($new_page_text)) {
$page->pressButton($button_text);
// Make sure the changes are present.
// @todo Use a wait method that will take into account the form submitting
// and all JavaScript activity. https://www.drupal.org/node/2837676
// The use \Behat\Mink\WebAssert::pageTextContains to check text.
$this->assertJsCondition('jQuery("' . $block_selector . ' ' . $label_selector . '").html() == "' . $new_page_text . '"');
}
$this->openBlockForm($block_selector);
$this->toggleEditingMode();
// Canvas should close when editing module is closed.
$this->waitForOffCanvasToClose();
// Go into Edit mode again.
$this->toggleEditingMode();
$element_selector = "$block_selector {$element_selector}";
// Open block form by clicking a element inside the block.
// This confirms that default action for links and form elements is
// suppressed.
$this->openBlockForm($element_selector);
// Exit edit mode.
$this->toggleEditingMode();
}
/**
* Dataprovider for testBlocks().
*/
public function providerTestBlocks() {
$blocks = [
[
'block-powered' => [
'id' => 'block-powered',
'new_page_text' => 'Can you imagine anyone showing the label on this block?',
'element_selector' => '.content a',
'label_selector' => 'h2',
'button_text' => 'Save Powered by Drupal',
'toolbar_item' => '#toolbar-item-user',
],
[
'block-branding' => [
'id' => 'block-branding',
'new_page_text' => 'The site that will live a very short life.',
'element_selector' => 'a[rel="home"]:nth-child(2)',
'label_selector' => '.site-branding__name a',
'button_text' => 'Save Site branding',
'toolbar_item' => '#toolbar-item-administration',
],
[
'block-search' => [
'id' => 'block-search',
'new_page_text' => NULL,
'element_selector' => '#edit-submit',
'label_selector' => 'h2',
'button_text' => 'Save Search form',
'toolbar_item' => NULL,
],
];
$page = $this->getSession()->getPage();
foreach ($blocks as $block) {
$block_selector = '#' . $block['id'];
$this->drupalGet('user');
if (isset($block['toolbar_item'])) {
// Check that you can open a toolbar tray and it will be closed after
// entering edit mode.
if ($element = $page->find('css', "#toolbar-administration a.is-active")) {
// If a tray was open from page load close it.
$element->click();
$this->waitForNoElement("#toolbar-administration a.is-active");
}
$page->find('css', $block['toolbar_item'])->click();
$this->waitForElement("{$block['toolbar_item']}.is-active");
}
$this->toggleEditingMode();
if (isset($block['toolbar_item'])) {
$this->waitForNoElement("{$block['toolbar_item']}.is-active");
}
$this->openBlockForm($block_selector);
switch ($block['id']) {
case 'block-powered':
// Fill out form, save the form.
$page->fillField('settings[label]', $block['new_page_text']);
$page->checkField('settings[label_display]');
break;
case 'block-branding':
// Fill out form, save the form.
$page->fillField('settings[site_information][site_name]', $block['new_page_text']);
break;
}
if (isset($block['new_page_text'])) {
$page->pressButton($block['button_text']);
// Make sure the changes are present.
$this->assertSession()->assertWaitOnAjaxRequest();
$web_assert->pageTextContains($block['new_page_text']);
}
$this->openBlockForm($block_selector);
$this->toggleEditingMode();
// Canvas should close when editing module is closed.
$this->waitForOffCanvasToClose();
// Go into Edit mode again.
$this->toggleEditingMode();
$element_selector = "$block_selector {$block['element_selector']}";
// Open block form by clicking a element inside the block.
// This confirms that default action for links and form elements is
// suppressed.
$this->openBlockForm($element_selector);
// Exit edit mode.
$this->toggleEditingMode();
}
return $blocks;
}
/**

View file

@ -9,6 +9,19 @@ use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
*/
abstract class OutsideInJavascriptTestBase extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
protected function drupalGet($path, array $options = array(), array $headers = array()) {
$return = parent::drupalGet($path, $options, $headers);
// After the page loaded we need to additionally wait until the settings
// tray Ajax activity is done.
$this->assertSession()->assertWaitOnAjaxRequest();
return $return;
}
/**
* Enables a theme.
*

View file

@ -17,16 +17,6 @@ $connection->insert('key_value')
'name' => 'rest',
'value' => 'i:8000;',
])
->fields([
'collection' => 'system.schema',
'name' => 'serialization',
'value' => 'i:8000;',
])
->fields([
'collection' => 'system.schema',
'name' => 'basic_auth',
'value' => 'i:8000;',
])
->execute();
// Update core.extension.
@ -37,9 +27,9 @@ $extensions = $connection->select('config')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['basic_auth'] = 8000;
$extensions['module']['rest'] = 8000;
$extensions['module']['serialization'] = 8000;
$extensions['module']['basic_auth'] = 0;
$extensions['module']['rest'] = 0;
$extensions['module']['serialization'] = 0;
$connection->update('config')
->fields([
'data' => serialize($extensions),
@ -58,7 +48,7 @@ $config = [
],
],
],
'link_domain' => '~',
'link_domain' => NULL,
];
$data = $connection->insert('config')
->fields([

View file

@ -17,11 +17,6 @@ $connection->insert('key_value')
'name' => 'rest',
'value' => 'i:8000;',
])
->fields([
'collection' => 'system.schema',
'name' => 'serialization',
'value' => 'i:8000;',
])
->execute();
// Update core.extension.
@ -32,8 +27,8 @@ $extensions = $connection->select('config')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['rest'] = 8000;
$extensions['module']['serialization'] = 8000;
$extensions['module']['rest'] = 0;
$extensions['module']['serialization'] = 0;
$connection->update('config')
->fields([
'data' => serialize($extensions),
@ -52,7 +47,7 @@ $config = [
],
],
],
'link_domain' => '~',
'link_domain' => NULL,
];
$data = $connection->insert('config')
->fields([

View file

@ -18,16 +18,6 @@ $connection->insert('key_value')
'name' => 'rest',
'value' => 'i:8000;',
])
->fields([
'collection' => 'system.schema',
'name' => 'serialization',
'value' => 'i:8000;',
])
->fields([
'collection' => 'system.schema',
'name' => 'basic_auth',
'value' => 'i:8000;',
])
->execute();
// Update core.extension.

View file

@ -383,8 +383,11 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$get_headers = $response->getHeaders();
// Verify that the GET and HEAD responses are the same. The only difference
// is that there's no body.
$ignored_headers = ['Date', 'Content-Length', 'X-Drupal-Cache', 'X-Drupal-Dynamic-Cache'];
// is that there's no body. For this reason the 'Transfer-Encoding' header
// is also added to the list of headers to ignore, as this could be added to
// GET requests - depending on web server configuration. This would usually
// be 'Transfer-Encoding: chunked'.
$ignored_headers = ['Date', 'Content-Length', 'X-Drupal-Cache', 'X-Drupal-Dynamic-Cache', 'Transfer-Encoding'];
foreach ($ignored_headers as $ignored_header) {
unset($head_headers[$ignored_header]);
unset($get_headers[$ignored_header]);

View file

@ -411,6 +411,7 @@ class ModulesListForm extends FormBase {
foreach (array_keys($modules['install']) as $module) {
if (!drupal_check_module($module)) {
unset($modules['install'][$module]);
unset($modules['experimental'][$module]);
foreach (array_keys($data[$module]->required_by) as $dependent) {
unset($modules['install'][$dependent]);
unset($modules['dependencies'][$dependent]);

View file

@ -120,6 +120,14 @@ class ExperimentalModuleTest extends WebTestBase {
$this->drupalPostForm(NULL, [], 'Continue');
$this->assertText('2 modules have been enabled: Experimental Test, Experimental Dependency Test');
// Try to enable an experimental module that can not be due to
// hook_requirements().
\Drupal::state()->set('experimental_module_requirements_test_requirements', TRUE);
$edit = [];
$edit["modules[Core (Experimental)][experimental_module_requirements_test][enable]"] = TRUE;
$this->drupalPostForm('admin/modules', $edit, 'Install');
$this->assertUrl('admin/modules', [], 'If the module can not be installed we are not taken to the confirm form.');
$this->assertText('The Experimental Test Requirements module can not be installed.');
}
}

View file

@ -192,6 +192,8 @@ abstract class UpdatePathTestBase extends WebTestBase {
$this->container = \Drupal::getContainer();
$this->replaceUser1();
require_once \Drupal::root() . '/core/includes/update.inc';
}
/**
@ -251,6 +253,28 @@ abstract class UpdatePathTestBase extends WebTestBase {
if ($this->checkFailedUpdates) {
$this->assertNoRaw('<strong>' . t('Failed:') . '</strong>');
// Ensure that there are no pending updates.
foreach (['update', 'post_update'] as $update_type) {
switch ($update_type) {
case 'update':
$all_updates = update_get_update_list();
break;
case 'post_update':
$all_updates = \Drupal::service('update.post_update_registry')->getPendingUpdateInformation();
break;
}
foreach ($all_updates as $module => $updates) {
if (!empty($updates['pending'])) {
foreach (array_keys($updates['pending']) as $update_name) {
$this->fail("The $update_name() update function from the $module module did not run.");
}
}
}
}
// Reset the static cache of drupal_get_installed_schema_version() so that
// more complex update path testing works.
drupal_static_reset('drupal_get_installed_schema_version');
// The config schema can be incorrect while the update functions are being
// executed. But once the update has been completed, it needs to be valid
// again. Assert the schema of all configuration objects now.

View file

@ -0,0 +1,6 @@
name: 'Experimental Requirements Test'
type: module
description: 'Module in the experimental package to test hook_requirements() with an experimental module.'
package: Core (Experimental)
version: VERSION
core: 8.x

View file

@ -0,0 +1,20 @@
<?php
/**
* @file
* Experimental Test Requirements module to test hook_requirements().
*/
/**
* Implements hook_requirements().
*/
function experimental_module_requirements_test_requirements() {
$requirements = [];
if (\Drupal::state()->get('experimental_module_requirements_test_requirements', FALSE)) {
$requirements['experimental_module_requirements_test_requirements'] = [
'severity' => REQUIREMENT_ERROR,
'description' => t('The Experimental Test Requirements module can not be installed.'),
];
}
return $requirements;
}

View file

@ -0,0 +1,18 @@
<?php
/**
* @file
* Experimental Test Requirements module to test hook_requirements().
*/
/**
* Implements hook_help().
*/
function experimental_module_requirements_test_help($route_name) {
switch ($route_name) {
case 'help.page.experimental_module_requirements_test':
// Make the help text conform to core standards. See
// \Drupal\system\Tests\Module\InstallUninstallTest::assertHelp().
return t('The Experimental Requirements Test module is not done yet. It may eat your data, but you can read the <a href=":url">online documentation for the Experimental Requirements Test module</a>.', [':url' => 'http://www.example.com']);
}
}

View file

@ -3,7 +3,7 @@ label: Taxonomy terms
migration_tags:
- Drupal 6
source:
plugin: taxonomy_term
plugin: d6_taxonomy_term
process:
# If you are using this file to build a custom migration consider removing
# the tid field to allow incremental migrations.

View file

@ -2,8 +2,9 @@ id: d7_taxonomy_term
label: Taxonomy terms
migration_tags:
- Drupal 7
deriver: Drupal\taxonomy\Plugin\migrate\D7TaxonomyTermDeriver
source:
plugin: taxonomy_term
plugin: d7_taxonomy_term
process:
# If you are using this file to build a custom migration consider removing
# the tid field to allow incremental migrations.
@ -35,3 +36,5 @@ destination:
migration_dependencies:
required:
- d7_taxonomy_vocabulary
optional:
- d7_field_instance

View file

@ -0,0 +1,130 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\Migration;
use Drupal\migrate\Plugin\MigrationDeriverTrait;
use Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Deriver for Drupal 7 taxonomy term migrations based on vocabularies.
*/
class D7TaxonomyTermDeriver extends DeriverBase implements ContainerDeriverInterface {
use MigrationDeriverTrait;
/**
* The base plugin ID this derivative is for.
*
* @var string
*/
protected $basePluginId;
/**
* Already-instantiated cckfield plugins, keyed by ID.
*
* @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldInterface[]
*/
protected $cckPluginCache;
/**
* The CCK plugin manager.
*
* @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface
*/
protected $cckPluginManager;
/**
* D7TaxonomyTermDeriver constructor.
*
* @param string $base_plugin_id
* The base plugin ID for the plugin ID.
* @param \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface $cck_manager
* The CCK plugin manager.
*/
public function __construct($base_plugin_id, MigrateCckFieldPluginManagerInterface $cck_manager) {
$this->basePluginId = $base_plugin_id;
$this->cckPluginManager = $cck_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('plugin.manager.migrate.cckfield')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$fields = [];
try {
$source_plugin = static::getSourcePlugin('d7_field_instance');
$source_plugin->checkRequirements();
// Read all field instance definitions in the source database.
foreach ($source_plugin as $row) {
if ($row->getSourceProperty('entity_type') == 'taxonomy_term') {
$fields[$row->getSourceProperty('bundle')][$row->getSourceProperty('field_name')] = $row->getSource();
}
}
}
catch (RequirementsException $e) {
// If checkRequirements() failed then the field module did not exist and
// we do not have any fields. Therefore, $fields will be empty and below
// we'll create a migration just for the node properties.
}
try {
foreach (static::getSourcePlugin('d7_taxonomy_vocabulary') as $row) {
$bundle = $row->getSourceProperty('machine_name');
$values = $base_plugin_definition;
$values['label'] = t('@label (@type)', [
'@label' => $values['label'],
'@type' => $row->getSourceProperty('name'),
]);
$values['source']['bundle'] = $bundle;
$values['destination']['default_bundle'] = $bundle;
/** @var Migration $migration */
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($values);
if (isset($fields[$bundle])) {
foreach ($fields[$bundle] as $field_name => $info) {
$field_type = $info['type'];
try {
$plugin_id = $this->cckPluginManager->getPluginIdFromFieldType($field_type, ['core' => 7], $migration);
if (!isset($this->cckPluginCache[$field_type])) {
$this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($plugin_id, ['core' => 7], $migration);
}
$this->cckPluginCache[$field_type]
->processCckFieldValues($migration, $field_name, $info);
}
catch (PluginNotFoundException $ex) {
$migration->setProcessOfProperty($field_name, $field_name);
}
}
}
$this->derivatives[$bundle] = $migration->getPluginDefinition();
}
}
catch (DatabaseExceptionWrapper $e) {
// Once we begin iterating the source plugin it is possible that the
// source tables will not exist. This can happen when the
// MigrationPluginManager gathers up the migration definitions but we do
// not actually have a Drupal 7 source database.
}
return $this->derivatives;
}
}

View file

@ -14,6 +14,10 @@ use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
* id = "taxonomy_term",
* source_provider = "taxonomy"
* )
*
* @deprecated in Drupal 8.3.0, intended to be removed in Drupal 9.0.0.
* Use \Drupal\taxonomy\Plugin\migrate\source\d6\Term or
* \Drupal\taxonomy\Plugin\migrate\source\d7\Term.
*/
class Term extends DrupalSqlBase {
@ -50,7 +54,7 @@ class Term extends DrupalSqlBase {
->orderBy('td.tid');
if (isset($this->configuration['vocabulary'])) {
$query->condition('td.vid', $this->configuration['vocabulary'], 'IN');
$query->condition('td.vid', (array) $this->configuration['vocabulary'], 'IN');
}
return $query;

View file

@ -0,0 +1,74 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Taxonomy term source from database.
*
* @todo Support term_relation, term_synonym table if possible.
*
* @MigrateSource(
* id = "d6_taxonomy_term",
* source_provider = "taxonomy"
* )
*/
class Term extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('term_data', 'td')
->fields('td')
->distinct()
->orderBy('td.tid');
if (isset($this->configuration['bundle'])) {
$query->condition('td.vid', (array) $this->configuration['bundle'], 'IN');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'tid' => $this->t('The term ID.'),
'vid' => $this->t('Existing term VID'),
'name' => $this->t('The name of the term.'),
'description' => $this->t('The term description.'),
'weight' => $this->t('Weight'),
'parent' => $this->t("The Drupal term IDs of the term's parents."),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Find parents for this row.
$parents = $this->select('term_hierarchy', 'th')
->fields('th', ['parent', 'tid'])
->condition('tid', $row->getSourceProperty('tid'))
->execute()
->fetchCol();
$row->setSourceProperty('parent', $parents);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['tid']['type'] = 'integer';
return $ids;
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
/**
* Taxonomy term source from database.
*
* @todo Support term_relation, term_synonym table if possible.
*
* @MigrateSource(
* id = "d7_taxonomy_term",
* source_provider = "taxonomy"
* )
*/
class Term extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('taxonomy_term_data', 'td')
->fields('td')
->distinct()
->orderBy('tid');
$query->leftJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
$query->addField('tv', 'machine_name');
if (isset($this->configuration['bundle'])) {
$query->condition('tv.machine_name', (array) $this->configuration['bundle'], 'IN');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'tid' => $this->t('The term ID.'),
'vid' => $this->t('Existing term VID'),
'machine_name' => $this->t('Vocabulary machine name'),
'name' => $this->t('The name of the term.'),
'description' => $this->t('The term description.'),
'weight' => $this->t('Weight'),
'parent' => $this->t("The Drupal term IDs of the term's parents."),
'format' => $this->t("Format of the term description."),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Get Field API field values.
foreach (array_keys($this->getFields('taxonomy_term', $row->getSourceProperty('machine_name'))) as $field) {
$tid = $row->getSourceProperty('tid');
$row->setSourceProperty($field, $this->getFieldValues('taxonomy_term', $field, $tid));
}
// Find parents for this row.
$parents = $this->select('taxonomy_term_hierarchy', 'th')
->fields('th', ['parent', 'tid'])
->condition('tid', $row->getSourceProperty('tid'))
->execute()
->fetchCol();
$row->setSourceProperty('parent', $parents);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['tid']['type'] = 'integer';
return $ids;
}
}

View file

@ -13,7 +13,16 @@ use Drupal\taxonomy\TermInterface;
*/
class MigrateTaxonomyTermTest extends MigrateDrupal7TestBase {
public static $modules = array('taxonomy', 'text');
public static $modules = [
'comment',
'datetime',
'image',
'link',
'node',
'taxonomy',
'telephone',
'text',
];
/**
* The cached taxonomy tree items, keyed by vid and tid.
@ -28,7 +37,16 @@ class MigrateTaxonomyTermTest extends MigrateDrupal7TestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('taxonomy_term');
$this->executeMigrations(['d7_taxonomy_vocabulary', 'd7_taxonomy_term']);
$this->installConfig(static::$modules);
$this->executeMigrations([
'd7_node_type',
'd7_comment_type',
'd7_field',
'd7_taxonomy_vocabulary',
'd7_field_instance',
'd7_taxonomy_term'
]);
}
/**
@ -48,8 +66,12 @@ class MigrateTaxonomyTermTest extends MigrateDrupal7TestBase {
* The weight the migrated entity should have.
* @param array $expected_parents
* The parent terms the migrated entity should have.
* @param int $expected_field_integer_value
* The value the migrated entity field should have.
* @param int $expected_term_reference_tid
* The term reference id the migrated entity field should have.
*/
protected function assertEntity($id, $expected_label, $expected_vid, $expected_description = '', $expected_format = NULL, $expected_weight = 0, $expected_parents = []) {
protected function assertEntity($id, $expected_label, $expected_vid, $expected_description = '', $expected_format = NULL, $expected_weight = 0, $expected_parents = [], $expected_field_integer_value = NULL, $expected_term_reference_tid = NULL) {
/** @var \Drupal\taxonomy\TermInterface $entity */
$entity = Term::load($id);
$this->assertTrue($entity instanceof TermInterface);
@ -60,6 +82,14 @@ class MigrateTaxonomyTermTest extends MigrateDrupal7TestBase {
$this->assertEqual($expected_weight, $entity->getWeight());
$this->assertIdentical($expected_parents, $this->getParentIDs($id));
$this->assertHierarchy($expected_vid, $id, $expected_parents);
if (!is_null($expected_field_integer_value)) {
$this->assertTrue($entity->hasField('field_integer'));
$this->assertEquals($expected_field_integer_value, $entity->field_integer->value);
}
if (!is_null($expected_term_reference_tid)) {
$this->assertTrue($entity->hasField('field_integer'));
$this->assertEquals($expected_term_reference_tid, $entity->field_term_reference->target_id);
}
}
/**
@ -67,9 +97,9 @@ class MigrateTaxonomyTermTest extends MigrateDrupal7TestBase {
*/
public function testTaxonomyTerms() {
$this->assertEntity(1, 'General discussion', 'forums', '', NULL, 2);
$this->assertEntity(2, 'Term1', 'test_vocabulary', 'The first term.', 'filtered_html');
$this->assertEntity(2, 'Term1', 'test_vocabulary', 'The first term.', 'filtered_html', 0, [], NULL, 3);
$this->assertEntity(3, 'Term2', 'test_vocabulary', 'The second term.', 'filtered_html');
$this->assertEntity(4, 'Term3', 'test_vocabulary', 'The third term.', 'full_html', 0, [3]);
$this->assertEntity(4, 'Term3', 'test_vocabulary', 'The third term.', 'full_html', 0, [3], 6);
$this->assertEntity(5, 'Custom Forum', 'forums', 'Where the cool kids are.', NULL, 3);
$this->assertEntity(6, 'Games', 'forums', '', NULL, 4);
$this->assertEntity(7, 'Minecraft', 'forums', '', NULL, 1, [6]);

View file

@ -1,11 +1,11 @@
<?php
namespace Drupal\Tests\taxonomy\Kernel\Plugin\migrate\source;
namespace Drupal\Tests\taxonomy\Kernel\Plugin\migrate\source\d6;
/**
* Tests the taxonomy term source with vocabulary filter.
*
* @covers \Drupal\taxonomy\Plugin\migrate\source\Term
* @covers \Drupal\taxonomy\Plugin\migrate\source\d6\Term
* @group taxonomy
*/
class TermSourceWithVocabularyFilterTest extends TermTest {
@ -47,7 +47,7 @@ class TermSourceWithVocabularyFilterTest extends TermTest {
// Set up source plugin configuration.
$tests[0]['configuration'] = [
'vocabulary' => [5],
'bundle' => [5],
];
return $tests;

View file

@ -1,13 +1,13 @@
<?php
namespace Drupal\Tests\taxonomy\Kernel\Plugin\migrate\source;
namespace Drupal\Tests\taxonomy\Kernel\Plugin\migrate\source\d6;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests taxonomy term source plugin.
*
* @covers \Drupal\taxonomy\Plugin\migrate\source\Term
* @covers \Drupal\taxonomy\Plugin\migrate\source\d6\Term
* @group taxonomy
*/
class TermTest extends MigrateSqlSourceTestBase {
@ -67,6 +67,13 @@ class TermTest extends MigrateSqlSourceTestBase {
'description' => 'description value 6',
'weight' => 0,
],
[
'tid' => 7,
'vid' => 3,
'name' => 'name value 7',
'description' => 'description value 7',
'weight' => 0,
],
];
$tests[0]['source_data']['term_hierarchy'] = [
[
@ -97,6 +104,10 @@ class TermTest extends MigrateSqlSourceTestBase {
'tid' => 6,
'parent' => 2,
],
[
'tid' => 7,
'parent' => 0,
],
];
// The expected results.
@ -149,8 +160,58 @@ class TermTest extends MigrateSqlSourceTestBase {
'weight' => 0,
'parent' => [3, 2],
],
[
'tid' => 7,
'vid' => 3,
'name' => 'name value 7',
'description' => 'description value 7',
'weight' => 0,
'parent' => [0],
],
];
$tests[0]['expected_count'] = NULL;
// Empty configuration will return terms for all vocabularies.
$tests[0]['configuration'] = [];
// Change configuration to get one vocabulary, 5.
$tests[1]['source_data'] = $tests[0]['source_data'];
$tests[1]['expected_data'] = [
[
'tid' => 1,
'vid' => 5,
'name' => 'name value 1',
'description' => 'description value 1',
'weight' => 0,
'parent' => [0],
],
[
'tid' => 4,
'vid' => 5,
'name' => 'name value 4',
'description' => 'description value 4',
'weight' => 1,
'parent' => [1],
],
];
$tests[1]['expected_count'] = NULL;
$tests[1]['configuration']['bundle'] = ['5'];
// Same as previous test, but with configuration vocabulary as a string
// instead of an array.
$tests[2]['source_data'] = $tests[0]['source_data'];
$tests[2]['expected_data'] = $tests[1]['expected_data'];
$tests[2]['expected_count'] = NULL;
$tests[2]['configuration']['bundle'] = '5';
// Change configuration to get two vocabularies, 5 and 6.
$tests[3]['source_data'] = $tests[0]['source_data'];
$tests[3]['expected_data'] = $tests[0]['expected_data'];
// Remove the last element because it is for vid 3.
array_pop($tests[3]['expected_data']);
$tests[3]['expected_count'] = NULL;
$tests[3]['configuration']['bundle'] = ['5', '6'];
return $tests;
}

View file

@ -0,0 +1,56 @@
<?php
namespace Drupal\Tests\taxonomy\Kernel\Plugin\migrate\source\d7;
/**
* Tests the taxonomy term source with vocabulary filter.
*
* @covers \Drupal\taxonomy\Plugin\migrate\source\d7\Term
* @group taxonomy
*/
class TermSourceWithVocabularyFilterTest extends TermTest {
/**
* {@inheritdoc}
*/
public static $modules = ['taxonomy', 'migrate_drupal'];
/**
* {@inheritdoc}
*/
public function providerSource() {
// Get the source data from parent.
$tests = parent::providerSource();
// The expected results.
$tests[0]['expected_data'] = [
[
'tid' => 1,
'vid' => 5,
'name' => 'name value 1',
'description' => 'description value 1',
'weight' => 0,
'parent' => [0],
],
[
'tid' => 4,
'vid' => 5,
'name' => 'name value 4',
'description' => 'description value 4',
'weight' => 1,
'parent' => [1],
],
];
// We know there are two rows with machine_name == 'tags'.
$tests[0]['expected_count'] = 2;
// Set up source plugin configuration.
$tests[0]['configuration'] = [
'bundle' => ['tags'],
];
return $tests;
}
}

View file

@ -0,0 +1,258 @@
<?php
namespace Drupal\Tests\taxonomy\Kernel\Plugin\migrate\source\d7;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests taxonomy term source plugin.
*
* @covers \Drupal\taxonomy\Plugin\migrate\source\d7\Term
* @group taxonomy
*/
class TermTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['taxonomy', 'migrate_drupal'];
/**
* {@inheritdoc}
*/
public function providerSource() {
$tests = [];
// The source data.
$tests[0]['source_data']['taxonomy_term_data'] = [
[
'tid' => 1,
'vid' => 5,
'name' => 'name value 1',
'description' => 'description value 1',
'weight' => 0,
],
[
'tid' => 2,
'vid' => 6,
'name' => 'name value 2',
'description' => 'description value 2',
'weight' => 0,
],
[
'tid' => 3,
'vid' => 6,
'name' => 'name value 3',
'description' => 'description value 3',
'weight' => 0,
],
[
'tid' => 4,
'vid' => 5,
'name' => 'name value 4',
'description' => 'description value 4',
'weight' => 1,
],
[
'tid' => 5,
'vid' => 6,
'name' => 'name value 5',
'description' => 'description value 5',
'weight' => 1,
],
[
'tid' => 6,
'vid' => 6,
'name' => 'name value 6',
'description' => 'description value 6',
'weight' => 0,
],
[
'tid' => 7,
'vid' => 3,
'name' => 'name value 7',
'description' => 'description value 7',
'weight' => 0,
],
];
$tests[0]['source_data']['taxonomy_term_hierarchy'] = [
[
'tid' => 1,
'parent' => 0,
],
[
'tid' => 2,
'parent' => 0,
],
[
'tid' => 3,
'parent' => 0,
],
[
'tid' => 4,
'parent' => 1,
],
[
'tid' => 5,
'parent' => 2,
],
[
'tid' => 6,
'parent' => 3,
],
[
'tid' => 6,
'parent' => 2,
],
[
'tid' => 7,
'parent' => 0,
],
];
$tests[0]['source_data']['taxonomy_vocabulary'] = [
[
'vid' => 5,
'machine_name' => 'tags',
],
[
'vid' => 6,
'machine_name' => 'categories',
],
];
$tests[0]['source_data']['field_config_instance'] = [
[
'field_name' => 'field_term_field',
'entity_type' => 'taxonomy_term',
'bundle' => 'tags',
'deleted' => 0,
],
[
'field_name' => 'field_term_field',
'entity_type' => 'taxonomy_term',
'bundle' => 'categories',
'deleted' => 0,
],
];
$tests[0]['source_data']['field_data_field_term_field'] = [
[
'entity_type' => 'taxonomy_term',
'bundle' => 'tags',
'deleted' => 0,
'entity_id' => 1,
'delta' => 0,
],
[
'entity_type' => 'taxonomy_term',
'bundle' => 'categories',
'deleted' => 0,
'entity_id' => 1,
'delta' => 0,
],
];
// The expected results.
$tests[0]['expected_data'] = [
[
'tid' => 1,
'vid' => 5,
'name' => 'name value 1',
'description' => 'description value 1',
'weight' => 0,
'parent' => [0],
],
[
'tid' => 2,
'vid' => 6,
'name' => 'name value 2',
'description' => 'description value 2',
'weight' => 0,
'parent' => [0],
],
[
'tid' => 3,
'vid' => 6,
'name' => 'name value 3',
'description' => 'description value 3',
'weight' => 0,
'parent' => [0],
],
[
'tid' => 4,
'vid' => 5,
'name' => 'name value 4',
'description' => 'description value 4',
'weight' => 1,
'parent' => [1],
],
[
'tid' => 5,
'vid' => 6,
'name' => 'name value 5',
'description' => 'description value 5',
'weight' => 1,
'parent' => [2],
],
[
'tid' => 6,
'vid' => 6,
'name' => 'name value 6',
'description' => 'description value 6',
'weight' => 0,
'parent' => [3, 2],
],
[
'tid' => 7,
'vid' => 3,
'name' => 'name value 7',
'description' => 'description value 7',
'weight' => 0,
'parent' => [0],
],
];
$tests[0]['expected_count'] = NULL;
// Empty configuration will return terms for all vocabularies.
$tests[0]['configuration'] = [];
// Change configuration to get one vocabulary, "tags".
$tests[1]['source_data'] = $tests[0]['source_data'];
$tests[1]['expected_data'] = [
[
'tid' => 1,
'vid' => 5,
'name' => 'name value 1',
'description' => 'description value 1',
'weight' => 0,
'parent' => [0],
],
[
'tid' => 4,
'vid' => 5,
'name' => 'name value 4',
'description' => 'description value 4',
'weight' => 1,
'parent' => [1],
],
];
$tests[1]['expected_count'] = NULL;
$tests[1]['configuration']['bundle'] = ['tags'];
// Same as previous test, but with configuration vocabulary as a string
// instead of an array.
$tests[2]['source_data'] = $tests[0]['source_data'];
$tests[2]['expected_data'] = $tests[1]['expected_data'];
$tests[2]['expected_count'] = NULL;
$tests[2]['configuration']['bundle'] = 'tags';
// Change configuration to get two vocabularies, "tags" and "categories".
$tests[3]['source_data'] = $tests[0]['source_data'];
$tests[3]['expected_data'] = $tests[0]['expected_data'];
// Remove the last element because it is for vid 3.
array_pop($tests[3]['expected_data']);
$tests[3]['expected_count'] = NULL;
$tests[3]['configuration']['bundle'] = ['tags', 'categories'];
return $tests;
}
}

View file

@ -3,12 +3,8 @@ label: Profile values
class: Drupal\user\Plugin\migrate\ProfileValues
migration_tags:
- Drupal 6
builder:
plugin: d6_profile_values
source:
plugin: d6_profile_field_values
load:
plugin: drupal_entity
process:
uid: uid
destination:

View file

@ -233,7 +233,7 @@ class UserAuthenticationController extends ControllerBase implements ContainerIn
/**
* Logs out a user.
*
* @return \Drupal\rest\ResourceResponse
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
*/
public function logout() {

View file

@ -1172,7 +1172,7 @@ abstract class ArgumentPluginBase extends HandlerBase implements CacheableDepend
* Moves argument options into their place.
*
* When configuring the default argument behavior, almost each of the radio
* buttons has its own fieldset shown bellow it when the radio button is
* buttons has its own fieldset shown below it when the radio button is
* clicked. That fieldset is created through a custom form process callback.
* Each element that has #argument_option defined and pointing to a default
* behavior gets moved to the appropriate fieldset.

View file

@ -206,7 +206,7 @@ class InOperator extends FilterPluginBase {
}
if (empty($this->options['expose']['multiple'])) {
if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce'])) || isset($this->options['value']['all'])) {
$default_value = 'All';
}
elseif (empty($default_value)) {

View file

@ -18,19 +18,24 @@ class FilterTest extends PluginTestBase {
*
* @var array
*/
public static $testViews = array('test_filter');
public static $testViews = array('test_filter', 'test_filter_in_operator_ui');
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('views_ui');
public static $modules = array('views_ui', 'node');
protected function setUp() {
parent::setUp();
$this->enableViewsTestModule();
$this->adminUser = $this->drupalCreateUser(array('administer views'));
$this->drupalLogin($this->adminUser);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']);
}
/**
@ -138,4 +143,23 @@ class FilterTest extends PluginTestBase {
$this->assertEqual(count($view->result), 5, format_string('All @count results returned', array('@count' => count($view->displayHandlers))));
}
/**
* Test no error message is displayed when all options are selected in an
* exposed filter.
*/
public function testInOperatorSelectAllOptions() {
$view = Views::getView('test_filter_in_operator_ui');
$row['row[type]'] = 'fields';
$this->drupalPostForm('admin/structure/views/nojs/display/test_filter_in_operator_ui/default/row', $row, t('Apply'));
$field['name[node_field_data.nid]'] = TRUE;
$this->drupalPostForm('admin/structure/views/nojs/add-handler/test_filter_in_operator_ui/default/field', $field, t('Add and configure fields'));
$this->drupalPostForm('admin/structure/views/nojs/handler/test_filter_in_operator_ui/default/field/nid', [], t('Apply'));
$edit['options[value][all]'] = TRUE;
$edit['options[value][article]'] = TRUE;
$edit['options[value][page]'] = TRUE;
$this->drupalPostForm('admin/structure/views/nojs/handler/test_filter_in_operator_ui/default/filter/type', $edit, t('Apply'));
$this->drupalPostForm('admin/structure/views/view/test_filter_in_operator_ui/edit/default', [], t('Save'));
$this->drupalPostForm(NULL, [], t('Update preview'));
$this->assertNoText('An illegal choice has been detected.');
}
}

View file

@ -61,7 +61,7 @@ class Views {
/**
* Returns the views data helper service.
*
* @return \Drupal\views\ViewsData
* @return \Drupal\views\ViewsDataHelper
* Returns a views data helper object.
*/
public static function viewsDataHelper() {

View file

@ -2,6 +2,10 @@
namespace Drupal\FunctionalJavascriptTests;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\ElementHtmlException;
use Behat\Mink\Exception\ElementNotFoundException;
use Behat\Mink\Exception\UnsupportedDriverActionException;
use Drupal\Tests\WebAssert;
/**
@ -39,4 +43,199 @@ class JSWebAssert extends WebAssert {
$this->assertWaitOnAjaxRequest();
}
/**
* Test that a node, or it's specific corner, is visible in the viewport.
*
* Note: Always set the viewport size. This can be done with a PhantomJS
* startup parameter or in your test with \Behat\Mink\Session->resizeWindow().
* Drupal CI Javascript tests by default use a viewport of 1024x768px.
*
* @param string $selector_type
* The element selector type (CSS, XPath).
* @param string|array $selector
* The element selector. Note: the first found element is used.
* @param bool|string $corner
* (Optional) The corner to test:
* topLeft, topRight, bottomRight, bottomLeft.
* Or FALSE to check the complete element (default).
* @param string $message
* (optional) A message for the exception.
*
* @throws \Behat\Mink\Exception\ElementHtmlException
* When the element doesn't exist.
* @throws \Behat\Mink\Exception\ElementNotFoundException
* When the element is not visible in the viewport.
*/
public function assertVisibleInViewport($selector_type, $selector, $corner = FALSE, $message = 'Element is not visible in the viewport.') {
$node = $this->session->getPage()->find($selector_type, $selector);
if ($node === NULL) {
if (is_array($selector)) {
$selector = implode(' ', $selector);
}
throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector);
}
// Check if the node is visible on the page, which is a prerequisite of
// being visible in the viewport.
if (!$node->isVisible()) {
throw new ElementHtmlException($message, $this->session->getDriver(), $node);
}
$result = $this->checkNodeVisibilityInViewport($node, $corner);
if (!$result) {
throw new ElementHtmlException($message, $this->session->getDriver(), $node);
}
}
/**
* Test that a node, or its specific corner, is not visible in the viewport.
*
* Note: the node should exist in the page, otherwise this assertion fails.
*
* @param string $selector_type
* The element selector type (CSS, XPath).
* @param string|array $selector
* The element selector. Note: the first found element is used.
* @param bool|string $corner
* (Optional) Corner to test: topLeft, topRight, bottomRight, bottomLeft.
* Or FALSE to check the complete element (default).
* @param string $message
* (optional) A message for the exception.
*
* @throws \Behat\Mink\Exception\ElementHtmlException
* When the element doesn't exist.
* @throws \Behat\Mink\Exception\ElementNotFoundException
* When the element is not visible in the viewport.
*
* @see \Drupal\FunctionalJavascriptTests\JSWebAssert::assertVisibleInViewport()
*/
public function assertNotVisibleInViewport($selector_type, $selector, $corner = FALSE, $message = 'Element is visible in the viewport.') {
$node = $this->session->getPage()->find($selector_type, $selector);
if ($node === NULL) {
if (is_array($selector)) {
$selector = implode(' ', $selector);
}
throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector);
}
$result = $this->checkNodeVisibilityInViewport($node, $corner);
if ($result) {
throw new ElementHtmlException($message, $this->session->getDriver(), $node);
}
}
/**
* Check the visibility of a node, or it's specific corner.
*
* @param \Behat\Mink\Element\NodeElement $node
* A valid node.
* @param bool|string $corner
* (Optional) Corner to test: topLeft, topRight, bottomRight, bottomLeft.
* Or FALSE to check the complete element (default).
*
* @return bool
* Returns TRUE if the node is visible in the viewport, FALSE otherwise.
*
* @throws \Behat\Mink\Exception\UnsupportedDriverActionException
* When an invalid corner specification is given.
*/
private function checkNodeVisibilityInViewport(NodeElement $node, $corner = FALSE) {
$xpath = $node->getXpath();
// Build the Javascript to test if the complete element or a specific corner
// is in the viewport.
switch ($corner) {
case 'topLeft':
$test_javascript_function = <<<JS
function t(r, lx, ly) {
return (
r.top >= 0 &&
r.top <= ly &&
r.left >= 0 &&
r.left <= lx
)
}
JS;
break;
case 'topRight':
$test_javascript_function = <<<JS
function t(r, lx, ly) {
return (
r.top >= 0 &&
r.top <= ly &&
r.right >= 0 &&
r.right <= lx
);
}
JS;
break;
case 'bottomRight':
$test_javascript_function = <<<JS
function t(r, lx, ly) {
return (
r.bottom >= 0 &&
r.bottom <= ly &&
r.right >= 0 &&
r.right <= lx
);
}
JS;
break;
case 'bottomLeft':
$test_javascript_function = <<<JS
function t(r, lx, ly) {
return (
r.bottom >= 0 &&
r.bottom <= ly &&
r.left >= 0 &&
r.left <= lx
);
}
JS;
break;
case FALSE:
$test_javascript_function = <<<JS
function t(r, lx, ly) {
return (
r.top >= 0 &&
r.left >= 0 &&
r.bottom <= ly &&
r.right <= lx
);
}
JS;
break;
// Throw an exception if an invalid corner parameter is given.
default:
throw new UnsupportedDriverActionException($corner, $this->session->getDriver());
}
// Build the full Javascript test. The shared logic gets the corner
// specific test logic injected.
$full_javascript_visibility_test = <<<JS
(function(t){
var w = window,
d = document,
e = d.documentElement,
n = d.evaluate("$xpath", d, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue,
r = n.getBoundingClientRect(),
lx = (w.innerWidth || e.clientWidth),
ly = (w.innerHeight || e.clientHeight);
return t(r, lx, ly);
}($test_javascript_function));
JS;
// Check the visibility by injecting and executing the full Javascript test
// script in the page.
return $this->session->evaluateScript($full_javascript_visibility_test);
}
}

View file

@ -146,7 +146,7 @@ abstract class KernelTestBase extends \PHPUnit_Framework_TestCase implements Ser
*
* @var array
*/
public static $modules = array();
protected static $modules = array();
/**
* The virtual filesystem root directory.

View file

@ -175,7 +175,7 @@ abstract class BrowserTestBase extends \PHPUnit_Framework_TestCase {
*
* @see \Drupal\Tests\BrowserTestBase::installDrupal()
*/
public static $modules = [];
protected static $modules = [];
/**
* An array of config object names that are excluded from schema checking.