Better XSS Protection via hashed token

Allows moderation links to be generated for users other than the currently logged in user, as it doesn't rely on the current session.
This commit is contained in:
Damian Mooyman 2015-03-27 17:40:00 +13:00
parent 2a20037e49
commit 9087261654
6 changed files with 534 additions and 296 deletions

View File

@ -6,13 +6,13 @@ php:
env: env:
- DB=MYSQL CORE_RELEASE=3.1 - DB=MYSQL CORE_RELEASE=3.1
- DB=MYSQL CORE_RELEASE=master - DB=MYSQL CORE_RELEASE=3
- DB=PGSQL CORE_RELEASE=3.1 - DB=PGSQL CORE_RELEASE=3.1
matrix: matrix:
include: include:
- php: 5.4 - php: 5.4
env: DB=MYSQL CORE_RELEASE=master env: DB=MYSQL CORE_RELEASE=3
before_script: before_script:
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support - git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
@ -20,4 +20,4 @@ before_script:
- cd ~/builds/ss - cd ~/builds/ss
script: script:
- phpunit comments/tests/ - vendor/bin/phpunit comments/tests/

View File

@ -130,80 +130,70 @@ class CommentingController extends Controller {
* Deletes a given {@link Comment} via the URL. * Deletes a given {@link Comment} via the URL.
*/ */
public function delete() { public function delete() {
if(!$this->checkSecurityToken($this->request)) { $comment = $this->getComment();
return $this->httpError(400); if(!$comment) return $this->httpError(404);
} if(!$comment->canDelete()) return $this->httpError(403);
if(!$comment->getSecurityToken()->checkRequest($this->request)) return $this->httpError(400);
if(($comment = $this->getComment()) && $comment->canDelete()) { $comment->delete();
$comment->delete();
return ($this->request->isAjax()) ? true : $this->redirectBack();
}
return $this->httpError(404); return $this->request->isAjax()
? true
: $this->redirectBack();
} }
/** /**
* Marks a given {@link Comment} as spam. Removes the comment from display * Marks a given {@link Comment} as spam. Removes the comment from display
*/ */
public function spam() { public function spam() {
if(!$this->checkSecurityToken($this->request)) {
return $this->httpError(400);
}
$comment = $this->getComment(); $comment = $this->getComment();
if(!$comment) return $this->httpError(404);
if(!$comment->canEdit()) return $this->httpError(403);
if(!$comment->getSecurityToken()->checkRequest($this->request)) return $this->httpError(400);
$comment->IsSpam = true;
$comment->Moderated = true;
$comment->write();
if(($comment = $this->getComment()) && $comment->canEdit()) { return $this->request->isAjax()
$comment->IsSpam = true; ? $comment->renderWith('CommentsInterface_singlecomment')
$comment->Moderated = true; : $this->redirectBack();
$comment->write();
return ($this->request->isAjax()) ? $comment->renderWith('CommentsInterface_singlecomment') : $this->redirectBack();
}
return $this->httpError(404);
} }
/** /**
* Marks a given {@link Comment} as ham (not spam). * Marks a given {@link Comment} as ham (not spam).
*/ */
public function ham() { public function ham() {
if(!$this->checkSecurityToken($this->request)) {
return $this->httpError(400);
}
$comment = $this->getComment(); $comment = $this->getComment();
if(!$comment) return $this->httpError(404);
if(!$comment->canEdit()) return $this->httpError(403);
if(!$comment->getSecurityToken()->checkRequest($this->request)) return $this->httpError(400);
$comment->IsSpam = false;
$comment->Moderated = true;
$comment->write();
if(($comment = $this->getComment()) && $comment->canEdit()) { return $this->request->isAjax()
$comment->IsSpam = false; ? $comment->renderWith('CommentsInterface_singlecomment')
$comment->Moderated = true; : $this->redirectBack();
$comment->write();
return ($this->request->isAjax()) ? $comment->renderWith('CommentsInterface_singlecomment') : $this->redirectBack();
}
return $this->httpError(404);
} }
/** /**
* Marks a given {@link Comment} as approved. * Marks a given {@link Comment} as approved.
*/ */
public function approve() { public function approve() {
if(!$this->checkSecurityToken($this->request)) {
return $this->httpError(400);
}
$comment = $this->getComment(); $comment = $this->getComment();
if(!$comment) return $this->httpError(404);
if(!$comment->canEdit()) return $this->httpError(403);
if(!$comment->getSecurityToken()->checkRequest($this->request)) return $this->httpError(400);
if(($comment = $this->getComment()) && $comment->canEdit()) { $comment->IsSpam = false;
$comment->IsSpam = false; $comment->Moderated = true;
$comment->Moderated = true; $comment->write();
$comment->write();
return ($this->request->isAjax()) ? $comment->renderWith('CommentsInterface_singlecomment') : $this->redirectBack();
}
return $this->httpError(404); return $this->request->isAjax()
? $comment->renderWith('CommentsInterface_singlecomment')
: $this->redirectBack();
} }
/** /**
@ -226,20 +216,6 @@ class CommentingController extends Controller {
return false; return false;
} }
/**
* Checks the security token given with the URL to prevent CSRF attacks
* against administrators allowing users to hijack comment moderation.
*
* @param SS_HTTPRequest
*
* @return boolean
*/
public function checkSecurityToken($req) {
$token = SecurityToken::inst();
return $token->checkRequest($req);
}
/** /**
* Post a comment form * Post a comment form
* *

View File

@ -2,7 +2,18 @@
/** /**
* Represents a single comment object. * 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
* @method Member Author()
* @package comments * @package comments
*/ */
class Comment extends DataObject { class Comment extends DataObject {
@ -16,7 +27,8 @@ class Comment extends DataObject {
"Moderated" => "Boolean", "Moderated" => "Boolean",
"IsSpam" => "Boolean", "IsSpam" => "Boolean",
"ParentID" => "Int", "ParentID" => "Int",
'AllowHtml' => "Boolean" 'AllowHtml' => "Boolean",
"SecretToken" => "Varchar(255)",
); );
private static $has_one = array( private static $has_one = array(
@ -36,7 +48,12 @@ class Comment extends DataObject {
private static $casting = array( private static $casting = array(
'AuthorName' => 'Varchar', 'AuthorName' => 'Varchar',
'RSSName' => 'Varchar' 'RSSName' => 'Varchar',
'DeleteLink' => 'Varchar',
'SpamLink' => 'Varchar',
'HamLink' => 'Varchar',
'ApproveLink' => 'Varchar',
'Permalink' => 'Varchar',
); );
private static $searchable_fields = array( private static $searchable_fields = array(
@ -64,6 +81,13 @@ class Comment extends DataObject {
$this->Comment = $this->purifyHtml($this->Comment); $this->Comment = $this->purifyHtml($this->Comment);
} }
} }
/**
* @return Comment_SecurityToken
*/
public function getSecurityToken() {
return Injector::inst()->createWithArgs('Comment_SecurityToken', array($this));
}
/** /**
* Migrates the old {@link PageComment} objects to {@link Comment} * Migrates the old {@link PageComment} objects to {@link Comment}
@ -100,9 +124,9 @@ class Comment extends DataObject {
* @return string link to this comment. * @return string link to this comment.
*/ */
public function Link($action = "") { public function Link($action = "") {
if($parent = $this->getParent()){ if($parent = $this->getParent()){
return $parent->Link($action) . '#' . $this->Permalink(); return $parent->Link($action) . '#' . $this->Permalink();
} }
} }
/** /**
@ -283,55 +307,70 @@ class Comment extends DataObject {
} }
/** /**
* 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
* @return string * @return string
*/ */
public function DeleteLink() { protected function actionLink($action, $member = null) {
if($this->canDelete()) { if(!$member) $member = Member::currentUser();
$token = SecurityToken::inst(); if(!$member) return false;
return DBField::create_field("Varchar", Director::absoluteURL($token->addToUrl(sprintf( $url = Controller::join_links(
"CommentingController/delete/%s", (int) $this->ID Director::baseURL(),
)))); "CommentingController",
} $action,
$this->ID
);
// Limit access for this user
$token = $this->getSecurityToken();
return $token->addToUrl($url, $member);
}
/**
* Link to delete this comment
*
* @param Member $member
* @return string
*/
public function DeleteLink($member = null) {
if(!$this->canDelete($member)) return false;
return $this->actionLink('delete', $member);
} }
/** /**
* Link to mark as spam
*
* @param Member $member
* @return string * @return string
*/ */
public function SpamLink() { public function SpamLink($member = null) {
if($this->canEdit() && !$this->IsSpam) { if(!$this->canEdit($member) || $this->IsSpam) return false;
$token = SecurityToken::inst(); return $this->actionLink('spam', $member);
return DBField::create_field("Varchar", Director::absoluteURL($token->addToUrl(sprintf(
"CommentingController/spam/%s", (int) $this->ID
))));
}
} }
/** /**
* Link to mark as not-spam (ham)
*
* @param Member $member
* @return string * @return string
*/ */
public function HamLink() { public function HamLink($member = null) {
if($this->canEdit() && $this->IsSpam) { if(!$this->canEdit($member) || !$this->IsSpam) return false;
$token = SecurityToken::inst(); return $this->actionLink('ham', $member);
return DBField::create_field("Varchar", Director::absoluteURL($token->addToUrl(sprintf(
"CommentingController/ham/%s", (int) $this->ID
))));
}
} }
/** /**
* Link to approve this comment
*
* @param Member $member
* @return string * @return string
*/ */
public function ApproveLink() { public function ApproveLink($member = null) {
if($this->canEdit() && !$this->Moderated) { if(!$this->canEdit($member) || $this->Moderated) return false;
$token = SecurityToken::inst(); return $this->actionLink('approve', $member);
return DBField::create_field("Varchar", Director::absoluteURL($token->addToUrl(sprintf(
"CommentingController/approve/%s", (int) $this->ID
))));
}
} }
/** /**
@ -367,9 +406,8 @@ class Comment extends DataObject {
*/ */
public function getCMSFields() { public function getCMSFields() {
$fields = parent::getCMSFields(); $fields = parent::getCMSFields();
$parent = $this->getParent()->ID;
$hidden = array('ParentID', 'AuthorID', 'BaseClass', 'AllowHtml'); $hidden = array('ParentID', 'AuthorID', 'BaseClass', 'AllowHtml', 'SecretToken');
foreach($hidden as $private) { foreach($hidden as $private) {
$fields->removeByName($private); $fields->removeByName($private);
@ -419,3 +457,136 @@ class Comment extends DataObject {
return $gravatar; return $gravatar;
} }
} }
/**
* Provides the ability to generate cryptographically secure tokens for comment moderation
*/
class Comment_SecurityToken {
private $secret = null;
/**
* @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
*
* @param string $salt
* @return string
*/
protected function getToken($salt) {
return function_exists('hash_pbkdf2')
? hash_pbkdf2('sha256', $this->secret, $salt, 1000, 30)
: $this->hash_pbkdf2('sha256', $this->secret, $salt, 100, 30);
}
/**
* 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 controller action.
*
* @param string $salt Single use salt
* @param type $member Member object
* @return string Generated salt specific to this member
*/
protected function memberSalt($salt, $member) {
// Fallback to salting with ID in case the member has not one set
return $salt . ($member->Salt ?: $member->ID);
}
/**
* @param string $url Comment action URL
* @param Member $member Member to restrict access to this action to
* @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);
return Controller::join_links(
$url,
sprintf(
'?t=%s&s=%s',
urlencode($token),
urlencode($salt)
)
);
}
/**
* @param SS_HTTPRequest $request
* @return boolean
*/
public function checkRequest($request) {
$member = Member::currentUser();
if(!$member) return false;
$salt = $request->getVar('s');
$memberSalt = $this->memberSalt($salt, $member);
$token = $this->getToken($memberSalt);
// Ensure tokens match
return $token === $request->getVar('t');
}
/**
* Generates new random key
*
* @param integer $length
* @return string
*/
protected function generate($length = null) {
$generator = new RandomGenerator();
$result = $generator->randomToken('sha256');
if($length !== null) return substr ($result, 0, $length);
return $result;
}
/*-----------------------------------------------------------
* PBKDF2 Implementation (described in RFC 2898) from php.net
*-----------------------------------------------------------
* @param string a hash algorithm
* @param string p password
* @param string s salt
* @param int c iteration count (use 1000 or higher)
* @param int kl derived key length
* @param int st start position of result
*
* @return string derived key
*/
private function hash_pbkdf2 ($a, $p, $s, $c, $kl, $st=0) {
$kb = $st+$kl; // Key blocks to compute
$dk = ''; // Derived key
// Create key
for ($block=1; $block<=$kb; $block++) {
// Initial hash for this block
$ib = $h = hash_hmac($a, $s . pack('N', $block), $p, true);
// Perform block iterations
for ($i=1; $i<$c; $i++) {
// XOR each iterate
$ib ^= ($h = hash_hmac($a, $h, $p, true));
}
$dk .= $ib; // Append iterated block
}
// Return derived key of correct length
return substr($dk, $st, $kl);
}
}

View File

@ -14,6 +14,9 @@
"suggest": { "suggest": {
"ezyang/htmlpurifier": "4.*" "ezyang/htmlpurifier": "4.*"
}, },
"require-dev": {
"phpunit/PHPUnit": "~3.7@stable"
},
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.2.x-dev" "dev-master": "1.2.x-dev"

View File

@ -90,82 +90,168 @@ class CommentsTest extends FunctionalTest {
} }
public function testDeleteComment() { public function testDeleteComment() {
// Test anonymous user
if($member = Member::currentUser()) $member->logOut();
$comment = $this->objFromFixture('Comment', 'firstComA'); $comment = $this->objFromFixture('Comment', 'firstComA');
$this->assertNull($comment->DeleteLink(), 'No permission to see delete link'); $commentID = $comment->ID;
$this->assertFalse($comment->DeleteLink(), 'No permission to see delete link');
$delete = $this->get('CommentingController/delete/'.$comment->ID); $delete = $this->get('CommentingController/delete/'.$comment->ID);
$check = DataObject::get_by_id('Comment', $comment->ID); $this->assertEquals(403, $delete->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID);
$this->assertTrue($check && $check->exists()); $this->assertTrue($check && $check->exists());
$firstPage = $this->objFromFixture('CommentableItem', 'first'); // Test non-authenticated user
$this->autoFollowRedirection = false; $this->logInAs('visitor');
$this->assertFalse($comment->DeleteLink(), 'No permission to see delete link');
// Test authenticated user
$this->logInAs('commentadmin'); $this->logInAs('commentadmin');
$comment = $this->objFromFixture('Comment', 'firstComA');
$firstComment = $this->objFromFixture('Comment', 'firstComA'); $commentID = $comment->ID;
$firstCommentID = $firstComment->ID; $adminComment1Link = $comment->DeleteLink();
Director::test($firstPage->RelativeLink(), null, $this->session()); $this->assertContains('CommentingController/delete/'.$commentID.'?t=', $adminComment1Link);
$delete = $this->get('CommentingController/delete/'.$firstComment->ID);
$check = DataObject::get_by_id('Comment', $firstCommentID); // Test that this link can't be shared / XSS exploited
$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);
$this->autoFollowRedirection = false;
$delete = $this->get($adminComment2Link);
$this->assertEquals(302, $delete->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID);
$this->assertFalse($check && $check->exists()); $this->assertFalse($check && $check->exists());
} }
public function testSpamComment() { public function testSpamComment() {
// Test anonymous user
if($member = Member::currentUser()) $member->logOut();
$comment = $this->objFromFixture('Comment', 'firstComA'); $comment = $this->objFromFixture('Comment', 'firstComA');
$this->assertNull($comment->SpamLink(), 'No permission to see mark as spam link'); $commentID = $comment->ID;
$this->assertFalse($comment->SpamLink(), 'No permission to see mark as spam link');
$spam = $this->get('CommentingController/spam/'.$comment->ID); $spam = $this->get('CommentingController/spam/'.$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');
// Test non-authenticated user
$this->logInAs('visitor');
$this->assertFalse($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
$this->logInAs('commentadmin2');
$spam = $this->get($adminComment1Link);
$this->assertEquals(400, $spam->getStatusCode());
$check = DataObject::get_by_id('Comment', $comment->ID); $check = DataObject::get_by_id('Comment', $comment->ID);
$this->assertEquals(0, $check->IsSpam, 'No permission to mark as spam'); $this->assertEquals(0, $check->IsSpam, 'No permission to mark as spam');
// Test that this other admin can spam the comment with their own link
$adminComment2Link = $comment->SpamLink();
$this->assertNotEquals($adminComment2Link, $adminComment1Link);
$this->autoFollowRedirection = false; $this->autoFollowRedirection = false;
$this->logInAs('commentadmin'); $spam = $this->get($adminComment2Link);
$this->assertEquals(302, $spam->getStatusCode());
$this->assertContains('CommentingController/spam/'. $comment->ID, $comment->SpamLink()->getValue()); $check = DataObject::get_by_id('Comment', $commentID);
$spam = $this->get('CommentingController/spam/'.$comment->ID);
$check = DataObject::get_by_id('Comment', $comment->ID);
$this->assertEquals(1, $check->IsSpam); $this->assertEquals(1, $check->IsSpam);
$this->assertNull($check->SpamLink()); // Cannot re-spam spammed comment
$this->assertFalse($check->SpamLink());
} }
public function testHamComment() { public function testHamComment() {
// Test anonymous user
if($member = Member::currentUser()) $member->logOut();
$comment = $this->objFromFixture('Comment', 'secondComC'); $comment = $this->objFromFixture('Comment', 'secondComC');
$this->assertNull($comment->HamLink(), 'No permission to see mark as ham link'); $commentID = $comment->ID;
$this->assertFalse($comment->HamLink(), 'No permission to see mark as ham link');
$ham = $this->get('CommentingController/ham/'.$comment->ID); $ham = $this->get('CommentingController/ham/'.$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->assertFalse($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
$this->logInAs('commentadmin2');
$ham = $this->get($adminComment1Link);
$this->assertEquals(400, $ham->getStatusCode());
$check = DataObject::get_by_id('Comment', $comment->ID); $check = DataObject::get_by_id('Comment', $comment->ID);
$this->assertEquals(1, $check->IsSpam, 'No permission to mark as ham'); $this->assertEquals(1, $check->IsSpam, 'No permission to mark as ham');
// Test that this other admin can ham the comment with their own link
$adminComment2Link = $comment->HamLink();
$this->assertNotEquals($adminComment2Link, $adminComment1Link);
$this->autoFollowRedirection = false; $this->autoFollowRedirection = false;
$this->logInAs('commentadmin'); $ham = $this->get($adminComment2Link);
$this->assertEquals(302, $ham->getStatusCode());
$this->assertContains('CommentingController/ham/'. $comment->ID, $comment->HamLink()->getValue()); $check = DataObject::get_by_id('Comment', $commentID);
$ham = $this->get('CommentingController/ham/'.$comment->ID);
$check = DataObject::get_by_id('Comment', $comment->ID);
$this->assertEquals(0, $check->IsSpam); $this->assertEquals(0, $check->IsSpam);
$this->assertNull($check->HamLink()); // Cannot re-ham hammed comment
$this->assertFalse($check->HamLink());
} }
public function testApproveComment() { public function testApproveComment() {
// Test anonymous user
if($member = Member::currentUser()) $member->logOut();
$comment = $this->objFromFixture('Comment', 'secondComB'); $comment = $this->objFromFixture('Comment', 'secondComB');
$this->assertNull($comment->ApproveLink(), 'No permission to see mark as approved link'); $commentID = $comment->ID;
$ham = $this->get('CommentingController/approve/'.$comment->ID); $this->assertFalse($comment->ApproveLink(), 'No permission to see approve link');
$approve = $this->get('CommentingController/approve/'.$comment->ID);
$this->assertEquals(403, $approve->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID);
$this->assertEquals(0, $check->Moderated, 'No permission to approve');
$check = DataObject::get_by_id('Comment', $comment->ID); // Test non-authenticated user
$this->assertEquals(0, $check->Moderated, 'No permission to mark as approved'); $this->logInAs('visitor');
$this->assertFalse($comment->ApproveLink(), 'No permission to see approve link');
$this->autoFollowRedirection = false; // Test authenticated user
$this->logInAs('commentadmin'); $this->logInAs('commentadmin');
$comment = $this->objFromFixture('Comment', 'secondComB');
$commentID = $comment->ID;
$adminComment1Link = $comment->ApproveLink();
$this->assertContains('CommentingController/approve/'.$commentID.'?t=', $adminComment1Link);
$this->assertContains('CommentingController/approve/'. $comment->ID, $comment->ApproveLink()->getValue()); // Test that this link can't be shared / XSS exploited
$this->logInAs('commentadmin2');
$ham = $this->get('CommentingController/approve/'.$comment->ID); $approve = $this->get($adminComment1Link);
$this->assertEquals(400, $approve->getStatusCode());
$check = DataObject::get_by_id('Comment', $comment->ID); $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->autoFollowRedirection = false;
$approve = $this->get($adminComment2Link);
$this->assertEquals(302, $approve->getStatusCode());
$check = DataObject::get_by_id('Comment', $commentID);
$this->assertEquals(1, $check->Moderated); $this->assertEquals(1, $check->Moderated);
$this->assertNull($check->ApproveLink()); // Cannot re-approve approved comment
$this->assertFalse($check->ApproveLink());
} }
public function testCommenterURLWrite() { public function testCommenterURLWrite() {

View File

@ -1,167 +1,169 @@
Member: Member:
commentadmin: commentadmin:
FirstName: admin FirstName: admin
visitor: commentadmin2:
FirstName: visitor FirstName: admin2
visitor:
FirstName: visitor
Group: Group:
commentadmins: commentadmins:
Title: Admin Title: Admin
Members: =>Member.commentadmin Members: =>Member.commentadmin, =>Member.commentadmin2
Permission: Permission:
admin: admin:
Code: CMS_ACCESS_CommentAdmin Code: CMS_ACCESS_CommentAdmin
Group: =>Group.commentadmins Group: =>Group.commentadmins
CommentableItem: CommentableItem:
first: first:
Title: First Title: First
ProvideComments: 1 ProvideComments: 1
second: second:
Title: Second Title: Second
ProvideComments: 1 ProvideComments: 1
third: third:
Title: Third Title: Third
ProvideComments: 1 ProvideComments: 1
nocomments: nocomments:
Title: No comments Title: No comments
ProvideComments: 0 ProvideComments: 0
spammed: spammed:
ProvideComments: 1 ProvideComments: 1
Title: spammed Title: spammed
Comment: Comment:
firstComA: firstComA:
ParentID: =>CommentableItem.first ParentID: =>CommentableItem.first
Name: FA Name: FA
Comment: textFA Comment: textFA
BaseClass: CommentableItem BaseClass: CommentableItem
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
secondComA: secondComA:
ParentID: =>CommentableItem.second ParentID: =>CommentableItem.second
Name: SA Name: SA
Comment: textSA Comment: textSA
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
secondComB: secondComB:
ParentID: =>CommentableItem.second ParentID: =>CommentableItem.second
Name: SB Name: SB
Comment: textSB Comment: textSB
Moderated: 0 Moderated: 0
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
secondComC: secondComC:
ParentID: =>CommentableItem.second ParentID: =>CommentableItem.second
Name: SB Name: SB
Comment: textSB Comment: textSB
Moderated: 1 Moderated: 1
IsSpam: 1 IsSpam: 1
BaseClass: CommentableItem BaseClass: CommentableItem
thirdComA: thirdComA:
ParentID: =>CommentableItem.third ParentID: =>CommentableItem.third
Name: TA Name: TA
Comment: textTA Comment: textTA
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
thirdComB: thirdComB:
ParentID: =>CommentableItem.third ParentID: =>CommentableItem.third
Name: TB Name: TB
Comment: textTB Comment: textTB
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
thirdComC: thirdComC:
ParentID: =>CommentableItem.third ParentID: =>CommentableItem.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
thirdComD: thirdComD:
ParentID: =>CommentableItem.third ParentID: =>CommentableItem.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
BaseClass: CommentableItem BaseClass: CommentableItem
thirdComE: thirdComE:
ParentID: =>CommentableItem.third ParentID: =>CommentableItem.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
BaseClass: CommentableItem BaseClass: CommentableItem
thirdComF: thirdComF:
ParentID: =>CommentableItem.third ParentID: =>CommentableItem.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
thirdComG: thirdComG:
ParentID: =>CommentableItem.third ParentID: =>CommentableItem.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
thirdComH: thirdComH:
ParentID: =>CommentableItem.third ParentID: =>CommentableItem.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
thirdComI: thirdComI:
ParentID: =>CommentableItem.third ParentID: =>CommentableItem.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
thirdComJ: thirdComJ:
ParentID: =>CommentableItem.third ParentID: =>CommentableItem.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
thirdComK: thirdComK:
ParentID: =>CommentableItem.third ParentID: =>CommentableItem.third
Name: TC Name: TC
Comment: textTC Comment: textTC
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
disabledCom: disabledCom:
ParentID: =>CommentableItem.nocomments ParentID: =>CommentableItem.nocomments
Name: Disabled Name: Disabled
Moderated: 0 Moderated: 0
IsSpam: 1 IsSpam: 1
BaseClass: CommentableItem BaseClass: CommentableItem
testCommentList1: testCommentList1:
ParentID: =>CommentableItem.spammed ParentID: =>CommentableItem.spammed
Name: Comment 1 Name: Comment 1
Moderated: 0 Moderated: 0
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
testCommentList2: testCommentList2:
ParentID: =>CommentableItem.spammed ParentID: =>CommentableItem.spammed
Name: Comment 2 Name: Comment 2
Moderated: 1 Moderated: 1
IsSpam: 1 IsSpam: 1
BaseClass: CommentableItem BaseClass: CommentableItem
testCommentList3: testCommentList3:
ParentID: =>CommentableItem.spammed ParentID: =>CommentableItem.spammed
Name: Comment 3 Name: Comment 3
Moderated: 1 Moderated: 1
IsSpam: 0 IsSpam: 0
BaseClass: CommentableItem BaseClass: CommentableItem
testCommentList4: testCommentList4:
ParentID: =>CommentableItem.spammed ParentID: =>CommentableItem.spammed
Name: Comment 4 Name: Comment 4
Moderated: 0 Moderated: 0
IsSpam: 1 IsSpam: 1
BaseClass: CommentableItem BaseClass: CommentableItem