Skip to content
Snippets Groups Projects
Commit 5381ae6e authored by Scott Euser's avatar Scott Euser
Browse files

Issue #3460863 by scott_euser: Handle 414 errors in views reference field

parent 35e14526
No related branches found
Tags 8.x-1.0-rc1
No related merge requests found
{
"dictionaries": [
"dictionary",
"drupal",
"companies",
"fonts",
"html",
"php",
"softwareTerms",
"viewsreference"
],
"dictionaryDefinitions": [
{ "name": "viewsreference_extras", "path": "./dictionary.txt"}
],
"ignorePaths": [
"README.md"
]
}
\ No newline at end of file
################
# DrupalCI GitLabCI template
#
# Gitlab-ci.yml to replicate DrupalCI testing for Contrib
#
# With thanks to:
# * The GitLab Acceleration Initiative participants
# * DrupalSpoons
################
################
# Guidelines
#
# This template is designed to give any Contrib maintainer everything they need to test, without requiring modification. It is also designed to keep up to date with Core Development automatically through the use of include files that can be centrally maintained.
#
# However, you can modify this template if you have additional needs for your project.
################
################
# Includes
#
# Additional configuration can be provided through includes.
# One advantage of include files is that if they are updated upstream, the changes affect all pipelines using that include.
#
# Includes can be overridden by re-declaring anything provided in an include, here in gitlab-ci.yml
# https://docs.gitlab.com/ee/ci/yaml/includes.html#override-included-configuration-values
################
include:
################
# DrupalCI includes:
# As long as you include this, any future includes added by the Drupal Association will be accessible to your pipelines automatically.
# View these include files at https://git.drupalcode.org/project/gitlab_templates/
################
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.main.yml'
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
################
# Pipeline configuration variables
#
# These are the variables provided to the Run Pipeline form that a user may want to override.
#
# Docs at https://git.drupalcode.org/project/gitlab_templates/-/blob/1.0.x/includes/include.drupalci.variables.yml
################
variables:
OPT_IN_TEST_NEXT_MAJOR: '1'
RUN_JOB_UPGRADE_STATUS: '1'
###################################################################################
#
# *
# /(
# ((((,
# /(((((((
# ((((((((((*
# ,(((((((((((((((
# ,(((((((((((((((((((
# ((((((((((((((((((((((((*
# *(((((((((((((((((((((((((((((
# ((((((((((((((((((((((((((((((((((*
# *(((((((((((((((((( .((((((((((((((((((
# ((((((((((((((((((. /(((((((((((((((((*
# /((((((((((((((((( .(((((((((((((((((,
# ,(((((((((((((((((( ((((((((((((((((((
# .(((((((((((((((((((( .(((((((((((((((((
# ((((((((((((((((((((((( ((((((((((((((((/
# (((((((((((((((((((((((((((/ ,(((((((((((((((*
# .((((((((((((((/ /(((((((((((((. ,(((((((((((((((
# *(((((((((((((( ,(((((((((((((/ *((((((((((((((.
# ((((((((((((((, /(((((((((((((. ((((((((((((((,
# (((((((((((((/ ,(((((((((((((* ,(((((((((((((,
# *((((((((((((( .((((((((((((((( ,(((((((((((((
# ((((((((((((/ /((((((((((((((((((. ,((((((((((((/
# ((((((((((((( *(((((((((((((((((((((((* *((((((((((((
# ((((((((((((( ,(((((((((((((..((((((((((((( *((((((((((((
# ((((((((((((, /((((((((((((* /((((((((((((/ ((((((((((((
# ((((((((((((( /((((((((((((/ (((((((((((((* ((((((((((((
# (((((((((((((/ /(((((((((((( ,((((((((((((, *((((((((((((
# (((((((((((((( *(((((((((((/ *((((((((((((. ((((((((((((/
# *((((((((((((((((((((((((((, /(((((((((((((((((((((((((
# ((((((((((((((((((((((((( ((((((((((((((((((((((((,
# .(((((((((((((((((((((((/ ,(((((((((((((((((((((((
# ((((((((((((((((((((((/ ,(((((((((((((((((((((/
# *((((((((((((((((((((( (((((((((((((((((((((,
# ,(((((((((((((((((((((, ((((((((((((((((((((/
# ,(((((((((((((((((((((* /((((((((((((((((((((
# ((((((((((((((((((((((, ,/((((((((((((((((((((,
# ,(((((((((((((((((((((((((((((((((((((((((((((((((((
# .(((((((((((((((((((((((((((((((((((((((((((((
# .((((((((((((((((((((((((((((((((((((,.
# .,(((((((((((((((((((((((((.
#
###################################################################################
{
"name": "drupal/viewsreference_extras",
"type": "drupal-module",
"description": "Views Reference Extras",
"keywords": ["Drupal"],
"license": "GPL-2.0-or-later",
"homepage": "http://drupal.org/project/viewsreference_extras",
"minimum-stability": "dev",
"support": {
"issues": "https://www.drupal.org/project/issues/viewsreference_extras",
"source": "https://git.drupalcode.org/project/viewsreference_extras"
},
"require": {
"drupal/viewsreference": "2.x-dev"
},
"require-dev": {
"drupal/views_ajax_history": "1.x-dev"
}
}
viewsreference
uncompress
# This file is copied from ./core/phpstan.neon.dist then set to only
# ignore errors applicable to this project rather than all errors ignored
# from Core as phpstan throws errors when ignore lines here are unused.
# Configuration file for PHPStan static code checking, see https://phpstan.org .
# PHPStan is triggered on Drupal CI in commit-code-check.sh.
includes:
- phar://phpstan.phar/conf/bleedingEdge.neon
parameters:
level: 1
paths:
- .
ignoreErrors:
# new static() is a best practice in Drupal, so we cannot fix that.
- "#^Unsafe usage of new static#"
\ No newline at end of file
<?php
namespace Drupal\viewsreference_extras;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\views\ViewExecutable;
use Drupal\viewsreference\ViewsReferenceCompressionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the views reference extras compression service.
*
* This service compresses the entity details then uses those to reload the
* views reference configuration on un-compress.
*/
class ViewsReferenceExtrasCompressionReload implements ViewsReferenceCompressionInterface, ContainerInjectionInterface {
/**
* Constructs the Views Reference Extras compression reload class.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser
* The current user.
*/
public function __construct(
protected EntityTypeManagerInterface $entityTypeManager,
protected AccountProxyInterface $currentUser,
) {
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function compress(array $viewsreference, ViewExecutable $view): array {
// Remove any stored data and existing compression, instead opting for a
// reload mechanism to load the configuration from source again.
unset($viewsreference['data']);
unset($viewsreference['compressed']);
// Compress the Views Reference Field settings as a JSON String to help
// avoid 414 errors with the request URI being too long.
$json = Json::encode($viewsreference);
return [
'reload' => UrlHelper::compressQueryParameter($json),
];
}
/**
* {@inheritdoc}
*/
public function uncompress(array $viewsreference, ViewExecutable $view): array {
if (!isset($viewsreference['reload'])) {
return $viewsreference;
}
$entity = FALSE;
$reload = UrlHelper::uncompressQueryParameter($viewsreference['reload']);
$reload = Json::decode($reload);
$entity_storage = $this->entityTypeManager->getStorage($reload['parent_entity_type']);
if (empty($reload['parent_revision_id'])) {
$entity = $entity_storage->load($reload['parent_entity_id']);
}
else {
// Load the entity with specific revision. Determine the correct
// keys to use by the entity type class in case they differ
// from the usual id and revision_id.
$entity_keys = $entity_storage->getEntityType()->getKeys();
$entities = $entity_storage->loadByProperties([
$entity_keys['id'] => $reload['parent_entity_id'],
$entity_keys['revision'] => $reload['parent_revision_id'],
]);
if ($entities) {
$entity = reset($entities);
}
}
// Safely load the data loaded in the field formatter. Step by step
// ensure that each bit of the recorded entity is available and matches to
// avoid an unforeseen errors. Double-check access as well in case the
// user manages to manipulate the compressed query string to result in a
// different entity ID.
if (
$entity instanceof FieldableEntityInterface
&& $entity->hasField($reload['parent_field_name'])
&& $entity->access('view', $this->currentUser)
&& $items = $entity->get($reload['parent_field_name'])
) {
// Retrieve the data that was unset in compression.
$reload['data'] = [];
foreach ($items as $delta => $item) {
if (isset($reload['field_item_delta']) && $reload['field_item_delta'] !== $delta) {
continue;
}
if (!empty($item->getValue()['data'])) {
$reload['data'] = unserialize($item->getValue()['data'], [
'allowed_classes' => FALSE,
]);
}
}
}
return $reload;
}
}
<?php
namespace Drupal\viewsreference_extras_test\Plugin\ViewsReferenceSetting;
use Drupal\Component\Plugin\PluginBase;
use Drupal\views\ViewExecutable;
use Drupal\viewsreference\Plugin\ViewsReferenceSettingInterface;
/**
* Used to test the compression reload of settings.
*
* @ViewsReferenceSetting(
* id = "test_compression_reload_setting",
* label = @Translation("Test compression reload setting"),
* default_value = {},
* )
*/
class TestCompressionReloadSetting extends PluginBase implements ViewsReferenceSettingInterface {
/**
* {@inheritdoc}
*/
public function alterFormField(array &$form_field) {
// Make a form field with an overly high amount of
// options to cause the compression to still pass
// the limits.
// @see viewsreference_views_pre_render().
$form_field['#title'] = 'Test compression';
$form_field['#type'] = 'checkboxes';
$form_field['#options'] = [];
foreach (range(0, 500) as $number) {
$form_field['#options']['test_option_' . $number] = 'test_option_' . $number;
}
}
/**
* {@inheritdoc}
*/
public function alterView(ViewExecutable $view, $value) {
}
}
name: 'Views Reference Extras Test Module'
description: 'Provides sample data to help test the module'
core_version_requirement: ^10 || ^11
package: Views
type: module
dependencies:
- drupal:node
- drupal:views
- viewsreference:viewsreference
- viewsreference_extras:viewsreference_extras
<?php
namespace Drupal\Tests\viewsreference_extras\FunctionalJavascript;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\UrlHelper;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\views\Views;
/**
* Tests the views reference extras compression and reload of settings.
*
* @group viewsreference_extras
*/
class ViewsReferenceExtrasCompressionReloadTest extends WebDriverTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to bypass access content.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* The node containing the views reference.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'viewsreference',
'views_ui',
'field_ui',
'big_pipe',
'block',
'test_views_reference_ajax_history',
'viewsreference_extras_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType([
'type' => 'page',
'name' => 'Basic page',
'display_submitted' => FALSE,
]);
$this->adminUser = $this->drupalCreateUser([
'access content',
'bypass node access',
'administer nodes',
'administer content types',
'administer node fields',
]);
// Create a content item with a views reference. The easiest way to test
// this is to ensure ajax history is also enabled, so we piggy-back
// off of that test module.
$this->node = $this->drupalCreateNode([
'title' => 'Test page with compression',
'type' => 'test_ajax_history',
'field_views_reference_history' => [
'target_id' => 'test_views_ajax_history',
'display_id' => 'block_1',
],
]);
$this->drupalLogin($this->adminUser);
}
/**
* Tests the Views Reference compression.
*/
public function testViewsReferenceCompression() {
// Create sample content items.
foreach (range(1, 100) as $count) {
$this->drupalCreateNode([
'title' => 'Test page for view ' . $count,
]);
}
// Enable the settings.
$this->drupalGet('admin/structure/types/manage/test_ajax_history/fields/node.test_ajax_history.field_views_reference_history');
$this->submitForm([
'settings[enabled_settings][limit]' => TRUE,
'settings[enabled_settings][test_compression_reload_setting]' => TRUE,
], 'Save settings');
$this->drupalGet('node/' . $this->node->id() . '/edit');
$this->click('#edit-field-views-reference-history-0-options');
$this->submitForm([
'field_views_reference_history[0][options][limit]' => 20,
'field_views_reference_history[0][options][test_compression_reload_setting][test_option_2]' => 'test_option_2',
'field_views_reference_history[0][options][test_compression_reload_setting][test_option_3]' => 'test_option_3',
'field_views_reference_history[0][options][test_compression_reload_setting][test_option_4]' => 'test_option_4',
], 'Save');
$this->drupalGet('node/' . $this->node->id() . '/edit');
$this->getSession()->getPage();
// Test pagination triggers ajax history.
$this->drupalGet('node/' . $this->node->id());
$this->assertSession()->pageTextContains('Test page for view 100');
$pagination_links = $this->getSession()->getPage()->findAll('css', '.pager__item a');
foreach ($pagination_links as $link) {
if (str_contains($link->getText(), '2')) {
$link->click();
break;
}
}
$this->assertSession()->waitForText('Test page for view 80');
$this->assertStringContainsString('page=1', $this->getSession()->getCurrentUrl());
// Next page.
$pagination_links = $this->getSession()->getPage()->findAll('css', '.pager__item a');
foreach ($pagination_links as $link) {
if (str_contains($link->getText(), '3')) {
$link->click();
break;
}
}
$this->assertSession()->waitForText('Test page for view 60');
$this->assertStringContainsString('page=2', $this->getSession()->getCurrentUrl());
// By default, Views Ajax History will have the viewsreference configuration
// in the URL on the second click and onwards if the compression is not in
// place. This ensures the compression has remained in place.
$this->assertStringContainsString('viewsreference[reload]', $this->getSession()->getCurrentUrl());
// Ensure that the compressed string, once un-compressed, resulted in the
// next page of results.
$this->assertSession()->waitForText('Test page for view 40');
$parsed_url = UrlHelper::parse($this->getSession()->getCurrentUrl());
// Check that the uncompressed value is still as expected.
$expected_data = [
'title' => NULL,
'pager' => NULL,
'offset' => NULL,
'limit' => '20',
'header' => NULL,
'argument' => NULL,
'test_compression_reload_setting' => [],
];
// Expected configuration should initially only contain what is needed
// to reload.
$expected_configuration = [
'enabled_settings' => [
'limit' => 'limit',
'test_compression_reload_setting' => 'test_compression_reload_setting',
],
'parent_entity_type' => 'node',
'parent_entity_id' => '1',
'parent_field_name' => 'field_views_reference_history',
];
$json = UrlHelper::uncompressQueryParameter($parsed_url['query']['viewsreference']['reload']);
$parameters = Json::decode($json);
$this->assertSame($expected_configuration, $parameters);
// Let's check what the uncompress would return and make sure that is
// working correctly.
// Set the selected 3 settings, leaving the rest empty.
foreach (range(0, 500) as $number) {
$expected_data['test_compression_reload_setting']['test_option_' . $number] = 0;
}
$expected_data['test_compression_reload_setting']['test_option_2'] = 'test_option_2';
$expected_data['test_compression_reload_setting']['test_option_3'] = 'test_option_3';
$expected_data['test_compression_reload_setting']['test_option_4'] = 'test_option_4';
ksort($expected_data);
$expected_configuration = [
'data' => $expected_data,
'enabled_settings' => [
'limit' => 'limit',
'test_compression_reload_setting' => 'test_compression_reload_setting',
],
'parent_entity_type' => 'node',
'parent_entity_id' => '1',
'parent_field_name' => 'field_views_reference_history',
];
$view = Views::getView('test_views_ajax_history');
$view->setDisplay('block_1');
/** @var \Drupal\viewsreference\ViewsReferenceCompressionInterface $compression */
$compression = \Drupal::service('viewsreference.compression');
$parameters = $compression->compress($parameters, $view);
$result = $compression->uncompress($parameters, $view);
// We do not care about the order, we just want them to have the same data.
ksort($result);
ksort($result['data']);
ksort($result['data']['test_compression_reload_setting']);
ksort($expected_configuration);
ksort($expected_configuration['data']);
ksort($expected_configuration['data']['test_compression_reload_setting']);
$this->assertSame($expected_configuration, $result);
// Check that the other parameters of the view have not been negatively
// affected: we should now see page 2 in the query parameters.
$this->assertSame('2', UrlHelper::uncompressQueryParameter($parsed_url['query']['page']));
// Finally we need to make sure that the Views Reference Field module
// is still actually respecting the updated configuration returned in the
// reload. The configured override of 20 per page rather than default 5
// should still be respected here after reload of settings from the entity.
$this->assertSession()->elementsCount('css', '.views-row', 20);
}
}
name: 'Views Reference Extras'
description: 'Provides extra options for the Views Reference Field module.'
core_version_requirement: ^10 || ^11
package: Views
type: module
dependencies:
- drupal:views
- viewsreference:viewsreference
test_dependencies:
- views_ajax_history:views_ajax_history
services:
viewsreference.compression:
class: Drupal\viewsreference_extras\ViewsReferenceExtrasCompressionReload
arguments: ['@entity_type.manager', '@current_user']
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment