Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Htmlawed Laravel Package

vanilla/htmlawed

Laravel integration for HTMLawed, a PHP HTML sanitizer and validator. It cleans user-submitted markup, balances tags, filters unsafe attributes and elements, and helps preserve safe formatting in rich text. Useful for sanitizing comments, CMS content, and other untrusted HTML.

View on GitHub
Deep Wiki
Context7

Getting Started

vanilla/htmlawed is a Composer-installable wrapper around htmLawed, a single-file HTML purification/filtering library. It exists because htmLawed itself ships as a raw .php file with no composer.json; this fork adds autoloading so you can require it normally. It is not a PSR-4 class-based package — it registers the global htmLawed() function via Composer's files autoload.

As of v2.2.15 the wrapper bundles upstream htmLawed 1.2.15. There is still no class API, no facade, no service provider, and no config file — the release is an upstream tracking bump, so everything below from the previous assessment still applies.

Minimal setup:

composer require vanilla/htmlawed

Verify autoloading worked (the function is global, not namespaced), and confirm which upstream version you actually resolved:

dd(function_exists('htmLawed')); // true
dd(htmLawed('', [], '', true));  // returns htmLawed's version string, e.g. "1.2.15"

Your first use case, sanitizing user-submitted rich text before storing:

$clean = htmLawed($request->input('body'), [
    'safe'       => 1,
    'elements'   => 'a, b, strong, em, i, u, p, br, ul, ol, li, blockquote, code, pre, h2, h3, h4',
    'attributes' => 'a=href,title,rel; * = class',
    'balance'    => -1,
    'scheme'     => 'http, https, mailto',
]);

Where to look first:

  • The generated htmLawed() call itself — there is no facade, service provider, or config file. Everything is the $config array and the $spec string.
  • Upstream htmLawed docs and changelog (htmLawed() config reference) — this fork tracks it closely, and the wrapper's own release notes are usually just a pointer to an upstream version bump.
  • vendor/vanilla/htmlawed/ for the exact htmLawed file pinned, since security-relevant filtering behavior lives in that single file and changes with each upstream release.

First practical step in a Laravel app: wrap htmLawed() in your own small HtmlSanitizer service (or similarly named class) and bind it into the container, so you never call the global function directly from controllers or Blade.

Implementation Patterns

1. Wrap the global function behind a service class. This is the single most important pattern. It gives you one place to test, one place to change profiles, and mockable behavior.

namespace App\Support;

class HtmlSanitizer
{
    public const PROFILES = [
        'comment' => [
            'safe'     => 1,
            'elements' => 'a, b, i, u, strong, em, p, br',
            'attributes' => 'a=href,title,rel',
        ],
        'article' => [
            'safe'     => 1,
            'elements' => 'a, p, br, ul, ol, li, h2, h3, h4, blockquote, code, pre, strong, em',
            'attributes' => 'a=href,title,rel; * = class',
        ],
    ];

    public function clean(?string $html, string $profile = 'comment'): string
    {
        if ($html === null || trim($html) === '') {
            return '';
        }

        return (string) htmLawed($html, self::PROFILES[$profile] + [
            'balance' => -1,
            'scheme'  => 'http, https, mailto',
        ]);
    }
}

Bind it in a service provider or use app()->singleton(HtmlSanitizer::class).

2. Sanitize at the boundary, escape everywhere else. Store sanitized HTML in a dedicated column (e.g., body_html) and keep the raw input in body_raw. In Blade, use {!! !!} only for the sanitized column and {{ }} for everything else. Never re-sanitize on every render — it is regex-heavy and expensive.

3. Form Request normalization. Sanitize in prepareForValidation or via a sanitize() method so controllers stay clean:

protected function prepareForValidation(): void
{
    if ($this->has('body')) {
        $this->merge([
            'body' => app(HtmlSanitizer::class)->clean($this->input('body'), 'article'),
        ]);
    }
}

4. Validation rule as a defense-in-depth check. Don't rely on the rule to sanitize; use it to reject clearly hostile input, then sanitize anyway.

Validator::extend('safe_html', function ($attribute, $value) {
    $sanitized = app(HtmlSanitizer::class)->clean($value);
    return trim(strip_tags($sanitized)) !== '' || trim($value) === '';
});

5. Blade directives. Register a directive so designers can opt into safe output explicitly:

Blade::directive('safeHtml', function ($expression) {
    return "<?php echo app(\\App\\Support\\HtmlSanitizer::class)->clean({$expression}, 'comment'); ?>";
});

Used as @safeHtml($comment->body).

6. Rich-text editor integration. When importing from TipTap / Quill / TinyMCE, run the payload through htmLawed after the editor's own serialization. This strips editor-specific artifacts (<span style="...">, data-* attributes, inline style) and normalizes tags. Pair this with a DOMDocument-based link-attribute normalizer if you need rel="nofollow ugc" on external URLs — htmLawed's hook config supports attribute rewriting via a callback if you prefer to keep it to one pass.

