Initial commit with transfer of existing comments functionality

This commit is contained in:
Will Rossiter 2010-11-30 11:24:17 +13:00
commit a1df7f52df
12 changed files with 1592 additions and 0 deletions

17
LICENSE Normal file
View File

@ -0,0 +1,17 @@
Copyright (c) 2007-2010, SilverStripe Limited - www.silverstripe.com
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of SilverStripe nor the names of its contributors may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGE.

21
README.md Normal file
View File

@ -0,0 +1,21 @@
# Comments
## Maintainers
* Will Rossiter (Nickname: willr, wrossiter)
<will at silverstripe dot com>
## Introduction
This module provides a commenting functionality for Pages and other DataObjects. It replaces the built in
commenting functionality with a more flexible plug and play version.
For more documentation about the module see the docs/ folder.
## Requirements
* SilverStripe 3.0 (SilverStripe Trunk as of Dec '10)
## Installation
See docs/en/Installing.md

321
code/CommentAdmin.php Normal file
View File

@ -0,0 +1,321 @@
<?php
/**
* Comment administration system within the CMS
*
* @package comments
*/
class CommentAdmin extends LeftAndMain {
static $url_segment = 'comments';
static $url_rule = '/$Action';
static $menu_title = 'Comments';
static $allowed_actions = array(
'approvedmarked',
'deleteall',
'deletemarked',
'hammarked',
'showtable',
'spammarked',
'EditForm',
'unmoderated'
);
/**
* @var int The number of comments per page for the {@link CommentTable} in this admin.
*/
static $comments_per_page = '20';
public function init() {
parent::init();
Requirements::javascript(CMS_DIR . '/javascript/CommentAdmin_right.js');
Requirements::css(CMS_DIR . '/css/CommentAdmin.css');
}
public function showtable($params) {
return $this->getLastFormIn($this->renderWith('CommentAdmin_right'));
}
public function Section() {
$url = rtrim($_SERVER['REQUEST_URI'], '/');
if(strrpos($url, '&')) {
$url = substr($url, 0, strrpos($url, '&'));
}
$section = substr($url, strrpos($url, '/') + 1);
if($section != 'approved' && $section != 'unmoderated' && $section != 'spam') {
$section = Session::get('CommentsSection');
}
if($section != 'approved' && $section != 'unmoderated' && $section != 'spam') {
$section = 'approved';
}
return $section;
}
public function EditForm() {
$section = $this->Section();
if($section == 'approved') {
$filter = "\"IsSpam\" = 0 AND \"NeedsModeration\" = 0";
$title = "<h2>". _t('CommentAdmin.APPROVEDCOMMENTS', 'Approved Comments')."</h2>";
} else if($section == 'unmoderated') {
$filter = '"NeedsModeration" = 1';
$title = "<h2>"._t('CommentAdmin.COMMENTSAWAITINGMODERATION', 'Comments Awaiting Moderation')."</h2>";
} else {
$filter = '"IsSpam" = 1';
$title = "<h2>"._t('CommentAdmin.SPAM', 'Spam')."</h2>";
}
$filter .= ' AND "ParentID">0';
$tableFields = array(
"Name" => _t('CommentAdmin.AUTHOR', 'Author'),
"Comment" => _t('CommentAdmin.COMMENT', 'Comment'),
"Parent.Title" => _t('CommentAdmin.PAGE', 'Page'),
"CommenterURL" => _t('CommentAdmin.COMMENTERURL', 'URL'),
"Created" => _t('CommentAdmin.DATEPOSTED', 'Date Posted')
);
$popupFields = new FieldSet(
new TextField('Name', _t('CommentAdmin.NAME', 'Name')),
new TextField('CommenterURL', _t('CommentAdmin.COMMENTERURL', 'URL')),
new TextareaField('Comment', _t('CommentAdmin.COMMENT', 'Comment'))
);
$idField = new HiddenField('ID', '', $section);
$table = new CommentTableField($this, "Comments", "PageComment", $section, $tableFields, $popupFields, array($filter), 'Created DESC');
$table->setParentClass(false);
$table->setFieldCasting(array(
'Created' => 'SSDatetime->Full',
'Comment' => array('HTMLText->LimitCharacters', 150)
));
$table->setPageSize(self::get_comments_per_page());
$table->addSelectOptions(array('all'=>'All', 'none'=>'None'));
$table->Markable = true;
$fields = new FieldSet(
new LiteralField("Title", $title),
$idField,
$table
);
$actions = new FieldSet();
if($section == 'unmoderated') {
$actions->push(new FormAction('acceptmarked', _t('CommentAdmin.ACCEPT', 'Accept')));
}
if($section == 'approved' || $section == 'unmoderated') {
$actions->push(new FormAction('spammarked', _t('CommentAdmin.SPAMMARKED', 'Mark as spam')));
}
if($section == 'spam') {
$actions->push(new FormAction('hammarked', _t('CommentAdmin.MARKASNOTSPAM', 'Mark as not spam')));
}
$actions->push(new FormAction('deletemarked', _t('CommentAdmin.DELETE', 'Delete')));
if($section == 'spam') {
$actions->push(new FormAction('deleteall', _t('CommentAdmin.DELETEALL', 'Delete All')));
}
$form = new Form($this, "EditForm", $fields, $actions);
return $form;
}
function deletemarked() {
$numComments = 0;
$folderID = 0;
$deleteList = '';
if($_REQUEST['Comments']) {
foreach($_REQUEST['Comments'] as $commentid) {
$comment = DataObject::get_by_id('PageComment', $commentid);
if($comment) {
$comment->delete();
$numComments++;
}
}
} else {
user_error("No comments in $commentList could be found!", E_USER_ERROR);
}
echo <<<JS
$deleteList
$('Form_EditForm').getPageFromServer($('Form_EditForm_ID').value);
statusMessage("Deleted $numComments comments.");
JS;
}
function deleteall() {
$numComments = 0;
$spam = DataObject::get('PageComment', '"PageComment"."IsSpam" = 1');
if($spam) {
$numComments = $spam->Count();
foreach($spam as $comment) {
$comment->delete();
}
}
$msg = sprintf(_t('CommentAdmin.DELETED', 'Deleted %s comments.'), $numComments);
echo <<<JS
$('Form_EditForm').getPageFromServer($('Form_EditForm_ID').value);
statusMessage("$msg");
JS;
}
function spammarked() {
$numComments = 0;
$folderID = 0;
$deleteList = '';
if($_REQUEST['Comments']) {
foreach($_REQUEST['Comments'] as $commentid) {
$comment = DataObject::get_by_id('PageComment', $commentid);
if($comment) {
$comment->IsSpam = true;
$comment->NeedsModeration = false;
$comment->write();
if(SSAkismet::isEnabled()) {
try {
$akismet = new SSAkismet();
$akismet->setCommentAuthor($comment->getField('Name'));
$akismet->setCommentContent($comment->getField('Comment'));
$akismet->submitSpam();
} catch (Exception $e) {
// Akismet didn't work, most likely the service is down.
}
}
$numComments++;
}
}
} else {
user_error("No comments in $commentList could be found!", E_USER_ERROR);
}
$msg = sprintf(_t('CommentAdmin.MARKEDSPAM', 'Marked %s comments as spam.'), $numComments);
echo <<<JS
$deleteList
$('Form_EditForm').getPageFromServer($('Form_EditForm_ID').value);
statusMessage("$msg");
JS;
}
function hammarked() {
$numComments = 0;
$folderID = 0;
$deleteList = '';
if($_REQUEST['Comments']) {
foreach($_REQUEST['Comments'] as $commentid) {
$comment = DataObject::get_by_id('PageComment', $commentid);
if($comment) {
$comment->IsSpam = false;
$comment->NeedsModeration = false;
$comment->write();
if(SSAkismet::isEnabled()) {
try {
$akismet = new SSAkismet();
$akismet->setCommentAuthor($comment->getField('Name'));
$akismet->setCommentContent($comment->getField('Comment'));
$akismet->submitSpam();
} catch (Exception $e) {
// Akismet didn't work, most likely the service is down.
}
}
$numComments++;
}
}
} else {
user_error("No comments in $commentList could be found!", E_USER_ERROR);
}
$msg = sprintf(_t('CommentAdmin.MARKEDNOTSPAM', 'Marked %s comments as not spam.'), $numComments);
echo <<<JS
$deleteList
$('Form_EditForm').getPageFromServer($('Form_EditForm_ID').value);
statusMessage("$msg");
JS;
}
function acceptmarked() {
$numComments = 0;
$folderID = 0;
$deleteList = '';
if($_REQUEST['Comments']) {
foreach($_REQUEST['Comments'] as $commentid) {
$comment = DataObject::get_by_id('PageComment', $commentid);
if($comment) {
$comment->IsSpam = false;
$comment->NeedsModeration = false;
$comment->write();
$numComments++;
}
}
} else {
user_error("No comments in $commentList could be found!", E_USER_ERROR);
}
$msg = sprintf(_t('CommentAdmin.APPROVED', 'Accepted %s comments.'), $numComments);
echo <<<JS
$deleteList
$('Form_EditForm').getPageFromServer($('Form_EditForm_ID').value);
statusMessage("Accepted $numComments comments.");
JS;
}
/**
* Return the number of moderated comments
*/
function NumModerated() {
return DB::query("SELECT COUNT(*) FROM \"PageComment\" WHERE \"IsSpam\"=0 AND \"NeedsModeration\"=0")->value();
}
/**
* Return the number of unmoderated comments
*/
function NumUnmoderated() {
return DB::query("SELECT COUNT(*) FROM \"PageComment\" WHERE \"IsSpam\"=0 AND \"NeedsModeration\"=1")->value();
}
/**
* Return the number of comments marked as spam
*/
function NumSpam() {
return DB::query("SELECT COUNT(*) FROM \"PageComment\" WHERE \"IsSpam\"=1")->value();
}
/**
* @param $num int
*/
function set_comments_per_page($num){
self::$comments_per_page = $num;
}
/**
* @return int
*/
function get_comments_per_page(){
return self::$comments_per_page;
}
}
?>

View File

@ -0,0 +1,379 @@
<?php
/**
* Represents an interface for viewing and adding page comments
* Create one, passing the page discussed to the constructor. It can then be
* inserted into a template.
* @package cms
* @subpackage comments
*/
class PageCommentInterface extends RequestHandler {
static $url_handlers = array(
'$Item!' => '$Item',
);
static $allowed_actions = array(
'PostCommentForm',
);
protected $controller, $methodName, $page;
/**
* If this is true, you must be logged in to post a comment
* (and therefore, you don't need to specify a 'Your name' field unless
* your name is blank)
*
* @var bool
*/
static $comments_require_login = false;
/**
* If this is a valid permission code, you must be logged in
* and have the appropriate permission code on your account before you can
* post a comment.
*
* @var string
*/
static $comments_require_permission = "";
/**
* If this is true it will include the javascript for AJAX
* commenting. If it is set to false then it will not load
* the files required and it will fall back
*
* @var bool
*/
static $use_ajax_commenting = true;
/**
* If this is true then we should show the existing comments on
* the page even when we have disabled the comment form.
*
* If this is false the form + existing comments will be hidden
*
* @var bool
* @since 2.4 - Always show them by default
*/
static $show_comments_when_disabled = true;
/**
* Define how you want to order page comments by. By default order by newest
* to oldest.
*
* @var String - used as $orderby in DB query
* @since 2.4
*/
static $order_comments_by = "\"Created\" DESC";
/**
* Create a new page comment interface
* @param controller The controller that the interface is used on
* @param methodName The method to return this PageCommentInterface object
* @param page The page that we're commenting on
*/
function __construct($controller, $methodName, $page) {
$this->controller = $controller;
$this->methodName = $methodName;
$this->page = $page;
parent::__construct();
}
function Link() {
return Controller::join_links($this->controller->Link(), $this->methodName);
}
/**
* See {@link PageCommentInterface::$comments_require_login}
*
* @param boolean state The new state of this static field
*/
static function set_comments_require_login($state) {
self::$comments_require_login = (boolean) $state;
}
/**
* See {@link PageCommentInterface::$comments_require_permission}
*
* @param string permission The permission to check against.
*/
static function set_comments_require_permission($permission) {
self::$comments_require_permission = $permission;
}
/**
* See {@link PageCommentInterface::$show_comments_when_disabled}
*
* @param bool - show / hide the existing comments when disabled
*/
static function set_show_comments_when_disabled($state) {
self::$show_comments_when_disabled = $state;
}
/**
* See {@link PageCommentInterface::$order_comments_by}
*
* @param String
*/
static function set_order_comments_by($order) {
self::$order_comments_by = $order;
}
/**
* See {@link PageCommentInterface::$use_ajax_commenting}
*
* @param bool
*/
static function set_use_ajax_commenting($state) {
self::$use_ajax_commenting = $state;
}
function forTemplate() {
return $this->renderWith('PageCommentInterface');
}
/**
* @return boolean true if the currently logged in user can post a comment,
* false if they can't. Users can post comments by default, enforce
* security by using
* @link PageCommentInterface::set_comments_require_login() and
* @link {PageCommentInterface::set_comments_require_permission()}.
*/
static function CanPostComment() {
$member = Member::currentUser();
if(self::$comments_require_permission && $member && Permission::check(self::$comments_require_permission)) {
return true; // Comments require a certain permission, and the user has the correct permission
} elseif(self::$comments_require_login && $member && !self::$comments_require_permission) {
return true; // Comments only require that a member is logged in
} elseif(!self::$comments_require_permission && !self::$comments_require_login) {
return true; // Comments don't require anything - anyone can add a comment
}
return false;
}
/**
* if this page comment form requires users to have a
* valid permission code in order to post (used to customize the error
* message).
*
* @return bool
*/
function PostingRequiresPermission() {
return self::$comments_require_permission;
}
function Page() {
return $this->page;
}
function PostCommentForm() {
if(!$this->page->ProvideComments){
return false;
}
$fields = new FieldSet(
new HiddenField("ParentID", "ParentID", $this->page->ID)
);
$member = Member::currentUser();
if((self::$comments_require_login || self::$comments_require_permission) && $member && $member->FirstName) {
// note this was a ReadonlyField - which displayed the name in a span as well as the hidden field but
// it was not saving correctly. Have changed it to a hidden field. It passes the data correctly but I
// believe the id of the form field is wrong.
$fields->push(new ReadonlyField("NameView", _t('PageCommentInterface.YOURNAME', 'Your name'), $member->getName()));
$fields->push(new HiddenField("Name", "", $member->getName()));
} else {
$fields->push(new TextField("Name", _t('PageCommentInterface.YOURNAME', 'Your name')));
}
// optional commenter URL
$fields->push(new TextField("CommenterURL", _t('PageCommentInterface.COMMENTERURL', "Your website URL")));
if(MathSpamProtection::isEnabled()){
$fields->push(new TextField("Math", sprintf(_t('PageCommentInterface.SPAMQUESTION', "Spam protection question: %s"), MathSpamProtection::getMathQuestion())));
}
$fields->push(new TextareaField("Comment", _t('PageCommentInterface.YOURCOMMENT', "Comments")));
$form = new PageCommentInterface_Form($this, "PostCommentForm", $fields, new FieldSet(
new FormAction("postcomment", _t('PageCommentInterface.POST', 'Post'))
));
// Set it so the user gets redirected back down to the form upon form fail
$form->setRedirectToFormOnValidationError(true);
// Optional Spam Protection.
if(class_exists('SpamProtectorManager')) {
SpamProtectorManager::update_form($form, null, array('Name' => 'author_name', 'CommenterURL' => 'author_url', 'Comment' => 'post_body'));
self::set_use_ajax_commenting(false);
}
// Shall We use AJAX?
if(self::$use_ajax_commenting) {
Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/behaviour/behaviour.js');
Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/prototype/prototype.js');
Requirements::javascript(THIRDPARTY_DIR . '/scriptaculous/effects.js');
Requirements::javascript(CMS_DIR . '/javascript/PageCommentInterface.js');
}
// Load the data from Session
$form->loadDataFrom(array(
"Name" => Cookie::get("PageCommentInterface_Name"),
"Comment" => Cookie::get("PageCommentInterface_Comment"),
"CommenterURL" => Cookie::get("PageCommentInterface_CommenterURL")
));
return $form;
}
function Comments() {
// Comment limits
$limit = array();
$limit['start'] = isset($_GET['commentStart']) ? (int)$_GET['commentStart'] : 0;
$limit['limit'] = PageComment::$comments_per_page;
$spamfilter = isset($_GET['showspam']) ? '' : "AND \"IsSpam\" = 0";
$unmoderatedfilter = Permission::check('CMS_ACCESS_CommentAdmin') ? '' : "AND \"NeedsModeration\" = 0";
$order = self::$order_comments_by;
$comments = DataObject::get("PageComment", "\"ParentID\" = '" . Convert::raw2sql($this->page->ID) . "' $spamfilter $unmoderatedfilter", $order, "", $limit);
if(is_null($comments)) {
return;
}
// This allows us to use the normal 'start' GET variables as well (In the weird circumstance where you have paginated comments AND something else paginated)
$comments->setPaginationGetVar('commentStart');
return $comments;
}
function CommentRssLink() {
return Director::absoluteBaseURL() . "PageComment/rss?pageid=" . $this->page->ID;
}
/**
* A link to PageComment_Controller.deleteallcomments() which deletes all
* comments on a page referenced by the url param pageid
*/
function DeleteAllLink() {
if(Permission::check('CMS_ACCESS_CommentAdmin')) {
return Director::absoluteBaseURL() . "PageComment/deleteallcomments?pageid=" . $this->page->ID;
}
}
}
/**
* @package cms
* @subpackage comments
*/
class PageCommentInterface_Form extends Form {
function postcomment($data) {
// Spam filtering
Cookie::set("PageCommentInterface_Name", $data['Name']);
Cookie::set("PageCommentInterface_CommenterURL", $data['CommenterURL']);
Cookie::set("PageCommentInterface_Comment", $data['Comment']);
if(SSAkismet::isEnabled()) {
try {
$akismet = new SSAkismet();
$akismet->setCommentAuthor($data['Name']);
$akismet->setCommentContent($data['Comment']);
if($akismet->isCommentSpam()) {
if(SSAkismet::getSaveSpam()) {
$comment = Object::create('PageComment');
$this->saveInto($comment);
$comment->setField("IsSpam", true);
$comment->write();
}
echo "<b>"._t('PageCommentInterface_Form.SPAMDETECTED', 'Spam detected!!') . "</b><br /><br />";
printf("If you believe this was in error, please email %s.", ereg_replace("@", " _(at)_", Email::getAdminEmail()));
echo "<br /><br />"._t('PageCommentInterface_Form.MSGYOUPOSTED', 'The message you posted was:'). "<br /><br />";
echo $data['Comment'];
return;
}
} catch (Exception $e) {
// Akismet didn't work, continue without spam check
}
}
//check if spam question was right.
if(MathSpamProtection::isEnabled()){
if(!MathSpamProtection::correctAnswer($data['Math'])){
if(!Director::is_ajax()) {
Director::redirectBack();
}
return "spamprotectionfailed"; //used by javascript for checking if the spam question was wrong
}
}
// If commenting can only be done by logged in users, make sure the user is logged in
$member = Member::currentUser();
if(PageCommentInterface::CanPostComment() && $member) {
$this->Fields()->push(new HiddenField("AuthorID", "Author ID", $member->ID));
} elseif(!PageCommentInterface::CanPostComment()) {
echo "You're not able to post comments to this page. Please ensure you are logged in and have an appropriate permission level.";
return;
}
$comment = Object::create('PageComment');
$this->saveInto($comment);
// Store the Session ID if needed for Spamprotection
if($session = Session::get('mollom_user_session_id')) {
$comment->SessionID = $session;
Session::clear('mollom_user_session_id');
}
$comment->IsSpam = false;
$comment->NeedsModeration = PageComment::moderationEnabled();
$comment->write();
Cookie::set("PageCommentInterface_Comment", '');
$moderationMsg = _t('PageCommentInterface_Form.AWAITINGMODERATION', "Your comment has been submitted and is now awaiting moderation.");
if(Director::is_ajax()) {
if($comment->NeedsModeration){
echo $moderationMsg;
} else{
echo $comment->renderWith('PageCommentInterface_singlecomment');
}
} else {
if($comment->NeedsModeration){
$this->sessionMessage($moderationMsg, 'good');
}
if($comment->ParentID) {
$page = DataObject::get_by_id("Page", $comment->ParentID);
if($page) {
// if it needs moderation then it won't appear in the list. Therefore
// we need to link to the comment holder rather than the individual comment
$url = ($comment->NeedsModeration) ? $page->Link() . '#PageComments_holder' : $page->Link() . '#PageComment_' . $comment->ID;
return Director::redirect($url);
}
}
return Director::redirectBack();
}
}
}
/**
* @package cms
* @subpackage comments
*/
class PageCommentInterface_Controller extends ContentController {
function __construct() {
parent::__construct(null);
}
function newspamquestion() {
if(Director::is_ajax()) {
echo Convert::raw2xml(sprintf(_t('PageCommentInterface_Controller.SPAMQUESTION', "Spam protection question: %s"),MathSpamProtection::getMathQuestion()));
}
}
}
?>

228
code/dataobjects/Comment.php Executable file
View File

@ -0,0 +1,228 @@
<?php
/**
* Represents a single comment object.
*
* @package comments
*/
class Comment extends DataObject {
static $db = array(
"Name" => "Varchar(200)",
"Comment" => "Text",
"Email" => "Varchar(200)"
"URL" => "Varchar(255)",
"SessionID" => "Varchar(255)",
);
static $has_one = array(
"Parent" => "DataObject",
"Author" => "Member"
);
static $has_many = array();
static $many_many = array();
static $defaults = array();
static $casting = array(
"RSSTitle" => "Varchar",
);
static $comments_per_page = 10;
static $moderate = false;
static $bbcode = false;
/**
* Return a link to this comment
* @return string link to this comment.
*/
function Link() {
return $this->Parent()->Link() . '#PageComment_'. $this->ID;
}
function getRSSName() {
if($this->Name) {
return $this->Name;
} elseif($this->Author()) {
return $this->Author()->getName();
}
}
function ParsedBBCode(){
$parser = new BBCodeParser($this->Comment);
return $parser->parse();
}
function DeleteLink() {
return ($this->canDelete()) ? "PageComment_Controller/deletecomment/$this->ID" : false;
}
function CommentTextWithLinks() {
$pattern = '|([a-zA-Z]+://)([a-zA-Z0-9?&%.;:/=+_-]*)|is';
$replace = '<a rel="nofollow" href="$1$2">$1$2</a>';
return preg_replace($pattern, $replace, $this->Comment);
}
function SpamLink() {
return ($this->canEdit() && !$this->IsSpam) ? "PageComment_Controller/reportspam/$this->ID" : false;
}
function HamLink() {
return ($this->canEdit() && $this->IsSpam) ? "PageComment_Controller/reportham/$this->ID" : false;
}
function ApproveLink() {
return ($this->canEdit() && $this->NeedsModeration) ? "PageComment_Controller/approve/$this->ID" : false;
}
function SpamClass() {
if($this->getField('IsSpam')) {
return 'spam';
} else if($this->getField('NeedsModeration')) {
return 'unmoderated';
} else {
return 'notspam';
}
}
function RSSTitle() {
return sprintf(
_t('PageComment.COMMENTBY', "Comment by '%s' on %s", PR_MEDIUM, 'Name, Page Title'),
Convert::raw2xml($this->getRSSName()),
$this->Parent()->Title
);
}
function PageTitle() {
return $this->Parent()->Title;
}
static function enableModeration() {
self::$moderate = true;
}
static function moderationEnabled() {
return self::$moderate;
}
static function enableBBCode() {
self::$bbcode = true;
}
static function bbCodeEnabled() {
return self::$bbcode;
}
/**
*
* @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
*
*/
function fieldLabels($includerelations = true) {
$labels = parent::fieldLabels($includerelations);
$labels['Name'] = _t('PageComment.Name', 'Author Name');
$labels['Comment'] = _t('PageComment.Comment', 'Comment');
$labels['IsSpam'] = _t('PageComment.IsSpam', 'Spam?');
$labels['NeedsModeration'] = _t('PageComment.NeedsModeration', 'Needs Moderation?');
return $labels;
}
/**
* This method is called just before this object is
* written to the database.
*
* Specifically, make sure "http://" exists at the start
* of the URL, if it doesn't have https:// or http://
*/
public function onBeforeWrite() {
parent::onBeforeWrite();
$url = $this->CommenterURL;
if($url) {
if(strtolower(substr($url, 0, 8)) != 'https://' && strtolower(substr($url, 0, 7)) != 'http://') {
$this->CommenterURL = 'http://' . $url;
}
}
}
/**
* This always returns true, and should be handled by {@link PageCommentInterface->CanPostComment()}.
*
* @todo Integrate with PageCommentInterface::$comments_require_permission and $comments_require_login
*
* @param Member $member
* @return Boolean
*/
function canCreate($member = null) {
return true;
}
/**
* Checks for association with a page,
* and {@link SiteTree->ProvidePermission} flag being set to TRUE.
* Note: There's an additional layer of permission control
* in {@link PageCommentInterface}.
*
* @param Member $member
* @return Boolean
*/
function canView($member = null) {
if(!$member) $member = Member::currentUser();
// Standard mechanism for accepting permission changes from decorators
$extended = $this->extendedCan('canView', $member);
if($extended !== null) return $extended;
$page = $this->Parent();
return (
($page && $page->ProvideComments)
|| (bool)Permission::checkMember($member, 'CMS_ACCESS_CommentAdmin')
);
}
/**
* Checks for "CMS_ACCESS_CommentAdmin" permission codes
* and {@link canView()}.
*
* @param Member $member
* @return Boolean
*/
function canEdit($member = null) {
if(!$member) $member = Member::currentUser();
// Standard mechanism for accepting permission changes from decorators
$extended = $this->extendedCan('canEdit', $member);
if($extended !== null) return $extended;
if(!$this->canView($member)) return false;
return (bool)Permission::checkMember($member, 'CMS_ACCESS_CommentAdmin');
}
/**
* Checks for "CMS_ACCESS_CommentAdmin" permission codes
* and {@link canEdit()}.
*
* @param Member $member
* @return Boolean
*/
function canDelete($member = null) {
if(!$member) $member = Member::currentUser();
// Standard mechanism for accepting permission changes from decorators
$extended = $this->extendedCan('canDelete', $member);
if($extended !== null) return $extended;
return $this->canEdit($member);
}
}

View File

@ -0,0 +1,16 @@
<?php
/**
* @package comments
*/
class CommentExtension extends DataObjectDecorator {
function Comments() {
return DataObject::get('Comment', "\"RecordClassID\" = '". $this->owner->ID ."' AND \"RecordClass\" = '". $this->ownerBaseClass ."'");
}
function CommentsForm() {
die();
}
}

View File

@ -0,0 +1,107 @@
<?php
/**
* Special kind of ComplexTableField for managing comments.
*
* @package comments
*/
class CommentTableField extends ComplexTableField {
protected $template = "CommentTableField";
protected $mode;
function __construct($controller, $name, $sourceClass, $mode, $fieldList, $detailFormFields = null, $sourceFilter = "", $sourceSort = "Created", $sourceJoin = "") {
$this->mode = $mode;
Session::set('CommentsSection', $mode);
parent::__construct($controller, $name, $sourceClass, $fieldList, $detailFormFields, $sourceFilter, $sourceSort, $sourceJoin);
$this->Markable = true;
// Note: These keys have special behaviour associated through TableListField.js
$this->selectOptions = array(
'all' => _t('CommentTableField.SELECTALL', 'All'),
'none' => _t('CommentTableField.SELECTNONE', 'None')
);
// search
$search = isset($_REQUEST['CommentSearch']) ? Convert::raw2sql($_REQUEST['CommentSearch']) : null;
if(!empty($_REQUEST['CommentSearch'])) {
$this->sourceFilter[] = "( \"Name\" LIKE '%$search%' OR \"Comment\" LIKE '%$search%')";
}
}
function FieldHolder() {
$ret = parent::FieldHolder();
Requirements::javascript(CMS_DIR . '/javascript/CommentTableField.js');
return $ret;
}
function Items() {
$this->sourceItems = $this->sourceItems();
if(!$this->sourceItems) {
return null;
}
$pageStart = (isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0;
$this->sourceItems->setPageLimits($pageStart, $this->pageSize, $this->totalCount);
$output = new DataObjectSet();
foreach($this->sourceItems as $pageIndex=>$item) {
$output->push(Object::create('CommentTableField_Item',$item, $this, $pageStart+$pageIndex));
}
return $output;
}
function HasSpamButton() {
return $this->mode == 'approved' || $this->mode == 'unmoderated';
}
function HasApproveButton() {
return $this->mode == 'unmoderated';
}
function HasHamButton() {
return $this->mode == 'spam';
}
function SearchForm() {
$query = isset($_GET['CommentSearch']) ? $_GET['CommentSearch'] : null;
$searchFields = new FieldGroup(
new TextField('CommentSearch', _t('CommentTableField.SEARCH', 'Search'), $query),
new HiddenField("ctf[ID]",'',$this->mode),
new HiddenField('CommentFieldName','',$this->name)
);
$actionFields = new LiteralField('CommentFilterButton','<input type="submit" name="CommentFilterButton" value="'. _t('CommentTableField.FILTER', 'Filter') .'" id="CommentFilterButton"/>');
$fieldContainer = new FieldGroup(
$searchFields,
$actionFields
);
return $fieldContainer->FieldHolder();
}
}
/**
* Single row of a {@link CommentTableField}
*
* @package comments
*/
class CommentTableField_Item extends ComplexTableField_Item {
function HasSpamButton() {
return $this->parent()->HasSpamButton();
}
function HasApproveButton() {
return $this->parent()->HasApproveButton();
}
function HasHamButton() {
return $this->parent()->HasHamButton();
}
}

246
javascript/CommentsInterface.js Executable file
View File

@ -0,0 +1,246 @@
/**
* Ajax to support the comment posting system
*/
PageCommentInterface = Class.create();
PageCommentInterface.prototype = {
initialize: function() {
Behaviour.register({
'#PageCommentInterface_Form_PostCommentForm_action_postcomment' : {
onclick : this.postComment
},
'#PageComments a.deletelink' : {
onclick : this.deleteComment
},
'#PageComments a.spamlink' : {
onclick : this.reportSpam
},
'#PageComments a.hamlink' : {
onclick : this.reportHam
},
'#PageComments a.approvelink' : {
onclick : this.approveComment
}
});
},
loadSpamQuestion: function(response) {
var spamQuestionDiv = $('Math');
var mathLabel = spamQuestionDiv.getElementsByTagName('label')[0];
mathLabel.innerHTML = response.responseText;
var mathQuestion = spamQuestionDiv.getElementsByTagName('input')[0];
mathQuestion.value = '';
},
postComment: function() {
var form = $("PageCommentInterface_Form_PostCommentForm");
var message = $("PageCommentInterface_Form_PostCommentForm_error");
if(form.elements.Name.value && form.elements.Comment.value) {
if(noComments = $('NoComments')) {
Element.remove(noComments);
var pageComments = document.createElement('ul');
pageComments.id = 'PageComments';
$('CommentHolder').appendChild(pageComments);
}
message.style.display = 'none';
// Create a new <li> for the post
var pageComments = $('PageComments').getElementsByTagName('li');
var __newComment = document.createElement('li');
// Add it to the list with a 'loading' message
$('PageComments').insertBefore(__newComment, pageComments[0]);
__newComment.innerHTML = '<p><img src="cms/images/network-save.gif" /> Loading...</p>';
// Submit the form via ajax
Ajax.SubmitForm(form, "action_postcomment", {
onSuccess : function(response) {
// Create an Ajax request to regenerate the spam protection question
//need to check if there is actually a spam question to change first
if(form.elements.Math){
new Ajax.Request(document.getElementsByTagName('base')[0].href+'PageCommentInterface_Controller/newspamquestion', {
onSuccess: loadSpamQuestion,
onFailure: Ajax.Evaluator
});
}
if(response.responseText != "spamprotectionfailed"){
__newComment.className ="even";
// Load the response into the new <li>
__newComment.innerHTML = response.responseText;
Behaviour.apply(__newComment);
// Flash it using Scriptaculous
new Effect.Highlight(__newComment, { endcolor: '#e9e9e9' } );
if(response.responseText.match('<b>Spam detected!!</b>')) {
__newComment.className = 'spam';
}
}else{
__newComment.innerHTML = "";
Behaviour.apply(__newComment);
message.style.display = '';
message.innerHTML = "You got the spam question wrong.";
}
},
onFailure : function(response) {
alert(response.responseText);
}
});
} else {
message.style.display = '';
message.innerHTML = "Please enter your name and a comment to be posted to the site.";
}
return false;
},
/**
* Ajax handler of moderation removal
*/
deleteComment: function() {
var __comment = this.parentNode.parentNode.parentNode;
__comment.getElementsByTagName('span')[0].innerHTML = "Removing...";
new Ajax.Request(this.href + '?ajax=1', {
onSuccess : function(response) {
// Clear our wee status message
__comment.getElementsByTagName('span')[0].innerHTML = "Removing...";
// Remove it using Scriptaculous
new Effect.Highlight(__comment, {
startcolor: '#cc9999' , endcolor: '#e9e9e9', duration: 0.5,
afterFinish : function () {
var commentList = __comment.parentNode;
commentList.removeChild(__comment);
if(!commentList.firstChild) {
$('CommentHolder').innerHTML = "<p id=\"NoComments\">No one has commented on this page yet.</p>";
}
}
} );
},
onFailure : function(response) {
alert(response.responseText);
}
});
return false;
},
/**
* Ajax handler of spam reporting
*/
reportSpam: function() {
var __comment = this.parentNode.parentNode.parentNode.parentNode;
__comment.getElementsByTagName('span')[0].innerHTML = "Reporting spam...";
new Ajax.Request(this.href + '?ajax=1', {
onSuccess : function(response) {
if(response.responseText != '') {
// Load the response into the <li>
__comment.innerHTML = response.responseText;
Behaviour.apply(__comment);
// Flash it using Scriptaculous
new Effect.Highlight(__comment, { endcolor: '#cc9999' } );
__comment.className = 'spam';
} else {
new Effect.Highlight(__comment, {
startcolor: '#cc9999' , endcolor: '#e9e9e9', duration: 0.5,
afterFinish : function() {
var commentList = __comment.parentNode;
commentList.removeChild(__comment);
if(!commentList.firstChild) {
$('CommentHolder').innerHTML = "<p id=\"NoComments\">No one has commented on this page yet.</p>";
}
}
} );
}
},
onFailure : function(response) {
alert(response.responseText);
}
});
return false;
},
/**
* Ajax handler of ham reporting
*/
reportHam: function() {
var __comment = this.parentNode.parentNode.parentNode.parentNode;
__comment.getElementsByTagName('span')[0].innerHTML = "Reporting as not spam...";
new Ajax.Request(this.href + '?ajax=1', {
onSuccess : function(response) {
// Load the response into the <li>
__comment.innerHTML = response.responseText;
Behaviour.apply(__comment);
// Flash it using Scriptaculous
new Effect.Highlight(__comment, { endcolor: '#e9e9e9' } );
__comment.className = 'notspam';
},
onFailure : function(response) {
alert(response.responseText);
}
});
return false;
},
/**
* Ajax handler of ham reporting
*/
approveComment: function() {
var __comment = this.parentNode.parentNode.parentNode.parentNode;
__comment.getElementsByTagName('span')[0].innerHTML = "Marking comment as approved...";
new Ajax.Request(this.href + '?ajax=1', {
onSuccess : function(response) {
// Load the response into the <li>
__comment.innerHTML = response.responseText;
Behaviour.apply(__comment);
// Flash it using Scriptaculous
new Effect.Highlight(__comment, { endcolor: '#e9e9e9' } );
__comment.className = 'notspam';
},
onFailure : function(response) {
alert(response.responseText);
}
});
return false;
}
}
PageCommentInterface.applyTo("#PageComments_holder");
function loadSpamQuestion(response) {
var spamQuestionDiv = $('Math');
var mathLabel = spamQuestionDiv.getElementsByTagName('label')[0];
mathLabel.innerHTML = response.responseText;
var mathQuestion = spamQuestionDiv.getElementsByTagName('input')[0];
mathQuestion.value = '';
}

View File

@ -0,0 +1,65 @@
<div id="PageComments_holder" class="typography">
<h4><% _t('POSTCOM','Post your comment') %></h4>
<% if PostCommentForm %>
<% if CanPostComment %>
$PostCommentForm
<% else %>
<p><% _t('COMMENTLOGINERROR', 'You cannot post comments until you have logged in') %><% if PostingRequiresPermission %>,<% _t('COMMENTPERMISSIONERROR', 'and that you have an appropriate permission level') %><% end_if %>.
<a href="Security/login?BackURL={$Page.Link}" title="Login to post a comment"><% _t('COMMENTPOSTLOGIN', 'Login Here') %></a>.
</p>
<% end_if %>
<% else %>
<p><% _t('COMMENTSDISABLED', 'Posting comments has been disabled') %>.</p>
<% end_if %>
<h4><% _t('COMMENTS','Comments') %></h4>
<div id="CommentHolder">
<% if Comments %>
<ul id="PageComments">
<% control Comments %>
<li class="$EvenOdd<% if FirstLast %> $FirstLast <% end_if %> $SpamClass">
<% include PageCommentInterface_singlecomment %>
</li>
<% end_control %>
</ul>
<% if Comments.MoreThanOnePage %>
<div id="PageCommentsPagination">
<p>
<% if Comments.PrevLink %>
<a href="$Comments.PrevLink">&laquo; <% _t('PREV','previous') %></a>
<% end_if %>
<% if Comments.Pages %>
<% control Comments.Pages %>
<% if CurrentBool %>
<strong>$PageNum</strong>
<% else %>
<a href="$Link">$PageNum</a>
<% end_if %>
<% end_control %>
<% end_if %>
<% if Comments.NextLink %>
<a href="$Comments.NextLink"><% _t('NEXT','next') %> &raquo;</a>
<% end_if %>
</p>
</div>
<% end_if %>
<% else %>
<p id="NoComments"><% _t('NOCOMMENTSYET','No one has commented on this page yet.') %></p>
<% end_if %>
</div>
<% if DeleteAllLink %>
<p id="DeleteComments"><a href="$DeleteAllLink">
<% _t('PageCommentInterface.DELETEALLCOMMENTS','Delete all comments on this page') %>
</a></p>
<% end_if %>
<p id="CommentsRSSFeed">
<a class="commentrss" href="$CommentRssLink"><% _t('RSSFEEDCOMMENTS', 'RSS feed for comments on this page') %></a> |
<a href="PageComment/rss" class="commentrss" title="<% _t('RSSVIEWALLCOMMENTS', 'View all Comments') %>"><% _t('RSSFEEDALLCOMMENTS', 'RSS feed for all comments') %></a>
</p>
</div>

View File

@ -0,0 +1,29 @@
<p class="comment" id="PageComment_$ID">
<% if bbCodeEnabled %>
$ParsedBBCode
<% else %>
$Comment.XML
<% end_if %>
</p>
<p class="info">
<% if CommenterURL %>
<% _t('PBY','Posted by') %> <a href="$CommenterURL.ATT">$Name.XML</a>, $Created.Nice ($Created.Ago)
<% else %>
<% _t('PBY','Posted by') %> $Name.XML, $Created.Nice ($Created.Ago)
<% end_if %>
</p>
<ul class="actionLinks">
<% if ApproveLink %>
<li><a href="$ApproveLink" class="approvelink"><% _t('APPROVE', 'approve this comment') %></a></li>
<% end_if %>
<% if SpamLink %>
<li><a href="$SpamLink" class="spamlink"><% _t('ISSPAM','this comment is spam') %></a></li>
<% end_if %>
<% if HamLink %>
<li><a href="$HamLink" class="hamlink"><% _t('ISNTSPAM','this comment is not spam') %></a></li>
<% end_if %>
<% if DeleteLink %>
<li class="last"><a href="$DeleteLink" class="deletelink"><% _t('REMCOM','remove this comment') %></a></li>
<% end_if %>
</ul>

101
tests/CommentsTest.php Normal file
View File

@ -0,0 +1,101 @@
<?php
/**
* @package comments
*/
class CommentsTest extends FunctionalTest {
static $fixture_file = 'comments/tests/CommentsTest.yml';
function testCanView() {
$visitor = $this->objFromFixture('Member', 'visitor');
$admin = $this->objFromFixture('Member', 'commentadmin');
$comment = $this->objFromFixture('PageComment', 'firstComA');
$this->assertTrue($comment->canView($visitor),
'Unauthenticated members can view comments associated to a page 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('PageComment', 'disabledCom');
$this->assertFalse($disabledComment->canView($visitor),
'Unauthenticated members can not view comments associated to a page with ProvideComments=0'
);
$this->assertTrue($disabledComment->canView($admin),
'Admins with CMS_ACCESS_CommentAdmin permissions can view comments associated to a page with ProvideComments=0'
);
}
function testCanEdit() {
$visitor = $this->objFromFixture('Member', 'visitor');
$admin = $this->objFromFixture('Member', 'commentadmin');
$comment = $this->objFromFixture('PageComment', 'firstComA');
$this->assertFalse($comment->canEdit($visitor));
$this->assertTrue($comment->canEdit($admin));
}
function testCanDelete() {
$visitor = $this->objFromFixture('Member', 'visitor');
$admin = $this->objFromFixture('Member', 'commentadmin');
$comment = $this->objFromFixture('PageComment', 'firstComA');
$this->assertFalse($comment->canEdit($visitor));
$this->assertTrue($comment->canEdit($admin));
}
function testDeleteComment() {
$firstPage = $this->objFromFixture('Page', 'first');
$this->autoFollowRedirection = false;
$this->logInAs('commentadmin');
$firstComment = $this->objFromFixture('PageComment', 'firstComA');
$firstCommentID = $firstComment->ID;
Director::test($firstPage->RelativeLink(), null, $this->session());
Director::test('PageComment/deletecomment/'.$firstComment->ID, null, $this->session());
$this->assertFalse(DataObject::get_by_id('PageComment', $firstCommentID));
}
function testDeleteAllCommentsOnPage() {
$second = $this->objFromFixture('Page', 'second');
$this->autoFollowRedirection = false;
$this->logInAs('commentadmin');
Director::test('second-page', null, $this->session());
Director::test('PageComment/deleteallcomments?pageid='.$second->ID,
null, $this->session());
Director::test('second-page', null, $this->session());
$secondComments = DataObject::get('PageComment', '"ParentID" = '.$second->ID);
$this->assertNull($secondComments);
$first = $this->objFromFixture('Page', 'first');
$firstComments = DataObject::get('PageComment', '"ParentID" = '.$first->ID);
$this->assertNotNull($firstComments);
$third = $this->objFromFixture('Page', 'third');
$thirdComments = DataObject::get('PageComment', '"ParentID" = '.$third->ID);
$this->assertEquals($thirdComments->Count(), 3);
}
function testCommenterURLWrite() {
$comment = new PageComment();
// 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',
);
$url = '://example.com';
foreach($protocols as $protocol) {
$comment->CommenterURL = $protocol . $url;
// The protocol should stay as if, assuming it is valid
$comment->write();
$this->assertEquals($comment->CommenterURL, $protocol . $url, $protocol . ':// is a valid protocol');
}
}
}

62
tests/CommentsTest.yml Normal file
View File

@ -0,0 +1,62 @@
Member:
commentadmin:
FirstName: admin
visitor:
FirstName: visitor
Group:
commentadmins:
Title: Admin
Members: =>Member.commentadmin
Permission:
admin:
Code: CMS_ACCESS_CommentAdmin
Group: =>Group.commentadmins
Page:
first:
Title: First page
URLSegment: first-page
ProvideComments: 1
second:
Title: Second page
URLSegment: second-page
ProvideComments: 1
third:
Title: Third page
URLSegment:third-page
ProvideComments: 1
pageNoComments:
Title: No comments
URLSegment: no-comments
ProvideComments: 0
PageComment:
firstComA:
ParentID: =>Page.first
Name: FA
Comment: textFA
secondComA:
ParentID: =>Page.second
Name: SA
Comment: textSA
secondComB:
ParentID: =>Page.second
Name: SB
Comment: textSB
thirdComA:
ParentID: =>Page.third
Name: TA
Comment: textTA
thirdComB:
ParentID: =>Page.third
Name: TB
Comment: textTB
thirdComC:
ParentID: =>Page.third
Name: TC
Comment: textTC
disabledCom:
ParentID: =>Page.pageNoComments
Name: Disabled