From e450807b1cce2e9501e47adf8367598d7e4c5528 Mon Sep 17 00:00:00 2001
From: Ingo Schommer
Date: Thu, 21 Feb 2013 16:39:57 +0100
Subject: [PATCH] NEW Optionally allow (sanitized) HTML in comments
---
code/Commenting.php | 4 +-
code/dataobjects/Comment.php | 41 +++++++++++-
composer.json | 3 +
docs/en/Configuration.md | 27 +++++++-
templates/CommentsInterface_singlecomment.ss | 6 +-
tests/CommentsTest.php | 67 ++++++++++++++++++++
6 files changed, 141 insertions(+), 7 deletions(-)
diff --git a/code/Commenting.php b/code/Commenting.php
index 2de8072..1339754 100644
--- a/code/Commenting.php
+++ b/code/Commenting.php
@@ -31,7 +31,9 @@ class Commenting {
'comments_per_page' => 10,
'comments_holder_id' => "comments-holder", // id for the comments holder
'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')
);
/**
diff --git a/code/dataobjects/Comment.php b/code/dataobjects/Comment.php
index 14e3aee..70ceb93 100755
--- a/code/dataobjects/Comment.php
+++ b/code/dataobjects/Comment.php
@@ -9,7 +9,7 @@ class Comment extends DataObject {
public static $db = array(
"Name" => "Varchar(200)",
- "Comment" => "Text",
+ "Comment" => "Text", // can contain sanitized HTML with 'html_allowed=true' config
"Email" => "Varchar(200)",
"URL" => "Varchar(255)",
"BaseClass" => "Varchar(200)",
@@ -55,7 +55,14 @@ class Comment extends DataObject {
'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}
@@ -144,7 +151,7 @@ class Comment extends DataObject {
return ($parent->Title) ? $parent->Title : $parent->ClassName . " #" . $parent->ID;
}
-
+
/**
* Comment-parent classnames obviousely vary, return the parent classname
*
@@ -330,4 +337,34 @@ class Comment extends DataObject {
$fields->replaceField('BaseClass', $baseClassField);
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);
+ }
}
diff --git a/composer.json b/composer.json
index 5d94af1..7183af7 100644
--- a/composer.json
+++ b/composer.json
@@ -12,5 +12,8 @@
"require":
{
"silverstripe/framework": "3.*"
+ },
+ "suggest": {
+ "ezyang/htmlpurifier": "4.*"
}
}
\ No newline at end of file
diff --git a/docs/en/Configuration.md b/docs/en/Configuration.md
index 31ee4de..2e68462 100644
--- a/docs/en/Configuration.md
+++ b/docs/en/Configuration.md
@@ -1,4 +1,6 @@
-## Configuration
+# Configuration
+
+## Overview
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_holder_id' => "comments-holder",
'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
@@ -25,4 +29,21 @@ on the built-in SiteTree commenting) use `set_config_value`
// mysite/_config.php - Returns the setting
Commenting::get_config_value('SiteTree', 'require_login');
-
\ No newline at end of file
+## 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 `my comment
';
+ $comment1->write();
+ $this->assertEquals(
+ 'my comment
',
+ $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 = 'my comment
';
+ $comment2->write();
+ $this->assertEquals(
+ 'my comment
',
+ $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 = 'my comment
';
+ $comment->ParentID = $item->ID;
+ $comment->BaseClass = 'CommentableItem';
+ $comment->write();
+
+ $html = $item->customise(array('CommentsEnabled' => true))->renderWith('CommentsInterface');
+ $this->assertContains(
+ '<p>my comment</p>',
+ $html
+ );
+
+ Commenting::set_config_value('CommentableItem','html_allowed', true);
+ $html = $item->customise(array('CommentsEnabled' => true))->renderWith('CommentsInterface');
+ $this->assertContains(
+ 'my comment
',
+ $html
+ );
+
+ Commenting::set_config_value('CommentableItem','html_allowed', $origAllowed);
+ }
}