NEW Optionally allow (sanitized) HTML in comments

This commit is contained in:
Ingo Schommer 2013-02-21 16:39:57 +01:00
parent 83a6de252f
commit e450807b1c
6 changed files with 141 additions and 7 deletions

View File

@ -31,7 +31,9 @@ class Commenting {
'comments_per_page' => 10, 'comments_per_page' => 10,
'comments_holder_id' => "comments-holder", // id for the comments holder 'comments_holder_id' => "comments-holder", // id for the comments holder
'comment_permalink_prefix' => "comment-", // id prefix for each comment. If needed make this different 'comment_permalink_prefix' => "comment-", // id prefix for each comment. If needed make this different
'require_moderation' => false 'require_moderation' => false,
'html_allowed' => false, // allow for sanitized HTML in comments
'html_allowed_elements' => array('p', 'br', 'a', 'img', 'i', 'b')
); );
/** /**

View File

@ -9,7 +9,7 @@ class Comment extends DataObject {
public static $db = array( public static $db = array(
"Name" => "Varchar(200)", "Name" => "Varchar(200)",
"Comment" => "Text", "Comment" => "Text", // can contain sanitized HTML with 'html_allowed=true' config
"Email" => "Varchar(200)", "Email" => "Varchar(200)",
"URL" => "Varchar(255)", "URL" => "Varchar(255)",
"BaseClass" => "Varchar(200)", "BaseClass" => "Varchar(200)",
@ -55,7 +55,14 @@ class Comment extends DataObject {
'IsSpam' => 'Is Spam' 'IsSpam' => 'Is Spam'
); );
public function onBeforeWrite() {
parent::onBeforeWrite();
// Sanitize HTML, because its expected to be passed to the template unescaped later
if($this->getAllowHtml()) {
$this->Comment = $this->purifyHtml($this->Comment);
}
}
/** /**
* Migrates the old {@link PageComment} objects to {@link Comment} * Migrates the old {@link PageComment} objects to {@link Comment}
@ -330,4 +337,34 @@ class Comment extends DataObject {
$fields->replaceField('BaseClass', $baseClassField); $fields->replaceField('BaseClass', $baseClassField);
return $fields; return $fields;
} }
public function getAllowHtml() {
return (
Commenting::has_commenting($this->BaseClass)
&& Commenting::get_config_value($this->BaseClass, 'html_allowed')
);
}
/**
* @param String $dirtyHtml
* @return String
*/
public function purifyHtml($dirtyHtml) {
$purifier = $this->getHtmlPurifierService();
return $purifier->purify($dirtyHtml);
}
/**
* @return HTMLPurifier (or anything with a "purify()" method)
*/
public function getHtmlPurifierService() {
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.AllowedElements',
Commenting::get_config_value($this->BaseClass, 'html_allowed_elements')
);
$config->set('AutoFormat.AutoParagraph', true);
$config->set('AutoFormat.Linkify', true);
$config->set('URI.DisableExternalResources', true);
return new HTMLPurifier($config);
}
} }

View File

@ -12,5 +12,8 @@
"require": "require":
{ {
"silverstripe/framework": "3.*" "silverstripe/framework": "3.*"
},
"suggest": {
"ezyang/htmlpurifier": "4.*"
} }
} }

View File

