diff --git a/code/Commenting.php b/code/Commenting.php index f81dd25..b4ec5c9 100644 --- a/code/Commenting.php +++ b/code/Commenting.php @@ -1,58 +1,60 @@ update($class, 'extensions', array('CommentsExtension')); - // Check if settings must be customised - if($settings === false) return; - if(!is_array($settings)) { - throw new InvalidArgumentException('$settings needs to be an array or null'); + if($settings === false) { + return; } + + if(!is_array($settings)) { + throw new InvalidArgumentException( + '$settings needs to be an array or null' + ); + } + Config::inst()->update($class, 'comments', $settings); } /** - * Removes commenting from a {@link DataObject}. Does not remove existing comments - * but does remove the extension. + * Removes commenting from a {@link DataObject}. Does not remove existing comments but does + * remove the extension. * * @deprecated since version 2.0 * - * @param string $class Class to remove {@link CommentsExtension} from + * @param string $class */ public static function remove($class) { Deprecation::notice('2.0', 'Using Commenting::remove is deprecated. Please use the config API instead'); + $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 * @@ -60,87 +62,99 @@ class Commenting { */ public static function has_commenting($class) { Deprecation::notice('2.0', 'Using Commenting::has_commenting is deprecated. Please use the config API instead'); + return $class::has_extension('CommentsExtension'); } /** - * Sets a value for a class of a given config setting. Passing 'all' as the class - * sets it for everything - * - * @deprecated since version 2.0 - * - * @param string $class Class to set the value on. Passing 'all' will set it to all - * active mappings - * @param string $key setting to change - * @param mixed $value value of the setting - */ - 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'); - if($class === "all") $class = 'CommentsExtension'; - Config::inst()->update($class, 'comments', array($key => $value)); - } - - /** - * Returns a given config value for a commenting class - * - * @deprecated since version 2.0 - * - * @param string $class - * @param string $key config value to return - * - * @throws Exception - * @return mixed - */ - public static function get_config_value($class, $key) { - Deprecation::notice( - '2.0', - 'Using Commenting::get_config_value is deprecated. Please use $parent->getCommentsOption() or ' - . 'CommentingController::getOption() instead' - ); - - // Get settings - if(!$class) { - $class = 'CommentsExtension'; - } elseif(!$class::has_extension('CommentsExtension')) { - throw new InvalidArgumentException("$class does not have commenting enabled"); - } - return singleton($class)->getCommentsOption($key); - } - - /** - * Determines whether a config value on the commenting extension - * matches a given value. + * Sets a value for a class of a given config setting. Passing 'all' as the class sets it for + * everything. * * @deprecated since version 2.0 * * @param string $class * @param string $key - * @param string $value Expected value - * @return boolean + * @param mixed $value */ - public static function config_value_equals($class, $key, $value) { - $check = self::get_config_value($class, $key); - if($check && ($check == $value)) return true; + 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'); + + if($class === "all") { + $class = 'CommentsExtension'; + } + + Config::inst()->update($class, 'comments', array($key => $value)); } /** - * Return whether a user can post on a given commenting instance + * Returns a given config value for a commenting class. * * @deprecated since version 2.0 - * + * * @param string $class - * @return boolean true + * @param string $key config value to return + * + * @return mixed + * + * @throws InvalidArgumentException + */ + public static function get_config_value($class, $key) { + Deprecation::notice('2.0', 'Using Commenting::get_config_value is deprecated. Please use $parent->getCommentsOption() or CommentingController::getOption() instead'); + + if(!$class) { + $class = 'CommentsExtension'; + } elseif(!$class::has_extension('CommentsExtension')) { + throw new InvalidArgumentException( + sprintf('%s does not have commenting enabled', $class) + ); + } + + return singleton($class)->getCommentsOption($key); + } + + /** + * Determines whether a config value on the commenting extension matches a given value. + * + * @deprecated since version 2.0 + * + * @param string $class + * @param string $key + * @param string $value + * + * @return bool + */ + public static function config_value_equals($class, $key, $value) { + $check = self::get_config_value($class, $key); + + if($check && ($check == $value)) { + return true; + } + + return false; + } + + /** + * Return whether a user can post on a given commenting instance. + * + * @deprecated since version 2.0 + * + * @param string $class + * + * @return bool */ 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(); - // Check 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'); + return !$requireLogin || $member; } } diff --git a/code/admin/CommentAdmin.php b/code/admin/CommentAdmin.php index 36538ed..788811a 100644 --- a/code/admin/CommentAdmin.php +++ b/code/admin/CommentAdmin.php @@ -1,18 +1,29 @@ array( - 'name' => _t('CommentAdmin.ADMIN_PERMISSION', "Access to 'Comments' section"), - 'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access') - ) + 'CMS_ACCESS_CommentAdmin' => array( + 'name' => _t('CommentAdmin.ADMIN_PERMISSION', 'Access to \'Comments\' section'), + 'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'), + ), ); } /** - * @return Form + * {@inheritdoc} */ 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); if($record && !$record->canView()) { @@ -48,61 +63,82 @@ class CommentAdmin extends LeftAndMain implements PermissionProvider { $commentsConfig = CommentsGridFieldConfig::create(); - $newComments = Comment::get()->filter('Moderated', 0); + $newComments = Comment::get() + ->filter('Moderated', 0); - $newGrid = new CommentsGridField( + $newCommentsGrid = new CommentsGridField( 'NewComments', _t('CommentsAdmin.NewComments', 'New'), $newComments, $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', _t('CommentsAdmin.ApprovedComments', 'Approved'), $approvedComments, $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', _t('CommentsAdmin.SpamComments', 'Spam'), $spamComments, $commentsConfig ); - $newCount = '(' . count($newComments) . ')'; - $approvedCount = '(' . count($approvedComments) . ')'; - $spamCount = '(' . count($spamComments) . ')'; + $spamCommentsCountLabel = sprintf('(%s)', count($spamComments)); - $fields = new FieldList( - $root = new TabSet( - 'Root', - new Tab('NewComments', _t('CommentAdmin.NewComments', 'New') . ' ' . $newCount, - $newGrid + $tabSet = new TabSet( + 'Root', + new Tab( + 'NewComments', + sprintf( + '%s %s', + _t('CommentAdmin.NewComments', 'New'), + $newCommentsCountLabel ), - new Tab('ApprovedComments', _t('CommentAdmin.ApprovedComments', 'Approved') . ' ' . $approvedCount, - $approvedGrid + $newCommentsGrid + ), + new Tab( + 'ApprovedComments', + sprintf( + '%s %s', + _t('CommentAdmin.ApprovedComments', 'Approved'), + $approvedCommentsCountLabel ), - new Tab('SpamComments', _t('CommentAdmin.SpamComments', 'Spam') . ' ' . $spamCount, - $spamGrid - ) + $approvedCommentsGrid + ), + new Tab( + 'SpamComments', + sprintf( + '%s %s', + _t('CommentAdmin.SpamComments', 'Spam'), + $spamCommentsCountLabel + ), + $spamCommentsGrid ) ); - $root->setTemplate('CMSTabSet'); - - $actions = new FieldList(); + $tabSet->setTemplate('CMSTabSet'); $form = new Form( $this, 'EditForm', - $fields, - $actions + new FieldList($tabSet), + new FieldList() ); $form->addExtraClass('cms-edit-form'); diff --git a/code/admin/CommentsGridField.php b/code/admin/CommentsGridField.php index b434820..88b72a4 100644 --- a/code/admin/CommentsGridField.php +++ b/code/admin/CommentsGridField.php @@ -3,6 +3,8 @@ class CommentsGridField extends GridField { /** * {@inheritdoc} + * + * @param Comment $record */ protected function newRow($total, $index, $record, $attributes, $content) { if(!isset($attributes['class'])) { diff --git a/code/admin/CommentsGridFieldAction.php b/code/admin/CommentsGridFieldAction.php index bb1fc30..0626c78 100644 --- a/code/admin/CommentsGridFieldAction.php +++ b/code/admin/CommentsGridFieldAction.php @@ -14,7 +14,9 @@ class CommentsGridFieldAction implements GridField_ColumnProvider, GridField_Act * {@inheritdoc} */ 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) { if($columnName == 'Actions') { - return array('title' => ''); + return array( + 'title' => '', + ); } + + return array(); } /** * {@inheritdoc} */ public function getColumnsHandled($gridField) { - return array('Actions'); + return array( + 'Actions', + ); } /** * {@inheritdoc} + * + * @param Comment $record */ public function getColumnContent($gridField, $record, $columnName) { - if(!$record->canEdit()) return; + if(!$record->canEdit()) { + return ''; + } - $field = ""; + $field = ''; if(!$record->IsSpam || !$record->Moderated) { $field .= GridField_FormAction::create( @@ -47,7 +59,9 @@ class CommentsGridFieldAction implements GridField_ColumnProvider, GridField_Act 'CustomAction' . $record->ID, 'Spam', 'spam', - array('RecordID' => $record->ID) + array( + 'RecordID' => $record->ID, + ) )->Field(); } @@ -57,7 +71,9 @@ class CommentsGridFieldAction implements GridField_ColumnProvider, GridField_Act 'CustomAction' . $record->ID, 'Approve', 'approve', - array('RecordID' => $record->ID) + array( + 'RecordID' => $record->ID, + ) )->Field(); } @@ -68,7 +84,10 @@ class CommentsGridFieldAction implements GridField_ColumnProvider, GridField_Act * {@inheritdoc} */ 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) { if($actionName == 'spam') { - $comment = Comment::get()->byID($arguments["RecordID"]); + /** + * @var Comment $comment + */ + $comment = Comment::get() + ->byID($arguments["RecordID"]); + $comment->markSpam(); - // output a success message to the user - Controller::curr()->getResponse()->setStatusCode( - 200, - 'Comment marked as spam.' - ); + Controller::curr() + ->getResponse() + ->setStatusCode( + 200, + 'Comment marked as spam.' + ); } if($actionName == 'approve') { - $comment = Comment::get()->byID($arguments["RecordID"]); + /** + * @var Comment $comment + */ + $comment = Comment::get() + ->byID($arguments["RecordID"]); + $comment->markApproved(); - // output a success message to the user - Controller::curr()->getResponse()->setStatusCode( - 200, - 'Comment approved.' - ); + Controller::curr() + ->getResponse() + ->setStatusCode( + 200, + 'Comment approved.' + ); } } -} \ No newline at end of file +} diff --git a/code/admin/CommentsGridFieldBulkAction.php b/code/admin/CommentsGridFieldBulkAction.php index 138ff70..e78f4ff 100644 --- a/code/admin/CommentsGridFieldBulkAction.php +++ b/code/admin/CommentsGridFieldBulkAction.php @@ -8,57 +8,83 @@ class CommentsGridFieldBulkAction extends GridFieldBulkActionHandler { } /** - * A {@link GridFieldBulkActionHandler} for bulk marking comments as spam + * A {@link GridFieldBulkActionHandler} for bulk marking comments as spam. * * @package comments */ class CommentsGridFieldBulkAction_Handlers extends CommentsGridFieldBulkAction { - + /** + * @var array + */ private static $allowed_actions = array( 'spam', 'approve', ); + /** + * @var array + */ private static $url_handlers = array( 'spam' => 'spam', 'approve' => 'approve', ); + /** + * @param SS_HTTPRequest $request + * + * @return SS_HTTPResponse + */ public function spam(SS_HTTPRequest $request) { $ids = array(); foreach($this->getRecords() as $record) { - array_push($ids, $record->ID); + /** + * @var Comment $record + */ $record->markSpam(); + + array_push($ids, $record->ID); } - $response = new SS_HTTPResponse(Convert::raw2json(array( - 'done' => true, - 'records' => $ids - ))); + $response = new SS_HTTPResponse( + Convert::raw2json(array( + 'done' => true, + 'records' => $ids, + )) + ); $response->addHeader('Content-Type', 'text/json'); return $response; } - + /** + * @param SS_HTTPRequest $request + * + * @return SS_HTTPResponse + */ public function approve(SS_HTTPRequest $request) { $ids = array(); foreach($this->getRecords() as $record) { - array_push($ids, $record->ID); + /** + * @var Comment $record + */ $record->markApproved(); + + array_push($ids, $record->ID); } - $response = new SS_HTTPResponse(Convert::raw2json(array( - 'done' => true, - 'records' => $ids - ))); + $response = new SS_HTTPResponse( + Convert::raw2json(array( + 'done' => true, + 'records' => $ids, + )) + ); $response->addHeader('Content-Type', 'text/json'); return $response; } -} \ No newline at end of file +} diff --git a/code/admin/CommentsGridFieldConfig.php b/code/admin/CommentsGridFieldConfig.php index bc01dd4..6cadca9 100644 --- a/code/admin/CommentsGridFieldConfig.php +++ b/code/admin/CommentsGridFieldConfig.php @@ -1,17 +1,24 @@ addComponent(new GridFieldExportButton()); - $this->addComponent(new CommentsGridFieldAction()); - // Format column + /** + * @var GridFieldDataColumns $columns + */ $columns = $this->getComponentByType('GridFieldDataColumns'); + $columns->setFieldFormatting(array( - 'ParentTitle' => function($value, &$item) { + 'ParentTitle' => function ($value, &$item) { return sprintf( '%s', Convert::raw2att($item->Link()), @@ -20,24 +27,27 @@ class CommentsGridFieldConfig extends GridFieldConfig_RecordEditor { } )); - // Add bulk option $manager = new GridFieldBulkManager(); $manager->addBulkAction( - 'spam', 'Spam', 'CommentsGridFieldBulkAction_Handlers', + 'spam', + 'Spam', + 'CommentsGridFieldBulkAction_Handlers', array( 'isAjax' => true, 'icon' => 'cross', - 'isDestructive' => false + 'isDestructive' => false, ) ); $manager->addBulkAction( - 'approve', 'Approve', 'CommentsGridFieldBulkAction_Handlers', + 'approve', + 'Approve', + 'CommentsGridFieldBulkAction_Handlers', array( 'isAjax' => true, 'icon' => 'cross', - 'isDestructive' => false + 'isDestructive' => false, ) ); @@ -46,4 +56,4 @@ class CommentsGridFieldConfig extends GridFieldConfig_RecordEditor { $this->addComponent($manager); } -} \ No newline at end of file +} diff --git a/code/controllers/CommentingController.php b/code/controllers/CommentingController.php index ca1bf3a..fab454f 100644 --- a/code/controllers/CommentingController.php +++ b/code/controllers/CommentingController.php @@ -3,9 +3,10 @@ /** * @package comments */ - class CommentingController extends Controller { - + /** + * @var array + */ private static $allowed_actions = array( 'delete', 'spam', @@ -15,55 +16,59 @@ class CommentingController extends Controller { 'CommentsForm', 'reply', 'doPostComment', - 'doPreviewComment' + 'doPreviewComment', ); + /** + * @var array + */ private static $url_handlers = array( 'reply/$ParentCommentID//$ID/$OtherID' => 'reply', ); /** - * Fields required for this form + * Fields required for this form. + * + * @config * * @var array - * @config */ private static $required_fields = array( 'Name', 'Email', - 'Comment' + 'Comment', ); /** - * Base class this commenting form is for + * Base class this commenting form is for. * * @var string */ - private $baseClass = ""; + private $baseClass = ''; /** - * The record this commenting form is for - * - * @var DataObject + * The record this commenting form is for. + * + * @var null|DataObject */ private $ownerRecord = null; /** - * Parent controller record + * Parent controller record. * - * @var Controller + * @var null|Controller */ private $ownerController = null; /** - * Backup url to return to + * Backup url to return to. * - * @var string + * @var null|string */ protected $fallbackReturnURL = null; /** - * Set the base class to use + * Set the base class to use. * * @param string $class */ @@ -72,7 +77,7 @@ class CommentingController extends Controller { } /** - * Get the base class used + * Get the base class used. * * @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 */ @@ -90,16 +95,16 @@ class CommentingController extends Controller { } /** - * Get the record + * Get the record. * - * @return DataObject + * @return null|DataObject */ public function getOwnerRecord() { return $this->ownerRecord; } /** - * Set the parent controller + * Set the parent 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() { return $this->ownerController; } /** - * Get the commenting option for the current state + * Get the commenting option for the current state. * * @param string $key - * @return mixed Result if the setting is available, or null otherwise + * + * @return mixed */ public function getOption($key) { - // If possible use the current record if($record = $this->getOwnerRecord()) { return $record->getCommentsOption($key); } - - // Otherwise a singleton of that record + if($class = $this->getBaseClass()) { return singleton($class)->getCommentsOption($key); } - // Otherwise just use the default options return singleton('CommentsExtension')->getCommentsOption($key); } - + /** - * Workaround for generating the link to this controller - * - * @return string + * {@inheritdoc} */ 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 */ @@ -156,12 +157,11 @@ class CommentingController extends Controller { } /** - * Return an RSSFeed of comments for a given set of comments or all - * comments on the website. + * Return an RSSFeed of comments for a given set of comments or all comments on the website. * - * To maintain backwards compatibility with 2.4 this supports mapping - * of PageComment/rss?pageid= as well as the new RSS format for comments - * of CommentingController/rss/{classname}/{id} + * To maintain backwards compatibility with 2.4 this supports mapping of + * PageComment/rss?pageid= as well as the new RSS format for comments of + * CommentingController/rss/{classname}/{id} * * @param SS_HTTPRequest * @@ -172,26 +172,26 @@ class CommentingController extends Controller { $class = $request->param('ID'); $id = $request->param('OtherID'); - // Support old pageid param if(!$id && !$class && ($id = $request->getVar('pageid'))) { $class = 'SiteTree'; } - $comments = Comment::get()->filter(array( - 'Moderated' => 1, - 'IsSpam' => 0, - )); + $comments = Comment::get() + ->filter(array( + 'Moderated' => 1, + 'IsSpam' => 0, + )); - // Check if class filter if($class) { if(!is_subclass_of($class, 'DataObject') || !$class::has_extension('CommentsExtension')) { return $this->httpError(404); } + $this->setBaseClass($class); + $comments = $comments->filter('BaseClass', $class); $link = Controller::join_links($link, $class); - // Check if id filter if($id) { $comments = $comments->filter('ParentID', $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->setPageLength($this->getOption('comments_per_page')); + + $comments->setPageLength( + $this->getOption('comments_per_page') + ); return new RSSFeed( - $comments, - $link, - $title, - $link, - 'Title', 'EscapedComment', 'AuthorName' + $comments, + $link, + $title, + $link, + 'Title', + 'EscapedComment', + 'AuthorName' ); } @@ -218,35 +223,53 @@ class CommentingController extends Controller { */ public function delete() { $comment = $this->getComment(); - if(!$comment) return $this->httpError(404); + + if(!$comment) { + $this->httpError(404); + } + if(!$comment->canDelete()) { 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(); - return $this->request->isAjax() - ? true - : $this->redirectBack(); + if($this->request->isAjax()) { + return true; + } + + 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() { $comment = $this->getComment(); - if(!$comment) return $this->httpError(404); + + if(!$comment) { + $this->httpError(404); + } + if(!$comment->canEdit()) { 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(); - return $this->request->isAjax() - ? $comment->renderWith('CommentsInterface_singlecomment') - : $this->redirectBack(); + if($this->request->isAjax()) { + return true; + } + + return $this->redirectBack(); } /** @@ -254,17 +277,26 @@ class CommentingController extends Controller { */ public function ham() { $comment = $this->getComment(); - if(!$comment) return $this->httpError(404); + + if(!$comment) { + $this->httpError(404); + } + if(!$comment->canEdit()) { 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(); - return $this->request->isAjax() - ? $comment->renderWith('CommentsInterface_singlecomment') - : $this->redirectBack(); + if($this->request->isAjax()) { + return true; + } + + return $this->redirectBack(); } /** @@ -272,33 +304,42 @@ class CommentingController extends Controller { */ public function approve() { $comment = $this->getComment(); - if(!$comment) return $this->httpError(404); + + if(!$comment) { + $this->httpError(404); + } + if(!$comment->canEdit()) { 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(); - return $this->request->isAjax() - ? $comment->renderWith('CommentsInterface_singlecomment') - : $this->redirectBack(); + if($this->request->isAjax()) { + return true; + } + + return $this->redirectBack(); } - + /** - * Returns the comment referenced in the URL (by ID). Permission checking - * should be done in the callee. + * Returns the comment referenced in the URL. Permission checking should be done in the callee. * - * @return Comment|false + * @return bool|Comment */ 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); if($comment) { $this->fallbackReturnURL = $comment->Link(); + return $comment; } } @@ -307,216 +348,201 @@ class CommentingController extends Controller { } /** - * Create a reply form for a specified comment + * Create a reply form for a specified comment. * * @param Comment $comment + * + * @return Form */ public function ReplyForm($comment) { - // Enables multiple forms with different names to use the same handler $form = $this->CommentsForm(); - $form->setName('ReplyForm_'.$comment->ID); + $form->setName('ReplyForm_' . $comment->ID); $form->addExtraClass('reply-form'); - // Load parent into reply form $form->loadDataFrom(array( - 'ParentCommentID' => $comment->ID + 'ParentCommentID' => $comment->ID, )); - // Customise action - $form->setFormAction($this->Link('reply', $comment->ID)); - + $form->setFormAction( + $this->Link('reply', $comment->ID) + ); + $this->extend('updateReplyForm', $form); + return $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 + * + * @return null|Form */ public function reply(SS_HTTPRequest $request) { - // Extract parent comment from reply and build this way if($parentID = $request->param('ParentCommentID')) { + /** + * @var null|Comment $comment + */ $comment = DataObject::get_by_id('Comment', $parentID, true); + if($comment) { return $this->ReplyForm($comment); } } - return $this->httpError(404); + + $this->httpError(404); + + return null; } /** - * Post a comment form + * Post a comment form. * * @return Form */ public function CommentsForm() { $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( $dataFields = new CompositeField( - // Name - TextField::create("Name", _t('CommentInterface.YOURNAME', 'Your name')) - ->setCustomValidationMessage($nameRequired) - ->setAttribute('data-msg-required', $nameRequired), - - // 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) + $this->getNameField(), + $this->getEmailField(), + $this->getURLField(), + $this->getCommentField() ), - HiddenField::create("ParentID"), - HiddenField::create("ReturnURL"), - HiddenField::create("ParentCommentID"), - HiddenField::create("BaseClass") + HiddenField::create('ParentID'), + HiddenField::create('ReturnURL'), + HiddenField::create('ParentCommentID'), + HiddenField::create('BaseClass') ); - // Preview formatted comment. Makes most sense when shortcodes or - // limited HTML is allowed. Populated by JS/Ajax. if($usePreview) { $fields->insertAfter( - ReadonlyField::create('PreviewComment', _t('CommentInterface.PREVIEWLABEL', 'Preview')) - ->setAttribute('style', 'display: none'), // enable through JS + $this->getPreviewCommentField(), 'Comment' ); } - + $dataFields->addExtraClass('data-fields'); - // save actions $actions = new FieldList( - new FormAction("doPostComment", _t('CommentInterface.POST', 'Post')) + new FormAction( + 'doPostComment', + _t('CommentInterface.POST', 'Post') + ) ); + if($usePreview) { $actions->push( - FormAction::create('doPreviewComment', _t('CommentInterface.PREVIEW', 'Preview')) - ->addExtraClass('action-minor') - ->setAttribute('style', 'display: none') // enable through JS + $this->getPreviewCommentAction() ); } - // required fields for server side $required = new RequiredFields($this->config()->required_fields); - // create the comment form - $form = new Form($this, 'CommentsForm', $fields, $actions, $required); + $form = new Form( + $this, + 'CommentsForm', + $fields, + $actions, + $required + ); - // if the record exists load the extra required data if($record = $this->getOwnerRecord()) { - - // Load member data $member = Member::currentUser(); if(($record->CommentsRequireLogin || $record->PostingRequiredPermission) && $member) { $fields = $form->Fields(); $fields->removeByName('Name'); $fields->removeByName('Email'); - $fields->insertBefore(new ReadonlyField("NameView", _t('CommentInterface.YOURNAME', 'Your name'), $member->getName()), 'URL'); - $fields->push(new HiddenField("Name", "", $member->getName())); - $fields->push(new HiddenField("Email", "", $member->Email)); + $fields->insertBefore(new ReadonlyField('NameView', _t('CommentInterface.YOURNAME', 'Your name'), $member->getName()), 'URL'); + $fields->push(new HiddenField('Name', '', $member->getName())); + $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( - 'ParentID' => $record->ID, - 'ReturnURL' => $this->request->getURL(), - 'BaseClass' => $this->getBaseClass() + 'ParentID' => $record->ID, + 'ReturnURL' => $this->request->getURL(), + 'BaseClass' => $this->getBaseClass() )); } - - // Set it so the user gets redirected back down to the form upon form fail + $form->setRedirectToFormOnValidationError(true); - // load any data from the cookies if($data = Cookie::get('CommentsForm_UserData')) { - $data = Convert::json2array($data); - - $form->loadDataFrom(array( - "Name" => isset($data['Name']) ? $data['Name'] : '', - "URL" => isset($data['URL']) ? $data['URL'] : '', - "Email" => isset($data['Email']) ? $data['Email'] : '' - )); - // allow previous value to fill if comment not stored in cookie (i.e. validation error) + $data = Convert::json2array($data); + + $data += array( + 'Name' => '', + 'URL' => '', + 'Email' => '', + ); + + $form->loadDataFrom($data); + $prevComment = Cookie::get('CommentsForm_Comment'); - if($prevComment && $prevComment != ''){ - $form->loadDataFrom(array("Comment" => $prevComment)); + + if($prevComment && $prevComment != '') { + $form->loadDataFrom(array( + 'Comment' => $prevComment, + )); } } if(!empty($member)) { $form->loadDataFrom($member); } - - // hook to allow further extensions to alter the comments form + $this->extend('alterCommentForm', $form); return $form; } - + /** * Process which creates a {@link Comment} once a user submits a comment from this form. * - * @param array $data + * @param array $data * @param Form $form + * + * @return bool|SS_HTTPResponse */ public function doPostComment($data, $form) { - // Load class and parent from data if(isset($data['BaseClass'])) { $this->setBaseClass($data['BaseClass']); } + if(isset($data['ParentID']) && ($class = $this->getBaseClass())) { $this->setOwnerRecord($class::get()->byID($data['ParentID'])); } - if(!$this->getOwnerRecord()) return $this->httpError(404); - - // cache users data - 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); - - // If commenting can only be done by logged in users, make sure the user is logged in + + if(!$this->getOwnerRecord()) { + return $this->httpError(404); + } + + Cookie::set('CommentsForm_UserData', Convert::raw2json($data)); + Cookie::set('CommentsForm_Comment', $data['Comment']); + + $this->extend('onBeforePostComment', $form); + if(!$this->getOwnerRecord()->canPostComment()) { return Security::permissionFailure( $this, _t( 'CommentingController.PERMISSIONFAILURE', - "You're not able to post comments to this page. Please ensure you are logged in and have an " - . "appropriate permission level." + 'You\'re not able to post comments to this page. Please ensure you are logged in and have an appropriate permission level.' ) ); } 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) { case 'Required': $requireModeration = true; @@ -524,7 +550,6 @@ class CommentingController extends Controller { case 'NonMembersOnly': $requireModeration = empty($member); break; - case 'None': default: $requireModeration = false; break; @@ -536,27 +561,20 @@ class CommentingController extends Controller { $comment->AllowHtml = $this->getOption('html_allowed'); $comment->Moderated = !$requireModeration; - // Save into DB, or call pre-save hooks to give accurate preview - $usePreview = $this->getOption('use_preview'); - $isPreview = $usePreview && !empty($data['IsPreview']); - if($isPreview) { + if($this->getOption('use_preview') && !empty($data['IsPreview'])) { $comment->extend('onBeforeWrite'); } else { $comment->write(); - // extend hook to allow extensions. Also see onBeforePostComment $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); } - - // clear the users comment since it passed validation + Cookie::set('CommentsForm_Comment', false); - // Find parent link if(!empty($data['ReturnURL'])) { $url = $data['ReturnURL']; } elseif($parent = $comment->getParent()) { @@ -565,37 +583,44 @@ class CommentingController extends Controller { return $this->redirectBack(); } - // Given a redirect page exists, attempt to link to the correct anchor if(!$comment->Moderated) { - // Display the "awaiting moderation" text - $holder = $this->getOption('comments_holder_id'); - $hash = "{$holder}_PostCommentForm_error"; + $hash = sprintf( + '%s_PostCommentForm_error', + $this->getOption('comments_holder_id') + ); } elseif($comment->IsSpam) { - // Link to the form with the error message contained $hash = $form->FormName(); } else { - // Link to the moderated, non-spam comment $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) { $data['IsPreview'] = 1; 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() { - // Don't cache the redirect back ever HTTP::set_cache_age(0); $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->requestVar('BackURL')) { $url = $this->request->requestVar('BackURL'); @@ -606,15 +631,150 @@ class CommentingController extends Controller { } } - if(!$url) $url = $this->fallbackReturnURL; - if(!$url) $url = Director::baseURL(); + if(!$url) { + $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)) { return $this->redirect($url); } else { 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'); } } diff --git a/code/extensions/CommentsExtension.php b/code/extensions/CommentsExtension.php index 626a4a8..3ecfc05 100644 --- a/code/extensions/CommentsExtension.php +++ b/code/extensions/CommentsExtension.php @@ -3,6 +3,10 @@ /** * Extension to {@link DataObject} to enable tracking comments. * + * @property bool $ProvideComments + * @property string $ModerationRequired + * @property bool $CommentsRequireLogin + * * @package comments */ class CommentsExtension extends DataExtension { @@ -32,9 +36,9 @@ class CommentsExtension extends DataExtension { * nested_comments: Enable nested comments * nested_depth: Max depth of nested comments in levels (where root is 1 depth) 0 means no limit. * - * @var array - * * @config + * + * @var array */ private static $comments = array( 'enabled' => true, @@ -78,41 +82,50 @@ class CommentsExtension extends DataExtension { * CMS configurable options should default to the config values */ public function populateDefaults() { - // Set if comments should be enabled by default - $this->owner->ProvideComments = $this->owner->getCommentsOption('enabled') ? 1 : 0; + $this->owner->ProvideComments = 0; - // If moderation options should be configurable via the CMS then - if($this->owner->getCommentsOption('require_moderation')) { - $this->owner->ModerationRequired = 'Required'; - } elseif($this->owner->getCommentsOption('require_moderation_nonmembers')) { - $this->owner->ModerationRequired = 'NonMembersOnly'; - } else { - $this->owner->ModerationRequired = 'None'; + if($this->owner->getCommentsOption('enabled')) { + $this->owner->ProvideComments = 1; } - $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 - * append a Provide Comments checkbox to allow authors to trigger - * whether or not to display comments + * If this extension is applied to a {@link SiteTree} record then append a Provide Comments + * checkbox to allow authors to trigger whether or not to display comments. * * @todo Allow customization of other {@link Commenting} configuration * * @param 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')) { - $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')) { $options->push( 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')) { - $moderationField = new DropdownField('ModerationRequired', 'Comment Moderation', array( - 'None' => _t('CommentsExtension.MODERATIONREQUIRED_NONE', 'No moderation required'), - 'Required' => _t('CommentsExtension.MODERATIONREQUIRED_REQUIRED', 'Moderate all comments'), - 'NonMembersOnly' => _t( - 'CommentsExtension.MODERATIONREQUIRED_NONMEMBERSONLY', - 'Only moderate non-members' - ), - )); + $moderationField = new DropdownField( + 'ModerationRequired', + 'Comment Moderation', + array( + 'None' => _t( + '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()) { $fields->addFieldsToTab('Root.Settings', $moderationField); } 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 - * Required: All comments + * None: No moderation required + * Required: All comments * NonMembersOnly: Only anonymous users * * @return string @@ -160,26 +183,30 @@ class CommentsExtension extends DataExtension { public function getModerationRequired() { if($this->owner->getCommentsOption('require_moderation_cms')) { 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() { if($this->owner->getCommentsOption('require_login_cms')) { 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() { $order = $this->owner->getCommentsOption('order_comments_by'); - $comments = CommentList::create($this->ownerBaseClass) - ->forForeignID($this->owner->ID) - ->sort($order); + + $comments = CommentList::create($this->ownerBaseClass); + $comments->forForeignID($this->owner->ID); + $comments->sort($order); + $this->owner->extend('updateAllComments', $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 */ public function AllVisibleComments() { $list = $this->AllComments(); - // Filter spam comments for non-administrators if configured $showSpam = $this->owner->getCommentsOption('frontend_spam') && $this->owner->canModerateComments(); + if(!$showSpam) { $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()); - if(!$showUnmoderated) { + $showUnModerated = ($this->owner->ModerationRequired === 'None') || ($this->owner->getCommentsOption('frontend_moderation') && $this->owner->canModerateComments()); + + if(!$showUnModerated) { $list = $list->filter('Moderated', 1); } $this->owner->extend('updateAllVisibleComments', $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 */ public function Comments() { $list = $this->AllVisibleComments(); - // If nesting comments, only show root level if($this->owner->getCommentsOption('nested_comments')) { $list = $list->filter('ParentCommentID', 0); } $this->owner->extend('updateComments', $list); + return $list; } /** - * Returns a paged list of the root level comments, with spam and unmoderated items excluded, - * for use in the frontend + * Returns a paged list of the root level comments, with spam and un-moderated items excluded. * * @return PaginatedList */ public function PagedComments() { $list = $this->Comments(); - // Add pagination $list = new PaginatedList($list, Controller::curr()->getRequest()); $list->setPaginationGetVar('commentsstart' . $this->owner->ID); $list->setPageLength($this->owner->getCommentsOption('comments_per_page')); $this->owner->extend('updatePagedComments', $list); + return $list; } /** * 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 * - * @return boolean + * @return bool */ public function getCommentsConfigured() { 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() { - // Don't display comments form for pseudo-pages (such as the login form) - if(!$this->owner->exists()) return false; - - // Determine which flag should be used to determine if this is enabled + if(!$this->owner->exists()) { + return false; + } + if($this->owner->getCommentsOption('enabled_cms')) { 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 */ @@ -301,71 +332,86 @@ class CommentsExtension extends DataExtension { */ public function getPostingRequiresPermission() { Deprecation::notice('2.0', 'Use getPostingRequiredPermission instead'); + 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() { return $this->owner->getCommentsOption('required_permission'); } + /** + * @return bool + */ public function canPost() { Deprecation::notice('2.0', 'Use canPostComment instead'); + 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) { - // Deny if not enabled for this object - if(!$this->owner->CommentsEnabled) return false; + if(!$this->owner->CommentsEnabled) { + return false; + } - // Check if member is required $requireLogin = $this->owner->CommentsRequireLogin; - if(!$requireLogin) return true; - // Check member is logged in - $member = $member ?: Member::currentUser(); - if(!$member) return false; + if(!$requireLogin) { + return true; + } + + if(!$member) { + $member = Member::currentUser(); + } + + if(!$member) { + return false; + } - // If member required check permissions $requiredPermission = $this->owner->PostingRequiredPermission; - if($requiredPermission && !Permission::checkMember($member, $requiredPermission)) return false; + + if($requiredPermission && !Permission::checkMember($member, $requiredPermission)) { + return false; + } 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) { - // Deny if not enabled for this object - if(!$this->owner->CommentsEnabled) return false; + if(!$this->owner->CommentsEnabled) { + return false; + } - // Fallback to can-edit return $this->owner->canEdit($member); } public function getRssLink() { Deprecation::notice('2.0', 'Use getCommentRSSLink instead'); + return $this->getCommentRSSLink(); } /** - * Gets the RSS link to all comments + * Gets the RSS link to all comments. * * @return string */ @@ -373,42 +419,44 @@ class CommentsExtension extends DataExtension { return Controller::join_links(Director::baseURL(), 'CommentingController/rss'); } + /** + * @return string + */ public function getRssLinkPage() { Deprecation::notice('2.0', 'Use getCommentRSSLinkPage instead'); + 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 */ public function getCommentRSSLinkPage() { 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 - * of the comments display. + * Comments interface for the front end. Includes the CommentAddForm and the composition of the + * comments display. * - * To customize the html see templates/CommentInterface.ss or extend this function with - * your own extension. - * - * @todo Cleanup the passing of all this configuration based functionality - * - * @see docs/en/Extending + * To customize the html see templates/CommentInterface.ss or extend this function with your + * own extension. */ public function CommentsForm() { - // Check if enabled $enabled = $this->getCommentsEnabled(); + if($enabled && $this->owner->getCommentsOption('include_js')) { Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-validate/lib/jquery.form.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(); @@ -417,12 +465,15 @@ class CommentsExtension extends DataExtension { $controller->setOwnerController(Controller::curr()); $moderatedSubmitted = Session::get('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 ->owner ->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 */ public function attachedToSiteTree() { $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() { - // This method is very commonly used, don't throw a warning just yet Deprecation::notice('1.0', '$PageComments is deprecated. Please use $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 * - * @return mixed Result if the setting is available, or null otherwise + * @return mixed */ public function getCommentsOption($key) { - $settings = $this->owner // In case singleton is called on the extension directly - ? $this->owner->config()->comments - : Config::inst()->get(__CLASS__, 'comments'); - $value = null; - if(isset($settings[$key])) $value = $settings[$key]; + if($this->owner) { + $settings = $this->owner->config()->comments; + } else { + $settings = Config::inst()->get(__CLASS__, 'comments'); + } + + $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; } /** - * Add moderation functions to the current fieldlist + * Add moderation functions to the current field list. * * @param FieldList $fields */ @@ -483,65 +544,91 @@ class CommentsExtension extends DataExtension { $commentsConfig = CommentsGridFieldConfig::create(); - $newComments = $this->owner->AllComments()->filter('Moderated', 0); + $newComments = Comment::get() + ->filter('Moderated', 0); - $newGrid = new CommentsGridField( + $newCommentsGrid = new CommentsGridField( 'NewComments', _t('CommentsAdmin.NewComments', 'New'), $newComments, $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', - _t('CommentsAdmin.Comments', 'Approved'), + _t('CommentsAdmin.ApprovedComments', 'Approved'), $approvedComments, $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', _t('CommentsAdmin.SpamComments', 'Spam'), $spamComments, $commentsConfig ); - $newCount = '(' . count($newComments) . ')'; - $approvedCount = '(' . count($approvedComments) . ')'; - $spamCount = '(' . count($spamComments) . ')'; + $spamCommentsCountLabel = sprintf('(%s)', count($spamComments)); if($fields->hasTabSet()) { $tabs = new TabSet( 'Comments', - new Tab('CommentsNewCommentsTab', _t('CommentAdmin.NewComments', 'New') . ' ' . $newCount, - $newGrid + new Tab( + 'NewComments', + sprintf( + '%s %s', + _t('CommentAdmin.NewComments', 'New'), + $newCommentsCountLabel + ), + $newCommentsGrid ), - new Tab('CommentsCommentsTab', _t('CommentAdmin.Comments', 'Approved') . ' ' . $approvedCount, - $approvedGrid + new Tab( + 'ApprovedComments', + sprintf( + '%s %s', + _t('CommentAdmin.ApprovedComments', 'Approved'), + $approvedCommentsCountLabel + ), + $approvedCommentsGrid ), - new Tab('CommentsSpamCommentsTab', _t('CommentAdmin.SpamComments', 'Spam') . ' ' . $spamCount, - $spamGrid + new Tab( + 'SpamComments', + sprintf( + '%s %s', + _t('CommentAdmin.SpamComments', 'Spam'), + $spamCommentsCountLabel + ), + $spamCommentsGrid ) ); $fields->addFieldToTab('Root', $tabs); } else { - $fields->push($newGrid); - $fields->push($approvedGrid); - $fields->push($spamGrid); + $fields->push($newCommentsGrid); + $fields->push($approvedCommentsGrid); + $fields->push($spamCommentsGrid); } } + /** + * {@inheritdoc} + */ public function updateCMSFields(FieldList $fields) { - // Disable moderation if not permitted if($this->owner->canModerateComments()) { $this->updateModerationFields($fields); } - // If this isn't a page we should merge the settings into the CMS fields if(!$this->attachedToSiteTree()) { $this->updateSettingsFields($fields); } diff --git a/code/model/Comment.php b/code/model/Comment.php index a6d8da1..f4ca9b9 100755 --- a/code/model/Comment.php +++ b/code/model/Comment.php @@ -3,25 +3,25 @@ /** * Represents a single comment object. * - * @property string $Name - * @property string $Comment - * @property string $Email - * @property string $URL - * @property string $BaseClass - * @property boolean $Moderated - * @property boolean $IsSpam True if the comment is known as spam - * @property integer $ParentID ID of the parent page / dataobject - * @property boolean $AllowHtml If true, treat $Comment as HTML instead of plain text - * @property string $SecretToken Secret admin token required to provide moderation links between sessions - * @property integer $Depth Depth of this comment in the nested chain + * @property string $Name + * @property string $Comment + * @property string $Email + * @property string $URL + * @property string $BaseClass + * @property bool $Moderated + * @property bool $IsSpam + * @property int $ParentID + * @property bool $AllowHtml + * @property string $SecretToken + * @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 */ class Comment extends DataObject { - /** * @var array */ @@ -39,22 +39,37 @@ class Comment extends DataObject { 'Depth' => 'Int', ); + /** + * @var array + */ private static $has_one = array( - "Author" => "Member", - "ParentComment" => "Comment", + 'Author' => 'Member', + 'ParentComment' => 'Comment', ); + /** + * @var array + */ private static $has_many = array( - "ChildComments" => "Comment" + 'ChildComments' => 'Comment' ); + /** + * @var string + */ private static $default_sort = '"Created" DESC'; + /** + * @var array + */ private static $defaults = array( 'Moderated' => 0, 'IsSpam' => 0, ); + /** + * @var array + */ private static $casting = array( 'Title' => 'Varchar', 'ParentTitle' => 'Varchar', @@ -68,6 +83,9 @@ class Comment extends DataObject { 'Permalink' => 'Varchar', ); + /** + * @var array + */ private static $searchable_fields = array( 'Name', 'Email', @@ -76,6 +94,9 @@ class Comment extends DataObject { 'BaseClass', ); + /** + * @var array + */ private static $summary_fields = array( 'Name' => 'Submitted By', 'Email' => 'Email', @@ -85,26 +106,32 @@ class Comment extends DataObject { 'IsSpam' => 'Is Spam', ); + /** + * @var array + */ private static $field_labels = array( 'Author' => 'Author Member', ); + /** + * {@inheritdoc + */ public function onBeforeWrite() { parent::onBeforeWrite(); - // Sanitize HTML, because its expected to be passed to the template unescaped later if($this->AllowHtml) { $this->Comment = $this->purifyHtml($this->Comment); } - // Check comment depth $this->updateDepth(); } + /** + * {@inheritdoc + */ public function onBeforeDelete() { parent::onBeforeDelete(); - // Delete all children foreach($this->ChildComments() as $comment) { $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() { parent::requireDefaultRecords(); if(DB::getConn()->hasTable('PageComment')) { - $comments = DB::query('SELECT * FROM "PageComment"'); + $comments = DB::query('SELECT * FROM PageComment'); if($comments) { while($pageComment = $comments->nextRecord()) { - // create a new comment from the older page comment $comment = new Comment(); $comment->update($pageComment); - // set the variables which have changed $comment->BaseClass = 'SiteTree'; - $comment->URL = (isset($pageComment['CommenterURL'])) ? $pageComment['CommenterURL'] : ''; - if((int) $pageComment['NeedsModeration'] == 0) $comment->Moderated = true; + $comment->URL = ''; + + if(isset($pageComment['CommenterURL'])) { + $comment->URL = $pageComment['CommenterURL']; + } + + if($pageComment['NeedsModeration'] == false) { + $comment->Moderated = true; + } $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 * - * @return string link to this comment. + * @return string */ public function Link($action = '') { if($parent = $this->getParent()) { return $parent->Link($action) . '#' . $this->Permalink(); } + + return ''; } /** - * Returns the permalink for this {@link Comment}. Inserted into - * the ID tag of the comment + * Returns the permalink for this {@link Comment}. Inserted into the ID tag of the comment. * * @return string */ public function Permalink() { - $prefix = $this->getOption('comment_permalink_prefix'); - return $prefix . $this->ID; + return $this->getOption('comment_permalink_prefix') . $this->ID; } /** - * Translate the form field labels for the CMS administration + * {@inheritdoc} * - * @param boolean $includerelations - * - * @return array + * @param bool $includeRelations */ - public function fieldLabels($includerelations = true) { - $labels = parent::fieldLabels($includerelations); + public function fieldLabels($includeRelations = true) { + $labels = parent::fieldLabels($includeRelations); $labels['Name'] = _t('Comment.NAME', 'Author Name'); $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 * - * @return mixed Result if the setting is available, or null otherwise + * @return mixed */ public function getOption($key) { - // If possible use the current record $record = $this->getParent(); + if(!$record && $this->BaseClass) { - // Otherwise a singleton of that record $record = singleton($this->BaseClass); } elseif(!$record) { - // Otherwise just use the default options $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() { - return $this->BaseClass && $this->ParentID - ? DataObject::get_by_id($this->BaseClass, $this->ParentID, true) - : null; + if($this->BaseClass && $this->ParentID) { + return DataObject::get_by_id($this->BaseClass, $this->ParentID, true); + } + + 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 */ public function getParentTitle() { - if($parent = $this->getParent()) { - return $parent->Title ?: ($parent->ClassName . ' #' . $parent->ID); + $parent = $this->getParent(); + + 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 */ @@ -245,16 +280,19 @@ class Comment extends DataObject { return $this->BaseClass; } + /** + * {@inheritdoc} + */ public function castingHelper($field) { - // Safely escape the comment if($field === 'EscapedComment') { return $this->AllowHtml ? 'HTMLText' : 'Text'; } + return parent::castingHelper($field); } /** - * Content to be safely escaped on the frontend + * @todo escape this comment? (DOH!) * * @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() { return !$this->exists(); @@ -274,7 +312,7 @@ class Comment extends DataObject { /** * @todo needs to compare to the new {@link Commenting} configuration API * - * @param Member $member + * @param null|Member $member * * @return bool */ @@ -283,17 +321,18 @@ class Comment extends DataObject { } /** - * Checks for association with a page, and {@link SiteTree->ProvidePermission} - * flag being set to true. + * Checks for association with a page, and {@link SiteTree->ProvidePermission} flag being set + * to true. * - * @param Member $member + * @param null|int|Member $member * - * @return Boolean + * @return bool */ public function canView($member = null) { $member = $this->getMember($member); $extended = $this->extendedCan('canView', $member); + if($extended !== null) { return $extended; } @@ -303,9 +342,7 @@ class Comment extends DataObject { } if($parent = $this->getParent()) { - return $parent->canView($member) - && $parent->has_extension('CommentsExtension') - && $parent->CommentsEnabled; + return $parent->canView($member) && $parent->has_extension('CommentsExtension') && $parent->CommentsEnabled; } return false; @@ -316,7 +353,7 @@ class Comment extends DataObject { * * @param null|int|Member $member * - * @return Boolean + * @return bool */ public function canEdit($member = null) { $member = $this->getMember($member); @@ -326,6 +363,7 @@ class Comment extends DataObject { } $extended = $this->extendedCan('canEdit', $member); + if($extended !== null) { return $extended; } @@ -346,7 +384,7 @@ class Comment extends DataObject { * * @param null|int|Member $member * - * @return Boolean + * @return bool */ public function canDelete($member = null) { $member = $this->getMember($member); @@ -356,6 +394,7 @@ class Comment extends DataObject { } $extended = $this->extendedCan('canDelete', $member); + if($extended !== null) { return $extended; } @@ -366,8 +405,9 @@ class Comment extends DataObject { /** * Resolves Member object. * - * @param Member|int|null $member - * @return Member|null + * @param null|int|Member $member + * + * @return null|Member */ protected function getMember($member = null) { 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 */ @@ -392,19 +432,26 @@ class Comment extends DataObject { } else if($author = $this->Author()) { 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 Member $member The member authorised to invoke this action + * @param string $action + * @param null|Member $member * * @return string */ protected function actionLink($action, $member = null) { - if(!$member) $member = Member::currentUser(); - if(!$member) return false; + if(!$member) { + $member = Member::currentUser(); + } + + if(!$member) { + return false; + } $url = Controller::join_links( Director::baseURL(), @@ -413,92 +460,103 @@ class Comment extends DataObject { $this->ID ); - // Limit access for this user $token = $this->getSecurityToken(); + 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) { if($this->canDelete($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) { if($this->canEdit($member) && !$this->IsSpam) { 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) { if($this->canEdit($member) && $this->IsSpam) { 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) { if($this->canEdit($member) && !$this->Moderated) { return $this->actionLink('approve', $member); } + + return null; } /** - * Mark this comment as spam + * Mark this comment as spam. */ public function markSpam() { $this->IsSpam = true; $this->Moderated = true; $this->write(); + $this->extend('afterMarkSpam'); } /** - * Mark this comment as approved + * Mark this comment as approved. */ public function markApproved() { $this->IsSpam = false; $this->Moderated = true; $this->write(); + $this->extend('afterMarkApproved'); } /** - * Mark this comment as unapproved + * Mark this comment as unapproved. */ public function markUnapproved() { $this->Moderated = false; $this->write(); + $this->extend('afterMarkUnapproved'); } - + /** * @return string */ @@ -507,90 +565,120 @@ class Comment extends DataObject { return 'spam'; } else if(!$this->Moderated) { return 'unmoderated'; - } else { - return 'notspam'; } + + return 'notspam'; } /** * @return string */ 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()) { - if($parent->Title) { - $title .= sprintf(' %s %s', _t('Comment.ON', 'on'), $parent->Title); - } + $parent = $this->getParent(); + + if($parent && $parent->Title) { + $title .= sprintf( + ' %s %s', + _t('Comment.ON', 'on'), + $parent->Title + ); } return $title; } - /* - * Modify the default fields shown to the user + /** + * Modify the default fields shown to the user. */ 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( - $this - ->obj('Created') - ->scaffoldFormField($this->fieldLabel('Created')) - ->performReadonlyTransformation(), - TextField::create('Name', $this->fieldLabel('AuthorName')), - $commentField::create('Comment', $this->fieldLabel('Comment')), - 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' - )) + $createdField, + $nameField, + $commentField, + $emailField, + $urlField, + $fieldGroup ); - // Show member name if given - if(($author = $this->Author()) && $author->exists()) { + $author = $this->Author(); + + if($author && $author->exists()) { + $authorMemberField = TextField::create('AuthorMember', $this->fieldLabel('Author'), $author->Title); + $authorMemberField->performReadonlyTransformation(); + $fields->insertAfter( - TextField::create('AuthorMember', $this->fieldLabel('Author'), $author->Title) - ->performReadonlyTransformation(), + $authorMemberField, 'Name' ); } - // Show parent comment if given - if(($parent = $this->ParentComment()) && $parent->exists()) { - $fields->push(new HeaderField( - 'ParentComment_Title', - _t('Comment.ParentComment_Title', 'This comment is a reply to the below') - )); - // Created date + $parent = $this->ParentComment(); + + if($parent && $parent->exists()) { $fields->push( - $parent - ->obj('Created') + new HeaderField( + 'ParentComment_Title', + _t('Comment.ParentComment_Title', 'This comment is a reply to the below') + ) + ); + + $fields->push( + $parent->obj('Created') ->scaffoldFormField($parent->fieldLabel('Created')) ->setName('ParentComment_Created') ->setValue($parent->Created) ->performReadonlyTransformation() ); - // Name (could be member or string value) $fields->push( - $parent - ->obj('AuthorName') + $parent->obj('AuthorName') ->scaffoldFormField($parent->fieldLabel('AuthorName')) ->setName('ParentComment_AuthorName') ->setValue($parent->getAuthorName()) ->performReadonlyTransformation() ); - // Comment body $fields->push( - $parent - ->obj('EscapedComment') + $parent->obj('EscapedComment') ->scaffoldFormField($parent->fieldLabel('Comment')) ->setName('ParentComment_EscapedComment') ->setValue($parent->Comment) @@ -599,157 +687,165 @@ class Comment extends DataObject { } $this->extend('updateCMSFields', $fields); + return $fields; } /** - * @param String $dirtyHtml + * @param string $dirtyHtml * - * @return String + * @return string */ public function purifyHtml($dirtyHtml) { - $purifier = $this->getHtmlPurifierService(); - return $purifier->purify($dirtyHtml); + return $this->getHtmlPurifierService() + ->purify($dirtyHtml); } /** - * @return HTMLPurifier (or anything with a "purify()" method) + * @return HTMLPurifier */ public function getHtmlPurifierService() { $config = HTMLPurifier_Config::createDefault(); + $config->set('HTML.AllowedElements', $this->getOption('html_allowed_elements')); $config->set('AutoFormat.AutoParagraph', true); $config->set('AutoFormat.Linkify', true); $config->set('URI.DisableExternalResources', true); $config->set('Cache.SerializerPath', getTempFolder()); + return new HTMLPurifier($config); } /** - * Calculate the Gravatar link from the email address + * Calculate the Gravatar link from the email address. * * @return string */ public function Gravatar() { - $gravatar = ''; - $use_gravatar = $this->getOption('use_gravatar'); - if($use_gravatar) { - $gravatar = 'http://www.gravatar.com/avatar/' . md5(strtolower(trim($this->Email))); - $gravatarsize = $this->getOption('gravatar_size'); - $gravatardefault = $this->getOption('gravatar_default'); - $gravatarrating = $this->getOption('gravatar_rating'); - $gravatar .= '?s=' . $gravatarsize . '&d=' . $gravatardefault . '&r=' . $gravatarrating; + if($this->getOption('use_gravatar')) { + return sprintf( + 'http://www.gravatar.com/avatar/%s?s=%s&d=%s&r=%s', + md5(strtolower(trim($this->Email))), + $this->getOption('gravatar_size'), + $this->getOption('gravatar_default'), + $this->getOption('gravatar_rating') + ); } - 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() { - // Check reply option if(!$this->getOption('nested_comments')) { return false; } - // Check if depth is limited $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 */ public function AllReplies() { - // No replies if disabled if(!$this->getRepliesEnabled()) { return new ArrayList(); } - - // Get all non-spam comments - $order = $this->getOption('order_replies_by') - ?: $this->getOption('order_comments_by'); + + $order = $this->getOption('order_replies_by'); + + if(!$order) { + $order = $this->getOption('order_comments_by'); + } + $list = $this ->ChildComments() ->sort($order); $this->extend('updateAllReplies', $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 */ public function Replies() { - // No replies if disabled if(!$this->getRepliesEnabled()) { return new ArrayList(); } + $list = $this->AllReplies(); - - // Filter spam comments for non-administrators if configured + $parent = $this->getParent(); - $showSpam = $this->getOption('frontend_spam') && $parent && $parent->canModerateComments(); + + $showSpam = $parent && $parent->canModerateComments() && $this->getOption('frontend_spam'); + if(!$showSpam) { $list = $list->filter('IsSpam', 0); } - // Filter un-moderated comments for non-administrators if moderation is enabled - $showUnmoderated = $parent && ( - ($parent->ModerationRequired === 'None') - || ($this->getOption('frontend_moderation') && $parent->canModerateComments()) - ); - if (!$showUnmoderated) { - $list = $list->filter('Moderated', 1); + $noModerationRequired = $parent && $parent->ModerationRequired === 'None'; + + $showUnModerated = $noModerationRequired || $showSpam; + + if(!$showUnModerated) { + $list = $list->filter('Moderated', 1); } $this->extend('updateReplies', $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 */ public function PagedReplies() { $list = $this->Replies(); - // Add pagination $list = new PaginatedList($list, Controller::curr()->getRequest()); - $list->setPaginationGetVar('repliesstart'.$this->ID); + + $list->setPaginationGetVar('repliesstart' . $this->ID); $list->setPageLength($this->getOption('comments_per_page')); $this->extend('updatePagedReplies', $list); + return $list; } /** - * Generate a reply form for this comment + * Generate a reply form for this comment. * * @return Form */ public function ReplyForm() { - // Ensure replies are enabled if(!$this->getRepliesEnabled()) { return null; } - // Check parent is available $parent = $this->getParent(); + if(!$parent || !$parent->exists()) { return null; } - // Build reply controller $controller = CommentingController::create(); + $controller->setOwnerRecord($parent); $controller->setBaseClass($parent->ClassName); $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() { $parent = $this->ParentComment(); + if($parent && $parent->exists()) { $parent->updateDepth(); + $this->Depth = $parent->Depth + 1; } else { $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 { - + /** + * @var null|string + */ 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) { if(!$comment->SecretToken) { $comment->SecretToken = $this->generate(); $comment->write(); } + $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 * @@ -805,30 +905,34 @@ class Comment_SecurityToken { * Get the member-specific salt. * * 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. * - * @param string $salt Single use salt - * @param Member $member Member object + * @param string $salt + * @param Member $member * - * @return string Generated salt specific to this member + * @return string */ protected function memberSalt($salt, $member) { - // Fallback to salting with ID in case the member has not one set - return $salt . ($member->Salt ?: $member->ID); + $pepper = $member->Salt; + + if(!$pepper) { + $pepper = $member->ID; + } + + return $salt . $pepper; } /** - * @param string $url Comment action URL - * @param Member $member Member to restrict access to this action to + * @param string $url + * @param Member $member * * @return string */ public function addToUrl($url, $member) { - $salt = $this->generate(15); // New random salt; Will be passed into url - // Generate salt specific to this member - $memberSalt = $this->memberSalt($salt, $member); - $token = $this->getToken($memberSalt); + $salt = $this->generate(15); + $token = $this->getToken($this->memberSalt($salt, $member)); + return Controller::join_links( $url, sprintf( @@ -842,32 +946,37 @@ class Comment_SecurityToken { /** * @param SS_HTTPRequest $request * - * @return boolean + * @return bool */ public function checkRequest($request) { $member = Member::currentUser(); - if(!$member) return false; + + if(!$member) { + return false; + } $salt = $request->getVar('s'); - $memberSalt = $this->memberSalt($salt, $member); - $token = $this->getToken($memberSalt); + $token = $this->getToken($this->memberSalt($salt, $member)); - // Ensure tokens match return $token === $request->getVar('t'); } - /** - * Generates new random key + * Generates new random key. * - * @param integer $length + * @param null|int $length * * @return string */ protected function generate($length = null) { $generator = new RandomGenerator(); + $result = $generator->randomToken('sha256'); - if($length !== null) return substr($result, 0, $length); + + if($length !== null) { + return substr($result, 0, $length); + } + return $result; } } diff --git a/code/model/CommentList.php b/code/model/CommentList.php index 3296b13..8834943 100644 --- a/code/model/CommentList.php +++ b/code/model/CommentList.php @@ -1,16 +1,13 @@ dataQuery->getQueryParam('Foreign.Class'); } + /** + * {@inheritdoc} + */ public function __construct($parentClassName) { parent::__construct('Comment', 'ParentID'); - - // Ensure underlying DataQuery globally references the class filter $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)); + $this->dataQuery->where(sprintf( - "\"BaseClass\" IN ('%s')", implode("', '", $classNames) + 'BaseClass IN (\'%s\')', + implode('\', \'', $classNames) )); } /** * Adds the item to this relation. - * - * @param Comment $item The comment to be added + * + * @param Comment $comment */ - public function add($item) { - // Check item given - if(is_numeric($item)) { - $item = Comment::get()->byID($item); - } - if(!($item instanceof Comment)) { - throw new InvalidArgumentException("CommentList::add() expecting a Comment object, or ID value"); - } - - // Validate foreignID - $foreignID = $this->getForeignID(); - if(!$foreignID || is_array($foreignID)) { - throw new InvalidArgumentException("CommentList::add() can't be called until a single foreign ID is set"); + public function add($comment) { + if(is_numeric($comment)) { + $comment = Comment::get()->byID($comment); } - $item->ParentID = $foreignID; - $item->BaseClass = $this->getForeignClass(); - $item->write(); - } - /** - * 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 - */ - public function remove($item) { - // Check item given - if(is_numeric($item)) { - $item = Comment::get()->byID($item); + if(!$comment instanceof Comment) { + throw new InvalidArgumentException( + 'CommentList::add() expecting a Comment object, or ID value' + ); } - if(!($item instanceof Comment)) { - throw new InvalidArgumentException("CommentList::remove() expecting a Comment object, or ID", - E_USER_ERROR); - } - - // Don't remove item with unrelated class key - $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 + $foreignID = $this->getForeignID(); - if( empty($foreignID) - || (is_array($foreignID) && in_array($item->ParentID, $foreignID)) - || $foreignID == $item->ParentID - ) { - $item->ParentID = null; - $item->BaseClass = null; - $item->write(); + + if(!$foreignID || is_array($foreignID)) { + throw new InvalidArgumentException( + 'CommentList::add() can\'t be called until a single foreign ID is set' + ); + } + + $comment->ParentID = $foreignID; + $comment->BaseClass = $this->getForeignClass(); + $comment->write(); + } + + /** + * Remove a Comment from this relation by clearing the foreign key. Does not actually delete + * the comment. + * + * @param Comment $comment + */ + public function remove($comment) { + if(is_numeric($comment)) { + $comment = Comment::get()->byID($comment); + } + + if(!$comment instanceof Comment) { + throw new InvalidArgumentException( + 'CommentList::remove() expecting a Comment object, or ID', + E_USER_ERROR + ); + } + + $foreignClass = $this->getForeignClass(); + + $subclasses = ClassInfo::subclassesFor($foreignClass); + + if(!in_array($comment->BaseClass, $subclasses)) { + return; + } + + $foreignID = $this->getForeignID(); + + if(empty($foreignID) || $foreignID == $comment->ParentID || (is_array($foreignID) && in_array($comment->ParentID, $foreignID))) { + $comment->ParentID = null; + $comment->BaseClass = null; + $comment->write(); } } } diff --git a/tests/CommentingControllerTest.php b/tests/CommentingControllerTest.php index 94a09e7..0f83bc5 100644 --- a/tests/CommentingControllerTest.php +++ b/tests/CommentingControllerTest.php @@ -1,114 +1,142 @@ securityEnabled) { SecurityToken::enable(); } else { SecurityToken::disable(); } + parent::tearDown(); } + /** + * {@inheritdoc} + */ public function setUp() { parent::setUp(); + $this->securityEnabled = SecurityToken::is_enabled(); } public function testRSS() { - $item = $this->objFromFixture('CommentableItem', 'first'); + $item = $this->objFromFixture('HasComments', 'first'); - // comments sitewide $response = $this->get('CommentingController/rss'); - $this->assertEquals(10, substr_count($response->getBody(), ""), "10 approved, non spam comments on page 1"); + + $this->assertEquals(10, substr_count($response->getBody(), ''), '10 approved, non spam comments on page 1'); $response = $this->get('CommentingController/rss?start=10'); - $this->assertEquals(4, substr_count($response->getBody(), ""), "3 approved, non spam comments on page 2"); - // all comments on a type - $response = $this->get('CommentingController/rss/CommentableItem'); - $this->assertEquals(10, substr_count($response->getBody(), "")); + $this->assertEquals(4, substr_count($response->getBody(), ''), '3 approved, non spam comments on page 2'); - $response = $this->get('CommentingController/rss/CommentableItem?start=10'); - $this->assertEquals(4, substr_count($response->getBody(), ""), "3 approved, non spam comments on page 2"); + $response = $this->get('CommentingController/rss/HasComments'); - // specific page - $response = $this->get('CommentingController/rss/CommentableItem/'.$item->ID); - $this->assertEquals(1, substr_count($response->getBody(), "")); + $this->assertEquals(10, substr_count($response->getBody(), '')); + + $response = $this->get('CommentingController/rss/HasComments?start=10'); + + $this->assertEquals(4, substr_count($response->getBody(), ''), '3 approved, non spam comments on page 2'); + + $response = $this->get('CommentingController/rss/HasComments/' . $item->ID); + + $this->assertEquals(1, substr_count($response->getBody(), '')); $this->assertContains('FA', $response->getBody()); - // test accessing comments on a type that doesn't exist $response = $this->get('CommentingController/rss/Fake'); $this->assertEquals(404, $response->getStatusCode()); } public function testCommentsForm() { 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', array( 'Name' => 'Poster', 'Email' => 'guy@test.com', 'Comment' => 'My Comment', 'ParentID' => $parent->ID, - 'BaseClass' => 'CommentableItem', + 'BaseClass' => 'HasComments', 'action_doPostComment' => 'Post' ) ); $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( - 'Name' => 'Poster', - 'Email' => 'guy@test.com', - 'Comment' => 'My Comment', - 'ParentID' => $parent->ID, - 'BaseClass' => 'CommentableItem', - )), + array( + array( + 'Name' => 'Poster', + 'Email' => 'guy@test.com', + 'Comment' => 'My Comment', + 'ParentID' => $parent->ID, + 'BaseClass' => 'HasComments', + ) + ), Comment::get()->filter('Email', 'guy@test.com') ); - - // Test posting to parent comment + + /** + * @var Comment $parentComment + */ $parentComment = $this->objFromFixture('Comment', 'firstComA'); + $this->assertEquals(0, $parentComment->ChildComments()->count()); $response = $this->post( - 'CommentingController/reply/'.$parentComment->ID, + 'CommentingController/reply/' . $parentComment->ID, array( 'Name' => 'Test Author', 'Email' => 'test@test.com', 'Comment' => 'Making a reply to firstComA', 'ParentID' => $parent->ID, - 'BaseClass' => 'CommentableItem', + 'BaseClass' => 'HasComments', 'ParentCommentID' => $parentComment->ID, 'action_doPostComment' => 'Post' ) ); + $this->assertEquals(302, $response->getStatusCode()); - $this->assertStringStartsWith('CommentableItem_Controller#comment-', $response->getHeader('Location')); - $this->assertDOSEquals(array(array( - 'Name' => 'Test Author', + $this->assertStringStartsWith('HasComments_Controller#comment-', $response->getHeader('Location')); + $this->assertDOSEquals(array( + array( + 'Name' => 'Test Author', 'Email' => 'test@test.com', 'Comment' => 'Making a reply to firstComA', 'ParentID' => $parent->ID, - 'BaseClass' => 'CommentableItem', + 'BaseClass' => 'HasComments', 'ParentCommentID' => $parentComment->ID - )), $parentComment->ChildComments()); + ) + ), $parentComment->ChildComments()); } } diff --git a/tests/CommentsTest.php b/tests/CommentsTest.php index ffd037f..4bd8a0e 100644 --- a/tests/CommentsTest.php +++ b/tests/CommentsTest.php @@ -1,21 +1,30 @@ update('CommentsExtension', 'comments', array( 'enabled' => true, 'enabled_cms' => false, @@ -29,188 +38,244 @@ class CommentsTest extends FunctionalTest { 'frontend_spam' => false, )); - // Configure this dataobject - Config::inst()->update('CommentableItem', 'comments', array( - 'enabled_cms' => true + Config::inst()->update('HasComments', 'comments', array( + 'enabled_cms' => true, )); } + /** + * {@inheritdoc} + */ public function tearDown() { Config::unnest(); + parent::tearDown(); } public function testCommentsList() { - // comments don't require moderation so unmoderated comments can be - // shown but not spam posts - Config::inst()->update('CommentableItem', 'comments', array( + Config::inst()->update('HasComments', 'comments', array( 'require_moderation_nonmembers' => false, 'require_moderation' => false, 'require_moderation_cms' => false, )); - $item = $this->objFromFixture('CommentableItem', 'spammed'); + /** + * @var HasComments $item + */ + $item = $this->objFromFixture('HasComments', 'spammed'); + $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'); + Config::inst()->update('HasComments', 'comments', array('require_moderation_nonmembers' => true)); - // 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); - // Check that require_moderation overrides this option - Config::inst()->update('CommentableItem', 'comments', array('require_moderation' => true)); + Config::inst()->update('HasComments', 'comments', array('require_moderation' => true)); + $this->assertEquals('Required', $item->ModerationRequired); - $this->assertDOSEquals(array( - array('Name' => 'Comment 3') - ), $item->Comments(), 'Only 1 non spam, moderated post should be shown'); + $this->assertDOSEquals( + array( + array('Name' => 'Comment 3') + ), + $item->Comments(), + 'Only 1 non spam, moderated post should be shown' + ); $this->assertEquals(1, $item->Comments()->Count()); - // require_moderation_nonmembers still filters out unmoderated comments - Config::inst()->update('CommentableItem', 'comments', array('require_moderation' => false)); + Config::inst()->update('HasComments', 'comments', array('require_moderation' => false)); + $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()); - // With unmoderated comments set to display in frontend - Config::inst()->update('CommentableItem', 'comments', array( + Config::inst()->update('HasComments', 'comments', array( 'require_moderation' => true, - 'frontend_moderation' => true + 'frontend_moderation' => true, )); + $this->assertEquals(1, $item->Comments()->Count()); $this->logInWithPermission('ADMIN'); + $this->assertEquals(2, $item->Comments()->Count()); - // With spam comments set to display in frontend - Config::inst()->update('CommentableItem', 'comments', array( + Config::inst()->update('HasComments', 'comments', array( 'require_moderation' => true, 'frontend_moderation' => false, 'frontend_spam' => true, )); - if($member = Member::currentUser()) $member->logOut(); + + if($member = Member::currentUser()) { + $member->logOut(); + } + $this->assertEquals(1, $item->Comments()->Count()); $this->logInWithPermission('ADMIN'); + $this->assertEquals(2, $item->Comments()->Count()); - - // With spam and unmoderated comments set to display in frontend - Config::inst()->update('CommentableItem', 'comments', array( + Config::inst()->update('HasComments', 'comments', array( 'require_moderation' => true, 'frontend_moderation' => true, 'frontend_spam' => true, )); - if($member = Member::currentUser()) $member->logOut(); + + if($member = Member::currentUser()) { + $member->logOut(); + } + $this->assertEquals(1, $item->Comments()->Count()); $this->logInWithPermission('ADMIN'); + $this->assertEquals(4, $item->Comments()->Count()); } /** - * Test moderation options configured via the CMS + * Test moderation options configured via the CMS. */ public function testCommentCMSModerationList() { - // comments don't require moderation so unmoderated comments can be - // shown but not spam posts - Config::inst()->update('CommentableItem', 'comments', array( + Config::inst()->update('HasComments', 'comments', array( 'require_moderation' => true, 'require_moderation_cms' => true, )); - $item = $this->objFromFixture('CommentableItem', 'spammed'); + /** + * @var HasComments $item + */ + $item = $this->objFromFixture('HasComments', 'spammed'); + $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->write(); + $this->assertEquals('NonMembersOnly', $item->ModerationRequired); - // Check that require_moderation overrides this option $item->ModerationRequired = 'Required'; $item->write(); + $this->assertEquals('Required', $item->ModerationRequired); - $this->assertDOSEquals(array( - array('Name' => 'Comment 3') - ), $item->Comments(), 'Only 1 non spam, moderated post should be shown'); - $this->assertEquals(1, $item->Comments()->Count()); + $this->assertDOSEquals( + array( + array('Name' => 'Comment 3') + ), + $item->Comments(), + 'Only 1 non spam, moderated post should be shown' + ); - // require_moderation_nonmembers still filters out unmoderated comments $item->ModerationRequired = 'NonMembersOnly'; $item->write(); + $this->assertEquals(1, $item->Comments()->Count()); $item->ModerationRequired = 'None'; $item->write(); + $this->assertEquals(2, $item->Comments()->Count()); } public function testCanPostComment() { - Config::inst()->update('CommentableItem', 'comments', array( + Config::inst()->update('HasComments', 'comments', array( 'require_login' => false, 'require_login_cms' => 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->assertTrue($item->canPostComment()); - // Test permission required to post - Config::inst()->update('CommentableItem', 'comments', array( + Config::inst()->update('HasComments', 'comments', array( 'require_login' => true, 'required_permission' => 'POSTING_PERMISSION', )); + $this->assertTrue($item->CommentsRequireLogin); $this->assertFalse($item->canPostComment()); + $this->logInWithPermission('WRONG_ONE'); + $this->assertFalse($item->canPostComment()); + $this->logInWithPermission('POSTING_PERMISSION'); - $this->assertTrue($item->canPostComment()); - $this->logInWithPermission('ADMIN'); + $this->assertTrue($item->canPostComment()); - // Test require login to post, but not any permissions - Config::inst()->update('CommentableItem', 'comments', array( + $this->logInWithPermission('ADMIN'); + + $this->assertTrue($item->canPostComment()); + + Config::inst()->update('HasComments', 'comments', array( 'required_permission' => false, )); + $this->assertTrue($item->CommentsRequireLogin); - if($member = Member::currentUser()) $member->logOut(); + + if($member = Member::currentUser()) { + $member->logOut(); + } + $this->assertFalse($item->canPostComment()); + $this->logInWithPermission('ANY_PERMISSION'); + $this->assertTrue($item->canPostComment()); - // Test options set via CMS - Config::inst()->update('CommentableItem', 'comments', array( + Config::inst()->update('HasComments', 'comments', array( 'require_login' => true, 'require_login_cms' => true, )); + $this->assertFalse($item->CommentsRequireLogin); $this->assertTrue($item2->CommentsRequireLogin); - if($member = Member::currentUser()) $member->logOut(); + + if($member = Member::currentUser()) { + $member->logOut(); + } + $this->assertTrue($item->canPostComment()); $this->assertFalse($item2->canPostComment()); - // Login grants permission to post $this->logInWithPermission('ANY_PERMISSION'); + $this->assertTrue($item->canPostComment()); $this->assertTrue($item2->canPostComment()); - + } public function testCanView() { @@ -218,15 +283,15 @@ class CommentsTest extends FunctionalTest { $admin = $this->objFromFixture('Member', 'commentadmin'); $comment = $this->objFromFixture('Comment', 'firstComA'); - $this->assertTrue($comment->canView($visitor), + $this->assertTrue($comment->canView($visitor), 'Unauthenticated members can view comments associated to a object with ProvideComments=1' ); $this->assertTrue($comment->canView($admin), 'Admins with CMS_ACCESS_CommentAdmin permissions can view comments associated to a page with ProvideComments=1' ); - + $disabledComment = $this->objFromFixture('Comment', 'disabledCom'); - + $this->assertFalse($disabledComment->canView($visitor), 'Unauthenticated members can not view comments associated to a object with ProvideComments=0' ); @@ -235,221 +300,278 @@ class CommentsTest extends FunctionalTest { 'Admins with CMS_ACCESS_CommentAdmin permissions can view comments associated to a page with ProvideComments=0' ); } - + public function testCanEdit() { $visitor = $this->objFromFixture('Member', 'visitor'); $admin = $this->objFromFixture('Member', 'commentadmin'); $comment = $this->objFromFixture('Comment', 'firstComA'); - + $this->assertFalse($comment->canEdit($visitor)); $this->assertTrue($comment->canEdit($admin)); } - + public function testCanDelete() { $visitor = $this->objFromFixture('Member', 'visitor'); $admin = $this->objFromFixture('Member', 'commentadmin'); $comment = $this->objFromFixture('Comment', 'firstComA'); - + $this->assertFalse($comment->canEdit($visitor)); $this->assertTrue($comment->canEdit($admin)); } - + public function testDeleteComment() { - // Test anonymous user - if($member = Member::currentUser()) $member->logOut(); + if($member = Member::currentUser()) { + $member->logOut(); + } + + /** + * @var Comment $comment + */ $comment = $this->objFromFixture('Comment', 'firstComA'); - $commentID = $comment->ID; + $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()); - $check = DataObject::get_by_id('Comment', $commentID); - $this->assertTrue($check && $check->exists()); - // Test non-authenticated user $this->logInAs('visitor'); + $this->assertNull($comment->DeleteLink(), 'No permission to see delete link'); - // Test authenticated user $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'); - $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 - $adminComment2Link = $comment->DeleteLink(); - $this->assertNotEquals($adminComment2Link, $adminComment1Link); + $delete = $this->get($commentAdminLink); + + $this->assertEquals(400, $delete->getStatusCode()); + $this->assertNotEquals($comment->DeleteLink(), $commentAdminLink); + $this->autoFollowRedirection = false; - $delete = $this->get($adminComment2Link); + + $delete = $this->get($comment->DeleteLink()); + $this->assertEquals(302, $delete->getStatusCode()); - $check = DataObject::get_by_id('Comment', $commentID); - $this->assertFalse($check && $check->exists()); + $this->assertFalse(DataObject::get_by_id('Comment', $comment->ID)); } public function testSpamComment() { - // Test anonymous user - if($member = Member::currentUser()) $member->logOut(); + if($member = Member::currentUser()) { + $member->logOut(); + } + + /** + * @var Comment $comment + */ $comment = $this->objFromFixture('Comment', 'firstComA'); - $commentID = $comment->ID; + $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()); - $check = DataObject::get_by_id('Comment', $commentID); - $this->assertEquals(0, $check->IsSpam, 'No permission to mark as spam'); + $this->assertEquals(0, $comment->IsSpam, 'No permission to mark as spam'); - // Test non-authenticated user $this->logInAs('visitor'); + $this->assertNull($comment->SpamLink(), 'No permission to see mark as spam link'); - // Test authenticated user $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'); - $spam = $this->get($adminComment1Link); + + $spam = $this->get($commentAdminLink); + $this->assertEquals(400, $spam->getStatusCode()); - $check = DataObject::get_by_id('Comment', $comment->ID); - $this->assertEquals(0, $check->IsSpam, 'No permission to mark as spam'); + $this->assertNotEquals($comment->SpamLink(), $commentAdminLink); - // Test that this other admin can spam the comment with their own link - $adminComment2Link = $comment->SpamLink(); - $this->assertNotEquals($adminComment2Link, $adminComment1Link); $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 - $this->assertNull($check->SpamLink()); + $spam = $this->get($comment->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() { - // Test anonymous user - if($member = Member::currentUser()) $member->logOut(); + if($member = Member::currentUser()) { + $member->logOut(); + } + + /** + * @var Comment $comment + */ $comment = $this->objFromFixture('Comment', 'secondComC'); - $commentID = $comment->ID; + $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()); - $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->assertNull($comment->HamLink(), 'No permission to see mark as ham link'); - // Test authenticated user $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'); - $ham = $this->get($adminComment1Link); + + $ham = $this->get($adminCommentLink); + $this->assertEquals(400, $ham->getStatusCode()); - $check = DataObject::get_by_id('Comment', $comment->ID); - $this->assertEquals(1, $check->IsSpam, 'No permission to mark as ham'); + $this->assertNotEquals($comment->HamLink(), $adminCommentLink); - // Test that this other admin can ham the comment with their own link - $adminComment2Link = $comment->HamLink(); - $this->assertNotEquals($adminComment2Link, $adminComment1Link); $this->autoFollowRedirection = false; - $ham = $this->get($adminComment2Link); + + $ham = $this->get($comment->HamLink()); + $this->assertEquals(302, $ham->getStatusCode()); - $check = DataObject::get_by_id('Comment', $commentID); - $this->assertEquals(0, $check->IsSpam); - // Cannot re-ham hammed comment - $this->assertNull($check->HamLink()); + /** + * @var Comment $comment + */ + $comment = DataObject::get_by_id('Comment', $comment->ID); + + $this->assertEquals(0, $comment->IsSpam); + $this->assertNull($comment->HamLink()); } - + public function testApproveComment() { - // Test anonymous user - if($member = Member::currentUser()) $member->logOut(); + if($member = Member::currentUser()) { + $member->logOut(); + } + + /** + * @var Comment $comment + */ $comment = $this->objFromFixture('Comment', 'secondComB'); - $commentID = $comment->ID; + $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()); - $check = DataObject::get_by_id('Comment', $commentID); - $this->assertEquals(0, $check->Moderated, 'No permission to approve'); - // Test non-authenticated user $this->logInAs('visitor'); + $this->assertNull($comment->ApproveLink(), 'No permission to see approve link'); - // Test authenticated user $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'); - $approve = $this->get($adminComment1Link); + + $approve = $this->get($adminCommentLink); + $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 - $adminComment2Link = $comment->ApproveLink(); - $this->assertNotEquals($adminComment2Link, $adminComment1Link); + $this->assertNotEquals($comment->ApproveLink(), $adminCommentLink); + $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 - $this->assertNull($check->ApproveLink()); + $approve = $this->get($comment->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() { $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( - 'Http', - 'Https', + 'HTTP', + 'HTTPS', ); + $url = '://example.com'; foreach($protocols as $protocol) { - $comment->CommenterURL = $protocol . $url; - // The protocol should stay as if, assuming it is valid + $comment->URL = $protocol . $url; $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() { if(!class_exists('HTMLPurifier')) { $this->markTestSkipped('HTMLPurifier class not found'); - return; } - $origAllowed = Commenting::get_config_value('CommentableItem','html_allowed'); - - // Without HTML allowed + $originalHtmlAllowed = Commenting::get_config_value('HasComments', 'html_allowed'); + $comment1 = new Comment(); - $comment1->BaseClass = 'CommentableItem'; + $comment1->BaseClass = 'HasComments'; $comment1->Comment = '

