diff --git a/_config.php b/_config.php index 1071adc..13b4766 100644 --- a/_config.php +++ b/_config.php @@ -1,31 +1,3 @@ - * // uses the default values - * Commenting::add('SiteTree'); - * - * // set configuration - * Commenting::add('SiteTree', array( - * 'require_login' => true - * )); - * - * - * To see all the configuration options read docs/en/Configuration.md or - * consult the Commenting class. - */ - -if(Config::inst()->get('Commenting', 'sitetree_comments') && class_exists('SiteTree') && !Commenting::has_commenting('SiteTree')) { - Commenting::add('SiteTree'); -} +Deprecation::notification_version('2.0', 'comments'); diff --git a/_config/routes.yml b/_config/routes.yml index c3a7537..959e233 100644 --- a/_config/routes.yml +++ b/_config/routes.yml @@ -7,4 +7,4 @@ Director: # handle old 2.4 style urls 'CommentingController//$Action/$ID/$OtherID': 'CommentingController' 'PageComments/$Action/$ID/$OtherID': 'CommentingController' - 'PageComments_Controller/$Action/$ID/$OtherID': 'CommentingController' \ No newline at end of file + 'PageComments_Controller/$Action/$ID/$OtherID': 'CommentingController' diff --git a/code/CommentList.php b/code/CommentList.php new file mode 100644 index 0000000..3296b13 --- /dev/null +++ b/code/CommentList.php @@ -0,0 +1,91 @@ +dataQuery->getQueryParam('Foreign.Class'); + } + + 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) + )); + } + + /** + * Adds the item to this relation. + * + * @param Comment $item The comment to be added + */ + 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"); + } + + $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(!($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(); + } + } +} diff --git a/code/Commenting.php b/code/Commenting.php index affeb04..f81dd25 100644 --- a/code/Commenting.php +++ b/code/Commenting.php @@ -9,180 +9,138 @@ * * For documentation on how to use this class see docs/en/Configuration.md * + * @deprecated since version 2.0 + * * @package comments */ - class Commenting { - /** - * @var array map of enabled {@link DataObject} and related configuration - */ - private static $enabled_classes = array(); - - /** - * @config - * @var bool Whether to enable commenting on SiteTree objects by default - */ - private static $sitetree_comments = true; - - /** - * @var array default configuration values - */ - private static $default_config = array( - 'require_login' => false, // boolean, whether a user needs to login - 'required_permission' => false, // required permission to comment (or array of permissions) - 'include_js' => true, // Enhance operation by ajax behaviour on moderation links - 'use_gravatar' => false, // set to true to show gravatar icons, - 'gravatar_size' => 80, // size of gravatar in pixels. This is the same as the standard default - 'gravatar_default' => 'identicon', // theme for 'not found' gravatar (see http://gravatar.com/site/implement/images/) - 'gravatar_rating' => 'g', // gravatar rating. This is the same as the standard default - 'show_comments_when_disabled' => false, // when comments are disabled should we show older comments (if available) - 'order_comments_by' => "\"Created\" DESC", - 'comments_per_page' => 10, - 'comments_holder_id' => "comments-holder", // id for the comments holder - 'comment_permalink_prefix' => "comment-", // id prefix for each comment. If needed make this different - 'require_moderation' => false, - 'require_moderation_nonmembers' => false, // requires moderation for comments posted by non-members. 'require_moderation' overrides this if set. - 'html_allowed' => false, // allow for sanitized HTML in comments - 'html_allowed_elements' => array('a', 'img', 'i', 'b'), - 'use_preview' => false, // preview formatted comment (when allowing HTML). Requires include_js=true - ); - /** * Adds commenting to a {@link DataObject} * + * @deprecated since version 2.0 + * * @param string classname to add commenting to - * @param array $setting Settings. See {@link self::$default_config} for + * @param array $settings Settings. See {@link self::$default_config} for * available settings * * @throws InvalidArgumentException */ public static function add($class, $settings = false) { - if($settings && !is_array($settings)) { - throw new InvalidArgumentException('$settings needs to be an array or null', E_USER_ERROR); - } - - self::$enabled_classes[$class] = $settings; + Deprecation::notice('2.0', 'Using Commenting::add is deprecated. Please use the config API instead'); + Config::inst()->update($class, 'extensions', array('CommentsExtension')); - $class::add_extension('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'); + } + Config::inst()->update($class, 'comments', $settings); } - + /** * 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 */ public static function remove($class) { - if(isset(self::$enabled_classes[$class])) { - unset(self::$enabled_classes[$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 * + * @deprecated since version 2.0 + * * @return bool */ public static function has_commenting($class) { - return (isset(self::$enabled_classes[$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) { - if($class == "all") { - if($enabledClasses = self::$enabled_classes) { - foreach($enabledClasses as $enabled) { - if(!is_array($enabled)) $enabled = array(); - - $enabled[$key] = $value; - } - } - } - else if(isset(self::$enabled_classes[$class])) { - if(!is_array(self::$enabled_classes[$class])) self::$enabled_classes[$class] = array(); - - self::$enabled_classes[$class][$key] = $value; - } - else { - throw new Exception("$class does not have commenting enabled", E_USER_ERROR); - } + 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 + * @throws Exception * @return mixed */ - public static function get_config_value($class = null, $key) { - if(!$class || isset(self::$enabled_classes[$class])) { - // custom configuration - if(isset(self::$enabled_classes[$class][$key])) return self::$enabled_classes[$class][$key]; - - // default configuration - if(isset(self::$default_config[$key])) return self::$default_config[$key]; - - // config value doesn't exist - throw new Exception("Config ($key) is not a valid configuration value", E_USER_WARNING); - } - else { - throw new Exception("$class does not have commenting enabled", E_USER_ERROR); + 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. * + * @deprecated since version 2.0 + * * @param string $class * @param string $key * @param string $value Expected value - * - * @return bool + * @return boolean */ public static function config_value_equals($class, $key, $value) { - try { - $check = self::get_config_value($class, $key); - - if($check && ($check == $value)) return true; - } - catch(Exception $e) {} - - return false; + $check = self::get_config_value($class, $key); + if($check && ($check == $value)) return true; } - + /** * Return whether a user can post on a given commenting instance * + * @deprecated since version 2.0 + * * @param string $class + * @return boolean true */ public static function can_member_post($class) { + Deprecation::notice('2.0', 'Use $instance->canPostComment() directly instead'); $member = Member::currentUser(); - - try { - $login = self::get_config_value($class, 'require_login'); - $permission = self::get_config_value($class, 'required_permission'); - - if($permission && !Permission::check($permission)) return false; - - if($login && !$member) return false; - } - catch(Exception $e) {} - - return true; + + // Check permission + $permission = self::get_config_value($class, 'required_permission'); + if($permission && !Permission::check($permission)) return false; + + // Check login required + $requireLogin = self::get_config_value($class, 'require_login'); + return !$requireLogin || $member; } } diff --git a/code/CommentAdmin.php b/code/admin/CommentAdmin.php similarity index 69% rename from code/CommentAdmin.php rename to code/admin/CommentAdmin.php index 32dcec2..661ea21 100644 --- a/code/CommentAdmin.php +++ b/code/admin/CommentAdmin.php @@ -46,37 +46,7 @@ class CommentAdmin extends LeftAndMain implements PermissionProvider { return Security::permissionFailure($this); } - $commentsConfig = GridFieldConfig::create()->addComponents( - new GridFieldFilterHeader(), - $columns = new GridFieldDataColumns(), - new GridFieldSortableHeader(), - new GridFieldPaginator(25), - new GridFieldDeleteAction(), - new GridFieldDetailForm(), - new GridFieldExportButton(), - new GridFieldEditButton(), - new GridFieldDetailForm(), - $manager = new GridFieldBulkManager() - ); - - $manager->addBulkAction( - 'markAsSpam', 'Mark as spam', 'CommentsGridFieldBulkAction_MarkAsSpam', - array( - 'isAjax' => true, - 'icon' => 'delete', - 'isDestructive' => true - ) - ); - - $columns->setFieldFormatting(array( - 'ParentTitle' => function($value, &$item) { - return sprintf( - '%s', - Convert::raw2xml($item->Link()), - Convert::raw2xml($value) - ); - } - )); + $commentsConfig = CommentsGridFieldConfig::create(); $needs = new GridField( 'Comments', @@ -87,7 +57,7 @@ class CommentAdmin extends LeftAndMain implements PermissionProvider { $moderated = new GridField( 'CommentsModerated', - _t('CommentsAdmin.CommentsModerated'), + _t('CommentsAdmin.Moderated', 'Moderated'), Comment::get()->filter('Moderated',1), $commentsConfig ); diff --git a/code/CommentsGridFieldBulkAction.php b/code/admin/CommentsGridFieldBulkAction.php similarity index 100% rename from code/CommentsGridFieldBulkAction.php rename to code/admin/CommentsGridFieldBulkAction.php diff --git a/code/admin/CommentsGridFieldConfig.php b/code/admin/CommentsGridFieldConfig.php new file mode 100644 index 0000000..cf848a3 --- /dev/null +++ b/code/admin/CommentsGridFieldConfig.php @@ -0,0 +1,33 @@ +addComponent(new GridFieldExportButton()); + + // Format column + $columns = $this->getComponentByType('GridFieldDataColumns'); + $columns->setFieldFormatting(array( + 'ParentTitle' => function($value, &$item) { + return sprintf( + '%s', + Convert::raw2xml($item->Link()), + Convert::raw2xml($value) + ); + } + )); + + // Add bulk option + $manager = new GridFieldBulkManager(); + $manager->addBulkAction( + 'markAsSpam', 'Mark as spam', 'CommentsGridFieldBulkAction_MarkAsSpam', + array( + 'isAjax' => true, + 'icon' => 'delete', + 'isDestructive' => true + ) + ); + $this->addComponent($manager); + } +} \ No newline at end of file diff --git a/code/controllers/CommentingController.php b/code/controllers/CommentingController.php index d135430..704ef06 100644 --- a/code/controllers/CommentingController.php +++ b/code/controllers/CommentingController.php @@ -17,48 +17,122 @@ class CommentingController extends Controller { 'doPreviewComment' ); + /** + * Base class this commenting form is for + * + * @var string + */ private $baseClass = ""; - private $ownerRecord = ""; - private $ownerController = ""; + + /** + * The record this commenting form is for + * + * @var DataObject + */ + private $ownerRecord = null; + + /** + * Parent controller record + * + * @var Controller + */ + private $ownerController = null; + + /** + * Backup url to return to + * + * @var string + */ protected $fallbackReturnURL = null; - + + /** + * Set the base class to use + * + * @param string $class + */ public function setBaseClass($class) { $this->baseClass = $class; } - + + /** + * Get the base class used + * + * @return string + */ public function getBaseClass() { return $this->baseClass; } - + + /** + * Set the record this controller is working on + * + * @param DataObject $record + */ public function setOwnerRecord($record) { $this->ownerRecord = $record; } - + + /** + * Get the record + * + * @return DataObject + */ public function getOwnerRecord() { return $this->ownerRecord; } - + + /** + * Set the parent controller + * + * @param Controller $controller + */ public function setOwnerController($controller) { $this->ownerController = $controller; } - + + /** + * Get the parent controller + * + * @return Controller + */ public function getOwnerController() { return $this->ownerController; } + + /** + * Get the commenting option for the current state + * + * @param string $key + * @return mixed Result if the setting is available, or null otherwise + */ + 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 */ - public function Link($action = "", $id = '', $other = '') { - return Controller::join_links(__CLASS__ , $action, $id, $other); + public function Link($action = '', $id = '', $other = '') { + return Controller::join_links(Director::baseURL(), __CLASS__ , $action, $id, $other); } /** * Outputs the RSS feed of comments * - * @return XML + * @return HTMLText */ public function rss() { return $this->getFeed($this->request)->outputToBrowser(); @@ -81,42 +155,37 @@ 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, )); - if($request->getVar('pageid')) { - $comments = $comments->filter(array( - 'BaseClass' => 'SiteTree', - 'ParentID' => $request->getVar('pageid'), - )); - - $link = $this->Link('rss', 'SiteTree', $id); - - } elseif($class && $id) { - if(Commenting::has_commenting($class)) { - $comments = $comments->filter(array( - 'BaseClass' => $class, - 'ParentID' => $id, - )); - - $link = $this->Link('rss', Convert::raw2xml($class), (int) $id); - } else { + // Check if class filter + if($class) { + if(!is_subclass_of($class, 'DataObject') || !$class::has_extension('CommentsExtension')) { return $this->httpError(404); } - } elseif($class) { - if(Commenting::has_commenting($class)) { - $comments = $comments->filter('BaseClass', $class); - } else { - 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); + $this->setOwnerRecord(DataObject::get_by_id($class, $id)); } } $title = _t('CommentingController.RSSTITLE', "Comments RSS Feed"); $comments = new PaginatedList($comments, $request); - $comments->setPageLength(Commenting::get_config_value(null, 'comments_per_page')); + $comments->setPageLength($this->getOption('comments_per_page')); return new RSSFeed( $comments, @@ -232,8 +301,7 @@ class CommentingController extends Controller { * @return Form */ public function CommentsForm() { - $usePreview = Commenting::get_config_value($this->getBaseClass(), 'use_preview'); - $member = Member::currentUser(); + $usePreview = $this->getOption('use_preview'); $fields = new FieldList( $dataFields = new CompositeField( @@ -292,34 +360,30 @@ class CommentingController extends Controller { // create the comment form $form = new Form($this, 'CommentsForm', $fields, $actions, $required); + // Load member data + $requireLogin = $this->getOption('require_login'); + $permission = $this->getOption('required_permission'); + $member = Member::currentUser(); + if(($requireLogin || $permission) && $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)); + } + // if the record exists load the extra required data if($record = $this->getOwnerRecord()) { - $require_login = Commenting::get_config_value($this->getBaseClass(), 'require_login'); - $permission = Commenting::get_config_value($this->getBaseClass(), 'required_permission'); - - if(($require_login || $permission) && $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)); - - $form->setFields($fields); - } - // we do not want to read a new URL when the form has already been submitted // which in here, it hasn't been. - $url = (isset($_SERVER['REQUEST_URI'])) ? Director::protocolAndHost() . '' . $_SERVER['REQUEST_URI'] : false; - $form->loadDataFrom(array( 'ParentID' => $record->ID, - 'ReturnURL' => $url, + '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); @@ -336,7 +400,7 @@ class CommentingController extends Controller { // allow previous value to fill if comment not stored in cookie (i.e. validation error) $prevComment = Cookie::get('CommentsForm_Comment'); if($prevComment && $prevComment != ''){ - $form->loadDataFrom(array("Comment" => $prevComment)); + $form->loadDataFrom(array("Comment" => $prevComment)); } } @@ -357,13 +421,14 @@ class CommentingController extends Controller { * @param Form $form */ public function doPostComment($data, $form) { - $class = (isset($data['BaseClass'])) ? $data['BaseClass'] : $this->getBaseClass(); - $usePreview = Commenting::get_config_value($class, 'use_preview'); - $isPreview = ($usePreview && isset($data['IsPreview']) && $data['IsPreview']); - - // if no class then we cannot work out what controller or model they - // are on so throw an error - if(!$class) user_error("No OwnerClass set on CommentingController.", E_USER_ERROR); + // 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)); @@ -373,7 +438,7 @@ class CommentingController extends Controller { $this->extend('onBeforePostComment', $form); // If commenting can only be done by logged in users, make sure the user is logged in - if(!Commenting::can_member_post($class)) { + if(!$this->getOwnerRecord()->canPostComment()) { return Security::permissionFailure( $this, _t( @@ -389,9 +454,9 @@ class CommentingController extends Controller { } // is moderation turned on - $requireModeration = Commenting::get_config_value($class, 'require_moderation'); - if(!$requireModeration){ - $requireModerationNonmembers = Commenting::get_config_value($class, 'require_moderation_nonmembers'); + $requireModeration = $this->getOption('require_moderation'); + if(!$requireModeration) { + $requireModerationNonmembers = $this->getOption('require_moderation_nonmembers'); $requireModeration = $requireModerationNonmembers ? !Member::currentUser() : false; } @@ -400,18 +465,19 @@ class CommentingController extends Controller { Session::set('CommentsModerated', 1); } - $comment = new Comment(); $form->saveInto($comment); - $comment->AllowHtml = Commenting::get_config_value($class, 'html_allowed'); + $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) { - $comment->extend('onBeforeWrite', $dummy); + $comment->extend('onBeforeWrite'); } else { - $comment->write(); + $comment->write(); // extend hook to allow extensions. Also see onBeforePostComment $this->extend('onAfterPostComment', $comment); @@ -432,7 +498,7 @@ class CommentingController extends Controller { // Given a redirect page exists, attempt to link to the correct anchor if(!$comment->Moderated) { // Display the "awaiting moderation" text - $holder = Commenting::get_config_value($comment->BaseClass, 'comments_holder_id'); + $holder = $this->getOption('comments_holder_id'); $hash = "{$holder}_PostCommentForm_error"; } elseif($comment->IsSpam) { // Link to the form with the error message contained diff --git a/code/dataobjects/Comment.php b/code/dataobjects/Comment.php index 06fbd94..a8cb12b 100755 --- a/code/dataobjects/Comment.php +++ b/code/dataobjects/Comment.php @@ -13,40 +13,40 @@ * @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() + * @method HasManyList ChildComments() List of child comments + * @method Member Author() Member object who created this comment * @package comments */ class Comment extends DataObject { private static $db = array( - "Name" => "Varchar(200)", - "Comment" => "Text", - "Email" => "Varchar(200)", - "URL" => "Varchar(255)", - "BaseClass" => "Varchar(200)", - "Moderated" => "Boolean", - "IsSpam" => "Boolean", - "ParentID" => "Int", - 'AllowHtml' => "Boolean", - "SecretToken" => "Varchar(255)", + "Name" => "Varchar(200)", + "Comment" => "Text", + "Email" => "Varchar(200)", + "URL" => "Varchar(255)", + "BaseClass" => "Varchar(200)", + "Moderated" => "Boolean(1)", + "IsSpam" => "Boolean(0)", + "ParentID" => "Int", + 'AllowHtml' => "Boolean", + "SecretToken" => "Varchar(255)", ); private static $has_one = array( - "Author" => "Member" + "Author" => "Member", ); private static $default_sort = '"Created" DESC'; - private static $has_many = array(); - - private static $many_many = array(); - private static $defaults = array( "Moderated" => 1, - "IsSpam" => 0 + "IsSpam" => 0, ); private static $casting = array( + 'Title' => 'Varchar', + 'ParentTitle' => 'Varchar', + 'ParentClassName' => 'Varchar', 'AuthorName' => 'Varchar', 'RSSName' => 'Varchar', 'DeleteLink' => 'Varchar', @@ -70,7 +70,7 @@ class Comment extends DataObject { 'Comment' => 'Comment', 'Created' => 'Date Posted', 'ParentTitle' => 'Parent', - 'IsSpam' => 'Is Spam' + 'IsSpam' => 'Is Spam', ); public function onBeforeWrite() { @@ -136,8 +136,7 @@ class Comment extends DataObject { * @return string */ public function Permalink() { - $prefix = Commenting::get_config_value($this->BaseClass, 'comment_permalink_prefix'); - + $prefix = $this->getOption('comment_permalink_prefix'); return $prefix . $this->ID; } @@ -159,6 +158,26 @@ class Comment extends DataObject { return $labels; } + + /** + * Get the commenting option + * + * @param string $key + * @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); + } /** * Returns the parent {@link DataObject} this comment is attached too @@ -166,11 +185,9 @@ class Comment extends DataObject { * @return DataObject */ public function getParent() { - if(!$this->BaseClass) { - $this->BaseClass = "SiteTree"; - } - - return ($this->ParentID) ? DataObject::get_by_id($this->BaseClass, $this->ParentID) : null; + return $this->BaseClass && $this->ParentID + ? DataObject::get_by_id($this->BaseClass, $this->ParentID, true) + : null; } @@ -180,43 +197,35 @@ class Comment extends DataObject { * @return string */ public function getParentTitle() { - if($parent = $this->getParent()){ - return ($parent && $parent->Title) ? $parent->Title : $parent->ClassName . " #" . $parent->ID; - } + if($parent = $this->getParent()) { + return $parent->Title ?: ($parent->ClassName . " #" . $parent->ID); + } } /** - * Comment-parent classnames obviousely vary, return the parent classname + * Comment-parent classnames obviously vary, return the parent classname * * @return string */ public function getParentClassName() { - $default = 'SiteTree'; - - if(!$this->BaseClass) { - return $default; - } - return $this->BaseClass; } + + public function castingHelper($field) { + // Safely escape the comment + if($field === 'EscapedComment') { + return $this->AllowHtml ? 'HTMLText' : 'Varchar'; + } + return parent::castingHelper($field); + } /** - * Return the content for this comment escaped depending on the Html state. + * Content to be safely escaped on the frontend * - * @return HTMLText + * @return string */ public function getEscapedComment() { - $comment = $this->dbObject('Comment'); - - if ($comment->exists()) { - if ($this->AllowHtml) { - return DBField::create_field('HTMLText', nl2br($comment->RAW())); - } else { - return DBField::create_field('HTMLText', sprintf("

