diff --git a/wire/modules/Fieldtype/FieldtypeComments/Comment.php b/wire/modules/Fieldtype/FieldtypeComments/Comment.php index 9db03e77..0af227d8 100644 --- a/wire/modules/Fieldtype/FieldtypeComments/Comment.php +++ b/wire/modules/Fieldtype/FieldtypeComments/Comment.php @@ -5,7 +5,7 @@ * * Class that contains an individual comment. * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2020 by Ryan Cramer * https://processwire.com * * @property int $id @@ -27,7 +27,12 @@ * @property string $subcode * @property int $upvotes * @property int $downvotes - * @property int $stars + * @property int $stars + * @property-read Comment|null $parent Parent comment when depth is enabled or null if no parent (since 3.0.149) + * @property-read CommentArray $parents All parent comments (since 3.0.149) + * @property-read CommentArray $children Immediate child comments (since 3.0.149) + * @property-read int $depth Current comment depth (since 3.0.149) + * @property-read bool $loaded True when comment is fully loaded from DB (since 3.0.149) * */ @@ -172,6 +177,13 @@ class Comment extends WireData { $this->set('stars', 0); } + /** + * Get property + * + * @param string $key + * @return mixed + * + */ public function get($key) { if($key == 'user' || $key == 'createdUser') { @@ -189,6 +201,9 @@ class Comment extends WireData { } else if($key == 'parent') { return $this->parent(); + + } else if($key == 'parents') { + return $this->parents(); } else if($key == 'children') { return $this->children(); @@ -207,6 +222,12 @@ class Comment extends WireData { } else if($key == 'textFormatted') { return $this->textFormatted; + + } else if($key == 'depth') { + return $this->depth(); + + } else if($key === 'loaded') { + return $this->loaded; } return parent::get($key); @@ -250,6 +271,14 @@ class Comment extends WireData { return $value; } + /** + * Set property + * + * @param string $key + * @param mixed $value + * @return self|WireData + * + */ public function set($key, $value) { if(in_array($key, array('id', 'parent_id', 'status', 'flags', 'pages_id', 'created', 'created_users_id'))) { @@ -285,6 +314,11 @@ class Comment extends WireData { if($value < 1) $value = 0; if($value > 5) $value = 5; } + + if($key == 'parent_id' && parent::get('parent_id') != $value) { + // reset a cached parent value, if present + $this->_parent = null; + } return parent::set($key, $value); } @@ -355,26 +389,71 @@ class Comment extends WireData { public function gravatar($rating = 'g', $imageset = 'mm', $size = 80) { return self::getGravatar($this->email, $rating, $imageset, $size); } - + + /** + * Set Page that this Comment belongs to + * + * @param Page $page + * + */ public function setPage(Page $page) { $this->page = $page; } - + + /** + * Set Field that this Comment belongs to + * + * @param Field $field + * + */ public function setField(Field $field) { $this->field = $field; } - + + /** + * Get Page that this Comment belongs to + * + * @return null|Page + * + */ public function getPage() { return $this->page; } + /** + * Get Field that this Comment belongs to + * + * @return null|CommentField + * + */ public function getField() { return $this->field; } - + + /** + * Set whether Comment is fully loaded and ready for use + * + * To get loaded state access the $loaded property of the Comment object. + * + * #pw-internal + * + * @param bool $loaded + * + */ public function setIsLoaded($loaded) { $this->loaded = $loaded ? true : false; } + + /** + * Get current comment depth + * + * @return int + * @since 3.0.149 + * + */ + public function depth() { + return count($this->parents()); + } /** * Return the parent comment, if applicable @@ -384,11 +463,10 @@ class Comment extends WireData { */ public function parent() { if(!is_null($this->_parent)) return $this->_parent; - $field = $this->getField(); - if(!$field->depth) return null; $parent_id = $this->parent_id; if(!$parent_id) return null; - $comments = $this->getPage()->get($field->name); + $field = $this->getField(); + $comments = $this->getPage()->get($field->name); // no getPageComments() call intentional $parent = null; foreach($comments as $c) { if($c->id != $parent_id) continue; @@ -399,6 +477,25 @@ class Comment extends WireData { return $parent; } + /** + * Get CommentArray of all parent comments for this one + * + * Order is closest parent to furthest parent + * + * @return CommentArray + * @since 3.0.149 + * + */ + public function parents() { + $parents = $this->getPageComments()->makeNew(); + $parent = $this->parent(); + while($parent && $parent->id) { + $parents->add($parent); + $parent = $parent->parent(); + } + return $parents; + } + /** * Return children comments, if applicable * @@ -407,10 +504,11 @@ class Comment extends WireData { */ public function children() { /** @var CommentArray $comments */ - $comments = $this->getPageComments(); - $children = $comments->makeNew(); + // $comments = $this->getPageComments(); $page = $this->getPage(); $field = $this->getField(); + $comments = $this->getPage()->get($field->name); + $children = $comments->makeNew(); if($page) $children->setPage($this->getPage()); if($field) $children->setField($this->getField()); $id = $this->id; @@ -422,7 +520,40 @@ class Comment extends WireData { } /** - * Get array that holds all the comments for the current Page/Field + * Does this comment have the given child comment? + * + * @param int|Comment $comment Comment or Comment ID + * @param bool $recursive Check all descending children recursively? Use false to check only direct children. (default=true) + * @return bool + * @since 3.0.149 + * + */ + public function hasChild($comment, $recursive = true) { + + $id = $comment instanceof Comment ? $comment->id : (int) $comment; + $has = false; + $children = $this->children(); + + // direct children + foreach($children as $child) { + if($child->id == $id) $has = true; + if($has) break; + } + + if($has || !$recursive) return $has; + + // recursive children + foreach($children as $child) { + /** @var Comment $child */ + if($child->hasChild($id, true)) $has = true; + if($has) break; + } + + return $has; + } + + /** + * Get CommentArray that holds all the comments for the current Page/Field * * #pw-internal * @@ -431,11 +562,31 @@ class Comment extends WireData { * */ public function getPageComments($autoDetect = true) { - if($autoDetect && !$this->pageComments) { - $field = $this->getField(); - $this->pageComments = $this->getPage()->get($field->name); + + $pageComments = $this->pageComments; + $page = $this->getPage(); + $field = $this->getField(); + + if($pageComments && $autoDetect) { + // check if the CommentsArray doesn't share the same Page/Field as the Comment + // this could be the case if CommentsArray was from search results rather than Page value + $pageCommentsPage = $pageComments->getPage(); + $pageCommentsField = $pageComments->getField(); + if($page && $pageCommentsPage && "$page" !== "$pageCommentsPage") { + $pageComments = null; + } else if($field && $pageCommentsField && "$field" !== "$pageCommentsField") { + $pageComments = null; + } } - return $this->pageComments; + + if(!$pageComments && $autoDetect) { + if($page && $field) { + $pageComments = $page->get($field->name); + $this->pageComments = $pageComments; + } + } + + return $pageComments; } /** @@ -508,6 +659,8 @@ class Comment extends WireData { /** * Return URL to edit comment * + * @return string + * */ public function editUrl() { if(!$this->page || !$this->page->id) return ''; diff --git a/wire/modules/Fieldtype/FieldtypeComments/CommentArray.php b/wire/modules/Fieldtype/FieldtypeComments/CommentArray.php index 834e02ed..7f48867b 100644 --- a/wire/modules/Fieldtype/FieldtypeComments/CommentArray.php +++ b/wire/modules/Fieldtype/FieldtypeComments/CommentArray.php @@ -324,6 +324,29 @@ class CommentArray extends PaginatedArray implements WirePaginatable { parent::trackAdd($item, $key); if(!$item->getPageComments(false)) $item->setPageComments($this); } + + /** + * Does this CommentArray have the given Comment (or comment ID)? + * + * Note: this method is very specific in purpose, accepting only a Comment object or ID. + * You can use the has() method for more flexibility. + * + * @param Comment|int $comment + * @return bool + * @since 3.0.149 + * @see WireArray::has() + * + */ + public function hasComment($comment) { + $commentID = $comment instanceof Comment ? $comment->id : (int) $comment; + $has = false; + foreach($this as $item) { + if($item->id !== $commentID) continue; + $has = true; + break; + } + return $has; + } } diff --git a/wire/modules/Fieldtype/FieldtypeComments/CommentField.php b/wire/modules/Fieldtype/FieldtypeComments/CommentField.php index 14d8e1ed..8660163b 100644 --- a/wire/modules/Fieldtype/FieldtypeComments/CommentField.php +++ b/wire/modules/Fieldtype/FieldtypeComments/CommentField.php @@ -5,8 +5,28 @@ * * Custom “Field” class for Comments fields. * - * ProcessWire 3.x, Copyright 2019 by Ryan Cramer + * ProcessWire 3.x, Copyright 2020 by Ryan Cramer * https://processwire.com + * + * @property int $moderate + * @property int|bool $redirectAfterPost + * @property int|bool $quietSave + * @property string $notificationEmail + * @property string $fromEmail + * @property int $notifySpam + * @property int $useNotify See Comment::flagNotify* constants + * @property int|bool $useAkismet + * @property int $deleteSpamDays + * @property int $depth + * @property int|bool $sortNewest + * @property int|bool $useWebsite + * @property string $dateFormat + * @property int $useVotes + * @property int $useStars + * @property string $useGravatar + * @property int $schemaVersion + * + * @todo Some more methods from FieldtypeComments can be moved into this class * */ @@ -98,6 +118,131 @@ class CommentField extends Field { return $this->getFieldtype()->voteComment($page, $this, $comment, $up); } + /** + * Allow given Comment to have given parent comment? + * + * @param Comment $comment + * @param Comment|int $parent + * @param bool $verbose Report reason why not to standard errors? (default=false) + * @return bool + * @since 3.0.149 + * + */ + public function allowCommentParent(Comment $comment, $parent, $verbose = false) { + + $parentID = $parent instanceof Comment ? (int) $parent->id : (int) $parent; + if($parentID === 0) return true; // comment with no parent is always allowed + + $error = "Comment $comment->id cannot be reply-to comment $parentID — "; + $commentField = $comment->getField(); + $commentPage = $comment->getPage(); + + if(!$commentField) $commentField = $this; + + if("$commentField" !== "$this") { + if($verbose) $this->error("$error Comments cannot be moved between fields ($commentField != $this)"); + return false; + } + + if($parentID == $comment->id) { + if($verbose) $this->error("$error Comment cannot be its own parent"); + return false; + } + + $maxDepth = (int) $this->get('depth'); + if(!$maxDepth) { + if($verbose) $this->error("$error Comment depth is not enabled in field settings"); + return false; + } + + // determine if current page even has the requested parent comment + $parentComment = false; /** @var bool|Comment $parentComment */ + $pageComments = $commentPage ? $commentPage->get($commentField->name) : array(); + foreach($pageComments as $pageComment) { + if($pageComment->id === $parentID) $parentComment = $pageComment; + if($parentComment) break; + } + // if($parentComment) $this->message("Found parent comment $parentComment on page " . $comment->getPage()); + + // if comment is not present here at all, do not allow as a parent + if(!$parentComment) { + if($verbose) $this->error("$error Page $commentPage does not have parent comment $parentID"); + return false; + } + + // if depth would exceed max allowed depth, comment not allowed + if($parentComment->depth() >= $maxDepth) { + if($verbose) $this->error("$error Exceeds max allowed depth setting ($maxDepth)"); + return false; + } + + // if this comment already has the given one as a child, it cannot be its parent + if($comment->hasChild($parentID, true)) { + if($verbose) $this->error("$error Comment $parentID is already a child of comment $comment->id"); + return false; + } + + return true; + } + + /** + * Allow given comment to live on given page? + * + * @param Comment $comment + * @param Page $page + * @param bool $verbose Report reason why not to standard errors? (default=false) + * @return bool + * @since 3.0.149 + * + */ + public function allowCommentPage(Comment $comment, Page $page, $verbose = false) { + $error = "Comment $comment->id cannot be on page $page->id — "; + + // check if page has the current comment field + $commentField = $comment->getField(); + if(!$commentField) $commentField = $this; + if(!$page->hasField($commentField)) { + if($verbose) $this->error("$error Page does not have field: $commentField"); + return false; + } + + // if comment is already assigned to the Page then it is allowed + $commentPage = $comment->getPage(); + if($commentPage && $commentPage->id === $page->id) return true; + + // check if comment has a parent comment + $parentID = $comment->parent_id; + if($parentID) { + $pageComments = $page->get($commentField->name); + if(!$pageComments || !$pageComments->hasComment($parentID)) { + if($verbose) $this->error("$error Comment has parent comment $parentID which does not exist on page $page->id"); + return false; + } + } + + return true; + } + + /** + * May the given comment be deleted? + * + * @param Comment $comment + * @return bool + * + */ + public function allowDeleteComment(Comment $comment) { + $children = $comment->children(); + if(!$children->count()) return true; + $allow = true; + foreach($children as $child) { + if($child->id > 0 && $child->status < Comment::statusDelete) { + $allow = false; + break; + } + } + return $allow; + } + /** * @return FieldtypeComments|Fieldtype * diff --git a/wire/modules/Fieldtype/FieldtypeComments/FieldtypeComments.module b/wire/modules/Fieldtype/FieldtypeComments/FieldtypeComments.module index 5e52cc46..30d16c1b 100644 --- a/wire/modules/Fieldtype/FieldtypeComments/FieldtypeComments.module +++ b/wire/modules/Fieldtype/FieldtypeComments/FieldtypeComments.module @@ -1289,7 +1289,6 @@ class FieldtypeComments extends FieldtypeMulti { $pageID = $row['pages_id']; if(isset($commentPages[$pageID])) { $page = $commentPages[$pageID]; - $comment->setPage($commentPages[$pageID]); } else { $page = $this->wire('pages')->get((int) $pageID); $commentPages[$page->id] = $page;