my comment

'; $comment1->write(); + $this->assertEquals( '

my comment

', $comment1->Comment, @@ -457,19 +579,20 @@ class CommentsTest extends FunctionalTest { 'which is correct behaviour because the HTML will be escaped' ); - // With HTML allowed - Commenting::set_config_value('CommentableItem','html_allowed', true); + Commenting::set_config_value('HasComments', 'html_allowed', true); + $comment2 = new Comment(); - $comment2->BaseClass = 'CommentableItem'; + $comment2->BaseClass = 'HasComments'; $comment2->Comment = '

my comment

'; $comment2->write(); + $this->assertEquals( '

my comment

', $comment2->Comment, 'Removes HTML tags which are not on the whitelist' ); - Commenting::set_config_value('CommentableItem','html_allowed', $origAllowed); + Commenting::set_config_value('HasComments', 'html_allowed', $originalHtmlAllowed); } public function testDefaultTemplateRendersHtmlWithAllowHtml() { @@ -477,63 +600,92 @@ class CommentsTest extends FunctionalTest { $this->markTestSkipped('HTMLPurifier class not found'); } - $origAllowed = Commenting::get_config_value('CommentableItem', 'html_allowed'); - $item = new CommentableItem(); + $originalHtmlAllowed = Commenting::get_config_value('HasComments', 'html_allowed'); + + $item = new HasComments(); $item->write(); - // Without HTML allowed $comment = new Comment(); $comment->Comment = '