%s

", nl2br($comment->XML()))); - } - } - - return $comment; + return $this->Comment; } /** @@ -225,7 +234,7 @@ class Comment extends DataObject { * @return boolean */ public function isPreview() { - return ($this->ID < 1); + return !$this->exists(); } /** @@ -250,11 +259,13 @@ class Comment extends DataObject { // Standard mechanism for accepting permission changes from decorators $extended = $this->extendedCan('canView', $member); if($extended !== null) return $extended; - - $page = $this->getParent(); - $admin = (bool) Permission::checkMember($member, 'CMS_ACCESS_CommentAdmin'); - return (($page && $page->ProvideComments && $page->canView($member)) || $admin); + // 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); } /** @@ -301,8 +312,8 @@ class Comment extends DataObject { public function getAuthorName() { if($this->Name) { return $this->Name; - } else if($this->Author()) { - return $this->Author()->getName(); + } else if($author = $this->Author()) { + return $author->getName(); } } @@ -434,9 +445,7 @@ class Comment extends DataObject { */ public function getHtmlPurifierService() { $config = HTMLPurifier_Config::createDefault(); - $config->set('HTML.AllowedElements', - Commenting::get_config_value($this->BaseClass, 'html_allowed_elements') - ); + $config->set('HTML.AllowedElements', $this->getOption('html_allowed_elements')); $config->set('AutoFormat.AutoParagraph', true); $config->set('AutoFormat.Linkify', true); $config->set('URI.DisableExternalResources', true); @@ -444,17 +453,19 @@ class Comment extends DataObject { return new HTMLPurifier($config); } - /* - Calcualate the gravatar link from the email address - */ + /** + * Calcualate the gravatar link from the email address + * + * @return string + */ public function Gravatar() { $gravatar = ''; - $use_gravatar = Commenting::get_config_value($this->BaseClass, 'use_gravatar'); + $use_gravatar = $this->getOption('use_gravatar'); if ($use_gravatar) { $gravatar = "http://www.gravatar.com/avatar/" . md5( strtolower(trim($this->Email))); - $gravatarsize = Commenting::get_config_value($this->BaseClass, 'gravatar_size'); - $gravatardefault = Commenting::get_config_value($this->BaseClass, 'gravatar_default'); - $gravatarrating = Commenting::get_config_value($this->BaseClass, 'gravatar_rating'); + $gravatarsize = $this->getOption('gravatar_size'); + $gravatardefault = $this->getOption('gravatar_default'); + $gravatarrating = $this->getOption('gravatar_rating'); $gravatar.= "?s=".$gravatarsize."&d=".$gravatardefault."&r=".$gravatarrating; } diff --git a/code/extensions/CommentsExtension.php b/code/extensions/CommentsExtension.php index 53065db..c13ba58 100644 --- a/code/extensions/CommentsExtension.php +++ b/code/extensions/CommentsExtension.php @@ -7,10 +7,37 @@ */ class CommentsExtension extends DataExtension { - + + /** + * Default configuration values + * + * @var array + * @config + */ + private static $comments = array( + 'enabled' => true, // Allows commenting to be disabled even if the extension is present + 'require_login' => false, // boolean, whether a user needs to login + 'required_permission' => false, // required permission to comment (or array of permissions) + 'include_js' => true, // Enhance operation by ajax behaviour on moderation links + 'use_gravatar' => false, // set to true to show gravatar icons, + 'gravatar_size' => 80, // size of gravatar in pixels. This is the same as the standard default + 'gravatar_default' => 'identicon', // theme for 'not found' gravatar (see http://gravatar.com/site/implement/images/) + 'gravatar_rating' => 'g', // gravatar rating. This is the same as the standard default + 'show_comments_when_disabled' => false, // when comments are disabled should we show older comments (if available) + 'order_comments_by' => "\"Created\" DESC", + 'comments_per_page' => 10, + 'comments_holder_id' => "comments-holder", // id for the comments holder + 'comment_permalink_prefix' => "comment-", // id prefix for each comment. If needed make this different + 'require_moderation' => false, + 'require_moderation_nonmembers' => false, // requires moderation for comments posted by non-members. 'require_moderation' overrides this if set. + 'html_allowed' => false, // allow for sanitized HTML in comments + 'html_allowed_elements' => array('a', 'img', 'i', 'b'), + 'use_preview' => false, // preview formatted comment (when allowing HTML). Requires include_js=true + ); + public static function get_extra_config($class, $extension, $args = null) { $config = array(); - + // if it is attached to the SiteTree then we need to add ProvideComments if(is_subclass_of($class, 'SiteTree') || $class == 'SiteTree') { $config['db'] = array('ProvideComments' => 'Boolean'); @@ -31,53 +58,188 @@ class CommentsExtension extends DataExtension { */ public function updateSettingsFields(FieldList $fields) { if($this->attachedToSiteTree()) { - $fields->addFieldToTab('Root.Settings', + $fields->addFieldToTab('Root.Settings', new CheckboxField('ProvideComments', _t('Comment.ALLOWCOMMENTS', 'Allow Comments')) ); } } + + /** + * Returns the RelationList of all comments against this object. Can be used as a data source + * for a gridfield with write access. + * + * @return CommentList + */ + public function AllComments() { + $comments = CommentList::create($this->ownerBaseClass)->forForeignID($this->owner->ID); + $this->owner->extend('updateAllComments', $comments); + return $comments; + } + + public function getComments() { + Deprecation::notice('2.0', 'Use PagedComments to get paged coments'); + return $this->PagedComments(); + } /** - * Returns a list of all the comments attached to this record. + * Returns the root level comments, with spam and unmoderated items excluded, for use in the frontend + * + * @return CommentList + */ + public function Comments() { + // Get all non-spam comments + $order = $this->owner->getCommentsOption('order_comments_by'); + $list = $this + ->AllComments() + ->sort($order) + ->filter('IsSpam', 0); + + // Filter unmoderated comments for non-administrators if moderation is enabled + if ($this->owner->getCommentsOption('require_moderation') + || $this->owner->getCommentsOption('require_moderation_nonmembers') + ) { + $list = $list->filter('Moderated', 1); + } + + $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 * * @return PaginatedList */ - public function getComments() { - $order = Commenting::get_config_value($this->ownerBaseClass, 'order_comments_by'); - - $list = Comment::get()->filter(array( - 'ParentID' => $this->owner->ID, - 'BaseClass' => $this->ownerBaseClass - ))->sort($order); - - // Filter content for unauthorised users - if (!($member = Member::currentUser()) || !Permission::checkMember($member, 'CMS_ACCESS_CommentAdmin')) { - - // Filter unmoderated comments for non-administrators if moderation is enabled - if (Commenting::get_config_value($this->ownerBaseClass, 'require_moderation') || Commenting::get_config_value($this->ownerBaseClass, 'require_moderation_nonmembers')) { - $list = $list->filter(array( - 'Moderated' => 1, - 'IsSpam' => 0 - )); - } else { - // Filter spam comments for non-administrators if auto-moderated - $list = $list->filter('IsSpam', 0); - } - } - - $list = new PaginatedList($list); - - $list->setPageLength(Commenting::get_config_value( - $this->ownerBaseClass, 'comments_per_page' - )); - - - $controller = Controller::curr(); - $list->setPageStart($controller->request->getVar("commentsstart". $this->owner->ID)); - $list->setPaginationGetVar("commentsstart". $this->owner->ID); + 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 + * + * @deprecated since version 2.0 + * + * @return boolean + */ + 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' + } + + /** + * Determine if comments are enabled for this instance + * + * @return boolean + */ + public function getCommentsEnabled() { + if(!$this->owner->getCommentsOption('enabled')) return false; + + // Non-page objects always have comments enabled + return !$this->attachedToSiteTree() || $this->owner->ProvideComments; + } + + /** + * Get the HTML ID for the comment holder in the template + * + * @return string + */ + public function getCommentHolderID() { + return $this->owner->getCommentsOption('comments_holder_id'); + } + + /** + * @deprecated since version 2.0 + */ + 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) + * + * @return string|array Permission or list of permissions, if required + */ + public function getPostingRequiredPermission() { + return $this->owner->getCommentsOption('required_permission'); + } + + public function canPost() { + Deprecation::notice('2.0', 'Use canPostComment instead'); + return $this->canPostComment(); + } + + /** + * Determine if a user can post comments on this item + * + * @param Member $member Member to check + * @return boolean + */ + public function canPostComment($member = null) { + // Check if member is required + $requireLogin = $this->owner->getCommentsOption('require_login'); + if(!$requireLogin) return true; + + // Check member is logged in + $member = $member ?: Member::currentUser(); + if(!$member) return false; + + // If member required check permissions + $requiredPermission = $this->getPostingRequiredPermission(); + if($requiredPermission && !Permission::checkMember($member, $requiredPermission)) return false; + + return true; + } + + /** + * Determine if this member can moderate comments in the CMS + * + * @param Member $member + * @return boolean + */ + public function canModerateComments($member = null) { + 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 + * + * @return string + */ + public function getCommentRSSLink() { + return Controller::join_links(Director::baseURL(), "CommentingController/rss"); + } + + public function getRssLinkPage() { + Deprecation::notice('2.0', 'Use getCommentRSSLinkPage instead'); + return $this->getCommentRSSLinkPage(); + } + + /** + * 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 + ); + } /** * Comments interface for the front end. Includes the CommentAddForm and the composition @@ -91,23 +253,15 @@ class CommentsExtension extends DataExtension { * @see docs/en/Extending */ public function CommentsForm() { - if(Commenting::has_commenting($this->ownerBaseClass) && Commenting::get_config_value($this->ownerBaseClass, 'include_js')) { + // 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-validate/lib/jquery.form.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-validate/jquery.validate.pack.js'); Requirements::javascript('comments/javascript/CommentsInterface.js'); } - - $interface = new SSViewer('CommentsInterface'); - // detect whether we comments are enabled. By default if $CommentsForm is included - // on a {@link DataObject} then it is enabled, however {@link SiteTree} objects can - // trigger comments on / off via ProvideComments - $enabled = (!$this->attachedToSiteTree() || $this->owner->ProvideComments) ? true : false; - - // do not include the comments on pages which don't have id's such as security pages - if($this->owner->ID < 0) return false; - $controller = CommentingController::create(); $controller->setOwnerRecord($this->owner); $controller->setBaseClass($this->ownerBaseClass); @@ -120,18 +274,13 @@ class CommentsExtension extends DataExtension { // 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 $interface->process(new ArrayData(array( - 'CommentHolderID' => Commenting::get_config_value($this->ownerBaseClass, 'comments_holder_id'), - 'PostingRequiresPermission' => Commenting::get_config_value($this->ownerBaseClass, 'required_permission'), - 'CanPost' => Commenting::can_member_post($this->ownerBaseClass), - 'RssLink' => "CommentingController/rss", - 'RssLinkPage' => "CommentingController/rss/". $this->ownerBaseClass . '/'.$this->owner->ID, - 'CommentsEnabled' => $enabled, - 'Parent' => $this->owner, - 'AddCommentForm' => $form, - 'ModeratedSubmitted' => $moderatedSubmitted, - 'Comments' => $this->getComments() - ))); + return $this + ->owner + ->customise(array( + 'AddCommentForm' => $form, + 'ModeratedSubmitted' => $moderatedSubmitted, + )) + ->renderWith('CommentsInterface'); } /** @@ -144,14 +293,71 @@ class CommentsExtension extends DataExtension { return (is_subclass_of($class, 'SiteTree')) || ($class == 'SiteTree'); } - + /** * @deprecated 1.0 Please use {@link CommentsExtension->CommentsForm()} */ public function PageComments() { // This method is very commonly used, don't throw a warning just yet - //user_error('$PageComments is deprecated. Please use $CommentsForm', E_USER_WARNING); - + Deprecation::notice('1.0', '$PageComments is deprecated. Please use $CommentsForm'); return $this->CommentsForm(); } + + /** + * Get the commenting option for this object + * + * 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 + */ + 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]; + + // To allow other extensions to customise this option + if($this->owner) $this->owner->extend('updateCommentsOption', $key, $value); + return $value; + } + + public function updateCMSFields(\FieldList $fields) { + // Disable moderation if not permitted + if(!$this->owner->canModerateComments()) return; + + // Create gridfield config + $commentsConfig = CommentsGridFieldConfig::create(); + + $needs = new GridField( + 'CommentsNeedsModeration', + _t('CommentsAdmin.NeedsModeration', 'Needs Moderation'), + $this->owner->AllComments()->filter('Moderated', 0), + $commentsConfig + ); + + $moderated = new GridField( + 'CommentsModerated', + _t('CommentsAdmin.Moderated', 'Moderated'), + $this->owner->AllComments()->filter('Moderated', 1), + $commentsConfig + ); + + if($fields->hasTabSet()) { + $tabset = new TabSet( + 'Comments', + new Tab('CommentsNeedsModerationTab', _t('CommentAdmin.NeedsModeration', 'Needs Moderation'), + $needs + ), + new Tab('CommentsModeratedTab', _t('CommentAdmin.Moderated', 'Moderated'), + $moderated + ) + ); + $fields->addFieldToTab('Root', $tabset); + } else { + $fields->push($needs); + $fields->push($moderated); + } + } } diff --git a/composer.json b/composer.json index efebff5..100dda9 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "2.0.x-dev" } } } diff --git a/docs/en/Configuration.md b/docs/en/Configuration.md index 2958aae..136d1ba 100644 --- a/docs/en/Configuration.md +++ b/docs/en/Configuration.md @@ -5,33 +5,62 @@ The module provides a number of built in configuration settings below are the default settings - // mysite/_config.php - - Commenting::add('Foo', array( - 'require_login' => false, // boolean, whether a user needs to login - 'required_permission' => false, // required permission to comment (or array of permissions) - 'include_js' => true, // Enhance operation by ajax behaviour on moderation links - 'show_comments_when_disabled' => false, // when comments are disabled should we show older comments (if available) - 'order_comments_by' => "\"Created\" DESC", - 'comments_per_page' => 10, - 'comments_holder_id' => "comments-holder", // id for the comments holder - 'comment_permalink_prefix' => "comment-", // id prefix for each comment. If needed make this different - 'require_moderation' => false, - 'html_allowed' => false, // allow for sanitized HTML in comments - 'html_allowed_elements' => array('a', 'img', 'i', 'b'), - 'use_preview' => false, // preview formatted comment (when allowing HTML). Requires include_js=true - 'use_gravatar' => false, - 'gravatar_size' => 80 - )); - +In order to add commenting to your site, the minimum amount of work necessary is to add the `CommentsExtension` to +the base class for the object which holds comments. + +```yaml +SiteTree: + extensions: + - CommentsExtension +``` + +## Configuration + +In order to configure options for any class you should assign the specific option a value under the 'comments' +config of the specified class. + +```yaml +SiteTree: + extensions: + - CommentsExtension + comments: + require_login: false # boolean, whether a user needs to login + required_permission: false # required permission to comment (or array of permissions) + include_js: true # Enhance operation by ajax behaviour on moderation links + show_comments_when_disabled: false # when comments are disabled should we show older comments (if available) + order_comments_by: '"Created" DESC' + comments_per_page: 10 + comments_holder_id: 'comments-holder' # id for the comments holder + comment_permalink_prefix: 'comment-' # id prefix for each comment. If needed make this different + require_moderation: false + html_allowed: false # allow for sanitized HTML in comments + html_allowed_elements: + - a + - img + - i + - b + use_preview: false # preview formatted comment (when allowing HTML). Requires include_js=true + use_gravatar: false + gravatar_size: 80 +``` + + If you want to customize any of the configuration options after you have added the extension (or on the built-in SiteTree commenting) use `set_config_value` - // mysite/_config.php - Sets require_login to true for all pages - Commenting::set_config_value('SiteTree', 'require_login', true); - - // mysite/_config.php - Returns the setting - Commenting::get_config_value('SiteTree', 'require_login'); +```yaml +# Set the default option for pages to require login +SiteTree: + comments: + require_login: true +``` + + +```php +// Get the setting +$loginRequired = singleton('SiteTree')->getCommentsOption('require_login'); +``` + ## HTML Comments @@ -56,12 +85,20 @@ properly sanitized. Don't allow tags like `