mirror of
https://github.com/silverstripe/silverstripe-comments
synced 2024-10-22 11:05:49 +02:00
Merge pull request #49 from PutmanMedia/pulls/html-and-preview
This commit is contained in:
commit
cc0ec9b224
@ -31,7 +31,10 @@ 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'),
|
||||||
|
'use_preview' => false, // preview formatted comment (when allowing HTML)
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,7 +13,8 @@ class CommentingController extends Controller {
|
|||||||
'approve',
|
'approve',
|
||||||
'rss',
|
'rss',
|
||||||
'CommentsForm',
|
'CommentsForm',
|
||||||
'doPostComment'
|
'doPostComment',
|
||||||
|
'doPreviewComment'
|
||||||
);
|
);
|
||||||
|
|
||||||
private $baseClass = "";
|
private $baseClass = "";
|
||||||
@ -239,7 +240,7 @@ class CommentingController extends Controller {
|
|||||||
* @return Form
|
* @return Form
|
||||||
*/
|
*/
|
||||||
public function CommentsForm() {
|
public function CommentsForm() {
|
||||||
|
$usePreview = Commenting::get_config_value($this->getBaseClass(), 'use_preview');
|
||||||
$member = Member::currentUser();
|
$member = Member::currentUser();
|
||||||
$fields = new FieldList(
|
$fields = new FieldList(
|
||||||
TextField::create("Name", _t('CommentInterface.YOURNAME', 'Your name'))
|
TextField::create("Name", _t('CommentInterface.YOURNAME', 'Your name'))
|
||||||
@ -263,10 +264,28 @@ class CommentingController extends Controller {
|
|||||||
HiddenField::create("BaseClass")
|
HiddenField::create("BaseClass")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Preview formatted comment. Makes most sense when shortcodes or
|
||||||
|
// limited HTML is allowed. Populated by JS/Ajax.
|
||||||
|
if($usePreview) {
|
||||||
|
$fields->insertAfter(
|
||||||
|
ReadonlyField::create('PreviewComment', _t('CommentInterface.PREVIEWLABEL', 'Preview'))
|
||||||
|
->setAttribute('style', 'display: none'), // enable through JS
|
||||||
|
'Comment'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// save actions
|
// save actions
|
||||||
$actions = new FieldList(
|
$actions = new FieldList(
|
||||||
new FormAction("doPostComment", _t('CommentInterface.POST', 'Post'))
|
new FormAction("doPostComment", _t('CommentInterface.POST', 'Post'))
|
||||||
);
|
);
|
||||||
|
if($usePreview) {
|
||||||
|
$actions->push(
|
||||||
|
FormAction::create('doPreviewComment', _t('CommentInterface.PREVIEW', 'Preview'))
|
||||||
|
->addExtraClass('action-minor')
|
||||||
|
->setAttribute('style', 'display: none') // enable through JS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// required fields for server side
|
// required fields for server side
|
||||||
$required = new RequiredFields(array(
|
$required = new RequiredFields(array(
|
||||||
@ -340,6 +359,8 @@ class CommentingController extends Controller {
|
|||||||
*/
|
*/
|
||||||
public function doPostComment($data, $form) {
|
public function doPostComment($data, $form) {
|
||||||
$class = (isset($data['BaseClass'])) ? $data['BaseClass'] : $this->getBaseClass();
|
$class = (isset($data['BaseClass'])) ? $data['BaseClass'] : $this->getBaseClass();
|
||||||
|
$usePreview = Commenting::get_config_value($class, 'use_preview');
|
||||||
|
$isPreview = ($usePreview && isset($data['preview']) && $data['preview']);
|
||||||
|
|
||||||
// if no class then we cannot work out what controller or model they
|
// if no class then we cannot work out what controller or model they
|
||||||
// are on so throw an error
|
// are on so throw an error
|
||||||
@ -378,7 +399,13 @@ class CommentingController extends Controller {
|
|||||||
$form->saveInto($comment);
|
$form->saveInto($comment);
|
||||||
|
|
||||||
$comment->Moderated = ($moderated) ? false : true;
|
$comment->Moderated = ($moderated) ? false : true;
|
||||||
|
|
||||||
|
// Save into DB, or call pre-save hooks to give accurate preview
|
||||||
|
if($isPreview) {
|
||||||
|
$comment->onBeforeWrite();
|
||||||
|
} else {
|
||||||
$comment->write();
|
$comment->write();
|
||||||
|
}
|
||||||
|
|
||||||
// extend hook to allow extensions. Also see onBeforePostComment
|
// extend hook to allow extensions. Also see onBeforePostComment
|
||||||
$this->extend('onAfterPostComment', $comment);
|
$this->extend('onAfterPostComment', $comment);
|
||||||
@ -401,4 +428,9 @@ class CommentingController extends Controller {
|
|||||||
|
|
||||||
return ($url) ? $this->redirect($url .'#'. $hash) : $this->redirectBack();
|
return ($url) ? $this->redirect($url .'#'. $hash) : $this->redirectBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function doPreviewComment($data, $form) {
|
||||||
|
$data['IsPreview'] = 1;
|
||||||
|
return $this->doPostComment($data, $form);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,5 +12,8 @@
|
|||||||
"require":
|
"require":
|
||||||
{
|
{
|
||||||
"silverstripe/framework": "3.*"
|
"silverstripe/framework": "3.*"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ezyang/htmlpurifier": "4.*"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,10 @@ 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'),
|
||||||
|
'use_preview' => false, // preview formatted comment (when allowing HTML)
|
||||||
);
|
);
|
||||||
|
|
||||||
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 +30,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.
|
||||||
|
@ -9,8 +9,14 @@
|
|||||||
commentsList = $('.comments-list', commentsHolder),
|
commentsList = $('.comments-list', commentsHolder),
|
||||||
pagination = $('.comments-pagination'),
|
pagination = $('.comments-pagination'),
|
||||||
noCommentsYet = $('.no-comments-yet', commentsHolder),
|
noCommentsYet = $('.no-comments-yet', commentsHolder),
|
||||||
form = $('form', container);
|
form = $('form', container),
|
||||||
|
previewEl = form.find('#PreviewComment');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init
|
||||||
|
*/
|
||||||
|
previewEl.hide();
|
||||||
|
$(':submit[name=action_doPreviewComment]').show();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate
|
* Validate
|
||||||
@ -70,7 +76,6 @@
|
|||||||
* this inclues the spam and approve links
|
* this inclues the spam and approve links
|
||||||
*/
|
*/
|
||||||
form.submit(function (e) {
|
form.submit(function (e) {
|
||||||
|
|
||||||
// trigger validation
|
// trigger validation
|
||||||
if(!form.validate().valid()){
|
if(!form.validate().valid()){
|
||||||
return false;
|
return false;
|
||||||
@ -103,6 +108,36 @@
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview comment by fetching it from the server via ajax.
|
||||||
|
*/
|
||||||
|
$(':submit[name=action_doPreviewComment]', form).click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if(!form.validate().valid()) return false;
|
||||||
|
|
||||||
|
previewEl.show().addClass('loading').find('.middleColumn').html(' ');
|
||||||
|
form.ajaxSubmit({
|
||||||
|
success: function(response) {
|
||||||
|
var responseEl = $(response);
|
||||||
|
if(responseEl.is('form')) {
|
||||||
|
// Validation failed, renders form instead of single comment
|
||||||
|
form.replaceWith(responseEl);
|
||||||
|
} else {
|
||||||
|
// Default behaviour
|
||||||
|
previewEl.removeClass('loading').find('.middleColumn').html(responseEl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {'action_doPreviewComment': 1}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide outdated preview on form changes
|
||||||
|
*/
|
||||||
|
$(':input', form).on('change keydown', function() {
|
||||||
|
previewEl.hide();
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clicking one of the metalinks performs the operation via ajax
|
* Clicking one of the metalinks performs the operation via ajax
|
||||||
|
@ -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">
|
||||||
|
@ -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(
|
||||||
|
'<p>my comment</p>',
|
||||||
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user