@ -1,4 +1,6 @@
## Configuration # Configuration
## Overview
The module provides a number of built in configuration settings below are the default settings The module provides a number of built in configuration settings below are the default settings
@ -13,7 +15,9 @@ The module provides a number of built in configuration settings below are the de
'comments_per_page' => 10, 'comments_per_page' => 10,
'comments_holder_id' => "comments-holder", 'comments_holder_id' => "comments-holder",
'comment_permalink_prefix' => "comment-", 'comment_permalink_prefix' => "comment-",
'require_moderation' => false 'require_moderation' => false,
'html_allowed' => false, // allow for sanitized HTML in comments
'html_allowed_elements' => array('p', 'br', 'a', 'img', 'i', 'b')
); );
If you want to customize any of the configuration options after you have added the extension (or If you want to customize any of the configuration options after you have added the extension (or
@ -25,4 +29,21 @@ on the built-in SiteTree commenting) use `set_config_value`
// mysite/_config.php - Returns the setting // mysite/_config.php - Returns the setting
Commenting::get_config_value('SiteTree', 'require_login'); Commenting::get_config_value('SiteTree', 'require_login');
## HTML Comments
Comments can be configured to contain a restricted set of HTML tags
through the `html_allowed` and `html_allowed_elements` settings.
Raw HTML is hardly user friendly, but combined with a rich-text editor
of your own choosing it can allow rich comment formatting.
In order to use this feature, you need to install the
[HTMLPurifier](http://htmlpurifier.org/) library.
The easiest way to do this is through [Composer](http://getcomposer.org).
{
"require": {"ezyang/htmlpurifier": "4.*"}
}
**Important**: Rendering user-provided HTML on your website always risks
exposing your users to cross-site scripting (XSS) attacks, if the HTML
isn't properly sanitized. Don't allow tags like `<script>` or arbitrary attributes.

View File

@ -1,5 +1,9 @@
<div class="comment" id="$Permalink"> <div class="comment" id="$Permalink">
<% if AllowHtml %>
$Comment.RAW
<% else %>
<p>$Comment.XML</p> <p>$Comment.XML</p>
<% end_if %>
</div> </div>
<p class="info"> <p class="info">

View File

@ -155,6 +155,73 @@ class CommentsTest extends FunctionalTest {
$this->assertEquals($comment->CommenterURL, $protocol . $url, $protocol . ':// is a valid protocol'); $this->assertEquals($comment->CommenterURL, $protocol . $url, $protocol . ':// is a valid protocol');
} }
} }
public function testSanitizesWithAllowHtml() {
if(!class_exists('HTMLPurifier')) {
$this->markTestSkipped('HTMLPurifier class not found');
return;
}
$origAllowed = Commenting::get_config_value('CommentableItem','html_allowed');
// Without HTML allowed
$comment1 = new Comment();
$comment1->BaseClass = 'CommentableItem';
$comment1->Comment = '<p><script>alert("w00t")</script>my comment</p>';
$comment1->write();
$this->assertEquals(
'<p><script>alert("w00t")</script>my comment</p>',
$comment1->Comment,
'Does not remove HTML tags with html_allowed=false, ' .
'which is correct behaviour because the HTML will be escaped'
);
// With HTML allowed
Commenting::set_config_value('CommentableItem','html_allowed', true);
$comment2 = new Comment();
$comment2->BaseClass = 'CommentableItem';
$comment2->Comment = '<p><script>alert("w00t")</script>my comment</p>';
$comment2->write();
$this->assertEquals(
'<p>my comment</p>',
$comment2->Comment,
'Removes HTML tags which are not on the whitelist'
);
Commenting::set_config_value('CommentableItem','html_allowed', $origAllowed);
}
public function testDefaultTemplateRendersHtmlWithAllowHtml() {
if(!class_exists('HTMLPurifier')) {
$this->markTestSkipped('HTMLPurifier class not found');
}
$origAllowed = Commenting::get_config_value('CommentableItem', 'html_allowed');
$item = new CommentableItem();
$item->write();
// Without HTML allowed
$comment = new Comment();
$comment->Comment = '<p>my comment</p>';
$comment->ParentID = $item->ID;
$comment->BaseClass = 'CommentableItem';
$comment->write();
$html = $item->customise(array('CommentsEnabled' => true))->renderWith('CommentsInterface');
$this->assertContains(
'&lt;p&gt;my comment&lt;/p&gt;',
$html
);
Commenting::set_config_value('CommentableItem','html_allowed', true);
$html = $item->customise(array('CommentsEnabled' => true))->renderWith('CommentsInterface');
$this->assertContains(
'<p>my comment</p>',
$html
);
Commenting::set_config_value('CommentableItem','html_allowed', $origAllowed);
}
} }