Rev 22 | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** Comments Manager** Manage all comments field data in chronological order.** ProcessWire 3.x* Copyright (C) 2022 by Ryan Cramer* This file licensed under Mozilla Public License v2.0 http://mozilla.org/MPL/2.0/** https://processwire.com** @method string renderComment(Comment $comment, array $options = array())**/class ProcessCommentsManager extends Process {/*** Return information about this module (required)**/public static function getModuleInfo() {return array('title' => __('Comments', __FILE__),'summary' => __('Manage comments in your site outside of the page editor.', __FILE__),'version' => 12,'author' => 'Ryan Cramer','icon' => 'comments','requires' => 'FieldtypeComments','searchable' => 'comments','permission' => 'comments-manager','permissions' => array('comments-manager' => 'Use the comments manager',),'page' => array('name' => 'comments','parent' => 'setup','title' => 'Comments',),'nav' => array(array('url' => '?go=approved','label' => __('Approved', __FILE__),),array('url' => '?go=pending','label' => __('Pending', __FILE__),),array('url' => '?go=spam','label' => __('Spam', __FILE__),),array('url' => '?go=all','label' => __('All', __FILE__),)));}/*** Statuses and names that a Comment can have**/protected $statuses = array();/*** Translated statuses**/protected $statusTranslations = array();/*** Translated flags** @var array**/protected $notifyFlagsTranslations = array();/*** Number of comments to show per page**/protected $limit = 10;/*** Headline for masthead**/protected $headline = '';/*** Translated 'All' label** @var string**/protected $labelAll = 'All';/*** Initialize the comments manager and define the statuses**/public function init() {$this->wire()->modules->get('FieldtypeComments');parent::init();$this->statuses = array(Comment::statusFeatured => 'featured',Comment::statusApproved => 'approved',Comment::statusPending => 'pending',Comment::statusSpam => 'spam',Comment::statusDelete => 'delete');$this->statusTranslations = array(Comment::statusFeatured => $this->_('Featured'),Comment::statusApproved => $this->_('Approved'),Comment::statusPending => $this->_('Pending'),Comment::statusSpam => $this->_('Spam'),Comment::statusDelete => $this->_('Delete'));$this->labelAll = $this->_('All');$this->notifyFlagsTranslations = array(0 => $this->_('No'),Comment::flagNotifyAll => $this->labelAll,Comment::flagNotifyReply => $this->_('Replies'),);if(wireClassExists("CommentStars")) {$config = $this->config;$cssFile = $config->urls('FieldtypeComments') . 'comments.css';$jsFile = $config->urls('FieldtypeComments') . 'comments.js';$config->styles->add($cssFile);$config->scripts->add($jsFile);CommentStars::setDefault('star', wireIconMarkup('star'));}}/*** Ask the user to select which comments field they want to manage** Or, redirect to the comments field if there is only 1.** @return string**/public function ___execute() {$this->checkInstall();// locate all the FieldtypeComments fields$fields = array();foreach($this->fields as $field) {/** @var Field $field */if($field->type instanceof FieldtypeComments) $fields[] = $field;}$count = count($fields);if(!$count) {$error = $this->_('There are no comments fields installed');$this->error($error);return "<p>$error</p>";}$go = $this->wire()->sanitizer->pageName($this->wire('input')->get('go'));if($count == 1 || $go) {$field = reset($fields);$to = 'all';if($go && in_array($go, $this->statuses)) $to = $go;$this->wire()->session->redirect("./list/$field->name/$to/");return '';}$out = "<h2>" . $this->_('Please select a comments field') . "</h2><ul>";foreach($fields as $field) {$out .= "<li><a href='./list/$field->name/pending/'>$field->name</a></li>";}$out .= "</ul>";return $out;}/*** Execute the comments list** @return string**/public function ___executeList() {$session = $this->wire('session'); /** @var Session $session */$input = $this->wire('input'); /** @var WireInput $input */$sanitizer = $this->wire('sanitizer'); /** @var Sanitizer $sanitizer */$page = $this->wire('page'); /** @var Page $page */$commentID = (int) $input->get('id');$name = $sanitizer->fieldName($input->urlSegment2);if(!$name) return $this->error($this->_('No comments field specified in URL'));$field = $this->fields->get($name);if(!$field || !$field->type instanceof FieldtypeComments) return $this->error($this->_('Unrecognized field'));$status = $input->urlSegment3;if(empty($status) || ($status != 'all' && !in_array($status, $this->statuses))) {$redirectUrl = $page->url() . "list/$field->name/all/";if($commentID) $redirectUrl .= "?id=$commentID";$session->redirect($redirectUrl);}$statusNum = array_search($status, $this->statuses);$headline = $statusNum !== false ? $this->statusTranslations[$statusNum] : $status;if($headline === 'all') $headline = $this->labelAll;$this->breadcrumb('../', $field->getLabel());$limit = (int) $input->get('limit');if($limit) {$session->setFor($this, 'limit', $limit);$session->redirect('./');} else {$limit = (int) $session->getFor($this, 'limit');if(!$limit) $limit = (int) $this->limit;}$sort = $sanitizer->name($input->get('sort'));if($sort) {$session->setFor($this, 'sort', $sort);$session->redirect('./');} else {$sort = $session->getFor($this, 'sort');if(!$sort) $sort = '-created';}$start = ($input->pageNum() - 1) * $limit;$selector = "start=$start, limit=$limit, sort=$sort";if($status != 'all') $selector .= ", status=$statusNum";$filterOut = '';$filterLabels = array('id' => $this->_('ID'),'parent_id' => $this->_('Replies to'),'pages_id' => $this->_('Page'),);$properties = array('cite','email','text','ip','id','pages_id','parent_id','stars');$q = $input->get('q');if($q !== null) {// query $q that contain a selector$q = trim($sanitizer->text($q, array('stripTags' => false)));$op = Selectors::stringHasOperator($q, true);if($op) {list($property, $value) = explode($op, $q, 2);$property = $sanitizer->fieldName($property);if(!in_array($property, $properties)) $property = '';} else {$property = 'text';$op = strpos($q, ' ') ? '~=' : '%=';$value = $q;}if($property && $value) {$selector .= ", $property$op" . $sanitizer->selectorValue($value);$input->whitelist('q', "$property$op$value");}$filterOut .= $sanitizer->entities(", $property$op$value");}foreach($properties as $key) {$value = $input->get($key);if(is_null($value)) continue;if($key == 'id' || $key == 'pages_id' || $key == 'parent_id' || $key == 'stars') {$value = (int) $value;} else {$value = trim($sanitizer->text($value));}$input->whitelist($key, $value);$value = $sanitizer->selectorValue($value);$selector .= ", $key=$value";$filterLabel = isset($filterLabels[$key]) ? $filterLabels[$key] : ucfirst($key);$filterOut .= $sanitizer->entities(", $filterLabel: $value");}/** @var FieldtypeComments $fieldtype */$fieldtype = $field->type;$comments = $fieldtype->find($field, $selector);if($input->post('processComments')) {$this->processComments($comments, $field);}if($filterOut) {$this->breadcrumb('./', $headline);$headline = trim($filterOut, ", ");}$this->headline = $headline;return $this->renderComments($comments);}/*** Execute the comment meta viewer/editor** @return string* @since 3.0.203**/public function ___executeMeta() {$input = $this->wire()->input;$commentId = (int) $input->get('comment_id');$fieldId = (int) $input->get('field_id');$pageId = (int) $input->get('page_id');$modal = (int) $input->get('modal');if($commentId < 1 || $fieldId < 1 || $pageId < 1) {throw new WireException("Missing one of: comment_id, field_id, page_id");}$field = $this->wire()->fields->get($fieldId);$fieldtype = $field->type; /** @var FieldtypeComments $fieldtype */if(!$field || (!$fieldtype instanceof FieldtypeComments)) {throw new WireException("Invalid field specified");}$page = $this->wire()->pages->get($pageId);if(!$page->id || !$page->hasField($field) || !$page->editable($field)) {throw new WireException("Invalid or non-editable page specified");}$comment = $fieldtype->getCommentByID($page, $field, $commentId);if(!$comment) throw new WireException("Cannot find comment $commentId");$meta = $comment->getMeta();$flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;if(defined('JSON_UNESCAPED_LINE_TERMINATORS')) $flags = $flags | JSON_UNESCAPED_LINE_TERMINATORS;$metaJSON = empty($meta) ? '{ }' : json_encode($meta, $flags);$numRows = substr_count($metaJSON, "\n")+1;if($numRows < 10) $numRows = 10;$this->headline(sprintf($this->_('Meta data for comment #%d'), $commentId));if(empty($meta)) $this->message($this->_('Comment currently has no meta data'));// $out = "<pre id='meta-preview'>$metaEncodedJSON</pre>";$modules = $this->wire()->modules;/** @var InputfieldForm $form */$form = $modules->get('InputfieldForm');$form->attr('method', 'post');$form->attr('action', "./?comment_id=$commentId&page_id=$pageId&field_id=$fieldId&modal=$modal");/** @var InputfieldTextarea $f */$f = $modules->get('InputfieldTextarea');$f->attr('name', 'meta');$f->label = $this->_('Meta JSON editor');$f->val($metaJSON);$f->attr('rows', $numRows);$f->attr('style', "font-family:monospace;");$form->add($f);/** @var InputfieldSubmit $f */$f = $modules->get('InputfieldSubmit');$f->attr('name', 'submit_save');$f->val($this->_('Save changes'));$form->add($f);/** @var InputfieldSubmit $f */$f = $modules->get('InputfieldButton');$f->attr('name', 'submit_cancel');$f->setSecondary();$f->val($this->_('Cancel'));$form->add($f);if($input->post('submit_save')) {$session = $this->wire()->session;$form->processInput($input->post);$metaInput = $form->getChildByName('meta');$metaSaveJSON = $metaInput->val();if($metaSaveJSON === $metaJSON) {$this->message($this->_('No changes detected'));$session->redirect($form->attr('action'));} else {$meta = json_decode($metaSaveJSON, true);if(is_array($meta)) {$result = $fieldtype->updateComment($page, $field, $comment, array('meta' => $meta));if($result) {$this->message(sprintf($this->_('Updated meta for comment #%d'), $commentId));$session->redirect($form->attr('action'));} else {$this->error(sprintf($this->_('Error updating meta for comment #%d'), $commentId));}} else {$this->error($this->_('Cannot save because invalid JSON'));}}}$out = $form->render();return $out;}/*** Edit or add comment** @return string* @throws WireException* @throws WirePermissionException**/protected function ___executeEdit() {$sanitizer = $this->wire()->sanitizer;$input = $this->wire()->input;$user = $this->wire()->user;$submit = false;$out = '';if($input->post('submit_save_comment')) {$submit = true;$pageId = (int) $input->post('page_id');$fieldId = (int) $input->post('field_id');$parentId = (int) $input->post('parent_id');$commentId = (int) $input->post('comment_id');} else {$pageId = (int) $input->get('page_id');$fieldId = (int) $input->get('field_id');$parentId = (int) $input->get('parent_id');$commentId = (int) $input->get('comment_id');}$page = $this->wire()->pages->get($pageId);$field = $this->wire()->fields->get($fieldId); /** @var CommentField $field */if(!$page->id) throw new WireException("Cannot find page $pageId");if(!$field) throw new WireException("Cannot find field $fieldId");if(!$page->hasField($field)) throw new WireException("Page $pageId does not have field $field->name");if($commentId) {// editing existing comment$comment = $field->getCommentByID($page, $commentId);if(!$comment) throw new WireException('Comment not found');if($comment->getField()->id != $field->id) throw new WireException('Invalid field');if($comment->getPage()->id != $page->id) throw new WireException('Invalid page');$this->headline(sprintf($this->_('Edit comment #%d'), $comment->id));} else {// adding new comment$comment = new Comment();$this->wire($comment);$comment->setPage($page);$comment->setField($field);$comment->cite = $user->get('title|name');$comment->email = $user->get('email');$comment->created = time();$comment->status = Comment::statusApproved;if($parentId) {// new comment that is reply to existing comment$parentComment = $field->getCommentByID($page, $parentId);if(!$parentComment) throw new WireException("Cannot find parent comment $parentId");$comment->parent_id = $parentId;$this->headline(sprintf($this->_('Comment #%d by %s'), $parentId, $parentComment->getFormatted('cite')));// show comment being replied to$text = $sanitizer->entities1($parentComment->getFormattedCommentText());$when = date('Y/m/d H:i', $parentComment->created) . ' • ' . wireRelativeTimeStr($parentComment->created);$out .= "<p class='detail'>$when</p>";$out .= "<blockquote><p>$text</p></blockquote>";$out .= "<h2>" . $this->_('Your reply') . "</h2>";}}$form = $this->buildEditForm($page, $field, $comment);if($submit) {$this->processEditForm($form, $page, $field, $comment);}return $out . $form->render();}/*** Process comment edit form** @param InputfieldForm $form* @param Page $page* @param CommentField $field* @param Comment $comment* @return bool**/protected function processEditForm(InputfieldForm $form, Page $page, CommentField $field, Comment $comment) {$input = $this->wire()->input;$form->processInput($input->post);if(count($form->getErrors())) return false;$values = array();$properties = array('cite','email','website','text','created','status');if($field->useVotes) {$properties[] = 'upvotes';$properties[] = 'downvotes';}foreach($properties as $name) {$f = $form->getChildByName($name);if(!$f) continue;$value = $f->val();if($value != $comment->get($name)) {$comment->set($name, $value);$values[$name] = $value;}}if($field->useStars) {$stars = (int) $input->post('stars');if($stars != $comment->stars && $stars > 0 && $stars <= 5) {$comment->stars = $stars;$values['stars'] = $stars;}}$f = $field->useNotify ? $form->getChildByName('notify_author') : null;if($f) {$notify = (int) $f->val();if($this->applyCommentNotifyFlag($comment, $notify)) {$values['flags'] = $comment->flags;}}/** @var Inputfield $f */$f = $form->getChildByName('page_id');$pageId = $f->val();if($pageId != $page->id) {$f->error('Changing page is not currently supported with this form');return false;/*$newPage = $this->wire()->pages->get($pageId);if(!$newPage->editable()) {$f->error($this->_('Selected page is not editable'));return false;}if(!$newPage->hasField($field)) {$f->error(sprintf($this->_('Selected page does not have field: %s'), $field->name));return false;}*/}if($comment->id) {// existing commentif($comment->status === Comment::statusDelete) {$success = $field->deleteComment($page, $comment);if($success) {$this->message(sprintf($this->_('Deleted comment #%d'), $comment->id));$this->wire()->session->location("../list/$field->name/all/");}} else {$success = $field->updateComment($page, $comment, $values);if($success) $this->message(sprintf($this->_('Updated comment #%d'), $comment->id));}} else {// new comment/** @var Inputfield $f */$f = $form->getChildByName('notify');$notify = $f && $f->val();$comment->status = Comment::statusApproved;$success = $field->addComment($page, $comment, $notify);if($success) $this->message(sprintf($this->_('Added comment #%d'), $comment->id));}if($success) {$this->wire()->session->location("../list/$field->name/all/?id=$comment->id");} else {$this->error($this->_('Error saving comment'));}return $success;}/*** Build comment edit/add form** @param Page $page* @param CommentField $field* @param Comment $comment* @return InputfieldForm**/protected function buildEditForm(Page $page, CommentField $field, Comment $comment) {$modules = $this->wire()->modules;/** @var InputfieldForm $form */$form = $modules->get('InputfieldForm');/** @var InputfieldText $f */$f = $modules->get('InputfieldText');$f->attr('name', 'cite');$f->label = $this->_('Cite / author');$f->val($comment->cite);$f->required = true;$f->columnWidth = 50;$form->add($f);/** @var InputfieldEmail $f */$f = $modules->get('InputfieldEmail');$f->attr('name', 'email');$f->label = $this->_('Email');$f->val($comment->email);$f->columnWidth = 50;$f->required = true;$form->add($f);/** @var InputfieldSelect $f */$f = $modules->get('InputfieldSelect');$f->attr('name', 'status');$f->label = $this->_('Status');$f->required = true;foreach($this->statusTranslations as $status => $statusText) {$f->addOption($status, $statusText);}$f->val($comment->status);$f->columnWidth = 50;$form->add($f);/** @var InputfieldDatetime $f */$f = $modules->get('InputfieldDatetime');$f->attr('name', 'created');$f->label = $this->_('Created');$f->inputType = 'html';$f->htmlType = 'datetime';$f->val($comment->created);$f->required = true;$f->columnWidth = 50;$form->add($f);if($field->useWebsite) {/** @var InputfieldText $f */$f = $modules->get('InputfieldUrl');$f->attr('name', 'website');$f->label = $this->_('Website/URL');$f->val($comment->website);$form->add($f);}/** @var InputfieldTextarea $f */$f = $modules->get('InputfieldTextarea');$f->attr('name', 'text');$f->label = $this->_('Text');$f->val($comment->text);$f->attr('rows', 10);$f->required = true;$form->add($f);if($field->useVotes) {/** @var InputfieldInteger $f */$f = $modules->get('InputfieldInteger');$f->attr('name', 'upvotes');$f->label = $this->_('Upvotes');$f->val($comment->upvotes);$f->columnWidth = $field->useStars ? 25 : 50;$f->inputType = 'number';$form->add($f);/** @var InputfieldInteger $f */$f = $modules->get('InputfieldInteger');$f->attr('name', 'downvotes');$f->label = $this->_('Downvotes');$f->val($comment->upvotes);$f->columnWidth = $field->useStars ? 25 : 50;$f->inputType = 'number';$form->add($f);}if($field->useStars) {/** @var InputfieldMarkup $f */$f = $modules->get('InputfieldMarkup');$f->attr('name', 'stars');$f->label = $this->_('Stars');$f->value ="<input type='hidden' name='stars' value='$comment->stars' />" .$comment->renderStars(array('input' => true));if($field->useVotes) $f->columnWidth = 50;$form->add($f);}/** @var InputfieldRadios $f */$f = $modules->get('InputfieldRadios');$f->attr('name', 'notify_author');$f->label = $this->_('Notify the email listed above when');$f->addOption(0, $this->_('Never'));if($field->depth) $f->addOption(Comment::flagNotifyReply, $this->_('There is a reply to this comment'));$f->addOption(Comment::flagNotifyAll, $this->_('Anytime a new comment is posted on this page'));if($comment->flags & Comment::flagNotifyAll) {$f->val(Comment::flagNotifyAll);} else if($comment->flags & Comment::flagNotifyReply) {$f->val(Comment::flagNotifyReply);} else {$f->val(0);}$form->add($f);/** @var InputfieldToggle $f */if(!$comment->id) {$f->columnWidth = 50;$f = $modules->get('InputfieldToggle');$f->attr('name', 'notify');$f->label = $this->_('Allow notifications to others?');$f->detail = $this->_('When “Yes”, emails about this new comment will be sent to users that have opted in to receive notifications.');$f->val(true);$f->columnWidth = 50;$form->add($f);}/** @var InputfieldPageListSelect $f *//*$f = $modules->get('InputfieldPageListSelect');$f->attr('name', 'page_id');$f->label = $this->_('Page');$f->val($page->id);$form->add($f);*//** @var InputfieldHidden $f */$f = $modules->get('InputfieldHidden');$f->attr('name', 'page_id');$f->val($page->id);$form->add($f);/** @var InputfieldHidden $f */$f = $modules->get('InputfieldHidden');$f->attr('name', 'field_id');$f->val($field->id);$form->add($f);/** @var InputfieldHidden $f */$f = $modules->get('InputfieldHidden');$f->attr('name', 'comment_id');$f->val($comment->id);$form->add($f);/** @var InputfieldHidden $f */$f = $modules->get('InputfieldHidden');$f->attr('name', 'parent_id');$f->val($comment->parent_id);$form->add($f);/** @var InputfieldSubmit $f */$f = $modules->get('InputfieldSubmit');$f->attr('name', 'submit_save_comment');$f->showInHeader();$form->add($f);return $form;}/*** Process changes to posted comments** @param CommentArray $comments* @param CommentField $field**/protected function processComments(CommentArray $comments, Field $field) {$input = $this->wire()->input;$numDeleted = 0;$numChanged = 0;$isSuperuser = $this->user->isSuperuser();$allowChangeParent = $isSuperuser && $field->get('depth') > 0;$allowChangePage = $isSuperuser;$commentField = $field instanceof CommentField ? $field : null;/** @var FieldtypeComments $fieldtype */$fieldtype = $field->type;foreach($comments as $comment) {/** @var Comment $comment */$properties = array();$text = $input->post("CommentText$comment->id");if(!is_null($text) && $text != $comment->text) {$comment->text = $text; // cleans it$properties['text'] = $comment->text;$numChanged++;}if($field->get('useVotes')) {foreach(array("upvotes", "downvotes") as $name) {$votes = $input->post("Comment" . ucfirst($name) . $comment->id);if($votes !== null) {$votes = (int) $votes;if($votes != $comment->$name) {$comment->set($name, $votes);$properties[$name] = $comment->$name;$numChanged++;}}}}if($field->get('useStars')) {$stars = $input->post("CommentStars$comment->id");if($stars !== null) {$stars = (int) $stars;if($stars != $comment->stars) {$comment->set('stars', $stars);$properties['stars'] = $comment->stars;$numChanged++;}}}$_status = $input->post("CommentStatus$comment->id");$status = (int) $_status;if($status === Comment::statusDelete && (!$commentField || $commentField->allowDeleteComment($comment))) {if($fieldtype->deleteComment($comment->getPage(), $field, $comment)) {$this->message(sprintf($this->_('Deleted comment #%d'), $comment->id));$numDeleted++;}continue;}if($_status !== null && $status !== (int) $comment->status && array_key_exists($status, $this->statuses)) {$comment->status = $status;$numChanged++;$properties['status'] = $comment->status;}$notify = $input->post("CommentNotify$comment->id");if($notify !== null && $field->useNotify && ctype_digit($notify)) {$notify = (int) $notify;if($this->applyCommentNotifyFlag($comment, $notify)) {$properties['flags'] = $comment->flags;}}$changePage = null;if($allowChangePage) {// check for change of Page ID$pageID = (int) $input->post("CommentPage$comment->id");if($pageID > 0 && "$pageID" !== "$comment->page") {$page = $this->wire()->pages->get($pageID);$parentID = $comment->parent_id;if($parentID) $comment->parent_id = 0; // temporarily set to 0 for page changeif(!$page->id) {$this->error(sprintf($this->_('Unable to find page: %d'), $pageID));} else if(!$page->hasField($field)) {$this->error(sprintf($this->_('Page %d does not have field: %s'), $pageID, "$field"));} else if($commentField && !$commentField->allowCommentPage($comment, $page, true)) {// this one reports errors on its own} else {$this->message(sprintf($this->_('Moved comment #%1$d from page %2$d to page %3$d'), $comment->id, $comment->page->id, $pageID));$properties['pages_id'] = $pageID;if($comment->parent_id) {$comment->parent_id = 0;$properties['parent_id'] = 0;}$changePage = $page;$numChanged++;}if($changePage === null) {// if page was not changed, restore back to original parentif($parentID) $comment->parent_id = $parentID;}}}$changeParentID = null;if($allowChangeParent) {// check for change of parent on threaded comment$parentID = $input->post("CommentParent$comment->id");if(strlen("$parentID") && ctype_digit("$parentID")) {// allows for parent_id "0" but ignore blank$parentID = (int) $parentID;if($parentID != $comment->parent_id) {if(!empty($properties['pages_id'])) {// we will apply the parent change after the Page change has applied$changeParentID = $parentID;} else {// parent ID has changed to another parent, or to no parent (0)if($commentField && $commentField->allowCommentParent($comment, $parentID, true)) {$comment->parent_id = $parentID;$properties['parent_id'] = $parentID;$numChanged++;}}}} else {$parentID = null;}}if(count($properties)) {$fieldtype->updateComment($comment->getPage(), $field, $comment, $properties);$this->message(sprintf($this->_('Updated comment #%d'), $comment->id) . " (" . implode(', ', array_keys($properties)) . ")");}if($changeParentID !== null && $changePage !== null) {// parent ID has changed at the same time that Page ID changed, so we apply parentID change afterwards$comment->setPage($changePage);if($commentField && $commentField->allowCommentParent($comment, $changeParentID, true)) {$comment->parent_id = $changeParentID;$fieldtype->updateComment($changePage, $field, $comment, array('parent_id' => $changeParentID));$numChanged++;}}}if($numDeleted || $numChanged) {$pageNum = $input->pageNum() > 1 ? 'page' . $input->pageNum() : '';$this->session->redirect('./' . $pageNum . $this->getQueryString());}}/*** Render the markup for a single comment** Hookable since 3.0.204** @param Comment $comment* @param array $options Options (since 3.0.204)* @return string**/protected function ___renderComment(Comment $comment, array $options = array()) {$defaults = array('prependMarkup' => '','appendMarkup' => '','prependContentMarkup' => '','appendContentMarkup' => '',);$options = array_merge($defaults, $options);$sanitizer = $this->sanitizer;$page = $comment->getPage();$pageTitle = $sanitizer->entities1($page->get('title|name'));$field = $comment->getField();$adminTheme = $this->wire()->adminTheme;$isSuperuser = $this->user->isSuperuser();$allowDepth = $field->depth > 0;$allowDepthChange = $isSuperuser && $allowDepth;$allowPageChange = $isSuperuser;$parent = $comment->parent();$numChildren = 0;$text = $this->renderCommentText($comment);$id = $comment->id;/** @var JqueryUI $jQueryUI */$jQueryUI = $this->wire()->modules->get('JqueryUI');$jQueryUI->use('modal');$icons = array('edit' => 'edit','upvote' => 'arrow-up','downvote' => 'arrow-down','changed' => 'dot-circle-o','reply' => 'angle-double-down','replies' => 'angle-double-right','pageEdit' => 'pencil','pageView' => 'external-link-square','commentEdit' => 'pencil','commentReply' => 'reply','meta' => 'sliders',);$outs = array('status' => '','notify' => '','website' => '','stars' => '','votes' => '','page' => '','parent' => '','reply' => '','where' => '','children' => '',);$classes = array('input' => 'CommentInput','textarea' => '','radio' => '','checkbox' => '','table' => '');$labels = array('edit' => $this->_('edit'),'view' => $this->_('view'),'meta' => $this->_('meta'),'page' => $this->_('Page'),'date' => $this->_('When'),'status' => $this->_('Status'),'action' => $this->_('Action'),'cite' => $this->_('Cite'),'website' => $this->_('Web'),'email' => $this->_('Mail'),'none' => $this->_('None'),'parent' => $this->_('Parent'),'where' => $this->_('Where'),'reply' => $this->_('reply'),'replyTo' => $this->_('Reply to %s'),'stars' => $this->_('Stars'),'votes' => $this->_('Votes'),'notify' => $this->_('Notify'),'commentId' => $this->_('Comment ID'),'commentEdit' => $this->_('editor'),);$values = array('cite' => $comment->cite,'email' => $comment->email,'website' => $comment->website,'ip' => $comment->ip,'date' => wireDate($this->_('Y/m/d g:i a'), $comment->created),'dateRelative' => wireDate('relative', $comment->created),'parent' => $parent && $parent->id ? $parent->id : '','parentPlaceholder' => $parent && $parent->id ? '' : $labels['none'],'parentCite' => sprintf($labels['replyTo'], $labels['commentId']),'stars' => $comment->stars ? $comment->stars : '','upvotes' => $comment->upvotes,'downvotes' => $comment->downvotes,'page' => (int) "$comment->page",);$urls = array('parent' => $parent ? "../all/?id=$parent->id" : '','children' => "../all/?parent_id=$id",'siblings' => "../all/?pages_id=$page->id",'pageView' => "$page->url#Comment$id",'pageEdit' => $page->editUrl(),'metaEdit' => "../../../meta/?comment_id=$comment->id&page_id=$page->id&field_id=$field->id&modal=1",'commentReply' => "../../../edit/?parent_id=$comment->id&page_id=$page->id&field_id=$field->id",'commentEdit' => "../../../edit/?comment_id=$comment->id&page_id=$page->id&field_id=$field->id",'email' => "./?email=" . urlencode($values['email']),'cite' => "../all/?cite=" . urlencode($values['cite']),'ip' => "../all/?ip=" . urlencode($values['ip']),);$tooltips = array('parent' => $this->_('ID of the Comment that this one is replying to.'),'page' => $this->_('ID of the Page that this comment lives on.'),'viewAll' => $this->_('View all having value'),'viewPage' => $this->_('View page'),'edit' => $this->_('Edit value'),'editPage' => $this->_('Edit page'),'pageFilter' => $this->_('Show only comments from page'),'pageEdit' => $this->_('Edit page'),'pageView' => $this->_('View page'),'commentEdit' => $this->_('Comment editor'),'commentReply' => $this->_('Reply to this comment'),'meta' => $this->_('View/edit metadata'),);foreach($values as $key => $value) {$values[$key] = $sanitizer->entities($value);}foreach($icons as $key => $value) {$icons[$key] = wireIconMarkup($value, 'fw');}if($allowDepth) {$children = $comment->children();$numChildren = count($children);}if($adminTheme instanceof AdminThemeFramework) {$classes['input'] = trim("$classes[input] " . $adminTheme->getClass('input-small'));$classes['textarea'] = $adminTheme->getClass('textarea');$classes['radio'] = $adminTheme->getClass('input-radio');$classes['checkbox'] = $adminTheme->getClass('input-checkbox');$classes['table'] = $adminTheme->getClass('table');$classes['select'] = $adminTheme->getClass('select-small');// if(strpos($classes['input'], 'uk-input') !== false) $classes['input'] .= " uk-form-blank";}$outs['status'] = "<select name='CommentStatus$id' class='CommentStatus $classes[select]'>";foreach($this->statusTranslations as $status => $label) {if($status == Comment::statusDelete && $numChildren) continue;$selected = $comment->status == $status ? " selected='selected' " : '';$outs['status'] .= "<option value='$status'$selected>$label</option>";/*// $checked = $comment->status == $status ? "checked='checked' " : '';$outs['status'] .="<label class='CommentStatus'>" ."<input class='$classes[radio]' type='radio' name='CommentStatus$id' value='$status' $checked/> " ."<small>$label</small>" ."</label> ";*/}$outs['status'] .= "</select>";if($page->editable()) {$text ="<div class='CommentTextEditable' id='CommentText$id' data-textarea-class='$classes[textarea]'>" ."<p>$text <a class='CommentTextEdit' href='#'>$icons[edit] $labels[edit]</a></p>" ."</div>";} else {$text = "<p>$text</p>";}if($allowPageChange) $outs['page'] ="<span class='detail ui-priority-secondary'>Page #</span> " ."<input class='$classes[input] pw-tooltip' title='$tooltips[page]' name='CommentPage$id' value='$values[page]' /> ";if($allowDepthChange) $outs['parent'] ="<span class='detail ui-priority-secondary'>" . $this->_('Reply to #') . "</span> " ."<input class='$classes[input] pw-tooltip' title='$tooltips[parent]' placeholder='$values[parentPlaceholder]' name='CommentParent$id' value='$values[parent]' />";if($outs['parent'] || $outs['page']) $outs['where'] ="<tr>" ."<th>$labels[where]</th>" ."<td class='CommentWhere'>$outs[page]$outs[parent]</td>" ."</tr>";if($values['website']) $outs['website'] ="<tr>" ."<th>$labels[website]</th>" ."<td><a target='_blank' href='$values[website]'>$values[website]</a></td>" ."</tr>";if($field->useStars) $outs['stars'] ="<tr>" ."<th>$labels[stars]</th>" ."<td class='CommentStars'>" ."<input type='hidden' name='CommentStars$id' value='$values[stars]' />" .$comment->renderStars(array('input' => true)) ."</td>" ."</tr>";if($field->useVotes) $outs['votes'] ="<tr>" ."<th>$labels[votes]</th>" ."<td class='CommentVotes'>" ."<label class='CommentUpvotes'>" ."<span>$icons[upvote]</span>" ."<input class='$classes[input]' title='upvotes' type='number' min='0' name='CommentUpvotes$id' value='$values[upvotes]' />" ."</label> " ."<label class='CommentDownvotes'>" ."<span>$icons[downvote]</span>" ."<input class='$classes[input]' title='downvotes' type='number' min='0' name='CommentDownvotes$id' value='$values[downvotes]' />" ."</label> " ."</td>" ."</tr>";if($field->useNotify) {foreach($this->notifyFlagsTranslations as $flag => $label) {$checked = false;if($flag && $comment->flags & $flag) {$checked = true; // All or Replies} else if(!$flag && !($comment->flags & 2) && !($comment->flags & 4) && !($comment->flags & 8)) {$checked = true; // None}$checked = $checked ? "checked='checked' " : '';$outs['notify'] .="<label class='CommentNotify'>" ."<input class='$classes[radio]' type='radio' name='CommentNotify$id' value='$flag' $checked/> " ."<small>$label</small>" ."</label> ";}$outs['notify'] = "<tr><th>$labels[notify]</th><td>$outs[notify]</td></tr>";}if($parent) {$a = $sanitizer->entities($parent->cite) . " <a href='$urls[parent]'>$parent->id</a>";$outs['reply'] = // displayed after table"<p class='CommentReplyInfo detail'>" ."$icons[reply] " .sprintf($labels['replyTo'], $a) ."</p>";}if($numChildren) $outs['children'] = // displayed after table"<p class='CommentChildrenInfo detail'>" ."<a href='$urls[children]'>" ."$icons[replies] " .sprintf($this->_n('%d reply', '%d replies', $numChildren), $numChildren) ."</a>" ."</p>";/** @var FieldtypeComments $fieldtype */// $fieldtype = $field->type;// $who = $fieldtype->getNotifyEmails($page, $field, $comment);// $text .= "<pre>" . htmlentities(print_r($who, true)) . "</pre>";$numRows = 0;foreach($outs as $out) if(!empty($out)) $numRows++;$contentClass = 'CommentContent';if($numRows >= 7) $contentClass .= ' CommentContentLarge';$meta = $comment->getMeta();$metaCnt = count($meta);$metaTip = $tooltips['meta'] . ' (' . sprintf($this->_n('%d value', '%d values', $metaCnt), $metaCnt) . ')';$statusLinks = array("<a class='pw-tooltip' title='$tooltips[commentEdit]' href='$urls[commentEdit]'>$icons[commentEdit] $labels[commentEdit]</a>","<a class='pw-tooltip pw-modal' title='$metaTip' href='$urls[metaEdit]' data-buttons='button' data-close='button[name=submit_cancel]'>$icons[meta] $labels[meta]</a>",);if($comment->allowChildren()) {$statusLinks[] = "<a class='pw-tooltip' title='$tooltips[commentReply]' href='$urls[commentReply]'>$icons[commentReply] $labels[reply]</a>";}$out ="<div class='CommentItem ui-helper-clearfix CommentItemStatus$comment->status'>" .$options['prependMarkup'] ."<table class='CommentItemInfo $classes[table]' cellspacing='0'>" ."<tr class='CommentTitle'>" ."<th>" ."<label>" ."<input class='CommentCheckbox $classes[checkbox]' type='checkbox' name='comment[]' value='$id' /> " ."<span class='CommentID'>$id</span>" ."</label>" ."</th>" ."<td>" ."<a href='$urls[siblings]' class='pw-tooltip' title='$tooltips[pageFilter]'><strong>$pageTitle</strong></a> " ."<span class='detail'> " ."<a class='detail pw-tooltip' title='$tooltips[pageView]' href='$urls[pageView]'>$labels[view]</a> / " ."<a class='detail pw-tooltip' title='$tooltips[pageEdit]' href='$urls[pageEdit]'>$labels[edit]</a>" ."</span>" ."<span class='CommentChangedIcon'>$icons[changed]</span>" ."</td>" ."</tr>" ."<tr class='CommentItemStatus'>" ."<th>$labels[action]</th>" ."<td class='CommentStatus'>" .$outs['status'] . " " .implode(' ', $statusLinks) ."</td>" ."</tr>" .$outs['notify'] ."<tr>" ."<th>$labels[date]</th>" ."<td>$values[date] <span class='detail'>$values[dateRelative]</span></td>" ."</tr>" ."<tr>" ."<th>$labels[cite]</th>" ."<td class='CommentCite'>" ."<a href='$urls[cite]' class='pw-tooltip' title='$tooltips[viewAll]'>$values[cite]</a> " .//"<input type='text' class='$classes[input]' name='CommentCite$id' value='$values[cite]' hidden /> " . // @todo//"<a href='#' class='CommentToggleSiblings pw-tooltip' title='$tooltips[edit]'>$icons[edit]</a> " ."<a class='detail pw-tooltip' title='$tooltips[viewAll]' href='$urls[ip]'>$values[ip]</a> " ."</td>" ."</tr>" ."<tr>" ."<th>$labels[email]</th>" ."<td>" ."<a href='$urls[email]' class='pw-tooltip' title='$tooltips[viewAll]'>$values[email]</a> " .//"<input type='email' class='$classes[input]' name='CommentEmail$id' value='$values[email]' hidden /> " . // @todo//"<a href='#' class='CommentToggleSiblings pw-tooltip' title='$tooltips[edit]'>$icons[edit]</a>" ."</td>" ."</tr>" .$outs['website'] .$outs['stars'] .$outs['votes'] .$outs['where'] ."</table>" ."<div class='$contentClass'>" .$options['prependContentMarkup'] ."$outs[reply]" ."<div class='CommentText'>$text</div>" ."$outs[children]" .$options['appendContentMarkup'] ."</div>" .$options['appendMarkup'] ."</div>";/*$out ="<div class='CommentItem ui-helper-clearfix CommentItemStatus{$comment->status}'>" ."$out " ."<div class='CommentContent'>" ."$outs[parent]" ."<div class='CommentText'>$text</div>" ."$outs[children]" ."</div>" ."</div>";*/$page->of(false);return $out;}/*** Prep comment text for output in editor** @param Comment $comment* @return string**/protected function renderCommentText(Comment $comment) {$text = $this->sanitizer->entities($comment->get('text'));$text = str_replace('\r', ' ', $text);$text = preg_replace('/\r?(\n)/', '\r', $text);$text = str_replace('\r\r', "<br />\n<br />\n", $text);$text = str_replace('\r', "<br />\n", $text);return $text;}/*** Render the comments list header** @param int $limit* @param bool $useVotes* @param bool $useStars* @return string**/protected function renderCommentsHeader($limit, $useVotes, $useStars) {$setStatusLabel = $this->_('Set status:');$perPageLabel = $this->_('per page');$adminTheme = $this->wire('adminTheme');$selectClass = $adminTheme instanceof AdminThemeFramework ? $adminTheme->getClass('select') : '';$checkboxClass = $adminTheme instanceof AdminThemeFramework ? $adminTheme->getClass('input-checkbox') : '';$pagerLimitOut = "<select class='$selectClass' id='CommentLimitSelect'><option>10 $perPageLabel</option><option>25 $perPageLabel</option><option>50 $perPageLabel</option><option>100 $perPageLabel</option></select>";$pagerLimitOut = str_replace("<option>$limit ", "<option selected>$limit ", $pagerLimitOut);$checkAllLabel = $this->_('Check/uncheck all');$checkAll ="<label class='pw-tooltip' title='$checkAllLabel'><input class='$checkboxClass' type='checkbox' id='CommentCheckAll' /> " ."<span class='detail'></span></label>";$noCheckedLabel = $this->_('There are no checked items');$actionsOut ="<select class='$selectClass' id='CommentActions' data-nochecked='$noCheckedLabel'>" ."<option value=''>" . $this->_('Actions (checked items)') . "</option>";foreach($this->statusTranslations as $status => $label) {$actionsOut .= "<option value='$status'>$setStatusLabel $label</option>";}if($useVotes) {$actionsOut .= "<option value='reset-upvotes'>" . $this->_('Reset: Upvotes') . "</option>";$actionsOut .= "<option value='reset-downvotes'>" . $this->_('Reset: Downvotes') . "</option>";}$actionsOut .= "</select>";$sorts = array('-created' => $this->_('Date (new–old)'),'created' => $this->_('Date (old–new)'),);if($useStars) {$sorts['-stars'] = $this->_('Stars (high–low)');$sorts['stars'] = $this->_('Stars (low–high)');}if($useVotes) {$sorts['upvotes'] = $this->_('Upvotes');$sorts['downvotes'] = $this->_('Downvotes');}$sortByOut = "<select class='$selectClass' id='CommentListSort'>";$sortLabelPrefix = $this->_('Sort:');foreach($sorts as $sortKey => $sortLabel) {$sortByOut .= "<option value='$sortKey'>$sortLabelPrefix $sortLabel</option>";}$sortByOut .= "</select>";$sort = $this->wire('session')->getFor($this, 'sort');if(empty($sort)) $sort = "-created";$sortByOut = str_replace("'$sort'", "'$sort' selected", $sortByOut);return"<p class='CommentCheckAll'>$checkAll</p>" ."<p class='CommentActions'>$actionsOut</p>" ."<p class='CommentSorts'>$sortByOut</p>" ."<p class='CommentLimitSelect'>$pagerLimitOut</p>";}/*** Render the markup for a list of comments** @param CommentArray $comments* @return string**/protected function renderComments(CommentArray $comments) {$input = $this->wire()->input;$commentsBody = '';$cnt = 0;$status = $input->urlSegment3;$start = $comments->getStart();$limit = $comments->getLimit();$total = $comments->getTotal();$pageNumPrefix = $this->config->pageNumUrlPrefix;$pageNum = $input->pageNum();$queryString = $this->getQueryString();$unsavedChangesLabel = $this->_('You have unsaved changes!');$field = $comments->getField();$pagesId = (int) $input->get('pages_id');if($pagesId) $this->breadcrumb("./$queryString", "Page $pagesId");foreach($comments as $comment) {/** @var Comment $comment */if($status && $status != 'all' && $this->statuses[$comment->status] != $status) continue;$commentsBody .= $this->renderComment($comment, array());$cnt++;if($cnt >= $limit) break;}/** @var MarkupPagerNav $pager */$pager = $this->wire()->modules->get('MarkupPagerNav');$pagerOut = $pager->render($comments, array('queryString' => $queryString,'baseUrl' => "./"));/** @var JqueryWireTabs $wireTabs */$wireTabs = $this->modules->get('JqueryWireTabs');$tabs = array();$class = $input->urlSegment3 === 'all' ? 'on' : '';$tabs["tabStatusAll"] = "<a class='$class' href='../all/$queryString'>" . $this->labelAll . "</a>";foreach($this->statuses as $status => $name) {if($status == Comment::statusDelete) continue;$class = $input->urlSegment3 === $name ? 'on' : '';$label = $this->statusTranslations[$status];if($label === $name) $label = ucfirst($label);$tabs["tabStatus$status"] = "<a class='$class' href='../$name/$queryString'>$label</a>";}$tabsOut = $wireTabs->renderTabList($tabs);if($total) {$this->headline .= ' (' . ($start + 1) . "–" . ($start + $cnt) . " " . sprintf($this->_('of %d'), $total) . ')';} else {$this->headline .= ' (0)';}$this->headline($this->headline);if($cnt) {/** @var InputfieldSubmit $button */$button = $this->modules->get('InputfieldSubmit');$button->attr('name', 'processComments');$button->showInHeader();$button = $button->render();} else $button = '';if($input->pageNum > 1) {$queryString = "./$pageNumPrefix$pageNum$queryString";}if(!count($comments)) {return"<form>" .$tabsOut ."<h2>" . $this->_('None to display') . "</h2>" ."</form>";}$commentsHeader = $this->renderCommentsHeader($limit, $field->get('useVotes'), $field->get('useStars'));return"<form id='CommentListForm' action='$queryString' method='post' data-unsaved='$unsavedChangesLabel'>" .$tabsOut ."<div id='CommentListHeader' class='ui-helper-clearfix'>" .$pagerOut .$commentsHeader ."</div>" ."<div class='CommentItems ui-helper-clearfix'>" .$commentsBody ."</div>" .$pagerOut .$button ."</form>";}protected function getQueryString() {$queryString = '';foreach($this->input->whitelist as $key => $value) {$queryString .= $this->wire('sanitizer')->entities($key) . "=" . $this->wire('sanitizer')->entities($value) . "&";}$queryString = trim($queryString, '&');if($queryString) $queryString = "?$queryString";return $queryString;}/*** Apply the "notify" flag value to comment** @param Comment $comment* @param int $notify* @return bool Returns true if flags were changed, false if not**/protected function applyCommentNotifyFlag(Comment $comment, $notify) {$notify = (int) $notify;$flags = $comment->flags;$flagsPrev = $flags;if($notify === 0 && $comment->flags) {// remove flagif($comment->flags & Comment::flagNotifyAll) $flags = $flags & ~Comment::flagNotifyAll; // removeif($comment->flags & Comment::flagNotifyReply) $flags = $flags & ~Comment::flagNotifyReply; // removeif($comment->flags & Comment::flagNotifyConfirmed) $flags = $flags & ~Comment::flagNotifyConfirmed; // remove} else if($notify === Comment::flagNotifyAll && !($flags & Comment::flagNotifyAll)) {if($flags & Comment::flagNotifyReply) $flags = $flags & ~Comment::flagNotifyReply; // remove$flags = $flags | Comment::flagNotifyAll; // add} else if($notify === Comment::flagNotifyReply) {if($flags & Comment::flagNotifyAll) $flags = $flags & ~Comment::flagNotifyAll; // remove$flags = $flags | Comment::flagNotifyReply; // add}if($notify && !$comment->id) {// comments added in admin do not need email confirmation$flags = $flags | Comment::flagNotifyConfirmed;}if($flags != $flagsPrev) {$comment->flags = $flags;return true;}return false;}protected function checkInstall() {if($this->wire('modules')->isInstalled('ProcessLatestComments')) {$this->warning('Please uninstall the ProcessLatestComments module (this module replaces it).');}}public function ___install() {$this->checkInstall();parent::___install();}/*** Search for items containing $text and return an array representation of them** Implementation for SearchableModule interface** @param string $text Text to search for* @param array $options Options to modify behavior:* - `edit` (bool): True if any 'url' returned should be to edit items rather than view them* - `multilang` (bool): If true, search all languages rather than just current (default=true).* - `start` (int): Start index (0-based), if pagination active (default=0).* - `limit` (int): Limit to this many items, if pagination active (default=0, disabled).* @return array**/public function search($text, array $options = array()) {$sanitizer = $this->wire()->sanitizer;$page = $this->getProcessPage();$commentFields = array();foreach($this->wire()->fields as $field) {/** @var Field $field */if($field->type instanceof FieldtypeComments) $commentFields[$field->name] = $field;}$result = array('title' => $page->id ? (string) $page->title : $this->className(),'total' => 0,'url' => '','items' => array(),'properties' => array('text','cite','email','status','created','website','ip','user_agent','upvotes','downvotes','stars'));if($options['help']) return $result;$operator = empty($options['operator']) ? '%=' : $options['operator'];$value = $sanitizer->selectorValue($text);$summaryLength = $options['verbose'] ? 1024 : 200;if(!empty($options['property'])) {$selector = "$options[property]$operator$value";$q = "$options[property]$operator$text";} else {$selector = "text$operator$value";$q = "text$operator$text";}if(!empty($options['limit'])) $selector .= ", limit=$options[limit]";if(!empty($options['start'])) $selector .= ", start=$options[start]";foreach($commentFields as $field) {/** @var CommentField $field *//** @var CommentArray $comments */$comments = $field->type->find($selector);if(!count($comments)) continue;$result['total'] += $comments->getTotal();$url = $page->url() . "list/$field->name/all/";if(count($commentFields) === 1) {$result['url'] = $url . "?q=" . urlencode($q);}foreach($comments as $comment) {/** @var Comment $comment */$commentPage = $comment->getPage();$editUrl = $url . "?id=$comment->id";$item = array('id' => $comment->id,'name' => $comment->cite,'title' => $comment->cite,'subtitle' => $commentPage && $commentPage->id ? (string) $commentPage->get('title|name') : '','summary' => $sanitizer->truncate($comment->text, $summaryLength),'url' => empty($options['edit']) ? $comment->url() : $editUrl);$result['items'][] = $item;}}/* // for debugging:$result['items'][] = array('id' => 0,'name' => '','title' => $selector,'subtitle' => '','summary' => '','url' => '#');*/return $result;}}