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); + } }