This commit is contained in:
Christopher Pitt 2015-05-11 22:15:24 +12:00
parent 832fc8685a
commit 725f150c82
13 changed files with 1729 additions and 1066 deletions

View File

@ -1,58 +1,60 @@
<?php <?php
/** /**
* Helper Class for storing the configuration options. Retains the mapping between * Helper Class for storing the configuration options. Retains the mapping between objects which
* objects which have comments attached and the related configuration options. * have comments attached and the related configuration options.
* *
* Also handles adding the Commenting extension to the {@link DataObject} on behalf * Also handles adding the Commenting extension to the {@link DataObject} on behalf of the user.
* of the user.
*
* For documentation on how to use this class see docs/en/Configuration.md
* *
* @deprecated since version 2.0 * @deprecated since version 2.0
* *
* @package comments * @package comments
*/ */
class Commenting { class Commenting {
/** /**
* Adds commenting to a {@link DataObject} * Adds commenting to a {@link DataObject}.
* *
* @deprecated since version 2.0 * @deprecated since version 2.0
* *
* @param string classname to add commenting to * @param string $class
* @param array $settings Settings. See {@link self::$default_config} for * @param bool|array $settings
* available settings
* *
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public static function add($class, $settings = false) { public static function add($class, $settings = false) {
Deprecation::notice('2.0', 'Using Commenting::add is deprecated. Please use the config API instead'); Deprecation::notice('2.0', 'Using Commenting::add is deprecated. Please use the config API instead');
Config::inst()->update($class, 'extensions', array('CommentsExtension')); Config::inst()->update($class, 'extensions', array('CommentsExtension'));
// Check if settings must be customised if($settings === false) {
if($settings === false) return; return;
if(!is_array($settings)) {
throw new InvalidArgumentException('$settings needs to be an array or null');
} }
if(!is_array($settings)) {
throw new InvalidArgumentException(
'$settings needs to be an array or null'
);
}
Config::inst()->update($class, 'comments', $settings); Config::inst()->update($class, 'comments', $settings);
} }
/** /**
* Removes commenting from a {@link DataObject}. Does not remove existing comments * Removes commenting from a {@link DataObject}. Does not remove existing comments but does
* but does remove the extension. * remove the extension.
* *
* @deprecated since version 2.0 * @deprecated since version 2.0
* *
* @param string $class Class to remove {@link CommentsExtension} from * @param string $class
*/ */
public static function remove($class) { public static function remove($class) {
Deprecation::notice('2.0', 'Using Commenting::remove is deprecated. Please use the config API instead'); Deprecation::notice('2.0', 'Using Commenting::remove is deprecated. Please use the config API instead');
$class::remove_extension('CommentsExtension'); $class::remove_extension('CommentsExtension');
} }
/** /**
* Returns whether a given class name has commenting enabled * Returns whether a given class name has commenting enabled.
* *
* @deprecated since version 2.0 * @deprecated since version 2.0
* *
@ -60,87 +62,99 @@ class Commenting {
*/ */
public static function has_commenting($class) { public static function has_commenting($class) {
Deprecation::notice('2.0', 'Using Commenting::has_commenting is deprecated. Please use the config API instead'); Deprecation::notice('2.0', 'Using Commenting::has_commenting is deprecated. Please use the config API instead');
return $class::has_extension('CommentsExtension'); return $class::has_extension('CommentsExtension');
} }
/** /**
* Sets a value for a class of a given config setting. Passing 'all' as the class * Sets a value for a class of a given config setting. Passing 'all' as the class sets it for
* sets it for everything * everything.
* *
* @deprecated since version 2.0 * @deprecated since version 2.0
* *
* @param string $class Class to set the value on. Passing 'all' will set it to all * @param string $class
* active mappings * @param string $key
* @param string $key setting to change * @param mixed $value
* @param mixed $value value of the setting
*/ */
public static function set_config_value($class, $key, $value = false) { public static function set_config_value($class, $key, $value = false) {
Deprecation::notice('2.0', 'Commenting::set_config_value is deprecated. Use the config api instead'); Deprecation::notice('2.0', 'Commenting::set_config_value is deprecated. Use the config api instead');
if($class === "all") $class = 'CommentsExtension';
if($class === "all") {
$class = 'CommentsExtension';
}
Config::inst()->update($class, 'comments', array($key => $value)); Config::inst()->update($class, 'comments', array($key => $value));
} }
/** /**
* Returns a given config value for a commenting class * Returns a given config value for a commenting class.
* *
* @deprecated since version 2.0 * @deprecated since version 2.0
* *
* @param string $class * @param string $class
* @param string $key config value to return * @param string $key config value to return
* *
* @throws Exception
* @return mixed * @return mixed
*
* @throws InvalidArgumentException
*/ */
public static function get_config_value($class, $key) { public static function get_config_value($class, $key) {
Deprecation::notice( Deprecation::notice('2.0', 'Using Commenting::get_config_value is deprecated. Please use $parent->getCommentsOption() or CommentingController::getOption() instead');
'2.0',
'Using Commenting::get_config_value is deprecated. Please use $parent->getCommentsOption() or '
. 'CommentingController::getOption() instead'
);
// Get settings
if(!$class) { if(!$class) {
$class = 'CommentsExtension'; $class = 'CommentsExtension';
} elseif(!$class::has_extension('CommentsExtension')) { } elseif(!$class::has_extension('CommentsExtension')) {
throw new InvalidArgumentException("$class does not have commenting enabled"); throw new InvalidArgumentException(
sprintf('%s does not have commenting enabled', $class)
);
} }
return singleton($class)->getCommentsOption($key); return singleton($class)->getCommentsOption($key);
} }
/** /**
* Determines whether a config value on the commenting extension * Determines whether a config value on the commenting extension matches a given value.
* matches a given value.
* *
* @deprecated since version 2.0 * @deprecated since version 2.0
* *
* @param string $class * @param string $class
* @param string $key * @param string $key
* @param string $value Expected value * @param string $value
* @return boolean *
* @return bool
*/ */
public static function config_value_equals($class, $key, $value) { public static function config_value_equals($class, $key, $value) {
$check = self::get_config_value($class, $key); $check = self::get_config_value($class, $key);
if($check && ($check == $value)) return true;
if($check && ($check == $value)) {
return true;
}
return false;
} }
/** /**
* Return whether a user can post on a given commenting instance * Return whether a user can post on a given commenting instance.
* *
* @deprecated since version 2.0 * @deprecated since version 2.0
* *
* @param string $class * @param string $class
* @return boolean true *
* @return bool
*/ */
public static function can_member_post($class) { public static function can_member_post($class) {
Deprecation::notice('2.0', 'Use $instance->canPostComment() directly instead'); Deprecation::notice('2.0', 'Use $instance->canPostComment() directly instead');
$member = Member::currentUser(); $member = Member::currentUser();
// Check permission
$permission = self::get_config_value($class, 'required_permission'); $permission = self::get_config_value($class, 'required_permission');
if($permission && !Permission::check($permission)) return false;
// Check login required if($permission && !Permission::check($permission)) {
return false;
}
$requireLogin = self::get_config_value($class, 'require_login'); $requireLogin = self::get_config_value($class, 'require_login');
return !$requireLogin || $member; return !$requireLogin || $member;
} }
} }

View File

