Problem/Motivation
I am working site that is using a REST export Views display and found that Flag (module) links that are rendered in the View do not work because they have bad CSRF tokens. When rendering the same content as a Block (or any other non-REST export display), the links work correctly. When comparing the two links, I noticed that the CSRF tokens were different depending on which display I was using.
What I expect to happen is that the View will render a working CSRF link regardless of which Display plugin is used.
In order to reproduce this without involving a contrib module, I found that the same problem can be seen when rendering comment approval links (which also require a CSRF token) in a View.
Steps to reproduce the problem using comment approval links:
- Install Drupal with Standard install profile.
- Install the following the RESTful web services and Serialization modules
- Visit
/node/add/article
and add a new Article node
- Add a new comment to the Article
- Edit the comment and set the Status to Not Published
-
Visit
/admin/structure/views/add
to create a new View
- Show: Comments
- Create a Block
- Provide REST export
- REST export path: my-api
- Click: Save and edit
-
Edit the Block display
- Remove all Filters
- Show: Fields
- Add Field: Link to approve comment
-
Edit the REST export display
- Format - Serializer Settings: json
- Show: Fields
- Save the View
-
Testing results
- Preview the Block display and note the CSRF token value in the approve link: (looks like:
token=E0yCjIOD0AWQtp3nOYdO-RMoSrhRYC2-GF_7v616wKg
)
- Preview the REST export display and note the CSRF token value in the approve link: (looks like:
token=6kNQYpaNjaFUdToL0eHPM_AQCfeosXpocZiSzmLFCBE
)
-
Attempt to visit the URL provided by the REST export approve link:
/comment/1/approve?token=6kNQYpaNjaFUdToL0eHPM_AQCfeosXpocZiSzmLFCBE
Outcome: You will see the "Access Denied" page.
-
Attempt to visit the URL provided by the Block approve link:
/comment/1/approve?token=E0yCjIOD0AWQtp3nOYdO-RMoSrhRYC2-GF_7v616wKg
Outcome: You will be redirected to the node with a message that says "Comment approved."
I'm not positive this is the right track, but I dug into the various Views Display plugins and found a difference in the order of rendering between the RestExport display plugin and the DisplayPluginBase.
\Drupal\views\Plugin\views\display\DisplayPluginBase::render
applies the cacheable metadata, and then returns a render array containing theme functions and other details about the output. This defers the actual rendering of the fields until the very end, after the cache metadata has been applied.
See \Drupal\views\Plugin\views\display\DisplayPluginBase::render
The order of operations for rendering a field using the DisplayPluginBase
looks like this:
- Style::render
- Row::render
- Display::render (cache metadata applied)
- Field::render
Whereas the \Drupal\rest\Plugin\views\display\RestExport::render
method completely renders the fields first, and then applies cache metadata.
See \Drupal\rest\Plugin\views\display\RestExport::render
The order of operations for rendering a field using the RestExport
display looks like this:
- FieldRow::render
- Style::render
- Display::render (cache metadata applied)
Proposed resolution
Seems that the final rendering of fields should be deferred until after all cache metadata is applied, but this is just a guess. Someone more familiar with all the mechanisms at play here may know better.
Remaining tasks
Unsure.
User interface changes
Probably none.
API changes
Ideally if this is fixed it wouldn't affect any existing APIs or the output of sites that already make use of the REST export display plugin.
Manual testing short cut
Here is an export of the View this issue describes for testing:
uuid: 0bcb135e-3873-4a12-a453-2f63a86538c0
langcode: en
status: true
dependencies:
module:
- comment
- node
- rest
- serialization
- user
id: test_rest_csrf
label: test-rest-csrf
module: views
description: ''
tag: ''
base_table: comment_field_data
base_field: cid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access comments'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: some
options:
items_per_page: 5
offset: 0
style:
type: default
row:
type: fields
options:
default_field_elements: true
inline: { }
separator: ''
hide_empty: false
relationships:
node:
id: node
table: comment_field_data
field: node
entity_type: comment_field_data
required: true
plugin_id: standard
relationship: none
group_type: group
admin_label: Content
fields:
approve_comment:
id: approve_comment
table: comment
field: approve_comment
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
text: Approve
entity_type: comment
plugin_id: comment_link_approve
filters: { }
sorts: { }
title: test-rest-csrf
header: { }
footer: { }
empty: { }
arguments: { }
display_extenders: { }
filter_groups:
operator: AND
groups: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- user.permissions
tags: { }
block_1:
display_plugin: block
id: block_1
display_title: Block
position: 1
display_options:
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- user.permissions
tags: { }
rest_export_1:
display_plugin: rest_export
id: rest_export_1
display_title: 'REST export'
position: 2
display_options:
display_extenders: { }
path: my-api
style:
type: serializer
options:
formats:
json: json
defaults:
style: false
row: false
row:
type: data_field
options:
field_options:
subject:
alias: ''
raw_output: false
approve_comment:
alias: ''
raw_output: false
pager:
type: some
options:
items_per_page: 10
offset: 0
auth: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- request_format
- user.permissions
tags: { }