my comment

'; $comment->ParentID = $item->ID; - $comment->BaseClass = 'CommentableItem'; + $comment->BaseClass = 'HasComments'; $comment->write(); - - $html = $item->customise(array('CommentsEnabled' => true))->renderWith('CommentsInterface'); + + $html = $item + ->customise(array( + 'CommentsEnabled' => true, + )) + ->renderWith('CommentsInterface'); + $this->assertContains( '<p>my comment</p>', $html ); - Commenting::set_config_value('CommentableItem','html_allowed', true); - $html = $item->customise(array('CommentsEnabled' => true))->renderWith('CommentsInterface'); + Commenting::set_config_value('HasComments', 'html_allowed', true); + + $html = $item + ->customise(array( + 'CommentsEnabled' => true, + )) + ->renderWith('CommentsInterface'); + $this->assertContains( '

my comment

', $html ); - Commenting::set_config_value('CommentableItem','html_allowed', $origAllowed); + Commenting::set_config_value('HasComments', 'html_allowed', $originalHtmlAllowed); } } - /** + * @mixin CommentsExtension + * * @package comments * @subpackage tests */ -class CommentableItem extends DataObject implements TestOnly { - +class HasComments extends DataObject implements TestOnly { + /** + * @var array + */ private static $db = array( 'ProvideComments' => 'Boolean', - 'Title' => 'Varchar' + 'Title' => 'Varchar', ); + /** + * @var array + */ private static $extensions = array( - 'CommentsExtension' + 'CommentsExtension', ); + /** + * @return string + */ public function RelativeLink() { - return "CommentableItem_Controller"; + return 'HasComments_Controller'; } + /** + * {@inheritdoc} + */ public function canView($member = null) { return true; } + /** + * @return string + */ public function Link() { return $this->RelativeLink(); } + /** + * @return string + */ public function AbsoluteLink() { return Director::absoluteURL($this->RelativeLink()); } @@ -543,9 +695,11 @@ class CommentableItem extends DataObject implements TestOnly { * @package comments * @subpackage tests */ -class CommentableItem_Controller extends Controller implements TestOnly { - +class HasComments_Controller extends Controller implements TestOnly { + /** + * @return Form + */ public function index() { - return CommentableItem::get()->first()->CommentsForm(); + return HasComments::get()->first()->CommentsForm(); } } diff --git a/tests/CommentsTest.yml b/tests/CommentsTest.yml index 6ea339a..c30b922 100644 --- a/tests/CommentsTest.yml +++ b/tests/CommentsTest.yml @@ -16,7 +16,7 @@ Permission: Code: CMS_ACCESS_CommentAdmin Group: =>Group.commentadmins -CommentableItem: +HasComments: first: Title: First ProvideComments: 1 @@ -38,135 +38,135 @@ CommentableItem: Comment: firstComA: - ParentID: =>CommentableItem.first + ParentID: =>HasComments.first Name: FA Comment: textFA - BaseClass: CommentableItem + BaseClass: HasComments Moderated: 1 IsSpam: 0 secondComA: - ParentID: =>CommentableItem.second + ParentID: =>HasComments.second Name: SA Comment: textSA Moderated: 1 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments secondComB: - ParentID: =>CommentableItem.second + ParentID: =>HasComments.second Name: SB Comment: textSB Moderated: 0 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments secondComC: - ParentID: =>CommentableItem.second + ParentID: =>HasComments.second Name: SB Comment: textSB Moderated: 1 IsSpam: 1 - BaseClass: CommentableItem + BaseClass: HasComments thirdComA: - ParentID: =>CommentableItem.third + ParentID: =>HasComments.third Name: TA Comment: textTA Moderated: 1 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments thirdComB: - ParentID: =>CommentableItem.third + ParentID: =>HasComments.third Name: TB Comment: textTB Moderated: 1 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments thirdComC: - ParentID: =>CommentableItem.third + ParentID: =>HasComments.third Name: TC Comment: textTC Moderated: 1 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments thirdComD: - ParentID: =>CommentableItem.third + ParentID: =>HasComments.third Name: TC Comment: textTC Moderated: 1 - BaseClass: CommentableItem + BaseClass: HasComments thirdComE: - ParentID: =>CommentableItem.third + ParentID: =>HasComments.third Name: TC Comment: textTC Moderated: 1 - BaseClass: CommentableItem + BaseClass: HasComments thirdComF: - ParentID: =>CommentableItem.third + ParentID: =>HasComments.third Name: TC Comment: textTC Moderated: 1 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments thirdComG: - ParentID: =>CommentableItem.third + ParentID: =>HasComments.third Name: TC Comment: textTC Moderated: 1 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments thirdComH: - ParentID: =>CommentableItem.third + ParentID: =>HasComments.third Name: TC Comment: textTC Moderated: 1 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments thirdComI: - ParentID: =>CommentableItem.third + ParentID: =>HasComments.third Name: TC Comment: textTC Moderated: 1 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments thirdComJ: - ParentID: =>CommentableItem.third + ParentID: =>HasComments.third Name: TC Comment: textTC Moderated: 1 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments thirdComK: - ParentID: =>CommentableItem.third + ParentID: =>HasComments.third Name: TC Comment: textTC Moderated: 1 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments disabledCom: - ParentID: =>CommentableItem.nocomments + ParentID: =>HasComments.nocomments Name: Disabled Moderated: 0 IsSpam: 1 - BaseClass: CommentableItem + BaseClass: HasComments testCommentList1: - ParentID: =>CommentableItem.spammed + ParentID: =>HasComments.spammed Name: Comment 1 Moderated: 0 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments testCommentList2: - ParentID: =>CommentableItem.spammed + ParentID: =>HasComments.spammed Name: Comment 2 Moderated: 1 IsSpam: 1 - BaseClass: CommentableItem + BaseClass: HasComments testCommentList3: - ParentID: =>CommentableItem.spammed + ParentID: =>HasComments.spammed Name: Comment 3 Moderated: 1 IsSpam: 0 - BaseClass: CommentableItem + BaseClass: HasComments testCommentList4: - ParentID: =>CommentableItem.spammed + ParentID: =>HasComments.spammed Name: Comment 4 Moderated: 0 IsSpam: 1 - BaseClass: CommentableItem \ No newline at end of file + BaseClass: HasComments \ No newline at end of file