🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,60 @@
name: run-tests
on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'
jobs:
run-tests:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
php: [8.3, 8.2, 8.1]
laravel: ['9.*', '10.*', '11.*', '12.*']
dependency-version: [prefer-stable]
include:
- laravel: 11.*
testbench: 9.*
- laravel: 10.*
testbench: 8.*
- laravel: 9.*
testbench: 7.*
- laravel: 12.*
testbench: 10.*
exclude:
- laravel: 11.*
php: 8.1
- laravel: 9.*
php: 8.3
- laravel: 12.*
php: 8.1
name: ${{ matrix.os }} - P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.composer/cache/files
key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update --dev
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- name: Execute tests
run: vendor/bin/phpunit
@@ -0,0 +1,4 @@
/vendor
composer.phar
composer.lock
.DS_Store
@@ -0,0 +1,9 @@
filter:
excluded_paths:
- tests/*
build:
nodes:
analysis:
tests:
override:
- command: php-scrutinizer-run
@@ -0,0 +1,8 @@
preset: laravel
enabled:
- phpdoc_align
- phpdoc_separation
- unalign_double_arrow
disabled:
- laravel_phpdoc_alignment
- laravel_phpdoc_separation
@@ -0,0 +1,478 @@
<h1 align="center">Purify</h1>
<p align="center">
A Laravel wrapper for <a href="https://github.com/ezyang/htmlpurifier" target="_blank">HTMLPurifier</a> by <a href="https://github.com/ezyang" target="_blank">ezyang</a>.
</p>
<p align="center">
<a href="https://github.com/stevebauman/purify/actions" target="_blank"><img src="https://img.shields.io/github/actions/workflow/status/stevebauman/purify/run-tests.yml?branch=master&style=flat-square"/></a>
<a href="https://packagist.org/packages/stevebauman/purify" target="_blank"><img src="https://img.shields.io/packagist/v/stevebauman/purify.svg?style=flat-square"/></a>
<a href="https://packagist.org/packages/stevebauman/purify" target="_blank"><img src="https://img.shields.io/packagist/dt/stevebauman/purify.svg?style=flat-square"/></a>
<a href="https://packagist.org/packages/stevebauman/purify" target="_blank"><img src="https://img.shields.io/packagist/l/stevebauman/purify.svg?style=flat-square"/></a>
</p>
### Index
- [Requirements](#requirements)
- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [Cache](#cache)
- [Practices](#practices)
- [Upgrading from v4 to v5](#upgrading-from-v4-to-v5)
- [Upgrading from v5 to v6](#upgrading-from-v5-to-v6)
### Requirements
- PHP >= 7.4
- Laravel >= 7.0
### Installation
To install Purify, run the following command in the root of your project:
```bash
composer require stevebauman/purify
```
Then, publish the configuration file using:
```bash
php artisan vendor:publish --provider="Stevebauman\Purify\PurifyServiceProvider"
```
### Usage
##### Cleaning a String
To clean a users input, simply use the clean method:
```php
use Stevebauman\Purify\Facades\Purify;
$input = '<script>alert("Harmful Script");</script> <p style="border:1px solid black" class="text-gray-700">Test</p>';
// Returns '<p>Test</p>'
$cleaned = Purify::clean($input);
```
##### Cleaning an Array
Need to purify an array of user input? Just pass in an array:
```php
use Stevebauman\Purify\Facades\Purify;
$array = [
'<script>alert("Harmful Script");</script> <p style="border:1px solid black" class="text-gray-700">Test</p>',
'<script>alert("Harmful Script");</script> <p style="border:1px solid black" class="text-gray-700">Test</p>',
];
$cleaned = Purify::clean($array);
// array [
// '<p>Test</p>',
// '<p>Test</p>',
// ]
var_dump($cleaned);
```
##### Dynamic Configuration
Need a different configuration for a single input? Pass in a configuration array to the `config` method:
> **Note**: Configuration passed into the config method
> is **not** merged with your default configuration.
```php
use Stevebauman\Purify\Facades\Purify;
$config = ['HTML.Allowed' => 'div,b,a[href]'];
$cleaned = Purify::config($config)->clean($input);
```
### Configuration
Inside the configuration file, multiple HTMLPurifier configuration sets
can be specified, similar to Laravel's built-in `database`, `mail` and `logging` config.
Simply call `Purify::config($name)->clean($input)` to use another set of configuration.
For example, if we need to have a separate configuration for a comment system, we
can setup this configuration in the `config/purify.php` file:
```php
// config/purify.php
'configs' => [
// ...
'comments' => [
// Some configuration ...
],
]
```
Then, utilize it anywhere in your application by its name:
```php
use Stevebauman\Purify\Facades\Purify;
$cleanedContent = Purify::config('comments')->clean(request('content'));
```
For HTMLPurifier configuration documentation, please visit the HTMLPurifier Website:
http://htmlpurifier.org/live/configdoc/plain.html
### Cache
After running Purify once, [HTMLPurifier](https://github.com/ezyang/htmlpurifier) will auto-cache your
serialized `definitions` into the `serializer.cache` definition you have configured in `config/purify.php`.
> [!Important]
>
> If you have configured Purify to utilize the `CacheDefinitionCache` in the `serializer` option,
> this command will issue a `Cache::clear()` on the cache driver you have configured it to use.
>
> If you have configured Purify to utilize the `FilesystemDefinitionCache` in the `serializer` option,
> this command will clear the directory that you have configured it to store in.
>
> It is recommended to setup a unique filesystem path or disk (via `config/filesystems.php`) or cache store
> (via `config/cache.php`) for Purify if you intended to clear the serialized definitions using this command.
If you ever update the `definitions` configuration option, you must clear this HTMLPurifier cache.
You may do so via a `purify:clear` command:
```shell
php artisan purify:clear
```
#### Disabling Caching
To disable caching all together, you may set the `serializer` path to `null`:
```php
// config/purify.php
'serializer' => null,
```
This will cause your definitions to be serialized upon each application request.
This is especially useful when debugging or tweaking definition files to see immediate results.
> [!Important]
>
> Caching is recommended in production environments.
### Practices
If you're looking into sanitization, you're likely wanting to sanitize inputted user HTML
content that is then stored in your database to be rendered onto your application.
In this scenario, it's likely best practice to sanitize on the _way out_ instead of
the on the _way in_. The **database doesn't care what text it contains**.
This way you can allow anything to be inserted in the database, and have strong sanization rules on the way out.
To accomplish this, you may use the provided `PurifyHtmlOnGet` cast class on your Eloquent model:
```php
use Stevebauman\Purify\Casts\PurifyHtmlOnGet;
class Post extends Model
{
protected $casts = [
'content' => PurifyHtmlOnGet::class,
];
}
```
Or, implement it yourself via an Eloquent attribute mutator:
```php
use Stevebauman\Purify\Facades\Purify;
class Post extends Model
{
public function getContentAttribute($value)
{
return Purify::clean($value);
}
}
```
You can even configure the configuration that is used when casting by appending it's name to the cast:
```php
// config/purify.php
'configs' => [
// ...
'other' => [
// Some configuration ...
],
]
```
```php
protected $casts = [
'content' => PurifyHtmlOnGet::class.':other',
];
```
This helps tremendously if you change your sanization requirements later down
the line, then all rendered content will follow these sanization rules.
If you'd like to purify HTML while setting the value, you can use the inverse `PurifyHtmlOnSet` cast instead.
#### Custom HTML definitions
The `HTML.Doctype` configuration option denotes the schema to ultimately abide to.
You may want to extend these schema definitions to support custom elements or
attributes (e.g. `<foo>...</foo>`, or `<span foo="...">`) by specifying a
custom HTML element "definitions".
Purify ships with additional HTML5 definitions that HTMLPurifier does
not (yet) support of the box (via the `Html5Definition` class).
To create your own HTML definition, create a new class and have it implement `Definition`:
```php
namespace App;
use HTMLPurifier_HTMLDefinition;
use Stevebauman\Purify\Definitions\Definition;
class CustomDefinition implements Definition
{
/**
* Apply rules to the HTML Purifier definition.
*
* @param HTMLPurifier_HTMLDefinition $definition
*
* @return void
*/
public static function apply(HTMLPurifier_HTMLDefinition $definition)
{
// Customize the HTML purifier definition.
}
}
```
Then, reference this class in the `config/purify.php` file in the `definitions` key:
```php
// config/purify.php
'definitions' => \App\CustomDefinitions::class,
```
If you'd like to extend the built-in default `Html5Definition`, you can apply it to your custom definition:
```php
use Stevebauman\Purify\Definitions\Html5Definition;
class CustomDefinition implements Definition
{
public static function apply(HTMLPurifier_HTMLDefinition $definition)
{
Html5Definition::apply($definition);
// ...
}
}
```
##### Basecamp Trix Definition
Here's an example for customizing the definition in order to support Basecamp's Trix WYSIWYG editor
(credit to [Antonio Primera](https://github.com/stevebauman/purify/issues/7) & [Daniel Sun](https://github.com/stevebauman/purify/issues/77)):
```php
namespace App;
use HTMLPurifier_HTMLDefinition;
use Stevebauman\Purify\Definitions\Definition;
class TrixPurifierDefinitions implements Definition
{
/**
* Apply rules to the HTML Purifier definition.
*
* @param HTMLPurifier_HTMLDefinition $definition
*
* @return void
*/
public static function apply(HTMLPurifier_HTMLDefinition $definition)
{
$definition->addElement('figure', 'Inline', 'Inline', 'Common');
$definition->addAttribute('figure', 'class', 'Class');
$definition->addAttribute('figure', 'data-trix-attachment', 'Text');
$definition->addAttribute('figure', 'data-trix-attributes', 'Text');
$definition->addElement('figcaption', 'Inline', 'Inline', 'Common');
$definition->addAttribute('figcaption', 'class', 'Class');
$definition->addAttribute('figcaption', 'data-trix-placeholder', 'Text');
$definition->addAttribute('a', 'rel', 'Text');
$definition->addAttribute('a', 'tabindex', 'Text');
$definition->addAttribute('a', 'contenteditable', 'Enum#true,false');
$definition->addAttribute('a', 'data-trix-attachment', 'Text');
$definition->addAttribute('a', 'data-trix-content-type', 'Text');
$definition->addAttribute('a', 'data-trix-id', 'Number');
$definition->addElement('span', 'Block', 'Flow', 'Common');
$definition->addAttribute('span', 'data-trix-cursor-target', 'Enum#right,left');
$definition->addAttribute('span', 'data-trix-serialize', 'Enum#true,false');
$definition->addAttribute('img', 'data-trix-mutable', 'Enum#true,false');
$definition->addAttribute('img', 'data-trix-store-key', 'Text');
}
}
```
#### Custom CSS definitions
It's possible to override the CSS definitions, this allows you to customize what
inline styles you allow and their properties and values. This can help fill in
missing values for properties such as text-align, which by default is missing start
and end values. You can do this by creating a CSS definition.
To create your own CSS definition, create a new class and have it implement `CssDefinition`:
```php
namespace App;
use HTMLPurifier_CSSDefinition;
use Stevebauman\Purify\Definitions\CssDefinition;
class CustomCssDefinition implements CssDefinition
{
/**
* Apply rules to the CSS Purifier definition.
*
* @param HTMLPurifier_CSSDefinition $definition
*
* @return void
*/
public static function apply(HTMLPurifier_CSSDefinition $definition)
{
// Customize the CSS purifier definition.
$definition->info['text-align'] = new \HTMLPurifier_AttrDef_Enum(
['right', 'left', 'center', 'start', 'end'],
false,
);
}
}
```
Then, reference this class in the `config/purify.php` file in the `css-definitions` key:
```php
// config/purify.php
'css-definitions' => \App\CustomCssDefinition::class,
```
See the class HTMLPurifier_CSSDefinition in the HTMLPurifier library for other examples of what can be changed.
### Upgrading from v4 to v5
To upgrade from v4, install the latest version by running the below command in the root of your project:
```bash
composer require stevebauman/purify
```
Then, navigate into your published `config/purify.php` configuration file and
copy the `settings` array -- except for the following keys:
- `HTML.DocType`:
- `Core.Encoding`:
- `Cache.SerializerPath`:
```diff
'settings' => [
- 'Core.Encoding' => 'utf-8',
- 'Cache.SerializerPath' => storage_path('app/purify'),
- 'HTML.Doctype' => 'XHTML 1.0 Strict',
+ 'HTML.Allowed' => 'h1,h2,h3,h4,h5,h6,b,strong,i,em,a[href|title],ul,ol,li,p[style],br,span,img[width|height|alt|src]',
+ 'HTML.ForbiddenElements' => '',
+ 'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align',
+ 'AutoFormat.AutoParagraph' => false,
+ 'AutoFormat.RemoveEmpty' => false,
],
```
> **Important**: If you've created a unique storage path for `Cache.SerializerPath`,
> take note of this as well, so you can migrate it into the new configuration file.
Once copied, delete the `config/purify.php` file, and run the below command:
```bash
php artisan vendor:publish --provider="Stevebauman\Purify\PurifyServiceProvider"
```
Then, inside the newly published `config/purify.php` configuration file, paste
the keys (overwriting the current) into the `configs.default` array:
```diff
'configs' => [
'default' => [
'Core.Encoding' => 'utf-8',
'HTML.Doctype' => 'HTML 4.01 Transitional',
+ 'HTML.Allowed' => 'h1,h2,h3,h4,h5,h6,b,strong,i,em,a[href|title],ul,ol,li,p[style],br,span,img[width|height|alt|src]',
+ 'HTML.ForbiddenElements' => '',
+ 'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align',
+ 'AutoFormat.AutoParagraph' => false,
+ 'AutoFormat.RemoveEmpty' => false,
],
],
```
If you've created a unique serializer path (previously set via the old `Cache.SerializerPath` configuration
key mentioned above), then you may reconfigure this in the new `serializer` configuration key:
```php
'serializer' => storage_path('app/purify'),
```
You're all set!
### Upgrading from v5 to v6
In v6, the HTMLPurifier Serializer storage mechanism was updated for Laravel Vapour support, allowing
you to store the serialized HTMLPurifier definitions in a Redis cache, or an external filesystem.
To upgrade from v5, install the latest version by running the below command in the root of your project:
```bash
composer require stevebauman/purify
```
Then, navigate into your published `config/purify.php` configuration file and
replace the `serializer` configuration option with the below:
```diff
- 'serializer' => storage_path('app/purify'),
+ 'serializer' => [
+ 'disk' => env('FILESYSTEM_DISK', 'local'),
+ 'path' => 'purify',
+ 'cache' => \Stevebauman\Purify\Cache\FilesystemDefinitionCache::class,
+ ],
+
+ // 'serializer' => [
+ // 'driver' => env('CACHE_DRIVER', 'file'),
+ // 'cache' => \Stevebauman\Purify\Cache\CacheDefinitionCache::class,
+ // ],
```
This will update the syntax used to control the serializer cache mechanism. You may now uncomment
the below `serializer` cache definition if you would like to use a Laravel Cache driver
(such as Redis) to store the serialized definitions.
@@ -0,0 +1,57 @@
{
"name": "stevebauman/purify",
"description": "An HTML Purifier / Sanitizer for Laravel",
"keywords": [
"purify",
"purification",
"purifier",
"clean",
"cleaner",
"laravel",
"html"
],
"authors": [
{
"name": "Steve Bauman",
"email": "steven_bauman@outlook.com"
}
],
"license": "MIT",
"require": {
"php": ">=7.4",
"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"
},
"require-dev": {
"phpunit/phpunit": "^8.0|^9.0|^10.0|^11.5.3",
"orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0"
},
"minimum-stability": "dev",
"prefer-stable": true,
"archive": {
"exclude": [
"/tests"
]
},
"autoload": {
"psr-4": {
"Stevebauman\\Purify\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Stevebauman\\Purify\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"Stevebauman\\Purify\\PurifyServiceProvider"
],
"aliases": {
"Purify": "Stevebauman\\Purify\\Facades\\Purify"
}
}
}
}
@@ -0,0 +1,115 @@
<?php
use Stevebauman\Purify\Definitions\Html5Definition;
return [
/*
|--------------------------------------------------------------------------
| Default Config
|--------------------------------------------------------------------------
|
| This option defines the default config that is provided to HTMLPurifier.
|
*/
'default' => 'default',
/*
|--------------------------------------------------------------------------
| Config sets
|--------------------------------------------------------------------------
|
| Here you may configure various sets of configuration for differentiated use of HTMLPurifier.
| A specific set of configuration can be applied by calling the "config($name)" method on
| a Purify instance. Feel free to add/remove/customize these attributes as you wish.
|
| Documentation: http://htmlpurifier.org/live/configdoc/plain.html
|
| Core.Encoding The encoding to convert input to.
| HTML.Doctype Doctype to use during filtering.
| HTML.Allowed The allowed HTML Elements with their allowed attributes.
| HTML.ForbiddenElements The forbidden HTML elements. Elements that are listed in this
| string will be removed, however their content will remain.
| CSS.AllowedProperties The Allowed CSS properties.
| AutoFormat.AutoParagraph Newlines are converted in to paragraphs whenever possible.
| AutoFormat.RemoveEmpty Remove empty elements that contribute no semantic information to the document.
|
*/
'configs' => [
'default' => [
'Core.Encoding' => 'utf-8',
'HTML.Doctype' => 'HTML 4.01 Transitional',
'HTML.Allowed' => 'h1,h2,h3,h4,h5,h6,b,u,strong,i,em,s,del,a[href|title],ul,ol,li,p[style],br,span,img[width|height|alt|src],blockquote',
'HTML.ForbiddenElements' => '',
'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align',
'AutoFormat.AutoParagraph' => false,
'AutoFormat.RemoveEmpty' => false,
],
],
/*
|--------------------------------------------------------------------------
| HTMLPurifier definitions
|--------------------------------------------------------------------------
|
| Here you may specify a class that augments the HTML definitions used by
| HTMLPurifier. Additional HTML5 definitions are provided out of the box.
| When specifying a custom class, make sure it implements the interface:
|
| \Stevebauman\Purify\Definitions\Definition
|
| Note that these definitions are applied to every Purifier instance.
|
| Documentation: http://htmlpurifier.org/docs/enduser-customize.html
|
*/
'definitions' => Html5Definition::class,
/*
|--------------------------------------------------------------------------
| HTMLPurifier CSS definitions
|--------------------------------------------------------------------------
|
| Here you may specify a class that augments the CSS definitions used by
| HTMLPurifier. When specifying a custom class, make sure it implements
| the interface:
|
| \Stevebauman\Purify\Definitions\CssDefinition
|
| Note that these definitions are applied to every Purifier instance.
|
| CSS should be extending $definition->info['css-attribute'] = values
| See HTMLPurifier_CSSDefinition for further explanation
|
*/
'css-definitions' => null,
/*
|--------------------------------------------------------------------------
| Serializer
|--------------------------------------------------------------------------
|
| The storage implementation where HTMLPurifier can store its serializer files.
| If the filesystem cache is in use, the path must be writable through the
| storage disk by the web server, otherwise an exception will be thrown.
|
*/
'serializer' => [
'driver' => env('CACHE_STORE', env('CACHE_DRIVER', 'file')),
'cache' => \Stevebauman\Purify\Cache\CacheDefinitionCache::class,
],
// 'serializer' => [
// 'disk' => env('FILESYSTEM_DISK', 'local'),
// 'path' => 'purify',
// 'cache' => \Stevebauman\Purify\Cache\FilesystemDefinitionCache::class,
// ],
];
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Steve Bauman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<testsuites>
<testsuite name="Purify Test Suite">
<directory suffix="Test.php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
@@ -0,0 +1,193 @@
<?php
namespace Stevebauman\Purify\Cache;
use HTMLPurifier_Definition;
use HTMLPurifier_DefinitionCache;
use Illuminate\Support\Facades\Cache;
class CacheDefinitionCache extends HTMLPurifier_DefinitionCache
{
/**
* The cache repository.
*
* @var \Illuminate\Contracts\Cache\Repository
*/
protected $cache;
/**
* Constructor.
*
* @param string $type
*/
public function __construct($type)
{
parent::__construct($type);
$this->cache = Cache::driver(
config('purify.serializer.driver')
);
}
/**
* Adds a definition object to the cache.
*
* @param HTMLPurifier_Definition $def
* @param \HTMLPurifier_Config $config
*
* @return bool|void
*/
public function add($def, $config)
{
if (! $this->checkDefType($def)) {
return;
}
$key = $this->generateKey($config);
if ($this->cache->has($key)) {
return false;
}
return $this->cache->put($key, $this->encode($def));
}
/**
* Unconditionally saves a definition object to the cache.
*
* @param HTMLPurifier_Definition $def
* @param \HTMLPurifier_Config $config
*
* @return bool|void
*/
public function set($def, $config)
{
if (! $this->checkDefType($def)) {
return;
}
$key = $this->generateKey($config);
return $this->cache->put($key, $this->encode($def));
}
/**
* Replace an object in the cache.
*
* @param HTMLPurifier_Definition $def
* @param \HTMLPurifier_Config $config
*
* @return bool|void
*/
public function replace($def, $config)
{
if (! $this->checkDefType($def)) {
return;
}
$key = $this->generateKey($config);
if (! $this->cache->has($key)) {
return false;
}
return $this->cache->put($key, $this->encode($def));
}
/**
* Retrieves a definition object from the cache.
*
* @param \HTMLPurifier_Config $config
*
* @return bool|HTMLPurifier_Definition
*/
public function get($config)
{
$key = $this->generateKey($config);
if (! $this->cache->has($key)) {
return false;
}
return $this->decode($this->cache->get($key));
}
/**
* Removes a definition object to the cache.
*
* @param \HTMLPurifier_Config $config
*
* @return bool
*/
public function remove($config)
{
$key = $this->generateKey($config);
if (! $this->cache->has($key)) {
return false;
}
return $this->cache->delete($key);
}
/**
* Clears all objects from cache.
*
* @param \HTMLPurifier_Config $config
*
* @return bool
*/
public function flush($config)
{
return $this->cache->clear();
}
/**
* Clears all expired (older version or revision) objects from cache.
*
* @param \HTMLPurifier_Config $config
*
* @return bool
*/
public function cleanup($config)
{
$key = $this->generateKey($config);
if ($this->isOld($key, $config)) {
return $this->cache->delete($key);
}
return true;
}
/**
* Encode the definition for storage.
*
* @param HTMLPurifier_Definition $def
*
* @return string
*/
protected function encode($def)
{
return base64_encode(serialize($def));
}
/**
* Decode the definition from storage.
*
* @param string $def
*
* @return HTMLPurifier_Definition
*/
protected function decode($def)
{
// Backwards compatibility with old cache definitions.
$instance = @unserialize($def);
if ($instance instanceof HTMLPurifier_Definition) {
return $instance;
}
return unserialize(base64_decode($def));
}
}
@@ -0,0 +1,229 @@
<?php
namespace Stevebauman\Purify\Cache;
use HTMLPurifier_DefinitionCache;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class FilesystemDefinitionCache extends HTMLPurifier_DefinitionCache
{
/**
* The filesystem disk.
*
* @var \Illuminate\Contracts\Filesystem\Filesystem
*/
protected $disk;
/**
* Constructor.
*
* @param string $type
*/
public function __construct($type)
{
parent::__construct($type);
$this->disk = Storage::disk(
config('purify.serializer.disk')
);
}
/**
* Adds a definition object to the cache.
*
* @param \HTMLPurifier_Definition $def
* @param \HTMLPurifier_Config $config
*
* @return bool|void
*/
public function add($def, $config)
{
if (! $this->checkDefType($def)) {
return;
}
$file = $this->generateFilePath($config);
if ($this->disk->exists($file)) {
return false;
}
return $this->disk->put($file, serialize($def));
}
/**
* Unconditionally saves a definition object to the cache.
*
* @param \HTMLPurifier_Definition $def
* @param \HTMLPurifier_Config $config
*
* @return bool|void
*/
public function set($def, $config)
{
if (! $this->checkDefType($def)) {
return;
}
$file = $this->generateFilePath($config);
return $this->disk->put($file, serialize($def));
}
/**
* Replace an object in the cache.
*
* @param \HTMLPurifier_Definition $def
* @param \HTMLPurifier_Config $config
*
* @return bool|void
*/
public function replace($def, $config)
{
if (! $this->checkDefType($def)) {
return;
}
$file = $this->generateFilePath($config);
if (! $this->disk->exists($file)) {
return false;
}
return $this->disk->put($file, serialize($def));
}
/**
* Retrieves a definition object from the cache.
*
* @param \HTMLPurifier_Config $config
*
* @return bool|\HTMLPurifier_Config
*/
public function get($config)
{
$file = $this->generateFilePath($config);
if (! $this->disk->exists($file)) {
return false;
}
return unserialize($this->disk->get($file));
}
/**
* Removes a definition object to the cache.
*
* @param \HTMLPurifier_Config $config
*
* @return bool
*/
public function remove($config)
{
$file = $this->generateFilePath($config);
if (! $this->disk->exists($file)) {
return false;
}
return $this->disk->delete($file);
}
/**
* Clears all objects from cache.
*
* @param \HTMLPurifier_Config $config
*
* @return bool
*/
public function flush($config)
{
$dir = $this->generateDirectoryPath($config);
foreach ($this->disk->files($dir) as $filename) {
if (Str::startsWith($filename, '.')) {
continue;
}
$this->disk->delete(
implode(DIRECTORY_SEPARATOR, [$dir, $filename])
);
}
return true;
}
/**
* Clears all expired (older version or revision) objects from cache.
*
* @param \HTMLPurifier_Config $config
*
* @return bool
*/
public function cleanup($config)
{
$dir = $this->generateDirectoryPath($config);
foreach ($this->disk->files($dir) as $filename) {
if (Str::startsWith($filename, '.')) {
continue;
}
$key = substr($filename, 0, strlen($filename) - 4);
if ($this->isOld($key, $config)) {
$this->disk->delete(
implode(DIRECTORY_SEPARATOR, [$dir, $filename])
);
}
}
return true;
}
/**
* Generates the file path.
*
* @param \HTMLPurifier_Config $config
*
* @return string
*/
public function generateFilePath($config)
{
$key = $this->generateKey($config);
return $this->generateDirectoryPath($config).'DefinitionCache.php/'.$key.'.ser';
}
/**
* Generates the path to the directory contain this cache's serial files.
*
* @note No trailing slash
*
* @param \HTMLPurifier_Config $config
*
* @return string
*/
public function generateDirectoryPath($config)
{
$base = $this->generateBaseDirectoryPath($config);
return $base.'/'.$this->type;
}
/**
* Generates path to base directory that contains all definition type
* serials.
*
* @param \HTMLPurifier_Config $config
*
* @return string
*/
public function generateBaseDirectoryPath($config)
{
$base = $config->get('Cache.SerializerPath');
return is_null($base) ? '/' : $base;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Stevebauman\Purify\Casts;
abstract class Caster
{
/**
* The name of the config to use for purification.
*
* @var string|null
*/
protected $config;
/**
* Constructor.
*
* @param string|null $config
*/
public function __construct($config = null)
{
$this->config = $config;
}
}
@@ -0,0 +1,43 @@
<?php
namespace Stevebauman\Purify\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Stevebauman\Purify\Facades\Purify;
class PurifyHtmlOnGet extends Caster implements CastsAttributes
{
/**
* Purify the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
*
* @return string|array|null
*/
public function get($model, $key, $value, $attributes)
{
if (is_null($value)) {
return null;
}
return Purify::config($this->config)->clean($value);
}
/**
* Prepare the value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
*
* @return array|string|null
*/
public function set($model, $key, $value, $attributes)
{
return $value;
}
}
@@ -0,0 +1,43 @@
<?php
namespace Stevebauman\Purify\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Stevebauman\Purify\Facades\Purify;
class PurifyHtmlOnSet extends Caster implements CastsAttributes
{
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
*
* @return string|array|null
*/
public function get($model, string $key, $value, array $attributes)
{
return $value;
}
/**
* Purify the value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
*
* @return array|string|null
*/
public function set($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return null;
}
return Purify::config($this->config)->clean($value);
}
}
@@ -0,0 +1,66 @@
<?php
namespace Stevebauman\Purify\Commands;
use Illuminate\Config\Repository;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Stevebauman\Purify\Cache\CacheDefinitionCache;
use Stevebauman\Purify\Cache\FilesystemDefinitionCache;
class ClearCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'purify:clear';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Flush the HTML purifier serializer cache';
/**
* Execute the console command.
*
* @return void
*/
public function handle(Repository $config)
{
if (empty($serializer = $config->get('purify.serializer'))) {
return $this->error(
'Purifier serializer path is not defined. Did you set it to null or forget to publish the configuration?'
);
}
/** @var class-string $cache */
$cache = $serializer['cache'];
if (is_a($cache, FilesystemDefinitionCache::class, true)) {
$disk = Storage::disk($serializer['disk']);
$disk->deleteDirectory($serializer['path']);
$disk->makeDirectory($serializer['path']);
return $this->info('HTML Purifier serializer filesystem cache cleared successfully.');
}
if (is_a($cache, CacheDefinitionCache::class, true)) {
$cache = Cache::driver($serializer['driver']);
$cache->clear();
return $this->info('HTML Purifier serializer cache cleared successfully.');
}
return $this->error(
sprintf('Unable to determine a clear cache strategy with the given cache instance [%s].', $cache)
);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Stevebauman\Purify\Definitions;
use HTMLPurifier_CSSDefinition;
interface CssDefinition
{
/**
* Apply rules to the CSS Purifier definition.
*
* @param HTMLPurifier_CSSDefinition $definition
*
* @return void
*/
public static function apply(HTMLPurifier_CSSDefinition $definition);
}
@@ -0,0 +1,17 @@
<?php
namespace Stevebauman\Purify\Definitions;
use HTMLPurifier_HTMLDefinition;
interface Definition
{
/**
* Apply rules to the HTML Purifier definition.
*
* @param HTMLPurifier_HTMLDefinition $definition
*
* @return void
*/
public static function apply(HTMLPurifier_HTMLDefinition $definition);
}
@@ -0,0 +1,73 @@
<?php
namespace Stevebauman\Purify\Definitions;
use HTMLPurifier_HTMLDefinition;
class Html5Definition implements Definition
{
/**
* Apply rules to the HTML Purifier definition.
*
* @param HTMLPurifier_HTMLDefinition $definition
*
* @return void
*/
public static function apply(HTMLPurifier_HTMLDefinition $definition)
{
// http://developers.whatwg.org/sections.html
$definition->addElement('section', 'Block', 'Flow', 'Common');
$definition->addElement('nav', 'Block', 'Flow', 'Common');
$definition->addElement('article', 'Block', 'Flow', 'Common');
$definition->addElement('aside', 'Block', 'Flow', 'Common');
$definition->addElement('header', 'Block', 'Flow', 'Common');
$definition->addElement('footer', 'Block', 'Flow', 'Common');
$definition->addElement('address', 'Block', 'Flow', 'Common');
$definition->addElement('hgroup', 'Block', 'Required: h1 | h2 | h3 | h4 | h5 | h6', 'Common');
// http://developers.whatwg.org/grouping-content.html
$definition->addElement('figure', 'Block', 'Optional: (figcaption, Flow) | (Flow, figcaption) | Flow', 'Common');
$definition->addElement('figcaption', 'Inline', 'Flow', 'Common');
// http://developers.whatwg.org/the-video-element.html#the-video-element
$definition->addElement('video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', [
'src' => 'URI',
'type' => 'Text',
'width' => 'Length',
'height' => 'Length',
'poster' => 'URI',
'preload' => 'Enum#auto,metadata,none',
'controls' => 'Bool',
]);
$definition->addElement('source', 'Block', 'Flow', 'Common', [
'src' => 'URI',
'type' => 'Text',
]);
// http://developers.whatwg.org/interactive-elements.html
$definition->addElement('details', 'Block', 'Flow', 'Common');
$definition->addElement('summary', 'Inline', 'Flow', 'Common', [
'open' => 'Bool',
]);
// http://developers.whatwg.org/text-level-semantics.html
$definition->addElement('u', 'Inline', 'Inline', 'Common');
$definition->addElement('s', 'Inline', 'Inline', 'Common');
$definition->addElement('var', 'Inline', 'Inline', 'Common');
$definition->addElement('sub', 'Inline', 'Inline', 'Common');
$definition->addElement('sup', 'Inline', 'Inline', 'Common');
$definition->addElement('mark', 'Inline', 'Inline', 'Common');
$definition->addElement('wbr', 'Inline', 'Empty', 'Core');
// http://developers.whatwg.org/edits.html
$definition->addElement('ins', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']);
$definition->addElement('del', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']);
$definition->addAttribute('table', 'height', 'Text');
$definition->addAttribute('td', 'border', 'Text');
$definition->addAttribute('th', 'border', 'Text');
$definition->addAttribute('tr', 'width', 'Text');
$definition->addAttribute('tr', 'height', 'Text');
$definition->addAttribute('tr', 'border', 'Text');
}
}
@@ -0,0 +1,23 @@
<?php
namespace Stevebauman\Purify\Facades;
use Illuminate\Support\Facades\Facade;
/**
* @method static \HTMLPurifier getPurifier()
* @method static array|string clean(array|string $input)
* @method static \Stevebauman\Purify\Purify config(string|array $driver = null)
*/
class Purify extends Facade
{
/**
* The facade accessor string.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'purify';
}
}
@@ -0,0 +1,50 @@
<?php
namespace Stevebauman\Purify;
use HTMLPurifier;
use HTMLPurifier_Config;
class Purify
{
/**
* The HTML Purifier instance.
*
* @var HTMLPurifier
*/
protected $purifier;
/**
* Constructor.
*
* @param HTMLPurifier_Config $config
*/
public function __construct(HTMLPurifier_Config $config)
{
$this->purifier = new HTMLPurifier($config);
}
/**
* Sanitize the given input.
*
* @param array|string $input
*
* @return array|string
*/
public function clean($input)
{
return is_array($input)
? $this->purifier->purifyArray($input)
: $this->purifier->purify($input);
}
/**
* Get the underlying HTML Purifier instance.
*
* @return HTMLPurifier
*/
public function getPurifier()
{
return $this->purifier;
}
}
@@ -0,0 +1,215 @@
<?php
namespace Stevebauman\Purify;
use HTMLPurifier_Config;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Manager;
use InvalidArgumentException;
use Stevebauman\Purify\Definitions\CssDefinition;
use Stevebauman\Purify\Definitions\Definition;
class PurifyManager extends Manager
{
/**
* The filesystem manager instance.
*
* @var \Illuminate\Filesystem\FilesystemManager
*/
protected $filesystem;
/**
* Constructor.
*
* @param Container $container
*/
public function __construct(Container $container)
{
parent::__construct($container);
$this->filesystem = $container->make('filesystem');
}
/**
* Convenience alias for driver().
*
* @param string|array|null $config
*
* @return Purify
*
* @throws \InvalidArgumentException
*/
public function config($config = null)
{
return $this->driver($config);
}
/**
* Get the default driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->config->get('purify.default');
}
/**
* Get a driver instance.
*
* @param string|array|null $driver
*
* @return Purify
*
* @throws \InvalidArgumentException
*/
public function driver($driver = null)
{
// First, we will check if the provided "driver" is an array. If so,
// we're dealing with an inline defined config. We'll serialize it
// into a string to dynamically define and set its configuration.
if (is_array($driver)) {
$config = $driver;
$driver = md5(serialize($driver));
$this->config->set("purify.configs.$driver", $config);
}
return parent::driver($driver);
}
/**
* Create a new driver instance.
*
* @param string|array $driver
*
* @return Purify
*
* @throws \InvalidArgumentException
*/
protected function createDriver($driver)
{
// First, we will determine if a custom driver creator exists for the given driver and
// if it does not we will check for a creator method for the driver. Custom creator
// callbacks allow developers to build their own "drivers" easily using Closures.
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);
}
if ($config = $this->resolveConfig($driver)) {
return $this->createInstance($driver, $config);
}
throw new InvalidArgumentException("Purify config [$driver] not defined.");
}
/**
* Resolve the configuration for the given config name.
*
* @param string $name
*
* @return array
*/
protected function resolveConfig($name)
{
return $this->config->get("purify.configs.$name");
}
/**
* Resolve the serializer filepath the given config name.
*
* @param string $name
*
* @return string|false
*/
protected function resolveSerializerPath($name)
{
$path = $this->config->get('purify.serializer.path');
if (empty($path)) {
return false;
}
return implode(DIRECTORY_SEPARATOR, [$path, $name]);
}
/**
* Create a new Purify instance with the given config.
*
* @param string $name
* @param array $config
*
* @return Purify
*/
protected function createInstance(string $name, array $config)
{
$serializerPath = $this->resolveSerializerPath($name);
if (! empty($serializerPath)) {
$this->prepareFilesystemStorage($serializerPath);
}
return new Purify(
$this->createHtmlConfig(array_merge(array_filter([
'Cache.SerializerPath' => $serializerPath,
]), $config))
);
}
/**
* Prepare the serializer path in the filesystem storage.
*
* @param string $serializerPath
*
* @return void
*/
protected function prepareFilesystemStorage(string $serializerPath)
{
$disk = $this->config->get('purify.serializer.disk');
if (empty($disk)) {
return;
}
$storage = $this->filesystem->disk($disk);
if (! $storage->exists($serializerPath)) {
$storage->makeDirectory($serializerPath);
}
}
/**
* Create an HTML purifier configuration instance.
*
* @param array $config
*
* @return HTMLPurifier_Config
*/
protected function createHtmlConfig($config)
{
$htmlConfig = HTMLPurifier_Config::create($config);
$htmlConfig->set('HTML.DefinitionID', 'HTML-purify');
$htmlConfig->set('HTML.DefinitionRev', 1);
$htmlConfig->set('Cache.DefinitionImpl', config('purify.serializer.cache'));
if ($definition = $htmlConfig->maybeGetRawHTMLDefinition()) {
$definitionsClass = $this->config->get('purify.definitions');
if ($definitionsClass && is_a($definitionsClass, Definition::class, true)) {
$definitionsClass::apply($definition);
}
}
if ($definition = $htmlConfig->getCSSDefinition()) {
$definitionsClass = $this->config->get('purify.css-definitions');
if ($definitionsClass && is_a($definitionsClass, CssDefinition::class, true)) {
$definitionsClass::apply($definition);
}
}
return $htmlConfig;
}
}
@@ -0,0 +1,54 @@
<?php
namespace Stevebauman\Purify;
use HTMLPurifier_DefinitionCacheFactory;
use Illuminate\Support\ServiceProvider;
use Stevebauman\Purify\Commands\ClearCommand;
class PurifyServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->mergeConfigFrom(__DIR__.'/../config/purify.php', 'purify');
$this->commands(ClearCommand::class);
$this->app->singleton('purify', function ($app) {
if ($cache = config('purify.serializer.cache')) {
HTMLPurifier_DefinitionCacheFactory::instance()->register($cache, $cache);
}
return new PurifyManager($app);
});
}
/**
* Register the publishable configuration.
*
* @return void
*/
public function boot()
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/../config/purify.php' => config_path('purify.php'),
], 'config');
}
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return ['purify'];
}
}
@@ -0,0 +1,34 @@
<?php
namespace Stevebauman\Purify\Tests;
use Stevebauman\Purify\Cache\CacheDefinitionCache;
use Stevebauman\Purify\Facades\Purify;
use Symfony\Component\Finder\Finder;
class CacheDefinitionCacheTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->app['config']->set('purify.serializer', [
'driver' => 'file',
'cache' => CacheDefinitionCache::class,
]);
}
public function test_cache_can_be_used()
{
Purify::clean('foo');
$dir = $this->app['config']->get('cache.stores.file.path');
$this->assertTrue(
Finder::create()
->in($dir)
->depth(0)
->hasResults()
);
}
}
@@ -0,0 +1,106 @@
<?php
namespace Stevebauman\Purify\Tests;
use Illuminate\Database\Eloquent\Model;
use Stevebauman\Purify\Casts\PurifyHtmlOnGet;
use Stevebauman\Purify\Casts\PurifyHtmlOnSet;
class CastsTest extends TestCase
{
public $testInput = '<script>alert("Harmful Script");</script><p style="a {color: #0000ff;}" class="a-different-class">Test<span>bar</span></p>';
public function test_purifies_on_get_with_default_config()
{
$this->app['config']->set('purify.configs.default', [
'HTML.Allowed' => 'p',
]);
$model = new PurifyingDefaultOnGetModel();
$model->body = $this->testInput;
$this->assertEquals($this->testInput, $model->getAttributes()['body']);
$this->assertEquals('<p>Testbar</p>', $model->body);
}
public function test_purifies_on_get_with_custom_config()
{
$this->app['config']->set('purify.configs.foo', [
'HTML.Allowed' => 'p,span',
]);
$model = new PurifyingFooOnGetModel();
$model->body = $this->testInput;
$this->assertEquals($this->testInput, $model->getAttributes()['body']);
$this->assertEquals('<p>Test<span>bar</span></p>', $model->body);
}
public function test_returns_null_on_get_when_value_is_null()
{
$model = new PurifyingDefaultOnGetModel();
$model->body = null;
$this->assertNull($model->body);
}
public function test_purifies_on_set_with_default_config()
{
$this->app['config']->set('purify.configs.default', [
'HTML.Allowed' => 'p',
]);
$model = new PurifyingDefaultOnSetModel();
$model->body = $this->testInput;
$this->assertEquals('<p>Testbar</p>', $model->getAttributes()['body']);
}
public function test_purifies_on_set_with_custom_config()
{
$this->app['config']->set('purify.configs.foo', [
'HTML.Allowed' => 'p,span',
]);
$model = new PurifyingFooOnSetModel();
$model->body = $this->testInput;
$this->assertEquals('<p>Test<span>bar</span></p>', $model->getAttributes()['body']);
}
public function test_sets_null_on_set_when_value_is_null()
{
$model = new PurifyingDefaultOnSetModel();
$model->body = null;
$this->assertNull($model->getAttributes()['body']);
}
}
class PurifyingDefaultOnGetModel extends Model
{
protected $casts = [
'body' => PurifyHtmlOnGet::class,
];
}
class PurifyingFooOnGetModel extends Model
{
protected $casts = [
'body' => PurifyHtmlOnGet::class.':foo',
];
}
class PurifyingDefaultOnSetModel extends Model
{
protected $casts = [
'body' => PurifyHtmlOnSet::class,
];
}
class PurifyingFooOnSetModel extends Model
{
protected $casts = [
'body' => PurifyHtmlOnSet::class.':foo',
];
}
@@ -0,0 +1,38 @@
<?php
namespace Stevebauman\Purify\Tests;
use Illuminate\Support\Facades\Storage;
use Stevebauman\Purify\Cache\FilesystemDefinitionCache;
use Stevebauman\Purify\Commands\ClearCommand;
use Stevebauman\Purify\Facades\Purify;
use Symfony\Component\Finder\Finder;
class FilesystemDefinitionCacheTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->app['config']->set('purify.serializer', [
'disk' => 'local',
'path' => 'purify',
'cache' => FilesystemDefinitionCache::class,
]);
$this->artisan(ClearCommand::class);
}
public function test_filesystem_can_be_used()
{
Purify::clean('foo');
$dir = $this->app['config']->get('purify.serializer.path');
$this->assertTrue(
Finder::create()->in(
Storage::path($dir)
)->depth(0)->hasResults()
);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Stevebauman\Purify\Tests;
use Illuminate\Support\Facades\Storage;
use Stevebauman\Purify\Commands\ClearCommand;
use Stevebauman\Purify\Facades\Purify;
class NullDefinitionCacheTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->artisan(ClearCommand::class);
$this->app['config']->set('purify.serializer', null);
}
public function test_null_cache_can_be_used()
{
Purify::clean('foo');
$this->assertEmpty(Storage::allFiles('purfiy'));
}
}
@@ -0,0 +1,185 @@
<?php
namespace Stevebauman\Purify\Tests;
use HTMLPurifier_CSSDefinition;
use HTMLPurifier_HTMLDefinition;
use Illuminate\Support\Facades\File;
use Stevebauman\Purify\Cache\CacheDefinitionCache;
use Stevebauman\Purify\Definitions\CssDefinition;
use Stevebauman\Purify\Definitions\Definition;
use Stevebauman\Purify\Facades\Purify;
use Stevebauman\Purify\PurifyServiceProvider;
class PurifyTest extends TestCase
{
public $testInput = '<script>alert("Harmful Script");</script><p style="a {color: #0000ff;}" class="a-different-class">Test</p>';
protected function setUp(): void
{
parent::setUp();
$this->app['config']->set('purify.serializer', [
'driver' => 'file',
'cache' => CacheDefinitionCache::class,
]);
}
public function test_configuration_file_is_published()
{
$this->artisan('vendor:publish', ['--provider' => PurifyServiceProvider::class]);
$this->assertFileExists(config_path('purify.php'));
File::delete(config_path('purify.php'));
File::deleteDirectory(storage_path('app/purify'));
}
public function test_input_is_sanitized()
{
$cleaned = Purify::clean($this->testInput);
$expected = '<p>Test</p>';
$this->assertEquals($expected, $cleaned);
}
public function test_input_arrays_are_sanitized()
{
$cleaned = Purify::clean([$this->testInput, $this->testInput]);
$expected = ['<p>Test</p>', '<p>Test</p>'];
$this->assertEquals($expected, $cleaned);
}
public function test_config_alias_is_available()
{
$instance = Purify::config();
$this->assertInstanceOf(\Stevebauman\Purify\Purify::class, $instance);
}
public function test_config_set_can_be_chosen()
{
$input = '<a href="http://www.google.ca">Google</a>';
$this->app['config']->set('purify.configs.foo', [
'HTML.TargetBlank' => true,
]);
$cleaned = Purify::driver('foo')->clean($input);
$expected = '<a href="http://www.google.ca" target="_blank" rel="noreferrer noopener">Google</a>';
$this->assertEquals($expected, $cleaned);
}
public function test_config_can_be_provided_inline()
{
$input = '<a href="http://www.google.ca">Google</a>';
$cleaned = Purify::config([
'HTML.TargetBlank' => true,
])->clean($input);
$expected = '<a href="http://www.google.ca" target="_blank" rel="noreferrer noopener">Google</a>';
$this->assertEquals($expected, $cleaned);
}
public function test_configs_are_independent()
{
$input = '<a href="http://www.google.ca">Google</a>';
$this->app['config']->set('purify.configs.foo', [
'HTML.TargetBlank' => true,
]);
$this->app['config']->set('purify.configs.bar', [
'HTML.TargetBlank' => true,
'HTML.TargetNoopener' => false,
]);
$cleaned1 = Purify::clean($input);
$cleaned2 = Purify::driver('foo')->clean($input);
$cleaned3 = Purify::driver('bar')->clean($input);
$expected1 = '<a href="http://www.google.ca">Google</a>';
$expected2 = '<a href="http://www.google.ca" target="_blank" rel="noreferrer noopener">Google</a>';
$expected3 = '<a href="http://www.google.ca" target="_blank" rel="noreferrer">Google</a>';
$this->assertEquals($expected1, $cleaned1);
$this->assertEquals($expected2, $cleaned2);
$this->assertEquals($expected3, $cleaned3);
}
public function test_custom_definitions_are_applied()
{
$this->app['config']->set('purify.definitions', FooDefinition::class);
$this->assertEquals(
'<span>Test</span>',
Purify::clean('<span class="foo">Test</span>')
);
$this->assertEquals(
'<span class="foo">Test</span>',
Purify::config(['HTML.Allowed' => 'span[class]'])->clean('<span class="foo">Test</span>')
);
$this->assertEquals(
'<span>Test</span>',
Purify::config(['HTML.Allowed' => 'span[class]'])->clean('<span class="bar">Test</span>')
);
}
public function test_custom_css_definitions_are_applied()
{
$this->app['config']->set('purify.css-definitions', FooCssDefinition::class);
$this->assertEquals(
'<p>Test</p>',
Purify::clean('<p style="text-align:left">Test</p>')
);
$this->assertEquals(
'<p>Test</p>',
Purify::clean('<p style="text-align:right">Test</p>')
);
$this->assertEquals(
'<p style="text-align:center;">Test</p>',
Purify::clean('<p style="text-align:center;">Test</p>')
);
$this->assertEquals(
'<p style="text-align:start;">Test</p>',
Purify::clean('<p style="text-align:start;">Test</p>')
);
$this->assertEquals(
'<p style="text-align:end;">Test</p>',
Purify::clean('<p style="text-align:end;">Test</p>')
);
}
}
class FooDefinition implements Definition
{
public static function apply(HTMLPurifier_HTMLDefinition $definition)
{
$definition->addAttribute('span', 'class', 'Enum#foo');
}
}
class FooCssDefinition implements CssDefinition
{
public static function apply(HTMLPurifier_CSSDefinition $definition)
{
$definition->info['text-align'] = new \HTMLPurifier_AttrDef_Enum(
['center', 'start', 'end'],
false,
);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Stevebauman\Purify\Tests;
use Orchestra\Testbench\TestCase as BaseTestCase;
use Stevebauman\Purify\Commands\ClearCommand;
use Stevebauman\Purify\PurifyManager;
use Stevebauman\Purify\PurifyServiceProvider;
class TestCase extends BaseTestCase
{
protected function tearDown(): void
{
$this->artisan(ClearCommand::class);
parent::tearDown();
}
protected function getPackageAliases($app)
{
return ['Purify' => PurifyManager::class];
}
protected function getPackageProviders($app)
{
return [PurifyServiceProvider::class];
}
}