@ -1,18 +1,29 @@
<?php <?php
/** /**
* Comment administration system within the CMS * Comment administration system within the CMS.
* *
* @package comments * @package comments
*/ */
class CommentAdmin extends LeftAndMain implements PermissionProvider { class CommentAdmin extends LeftAndMain implements PermissionProvider {
/**
* @var string
*/
private static $url_segment = 'comments'; private static $url_segment = 'comments';
/**
* @var string
*/
private static $url_rule = '/$Action'; private static $url_rule = '/$Action';
/**
* @var string
*/
private static $menu_title = 'Comments'; private static $menu_title = 'Comments';
/**
* @var array
*/
private static $allowed_actions = array( private static $allowed_actions = array(
'approvedmarked', 'approvedmarked',
'deleteall', 'deleteall',
@ -21,25 +32,29 @@ class CommentAdmin extends LeftAndMain implements PermissionProvider {
'showtable', 'showtable',
'spammarked', 'spammarked',
'EditForm', 'EditForm',
'unmoderated' 'unmoderated',
); );
/**
* {@inheritdoc}
*/
public function providePermissions() { public function providePermissions() {
return array( return array(
"CMS_ACCESS_CommentAdmin" => array( 'CMS_ACCESS_CommentAdmin' => array(
'name' => _t('CommentAdmin.ADMIN_PERMISSION', "Access to 'Comments' section"), 'name' => _t('CommentAdmin.ADMIN_PERMISSION', 'Access to \'Comments\' section'),
'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access') 'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
) ),
); );
} }
/** /**
* @return Form * {@inheritdoc}
*/ */
public function getEditForm($id = null, $fields = null) { public function getEditForm($id = null, $fields = null) {
if(!$id) $id = $this->currentPageID(); if(!$id) {
$id = $this->currentPageID();
}
$form = parent::getEditForm($id);
$record = $this->getRecord($id); $record = $this->getRecord($id);
if($record && !$record->canView()) { if($record && !$record->canView()) {
@ -48,61 +63,82 @@ class CommentAdmin extends LeftAndMain implements PermissionProvider {
$commentsConfig = CommentsGridFieldConfig::create(); $commentsConfig = CommentsGridFieldConfig::create();
$newComments = Comment::get()->filter('Moderated', 0); $newComments = Comment::get()
->filter('Moderated', 0);
$newGrid = new CommentsGridField( $newCommentsGrid = new CommentsGridField(
'NewComments', 'NewComments',
_t('CommentsAdmin.NewComments', 'New'), _t('CommentsAdmin.NewComments', 'New'),
$newComments, $newComments,
$commentsConfig $commentsConfig
); );
$approvedComments = Comment::get()->filter('Moderated', 1)->filter('IsSpam', 0); $newCommentsCountLabel = sprintf('(%s)', count($newComments));
$approvedGrid = new CommentsGridField( $approvedComments = Comment::get()
->filter('Moderated', 1)
->filter('IsSpam', 0);
$approvedCommentsGrid = new CommentsGridField(
'ApprovedComments', 'ApprovedComments',
_t('CommentsAdmin.ApprovedComments', 'Approved'), _t('CommentsAdmin.ApprovedComments', 'Approved'),
$approvedComments, $approvedComments,
$commentsConfig $commentsConfig
); );
$spamComments = Comment::get()->filter('Moderated', 1)->filter('IsSpam', 1); $approvedCommentsCountLabel = sprintf('(%s)', count($approvedComments));
$spamGrid = new CommentsGridField( $spamComments = Comment::get()
->filter('Moderated', 1)
->filter('IsSpam', 1);
$spamCommentsGrid = new CommentsGridField(
'SpamComments', 'SpamComments',
_t('CommentsAdmin.SpamComments', 'Spam'), _t('CommentsAdmin.SpamComments', 'Spam'),
$spamComments, $spamComments,
$commentsConfig $commentsConfig
); );
$newCount = '(' . count($newComments) . ')'; $spamCommentsCountLabel = sprintf('(%s)', count($spamComments));
$approvedCount = '(' . count($approvedComments) . ')';
$spamCount = '(' . count($spamComments) . ')';
$fields = new FieldList( $tabSet = new TabSet(
$root = new TabSet( 'Root',
'Root', new Tab(
new Tab('NewComments', _t('CommentAdmin.NewComments', 'New') . ' ' . $newCount, 'NewComments',
$newGrid sprintf(
'%s %s',
_t('CommentAdmin.NewComments', 'New'),
$newCommentsCountLabel
), ),
new Tab('ApprovedComments', _t('CommentAdmin.ApprovedComments', 'Approved') . ' ' . $approvedCount, $newCommentsGrid
$approvedGrid ),
new Tab(
'ApprovedComments',
sprintf(
'%s %s',
_t('CommentAdmin.ApprovedComments', 'Approved'),
$approvedCommentsCountLabel
), ),
new Tab('SpamComments', _t('CommentAdmin.SpamComments', 'Spam') . ' ' . $spamCount, $approvedCommentsGrid
$spamGrid ),
) new Tab(
'SpamComments',
sprintf(
'%s %s',
_t('CommentAdmin.SpamComments', 'Spam'),
$spamCommentsCountLabel
),
$spamCommentsGrid
) )
); );
$root->setTemplate('CMSTabSet'); $tabSet->setTemplate('CMSTabSet');
$actions = new FieldList();
$form = new Form( $form = new Form(
$this, $this,
'EditForm', 'EditForm',
$fields, new FieldList($tabSet),
$actions new FieldList()
); );
$form->addExtraClass('cms-edit-form'); $form->addExtraClass('cms-edit-form');

View File

@ -3,6 +3,8 @@
class CommentsGridField extends GridField { class CommentsGridField extends GridField {
/** /**
* {@inheritdoc} * {@inheritdoc}
*
* @param Comment $record
*/ */
protected function newRow($total, $index, $record, $attributes, $content) { protected function newRow($total, $index, $record, $attributes, $content) {
if(!isset($attributes['class'])) { if(!isset($attributes['class'])) {

View File

@ -14,7 +14,9 @@ class CommentsGridFieldAction implements GridField_ColumnProvider, GridField_Act
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getColumnAttributes($gridField, $record, $columnName) { public function getColumnAttributes($gridField, $record, $columnName) {
return array('class' => 'col-buttons'); return array(
'class' => 'col-buttons',
);
} }
/** /**
@ -22,24 +24,34 @@ class CommentsGridFieldAction implements GridField_ColumnProvider, GridField_Act
*/ */
public function getColumnMetadata($gridField, $columnName) { public function getColumnMetadata($gridField, $columnName) {
if($columnName == 'Actions') { if($columnName == 'Actions') {
return array('title' => ''); return array(
'title' => '',
);
} }
return array();
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getColumnsHandled($gridField) { public function getColumnsHandled($gridField) {
return array('Actions'); return array(
'Actions',
);
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*
* @param Comment $record
*/ */
public function getColumnContent($gridField, $record, $columnName) { public function getColumnContent($gridField, $record, $columnName) {
if(!$record->canEdit()) return; if(!$record->canEdit()) {
return '';
}
$field = ""; $field = '';
if(!$record->IsSpam || !$record->Moderated) { if(!$record->IsSpam || !$record->Moderated) {
$field .= GridField_FormAction::create( $field .= GridField_FormAction::create(
@ -47,7 +59,9 @@ class CommentsGridFieldAction implements GridField_ColumnProvider, GridField_Act
'CustomAction' . $record->ID, 'CustomAction' . $record->ID,
'Spam', 'Spam',
'spam', 'spam',
array('RecordID' => $record->ID) array(
'RecordID' => $record->ID,
)
)->Field(); )->Field();
} }
@ -57,7 +71,9 @@ class CommentsGridFieldAction implements GridField_ColumnProvider, GridField_Act
'CustomAction' . $record->ID, 'CustomAction' . $record->ID,
'Approve', 'Approve',
'approve', 'approve',
array('RecordID' => $record->ID) array(
'RecordID' => $record->ID,
)
)->Field(); )->Field();
} }
@ -68,7 +84,10 @@ class CommentsGridFieldAction implements GridField_ColumnProvider, GridField_Act
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getActions($gridField) { public function getActions($gridField) {
return array('spam', 'approve'); return array(
'spam',
'approve',
);
} }
/** /**
@ -76,25 +95,37 @@ class CommentsGridFieldAction implements GridField_ColumnProvider, GridField_Act
*/ */
public function handleAction(GridField $gridField, $actionName, $arguments, $data) { public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
if($actionName == 'spam') { if($actionName == 'spam') {
$comment = Comment::get()->byID($arguments["RecordID"]); /**
* @var Comment $comment
*/
$comment = Comment::get()
->byID($arguments["RecordID"]);
$comment->markSpam(); $comment->markSpam();
// output a success message to the user Controller::curr()
Controller::curr()->getResponse()->setStatusCode( ->getResponse()
200, ->setStatusCode(
'Comment marked as spam.' 200,
); 'Comment marked as spam.'
);
} }
if($actionName == 'approve') { if($actionName == 'approve') {
$comment = Comment::get()->byID($arguments["RecordID"]); /**
* @var Comment $comment
*/
$comment = Comment::get()
->byID($arguments["RecordID"]);
$comment->markApproved(); $comment->markApproved();
// output a success message to the user Controller::curr()
Controller::curr()->getResponse()->setStatusCode( ->getResponse()
200, ->setStatusCode(
'Comment approved.' 200,
); 'Comment approved.'
);
} }
} }
} }

View File

@ -8,54 +8,80 @@ class CommentsGridFieldBulkAction extends GridFieldBulkActionHandler {
} }
/** /**
* A {@link GridFieldBulkActionHandler} for bulk marking comments as spam * A {@link GridFieldBulkActionHandler} for bulk marking comments as spam.
* *
* @package comments * @package comments
*/ */
class CommentsGridFieldBulkAction_Handlers extends CommentsGridFieldBulkAction { class CommentsGridFieldBulkAction_Handlers extends CommentsGridFieldBulkAction {
/**
* @var array
*/
private static $allowed_actions = array( private static $allowed_actions = array(
'spam', 'spam',
'approve', 'approve',
); );
/**
* @var array
*/
private static $url_handlers = array( private static $url_handlers = array(
'spam' => 'spam', 'spam' => 'spam',
'approve' => 'approve', 'approve' => 'approve',
); );
/**
* @param SS_HTTPRequest $request
*
* @return SS_HTTPResponse
*/
public function spam(SS_HTTPRequest $request) { public function spam(SS_HTTPRequest $request) {
$ids = array(); $ids = array();
foreach($this->getRecords() as $record) { foreach($this->getRecords() as $record) {
array_push($ids, $record->ID); /**
* @var Comment $record
*/
$record->markSpam(); $record->markSpam();
array_push($ids, $record->ID);
} }
$response = new SS_HTTPResponse(Convert::raw2json(array( $response = new SS_HTTPResponse(
'done' => true, Convert::raw2json(array(
'records' => $ids 'done' => true,
))); 'records' => $ids,
))
);
$response->addHeader('Content-Type', 'text/json'); $response->addHeader('Content-Type', 'text/json');
return $response; return $response;
} }
/**
* @param SS_HTTPRequest $request
*
* @return SS_HTTPResponse
*/
public function approve(SS_HTTPRequest $request) { public function approve(SS_HTTPRequest $request) {
$ids = array(); $ids = array();
foreach($this->getRecords() as $record) { foreach($this->getRecords() as $record) {
array_push($ids, $record->ID); /**
* @var Comment $record
*/
$record->markApproved(); $record->markApproved();
array_push($ids, $record->ID);
} }
$response = new SS_HTTPResponse(Convert::raw2json(array( $response = new SS_HTTPResponse(
'done' => true, Convert::raw2json(array(
'records' => $ids 'done' => true,
))); 'records' => $ids,
))
);
$response->addHeader('Content-Type', 'text/json'); $response->addHeader('Content-Type', 'text/json');

View File

@ -1,17 +1,24 @@
<?php <?php
/**
* @method static CommentsGridFieldConfig create()
*/
class CommentsGridFieldConfig extends GridFieldConfig_RecordEditor { class CommentsGridFieldConfig extends GridFieldConfig_RecordEditor {
/**
* {@inheritdoc}
*/
public function __construct($itemsPerPage = 25) { public function __construct($itemsPerPage = 25) {
parent::__construct($itemsPerPage); parent::__construct($itemsPerPage);
// $this->addComponent(new GridFieldExportButton());
$this->addComponent(new CommentsGridFieldAction()); $this->addComponent(new CommentsGridFieldAction());
// Format column /**
* @var GridFieldDataColumns $columns
*/
$columns = $this->getComponentByType('GridFieldDataColumns'); $columns = $this->getComponentByType('GridFieldDataColumns');
$columns->setFieldFormatting(array( $columns->setFieldFormatting(array(
'ParentTitle' => function($value, &$item) { 'ParentTitle' => function ($value, &$item) {
return sprintf( return sprintf(
'<a href="%s" class="cms-panel-link external-link action" target="_blank">%s</a>', '<a href="%s" class="cms-panel-link external-link action" target="_blank">%s</a>',
Convert::raw2att($item->Link()), Convert::raw2att($item->Link()),
@ -20,24 +27,27 @@ class CommentsGridFieldConfig extends GridFieldConfig_RecordEditor {
} }
)); ));
// Add bulk option
$manager = new GridFieldBulkManager(); $manager = new GridFieldBulkManager();
$manager->addBulkAction( $manager->addBulkAction(
'spam', 'Spam', 'CommentsGridFieldBulkAction_Handlers', 'spam',
'Spam',
'CommentsGridFieldBulkAction_Handlers',
array( array(
'isAjax' => true, 'isAjax' => true,
'icon' => 'cross', 'icon' => 'cross',
'isDestructive' => false 'isDestructive' => false,
) )
); );
$manager->addBulkAction( $manager->addBulkAction(
'approve', 'Approve', 'CommentsGridFieldBulkAction_Handlers', 'approve',
'Approve',
'CommentsGridFieldBulkAction_Handlers',
array( array(
'isAjax' => true, 'isAjax' => true,
'icon' => 'cross', 'icon' => 'cross',
'isDestructive' => false 'isDestructive' => false,
) )
); );

View File

@ -3,9 +3,10 @@
/** /**
* @package comments * @package comments
*/ */
class CommentingController extends Controller { class CommentingController extends Controller {
/**
* @var array
*/
private static $allowed_actions = array( private static $allowed_actions = array(
'delete', 'delete',
'spam', 'spam',
@ -15,55 +16,59 @@ class CommentingController extends Controller {
'CommentsForm', 'CommentsForm',
'reply', 'reply',
'doPostComment', 'doPostComment',
'doPreviewComment' 'doPreviewComment',
); );
/**
* @var array
*/
private static $url_handlers = array( private static $url_handlers = array(
'reply/$ParentCommentID//$ID/$OtherID' => 'reply', 'reply/$ParentCommentID//$ID/$OtherID' => 'reply',
); );
/** /**
* Fields required for this form * Fields required for this form.
*
* @config
* *
* @var array * @var array
* @config
*/ */
private static $required_fields = array( private static $required_fields = array(
'Name', 'Name',
'Email', 'Email',
'Comment' 'Comment',
); );
/** /**
* Base class this commenting form is for * Base class this commenting form is for.
* *
* @var string * @var string
*/ */
private $baseClass = ""; private $baseClass = '';
/** /**
* The record this commenting form is for * The record this commenting form is for.
* *
* @var DataObject * @var null|DataObject
*/ */
private $ownerRecord = null; private $ownerRecord = null;
/** /**
* Parent controller record * Parent controller record.
* *
* @var Controller * @var null|Controller
*/ */
private $ownerController = null; private $ownerController = null;
/** /**
* Backup url to return to * Backup url to return to.
* *
* @var string * @var null|string
*/ */
protected $fallbackReturnURL = null; protected $fallbackReturnURL = null;
/** /**
* Set the base class to use * Set the base class to use.
* *
* @param string $class * @param string $class
*/ */
@ -72,7 +77,7 @@ class CommentingController extends Controller {
} }
/** /**
* Get the base class used * Get the base class used.
* *
* @return string * @return string
*/ */
@ -81,7 +86,7 @@ class CommentingController extends Controller {
} }
/** /**
* Set the record this controller is working on * Set the record this controller is working on.
* *
* @param DataObject $record * @param DataObject $record
*/ */
@ -90,16 +95,16 @@ class CommentingController extends Controller {
} }
/** /**
* Get the record * Get the record.
* *
* @return DataObject * @return null|DataObject
*/ */
public function getOwnerRecord() { public function getOwnerRecord() {
return $this->ownerRecord; return $this->ownerRecord;
} }
/** /**
* Set the parent controller * Set the parent controller.
* *
* @param Controller $controller * @param Controller $controller
*/ */
@ -108,46 +113,42 @@ class CommentingController extends Controller {
} }
/** /**
* Get the parent controller * Get the parent controller.
* *
* @return Controller * @return null|Controller
*/ */
public function getOwnerController() { public function getOwnerController() {
return $this->ownerController; return $this->ownerController;
} }
/** /**
* Get the commenting option for the current state * Get the commenting option for the current state.
* *
* @param string $key * @param string $key
* @return mixed Result if the setting is available, or null otherwise *
* @return mixed
*/ */
public function getOption($key) { public function getOption($key) {
// If possible use the current record
if($record = $this->getOwnerRecord()) { if($record = $this->getOwnerRecord()) {
return $record->getCommentsOption($key); return $record->getCommentsOption($key);
} }
// Otherwise a singleton of that record
if($class = $this->getBaseClass()) { if($class = $this->getBaseClass()) {
return singleton($class)->getCommentsOption($key); return singleton($class)->getCommentsOption($key);
} }
// Otherwise just use the default options
return singleton('CommentsExtension')->getCommentsOption($key); return singleton('CommentsExtension')->getCommentsOption($key);
} }
/** /**
* Workaround for generating the link to this controller * {@inheritdoc}
*
* @return string
*/ */
public function Link($action = '', $id = '', $other = '') { public function Link($action = '', $id = '', $other = '') {
return Controller::join_links(Director::baseURL(), __CLASS__ , $action, $id, $other); return Controller::join_links(Director::baseURL(), __CLASS__, $action, $id, $other);
} }
/** /**
* Outputs the RSS feed of comments * Outputs the RSS feed of comments.
* *
* @return HTMLText * @return HTMLText
*/ */
@ -156,12 +157,11 @@ class CommentingController extends Controller {
} }
/** /**
* Return an RSSFeed of comments for a given set of comments or all * Return an RSSFeed of comments for a given set of comments or all comments on the website.
* comments on the website.
* *
* To maintain backwards compatibility with 2.4 this supports mapping * To maintain backwards compatibility with 2.4 this supports mapping of
* of PageComment/rss?pageid= as well as the new RSS format for comments * PageComment/rss?pageid= as well as the new RSS format for comments of
* of CommentingController/rss/{classname}/{id} * CommentingController/rss/{classname}/{id}
* *
* @param SS_HTTPRequest * @param SS_HTTPRequest
* *
@ -172,26 +172,26 @@ class CommentingController extends Controller {
$class = $request->param('ID'); $class = $request->param('ID');
$id = $request->param('OtherID'); $id = $request->param('OtherID');
// Support old pageid param
if(!$id && !$class && ($id = $request->getVar('pageid'))) { if(!$id && !$class && ($id = $request->getVar('pageid'))) {
$class = 'SiteTree'; $class = 'SiteTree';
} }
$comments = Comment::get()->filter(array( $comments = Comment::get()
'Moderated' => 1, ->filter(array(
'IsSpam' => 0, 'Moderated' => 1,
)); 'IsSpam' => 0,
));
// Check if class filter
if($class) { if($class) {
if(!is_subclass_of($class, 'DataObject') || !$class::has_extension('CommentsExtension')) { if(!is_subclass_of($class, 'DataObject') || !$class::has_extension('CommentsExtension')) {
return $this->httpError(404); return $this->httpError(404);
} }
$this->setBaseClass($class); $this->setBaseClass($class);
$comments = $comments->filter('BaseClass', $class); $comments = $comments->filter('BaseClass', $class);
$link = Controller::join_links($link, $class); $link = Controller::join_links($link, $class);
// Check if id filter
if($id) { if($id) {
$comments = $comments->filter('ParentID', $id); $comments = $comments->filter('ParentID', $id);
$link = Controller::join_links($link, $id); $link = Controller::join_links($link, $id);
@ -199,17 +199,22 @@ class CommentingController extends Controller {
} }
} }
$title = _t('CommentingController.RSSTITLE', "Comments RSS Feed"); $title = _t('CommentingController.RSSTITLE', 'Comments RSS Feed');
$comments = new PaginatedList($comments, $request); $comments = new PaginatedList($comments, $request);
$comments->setPageLength($this->getOption('comments_per_page'));
$comments->setPageLength(
$this->getOption('comments_per_page')
);
return new RSSFeed( return new RSSFeed(
$comments, $comments,
$link, $link,
$title, $title,
$link, $link,
'Title', 'EscapedComment', 'AuthorName' 'Title',
'EscapedComment',
'AuthorName'
); );
} }
@ -218,35 +223,53 @@ class CommentingController extends Controller {
*/ */
public function delete() { public function delete() {
$comment = $this->getComment(); $comment = $this->getComment();
if(!$comment) return $this->httpError(404);
if(!$comment) {
$this->httpError(404);
}
if(!$comment->canDelete()) { if(!$comment->canDelete()) {
return Security::permissionFailure($this, 'You do not have permission to delete this comment'); return Security::permissionFailure($this, 'You do not have permission to delete this comment');
} }
if(!$comment->getSecurityToken()->checkRequest($this->request)) return $this->httpError(400);
if(!$comment->getSecurityToken()->checkRequest($this->request)) {
$this->httpError(400);
}
$comment->delete(); $comment->delete();
return $this->request->isAjax() if($this->request->isAjax()) {
? true return true;
: $this->redirectBack(); }
return $this->redirectBack();
} }
/** /**
* Marks a given {@link Comment} as spam. Removes the comment from display * Marks a given {@link Comment} as spam. Removes the comment from display.
*/ */
public function spam() { public function spam() {
$comment = $this->getComment(); $comment = $this->getComment();
if(!$comment) return $this->httpError(404);
if(!$comment) {
$this->httpError(404);
}
if(!$comment->canEdit()) { if(!$comment->canEdit()) {
return Security::permissionFailure($this, 'You do not have permission to edit this comment'); return Security::permissionFailure($this, 'You do not have permission to edit this comment');
} }
if(!$comment->getSecurityToken()->checkRequest($this->request)) return $this->httpError(400);
if(!$comment->getSecurityToken()->checkRequest($this->request)) {
$this->httpError(400);
}
$comment->markSpam(); $comment->markSpam();
return $this->request->isAjax() if($this->request->isAjax()) {
? $comment->renderWith('CommentsInterface_singlecomment') return true;
: $this->redirectBack(); }
return $this->redirectBack();
} }
/** /**
@ -254,17 +277,26 @@ class CommentingController extends Controller {
*/ */
public function ham() { public function ham() {
$comment = $this->getComment(); $comment = $this->getComment();
if(!$comment) return $this->httpError(404);
if(!$comment) {
$this->httpError(404);
}
if(!$comment->canEdit()) { if(!$comment->canEdit()) {
return Security::permissionFailure($this, 'You do not have permission to edit this comment'); return Security::permissionFailure($this, 'You do not have permission to edit this comment');
} }
if(!$comment->getSecurityToken()->checkRequest($this->request)) return $this->httpError(400);
if(!$comment->getSecurityToken()->checkRequest($this->request)) {
$this->httpError(400);
}
$comment->markApproved(); $comment->markApproved();
return $this->request->isAjax() if($this->request->isAjax()) {
? $comment->renderWith('CommentsInterface_singlecomment') return true;
: $this->redirectBack(); }
return $this->redirectBack();
} }
/** /**
@ -272,33 +304,42 @@ class CommentingController extends Controller {
*/ */
public function approve() { public function approve() {
$comment = $this->getComment(); $comment = $this->getComment();
if(!$comment) return $this->httpError(404);
if(!$comment) {
$this->httpError(404);
}
if(!$comment->canEdit()) { if(!$comment->canEdit()) {
return Security::permissionFailure($this, 'You do not have permission to approve this comment'); return Security::permissionFailure($this, 'You do not have permission to approve this comment');
} }
if(!$comment->getSecurityToken()->checkRequest($this->request)) return $this->httpError(400);
if(!$comment->getSecurityToken()->checkRequest($this->request)) {
$this->httpError(400);
}
$comment->markApproved(); $comment->markApproved();
return $this->request->isAjax() if($this->request->isAjax()) {
? $comment->renderWith('CommentsInterface_singlecomment') return true;
: $this->redirectBack(); }
return $this->redirectBack();
} }
/** /**
* Returns the comment referenced in the URL (by ID). Permission checking * Returns the comment referenced in the URL. Permission checking should be done in the callee.
* should be done in the callee.
* *
* @return Comment|false * @return bool|Comment
*/ */
public function getComment() { public function getComment() {
$id = isset($this->urlParams['ID']) ? $this->urlParams['ID'] : false; if(isset($this->urlParams['ID'])) {
$id = $this->urlParams['ID'];
if($id) {
$comment = DataObject::get_by_id('Comment', $id); $comment = DataObject::get_by_id('Comment', $id);
if($comment) { if($comment) {
$this->fallbackReturnURL = $comment->Link(); $this->fallbackReturnURL = $comment->Link();
return $comment; return $comment;
} }
} }
@ -307,163 +348,147 @@ class CommentingController extends Controller {
} }
/** /**
* Create a reply form for a specified comment * Create a reply form for a specified comment.
* *
* @param Comment $comment * @param Comment $comment
*
* @return Form
*/ */
public function ReplyForm($comment) { public function ReplyForm($comment) {
// Enables multiple forms with different names to use the same handler
$form = $this->CommentsForm(); $form = $this->CommentsForm();
$form->setName('ReplyForm_'.$comment->ID); $form->setName('ReplyForm_' . $comment->ID);
$form->addExtraClass('reply-form'); $form->addExtraClass('reply-form');
// Load parent into reply form
$form->loadDataFrom(array( $form->loadDataFrom(array(
'ParentCommentID' => $comment->ID 'ParentCommentID' => $comment->ID,
)); ));
// Customise action $form->setFormAction(
$form->setFormAction($this->Link('reply', $comment->ID)); $this->Link('reply', $comment->ID)
);
$this->extend('updateReplyForm', $form); $this->extend('updateReplyForm', $form);
return $form; return $form;
} }
/** /**
* Request handler for reply form. * Request handler for reply form.
* This method will disambiguate multiple reply forms in the same method *
* This method will disambiguate multiple reply forms in the same method.
* *
* @param SS_HTTPRequest $request * @param SS_HTTPRequest $request
*
* @return null|Form
*/ */
public function reply(SS_HTTPRequest $request) { public function reply(SS_HTTPRequest $request) {
// Extract parent comment from reply and build this way
if($parentID = $request->param('ParentCommentID')) { if($parentID = $request->param('ParentCommentID')) {
/**
* @var null|Comment $comment
*/
$comment = DataObject::get_by_id('Comment', $parentID, true); $comment = DataObject::get_by_id('Comment', $parentID, true);
if($comment) { if($comment) {
return $this->ReplyForm($comment); return $this->ReplyForm($comment);
} }
} }
return $this->httpError(404);
$this->httpError(404);
return null;
} }
/** /**
* Post a comment form * Post a comment form.
* *
* @return Form * @return Form
*/ */
public function CommentsForm() { public function CommentsForm() {
$usePreview = $this->getOption('use_preview'); $usePreview = $this->getOption('use_preview');
$nameRequired = _t('CommentInterface.YOURNAME_MESSAGE_REQUIRED', 'Please enter your name');
$emailRequired = _t('CommentInterface.EMAILADDRESS_MESSAGE_REQUIRED', 'Please enter your email address');
$emailInvalid = _t('CommentInterface.EMAILADDRESS_MESSAGE_EMAIL', 'Please enter a valid email address');
$urlInvalid = _t('CommentInterface.COMMENT_MESSAGE_URL', 'Please enter a valid URL');
$commentRequired = _t('CommentInterface.COMMENT_MESSAGE_REQUIRED', 'Please enter your comment');
$fields = new FieldList( $fields = new FieldList(
$dataFields = new CompositeField( $dataFields = new CompositeField(
// Name $this->getNameField(),
TextField::create("Name", _t('CommentInterface.YOURNAME', 'Your name')) $this->getEmailField(),
->setCustomValidationMessage($nameRequired) $this->getURLField(),
->setAttribute('data-msg-required', $nameRequired), $this->getCommentField()
// Email
EmailField::create(
"Email",
_t('CommentingController.EMAILADDRESS', "Your email address (will not be published)")
)
->setCustomValidationMessage($emailRequired)
->setAttribute('data-msg-required', $emailRequired)
->setAttribute('data-msg-email', $emailInvalid)
->setAttribute('data-rule-email', true),
// Url
TextField::create("URL", _t('CommentingController.WEBSITEURL', "Your website URL"))
->setAttribute('data-msg-url', $urlInvalid)
->setAttribute('data-rule-url', true),
// Comment
TextareaField::create("Comment", _t('CommentingController.COMMENTS', "Comments"))
->setCustomValidationMessage($commentRequired)
->setAttribute('data-msg-required', $commentRequired)
), ),
HiddenField::create("ParentID"), HiddenField::create('ParentID'),
HiddenField::create("ReturnURL"), HiddenField::create('ReturnURL'),
HiddenField::create("ParentCommentID"), HiddenField::create('ParentCommentID'),
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) { if($usePreview) {
$fields->insertAfter( $fields->insertAfter(
ReadonlyField::create('PreviewComment', _t('CommentInterface.PREVIEWLABEL', 'Preview')) $this->getPreviewCommentField(),
->setAttribute('style', 'display: none'), // enable through JS
'Comment' 'Comment'
); );
} }
$dataFields->addExtraClass('data-fields'); $dataFields->addExtraClass('data-fields');
// save actions
$actions = new FieldList( $actions = new FieldList(
new FormAction("doPostComment", _t('CommentInterface.POST', 'Post')) new FormAction(
'doPostComment',
_t('CommentInterface.POST', 'Post')
)
); );
if($usePreview) { if($usePreview) {
$actions->push( $actions->push(
FormAction::create('doPreviewComment', _t('CommentInterface.PREVIEW', 'Preview')) $this->getPreviewCommentAction()
->addExtraClass('action-minor')
->setAttribute('style', 'display: none') // enable through JS
); );
} }
// required fields for server side
$required = new RequiredFields($this->config()->required_fields); $required = new RequiredFields($this->config()->required_fields);
// create the comment form $form = new Form(
$form = new Form($this, 'CommentsForm', $fields, $actions, $required); $this,
'CommentsForm',
$fields,
$actions,
$required
);
// if the record exists load the extra required data
if($record = $this->getOwnerRecord()) { if($record = $this->getOwnerRecord()) {
// Load member data
$member = Member::currentUser(); $member = Member::currentUser();
if(($record->CommentsRequireLogin || $record->PostingRequiredPermission) && $member) { if(($record->CommentsRequireLogin || $record->PostingRequiredPermission) && $member) {
$fields = $form->Fields(); $fields = $form->Fields();
$fields->removeByName('Name'); $fields->removeByName('Name');
$fields->removeByName('Email'); $fields->removeByName('Email');
$fields->insertBefore(new ReadonlyField("NameView", _t('CommentInterface.YOURNAME', 'Your name'), $member->getName()), 'URL'); $fields->insertBefore(new ReadonlyField('NameView', _t('CommentInterface.YOURNAME', 'Your name'), $member->getName()), 'URL');
$fields->push(new HiddenField("Name", "", $member->getName())); $fields->push(new HiddenField('Name', '', $member->getName()));
$fields->push(new HiddenField("Email", "", $member->Email)); $fields->push(new HiddenField('Email', '', $member->Email));
} }
// we do not want to read a new URL when the form has already been submitted
// which in here, it hasn't been.
$form->loadDataFrom(array( $form->loadDataFrom(array(
'ParentID' => $record->ID, 'ParentID' => $record->ID,
'ReturnURL' => $this->request->getURL(), 'ReturnURL' => $this->request->getURL(),
'BaseClass' => $this->getBaseClass() 'BaseClass' => $this->getBaseClass()
)); ));
} }
// Set it so the user gets redirected back down to the form upon form fail
$form->setRedirectToFormOnValidationError(true); $form->setRedirectToFormOnValidationError(true);
// load any data from the cookies
if($data = Cookie::get('CommentsForm_UserData')) { if($data = Cookie::get('CommentsForm_UserData')) {
$data = Convert::json2array($data); $data = Convert::json2array($data);
$form->loadDataFrom(array( $data += array(
"Name" => isset($data['Name']) ? $data['Name'] : '', 'Name' => '',
"URL" => isset($data['URL']) ? $data['URL'] : '', 'URL' => '',
"Email" => isset($data['Email']) ? $data['Email'] : '' 'Email' => '',
)); );
// allow previous value to fill if comment not stored in cookie (i.e. validation error)
$form->loadDataFrom($data);
$prevComment = Cookie::get('CommentsForm_Comment'); $prevComment = Cookie::get('CommentsForm_Comment');
if($prevComment && $prevComment != ''){
$form->loadDataFrom(array("Comment" => $prevComment)); if($prevComment && $prevComment != '') {
$form->loadDataFrom(array(
'Comment' => $prevComment,
));
} }
} }
@ -471,7 +496,6 @@ class CommentingController extends Controller {
$form->loadDataFrom($member); $form->loadDataFrom($member);
} }
// hook to allow further extensions to alter the comments form
$this->extend('alterCommentForm', $form); $this->extend('alterCommentForm', $form);
return $form; return $form;
@ -482,41 +506,43 @@ class CommentingController extends Controller {
* *
* @param array $data * @param array $data
* @param Form $form * @param Form $form
*
* @return bool|SS_HTTPResponse
*/ */
public function doPostComment($data, $form) { public function doPostComment($data, $form) {
// Load class and parent from data
if(isset($data['BaseClass'])) { if(isset($data['BaseClass'])) {
$this->setBaseClass($data['BaseClass']); $this->setBaseClass($data['BaseClass']);
} }
if(isset($data['ParentID']) && ($class = $this->getBaseClass())) { if(isset($data['ParentID']) && ($class = $this->getBaseClass())) {
$this->setOwnerRecord($class::get()->byID($data['ParentID'])); $this->setOwnerRecord($class::get()->byID($data['ParentID']));
} }
if(!$this->getOwnerRecord()) return $this->httpError(404);
// cache users data if(!$this->getOwnerRecord()) {
Cookie::set("CommentsForm_UserData", Convert::raw2json($data)); return $this->httpError(404);
Cookie::set("CommentsForm_Comment", $data['Comment']); }
Cookie::set('CommentsForm_UserData', Convert::raw2json($data));
Cookie::set('CommentsForm_Comment', $data['Comment']);
// extend hook to allow extensions. Also see onAfterPostComment
$this->extend('onBeforePostComment', $form); $this->extend('onBeforePostComment', $form);
// If commenting can only be done by logged in users, make sure the user is logged in
if(!$this->getOwnerRecord()->canPostComment()) { if(!$this->getOwnerRecord()->canPostComment()) {
return Security::permissionFailure( return Security::permissionFailure(
$this, $this,
_t( _t(
'CommentingController.PERMISSIONFAILURE', 'CommentingController.PERMISSIONFAILURE',
"You're not able to post comments to this page. Please ensure you are logged in and have an " 'You\'re not able to post comments to this page. Please ensure you are logged in and have an appropriate permission level.'
. "appropriate permission level."
) )
); );
} }
if($member = Member::currentUser()) { if($member = Member::currentUser()) {
$form->Fields()->push(new HiddenField("AuthorID", "Author ID", $member->ID)); $form->Fields()->push(
new HiddenField('AuthorID', 'Author ID', $member->ID)
);
} }
// What kind of moderation is required?
switch($this->getOwnerRecord()->ModerationRequired) { switch($this->getOwnerRecord()->ModerationRequired) {
case 'Required': case 'Required':
$requireModeration = true; $requireModeration = true;
@ -524,7 +550,6 @@ class CommentingController extends Controller {
case 'NonMembersOnly': case 'NonMembersOnly':
$requireModeration = empty($member); $requireModeration = empty($member);
break; break;
case 'None':
default: default:
$requireModeration = false; $requireModeration = false;
break; break;
@ -536,27 +561,20 @@ class CommentingController extends Controller {
$comment->AllowHtml = $this->getOption('html_allowed'); $comment->AllowHtml = $this->getOption('html_allowed');
$comment->Moderated = !$requireModeration; $comment->Moderated = !$requireModeration;
// Save into DB, or call pre-save hooks to give accurate preview if($this->getOption('use_preview') && !empty($data['IsPreview'])) {
$usePreview = $this->getOption('use_preview');
$isPreview = $usePreview && !empty($data['IsPreview']);
if($isPreview) {
$comment->extend('onBeforeWrite'); $comment->extend('onBeforeWrite');
} else { } else {
$comment->write(); $comment->write();
// extend hook to allow extensions. Also see onBeforePostComment
$this->extend('onAfterPostComment', $comment); $this->extend('onAfterPostComment', $comment);
} }
// we want to show a notification if comments are moderated if($requireModeration && !$comment->IsSpam) {
if ($requireModeration && !$comment->IsSpam) {
Session::set('CommentsModerated', 1); Session::set('CommentsModerated', 1);
} }
// clear the users comment since it passed validation
Cookie::set('CommentsForm_Comment', false); Cookie::set('CommentsForm_Comment', false);
// Find parent link
if(!empty($data['ReturnURL'])) { if(!empty($data['ReturnURL'])) {
$url = $data['ReturnURL']; $url = $data['ReturnURL'];
} elseif($parent = $comment->getParent()) { } elseif($parent = $comment->getParent()) {
@ -565,37 +583,44 @@ class CommentingController extends Controller {
return $this->redirectBack(); return $this->redirectBack();
} }
// Given a redirect page exists, attempt to link to the correct anchor
if(!$comment->Moderated) { if(!$comment->Moderated) {
// Display the "awaiting moderation" text $hash = sprintf(
$holder = $this->getOption('comments_holder_id'); '%s_PostCommentForm_error',
$hash = "{$holder}_PostCommentForm_error"; $this->getOption('comments_holder_id')
);
} elseif($comment->IsSpam) { } elseif($comment->IsSpam) {
// Link to the form with the error message contained
$hash = $form->FormName(); $hash = $form->FormName();
} else { } else {
// Link to the moderated, non-spam comment
$hash = $comment->Permalink(); $hash = $comment->Permalink();
} }
return $this->redirect(Controller::join_links($url, "#{$hash}")); return $this->redirect(Controller::join_links($url, '#' . $hash));
} }
/**
* @param array $data
* @param Form $form
*
* @return bool|SS_HTTPResponse
*/
public function doPreviewComment($data, $form) { public function doPreviewComment($data, $form) {
$data['IsPreview'] = 1; $data['IsPreview'] = 1;
return $this->doPostComment($data, $form); return $this->doPostComment($data, $form);
} }
/**
* In edge-cases, this will be called outside of a handleRequest() context; in that case,
* redirect to the homepage. Don't break into the global state at this stage because we'll
* be calling from a test context or something else where the global state is inappropriate.
*
* @return bool|SS_HTTPResponse
*/
public function redirectBack() { public function redirectBack() {
// Don't cache the redirect back ever
HTTP::set_cache_age(0); HTTP::set_cache_age(0);
$url = null; $url = null;
// In edge-cases, this will be called outside of a handleRequest() context; in that case,
// redirect to the homepage - don't break into the global state at this stage because we'll
// be calling from a test context or something else where the global state is inappropraite
if($this->request) { if($this->request) {
if($this->request->requestVar('BackURL')) { if($this->request->requestVar('BackURL')) {
$url = $this->request->requestVar('BackURL'); $url = $this->request->requestVar('BackURL');
@ -606,15 +631,150 @@ class CommentingController extends Controller {
} }
} }
if(!$url) $url = $this->fallbackReturnURL; if(!$url) {
if(!$url) $url = Director::baseURL(); $url = $this->fallbackReturnURL;
}
if(!$url) {
$url = Director::baseURL();
}
// absolute redirection URLs not located on this site may cause phishing
if(Director::is_site_url($url)) { if(Director::is_site_url($url)) {
return $this->redirect($url); return $this->redirect($url);
} else { } else {
return false; return false;
} }
}
/**
* @return TextField
*/
protected function getNameField() {
$nameFieldLabel = _t(
'CommentInterface.YOURNAME',
'Your name'
);
$nameRequiredLabel = _t(
'CommentInterface.YOURNAME_MESSAGE_REQUIRED',
'Please enter your name'
);
$nameField = TextField::create('Name', $nameFieldLabel);
$nameField->setCustomValidationMessage($nameRequiredLabel);
$nameField->setAttribute('data-msg-required', $nameRequiredLabel);
return $nameField;
}
/**
* @return EmailField
*/
protected function getEmailField() {
$emailFieldLabel = _t(
'CommentingController.EMAILADDRESS',
'Your email address (will not be published)'
);
$emailFieldRequiredLabel = _t(
'CommentInterface.EMAILADDRESS_MESSAGE_REQUIRED',
'Please enter your email address'
);
$emailFieldInvalidLabel = _t(
'CommentInterface.EMAILADDRESS_MESSAGE_EMAIL',
'Please enter a valid email address'
);
$emailField = EmailField::create('Email', $emailFieldLabel);
$emailField->setCustomValidationMessage($emailFieldRequiredLabel);
$emailField->setAttribute('data-msg-required', $emailFieldRequiredLabel);
$emailField->setAttribute('data-msg-email', $emailFieldInvalidLabel);
$emailField->setAttribute('data-rule-email', true);
return $emailField;
}
/**
* @return TextField
*/
protected function getURLField() {
$urlFieldLabel = _t(
'CommentingController.WEBSITEURL',
'Your website URL'
);
$urlInvalidLabel = _t(
'CommentInterface.COMMENT_MESSAGE_URL',
'Please enter a valid URL'
);
$urlField = TextField::create('URL', $urlFieldLabel);
$urlField->setAttribute('data-msg-url', $urlInvalidLabel);
$urlField->setAttribute('data-rule-url', true);
return $urlField;
}
/**
* @return TextareaField
*/
protected function getCommentField() {
$commentFieldLabel = _t(
'CommentingController.COMMENTS',
'Comments'
);
$commentRequiredLabel = _t(
'CommentInterface.COMMENT_MESSAGE_REQUIRED',
'Please enter your comment'
);
$commentField = TextareaField::create('Comment', $commentFieldLabel);
$commentField->setCustomValidationMessage($commentRequiredLabel);
$commentField->setAttribute('data-msg-required', $commentRequiredLabel);
return $commentField;
}
/**
* @return ReadonlyField
*/
protected function getPreviewCommentField() {
$previewCommentFieldLabel = _t(
'CommentInterface.PREVIEWLABEL',
'Preview'
);
$previewCommentField = ReadonlyField::create(
'PreviewComment',
$previewCommentFieldLabel
);
$previewCommentField->setAttribute('style', 'display: none');
return $previewCommentField;
}
/**
* @return FormAction
*/
protected function getPreviewCommentAction() {
$previewCommentActionLabel = _t(
'CommentInterface.PREVIEW',
'Preview'
);
$previewCommentAction = FormAction::create(
'doPreviewComment',
$previewCommentActionLabel
);
$previewCommentAction->addExtraClass('action-minor');
$previewCommentAction->setAttribute('style', 'display: none');
} }
} }

View File

@ -3,6 +3,10 @@
/** /**
* Extension to {@link DataObject} to enable tracking comments. * Extension to {@link DataObject} to enable tracking comments.
* *
* @property bool $ProvideComments
* @property string $ModerationRequired
* @property bool $CommentsRequireLogin
*
* @package comments * @package comments
*/ */
class CommentsExtension extends DataExtension { class CommentsExtension extends DataExtension {
@ -32,9 +36,9 @@ class CommentsExtension extends DataExtension {
* nested_comments: Enable nested comments * nested_comments: Enable nested comments
* nested_depth: Max depth of nested comments in levels (where root is 1 depth) 0 means no limit. * nested_depth: Max depth of nested comments in levels (where root is 1 depth) 0 means no limit.
* *
* @var array
*
* @config * @config
*
* @var array
*/ */
private static $comments = array( private static $comments = array(
'enabled' => true, 'enabled' => true,
@ -78,41 +82,50 @@ class CommentsExtension extends DataExtension {
* CMS configurable options should default to the config values * CMS configurable options should default to the config values
*/ */
public function populateDefaults() { public function populateDefaults() {
// Set if comments should be enabled by default $this->owner->ProvideComments = 0;
$this->owner->ProvideComments = $this->owner->getCommentsOption('enabled') ? 1 : 0;
// If moderation options should be configurable via the CMS then if($this->owner->getCommentsOption('enabled')) {
if($this->owner->getCommentsOption('require_moderation')) { $this->owner->ProvideComments = 1;
$this->owner->ModerationRequired = 'Required';
} elseif($this->owner->getCommentsOption('require_moderation_nonmembers')) {
$this->owner->ModerationRequired = 'NonMembersOnly';
} else {
$this->owner->ModerationRequired = 'None';
} }
$this->owner->CommentsRequireLogin = $this->owner->getCommentsOption('require_login') ? 1 : 0; $this->owner->ModerationRequired = 'None';
if($this->owner->getCommentsOption('require_moderation')) {
$this->owner->ModerationRequired = 'Required';
}
if($this->owner->getCommentsOption('require_moderation_nonmembers')) {
$this->owner->ModerationRequired = 'NonMembersOnly';
}
$this->owner->CommentsRequireLogin = 0;
if($this->owner->getCommentsOption('require_login')) {
$this->owner->CommentsRequireLogin = 1;
}
} }
/** /**
* If this extension is applied to a {@link SiteTree} record then * If this extension is applied to a {@link SiteTree} record then append a Provide Comments
* append a Provide Comments checkbox to allow authors to trigger * checkbox to allow authors to trigger whether or not to display comments.
* whether or not to display comments
* *
* @todo Allow customization of other {@link Commenting} configuration * @todo Allow customization of other {@link Commenting} configuration
* *
* @param FieldList $fields * @param FieldList $fields
*/ */
public function updateSettingsFields(FieldList $fields) { public function updateSettingsFields(FieldList $fields) {
$options = FieldGroup::create();
$options->setTitle(_t('CommentsExtension.COMMENTOPTIONS', 'Comments'));
$options = FieldGroup::create()->setTitle(_t('CommentsExtension.COMMENTOPTIONS', 'Comments'));
// Check if enabled setting should be cms configurable
if($this->owner->getCommentsOption('enabled_cms')) { if($this->owner->getCommentsOption('enabled_cms')) {
$options->push(new CheckboxField('ProvideComments', _t('Comment.ALLOWCOMMENTS', 'Allow Comments'))); $options->push(
new CheckboxField(
'ProvideComments',
_t('Comment.ALLOWCOMMENTS', 'Allow Comments')
)
);
} }
// Check if we should require users to login to comment
if($this->owner->getCommentsOption('require_login_cms')) { if($this->owner->getCommentsOption('require_login_cms')) {
$options->push( $options->push(
new CheckboxField( new CheckboxField(
@ -130,16 +143,26 @@ class CommentsExtension extends DataExtension {
} }
} }
// Check if moderation should be enabled via cms configurable
if($this->owner->getCommentsOption('require_moderation_cms')) { if($this->owner->getCommentsOption('require_moderation_cms')) {
$moderationField = new DropdownField('ModerationRequired', 'Comment Moderation', array( $moderationField = new DropdownField(
'None' => _t('CommentsExtension.MODERATIONREQUIRED_NONE', 'No moderation required'), 'ModerationRequired',
'Required' => _t('CommentsExtension.MODERATIONREQUIRED_REQUIRED', 'Moderate all comments'), 'Comment Moderation',
'NonMembersOnly' => _t( array(
'CommentsExtension.MODERATIONREQUIRED_NONMEMBERSONLY', 'None' => _t(
'Only moderate non-members' 'CommentsExtension.MODERATIONREQUIRED_NONE',
), 'No moderation required'
)); ),
'Required' => _t(
'CommentsExtension.MODERATIONREQUIRED_REQUIRED',
'Moderate all comments'
),
'NonMembersOnly' => _t(
'CommentsExtension.MODERATIONREQUIRED_NONMEMBERSONLY',
'Only moderate non-members'
),
)
);
if($fields->hasTabSet()) { if($fields->hasTabSet()) {
$fields->addFieldsToTab('Root.Settings', $moderationField); $fields->addFieldsToTab('Root.Settings', $moderationField);
} else { } else {
@ -149,10 +172,10 @@ class CommentsExtension extends DataExtension {
} }
/** /**
* Get comment moderation rules for this parent * Get comment moderation rules for this parent.
* *
* None: No moderation required * None: No moderation required
* Required: All comments * Required: All comments
* NonMembersOnly: Only anonymous users * NonMembersOnly: Only anonymous users
* *
* @return string * @return string
@ -160,26 +183,30 @@ class CommentsExtension extends DataExtension {
public function getModerationRequired() { public function getModerationRequired() {
if($this->owner->getCommentsOption('require_moderation_cms')) { if($this->owner->getCommentsOption('require_moderation_cms')) {
return $this->owner->getField('ModerationRequired'); return $this->owner->getField('ModerationRequired');
} elseif($this->owner->getCommentsOption('require_moderation')) {
return 'Required';
} elseif($this->owner->getCommentsOption('require_moderation_nonmembers')) {
return 'NonMembersOnly';
} else {
return 'None';
} }
if($this->owner->getCommentsOption('require_moderation')) {
return 'Required';
}
if($this->owner->getCommentsOption('require_moderation_nonmembers')) {
return 'NonMembersOnly';
}
return 'None';
} }
/** /**
* Determine if users must be logged in to post comments * Determine if users must be logged in to post comments.
* *
* @return boolean * @return bool
*/ */
public function getCommentsRequireLogin() { public function getCommentsRequireLogin() {
if($this->owner->getCommentsOption('require_login_cms')) { if($this->owner->getCommentsOption('require_login_cms')) {
return (bool) $this->owner->getField('CommentsRequireLogin'); return (bool) $this->owner->getField('CommentsRequireLogin');
} else {
return (bool) $this->owner->getCommentsOption('require_login');
} }
return (bool) $this->owner->getCommentsOption('require_login');
} }
/** /**
@ -190,105 +217,109 @@ class CommentsExtension extends DataExtension {
*/ */
public function AllComments() { public function AllComments() {
$order = $this->owner->getCommentsOption('order_comments_by'); $order = $this->owner->getCommentsOption('order_comments_by');
$comments = CommentList::create($this->ownerBaseClass)
->forForeignID($this->owner->ID) $comments = CommentList::create($this->ownerBaseClass);
->sort($order); $comments->forForeignID($this->owner->ID);
$comments->sort($order);
$this->owner->extend('updateAllComments', $comments); $this->owner->extend('updateAllComments', $comments);
return $comments; return $comments;
} }
/** /**
* Returns all comments against this object, with with spam and unmoderated items excluded, for use in the frontend * Returns all comments against this object, with with spam and un-moderated items excluded.
* *
* @return CommentList * @return CommentList
*/ */
public function AllVisibleComments() { public function AllVisibleComments() {
$list = $this->AllComments(); $list = $this->AllComments();
// Filter spam comments for non-administrators if configured
$showSpam = $this->owner->getCommentsOption('frontend_spam') && $this->owner->canModerateComments(); $showSpam = $this->owner->getCommentsOption('frontend_spam') && $this->owner->canModerateComments();
if(!$showSpam) { if(!$showSpam) {
$list = $list->filter('IsSpam', 0); $list = $list->filter('IsSpam', 0);
} }
// Filter un-moderated comments for non-administrators if moderation is enabled $showUnModerated = ($this->owner->ModerationRequired === 'None') || ($this->owner->getCommentsOption('frontend_moderation') && $this->owner->canModerateComments());
$showUnmoderated = ($this->owner->ModerationRequired === 'None')
|| ($this->owner->getCommentsOption('frontend_moderation') && $this->owner->canModerateComments()); if(!$showUnModerated) {
if(!$showUnmoderated) {
$list = $list->filter('Moderated', 1); $list = $list->filter('Moderated', 1);
} }
$this->owner->extend('updateAllVisibleComments', $list); $this->owner->extend('updateAllVisibleComments', $list);
return $list; return $list;
} }
/** /**
* Returns the root level comments, with spam and unmoderated items excluded, for use in the frontend * Returns the root level comments, with spam and un-moderated items excluded.
* *
* @return CommentList * @return CommentList
*/ */
public function Comments() { public function Comments() {
$list = $this->AllVisibleComments(); $list = $this->AllVisibleComments();
// If nesting comments, only show root level
if($this->owner->getCommentsOption('nested_comments')) { if($this->owner->getCommentsOption('nested_comments')) {
$list = $list->filter('ParentCommentID', 0); $list = $list->filter('ParentCommentID', 0);
} }
$this->owner->extend('updateComments', $list); $this->owner->extend('updateComments', $list);
return $list; return $list;
} }
/** /**
* Returns a paged list of the root level comments, with spam and unmoderated items excluded, * Returns a paged list of the root level comments, with spam and un-moderated items excluded.
* for use in the frontend
* *
* @return PaginatedList * @return PaginatedList
*/ */
public function PagedComments() { public function PagedComments() {
$list = $this->Comments(); $list = $this->Comments();
// Add pagination
$list = new PaginatedList($list, Controller::curr()->getRequest()); $list = new PaginatedList($list, Controller::curr()->getRequest());
$list->setPaginationGetVar('commentsstart' . $this->owner->ID); $list->setPaginationGetVar('commentsstart' . $this->owner->ID);
$list->setPageLength($this->owner->getCommentsOption('comments_per_page')); $list->setPageLength($this->owner->getCommentsOption('comments_per_page'));
$this->owner->extend('updatePagedComments', $list); $this->owner->extend('updatePagedComments', $list);
return $list; return $list;
} }
/** /**
* Check if comments are configured for this page even if they are currently disabled. * Check if comments are configured for this page even if they are currently disabled.
* Do not include the comments on pages which don't have id's such as security pages *
* Do not include the comments on pages which don't have id's such as security pages.
* *
* @deprecated since version 2.0 * @deprecated since version 2.0
* *
* @return boolean * @return bool
*/ */
public function getCommentsConfigured() { public function getCommentsConfigured() {
Deprecation::notice('2.0', 'getCommentsConfigured is deprecated. Use getCommentsEnabled instead'); Deprecation::notice('2.0', 'getCommentsConfigured is deprecated. Use getCommentsEnabled instead');
return true; // by virtue of all classes with this extension being 'configured'
return true;
} }
/** /**
* Determine if comments are enabled for this instance * Determine if comments are enabled for this instance.
* *
* @return boolean * @return bool
*/ */
public function getCommentsEnabled() { public function getCommentsEnabled() {
// Don't display comments form for pseudo-pages (such as the login form) if(!$this->owner->exists()) {
if(!$this->owner->exists()) return false; return false;
}
// Determine which flag should be used to determine if this is enabled
if($this->owner->getCommentsOption('enabled_cms')) { if($this->owner->getCommentsOption('enabled_cms')) {
return $this->owner->ProvideComments; return $this->owner->ProvideComments;
} else {
return $this->owner->getCommentsOption('enabled');
} }
return $this->owner->getCommentsOption('enabled');
} }
/** /**
* Get the HTML ID for the comment holder in the template * Get the HTML ID for the comment holder in the template.
* *
* @return string * @return string
*/ */
@ -301,71 +332,86 @@ class CommentsExtension extends DataExtension {
*/ */
public function getPostingRequiresPermission() { public function getPostingRequiresPermission() {
Deprecation::notice('2.0', 'Use getPostingRequiredPermission instead'); Deprecation::notice('2.0', 'Use getPostingRequiredPermission instead');
return $this->getPostingRequiredPermission(); return $this->getPostingRequiredPermission();
} }
/** /**
* Permission codes required in order to post (or empty if none required) * Permission codes required in order to post (or empty if none required).
* *
* @return string|array Permission or list of permissions, if required * @return string|array
*/ */
public function getPostingRequiredPermission() { public function getPostingRequiredPermission() {
return $this->owner->getCommentsOption('required_permission'); return $this->owner->getCommentsOption('required_permission');
} }
/**
* @return bool
*/
public function canPost() { public function canPost() {
Deprecation::notice('2.0', 'Use canPostComment instead'); Deprecation::notice('2.0', 'Use canPostComment instead');
return $this->canPostComment(); return $this->canPostComment();
} }
/** /**
* Determine if a user can post comments on this item * Determine if a user can post comments on this item.
* *
* @param Member $member Member to check * @param null|Member $member
* *
* @return boolean * @return bool
*/ */
public function canPostComment($member = null) { public function canPostComment($member = null) {
// Deny if not enabled for this object if(!$this->owner->CommentsEnabled) {
if(!$this->owner->CommentsEnabled) return false; return false;
}
// Check if member is required
$requireLogin = $this->owner->CommentsRequireLogin; $requireLogin = $this->owner->CommentsRequireLogin;
if(!$requireLogin) return true;
// Check member is logged in if(!$requireLogin) {
$member = $member ?: Member::currentUser(); return true;
if(!$member) return false; }
if(!$member) {
$member = Member::currentUser();
}
if(!$member) {
return false;
}
// If member required check permissions
$requiredPermission = $this->owner->PostingRequiredPermission; $requiredPermission = $this->owner->PostingRequiredPermission;
if($requiredPermission && !Permission::checkMember($member, $requiredPermission)) return false;
if($requiredPermission && !Permission::checkMember($member, $requiredPermission)) {
return false;
}
return true; return true;
} }
/** /**
* Determine if this member can moderate comments in the CMS * Determine if this member can moderate comments in the CMS.
* *
* @param Member $member * @param null|Member $member
* *
* @return boolean * @return bool
*/ */
public function canModerateComments($member = null) { public function canModerateComments($member = null) {
// Deny if not enabled for this object if(!$this->owner->CommentsEnabled) {
if(!$this->owner->CommentsEnabled) return false; return false;
}
// Fallback to can-edit
return $this->owner->canEdit($member); return $this->owner->canEdit($member);
} }
public function getRssLink() { public function getRssLink() {
Deprecation::notice('2.0', 'Use getCommentRSSLink instead'); Deprecation::notice('2.0', 'Use getCommentRSSLink instead');
return $this->getCommentRSSLink(); return $this->getCommentRSSLink();
} }
/** /**
* Gets the RSS link to all comments * Gets the RSS link to all comments.
* *
* @return string * @return string
*/ */
@ -373,42 +419,44 @@ class CommentsExtension extends DataExtension {
return Controller::join_links(Director::baseURL(), 'CommentingController/rss'); return Controller::join_links(Director::baseURL(), 'CommentingController/rss');
} }
/**
* @return string
*/
public function getRssLinkPage() { public function getRssLinkPage() {
Deprecation::notice('2.0', 'Use getCommentRSSLinkPage instead'); Deprecation::notice('2.0', 'Use getCommentRSSLinkPage instead');
return $this->getCommentRSSLinkPage(); return $this->getCommentRSSLinkPage();
} }
/** /**
* Get the RSS link to all comments on this page * Get the RSS link to all comments on this page.
* *
* @return string * @return string
*/ */
public function getCommentRSSLinkPage() { public function getCommentRSSLinkPage() {
return Controller::join_links( return Controller::join_links(
$this->getCommentRSSLink(), $this->ownerBaseClass, $this->owner->ID $this->getCommentRSSLink(),
$this->ownerBaseClass,
$this->owner->ID
); );
} }
/** /**
* Comments interface for the front end. Includes the CommentAddForm and the composition * Comments interface for the front end. Includes the CommentAddForm and the composition of the
* of the comments display. * comments display.
* *
* To customize the html see templates/CommentInterface.ss or extend this function with * To customize the html see templates/CommentInterface.ss or extend this function with your
* your own extension. * own extension.
*
* @todo Cleanup the passing of all this configuration based functionality
*
* @see docs/en/Extending
*/ */
public function CommentsForm() { public function CommentsForm() {
// Check if enabled
$enabled = $this->getCommentsEnabled(); $enabled = $this->getCommentsEnabled();
if($enabled && $this->owner->getCommentsOption('include_js')) { if($enabled && $this->owner->getCommentsOption('include_js')) {
Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-validate/lib/jquery.form.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-validate/lib/jquery.form.js');
Requirements::javascript(COMMENTS_THIRDPARTY . '/jquery-validate/jquery.validate.min.js'); Requirements::javascript(COMMENTS_THIRDPARTY . '/jquery-validate/jquery.validate.min.js');
Requirements::javascript('comments/javascript/CommentsInterface.js'); Requirements::javascript(COMMENTS_DIR . '/javascript/CommentsInterface.js');
} }
$controller = CommentingController::create(); $controller = CommentingController::create();
@ -417,12 +465,15 @@ class CommentsExtension extends DataExtension {
$controller->setOwnerController(Controller::curr()); $controller->setOwnerController(Controller::curr());
$moderatedSubmitted = Session::get('CommentsModerated'); $moderatedSubmitted = Session::get('CommentsModerated');
Session::clear('CommentsModerated'); Session::clear('CommentsModerated');
$form = ($enabled) ? $controller->CommentsForm() : false; $form = false;
if($enabled) {
$form = $controller->CommentsForm();
}
// a little bit all over the show but to ensure a slightly easier upgrade for users
// return back the same variables as previously done in comments
return $this return $this
->owner ->owner
->customise(array( ->customise(array(
@ -433,48 +484,58 @@ class CommentsExtension extends DataExtension {
} }
/** /**
* Returns whether this extension instance is attached to a {@link SiteTree} object * Returns whether this extension instance is attached to a {@link SiteTree} object.
* *
* @return bool * @return bool
*/ */
public function attachedToSiteTree() { public function attachedToSiteTree() {
$class = $this->ownerBaseClass; $class = $this->ownerBaseClass;
return (is_subclass_of($class, 'SiteTree')) || ($class == 'SiteTree'); return $class === 'SiteTree' || is_subclass_of($class, 'SiteTree');
} }
/** /**
* @deprecated 1.0 Please use {@link CommentsExtension->CommentsForm()} * Please use {@link CommentsExtension->CommentsForm()}.
*
* @deprecated 1.0
*/ */
public function PageComments() { public function PageComments() {
// This method is very commonly used, don't throw a warning just yet
Deprecation::notice('1.0', '$PageComments is deprecated. Please use $CommentsForm'); Deprecation::notice('1.0', '$PageComments is deprecated. Please use $CommentsForm');
return $this->CommentsForm(); return $this->CommentsForm();
} }
/** /**
* Get the commenting option for this object * Get the commenting option for this object.
* *
* This can be overridden in any instance or extension to customise the option available * This can be overridden in any instance or extension to customise the option available.
* *
* @param string $key * @param string $key
* *
* @return mixed Result if the setting is available, or null otherwise * @return mixed
*/ */
public function getCommentsOption($key) { public function getCommentsOption($key) {
$settings = $this->owner // In case singleton is called on the extension directly if($this->owner) {
? $this->owner->config()->comments $settings = $this->owner->config()->comments;
: Config::inst()->get(__CLASS__, 'comments'); } else {
$value = null; $settings = Config::inst()->get(__CLASS__, 'comments');
if(isset($settings[$key])) $value = $settings[$key]; }
$value = null;
if(isset($settings[$key])) {
$value = $settings[$key];
}
if($this->owner) {
$this->owner->extend('updateCommentsOption', $key, $value);
}
// To allow other extensions to customise this option
if($this->owner) $this->owner->extend('updateCommentsOption', $key, $value);
return $value; return $value;
} }
/** /**
* Add moderation functions to the current fieldlist * Add moderation functions to the current field list.
* *
* @param FieldList $fields * @param FieldList $fields
*/ */
@ -483,65 +544,91 @@ class CommentsExtension extends DataExtension {
$commentsConfig = CommentsGridFieldConfig::create(); $commentsConfig = CommentsGridFieldConfig::create();
$newComments = $this->owner->AllComments()->filter('Moderated', 0); $newComments = Comment::get()
->filter('Moderated', 0);
$newGrid = new CommentsGridField( $newCommentsGrid = new CommentsGridField(
'NewComments', 'NewComments',
_t('CommentsAdmin.NewComments', 'New'), _t('CommentsAdmin.NewComments', 'New'),
$newComments, $newComments,
$commentsConfig $commentsConfig
); );
$approvedComments = $this->owner->AllComments()->filter('Moderated', 1)->filter('IsSpam', 0); $newCommentsCountLabel = sprintf('(%s)', count($newComments));
$approvedGrid = new CommentsGridField( $approvedComments = Comment::get()
->filter('Moderated', 1)
->filter('IsSpam', 0);
$approvedCommentsGrid = new CommentsGridField(
'ApprovedComments', 'ApprovedComments',
_t('CommentsAdmin.Comments', 'Approved'), _t('CommentsAdmin.ApprovedComments', 'Approved'),
$approvedComments, $approvedComments,
$commentsConfig $commentsConfig
); );
$spamComments = $this->owner->AllComments()->filter('Moderated', 1)->filter('IsSpam', 1); $approvedCommentsCountLabel = sprintf('(%s)', count($approvedComments));
$spamGrid = new CommentsGridField( $spamComments = Comment::get()
->filter('Moderated', 1)
->filter('IsSpam', 1);
$spamCommentsGrid = new CommentsGridField(
'SpamComments', 'SpamComments',
_t('CommentsAdmin.SpamComments', 'Spam'), _t('CommentsAdmin.SpamComments', 'Spam'),
$spamComments, $spamComments,
$commentsConfig $commentsConfig
); );
$newCount = '(' . count($newComments) . ')'; $spamCommentsCountLabel = sprintf('(%s)', count($spamComments));
$approvedCount = '(' . count($approvedComments) . ')';
$spamCount = '(' . count($spamComments) . ')';
if($fields->hasTabSet()) { if($fields->hasTabSet()) {
$tabs = new TabSet( $tabs = new TabSet(
'Comments', 'Comments',
new Tab('CommentsNewCommentsTab', _t('CommentAdmin.NewComments', 'New') . ' ' . $newCount, new Tab(
$newGrid 'NewComments',
sprintf(
'%s %s',
_t('CommentAdmin.NewComments', 'New'),
$newCommentsCountLabel
),
$newCommentsGrid
), ),
new Tab('CommentsCommentsTab', _t('CommentAdmin.Comments', 'Approved') . ' ' . $approvedCount, new Tab(
$approvedGrid 'ApprovedComments',
sprintf(
'%s %s',
_t('CommentAdmin.ApprovedComments', 'Approved'),
$approvedCommentsCountLabel
),
$approvedCommentsGrid
), ),
new Tab('CommentsSpamCommentsTab', _t('CommentAdmin.SpamComments', 'Spam') . ' ' . $spamCount, new Tab(
$spamGrid 'SpamComments',
sprintf(
'%s %s',
_t('CommentAdmin.SpamComments', 'Spam'),
$spamCommentsCountLabel
),
$spamCommentsGrid
) )
); );
$fields->addFieldToTab('Root', $tabs); $fields->addFieldToTab('Root', $tabs);
} else { } else {
$fields->push($newGrid); $fields->push($newCommentsGrid);
$fields->push($approvedGrid); $fields->push($approvedCommentsGrid);
$fields->push($spamGrid); $fields->push($spamCommentsGrid);
} }
} }
/**
* {@inheritdoc}
*/
public function updateCMSFields(FieldList $fields) { public function updateCMSFields(FieldList $fields) {
// Disable moderation if not permitted
if($this->owner->canModerateComments()) { if($this->owner->canModerateComments()) {
$this->updateModerationFields($fields); $this->updateModerationFields($fields);
} }
// If this isn't a page we should merge the settings into the CMS fields
if(!$this->attachedToSiteTree()) { if(!$this->attachedToSiteTree()) {
$this->updateSettingsFields($fields); $this->updateSettingsFields($fields);
} }

View File

@ -3,25 +3,25 @@
/** /**
* Represents a single comment object. * Represents a single comment object.
* *
* @property string $Name * @property string $Name
* @property string $Comment * @property string $Comment
* @property string $Email * @property string $Email
* @property string $URL * @property string $URL
* @property string $BaseClass * @property string $BaseClass
* @property boolean $Moderated * @property bool $Moderated
* @property boolean $IsSpam True if the comment is known as spam * @property bool $IsSpam
* @property integer $ParentID ID of the parent page / dataobject * @property int $ParentID
* @property boolean $AllowHtml If true, treat $Comment as HTML instead of plain text * @property bool $AllowHtml
* @property string $SecretToken Secret admin token required to provide moderation links between sessions * @property string $SecretToken
* @property integer $Depth Depth of this comment in the nested chain * @property int $Depth
*
* @method HasManyList ChildComments()
* @method Member Author()
* @method Comment ParentComment()
* *
* @method HasManyList ChildComments() List of child comments
* @method Member Author() Member object who created this comment
* @method Comment ParentComment() Parent comment this is a reply to
* @package comments * @package comments
*/ */
class Comment extends DataObject { class Comment extends DataObject {
/** /**
* @var array * @var array
*/ */
@ -39,22 +39,37 @@ class Comment extends DataObject {
'Depth' => 'Int', 'Depth' => 'Int',
); );
/**
* @var array
*/
private static $has_one = array( private static $has_one = array(
"Author" => "Member", 'Author' => 'Member',
"ParentComment" => "Comment", 'ParentComment' => 'Comment',
); );
/**
* @var array
*/
private static $has_many = array( private static $has_many = array(
"ChildComments" => "Comment" 'ChildComments' => 'Comment'
); );
/**
* @var string
*/
private static $default_sort = '"Created" DESC'; private static $default_sort = '"Created" DESC';
/**
* @var array
*/
private static $defaults = array( private static $defaults = array(
'Moderated' => 0, 'Moderated' => 0,
'IsSpam' => 0, 'IsSpam' => 0,
); );
/**
* @var array
*/
private static $casting = array( private static $casting = array(
'Title' => 'Varchar', 'Title' => 'Varchar',
'ParentTitle' => 'Varchar', 'ParentTitle' => 'Varchar',
@ -68,6 +83,9 @@ class Comment extends DataObject {
'Permalink' => 'Varchar', 'Permalink' => 'Varchar',
); );
/**
* @var array
*/
private static $searchable_fields = array( private static $searchable_fields = array(
'Name', 'Name',
'Email', 'Email',
@ -76,6 +94,9 @@ class Comment extends DataObject {
'BaseClass', 'BaseClass',
); );
/**
* @var array
*/
private static $summary_fields = array( private static $summary_fields = array(
'Name' => 'Submitted By', 'Name' => 'Submitted By',
'Email' => 'Email', 'Email' => 'Email',
@ -85,26 +106,32 @@ class Comment extends DataObject {
'IsSpam' => 'Is Spam', 'IsSpam' => 'Is Spam',
); );
/**
* @var array
*/
private static $field_labels = array( private static $field_labels = array(
'Author' => 'Author Member', 'Author' => 'Author Member',
); );
/**
* {@inheritdoc
*/
public function onBeforeWrite() { public function onBeforeWrite() {
parent::onBeforeWrite(); parent::onBeforeWrite();
// Sanitize HTML, because its expected to be passed to the template unescaped later
if($this->AllowHtml) { if($this->AllowHtml) {
$this->Comment = $this->purifyHtml($this->Comment); $this->Comment = $this->purifyHtml($this->Comment);
} }
// Check comment depth
$this->updateDepth(); $this->updateDepth();
} }
/**
* {@inheritdoc
*/
public function onBeforeDelete() { public function onBeforeDelete() {
parent::onBeforeDelete(); parent::onBeforeDelete();
// Delete all children
foreach($this->ChildComments() as $comment) { foreach($this->ChildComments() as $comment) {
$comment->delete(); $comment->delete();
} }
@ -118,24 +145,31 @@ class Comment extends DataObject {
} }
/** /**
* Migrates the old {@link PageComment} objects to {@link Comment} * {@inheritdoc}
*
* Migrates the old {@link PageComment} objects to {@link Comment}.
*/ */
public function requireDefaultRecords() { public function requireDefaultRecords() {
parent::requireDefaultRecords(); parent::requireDefaultRecords();
if(DB::getConn()->hasTable('PageComment')) { if(DB::getConn()->hasTable('PageComment')) {
$comments = DB::query('SELECT * FROM "PageComment"'); $comments = DB::query('SELECT * FROM PageComment');
if($comments) { if($comments) {
while($pageComment = $comments->nextRecord()) { while($pageComment = $comments->nextRecord()) {
// create a new comment from the older page comment
$comment = new Comment(); $comment = new Comment();
$comment->update($pageComment); $comment->update($pageComment);
// set the variables which have changed
$comment->BaseClass = 'SiteTree'; $comment->BaseClass = 'SiteTree';
$comment->URL = (isset($pageComment['CommenterURL'])) ? $pageComment['CommenterURL'] : ''; $comment->URL = '';
if((int) $pageComment['NeedsModeration'] == 0) $comment->Moderated = true;
if(isset($pageComment['CommenterURL'])) {
$comment->URL = $pageComment['CommenterURL'];
}
if($pageComment['NeedsModeration'] == false) {
$comment->Moderated = true;
}
$comment->write(); $comment->write();
} }
@ -147,38 +181,36 @@ class Comment extends DataObject {
} }
/** /**
* Return a link to this comment * Return a link to this comment.
* *
* @param string $action * @param string $action
* *
* @return string link to this comment. * @return string
*/ */
public function Link($action = '') { public function Link($action = '') {
if($parent = $this->getParent()) { if($parent = $this->getParent()) {
return $parent->Link($action) . '#' . $this->Permalink(); return $parent->Link($action) . '#' . $this->Permalink();
} }
return '';
} }
/** /**
* Returns the permalink for this {@link Comment}. Inserted into * Returns the permalink for this {@link Comment}. Inserted into the ID tag of the comment.
* the ID tag of the comment
* *
* @return string * @return string
*/ */
public function Permalink() { public function Permalink() {
$prefix = $this->getOption('comment_permalink_prefix'); return $this->getOption('comment_permalink_prefix') . $this->ID;
return $prefix . $this->ID;
} }
/** /**
* Translate the form field labels for the CMS administration * {@inheritdoc}
* *
* @param boolean $includerelations * @param bool $includeRelations
*
* @return array
*/ */
public function fieldLabels($includerelations = true) { public function fieldLabels($includeRelations = true) {
$labels = parent::fieldLabels($includerelations); $labels = parent::fieldLabels($includeRelations);
$labels['Name'] = _t('Comment.NAME', 'Author Name'); $labels['Name'] = _t('Comment.NAME', 'Author Name');
$labels['Comment'] = _t('Comment.COMMENT', 'Comment'); $labels['Comment'] = _t('Comment.COMMENT', 'Comment');
@ -193,20 +225,18 @@ class Comment extends DataObject {
} }
/** /**
* Get the commenting option * Get the commenting option.
* *
* @param string $key * @param string $key
* *
* @return mixed Result if the setting is available, or null otherwise * @return mixed
*/ */
public function getOption($key) { public function getOption($key) {
// If possible use the current record
$record = $this->getParent(); $record = $this->getParent();
if(!$record && $this->BaseClass) { if(!$record && $this->BaseClass) {
// Otherwise a singleton of that record
$record = singleton($this->BaseClass); $record = singleton($this->BaseClass);
} elseif(!$record) { } elseif(!$record) {
// Otherwise just use the default options
$record = singleton('CommentsExtension'); $record = singleton('CommentsExtension');
} }
@ -214,30 +244,35 @@ class Comment extends DataObject {
} }
/** /**
* Returns the parent {@link DataObject} this comment is attached too * Returns the parent {@link DataObject} this comment is attached too.
* *
* @return DataObject * @return null|DataObject
*/ */
public function getParent() { public function getParent() {
return $this->BaseClass && $this->ParentID if($this->BaseClass && $this->ParentID) {
? DataObject::get_by_id($this->BaseClass, $this->ParentID, true) return DataObject::get_by_id($this->BaseClass, $this->ParentID, true);
: null; }
return null;
} }
/** /**
* Returns a string to help identify the parent of the comment * Returns a string to help identify the parent of the comment.
* *
* @return string * @return string
*/ */
public function getParentTitle() { public function getParentTitle() {
if($parent = $this->getParent()) { $parent = $this->getParent();
return $parent->Title ?: ($parent->ClassName . ' #' . $parent->ID);
if($parent && $parent->Title) {
return $parent->Title;
} }
return $parent->ClassName . ' #' . $parent->ID;
} }
/** /**
* Comment-parent classnames obviously vary, return the parent classname * Comment-parent class names may vary, return the parent class name.
* *
* @return string * @return string
*/ */
@ -245,16 +280,19 @@ class Comment extends DataObject {
return $this->BaseClass; return $this->BaseClass;
} }
/**
* {@inheritdoc}
*/
public function castingHelper($field) { public function castingHelper($field) {
// Safely escape the comment
if($field === 'EscapedComment') { if($field === 'EscapedComment') {
return $this->AllowHtml ? 'HTMLText' : 'Text'; return $this->AllowHtml ? 'HTMLText' : 'Text';
} }
return parent::castingHelper($field); return parent::castingHelper($field);
} }
/** /**
* Content to be safely escaped on the frontend * @todo escape this comment? (DOH!)
* *
* @return string * @return string
*/ */
@ -263,9 +301,9 @@ class Comment extends DataObject {
} }
/** /**
* Return whether this comment is a preview (has not been written to the db) * Return whether this comment is a preview (has not been written to the db).
* *
* @return boolean * @return bool
*/ */
public function isPreview() { public function isPreview() {
return !$this->exists(); return !$this->exists();
@ -274,7 +312,7 @@ class Comment extends DataObject {
/** /**
* @todo needs to compare to the new {@link Commenting} configuration API * @todo needs to compare to the new {@link Commenting} configuration API
* *
* @param Member $member * @param null|Member $member
* *
* @return bool * @return bool
*/ */
@ -283,17 +321,18 @@ class Comment extends DataObject {
} }
/** /**
* Checks for association with a page, and {@link SiteTree->ProvidePermission} * Checks for association with a page, and {@link SiteTree->ProvidePermission} flag being set
* flag being set to true. * to true.
* *
* @param Member $member * @param null|int|Member $member
* *
* @return Boolean * @return bool
*/ */
public function canView($member = null) { public function canView($member = null) {
$member = $this->getMember($member); $member = $this->getMember($member);
$extended = $this->extendedCan('canView', $member); $extended = $this->extendedCan('canView', $member);
if($extended !== null) { if($extended !== null) {
return $extended; return $extended;
} }
@ -303,9 +342,7 @@ class Comment extends DataObject {
} }
if($parent = $this->getParent()) { if($parent = $this->getParent()) {
return $parent->canView($member) return $parent->canView($member) && $parent->has_extension('CommentsExtension') && $parent->CommentsEnabled;
&& $parent->has_extension('CommentsExtension')
&& $parent->CommentsEnabled;
} }
return false; return false;
@ -316,7 +353,7 @@ class Comment extends DataObject {
* *
* @param null|int|Member $member * @param null|int|Member $member
* *
* @return Boolean * @return bool
*/ */
public function canEdit($member = null) { public function canEdit($member = null) {
$member = $this->getMember($member); $member = $this->getMember($member);
@ -326,6 +363,7 @@ class Comment extends DataObject {
} }
$extended = $this->extendedCan('canEdit', $member); $extended = $this->extendedCan('canEdit', $member);
if($extended !== null) { if($extended !== null) {
return $extended; return $extended;
} }
@ -346,7 +384,7 @@ class Comment extends DataObject {
* *
* @param null|int|Member $member * @param null|int|Member $member
* *
* @return Boolean * @return bool
*/ */
public function canDelete($member = null) { public function canDelete($member = null) {
$member = $this->getMember($member); $member = $this->getMember($member);
@ -356,6 +394,7 @@ class Comment extends DataObject {
} }
$extended = $this->extendedCan('canDelete', $member); $extended = $this->extendedCan('canDelete', $member);
if($extended !== null) { if($extended !== null) {
return $extended; return $extended;
} }
@ -366,8 +405,9 @@ class Comment extends DataObject {
/** /**
* Resolves Member object. * Resolves Member object.
* *
* @param Member|int|null $member * @param null|int|Member $member
* @return Member|null *
* @return null|Member
*/ */
protected function getMember($member = null) { protected function getMember($member = null) {
if(!$member) { if(!$member) {
@ -382,7 +422,7 @@ class Comment extends DataObject {
} }
/** /**
* Return the authors name for the comment * Return the authors name for the comment.
* *
* @return string * @return string
*/ */
@ -392,19 +432,26 @@ class Comment extends DataObject {
} else if($author = $this->Author()) { } else if($author = $this->Author()) {
return $author->getName(); return $author->getName();
} }
return '';
} }
/** /**
* Generate a secure admin-action link authorised for the specified member * Generate a secure admin-action link authorised for the specified member.
* *
* @param string $action An action on CommentingController to link to * @param string $action
* @param Member $member The member authorised to invoke this action * @param null|Member $member
* *
* @return string * @return string
*/ */
protected function actionLink($action, $member = null) { protected function actionLink($action, $member = null) {
if(!$member) $member = Member::currentUser(); if(!$member) {
if(!$member) return false; $member = Member::currentUser();
}
if(!$member) {
return false;
}
$url = Controller::join_links( $url = Controller::join_links(
Director::baseURL(), Director::baseURL(),
@ -413,89 +460,100 @@ class Comment extends DataObject {
$this->ID $this->ID
); );
// Limit access for this user
$token = $this->getSecurityToken(); $token = $this->getSecurityToken();
return $token->addToUrl($url, $member); return $token->addToUrl($url, $member);
} }
/** /**
* Link to delete this comment * Link to delete this comment.
* *
* @param Member $member * @param null|Member $member
* *
* @return string * @return null|string
*/ */
public function DeleteLink($member = null) { public function DeleteLink($member = null) {
if($this->canDelete($member)) { if($this->canDelete($member)) {
return $this->actionLink('delete', $member); return $this->actionLink('delete', $member);
} }
return null;
} }
/** /**
* Link to mark as spam * Link to mark as spam.
* *
* @param Member $member * @param null|Member $member
* *
* @return string * @return null|string
*/ */
public function SpamLink($member = null) { public function SpamLink($member = null) {
if($this->canEdit($member) && !$this->IsSpam) { if($this->canEdit($member) && !$this->IsSpam) {
return $this->actionLink('spam', $member); return $this->actionLink('spam', $member);
} }
return null;
} }
/** /**
* Link to mark as not-spam (ham) * Link to mark as not-spam.
* *
* @param Member $member * @param null|Member $member
* *
* @return string * @return null|string
*/ */
public function HamLink($member = null) { public function HamLink($member = null) {
if($this->canEdit($member) && $this->IsSpam) { if($this->canEdit($member) && $this->IsSpam) {
return $this->actionLink('ham', $member); return $this->actionLink('ham', $member);
} }
return null;
} }
/** /**
* Link to approve this comment * Link to approve this comment.
* *
* @param Member $member * @param null|Member $member
* *
* @return string * @return null|string
*/ */
public function ApproveLink($member = null) { public function ApproveLink($member = null) {
if($this->canEdit($member) && !$this->Moderated) { if($this->canEdit($member) && !$this->Moderated) {
return $this->actionLink('approve', $member); return $this->actionLink('approve', $member);
} }
return null;
} }
/** /**
* Mark this comment as spam * Mark this comment as spam.
*/ */
public function markSpam() { public function markSpam() {
$this->IsSpam = true; $this->IsSpam = true;
$this->Moderated = true; $this->Moderated = true;
$this->write(); $this->write();
$this->extend('afterMarkSpam'); $this->extend('afterMarkSpam');
} }
/** /**
* Mark this comment as approved * Mark this comment as approved.
*/ */
public function markApproved() { public function markApproved() {
$this->IsSpam = false; $this->IsSpam = false;
$this->Moderated = true; $this->Moderated = true;
$this->write(); $this->write();
$this->extend('afterMarkApproved'); $this->extend('afterMarkApproved');
} }
/** /**
* Mark this comment as unapproved * Mark this comment as unapproved.
*/ */
public function markUnapproved() { public function markUnapproved() {
$this->Moderated = false; $this->Moderated = false;
$this->write(); $this->write();
$this->extend('afterMarkUnapproved'); $this->extend('afterMarkUnapproved');
} }
@ -507,90 +565,120 @@ class Comment extends DataObject {
return 'spam'; return 'spam';
} else if(!$this->Moderated) { } else if(!$this->Moderated) {
return 'unmoderated'; return 'unmoderated';
} else {
return 'notspam';
} }
return 'notspam';
} }
/** /**
* @return string * @return string
*/ */
public function getTitle() { public function getTitle() {
$title = sprintf(_t('Comment.COMMENTBY', 'Comment by %s', 'Name'), $this->getAuthorName()); $title = sprintf(
_t('Comment.COMMENTBY', 'Comment by %s', 'Name'),
$this->getAuthorName()
);
if($parent = $this->getParent()) { $parent = $this->getParent();
if($parent->Title) {
$title .= sprintf(' %s %s', _t('Comment.ON', 'on'), $parent->Title); if($parent && $parent->Title) {
} $title .= sprintf(
' %s %s',
_t('Comment.ON', 'on'),
$parent->Title
);
} }
return $title; return $title;
} }
/* /**
* Modify the default fields shown to the user * Modify the default fields shown to the user.
*/ */
public function getCMSFields() { public function getCMSFields() {
$commentField = $this->AllowHtml ? 'HtmlEditorField' : 'TextareaField';
$commentFieldType = 'TextareaField';
if($this->AllowHtml) {
$commentFieldType = 'HtmlEditorField';
}
$createdField = $this->obj('Created')
->scaffoldFormField($this->fieldLabel('Created'))
->performReadonlyTransformation();
$nameField = TextField::create('Name', $this->fieldLabel('AuthorName'));
$commentField = $commentFieldType::create('Comment', $this->fieldLabel('Comment'));
$emailField = EmailField::create('Email', $this->fieldLabel('Email'));
$urlField = TextField::create('URL', $this->fieldLabel('URL'));
$moderatedField = CheckboxField::create('Moderated', $this->fieldLabel('Moderated'));
$spamField = CheckboxField::create('IsSpam', $this->fieldLabel('IsSpam'));
$fieldGroup = FieldGroup::create(array(
$moderatedField,
$spamField,
));
$fieldGroup->setTitle('Options');
$fieldGroup->setDescription(_t(
'Comment.OPTION_DESCRIPTION',
'Unmoderated and spam comments will not be displayed until approved'
));
$fields = new FieldList( $fields = new FieldList(
$this $createdField,
->obj('Created') $nameField,
->scaffoldFormField($this->fieldLabel('Created')) $commentField,
->performReadonlyTransformation(), $emailField,
TextField::create('Name', $this->fieldLabel('AuthorName')), $urlField,
$commentField::create('Comment', $this->fieldLabel('Comment')), $fieldGroup
EmailField::create('Email', $this->fieldLabel('Email')),
TextField::create('URL', $this->fieldLabel('URL')),
FieldGroup::create(array(
CheckboxField::create('Moderated', $this->fieldLabel('Moderated')),
CheckboxField::create('IsSpam', $this->fieldLabel('IsSpam')),
))
->setTitle('Options')
->setDescription(_t(
'Comment.OPTION_DESCRIPTION',
'Unmoderated and spam comments will not be displayed until approved'
))
); );
// Show member name if given $author = $this->Author();
if(($author = $this->Author()) && $author->exists()) {
if($author && $author->exists()) {
$authorMemberField = TextField::create('AuthorMember', $this->fieldLabel('Author'), $author->Title);
$authorMemberField->performReadonlyTransformation();
$fields->insertAfter( $fields->insertAfter(
TextField::create('AuthorMember', $this->fieldLabel('Author'), $author->Title) $authorMemberField,
->performReadonlyTransformation(),
'Name' 'Name'
); );
} }
// Show parent comment if given $parent = $this->ParentComment();
if(($parent = $this->ParentComment()) && $parent->exists()) {
$fields->push(new HeaderField( if($parent && $parent->exists()) {
'ParentComment_Title',
_t('Comment.ParentComment_Title', 'This comment is a reply to the below')
));
// Created date
$fields->push( $fields->push(
$parent new HeaderField(
->obj('Created') 'ParentComment_Title',
_t('Comment.ParentComment_Title', 'This comment is a reply to the below')
)
);
$fields->push(
$parent->obj('Created')
->scaffoldFormField($parent->fieldLabel('Created')) ->scaffoldFormField($parent->fieldLabel('Created'))
->setName('ParentComment_Created') ->setName('ParentComment_Created')
->setValue($parent->Created) ->setValue($parent->Created)
->performReadonlyTransformation() ->performReadonlyTransformation()
); );
// Name (could be member or string value)
$fields->push( $fields->push(
$parent $parent->obj('AuthorName')
->obj('AuthorName')
->scaffoldFormField($parent->fieldLabel('AuthorName')) ->scaffoldFormField($parent->fieldLabel('AuthorName'))
->setName('ParentComment_AuthorName') ->setName('ParentComment_AuthorName')
->setValue($parent->getAuthorName()) ->setValue($parent->getAuthorName())
->performReadonlyTransformation() ->performReadonlyTransformation()
); );
// Comment body
$fields->push( $fields->push(
$parent $parent->obj('EscapedComment')
->obj('EscapedComment')
->scaffoldFormField($parent->fieldLabel('Comment')) ->scaffoldFormField($parent->fieldLabel('Comment'))
->setName('ParentComment_EscapedComment') ->setName('ParentComment_EscapedComment')
->setValue($parent->Comment) ->setValue($parent->Comment)
@ -599,157 +687,165 @@ class Comment extends DataObject {
} }
$this->extend('updateCMSFields', $fields); $this->extend('updateCMSFields', $fields);
return $fields; return $fields;
} }
/** /**
* @param String $dirtyHtml * @param string $dirtyHtml
* *
* @return String * @return string
*/ */
public function purifyHtml($dirtyHtml) { public function purifyHtml($dirtyHtml) {
$purifier = $this->getHtmlPurifierService(); return $this->getHtmlPurifierService()
return $purifier->purify($dirtyHtml); ->purify($dirtyHtml);
} }
/** /**
* @return HTMLPurifier (or anything with a "purify()" method) * @return HTMLPurifier
*/ */
public function getHtmlPurifierService() { public function getHtmlPurifierService() {
$config = HTMLPurifier_Config::createDefault(); $config = HTMLPurifier_Config::createDefault();
$config->set('HTML.AllowedElements', $this->getOption('html_allowed_elements')); $config->set('HTML.AllowedElements', $this->getOption('html_allowed_elements'));
$config->set('AutoFormat.AutoParagraph', true); $config->set('AutoFormat.AutoParagraph', true);
$config->set('AutoFormat.Linkify', true); $config->set('AutoFormat.Linkify', true);
$config->set('URI.DisableExternalResources', true); $config->set('URI.DisableExternalResources', true);
$config->set('Cache.SerializerPath', getTempFolder()); $config->set('Cache.SerializerPath', getTempFolder());
return new HTMLPurifier($config); return new HTMLPurifier($config);
} }
/** /**
* Calculate the Gravatar link from the email address * Calculate the Gravatar link from the email address.
* *
* @return string * @return string
*/ */
public function Gravatar() { public function Gravatar() {
$gravatar = ''; if($this->getOption('use_gravatar')) {
$use_gravatar = $this->getOption('use_gravatar'); return sprintf(
if($use_gravatar) { 'http://www.gravatar.com/avatar/%s?s=%s&d=%s&r=%s',
$gravatar = 'http://www.gravatar.com/avatar/' . md5(strtolower(trim($this->Email))); md5(strtolower(trim($this->Email))),
$gravatarsize = $this->getOption('gravatar_size'); $this->getOption('gravatar_size'),
$gravatardefault = $this->getOption('gravatar_default'); $this->getOption('gravatar_default'),
$gravatarrating = $this->getOption('gravatar_rating'); $this->getOption('gravatar_rating')
$gravatar .= '?s=' . $gravatarsize . '&d=' . $gravatardefault . '&r=' . $gravatarrating; );
} }
return $gravatar; return '';
} }
/** /**
* Determine if replies are enabled for this instance * Determine if replies are enabled for this instance.
* *
* @return boolean * @return bool
*/ */
public function getRepliesEnabled() { public function getRepliesEnabled() {
// Check reply option
if(!$this->getOption('nested_comments')) { if(!$this->getOption('nested_comments')) {
return false; return false;
} }
// Check if depth is limited
$maxLevel = $this->getOption('nested_depth'); $maxLevel = $this->getOption('nested_depth');
return !$maxLevel || $this->Depth < $maxLevel;
return !$maxLevel || $this->Depth < (int) $maxLevel;
} }
/** /**
* Returns the list of all replies * Returns the list of all replies.
* *
* @return SS_List * @return SS_List
*/ */
public function AllReplies() { public function AllReplies() {
// No replies if disabled
if(!$this->getRepliesEnabled()) { if(!$this->getRepliesEnabled()) {
return new ArrayList(); return new ArrayList();
} }
// Get all non-spam comments $order = $this->getOption('order_replies_by');
$order = $this->getOption('order_replies_by')
?: $this->getOption('order_comments_by'); if(!$order) {
$order = $this->getOption('order_comments_by');
}
$list = $this $list = $this
->ChildComments() ->ChildComments()
->sort($order); ->sort($order);
$this->extend('updateAllReplies', $list); $this->extend('updateAllReplies', $list);
return $list; return $list;
} }
/** /**
* Returns the list of replies, with spam and unmoderated items excluded, for use in the frontend * Returns the list of replies, with spam and un-moderated items excluded, for use in the
* frontend.
* *
* @return SS_List * @return SS_List
*/ */
public function Replies() { public function Replies() {
// No replies if disabled
if(!$this->getRepliesEnabled()) { if(!$this->getRepliesEnabled()) {
return new ArrayList(); return new ArrayList();
} }
$list = $this->AllReplies(); $list = $this->AllReplies();
// Filter spam comments for non-administrators if configured
$parent = $this->getParent(); $parent = $this->getParent();
$showSpam = $this->getOption('frontend_spam') && $parent && $parent->canModerateComments();
$showSpam = $parent && $parent->canModerateComments() && $this->getOption('frontend_spam');
if(!$showSpam) { if(!$showSpam) {
$list = $list->filter('IsSpam', 0); $list = $list->filter('IsSpam', 0);
} }
// Filter un-moderated comments for non-administrators if moderation is enabled $noModerationRequired = $parent && $parent->ModerationRequired === 'None';
$showUnmoderated = $parent && (
($parent->ModerationRequired === 'None') $showUnModerated = $noModerationRequired || $showSpam;
|| ($this->getOption('frontend_moderation') && $parent->canModerateComments())
); if(!$showUnModerated) {
if (!$showUnmoderated) { $list = $list->filter('Moderated', 1);
$list = $list->filter('Moderated', 1);
} }
$this->extend('updateReplies', $list); $this->extend('updateReplies', $list);
return $list; return $list;
} }
/** /**
* Returns the list of replies paged, with spam and unmoderated items excluded, for use in the frontend * Returns the list of replies paged, with spam and un-moderated items excluded, for use in the
* frontend.
* *
* @return PaginatedList * @return PaginatedList
*/ */
public function PagedReplies() { public function PagedReplies() {
$list = $this->Replies(); $list = $this->Replies();
// Add pagination
$list = new PaginatedList($list, Controller::curr()->getRequest()); $list = new PaginatedList($list, Controller::curr()->getRequest());
$list->setPaginationGetVar('repliesstart'.$this->ID);
$list->setPaginationGetVar('repliesstart' . $this->ID);
$list->setPageLength($this->getOption('comments_per_page')); $list->setPageLength($this->getOption('comments_per_page'));
$this->extend('updatePagedReplies', $list); $this->extend('updatePagedReplies', $list);
return $list; return $list;
} }
/** /**
* Generate a reply form for this comment * Generate a reply form for this comment.
* *
* @return Form * @return Form
*/ */
public function ReplyForm() { public function ReplyForm() {
// Ensure replies are enabled
if(!$this->getRepliesEnabled()) { if(!$this->getRepliesEnabled()) {
return null; return null;
} }
// Check parent is available
$parent = $this->getParent(); $parent = $this->getParent();
if(!$parent || !$parent->exists()) { if(!$parent || !$parent->exists()) {
return null; return null;
} }
// Build reply controller
$controller = CommentingController::create(); $controller = CommentingController::create();
$controller->setOwnerRecord($parent); $controller->setOwnerRecord($parent);
$controller->setBaseClass($parent->ClassName); $controller->setBaseClass($parent->ClassName);
$controller->setOwnerController(Controller::curr()); $controller->setOwnerController(Controller::curr());
@ -758,12 +854,14 @@ class Comment extends DataObject {
} }
/** /**
* Refresh of this comment in the hierarchy * Refresh of this comment in the hierarchy.
*/ */
public function updateDepth() { public function updateDepth() {
$parent = $this->ParentComment(); $parent = $this->ParentComment();
if($parent && $parent->exists()) { if($parent && $parent->exists()) {
$parent->updateDepth(); $parent->updateDepth();
$this->Depth = $parent->Depth + 1; $this->Depth = $parent->Depth + 1;
} else { } else {
$this->Depth = 1; $this->Depth = 1;
@ -771,27 +869,29 @@ class Comment extends DataObject {
} }
} }
/** /**
* Provides the ability to generate cryptographically secure tokens for comment moderation * Provides the ability to generate cryptographically secure tokens for comment moderation.
*/ */
class Comment_SecurityToken { class Comment_SecurityToken {
/**
* @var null|string
*/
private $secret = null; private $secret = null;
/** /**
* @param Comment $comment Comment to generate this token for * @param Comment $comment Comment to generate this token for.
*/ */
public function __construct($comment) { public function __construct($comment) {
if(!$comment->SecretToken) { if(!$comment->SecretToken) {
$comment->SecretToken = $this->generate(); $comment->SecretToken = $this->generate();
$comment->write(); $comment->write();
} }
$this->secret = $comment->SecretToken; $this->secret = $comment->SecretToken;
} }
/** /**
* Generate the token for the given salt and current secret * Generate the token for the given salt and current secret.
* *
* @param string $salt * @param string $salt
* *
@ -805,30 +905,34 @@ class Comment_SecurityToken {
* Get the member-specific salt. * Get the member-specific salt.
* *
* The reason for making the salt specific to a user is that it cannot be "passed in" via a * The reason for making the salt specific to a user is that it cannot be "passed in" via a
* querystring, requiring the same user to be present at both the link generation and the * query string, requiring the same user to be present at both the link generation and the
* controller action. * controller action.
* *
* @param string $salt Single use salt * @param string $salt
* @param Member $member Member object * @param Member $member
* *
* @return string Generated salt specific to this member * @return string
*/ */
protected function memberSalt($salt, $member) { protected function memberSalt($salt, $member) {
// Fallback to salting with ID in case the member has not one set $pepper = $member->Salt;
return $salt . ($member->Salt ?: $member->ID);
if(!$pepper) {
$pepper = $member->ID;
}
return $salt . $pepper;
} }
/** /**
* @param string $url Comment action URL * @param string $url
* @param Member $member Member to restrict access to this action to * @param Member $member
* *
* @return string * @return string
*/ */
public function addToUrl($url, $member) { public function addToUrl($url, $member) {
$salt = $this->generate(15); // New random salt; Will be passed into url $salt = $this->generate(15);
// Generate salt specific to this member $token = $this->getToken($this->memberSalt($salt, $member));
$memberSalt = $this->memberSalt($salt, $member);
$token = $this->getToken($memberSalt);
return Controller::join_links( return Controller::join_links(
$url, $url,
sprintf( sprintf(
@ -842,32 +946,37 @@ class Comment_SecurityToken {
/** /**
* @param SS_HTTPRequest $request * @param SS_HTTPRequest $request
* *
* @return boolean * @return bool
*/ */
public function checkRequest($request) { public function checkRequest($request) {
$member = Member::currentUser(); $member = Member::currentUser();
if(!$member) return false;
if(!$member) {
return false;
}
$salt = $request->getVar('s'); $salt = $request->getVar('s');
$memberSalt = $this->memberSalt($salt, $member); $token = $this->getToken($this->memberSalt($salt, $member));
$token = $this->getToken($memberSalt);
// Ensure tokens match
return $token === $request->getVar('t'); return $token === $request->getVar('t');
} }
/** /**
* Generates new random key * Generates new random key.
* *
* @param integer $length * @param null|int $length
* *
* @return string * @return string
*/ */
protected function generate($length = null) { protected function generate($length = null) {
$generator = new RandomGenerator(); $generator = new RandomGenerator();
$result = $generator->randomToken('sha256'); $result = $generator->randomToken('sha256');
if($length !== null) return substr($result, 0, $length);
if($length !== null) {
return substr($result, 0, $length);
}
return $result; return $result;
} }
} }

View File

@ -1,16 +1,13 @@
<?php <?php
/** /**
* Handles polymorphic relation for commentlist * Handles polymorphic relation for comment list.
* *
* Uses elements of PolymorphicHasManyList in 3.2 * Uses elements of PolymorphicHasManyList in 3.2
*
* @author dmooyman
*/ */
class CommentList extends HasManyList { class CommentList extends HasManyList {
/** /**
* Retrieve the name of the class this relation is filtered by * Retrieve the name of the class this relation is filtered by.
* *
* @return string * @return string
*/ */
@ -18,74 +15,83 @@ class CommentList extends HasManyList {
return $this->dataQuery->getQueryParam('Foreign.Class'); return $this->dataQuery->getQueryParam('Foreign.Class');
} }
/**
* {@inheritdoc}
*/
public function __construct($parentClassName) { public function __construct($parentClassName) {
parent::__construct('Comment', 'ParentID'); parent::__construct('Comment', 'ParentID');
// Ensure underlying DataQuery globally references the class filter
$this->dataQuery->setQueryParam('Foreign.Class', $parentClassName); $this->dataQuery->setQueryParam('Foreign.Class', $parentClassName);
// For queries with multiple foreign IDs (such as that generated by
// DataList::relation) the filter must be generalised to filter by subclasses
$classNames = Convert::raw2sql(ClassInfo::subclassesFor($parentClassName)); $classNames = Convert::raw2sql(ClassInfo::subclassesFor($parentClassName));
$this->dataQuery->where(sprintf( $this->dataQuery->where(sprintf(
"\"BaseClass\" IN ('%s')", implode("', '", $classNames) 'BaseClass IN (\'%s\')',
implode('\', \'', $classNames)
)); ));
} }
/** /**
* Adds the item to this relation. * Adds the item to this relation.
* *
* @param Comment $item The comment to be added * @param Comment $comment
*/ */
public function add($item) { public function add($comment) {
// Check item given if(is_numeric($comment)) {
if(is_numeric($item)) { $comment = Comment::get()->byID($comment);
$item = Comment::get()->byID($item); }
}
if(!($item instanceof Comment)) { if(!$comment instanceof Comment) {
throw new InvalidArgumentException("CommentList::add() expecting a Comment object, or ID value"); throw new InvalidArgumentException(
'CommentList::add() expecting a Comment object, or ID value'
);
} }
// Validate foreignID
$foreignID = $this->getForeignID(); $foreignID = $this->getForeignID();
if(!$foreignID || is_array($foreignID)) { if(!$foreignID || is_array($foreignID)) {
throw new InvalidArgumentException("CommentList::add() can't be called until a single foreign ID is set"); throw new InvalidArgumentException(
'CommentList::add() can\'t be called until a single foreign ID is set'
);
} }
$item->ParentID = $foreignID; $comment->ParentID = $foreignID;
$item->BaseClass = $this->getForeignClass(); $comment->BaseClass = $this->getForeignClass();
$item->write(); $comment->write();
} }
/** /**
* Remove a Comment from this relation by clearing the foreign key. Does not actually delete the comment. * Remove a Comment from this relation by clearing the foreign key. Does not actually delete
* the comment.
* *
* @param Comment $item The Comment to be removed * @param Comment $comment
*/ */
public function remove($item) { public function remove($comment) {
// Check item given if(is_numeric($comment)) {
if(is_numeric($item)) { $comment = Comment::get()->byID($comment);
$item = Comment::get()->byID($item); }
}
if(!($item instanceof Comment)) { if(!$comment instanceof Comment) {
throw new InvalidArgumentException("CommentList::remove() expecting a Comment object, or ID", throw new InvalidArgumentException(
E_USER_ERROR); 'CommentList::remove() expecting a Comment object, or ID',
E_USER_ERROR
);
} }
// Don't remove item with unrelated class key
$foreignClass = $this->getForeignClass(); $foreignClass = $this->getForeignClass();
$classNames = ClassInfo::subclassesFor($foreignClass);
if(!in_array($item->BaseClass, $classNames)) return;
// Don't remove item which doesn't belong to this list $subclasses = ClassInfo::subclassesFor($foreignClass);
if(!in_array($comment->BaseClass, $subclasses)) {
return;
}
$foreignID = $this->getForeignID(); $foreignID = $this->getForeignID();
if( empty($foreignID)
|| (is_array($foreignID) && in_array($item->ParentID, $foreignID)) if(empty($foreignID) || $foreignID == $comment->ParentID || (is_array($foreignID) && in_array($comment->ParentID, $foreignID))) {
|| $foreignID == $item->ParentID $comment->ParentID = null;
) { $comment->BaseClass = null;
$item->ParentID = null; $comment->write();
$item->BaseClass = null;
$item->write();
} }
} }
} }

View File

@ -1,114 +1,142 @@
<?php <?php
/** /**
* @mixin PHPUnit_Framework_TestCase
*
* @package comments * @package comments
* @subpackage tests * @subpackage tests
*/ */
class CommentingControllerTest extends FunctionalTest { class CommentingControllerTest extends FunctionalTest {
/**
* @var string
*/
public static $fixture_file = 'CommentsTest.yml'; public static $fixture_file = 'CommentsTest.yml';
/**
* @var array
*/
protected $extraDataObjects = array( protected $extraDataObjects = array(
'CommentableItem' 'HasComments',
); );
/**
* @var bool
*/
protected $securityEnabled; protected $securityEnabled;
/**
* {@inheritdoc}
*/
public function tearDown() { public function tearDown() {
if($this->securityEnabled) { if($this->securityEnabled) {
SecurityToken::enable(); SecurityToken::enable();
} else { } else {
SecurityToken::disable(); SecurityToken::disable();
} }
parent::tearDown(); parent::tearDown();
} }
/**
* {@inheritdoc}
*/
public function setUp() { public function setUp() {
parent::setUp(); parent::setUp();
$this->securityEnabled = SecurityToken::is_enabled(); $this->securityEnabled = SecurityToken::is_enabled();
} }
public function testRSS() { public function testRSS() {
$item = $this->objFromFixture('CommentableItem', 'first'); $item = $this->objFromFixture('HasComments', 'first');
// comments sitewide
$response = $this->get('CommentingController/rss'); $response = $this->get('CommentingController/rss');
$this->assertEquals(10, substr_count($response->getBody(), "<item>"), "10 approved, non spam comments on page 1");
$this->assertEquals(10, substr_count($response->getBody(), '<item>'), '10 approved, non spam comments on page 1');
$response = $this->get('CommentingController/rss?start=10'); $response = $this->get('CommentingController/rss?start=10');
$this->assertEquals(4, substr_count($response->getBody(), "<item>"), "3 approved, non spam comments on page 2");
// all comments on a type $this->assertEquals(4, substr_count($response->getBody(), '<item>'), '3 approved, non spam comments on page 2');
$response = $this->get('CommentingController/rss/CommentableItem');
$this->assertEquals(10, substr_count($response->getBody(), "<item>"));
$response = $this->get('CommentingController/rss/CommentableItem?start=10'); $response = $this->get('CommentingController/rss/HasComments');
$this->assertEquals(4, substr_count($response->getBody(), "<item>"), "3 approved, non spam comments on page 2");
// specific page $this->assertEquals(10, substr_count($response->getBody(), '<item>'));
$response = $this->get('CommentingController/rss/CommentableItem/'.$item->ID);
$this->assertEquals(1, substr_count($response->getBody(), "<item>")); $response = $this->get('CommentingController/rss/HasComments?start=10');
$this->assertEquals(4, substr_count($response->getBody(), '<item>'), '3 approved, non spam comments on page 2');
$response = $this->get('CommentingController/rss/HasComments/' . $item->ID);
$this->assertEquals(1, substr_count($response->getBody(), '<item>'));
$this->assertContains('<dc:creator>FA</dc:creator>', $response->getBody()); $this->assertContains('<dc:creator>FA</dc:creator>', $response->getBody());
// test accessing comments on a type that doesn't exist
$response = $this->get('CommentingController/rss/Fake'); $response = $this->get('CommentingController/rss/Fake');
$this->assertEquals(404, $response->getStatusCode()); $this->assertEquals(404, $response->getStatusCode());
} }
public function testCommentsForm() { public function testCommentsForm() {
SecurityToken::disable(); SecurityToken::disable();
$this->autoFollowRedirection = false;
$parent = $this->objFromFixture('CommentableItem', 'first');
// Test posting to base comment $this->autoFollowRedirection = false;
$parent = $this->objFromFixture('HasComments', 'first');
$response = $this->post('CommentingController/CommentsForm', $response = $this->post('CommentingController/CommentsForm',
array( array(
'Name' => 'Poster', 'Name' => 'Poster',
'Email' => 'guy@test.com', 'Email' => 'guy@test.com',
'Comment' => 'My Comment', 'Comment' => 'My Comment',
'ParentID' => $parent->ID, 'ParentID' => $parent->ID,
'BaseClass' => 'CommentableItem', 'BaseClass' => 'HasComments',
'action_doPostComment' => 'Post' 'action_doPostComment' => 'Post'
) )
); );
$this->assertEquals(302, $response->getStatusCode()); $this->assertEquals(302, $response->getStatusCode());
$this->assertStringStartsWith('CommentableItem_Controller#comment-', $response->getHeader('Location')); $this->assertStringStartsWith('HasComments_Controller#comment-', $response->getHeader('Location'));
$this->assertDOSEquals( $this->assertDOSEquals(
array(array( array(
'Name' => 'Poster', array(
'Email' => 'guy@test.com', 'Name' => 'Poster',
'Comment' => 'My Comment', 'Email' => 'guy@test.com',
'ParentID' => $parent->ID, 'Comment' => 'My Comment',
'BaseClass' => 'CommentableItem', 'ParentID' => $parent->ID,
)), 'BaseClass' => 'HasComments',
)
),
Comment::get()->filter('Email', 'guy@test.com') Comment::get()->filter('Email', 'guy@test.com')
); );
// Test posting to parent comment /**
* @var Comment $parentComment
*/
$parentComment = $this->objFromFixture('Comment', 'firstComA'); $parentComment = $this->objFromFixture('Comment', 'firstComA');
$this->assertEquals(0, $parentComment->ChildComments()->count()); $this->assertEquals(0, $parentComment->ChildComments()->count());
$response = $this->post( $response = $this->post(
'CommentingController/reply/'.$parentComment->ID, 'CommentingController/reply/' . $parentComment->ID,
array( array(
'Name' => 'Test Author', 'Name' => 'Test Author',
'Email' => 'test@test.com', 'Email' => 'test@test.com',
'Comment' => 'Making a reply to firstComA', 'Comment' => 'Making a reply to firstComA',
'ParentID' => $parent->ID, 'ParentID' => $parent->ID,
'BaseClass' => 'CommentableItem', 'BaseClass' => 'HasComments',
'ParentCommentID' => $parentComment->ID, 'ParentCommentID' => $parentComment->ID,
'action_doPostComment' => 'Post' 'action_doPostComment' => 'Post'
) )
); );
$this->assertEquals(302, $response->getStatusCode()); $this->assertEquals(302, $response->getStatusCode());
$this->assertStringStartsWith('CommentableItem_Controller#comment-', $response->getHeader('Location')); $this->assertStringStartsWith('HasComments_Controller#comment-', $response->getHeader('Location'));
$this->assertDOSEquals(array(array( $this->assertDOSEquals(array(
'Name' => 'Test Author', array(
'Name' => 'Test Author',
'Email' => 'test@test.com', 'Email' => 'test@test.com',
'Comment' => 'Making a reply to firstComA', 'Comment' => 'Making a reply to firstComA',
'ParentID' => $parent->ID, 'ParentID' => $parent->ID,
'BaseClass' => 'CommentableItem', 'BaseClass' => 'HasComments',
'ParentCommentID' => $parentComment->ID 'ParentCommentID' => $parentComment->ID
)), $parentComment->ChildComments()); )
), $parentComment->ChildComments());
} }
} }

View File

@ -1,21 +1,30 @@
<?php <?php
/** /**
* @mixin PHPUnit_Framework_TestCase
*
* @package comments * @package comments
*/ */
class CommentsTest extends FunctionalTest { class CommentsTest extends FunctionalTest {
/**
* @var string
*/
public static $fixture_file = 'comments/tests/CommentsTest.yml'; public static $fixture_file = 'comments/tests/CommentsTest.yml';
/**
* @var array
*/
protected $extraDataObjects = array( protected $extraDataObjects = array(
'CommentableItem' 'HasComments',
); );
/**
* {@inheritdoc}
*/
public function setUp() { public function setUp() {
parent::setUp(); parent::setUp();
Config::nest(); Config::nest();
// Set good default values
Config::inst()->update('CommentsExtension', 'comments', array( Config::inst()->update('CommentsExtension', 'comments', array(
'enabled' => true, 'enabled' => true,
'enabled_cms' => false, 'enabled_cms' => false,
@ -29,185 +38,241 @@ class CommentsTest extends FunctionalTest {
'frontend_spam' => false, 'frontend_spam' => false,
)); ));
// Configure this dataobject Config::inst()->update('HasComments', 'comments', array(
Config::inst()->update('CommentableItem', 'comments', array( 'enabled_cms' => true,
'enabled_cms' => true
)); ));
} }
/**
* {@inheritdoc}
*/
public function tearDown() { public function tearDown() {
Config::unnest(); Config::unnest();
parent::tearDown(); parent::tearDown();
} }
public function testCommentsList() { public function testCommentsList() {
// comments don't require moderation so unmoderated comments can be Config::inst()->update('HasComments', 'comments', array(
// shown but not spam posts
Config::inst()->update('CommentableItem', 'comments', array(
'require_moderation_nonmembers' => false, 'require_moderation_nonmembers' => false,
'require_moderation' => false, 'require_moderation' => false,
'require_moderation_cms' => false, 'require_moderation_cms' => false,
)); ));
$item = $this->objFromFixture('CommentableItem', 'spammed'); /**
* @var HasComments $item
*/
$item = $this->objFromFixture('HasComments', 'spammed');
$this->assertEquals('None', $item->ModerationRequired); $this->assertEquals('None', $item->ModerationRequired);
$this->assertDOSEquals(
array(
array('Name' => 'Comment 1'),
array('Name' => 'Comment 3')
),
$item->Comments(),
'Only 2 non spam posts should be shown'
);
$this->assertDOSEquals(array( Config::inst()->update('HasComments', 'comments', array('require_moderation_nonmembers' => true));
array('Name' => 'Comment 1'),
array('Name' => 'Comment 3')
), $item->Comments(), 'Only 2 non spam posts should be shown');
// when moderated, only moderated, non spam posts should be shown.
Config::inst()->update('CommentableItem', 'comments', array('require_moderation_nonmembers' => true));
$this->assertEquals('NonMembersOnly', $item->ModerationRequired); $this->assertEquals('NonMembersOnly', $item->ModerationRequired);
// Check that require_moderation overrides this option Config::inst()->update('HasComments', 'comments', array('require_moderation' => true));
Config::inst()->update('CommentableItem', 'comments', array('require_moderation' => true));
$this->assertEquals('Required', $item->ModerationRequired); $this->assertEquals('Required', $item->ModerationRequired);
$this->assertDOSEquals(array( $this->assertDOSEquals(
array('Name' => 'Comment 3') array(
), $item->Comments(), 'Only 1 non spam, moderated post should be shown'); array('Name' => 'Comment 3')
),
$item->Comments(),
'Only 1 non spam, moderated post should be shown'
);
$this->assertEquals(1, $item->Comments()->Count()); $this->assertEquals(1, $item->Comments()->Count());
// require_moderation_nonmembers still filters out unmoderated comments Config::inst()->update('HasComments', 'comments', array('require_moderation' => false));
Config::inst()->update('CommentableItem', 'comments', array('require_moderation' => false));
$this->assertEquals(1, $item->Comments()->Count()); $this->assertEquals(1, $item->Comments()->Count());
Config::inst()->update('CommentableItem', 'comments', array('require_moderation_nonmembers' => false)); Config::inst()->update('HasComments', 'comments', array('require_moderation_nonmembers' => false));
$this->assertEquals(2, $item->Comments()->Count()); $this->assertEquals(2, $item->Comments()->Count());
// With unmoderated comments set to display in frontend Config::inst()->update('HasComments', 'comments', array(
Config::inst()->update('CommentableItem', 'comments', array(
'require_moderation' => true, 'require_moderation' => true,
'frontend_moderation' => true 'frontend_moderation' => true,
)); ));
$this->assertEquals(1, $item->Comments()->Count()); $this->assertEquals(1, $item->Comments()->Count());
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
$this->assertEquals(2, $item->Comments()->Count()); $this->assertEquals(2, $item->Comments()->Count());
// With spam comments set to display in frontend Config::inst()->update('HasComments', 'comments', array(
Config::inst()->update('CommentableItem', 'comments', array(
'require_moderation' => true, 'require_moderation' => true,
'frontend_moderation' => false, 'frontend_moderation' => false,
'frontend_spam' => true, 'frontend_spam' => true,
)); ));
if($member = Member::currentUser()) $member->logOut();
if($member = Member::currentUser()) {
$member->logOut();
}
$this->assertEquals(1, $item->Comments()->Count()); $this->assertEquals(1, $item->Comments()->Count());
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
$this->assertEquals(2, $item->Comments()->Count()); $this->assertEquals(2, $item->Comments()->Count());
Config::inst()->update('HasComments', 'comments', array(
// With spam and unmoderated comments set to display in frontend
Config::inst()->update('CommentableItem', 'comments', array(
'require_moderation' => true, 'require_moderation' => true,
'frontend_moderation' => true, 'frontend_moderation' => true,
'frontend_spam' => true, 'frontend_spam' => true,
)); ));
if($member = Member::currentUser()) $member->logOut();
if($member = Member::currentUser()) {
$member->logOut();
}
$this->assertEquals(1, $item->Comments()->Count()); $this->assertEquals(1, $item->Comments()->Count());
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
$this->assertEquals(4, $item->Comments()->Count()); $this->assertEquals(4, $item->Comments()->Count());
} }
/** /**
* Test moderation options configured via the CMS * Test moderation options configured via the CMS.
*/ */
public function testCommentCMSModerationList() { public function testCommentCMSModerationList() {
// comments don't require moderation so unmoderated comments can be Config::inst()->update('HasComments', 'comments', array(
// shown but not spam posts
Config::inst()->update('CommentableItem', 'comments', array(
'require_moderation' => true, 'require_moderation' => true,
'require_moderation_cms' => true, 'require_moderation_cms' => true,
)); ));
$item = $this->objFromFixture('CommentableItem', 'spammed'); /**
* @var HasComments $item
*/
$item = $this->objFromFixture('HasComments', 'spammed');
$this->assertEquals('None', $item->ModerationRequired); $this->assertEquals('None', $item->ModerationRequired);
$this->assertDOSEquals(
array(
array('Name' => 'Comment 1'),
array('Name' => 'Comment 3')
),
$item->Comments(),
'Only 2 non spam posts should be shown'
);
$this->assertDOSEquals(array(
array('Name' => 'Comment 1'),
array('Name' => 'Comment 3')
), $item->Comments(), 'Only 2 non spam posts should be shown');
// when moderated, only moderated, non spam posts should be shown.
$item->ModerationRequired = 'NonMembersOnly'; $item->ModerationRequired = 'NonMembersOnly';
$item->write(); $item->write();
$this->assertEquals('NonMembersOnly', $item->ModerationRequired); $this->assertEquals('NonMembersOnly', $item->ModerationRequired);
// Check that require_moderation overrides this option
$item->ModerationRequired = 'Required'; $item->ModerationRequired = 'Required';
$item->write(); $item->write();
$this->assertEquals('Required', $item->ModerationRequired); $this->assertEquals('Required', $item->ModerationRequired);
$this->assertDOSEquals(array( $this->assertDOSEquals(
array('Name' => 'Comment 3') array(
), $item->Comments(), 'Only 1 non spam, moderated post should be shown'); array('Name' => 'Comment 3')
$this->assertEquals(1, $item->Comments()->Count()); ),
$item->Comments(),
'Only 1 non spam, moderated post should be shown'
);
// require_moderation_nonmembers still filters out unmoderated comments
$item->ModerationRequired = 'NonMembersOnly'; $item->ModerationRequired = 'NonMembersOnly';
$item->write(); $item->write();
$this->assertEquals(1, $item->Comments()->Count()); $this->assertEquals(1, $item->Comments()->Count());
$item->ModerationRequired = 'None'; $item->ModerationRequired = 'None';
$item->write(); $item->write();
$this->assertEquals(2, $item->Comments()->Count()); $this->assertEquals(2, $item->Comments()->Count());
} }
public function testCanPostComment() { public function testCanPostComment() {
Config::inst()->update('CommentableItem', 'comments', array( Config::inst()->update('HasComments', 'comments', array(
'require_login' => false, 'require_login' => false,
'require_login_cms' => false, 'require_login_cms' => false,
'required_permission' => false, 'required_permission' => false,
)); ));
$item = $this->objFromFixture('CommentableItem', 'first');
$item2 = $this->objFromFixture('CommentableItem', 'second');
// Test restriction free commenting /**
if($member = Member::currentUser()) $member->logOut(); * @var HasComments $item
*/
$item = $this->objFromFixture('HasComments', 'first');
/**
* @var HasComments $item2
*/
$item2 = $this->objFromFixture('HasComments', 'second');
if($member = Member::currentUser()) {
$member->logOut();
}
$this->assertFalse($item->CommentsRequireLogin); $this->assertFalse($item->CommentsRequireLogin);
$this->assertTrue($item->canPostComment()); $this->assertTrue($item->canPostComment());
// Test permission required to post Config::inst()->update('HasComments', 'comments', array(
Config::inst()->update('CommentableItem', 'comments', array(
'require_login' => true, 'require_login' => true,
'required_permission' => 'POSTING_PERMISSION', 'required_permission' => 'POSTING_PERMISSION',
)); ));
$this->assertTrue($item->CommentsRequireLogin); $this->assertTrue($item->CommentsRequireLogin);
$this->assertFalse($item->canPostComment()); $this->assertFalse($item->canPostComment());
$this->logInWithPermission('WRONG_ONE'); $this->logInWithPermission('WRONG_ONE');
$this->assertFalse($item->canPostComment()); $this->assertFalse($item->canPostComment());
$this->logInWithPermission('POSTING_PERMISSION'); $this->logInWithPermission('POSTING_PERMISSION');
$this->assertTrue($item->canPostComment());
$this->logInWithPermission('ADMIN');
$this->assertTrue($item->canPostComment()); $this->assertTrue($item->canPostComment());
// Test require login to post, but not any permissions $this->logInWithPermission('ADMIN');
Config::inst()->update('CommentableItem', 'comments', array(
$this->assertTrue($item->canPostComment());
Config::inst()->update('HasComments', 'comments', array(
'required_permission' => false, 'required_permission' => false,
)); ));
$this->assertTrue($item->CommentsRequireLogin); $this->assertTrue($item->CommentsRequireLogin);
if($member = Member::currentUser()) $member->logOut();
if($member = Member::currentUser()) {
$member->logOut();
}
$this->assertFalse($item->canPostComment()); $this->assertFalse($item->canPostComment());
$this->logInWithPermission('ANY_PERMISSION'); $this->logInWithPermission('ANY_PERMISSION');
$this->assertTrue($item->canPostComment()); $this->assertTrue($item->canPostComment());
// Test options set via CMS Config::inst()->update('HasComments', 'comments', array(
Config::inst()->update('CommentableItem', 'comments', array(
'require_login' => true, 'require_login' => true,
'require_login_cms' => true, 'require_login_cms' => true,
)); ));
$this->assertFalse($item->CommentsRequireLogin); $this->assertFalse($item->CommentsRequireLogin);
$this->assertTrue($item2->CommentsRequireLogin); $this->assertTrue($item2->CommentsRequireLogin);
if($member = Member::currentUser()) $member->logOut();
if($member = Member::currentUser()) {
$member->logOut();
}
$this->assertTrue($item->canPostComment()); $this->assertTrue($item->canPostComment());
$this->assertFalse($item2->canPostComment()); $this->assertFalse($item2->canPostComment());
// Login grants permission to post
$this->logInWithPermission('ANY_PERMISSION'); $this->logInWithPermission('ANY_PERMISSION');
$this->assertTrue($item->canPostComment()); $this->assertTrue($item->canPostComment());
$this->assertTrue($item2->canPostComment()); $this->assertTrue($item2->canPostComment());
@ -255,201 +320,258 @@ class CommentsTest extends FunctionalTest {
} }
public function testDeleteComment() { public function testDeleteComment() {
// Test anonymous user if($member = Member::currentUser()) {
if($member = Member::currentUser()) $member->logOut(); $member->logOut();
}
/**
* @var Comment $comment
*/
$comment = $this->objFromFixture('Comment', 'firstComA'); $comment = $this->objFromFixture('Comment', 'firstComA');
$commentID = $comment->ID;
$this->assertNull($comment->DeleteLink(), 'No permission to see delete link'); $this->assertNull($comment->DeleteLink(), 'No permission to see delete link');
$delete = $this->get('CommentingController/delete/'.$comment->ID.'?ajax=1');
$delete = $this->get(sprintf(
'CommentingController/delete/%s?ajax=1',
$comment->ID
));
$this->assertEquals(403, $delete->getStatusCode()); $this->assertEquals(403, $delete->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID);
$this->assertTrue($check && $check->exists());
// Test non-authenticated user
$this->logInAs('visitor'); $this->logInAs('visitor');
$this->assertNull($comment->DeleteLink(), 'No permission to see delete link'); $this->assertNull($comment->DeleteLink(), 'No permission to see delete link');
// Test authenticated user
$this->logInAs('commentadmin'); $this->logInAs('commentadmin');
$comment = $this->objFromFixture('Comment', 'firstComA');
$commentID = $comment->ID;
$adminComment1Link = $comment->DeleteLink();
$this->assertContains('CommentingController/delete/'.$commentID.'?t=', $adminComment1Link);
// Test that this link can't be shared / XSS exploited $commentAdminLink = $comment->DeleteLink();
$this->assertContains(
sprintf(
'CommentingController/delete/%s?t=',
$comment->ID
),
$commentAdminLink
);
$this->logInAs('commentadmin2'); $this->logInAs('commentadmin2');
$delete = $this->get($adminComment1Link);
$this->assertEquals(400, $delete->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID);
$this->assertTrue($check && $check->exists());
// Test that this other admin can delete the comment with their own link $delete = $this->get($commentAdminLink);
$adminComment2Link = $comment->DeleteLink();
$this->assertNotEquals($adminComment2Link, $adminComment1Link); $this->assertEquals(400, $delete->getStatusCode());
$this->assertNotEquals($comment->DeleteLink(), $commentAdminLink);
$this->autoFollowRedirection = false; $this->autoFollowRedirection = false;
$delete = $this->get($adminComment2Link);
$delete = $this->get($comment->DeleteLink());
$this->assertEquals(302, $delete->getStatusCode()); $this->assertEquals(302, $delete->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID); $this->assertFalse(DataObject::get_by_id('Comment', $comment->ID));
$this->assertFalse($check && $check->exists());
} }
public function testSpamComment() { public function testSpamComment() {
// Test anonymous user if($member = Member::currentUser()) {
if($member = Member::currentUser()) $member->logOut(); $member->logOut();
}
/**
* @var Comment $comment
*/
$comment = $this->objFromFixture('Comment', 'firstComA'); $comment = $this->objFromFixture('Comment', 'firstComA');
$commentID = $comment->ID;
$this->assertNull($comment->SpamLink(), 'No permission to see mark as spam link'); $this->assertNull($comment->SpamLink(), 'No permission to see mark as spam link');
$spam = $this->get('CommentingController/spam/'.$comment->ID.'?ajax=1');
$spam = $this->get(sprintf(
'CommentingController/spam/%s?ajax=1',
$comment->ID
));
$this->assertEquals(403, $spam->getStatusCode()); $this->assertEquals(403, $spam->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID); $this->assertEquals(0, $comment->IsSpam, 'No permission to mark as spam');
$this->assertEquals(0, $check->IsSpam, 'No permission to mark as spam');
// Test non-authenticated user
$this->logInAs('visitor'); $this->logInAs('visitor');
$this->assertNull($comment->SpamLink(), 'No permission to see mark as spam link'); $this->assertNull($comment->SpamLink(), 'No permission to see mark as spam link');
// Test authenticated user
$this->logInAs('commentadmin'); $this->logInAs('commentadmin');
$comment = $this->objFromFixture('Comment', 'firstComA');
$commentID = $comment->ID;
$adminComment1Link = $comment->SpamLink();
$this->assertContains('CommentingController/spam/'.$commentID.'?t=', $adminComment1Link);
// Test that this link can't be shared / XSS exploited $commentAdminLink = $comment->SpamLink();
$this->assertContains(
sprintf(
'CommentingController/spam/%s?t=',
$comment->ID
),
$commentAdminLink
);
$this->logInAs('commentadmin2'); $this->logInAs('commentadmin2');
$spam = $this->get($adminComment1Link);
$spam = $this->get($commentAdminLink);
$this->assertEquals(400, $spam->getStatusCode()); $this->assertEquals(400, $spam->getStatusCode());
$check = DataObject::get_by_id('Comment', $comment->ID); $this->assertNotEquals($comment->SpamLink(), $commentAdminLink);
$this->assertEquals(0, $check->IsSpam, 'No permission to mark as spam');
// Test that this other admin can spam the comment with their own link
$adminComment2Link = $comment->SpamLink();
$this->assertNotEquals($adminComment2Link, $adminComment1Link);
$this->autoFollowRedirection = false; $this->autoFollowRedirection = false;
$spam = $this->get($adminComment2Link);
$this->assertEquals(302, $spam->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID);
$this->assertEquals(1, $check->IsSpam);
// Cannot re-spam spammed comment $spam = $this->get($comment->SpamLink());
$this->assertNull($check->SpamLink());
$this->assertEquals(302, $spam->getStatusCode());
/**
* @var Comment $comment
*/
$comment = DataObject::get_by_id('Comment', $comment->ID);
$this->assertEquals(1, $comment->IsSpam);
$this->assertNull($comment->SpamLink());
} }
public function testHamComment() { public function testHamComment() {
// Test anonymous user if($member = Member::currentUser()) {
if($member = Member::currentUser()) $member->logOut(); $member->logOut();
}
/**
* @var Comment $comment
*/
$comment = $this->objFromFixture('Comment', 'secondComC'); $comment = $this->objFromFixture('Comment', 'secondComC');
$commentID = $comment->ID;
$this->assertNull($comment->HamLink(), 'No permission to see mark as ham link'); $this->assertNull($comment->HamLink(), 'No permission to see mark as ham link');
$ham = $this->get('CommentingController/ham/'.$comment->ID.'?ajax=1');
$ham = $this->get(sprintf(
'CommentingController/ham/%s?ajax=1',
$comment->ID
));
$this->assertEquals(403, $ham->getStatusCode()); $this->assertEquals(403, $ham->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID);
$this->assertEquals(1, $check->IsSpam, 'No permission to mark as ham');
// Test non-authenticated user
$this->logInAs('visitor'); $this->logInAs('visitor');
$this->assertNull($comment->HamLink(), 'No permission to see mark as ham link'); $this->assertNull($comment->HamLink(), 'No permission to see mark as ham link');
// Test authenticated user
$this->logInAs('commentadmin'); $this->logInAs('commentadmin');
$comment = $this->objFromFixture('Comment', 'secondComC');
$commentID = $comment->ID;
$adminComment1Link = $comment->HamLink();
$this->assertContains('CommentingController/ham/'.$commentID.'?t=', $adminComment1Link);
// Test that this link can't be shared / XSS exploited $adminCommentLink = $comment->HamLink();
$this->assertContains(
sprintf(
'CommentingController/ham/%s?t=',
$comment->ID
),
$adminCommentLink
);
$this->logInAs('commentadmin2'); $this->logInAs('commentadmin2');
$ham = $this->get($adminComment1Link);
$ham = $this->get($adminCommentLink);
$this->assertEquals(400, $ham->getStatusCode()); $this->assertEquals(400, $ham->getStatusCode());
$check = DataObject::get_by_id('Comment', $comment->ID); $this->assertNotEquals($comment->HamLink(), $adminCommentLink);
$this->assertEquals(1, $check->IsSpam, 'No permission to mark as ham');
// Test that this other admin can ham the comment with their own link
$adminComment2Link = $comment->HamLink();
$this->assertNotEquals($adminComment2Link, $adminComment1Link);
$this->autoFollowRedirection = false; $this->autoFollowRedirection = false;
$ham = $this->get($adminComment2Link);
$this->assertEquals(302, $ham->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID);
$this->assertEquals(0, $check->IsSpam);
// Cannot re-ham hammed comment $ham = $this->get($comment->HamLink());
$this->assertNull($check->HamLink());
$this->assertEquals(302, $ham->getStatusCode());
/**
* @var Comment $comment
*/
$comment = DataObject::get_by_id('Comment', $comment->ID);
$this->assertEquals(0, $comment->IsSpam);
$this->assertNull($comment->HamLink());
} }
public function testApproveComment() { public function testApproveComment() {
// Test anonymous user if($member = Member::currentUser()) {
if($member = Member::currentUser()) $member->logOut(); $member->logOut();
}
/**
* @var Comment $comment
*/
$comment = $this->objFromFixture('Comment', 'secondComB'); $comment = $this->objFromFixture('Comment', 'secondComB');
$commentID = $comment->ID;
$this->assertNull($comment->ApproveLink(), 'No permission to see approve link'); $this->assertNull($comment->ApproveLink(), 'No permission to see approve link');
$approve = $this->get('CommentingController/approve/'.$comment->ID.'?ajax=1');
$approve = $this->get(sprintf(
'CommentingController/approve/%s?ajax=1',
$comment->ID
));
$this->assertEquals(403, $approve->getStatusCode()); $this->assertEquals(403, $approve->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID);
$this->assertEquals(0, $check->Moderated, 'No permission to approve');
// Test non-authenticated user
$this->logInAs('visitor'); $this->logInAs('visitor');
$this->assertNull($comment->ApproveLink(), 'No permission to see approve link'); $this->assertNull($comment->ApproveLink(), 'No permission to see approve link');
// Test authenticated user
$this->logInAs('commentadmin'); $this->logInAs('commentadmin');
$comment = $this->objFromFixture('Comment', 'secondComB');
$commentID = $comment->ID;
$adminComment1Link = $comment->ApproveLink();
$this->assertContains('CommentingController/approve/'.$commentID.'?t=', $adminComment1Link);
// Test that this link can't be shared / XSS exploited $adminCommentLink = $comment->ApproveLink();
$this->assertContains(
sprintf(
'CommentingController/approve/%s?t=',
$comment->ID
),
$adminCommentLink
);
$this->logInAs('commentadmin2'); $this->logInAs('commentadmin2');
$approve = $this->get($adminComment1Link);
$approve = $this->get($adminCommentLink);
$this->assertEquals(400, $approve->getStatusCode()); $this->assertEquals(400, $approve->getStatusCode());
$check = DataObject::get_by_id('Comment', $comment->ID);
$this->assertEquals(0, $check->Moderated, 'No permission to approve');
// Test that this other admin can approve the comment with their own link $this->assertNotEquals($comment->ApproveLink(), $adminCommentLink);
$adminComment2Link = $comment->ApproveLink();
$this->assertNotEquals($adminComment2Link, $adminComment1Link);
$this->autoFollowRedirection = false; $this->autoFollowRedirection = false;
$approve = $this->get($adminComment2Link);
$this->assertEquals(302, $approve->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID);
$this->assertEquals(1, $check->Moderated);
// Cannot re-approve approved comment $approve = $this->get($comment->ApproveLink());
$this->assertNull($check->ApproveLink());
$this->assertEquals(302, $approve->getStatusCode());
/**
* @var Comment $comment
*/
$comment = DataObject::get_by_id('Comment', $comment->ID);
$this->assertEquals(1, $comment->Moderated);
$this->assertNull($comment->ApproveLink());
} }
public function testCommenterURLWrite() { public function testCommenterURLWrite() {
$comment = new Comment(); $comment = new Comment();
// We only care about the CommenterURL, so only set that
// Check a http and https URL. Add more test urls here as needed.
$protocols = array( $protocols = array(
'Http', 'HTTP',
'Https', 'HTTPS',
); );
$url = '://example.com'; $url = '://example.com';
foreach($protocols as $protocol) { foreach($protocols as $protocol) {
$comment->CommenterURL = $protocol . $url; $comment->URL = $protocol . $url;
// The protocol should stay as if, assuming it is valid
$comment->write(); $comment->write();
$this->assertEquals($comment->CommenterURL, $protocol . $url, $protocol . ':// is a valid protocol');
$this->assertEquals($comment->URL, $protocol . $url, $protocol . ':// is a valid protocol');
} }
} }
public function testSanitizesWithAllowHtml() { public function testSanitizesWithAllowHtml() {
if(!class_exists('HTMLPurifier')) { if(!class_exists('HTMLPurifier')) {
$this->markTestSkipped('HTMLPurifier class not found'); $this->markTestSkipped('HTMLPurifier class not found');
return;
} }
$origAllowed = Commenting::get_config_value('CommentableItem','html_allowed'); $originalHtmlAllowed = Commenting::get_config_value('HasComments', 'html_allowed');
// Without HTML allowed
$comment1 = new Comment(); $comment1 = new Comment();
$comment1->BaseClass = 'CommentableItem'; $comment1->BaseClass = 'HasComments';
$comment1->Comment = '<p><script>alert("w00t")</script>my comment</p>'; $comment1->Comment = '<p><script>alert("w00t")</script>my comment</p>';
$comment1->write(); $comment1->write();
$this->assertEquals( $this->assertEquals(
'<p><script>alert("w00t")</script>my comment</p>', '<p><script>alert("w00t")</script>my comment</p>',
$comment1->Comment, $comment1->Comment,
@ -457,19 +579,20 @@ class CommentsTest extends FunctionalTest {
'which is correct behaviour because the HTML will be escaped' 'which is correct behaviour because the HTML will be escaped'
); );
// With HTML allowed Commenting::set_config_value('HasComments', 'html_allowed', true);
Commenting::set_config_value('CommentableItem','html_allowed', true);
$comment2 = new Comment(); $comment2 = new Comment();
$comment2->BaseClass = 'CommentableItem'; $comment2->BaseClass = 'HasComments';
$comment2->Comment = '<p><script>alert("w00t")</script>my comment</p>'; $comment2->Comment = '<p><script>alert("w00t")</script>my comment</p>';
$comment2->write(); $comment2->write();
$this->assertEquals( $this->assertEquals(
'<p>my comment</p>', '<p>my comment</p>',
$comment2->Comment, $comment2->Comment,
'Removes HTML tags which are not on the whitelist' 'Removes HTML tags which are not on the whitelist'
); );
Commenting::set_config_value('CommentableItem','html_allowed', $origAllowed); Commenting::set_config_value('HasComments', 'html_allowed', $originalHtmlAllowed);
} }
public function testDefaultTemplateRendersHtmlWithAllowHtml() { public function testDefaultTemplateRendersHtmlWithAllowHtml() {
@ -477,63 +600,92 @@ class CommentsTest extends FunctionalTest {
$this->markTestSkipped('HTMLPurifier class not found'); $this->markTestSkipped('HTMLPurifier class not found');
} }
$origAllowed = Commenting::get_config_value('CommentableItem', 'html_allowed'); $originalHtmlAllowed = Commenting::get_config_value('HasComments', 'html_allowed');
$item = new CommentableItem();
$item = new HasComments();
$item->write(); $item->write();
// Without HTML allowed
$comment = new Comment(); $comment = new Comment();
$comment->Comment = '<p>my comment</p>'; $comment->Comment = '<p>my comment</p>';
$comment->ParentID = $item->ID; $comment->ParentID = $item->ID;
$comment->BaseClass = 'CommentableItem'; $comment->BaseClass = 'HasComments';
$comment->write(); $comment->write();
$html = $item->customise(array('CommentsEnabled' => true))->renderWith('CommentsInterface'); $html = $item
->customise(array(
'CommentsEnabled' => true,
))
->renderWith('CommentsInterface');
$this->assertContains( $this->assertContains(
'&lt;p&gt;my comment&lt;/p&gt;', '&lt;p&gt;my comment&lt;/p&gt;',
$html $html
); );
Commenting::set_config_value('CommentableItem','html_allowed', true); Commenting::set_config_value('HasComments', 'html_allowed', true);
$html = $item->customise(array('CommentsEnabled' => true))->renderWith('CommentsInterface');
$html = $item
->customise(array(
'CommentsEnabled' => true,
))
->renderWith('CommentsInterface');
$this->assertContains( $this->assertContains(
'<p>my comment</p>', '<p>my comment</p>',
$html $html
); );
Commenting::set_config_value('CommentableItem','html_allowed', $origAllowed); Commenting::set_config_value('HasComments', 'html_allowed', $originalHtmlAllowed);
} }
} }
/** /**
* @mixin CommentsExtension
*
* @package comments * @package comments
* @subpackage tests * @subpackage tests
*/ */
class CommentableItem extends DataObject implements TestOnly { class HasComments extends DataObject implements TestOnly {
/**
* @var array
*/
private static $db = array( private static $db = array(
'ProvideComments' => 'Boolean', 'ProvideComments' => 'Boolean',
'Title' => 'Varchar' 'Title' => 'Varchar',
); );
/**
* @var array
*/
private static $extensions = array( private static $extensions = array(
'CommentsExtension' 'CommentsExtension',
); );
/**
* @return string
*/
public function RelativeLink() { public function RelativeLink() {
return "CommentableItem_Controller"; return 'HasComments_Controller';
} }
/**
* {@inheritdoc}
*/
public function canView($member = null) { public function canView($member = null) {
return true; return true;
} }
/**
* @return string
*/
public function Link() { public function Link() {
return $this->RelativeLink(); return $this->RelativeLink();
} }
/**
* @return string
*/
public function AbsoluteLink() { public function AbsoluteLink() {
return Director::absoluteURL($this->RelativeLink()); return Director::absoluteURL($this->RelativeLink());
} }
@ -543,9 +695,11 @@ class CommentableItem extends DataObject implements TestOnly {
* @package comments * @package comments
* @subpackage tests * @subpackage tests
*/ */
class CommentableItem_Controller extends Controller implements TestOnly { class HasComments_Controller extends Controller implements TestOnly {
/**
* @return Form
*/
public function index() { public function index() {
return CommentableItem::get()->first()->CommentsForm(); return HasComments::get()->first()->CommentsForm();
} }
} }

View File

@ -16,7 +16,7 @@ Permission:
Code: CMS_ACCESS_CommentAdmin Code: CMS_ACCESS_CommentAdmin
Group: =>Group.commentadmins Group: =>Group.commentadmins
CommentableItem: HasComments:
first: first:
Title: First Title: First
ProvideComments: 1 ProvideComments: 1
@ -38,135 +38,135 @@ CommentableItem:
Comment: Comment:
firstComA: firstComA:
ParentID: =>CommentableItem.first ParentID: =>HasComments.first
Name: FA Name: FA
Comment: textFA Comment: textFA
BaseClass: CommentableItem BaseClass: HasComments
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
secondComA: secondComA:
ParentID: =>CommentableItem.second ParentID: =>HasComments.second
Name: SA Name: SA
Comment: textSA Comment: textSA
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
secondComB: secondComB:
ParentID: =>CommentableItem.second ParentID: =>HasComments.second
Name: SB Name: SB
Comment: textSB Comment: textSB
Moderated: 0 Moderated: 0
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
secondComC: secondComC:
ParentID: =>CommentableItem.second ParentID: =>HasComments.second
Name: SB Name: SB
Comment: textSB Comment: textSB
Moderated: 1 Moderated: 1
IsSpam: 1 IsSpam: 1
BaseClass: CommentableItem BaseClass: HasComments
thirdComA: thirdComA:
ParentID: =>CommentableItem.third ParentID: =>HasComments.third
Name: TA Name: TA
Comment: textTA Comment: textTA
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
thirdComB: thirdComB:
ParentID: =>CommentableItem.third ParentID: =>HasComments.third
Name: TB Name: TB
Comment: textTB Comment: textTB
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
thirdComC: thirdComC:
ParentID: =>CommentableItem.third ParentID: =>HasComments.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
thirdComD: thirdComD:
ParentID: =>CommentableItem.third ParentID: =>HasComments.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
BaseClass: CommentableItem BaseClass: HasComments
thirdComE: thirdComE:
ParentID: =>CommentableItem.third ParentID: =>HasComments.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
BaseClass: CommentableItem BaseClass: HasComments
thirdComF: thirdComF:
ParentID: =>CommentableItem.third ParentID: =>HasComments.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
thirdComG: thirdComG:
ParentID: =>CommentableItem.third ParentID: =>HasComments.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
thirdComH: thirdComH:
ParentID: =>CommentableItem.third ParentID: =>HasComments.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
thirdComI: thirdComI:
ParentID: =>CommentableItem.third ParentID: =>HasComments.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
thirdComJ: thirdComJ:
ParentID: =>CommentableItem.third ParentID: =>HasComments.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
thirdComK: thirdComK:
ParentID: =>CommentableItem.third ParentID: =>HasComments.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
disabledCom: disabledCom:
ParentID: =>CommentableItem.nocomments ParentID: =>HasComments.nocomments
Name: Disabled Name: Disabled
Moderated: 0 Moderated: 0
IsSpam: 1 IsSpam: 1
BaseClass: CommentableItem BaseClass: HasComments
testCommentList1: testCommentList1:
ParentID: =>CommentableItem.spammed ParentID: =>HasComments.spammed
Name: Comment 1 Name: Comment 1
Moderated: 0 Moderated: 0
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
testCommentList2: testCommentList2:
ParentID: =>CommentableItem.spammed ParentID: =>HasComments.spammed
Name: Comment 2 Name: Comment 2
Moderated: 1 Moderated: 1
IsSpam: 1 IsSpam: 1
BaseClass: CommentableItem BaseClass: HasComments
testCommentList3: testCommentList3:
ParentID: =>CommentableItem.spammed ParentID: =>HasComments.spammed
Name: Comment 3 Name: Comment 3
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: HasComments
testCommentList4: testCommentList4:
ParentID: =>CommentableItem.spammed ParentID: =>HasComments.spammed
Name: Comment 4 Name: Comment 4
Moderated: 0 Moderated: 0
IsSpam: 1 IsSpam: 1
BaseClass: CommentableItem BaseClass: HasComments