Commit eb515644 authored by Dima Storozhuk's avatar Dima Storozhuk
Browse files

Escape CSV cell and header values before DataTables renders them

DataTables inserts cell values and column titles as HTML. Pass every value
through Drupal.checkPlain() first and run autolinking on the escaped text, so
the only markup a cell can contain is the anchor Autolinker builds itself.
Keep the plain header text for the sort control's accessible name (ariaTitle).

Also make the display behavior run on a stock site: declare core/jquery and
core/once as library dependencies (the script uses both), replace the jQuery
$.trim() calls removed in jQuery 4 with String.prototype.trim(), pass the
escape argument to fgetcsv() explicitly for PHP 8.4, and set the field type
category to file_upload as required by the field type categories introduced in
Drupal 10.2.

Add a FunctionalJavascript test for CSV values that contain markup, and update
the existing tests for DataTables 2 markup, the required search prompt and the
settings serialization. Add a GitLab CI config and a project word list.
parent e9b1f0f5
Loading
Loading
Loading
Loading
Loading
+18 −0
Changes for .cspell-project-words.txt: 18 added lines, 0 removed lines.
Original line number Diff line number Diff line
autolinker
Blankman
datatable
datatables
dtable
dtfc
dtfh
fieldable
fixedcolumns
fixedheader
floatingparent
focusability
Ginnis
langcode
papaparse
Phila
Repici
USWDS

.gitlab-ci.yml

0 → 100644
+53 −0
Changes for .gitlab-ci.yml: 53 added lines, 0 removed lines.
Original line number Diff line number Diff line
################
# GitLabCI template for Drupal projects.
#
# 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.
# As long as you include the project, ref and three files below, any future updates added by the Drupal Association will be used in your
# pipelines automatically. However, you can modify this template if you have additional needs for your project.
# The full documentation is on https://project.pages.drupalcode.org/gitlab_templates/
################

# For information on alternative values for 'ref' see https://project.pages.drupalcode.org/gitlab_templates/info/templates-version/
include:
  - 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 are defined with default values and descriptions in the file
# https://git.drupalcode.org/project/gitlab_templates/-/blob/main/includes/include.drupalci.variables.yml
################
variables:
  COMPOSER_MEMORY_LIMIT: '-1'
  # The module supports Drupal 10 and 11: test the previous major as well.
  OPT_IN_TEST_PREVIOUS_MAJOR: '1'
  # Test with both the minimum and the maximum supported PHP versions.
  OPT_IN_TEST_MAX_PHP: '1'
  # Run those variants automatically on merge request pipelines, not only when
  # triggered manually.
  _AUTORUN_PREVIOUS_MAJOR: 'push-and-mr'
  _AUTORUN_MAX_PHP: 'push-and-mr'

################
# cspell, phpcs and phpstan pass today, so keep them blocking. eslint and
# stylelint report pre-existing findings in code this change does not touch,
# so they are allowed to fail until those are cleaned up separately.
################
cspell:
  allow_failure: false

phpcs:
  allow_failure: false

phpstan:
  allow_failure: false

eslint:
  allow_failure: true

stylelint:
  allow_failure: true
+2 −0
Changes for csv_field.libraries.yml: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -6,6 +6,8 @@ csv_field:
    js/csv-field.js: {}
  dependencies:
    - core/drupal
    - core/jquery
    - core/once
    - csv_field/datatables_cdn
    - csv_field/datatables_fixedheader
    - csv_field/datatables_fixedcolumns
+47 −12
Changes for js/csv-field.js: 47 added lines, 12 removed lines.
Original line number Diff line number Diff line
(function ($, Drupal, once) {
  /**
   * Normalizes a parsed CSV value to a string.
   */
  const csvValueToText = (value) => {
    if (value === null || typeof value === 'undefined') {
      return '';
    }
    return String(value);
  };

  /**
   * Escapes a CSV value for insertion into HTML.
   *
   * DataTables inserts cell values and column titles as HTML, so everything
   * that comes out of the uploaded file must be escaped before it reaches
   * DataTables. Autolinking runs on the escaped text, so the only markup
   * that can end up in a cell is the anchor Autolinker builds itself.
   */
  const escapeCsvValue = (value) => Drupal.checkPlain(csvValueToText(value));

  Drupal.behaviors.csvDatatables = {
    attach: function attach(context, settings) {
      $(once('csv-table-processed', "div.csv-table", context)).each(function () {
@@ -66,17 +86,38 @@

          if (div.classList.contains("dataTable")) {
            tableSettings.columns = [];
            var headerData = parsed.data.shift();
            tableSettings.data = parsed.data;

            // Escapes a cell and, when enabled, turns the URLs in it into
            // links. URL columns are only escaped here: they are combined
            // with their text column below.
            var prepareCell = function (value, colNumber) {
              value = escapeCsvValue(value);
              if (autolink && urlColumnNumbers.indexOf(colNumber) === -1) {
                value = autolink.link(value);
              }
              return value;
            };

            var rawHeader = parsed.data.shift() || [];
            // Plain header text, used for the accessible names of the columns.
            var headerText = rawHeader.map(csvValueToText);
            var headerData = rawHeader.map(prepareCell);
            tableSettings.data = parsed.data.map(function (row) {
              return row.map(prepareCell);
            });

            if (tableSettings.data.length > 100) {
              tableSettings.deferRender = true;
            }

            tableSettings.data = parsed.data;

            for (var i = 0; i < headerData.length; i++) {
              tableSettings.columns.push({ title: headerData[i] });
              tableSettings.columns.push({
                title: headerData[i],
                // DataTables reuses the title for the sort control's
                // aria-label, which would expose the escaped form; give it
                // the plain text.
                ariaTitle: headerText[i],
              });
            }

            // Create links in table
@@ -121,6 +162,7 @@
                    urlColIndex < tableSettings.columns.length) {
                  // Update the URL column header with the text column's title
                  tableSettings.columns[urlColIndex].title = tableSettings.columns[textColIndex].title;
                  tableSettings.columns[urlColIndex].ariaTitle = tableSettings.columns[textColIndex].ariaTitle;
                  // Remove the text column header (columns to the left keep their indices)
                  tableSettings.columns.splice(textColIndex, 1);
                }
@@ -203,13 +245,6 @@
        Papa.parse(link.href, {
          download: true,
          skipEmptyLines: true,
          transform: function (value, colNumber) {
            // Skip autolinking for URL columns (they'll be processed later with custom labels)
            if (autolink && urlColumnNumbers.indexOf(colNumber) === -1) {
              value = autolink.link(value);
            }
            return value;
          },
          complete: (parsed) => renderTable(parsed),
        });
      });
+1 −1
Changes for src/Plugin/Field/FieldType/CsvFileItem.php: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -13,7 +13,7 @@ use Drupal\file\Plugin\Field\FieldType\FileItem;
 *   id = "csv_file",
 *   label = @Translation("CSV File"),
 *   description = @Translation("This field stores the ID of a CSV file as an integer value."),
 *   category = @Translation("Reference"),
 *   category = "file_upload",
 *   default_widget = "csv_file_generic",
 *   default_formatter = "csv_file_table",
 *   list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList",
Loading