Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions web/themes/contrib/civictheme/civictheme.theme
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
declare(strict_types=1);

use Drupal\civictheme\CivicthemeConstants;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Render\Component\Exception\ComponentNotFoundException;

require_once __DIR__ . '/includes/utilities.inc';
Expand Down Expand Up @@ -161,6 +162,7 @@ function civictheme_preprocess_last(array &$variables, string $hook): void {
function civictheme_preprocess_html(array &$variables): void {
_civictheme_preprocess_html__skip_link($variables);
_civictheme_preprocess_html__site_section($variables);
_civictheme_preprocess_html__search_head_title($variables);

// Disable modifier_class as this template is provided by Drupal.
$variables['modifier_class'] = FALSE;
Expand Down Expand Up @@ -275,4 +277,5 @@ function civictheme_page_attachments_alter(array &$attachments) {
catch (ComponentNotFoundException $exception) {
\Drupal::logger('civictheme')->error('Unable to find alert component: %message', ['%message' => $exception->getMessage()]);
}
$attachments['#cache']['contexts'] = Cache::mergeContexts($attachments['#cache']['contexts'] ?? [], ['url.query_args']);
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ components:
summary_length: 160
attachment:
use_media_name: true
search:
keyword_fields:
- keywords
- title
colors:
use_color_selector: true
use_brand_colors: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ display:
admin_label: ''
plugin_id: result
empty: false
content: 'Showing @start - @end of @total'
content: 'Showing @start - @end of @total @keywords'
footer: { }
display_extenders: { }
cache_metadata:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,19 +218,16 @@ display:
query_tags: { }
relationships: { }
header:
area:
id: area
result:
id: result
table: views
field: area
field: result
relationship: none
group_type: group
admin_label: ''
plugin_id: text
plugin_id: result
empty: false
content:
value: '<h3 class="ct-heading ct-list__title">Search results...</h3>'
format: civictheme_rich_text
tokenize: false
content: 'Showing @start - @end of @total @keywords'
footer: { }
display_extenders: { }
cache_metadata:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,15 @@ civictheme.settings:
use_media_name:
label: 'Use name of media'
type: boolean
search:
type: mapping
label: 'Search settings'
mapping:
keyword_fields:
label: 'Keyword fields'
type: sequence
sequence:
type: string
colors:
type: mapping
label: Colors
Expand Down
1 change: 1 addition & 0 deletions web/themes/contrib/civictheme/includes/form_element.inc
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ function _civictheme_preprocess_form_element__generic(array &$variables): void {
$title_display = 'hidden';
}
$variables['title_display'] = $title_display;
$variables['title_size'] = $element['#title_size'] ?? '';

$variables['orientation'] = $variables['orientation'] ?? $title_display === 'inline' ? 'horizontal' : 'vertical';

Expand Down
36 changes: 36 additions & 0 deletions web/themes/contrib/civictheme/includes/search.inc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
declare(strict_types=1);

use Drupal\civictheme\CivicthemeConstants;
use Drupal\civictheme\CivicthemeUtility;

/**
* Implements template_preprocess_block().
Expand Down Expand Up @@ -36,3 +37,38 @@ function civictheme_preprocess_block__civictheme_search(array &$variables): void

$variables['theme'] = civictheme_get_theme_config_manager()->load('components.header.theme', CivicthemeConstants::HEADER_THEME_DEFAULT);
}

/**
* Implements hook_preprocess_html().
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/
function _civictheme_preprocess_html__search_head_title(array &$variables): void {
// Load search fields from theme settings.
$setting_keyword_fields = civictheme_get_theme_config_manager()->load('components.search.keyword_fields') ?? '';
if (empty($setting_keyword_fields)) {
return;
}

// Add search keywords to head title.
$keywords = NULL;
$search_fields = CivicthemeUtility::multilineToArray($setting_keyword_fields);
foreach ($search_fields as $field) {
$query_field = \Drupal::request()->query->get($field);
if (!empty($query_field)) {
$keywords = $query_field;
break;
}
}
Comment on lines +56 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add input validation and error handling for query parameters.

The code retrieves query parameters without validation or error handling, which could lead to issues:

  1. No input validation: Query parameter values are not validated for length or type. A malicious user could pass extremely long strings that might cause memory issues or unexpected behavior.
  2. No null-safety: If \Drupal::request() returns null (though rare), this will cause a fatal error.

Apply this diff to add validation:

   $keywords = NULL;
   $search_fields = CivicthemeUtility::multilineToArray($setting_keyword_fields);
   foreach ($search_fields as $field) {
-    $query_field = \Drupal::request()->query->get($field);
+    $request = \Drupal::request();
+    if (!$request) {
+      continue;
+    }
+    $query_field = $request->query->get($field);
     if (!empty($query_field)) {
+      // Limit keyword length to prevent abuse.
+      $query_field = is_string($query_field) ? mb_substr($query_field, 0, 255) : '';
       $keywords = $query_field;
       break;
     }
   }
🤖 Prompt for AI Agents
In web/themes/contrib/civictheme/includes/search.inc around lines 54 to 60, the
code reads query parameters without null-safety or validation; update it to
first ensure \Drupal::request() returns a Request object
(null-check/type-check), then retrieve each $field value safely, ensure it is a
string, trim it, enforce a maximum length (e.g. 1024 chars) and optional
pattern/type checks, and only assign $keywords and break when the value passes
validation; if validation fails, skip that param and optionally log a warning or
set a safe default so no fatal error or unbounded input is accepted.

if (empty($keywords)) {
return;
}
$head_title = (string) ($variables['head_title']['title'] ?? '');
if (empty($head_title)) {
return;
}
$variables['head_title']['title'] = t("@title - Searching for '@keywords'", [
'@title' => $head_title,
'@keywords' => $keywords,
]);
}
84 changes: 84 additions & 0 deletions web/themes/contrib/civictheme/includes/views.inc
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
declare(strict_types=1);

use Drupal\civictheme\CivicthemeConstants;
use Drupal\civictheme\CivicthemeUtility;
use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Template\Attribute;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;

/**
Expand All @@ -21,6 +23,7 @@ function civictheme_preprocess_views_view(array &$variables): void {
_civictheme_preprocess_views_view__view($variables);
_civictheme_preprocess_views_view__pager($variables);
_civictheme_preprocess_views_view__search_page($variables);
_civictheme_preprocess_views_view__results_count($variables);
}

/**
Expand Down Expand Up @@ -160,13 +163,57 @@ function civictheme_preprocess_views_exposed_form(array &$variables): void {
}

if ($field_count == 1) {
// Use inline filter on search and auto pages if a single text field exists.
if (_civictheme_preprocess_views__exposed_form__inline_filter($variables, $view, $fields)) {
return;
}

// Use single filter.
_civictheme_preprocess_views__exposed_form__single_filter($variables);
}
elseif ($field_count > 1) {
_civictheme_preprocess_views__exposed_form__group_filter($variables);
}
}

/**
* Preprocess views exposed form to convert it to the Inline filter.
*
* @param array $variables
* Variables array.
* @param \Drupal\views\ViewExecutable|null $view
* The view object.
* @param array $fields
* Form fields.
*
* @return bool
* TRUE if inline filter was processed, FALSE otherwise.
*/
function _civictheme_preprocess_views__exposed_form__inline_filter(array &$variables, $view, array $fields): bool {
if (!($view instanceof ViewExecutable)) {
return FALSE;
}

if (!in_array($view->id(), ['civictheme_search', 'civictheme_automated_list'])) {
return FALSE;
}

$keyword_field = reset($fields);
if ($keyword_field['#type'] !== 'textfield') {
return FALSE;
}

$variables['inline_filter'] = TRUE;
$keyword_field['#title_size'] = 'extra-large';
$variables['filter_items'] = $keyword_field;
$submit_field = $variables['form']['actions']['submit'] ?? NULL;
if (!empty($submit_field)) {
$variables['submit_text'] = $submit_field['#value'] ?? '';
}

return TRUE;
}

/**
* Preprocess views exposed form to convert it to the Single filter.
*/
Expand Down Expand Up @@ -261,3 +308,40 @@ function _civictheme_preprocess_views_view__search_page(array &$variables): void
$variables['vertical_spacing'] = 'top';
}
}

/**
* Pre-process results count for views.
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/
function _civictheme_preprocess_views_view__results_count(array &$variables): void {
if (empty($variables['results_count'])) {
return;
}
if (!str_contains($variables['results_count'], '@keywords')) {
return;
}

// Load search fields from theme settings.
$setting_keyword_fields = civictheme_get_theme_config_manager()->load('components.search.keyword_fields') ?? '';
if (empty($setting_keyword_fields)) {
// Strip the @keywords token because it exists but won't be populated.
$variables['results_count'] = str_replace('@keywords', '', $variables['results_count']);
return;
}

$variables['#cache']['contexts'] = Cache::mergeContexts($variables['#cache']['contexts'] ?? [], ['url.query_args']);

$keywords = '';
$exposed_input = $variables['view']->getExposedInput();
$search_fields = CivicthemeUtility::multilineToArray($setting_keyword_fields);

foreach ($search_fields as $field) {
if (!empty($exposed_input[$field])) {
$keywords = t("for '@keywords'", ['@keywords' => $exposed_input[$field]])->__toString();
break;
}
}

$variables['results_count'] = str_replace('@keywords', $keywords, $variables['results_count']);
}
Comment on lines +317 to +347
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Guard against missing view object.

Line 335 calls $variables['view']->getExposedInput() without verifying that $variables['view'] exists, which could cause a fatal error.

Apply this diff to add a guard:

   if (!str_contains($variables['results_count'], '@keywords')) {
     return;
   }
+  if (empty($variables['view'])) {
+    $variables['results_count'] = str_replace('@keywords', '', $variables['results_count']);
+    return;
+  }
 
   // Load search fields from theme settings.

Verify the keyword extraction logic.

Lines 338-343 extract keywords from the first matching field and break immediately. If multiple search fields are configured (e.g., "keywords" and "title"), only the first field with a value will be used. Please confirm this is the intended behavior rather than concatenating or prioritizing keywords.

Run the following script to check how keyword_fields are configured and used:


🏁 Script executed:

#!/bin/bash
# Description: Check keyword_fields configuration and usage

# Find theme settings configuration
fd -e yml -e yaml . | xargs rg -l "keyword_fields" | head -5

# Check how keyword_fields are used in preprocessing
rg -A 5 "keyword_fields" --type=php

Length of output: 2731


🏁 Script executed:

#!/bin/bash
# Inspect default config for keyword_fields
echo "== YAML snippet for keyword_fields =="
rg -n "keyword_fields" -C2 web/themes/contrib/civictheme/config/install/civictheme.settings.yml

# Locate multilineToArray implementation
echo "== multilineToArray implementation =="
rg -n "function multilineToArray" -C5 web/themes/contrib/civictheme/src/Utility

Length of output: 258


Add guard for missing view and confirm keyword field priority.

  • After the str_contains check, guard against a missing view to avoid a fatal error:
   if (!str_contains($variables['results_count'], '@keywords')) {
     return;
   }
+  if (empty($variables['view'])) {
+    $variables['results_count'] = str_replace('@keywords', '', $variables['results_count']);
+    return;
+  }
  • Confirm that only the first non-empty field from components.search.keyword_fields is intended to replace @keywords rather than combining multiple fields.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function _civictheme_preprocess_views_view__results_count(array &$variables): void {
if (empty($variables['results_count'])) {
return;
}
if (!str_contains($variables['results_count'], '@keywords')) {
return;
}
// Load search fields from theme settings.
$setting_keyword_fields = civictheme_get_theme_config_manager()->load('components.search.keyword_fields') ?? '';
if (empty($setting_keyword_fields)) {
// Strip the @keywords token because it exists but won't be populated.
$variables['results_count'] = str_replace('@keywords', '', $variables['results_count']);
return;
}
$variables['#cache']['contexts'] = Cache::mergeContexts($variables['#cache']['contexts'] ?? [], ['url.query_args']);
$keywords = '';
$exposed_input = $variables['view']->getExposedInput();
$search_fields = CivicthemeUtility::multilineToArray($setting_keyword_fields);
foreach ($search_fields as $field) {
if (!empty($exposed_input[$field])) {
$keywords = t("for '@keywords'", ['@keywords' => $exposed_input[$field]])->__toString();
break;
}
}
$variables['results_count'] = str_replace('@keywords', $keywords, $variables['results_count']);
}
function _civictheme_preprocess_views_view__results_count(array &$variables): void {
if (empty($variables['results_count'])) {
return;
}
if (!str_contains($variables['results_count'], '@keywords')) {
return;
}
if (empty($variables['view'])) {
// Strip the @keywords token because no view is available to provide keywords.
$variables['results_count'] = str_replace('@keywords', '', $variables['results_count']);
return;
}
// Load search fields from theme settings.
$setting_keyword_fields = civictheme_get_theme_config_manager()->load('components.search.keyword_fields') ?? '';
if (empty($setting_keyword_fields)) {
// Strip the @keywords token because it exists but won't be populated.
$variables['results_count'] = str_replace('@keywords', '', $variables['results_count']);
return;
}
$variables['#cache']['contexts'] = Cache::mergeContexts($variables['#cache']['contexts'] ?? [], ['url.query_args']);
$keywords = '';
$exposed_input = $variables['view']->getExposedInput();
$search_fields = CivicthemeUtility::multilineToArray($setting_keyword_fields);
foreach ($search_fields as $field) {
if (!empty($exposed_input[$field])) {
$keywords = t("for '@keywords'", ['@keywords' => $exposed_input[$field]])->__toString();
break;
}
}
$variables['results_count'] = str_replace('@keywords', $keywords, $variables['results_count']);
}
🤖 Prompt for AI Agents
In web/themes/contrib/civictheme/includes/views.inc around lines 316 to 346, add
a null/instance guard after the str_contains check to ensure $variables['view']
exists and is an object before calling getExposedInput() to avoid a fatal error
(return early if missing), and make the replacement behavior explicit: either
keep the current logic which uses only the first non-empty field from
components.search.keyword_fields (document this with a short inline comment) or,
if the intent is to combine multiple fields, change the loop to accumulate
non-empty fields into $keywords (joined with a separator) and use that combined
string when replacing @keywords; also preserve cache context merging and the
existing token stripping fallback.

2 changes: 1 addition & 1 deletion web/themes/contrib/civictheme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"@civictheme/uikit": "github:civictheme/uikit#312d7574a273a69a33655210a81406b97354471e"
"@civictheme/uikit": "github:civictheme/uikit#bd1d7944ff7f1030359d31c61c9dd88f64038d8d"
},
"devDependencies": {
"@civictheme/scss-variables-extractor": "^0.2.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,21 @@ public function form(array &$form, FormStateInterface $form_state): void {
'#default_value' => $this->themeConfigManager->loadForComponent('attachment', 'use_media_name', TRUE),
];

$form['components']['search'] = [
'#type' => 'details',
'#title' => $this->t('Search'),
'#group' => 'components',
'#tree' => TRUE,
];

$form['components']['search']['keyword_fields'] = [
'#type' => 'textarea',
'#title' => $this->t('Search and Automated list keyword fields'),
'#description' => $this->t('A list of machine names for views exposed filters keyword search.<br/>Fields added here will populate the "Showing @start - @end of @total @keywords" where @keywords is replaced with "for [keyword_field_value]" if a value exists.<br/>It will also be used in the browser page title as "@title - Searching for \'@keywords\' | @suffix".<br/>One keyword field per line.'),
'#default_value' => CivicthemeUtility::arrayToMultiline($this->themeConfigManager->load('components.search.keyword_fields', [])),
'#rows' => 4,
];

$form['#process'][] = $this->processForm(...);

// Auto-discover per-component validation and submit handlers.
Expand Down Expand Up @@ -588,6 +603,18 @@ public function submitFooter(array &$form, FormStateInterface $form_state): void
);
}

/**
* Submit callback for theme settings form of Search component.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @SuppressWarnings(PHPMD.StaticAccess)
*/
public function submitSearch(array &$form, FormStateInterface $form_state): void {
$keyword_fields = $form_state->getValue(['components', 'search', 'keyword_fields'], '');
$keyword_fields = CivicthemeUtility::multilineToArray($keyword_fields);
$form_state->setValue(['components', 'search', 'keyword_fields'], $keyword_fields);
}

/**
* Provide a description for a path field.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
theme: theme,
title: title,
title_display: title_display,
title_size: title_size,
type: type,
orientation: orientation,
placeholder: placeholder,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
form_attributes: form_attributes,
form_suffix: form_suffix,
} %}
{% elseif inline_filter %}
{% include 'civictheme:inline-filter' with {
theme: theme,
items: filter_items,
} only %}
Comment on lines +32 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify inline-filter component exists and preprocessing sets inline_filter variable.

The template now includes a civictheme:inline-filter component. Ensure:

  1. The component exists in the updated UIKit dependency
  2. The inline_filter variable is properly set in includes/views.inc preprocessing
  3. The filter_items variable is populated with the correct structure expected by the component

🏁 Script executed:

#!/bin/bash
# Check if inline-filter component exists in the components directory after uikit-install
fd -t d "inline-filter" components/

# Search for inline_filter variable assignment in views preprocessing
rg -n --type=php "inline_filter" web/themes/contrib/civictheme/includes/views.inc -C5

Length of output: 713


🏁 Script executed:

#!/bin/bash
# Locate directories named inline-filter
fd -t d inline-filter

# Locate twig templates named inline-filter
fd -t f "*inline-filter*.twig"

# Find any files referencing inline-filter
rg -l "inline-filter"

Length of output: 374


🏁 Script executed:

#!/bin/bash
# Search for inline-filter or inline_filter twig templates in the theme
find web/themes/contrib/civictheme/templates -type f \( -iname "*inline-filter*.twig" -o -iname "*inline_filter*.twig" \)

Length of output: 0


Add missing inline-filter component template
The include in web/themes/contrib/civictheme/templates/views/views-exposed-form.html.twig references civictheme:inline-filter, but no corresponding Twig template or component directory exists. Add a templates/components/inline-filter.html.twig (or your project’s component path) to define this component. Variables inline_filter and filter_items are already set in includes/views.inc.

🤖 Prompt for AI Agents
In web/themes/contrib/civictheme/templates/views/views-exposed-form.html.twig
around lines 32 to 36 the template includes "civictheme:inline-filter" but no
component template exists; create a new Twig component at
web/themes/contrib/civictheme/templates/components/inline-filter.html.twig (or
your project's component path) that accepts the passed-in variables (theme and
filter_items / inline_filter), loops over filter_items to render each filter
control with appropriate markup/classes (respect theme if needed), and returns
the expected HTML structure used by the views-exposed-form include; after adding
the file clear Drupal caches so the new component is discoverable.

{% else %}
{{ form }}
{% endif %}