silverstripe-comments/code/dataobjects/Comment.php

635 lines
15 KiB
PHP
Raw Normal View History

<?php
/**
* Represents a single comment object.
*
2015-04-13 05:41:18 +02:00
* @property string $Name
* @property string $Comment
* @property string $Email
* @property string $URL
* @property string $BaseClass
* @property boolean $Moderated
2015-04-13 05:41:18 +02:00
* @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 HasManyList ChildComments() List of child comments
* @method Member Author() Member object who created this comment
2015-04-13 05:41:18 +02:00
*
* @package comments
*/
class Comment extends DataObject {
2015-04-13 05:41:18 +02:00
/**
* @var array
*/
2013-04-11 14:23:03 +02:00
private static $db = array(
2015-04-13 05:41:18 +02:00
'Name' => 'Varchar(200)',
'Comment' => 'Text',
'Email' => 'Varchar(200)',
'URL' => 'Varchar(255)',
'BaseClass' => 'Varchar(200)',
'Moderated' => 'Boolean(0)',
'IsSpam' => 'Boolean(0)',
'ParentID' => 'Int',
'AllowHtml' => 'Boolean',
'SecretToken' => 'Varchar(255)',
);
2013-04-11 14:23:03 +02:00
private static $has_one = array(
2015-04-13 05:41:18 +02:00
'Author' => 'Member',
);
2015-04-13 05:41:18 +02:00
2013-04-11 14:23:03 +02:00
private static $default_sort = '"Created" DESC';
2015-04-13 05:41:18 +02:00
2013-04-11 14:23:03 +02:00
private static $defaults = array(
2015-04-13 05:41:18 +02:00
'Moderated' => 0,
'IsSpam' => 0,
);
2015-04-13 05:41:18 +02:00
2013-04-11 14:23:03 +02:00
private static $casting = array(
'Title' => 'Varchar',
'ParentTitle' => 'Varchar',
'ParentClassName' => 'Varchar',
'AuthorName' => 'Varchar',
'RSSName' => 'Varchar',
'DeleteLink' => 'Varchar',
'SpamLink' => 'Varchar',
'HamLink' => 'Varchar',
'ApproveLink' => 'Varchar',
'Permalink' => 'Varchar',
);
2013-04-11 14:23:03 +02:00
private static $searchable_fields = array(
'Name',
'Email',
'Comment',
'Created',
'BaseClass',
);
2015-04-13 05:41:18 +02:00
2013-04-11 14:23:03 +02:00
private static $summary_fields = array(
'Name' => 'Submitted By',
'Email' => 'Email',
'Comment' => 'Comment',
'Created' => 'Date Posted',
2015-04-13 05:41:18 +02:00
'ParentTitle' => 'Post',
'IsSpam' => 'Is Spam',
);
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);
}
}
/**
* @return Comment_SecurityToken
*/
public function getSecurityToken() {
return Injector::inst()->createWithArgs('Comment_SecurityToken', array($this));
}
2015-04-13 05:41:18 +02:00
/**
* Migrates the old {@link PageComment} objects to {@link Comment}
*/
public function requireDefaultRecords() {
parent::requireDefaultRecords();
2015-04-13 05:41:18 +02:00
if(DB::getConn()->hasTable('PageComment')) {
2015-04-13 05:41:18 +02:00
$comments = DB::query('SELECT * FROM "PageComment"');
if($comments) {
2010-12-14 01:26:29 +01:00
while($pageComment = $comments->nextRecord()) {
// create a new comment from the older page comment
$comment = new Comment();
$comment->update($pageComment);
2015-04-13 05:41:18 +02:00
// set the variables which have changed
$comment->BaseClass = 'SiteTree';
2015-04-13 05:41:18 +02:00
$comment->URL = (isset($pageComment['CommenterURL'])) ? $pageComment['CommenterURL'] : '';
if((int) $pageComment['NeedsModeration'] == 0) $comment->Moderated = true;
$comment->write();
}
}
2015-04-13 05:41:18 +02:00
DB::alteration_message('Migrated PageComment to Comment', 'changed');
DB::getConn()->dontRequireTable('PageComment');
}
}
2015-04-13 05:41:18 +02:00
/**
* Return a link to this comment
*
2015-04-13 05:41:18 +02:00
* @param string $action
*
* @return string link to this comment.
*/
2015-04-13 05:41:18 +02:00
public function Link($action = '') {
if($parent = $this->getParent()) {
return $parent->Link($action) . '#' . $this->Permalink();
}
}
2015-04-13 05:41:18 +02:00
/**
* 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;
}
2015-04-13 05:41:18 +02:00
/**
* Translate the form field labels for the CMS administration
*
* @param boolean $includerelations
2015-04-13 05:41:18 +02:00
*
* @return array
*/
public function fieldLabels($includerelations = true) {
$labels = parent::fieldLabels($includerelations);
2015-04-13 05:41:18 +02:00
$labels['Name'] = _t('Comment.NAME', 'Author Name');
$labels['Comment'] = _t('Comment.COMMENT', 'Comment');
$labels['Email'] = _t('Comment.EMAIL', 'Email');
$labels['URL'] = _t('Comment.URL', 'URL');
$labels['IsSpam'] = _t('Comment.ISSPAM', 'Spam?');
$labels['Moderated'] = _t('Comment.MODERATED', 'Moderated?');
$labels['ParentTitle'] = _t('Comment.PARENTTITLE', 'Parent');
$labels['Created'] = _t('Comment.CREATED', 'Date posted');
2015-04-13 05:41:18 +02:00
return $labels;
}
/**
* Get the commenting option
*
* @param string $key
2015-04-13 05:41:18 +02:00
*
* @return mixed Result if the setting is available, or null otherwise
*/
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');
}
return $record->getCommentsOption($key);
}
2015-04-13 05:41:18 +02:00
/**
* Returns the parent {@link DataObject} this comment is attached too
*
* @return DataObject
*/
public function getParent() {
return $this->BaseClass && $this->ParentID
? DataObject::get_by_id($this->BaseClass, $this->ParentID, true)
: null;
}
/**
* Returns a string to help identify the parent of the comment
*
* @return string
*/
public function getParentTitle() {
if($parent = $this->getParent()) {
2015-04-13 05:41:18 +02:00
return $parent->Title ?: ($parent->ClassName . ' #' . $parent->ID);
}
}
/**
* Comment-parent classnames obviously vary, return the parent classname
*
* @return string
*/
public function getParentClassName() {
return $this->BaseClass;
}
public function castingHelper($field) {
// Safely escape the comment
if($field === 'EscapedComment') {
return $this->AllowHtml ? 'HTMLText' : 'Varchar';
}
return parent::castingHelper($field);
}
2015-04-13 05:41:18 +02:00
/**
* Content to be safely escaped on the frontend
*
* @return string
*/
public function getEscapedComment() {
return $this->Comment;
}
/**
* Return whether this comment is a preview (has not been written to the db)
*
* @return boolean
*/
public function isPreview() {
return !$this->exists();
}
/**
* @todo needs to compare to the new {@link Commenting} configuration API
*
2015-04-13 05:41:18 +02:00
* @param Member $member
*
* @return bool
*/
public function canCreate($member = null) {
return false;
}
/**
2015-04-13 05:41:18 +02:00
* Checks for association with a page, and {@link SiteTree->ProvidePermission}
* flag being set to true.
2015-04-13 05:41:18 +02:00
*
* @param Member $member
2015-04-13 05:41:18 +02:00
*
* @return Boolean
*/
public function canView($member = null) {
if(!$member) $member = Member::currentUser();
2015-04-13 05:41:18 +02:00
// Standard mechanism for accepting permission changes from decorators
$extended = $this->extendedCan('canView', $member);
if($extended !== null) return $extended;
// Allow admin
if(Permission::checkMember($member, 'CMS_ACCESS_CommentAdmin')) return true;
// Check if parent has comments and can be viewed
$parent = $this->getParent();
return $parent && $parent->ProvideComments && $parent->canView($member);
}
2015-04-13 05:41:18 +02:00
/**
2015-04-13 05:41:18 +02:00
* Checks for "CMS_ACCESS_CommentAdmin" permission codes and
* {@link canView()}.
*
* @param Member $member
2015-04-13 05:41:18 +02:00
*
* @return Boolean
*/
public function canEdit($member = null) {
if(!$member) $member = Member::currentUser();
2015-04-13 05:41:18 +02:00
// Standard mechanism for accepting permission changes from decorators
$extended = $this->extendedCan('canEdit', $member);
if($extended !== null) return $extended;
2015-04-13 05:41:18 +02:00
if(!$this->canView($member)) return false;
2015-04-13 05:41:18 +02:00
return (bool) Permission::checkMember($member, 'CMS_ACCESS_CommentAdmin');
}
2015-04-13 05:41:18 +02:00
/**
2015-04-13 05:41:18 +02:00
* Checks for "CMS_ACCESS_CommentAdmin" permission codes and
* {@link canEdit()}.
2015-04-13 05:41:18 +02:00
*
* @param Member $member
2015-04-13 05:41:18 +02:00
*
* @return Boolean
*/
public function canDelete($member = null) {
if(!$member) $member = Member::currentUser();
2015-04-13 05:41:18 +02:00
// Standard mechanism for accepting permission changes from decorators
$extended = $this->extendedCan('canDelete', $member);
if($extended !== null) return $extended;
2015-04-13 05:41:18 +02:00
return $this->canEdit($member);
}
/**
* Return the authors name for the comment
*
* @return string
*/
public function getAuthorName() {
if($this->Name) {
return $this->Name;
} else if($author = $this->Author()) {
return $author->getName();
}
}
/**
* 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
2015-04-13 05:41:18 +02:00
*
* @return string
*/
protected function actionLink($action, $member = null) {
if(!$member) $member = Member::currentUser();
if(!$member) return false;
$url = Controller::join_links(
Director::baseURL(),
2015-04-13 05:41:18 +02:00
'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
2015-04-13 05:41:18 +02:00
*
* @return string
*/
public function DeleteLink($member = null) {
if($this->canDelete($member)) {
return $this->actionLink('delete', $member);
}
}
2015-04-13 05:41:18 +02:00
/**
* Link to mark as spam
*
* @param Member $member
2015-04-13 05:41:18 +02:00
*
* @return string
*/
public function SpamLink($member = null) {
if($this->canEdit($member) && !$this->IsSpam) {
return $this->actionLink('spam', $member);
}
}
2015-04-13 05:41:18 +02:00
/**
* Link to mark as not-spam (ham)
*
* @param Member $member
2015-04-13 05:41:18 +02:00
*
* @return string
*/
public function HamLink($member = null) {
if($this->canEdit($member) && $this->IsSpam) {
return $this->actionLink('ham', $member);
}
}
2015-04-13 05:41:18 +02:00
/**
* Link to approve this comment
*
* @param Member $member
2015-04-13 05:41:18 +02:00
*
* @return string
*/
public function ApproveLink($member = null) {
if($this->canEdit($member) && !$this->Moderated) {
return $this->actionLink('approve', $member);
}
}
2015-04-13 05:41:18 +02:00
/**
* @return string
*/
public function SpamClass() {
if($this->IsSpam) {
return 'spam';
} else if(!$this->Moderated) {
return 'unmoderated';
} else {
return 'notspam';
}
}
2015-04-13 05:41:18 +02:00
/**
* @return string
*/
public function getTitle() {
2015-04-13 05:41:18 +02:00
$title = sprintf(_t('Comment.COMMENTBY', 'Comment by %s', 'Name'), $this->getAuthorName());
if($parent = $this->getParent()) {
if($parent->Title) {
2015-04-13 05:41:18 +02:00
$title .= sprintf(' %s %s', _t('Comment.ON', 'on'), $parent->Title);
}
}
return $title;
}
/*
* Modify the default fields shown to the user
*/
public function getCMSFields() {
$fields = parent::getCMSFields();
$hidden = array('ParentID', 'AuthorID', 'BaseClass', 'AllowHtml', 'SecretToken');
foreach($hidden as $private) {
$fields->removeByName($private);
}
return $fields;
}
/**
* @param String $dirtyHtml
2015-04-13 05:41:18 +02:00
*
* @return String
*/
public function purifyHtml($dirtyHtml) {
$purifier = $this->getHtmlPurifierService();
return $purifier->purify($dirtyHtml);
}
/**
* @return HTMLPurifier (or anything with a "purify()" method)
*/
public function getHtmlPurifierService() {
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.AllowedElements', $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);
}
/**
2015-04-13 05:41:18 +02:00
* Calculate the Gravatar link from the email address
*
* @return string
*/
public function Gravatar() {
$gravatar = '';
$use_gravatar = $this->getOption('use_gravatar');
2015-04-13 05:41:18 +02:00
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');
2015-04-13 05:41:18 +02:00
$gravatar .= '?s=' . $gravatarsize . '&d=' . $gravatardefault . '&r=' . $gravatarrating;
}
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
2015-04-13 05:41:18 +02:00
*
* @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.
*
2015-04-13 05:41:18 +02:00
* 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 Member $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);
}
/**
2015-04-13 05:41:18 +02:00
* @param string $url Comment action URL
* @param Member $member Member to restrict access to this action to
2015-04-13 05:41:18 +02:00
*
* @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
2015-04-13 05:41:18 +02:00
*
* @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
2015-04-13 05:41:18 +02:00
*
* @return string
*/
protected function generate($length = null) {
$generator = new RandomGenerator();
$result = $generator->randomToken('sha256');
2015-04-13 05:41:18 +02:00
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
*/
2015-04-13 05:41:18 +02:00
private function hash_pbkdf2($a, $p, $s, $c, $kl, $st = 0) {
2015-04-13 05:41:18 +02:00
$kb = $st + $kl; // Key blocks to compute
$dk = ''; // Derived key
// Create key
2015-04-13 05:41:18 +02:00
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
2015-04-13 05:41:18 +02:00
for($i = 1; $i < $c; $i++) {
// XOR each iterate
2015-04-13 05:41:18 +02:00
$ib ^= ($h = hash_hmac($a, $h, $p, true));
}
2015-04-13 05:41:18 +02:00
$dk .= $ib; // Append iterated block
}
// Return derived key of correct length
return substr($dk, $st, $kl);
}
}