mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
add HTML rendering on config modal with tests
Models/Plugin will now sanitize "description" and "help text" before loading. This allows HTML from these fields to be rendered safely. Sanitization is done using Purify library for completeness (new dependency). A test suite of simple xss attacks is also added.
This commit is contained in:
parent
9019561bb3
commit
46e792bc6d
6 changed files with 470 additions and 158 deletions
|
|
@ -62,6 +62,11 @@ class Plugin extends Model
|
|||
$model->current_image = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Sanitize configuration template on save
|
||||
static::saving(function ($model): void {
|
||||
$model->sanitizeTemplate();
|
||||
});
|
||||
}
|
||||
|
||||
public function user()
|
||||
|
|
@ -69,6 +74,25 @@ class Plugin extends Model
|
|||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
// sanitize configuration template descriptions and help texts (since they allow HTML rendering)
|
||||
protected function sanitizeTemplate(): void
|
||||
{
|
||||
$template = $this->configuration_template;
|
||||
|
||||
if (isset($template['custom_fields']) && is_array($template['custom_fields'])) {
|
||||
foreach ($template['custom_fields'] as &$field) {
|
||||
if (isset($field['description'])) {
|
||||
$field['description'] = \Stevebauman\Purify\Facades\Purify::clean($field['description']);
|
||||
}
|
||||
if (isset($field['help_text'])) {
|
||||
$field['help_text'] = \Stevebauman\Purify\Facades\Purify::clean($field['help_text']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->configuration_template = $template;
|
||||
}
|
||||
}
|
||||
|
||||
public function hasMissingRequiredConfigurationFields(): bool
|
||||
{
|
||||
if (! isset($this->configuration_template['custom_fields']) || empty($this->configuration_template['custom_fields'])) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
"livewire/volt": "^1.7",
|
||||
"om/icalparser": "^3.2",
|
||||
"spatie/browsershot": "^5.0",
|
||||
"stevebauman/purify": "^6.3",
|
||||
"symfony/yaml": "^7.3",
|
||||
"wnx/sidecar-browsershot": "^2.6"
|
||||
},
|
||||
|
|
|
|||
129
composer.lock
generated
129
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "4d958d48655a5ad9e3de6b4a9fb52b0a",
|
||||
"content-hash": "25c2a1a4a2f2594adefe25ddb6a072fb",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
|
|
@ -814,6 +814,67 @@
|
|||
],
|
||||
"time": "2025-03-06T22:45:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ezyang/htmlpurifier",
|
||||
"version": "v4.19.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ezyang/htmlpurifier.git",
|
||||
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf",
|
||||
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cerdic/css-tidy": "^1.7 || ^2.0",
|
||||
"simpletest/simpletest": "dev-master"
|
||||
},
|
||||
"suggest": {
|
||||
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
|
||||
"ext-bcmath": "Used for unit conversion and imagecrash protection",
|
||||
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
|
||||
"ext-tidy": "Used for pretty-printing HTML"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"library/HTMLPurifier.composer.php"
|
||||
],
|
||||
"psr-0": {
|
||||
"HTMLPurifier": "library/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/library/HTMLPurifier/Language/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Edward Z. Yang",
|
||||
"email": "admin@htmlpurifier.org",
|
||||
"homepage": "http://ezyang.com"
|
||||
}
|
||||
],
|
||||
"description": "Standards compliant HTML filter written in PHP",
|
||||
"homepage": "http://htmlpurifier.org/",
|
||||
"keywords": [
|
||||
"html"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/ezyang/htmlpurifier/issues",
|
||||
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0"
|
||||
},
|
||||
"time": "2025-10-17T16:34:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "firebase/php-jwt",
|
||||
"version": "v6.11.1",
|
||||
|
|
@ -4947,6 +5008,72 @@
|
|||
],
|
||||
"time": "2025-01-13T13:04:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "stevebauman/purify",
|
||||
"version": "v6.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/stevebauman/purify.git",
|
||||
"reference": "3acb5e77904f420ce8aad8fa1c7f394e82daa500"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/stevebauman/purify/zipball/3acb5e77904f420ce8aad8fa1c7f394e82daa500",
|
||||
"reference": "3acb5e77904f420ce8aad8fa1c7f394e82daa500",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ezyang/htmlpurifier": "^4.17",
|
||||
"illuminate/contracts": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||
"php": ">=7.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0",
|
||||
"phpunit/phpunit": "^8.0|^9.0|^10.0|^11.5.3"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"Purify": "Stevebauman\\Purify\\Facades\\Purify"
|
||||
},
|
||||
"providers": [
|
||||
"Stevebauman\\Purify\\PurifyServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Stevebauman\\Purify\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Steve Bauman",
|
||||
"email": "steven_bauman@outlook.com"
|
||||
}
|
||||
],
|
||||
"description": "An HTML Purifier / Sanitizer for Laravel",
|
||||
"keywords": [
|
||||
"Purifier",
|
||||
"clean",
|
||||
"cleaner",
|
||||
"html",
|
||||
"laravel",
|
||||
"purification",
|
||||
"purify"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/stevebauman/purify/issues",
|
||||
"source": "https://github.com/stevebauman/purify/tree/v6.3.1"
|
||||
},
|
||||
"time": "2025-05-21T16:53:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/clock",
|
||||
"version": "v8.0.0",
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@
|
|||
@apply !mb-0 !leading-tight;
|
||||
}
|
||||
|
||||
[data-flux-description] a {
|
||||
@apply text-accent underline hover:opacity-80;
|
||||
}
|
||||
|
||||
input:focus[data-flux-control],
|
||||
textarea:focus[data-flux-control],
|
||||
select:focus[data-flux-control] {
|
||||
|
|
|
|||
|
|
@ -639,6 +639,13 @@ HTML;
|
|||
@php
|
||||
$fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name'];
|
||||
$rawValue = $configuration[$fieldKey] ?? ($field['default'] ?? '');
|
||||
# These are sanitized at PluginImportService when imported, safe to render HTML
|
||||
$safeDescription = $field['description'] ?? '';
|
||||
$safeHelp = $field['help_text'] ?? '';
|
||||
|
||||
//Important: Sanitize with Purify to prevent XSS attacks
|
||||
// $safeDescription = Stevebauman\Purify\Facades\Purify::clean($field['description'] ?? '');
|
||||
// $safeHelp = Stevebauman\Purify\Facades\Purify::clean($field['help_text'] ?? '');
|
||||
|
||||
// For code fields, if the value is an array, JSON encode it
|
||||
if ($field['field_type'] === 'code' && is_array($rawValue)) {
|
||||
|
|
@ -657,176 +664,211 @@ HTML;
|
|||
@endif
|
||||
|
||||
@if($field['field_type'] === 'string' || $field['field_type'] === 'url')
|
||||
<flux:input
|
||||
label="{{ $field['name'] }}"
|
||||
description="{{ $field['description'] ?? '' }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
/>
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:input
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
/>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
@elseif($field['field_type'] === 'text')
|
||||
<flux:textarea
|
||||
label="{{ $field['name'] }}"
|
||||
description="{{ $field['description'] ?? '' }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
/>
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:textarea
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
/>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
@elseif($field['field_type'] === 'code')
|
||||
<flux:textarea
|
||||
label="{{ $field['name'] }}"
|
||||
description="{{ $field['description'] ?? '' }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
rows="{{ $field['rows'] ?? 3 }}"
|
||||
placeholder="{{ $field['placeholder'] ?? null }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
class="font-mono"
|
||||
/>
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:textarea
|
||||
rows="{{ $field['rows'] ?? 3 }}"
|
||||
placeholder="{{ $field['placeholder'] ?? null }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
class="font-mono"
|
||||
/>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
@elseif($field['field_type'] === 'password')
|
||||
<flux:input
|
||||
type="password"
|
||||
label="{{ $field['name'] }}"
|
||||
description="{{ $field['description'] ?? '' }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
viewable
|
||||
/>
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:input
|
||||
type="password"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
viewable
|
||||
/>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
@elseif($field['field_type'] === 'copyable')
|
||||
<flux:input
|
||||
label="{{ $field['name'] }}"
|
||||
description="{{ $field['description'] ?? '' }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
value="{{ $field['value'] }}"
|
||||
copyable
|
||||
/>
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:input
|
||||
value="{{ $field['value'] }}"
|
||||
copyable
|
||||
/>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
@elseif($field['field_type'] === 'time_zone')
|
||||
<flux:select
|
||||
label="{{ $field['name'] }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
description="{{ $field['description'] ?? '' }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
>
|
||||
<option value="">Select timezone...</option>
|
||||
@foreach(timezone_identifiers_list() as $timezone)
|
||||
<option value="{{ $timezone }}" {{ $currentValue === $timezone ? 'selected' : '' }}>{{ $timezone }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:select
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $field['value'] }}"
|
||||
>
|
||||
<option value="">Select timezone...</option>
|
||||
@foreach(timezone_identifiers_list() as $timezone)
|
||||
<option value="{{ $timezone }}" {{ $currentValue === $timezone ? 'selected' : '' }}>{{ $timezone }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
@elseif($field['field_type'] === 'number')
|
||||
<flux:input
|
||||
type="number"
|
||||
label="{{ $field['name'] }}"
|
||||
description="{{ $field['description'] ?? $field['name'] }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
/>
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:input
|
||||
type="number"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
/>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
@elseif($field['field_type'] === 'boolean')
|
||||
<flux:checkbox
|
||||
label="{{ $field['name'] }}"
|
||||
description="{{ $field['description'] ?? $field['name'] }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
:checked="$currentValue"
|
||||
/>
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:checkbox
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
:checked="$currentValue"
|
||||
/>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
@elseif($field['field_type'] === 'date')
|
||||
<flux:input
|
||||
type="date"
|
||||
label="{{ $field['name'] }}"
|
||||
description="{{ $field['description'] ?? $field['name'] }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
/>
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:input
|
||||
type="date"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
/>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
@elseif($field['field_type'] === 'time')
|
||||
<flux:input
|
||||
type="time"
|
||||
label="{{ $field['name'] }}"
|
||||
description="{{ $field['description'] ?? $field['name'] }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
/>
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:input
|
||||
type="time"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
/>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
@elseif($field['field_type'] === 'select')
|
||||
@if(isset($field['multiple']) && $field['multiple'] === true)
|
||||
<flux:checkbox.group
|
||||
label="{{ $field['name'] }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
description="{{ $field['description'] ?? '' }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
>
|
||||
@if(isset($field['options']) && is_array($field['options']))
|
||||
@foreach($field['options'] as $option)
|
||||
@if(is_array($option))
|
||||
@foreach($option as $label => $value)
|
||||
<flux:checkbox label="{{ $label }}" value="{{ $value }}"/>
|
||||
@endforeach
|
||||
@else
|
||||
@php
|
||||
$key = mb_strtolower(str_replace(' ', '_', $option));
|
||||
@endphp
|
||||
<flux:checkbox label="{{ $option }}" value="{{ $key }}"/>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
</flux:checkbox.group>
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:checkbox.group wire:model="configuration.{{ $fieldKey }}">
|
||||
@if(isset($field['options']) && is_array($field['options']))
|
||||
@foreach($field['options'] as $option)
|
||||
@if(is_array($option))
|
||||
@foreach($option as $label => $value)
|
||||
<flux:checkbox label="{{ $label }}" value="{{ $value }}"/>
|
||||
@endforeach
|
||||
@else
|
||||
@php
|
||||
$key = mb_strtolower(str_replace(' ', '_', $option));
|
||||
@endphp
|
||||
<flux:checkbox label="{{ $option }}" value="{{ $key }}"/>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
</flux:checkbox.group>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
@else
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:select wire:model="configuration.{{ $fieldKey }}">
|
||||
<option value="">Select {{ $field['name'] }}...</option>
|
||||
@if(isset($field['options']) && is_array($field['options']))
|
||||
@foreach($field['options'] as $option)
|
||||
@if(is_array($option))
|
||||
@foreach($option as $label => $value)
|
||||
<option value="{{ $value }}" {{ $currentValue === $value ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
@else
|
||||
@php
|
||||
$key = mb_strtolower(str_replace(' ', '_', $option));
|
||||
@endphp
|
||||
<option value="{{ $key }}" {{ $currentValue === $key ? 'selected' : '' }}>{{ $option }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
</flux:select>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
@endif
|
||||
|
||||
@elseif($field['field_type'] === 'xhrSelect')
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:select
|
||||
label="{{ $field['name'] }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
description="{{ $field['description'] ?? '' }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
wire:init="loadXhrSelectOptions('{{ $fieldKey }}', '{{ $field['endpoint'] }}')"
|
||||
>
|
||||
<option value="">Select {{ $field['name'] }}...</option>
|
||||
@if(isset($field['options']) && is_array($field['options']))
|
||||
@foreach($field['options'] as $option)
|
||||
@if(isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey]))
|
||||
@foreach($xhrSelectOptions[$fieldKey] as $option)
|
||||
@if(is_array($option))
|
||||
@foreach($option as $label => $value)
|
||||
<option value="{{ $value }}" {{ $currentValue === $value ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
@if(isset($option['id']) && isset($option['name']))
|
||||
{{-- xhrSelectSearch format: { 'id' => 'db-456', 'name' => 'Team Goals' } --}}
|
||||
<option value="{{ $option['id'] }}" {{ $currentValue === (string)$option['id'] ? 'selected' : '' }}>{{ $option['name'] }}</option>
|
||||
@else
|
||||
{{-- xhrSelect format: { 'Braves' => 123 } --}}
|
||||
@foreach($option as $label => $value)
|
||||
<option value="{{ $value }}" {{ $currentValue === (string)$value ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
@endif
|
||||
@else
|
||||
@php
|
||||
$key = mb_strtolower(str_replace(' ', '_', $option));
|
||||
@endphp
|
||||
<option value="{{ $key }}" {{ $currentValue === $key ? 'selected' : '' }}>{{ $option }}</option>
|
||||
<option value="{{ $option }}" {{ $currentValue === (string)$option ? 'selected' : '' }}>{{ $option }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
</flux:select>
|
||||
@endif
|
||||
@elseif($field['field_type'] === 'xhrSelect')
|
||||
<flux:select
|
||||
label="{{ $field['name'] }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
description="{{ $field['description'] ?? '' }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? '' }}"
|
||||
wire:init="loadXhrSelectOptions('{{ $fieldKey }}', '{{ $field['endpoint'] }}')"
|
||||
>
|
||||
<option value="">Select {{ $field['name'] }}...</option>
|
||||
@if(isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey]))
|
||||
@foreach($xhrSelectOptions[$fieldKey] as $option)
|
||||
@if(is_array($option))
|
||||
@if(isset($option['id']) && isset($option['name']))
|
||||
{{-- xhrSelectSearch format: { 'id' => 'db-456', 'name' => 'Team Goals' } --}}
|
||||
<option value="{{ $option['id'] }}" {{ $currentValue === (string)$option['id'] ? 'selected' : '' }}>{{ $option['name'] }}</option>
|
||||
@else
|
||||
{{-- xhrSelect format: { 'Braves' => 123 } --}}
|
||||
@foreach($option as $label => $value)
|
||||
<option value="{{ $value }}" {{ $currentValue === (string)$value ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
@endif
|
||||
@else
|
||||
<option value="{{ $option }}" {{ $currentValue === (string)$option ? 'selected' : '' }}>{{ $option }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
</flux:select>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
@elseif($field['field_type'] === 'xhrSelectSearch')
|
||||
<div class="space-y-2">
|
||||
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{{ $field['description'] ?? '' }}</flux:description>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:input.group>
|
||||
<flux:input
|
||||
wire:model="searchQueries.{{ $fieldKey }}"
|
||||
|
|
@ -836,7 +878,7 @@ HTML;
|
|||
wire:click="searchXhrSelect('{{ $fieldKey }}', '{{ $field['endpoint'] }}')"
|
||||
icon="magnifying-glass"/>
|
||||
</flux:input.group>
|
||||
<flux:description>{{ $field['help_text'] ?? '' }}</flux:description>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
@if((isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey]) && count($xhrSelectOptions[$fieldKey]) > 0) || !empty($currentValue))
|
||||
<flux:select
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
|
|
@ -867,14 +909,16 @@ HTML;
|
|||
@endif
|
||||
</div>
|
||||
@elseif($field['field_type'] === 'multi_string')
|
||||
<flux:input
|
||||
label="{{ $field['name'] }}"
|
||||
description="{{ $field['description'] ?? '' }}"
|
||||
descriptionTrailing="{{ $field['help_text'] ?? 'Enter multiple values separated by commas' }}"
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
placeholder="{{ $field['placeholder'] ?? 'value1,value2' }}"
|
||||
/>
|
||||
<flux:field>
|
||||
<flux:label>{{ $field['name'] }}</flux:label>
|
||||
<flux:description>{!! $safeDescription !!}</flux:description>
|
||||
<flux:input
|
||||
wire:model="configuration.{{ $fieldKey }}"
|
||||
value="{{ $currentValue }}"
|
||||
placeholder="{{ $field['placeholder'] ?? 'value1,value2' }}"
|
||||
/>
|
||||
<flux:description>{!! $safeHelp !!}</flux:description>
|
||||
</flux:field>
|
||||
@else
|
||||
<flux:callout variant="warning">Field type "{{ $field['field_type'] }}" not yet supported</flux:callout>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@ use App\Models\Plugin;
|
|||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Volt\Volt;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class,RefreshDatabase::class);
|
||||
|
||||
test('plugin has required attributes', function (): void {
|
||||
$plugin = Plugin::factory()->create([
|
||||
|
|
@ -679,3 +683,111 @@ test('plugin render includes utc_offset and time_zone_iana in trmnl.user context
|
|||
->toContain('America/Chicago')
|
||||
->and($rendered)->toMatch('/\|-?\d+/'); // Should contain a pipe followed by a number (offset in seconds)
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Plugin security: XSS Payload Dataset
|
||||
* [Input, Expected to See, Dangerous parts that must be Missing]
|
||||
*/
|
||||
dataset('xss_vectors', [
|
||||
'standard_script' => [
|
||||
'Safe <script>alert(1)</script>',
|
||||
'Safe',
|
||||
['<script>', 'alert(1)']
|
||||
],
|
||||
'attribute_event_handlers' => [
|
||||
'<a href="https://trmnl.com" onmouseover="alert(1)" onclick="confirm()">Link</a>',
|
||||
'<a href="https://trmnl.com">Link</a>',
|
||||
['onmouseover', 'onclick', 'confirm()']
|
||||
],
|
||||
'javascript_protocol' => [
|
||||
'<a href="javascript:alert(1)">Click Me</a>',
|
||||
'<a>Click Me</a>',
|
||||
['javascript:']
|
||||
],
|
||||
'broken_tags_layout_break' => [
|
||||
'<b>Unclosed tag <script>alert(1)</script>',
|
||||
'<b>Unclosed tag',
|
||||
['<script>']
|
||||
],
|
||||
'iframe_injection' => [
|
||||
'Watch <iframe src="https://malicious.com"></iframe>',
|
||||
'Watch',
|
||||
['<iframe>', 'https://malicious.com']
|
||||
],
|
||||
'encoded_entities' => [
|
||||
'<a href="javascript:alert(1)">Link</a>',
|
||||
'<a>Link</a>',
|
||||
['javascript']
|
||||
],
|
||||
'img_onerror_fallback' => [
|
||||
'Photo <img src=x onerror=alert(1)>',
|
||||
'Photo',
|
||||
['onerror', 'alert(1)']
|
||||
],
|
||||
]);
|
||||
|
||||
test('plugin config descriptions are sanitized', function (string $input, string $expectedSee, array $forbidden) : void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
// This triggers the static::saving hook in the Plugin model
|
||||
$plugin = Plugin::create([
|
||||
'user_id' => $user->id,
|
||||
'name' => 'Security Test Plugin',
|
||||
'data_stale_minutes' => 15,
|
||||
'data_strategy' => 'static',
|
||||
'polling_verb' => 'get',
|
||||
'configuration_template' => [
|
||||
'custom_fields' => [
|
||||
[
|
||||
'keyname' => 'test_field',
|
||||
'field_type' => 'string',
|
||||
'name' => 'Secure Field',
|
||||
'description' => $input,
|
||||
],
|
||||
],
|
||||
],
|
||||
'configuration' => [],
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$test = Volt::test('plugins.recipe', ['plugin' => $plugin])
|
||||
->assertSeeHtml($expectedSee);
|
||||
|
||||
foreach ($forbidden as $malice) {
|
||||
$test->assertDontSeeHtml($malice);
|
||||
}
|
||||
})->with('xss_vectors');
|
||||
|
||||
test('plugin configuration help_text is sanitized', function (string $input, string $expectedSee, array $forbidden) : void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$plugin = Plugin::create([
|
||||
'user_id' => $user->id,
|
||||
'name' => 'Help Text Security Test',
|
||||
'data_stale_minutes' => 15,
|
||||
'data_strategy' => 'static',
|
||||
'polling_verb' => 'get',
|
||||
'configuration_template' => [
|
||||
'custom_fields' => [
|
||||
[
|
||||
'keyname' => 'test',
|
||||
'field_type' => 'string',
|
||||
'name' => 'Secure Field',
|
||||
'help_text' => $input,
|
||||
],
|
||||
],
|
||||
],
|
||||
'configuration' => [],
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$test = Volt::test('plugins.recipe', ['plugin' => $plugin])
|
||||
->assertSeeHtml($expectedSee);
|
||||
|
||||
foreach ($forbidden as $malice) {
|
||||
$test->assertDontSeeHtml($malice);
|
||||
}
|
||||
})->with('xss_vectors');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue