~ Mar 25, 2021

How To Show Markdown Safely

Using editors is great but can bring a big risk to your code, let's see how we can fix this.


So instead of just echoing the body text, you could first escape the HTML using htmlspecialchars and then parse the markdown.

use League\CommonMark\CommonMarkConverter;

// Let's create the class that does the parsing
$converter = new CommonMarkConverter([
    
// You could also use "escape" hear instead of escaping it yourself.
    
'html_input' => 'allow',
    
'allow_unsafe_links' => false,
]);

// First let's get the text
$markdown $model->body// '# Hello World!'
// Now let's escape the HTML tags
$markdown htmlspecialchars($markdownENT_QUOTES);
// Then parse the markdown to HTML
echo $converter->convertToHtml($markdown); // '<h1>Hello World!</h1>'

Basically, you're now done but this could sometimes take some time to parse the string to HTML and then the viewer of the page needs to wait on this. Also, why parse it every time when somebody goes to the page. So it would be better to cache it, In this case we would use the cache class of Laravel.

$cacheKey "{$model->getTable()}.{$model->id}.body";

echo 
Cache::rememberForever($cacheKey, function() use ($model) {
    
// First let's get the text
    
$markdown $model->body;
    
// Let's also use the escape function from Laraval as well
    
$markdown e($markdown);
    
// And use the Markdown class of Laravel
    // Using this you would need to escape the string first,
    // because it doesn’t escape html
    
return Markdown::parse($markdown);
});

Keep in mind with a somewhat fixed cache key like this one, you would need to delete the cache for this key when the body is updated. You could of course handle this pretty early with the model events of Laravel.

// Let's also create a public method on the model,
// this will reduce the building of the key everywhere
public function bodyCacheKey()
{
    return 
"{$this->getTable()}.{$this->id}.body";
}

protected static function 
booted(): void
{
    static:: 
updating(function ($model) {
        if (
$model->isDirty('body')) {
            
Cache::forget($model->bodyCacheKey());
        }
    });

    
// Also delete it when the record is deleted, to keep you cache storage clean
    
static:: deleting(function ($model) {
        
Cache::forget($model->bodyCacheKey());
    });
}

// Let's also place the parsing into it's own method on the model
public function parsedBody()
{
    return 
Cache::rememberForever($this->bodyCacheKey(), function() {
        
// And let's inline everything
        
return Markdown::parse(e($this->body));
    });

    
// If you want to use the arrow function
    
return Cache::rememberForever(
        
$this->bodyCacheKey(), fn() => Markdown::parse(e($this->body))
    );
}

© 2024 Gertjan Roke. All rights reserved. - ❤️ Since 1990