7. Email template rendering. If users can customize email templates, run the template through a stricter profile (safe => 1, no <a>, no <img>, no attributes) before compiling the Mailable. This is the practical defense against template-injection XSS in notification emails.

8. Output profiles per surface. Maintain two or three named profiles (comment, article, email) rather than passing ad-hoc config arrays in controllers. Config drift is how XSS regressions happen.

9. Upgrade via a fixture corpus, not by eyeballing. Because wrapper releases are upstream bumps with no changelog of their own, keep a set of golden HTML fixtures (nested tags, javascript: hrefs, unbalanced markup, entities, emoji/4-byte UTF-8, data-* attributes, <svg>/<math> payloads) asserted against HtmlSanitizer::clean(). Run it on every composer update; a passing suite plus a green diff on the fixtures is your only real signal that a bundled htmLawed bump did not change filtering behavior under you.

Gotchas and Tips

  • Wrapper versions do not track htmLawed versions. v2.2.15 bundles htmLawed 1.2.15, but the wrapper's own numbering is unrelated (the previous release was v2.2.5). A patch-level composer update can silently swap the entire sanitizer. Treat any version change here as a potential behavior change and regression-test.
  • The release notes are not a changelog. This release's notes are essentially "update htmLawed to 1.2.15" plus a contributor credit — no migration guide, no feature list. Read the upstream htmLawed changelog for the actual filtering/security fixes, and check whether any change affects the config keys you use (safe, elements, attributes, scheme, balance, hook).
  • License is NOASSERTION. The upstream htmLawed is dual-licensed LGPL-3.0 / GPL-2.0, but this fork's metadata is ambiguous. Get legal sign-off before shipping into a commercial product; you may be required to disclose modifications or license your wrapper accordingly.
  • It is a global function, not a class. You cannot mock htmLawed() with Mockery. Test through your wrapper, or use a function_exists guard + a namespaced fallback. Do not call it directly in tests.
  • safe => 1 is not full sanitization. Safe mode disallows known-dangerous elements (script, style, iframe, object) but you still must set elements and attributes explicitly. An attacker who injects <a href="javascript:..."> is only blocked because scheme restricts protocols — always set scheme.
  • style and on* attributes. Never allow * = style or * = on*. If you need styling, allow a narrow class attribute instead and control the CSS on your side.
  • balance requires intent. balance => 1 balances across the whole document (default), -1 balances within block elements, 0 disables (added upstream in 1.2). Unbalanced HTML from a WYSIWYG is common; -1 is the usual safe default but can produce unexpected wrapping.
  • $spec is a string, not an array. The third parameter uses a custom format like 'a=href,title; p=class'. Passing an array throws or silently misconfigures — read the docs before touching it.
  • UTF-8 and entities. htmLawed relies on mbstring; invalid UTF-8 input can be mangled or return an empty string. Normalize encoding (mb_convert_encoding($html, 'UTF-8', 'UTF-8')) before calling if you accept arbitrary user bytes. Recent upstream releases have included PHP 8.x deprecation/compat fixes, so re-run your suite on PHP version upgrades too.
  • Performance. htmLawed is regex-based and slower than DOM-based sanitizers (HTMLPurifier, Symfony HtmlSanitizer). On a 100KB comment it is fine; on a 1MB article, benchmark it. Sanitize once on write, cache the result, and never sanitize inside a loop.
  • Extension points. The hook config accepts a callable that htmLawed invokes for every element/attribute, giving you a last-mile customization point (e.g., force rel="nofollow" on external links, rewrite <img src> to a CDN proxy). It is powerful but undocumented outside upstream, so pin the package version when you use it — hook signatures and element/attribute names are exactly what an upstream bump can shift.
  • Debugging sanitized output. htmLawed silently drops disallowed tags — there is no error, no log, no exception. When output unexpectedly loses tags, bisect the elements/attributes config and check attributes syntax (space after ; is required between element rules in some versions).
  • Double-sanitization. Running htmLawed twice is not harmful but is wasteful and can drop previously-allowed tags if profiles differ. Pick one profile per data path and stick to it.
  • Activity and pinning. The wrapper is low-velocity (previous release 2023-09-29; this one a later upstream catch-up), and it exists primarily to serve Vanilla's own forum product. Pin with a conservative constraint (^2.2 or an exact tag), and prefer composer update --with-dependencies in a PR you can review rather than letting it float. If you need actively maintained sanitization with a class API, Laravel integration, and a real changelog, benchmark symfony/html-sanitizer or mews/purifier before committing.
  • No Artisan commands, no config publishing, no facade. Anything you want around this package — config file, singleton binding, Blade directive, validation rule, fixture corpus — you write yourself. That is the whole cost of adopting it, and it is unchanged in v2.2.15.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core