__('Comments', __FILE__), 'summary' => __('Manage comments in your site outside of the page editor.', __FILE__), 'version' => 10, '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->notifyFlagsTranslations = array( 0 => $this->_('No'), Comment::flagNotifyAll => $this->_('All'), Comment::flagNotifyReply => $this->_('Replies'), ); $this->labelAll = $this->_('All'); } /** * 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) { if($field->type instanceof FieldtypeComments) $fields[] = $field; } $count = count($fields); if(!$count) { $error = $this->_('There are no comments fields installed'); $this->error($error); return "

$error

"; } $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 = "

" . $this->_('Please select a comments field') . "

"; return $out; } /** * Execute the comments list * * @return string * */ public function ___executeList() { 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')); } $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); } /** * Process changes to posted comments * * @param CommentArray $comments * @param Field $field * */ protected function processComments(CommentArray $comments, Field $field) { $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; /** @var WireInput $input */ $input = $this->wire('input'); 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 = (int) $input->post("Comment" . ucfirst($name) . $comment->id); if($votes != $comment->$name) { $comment->set($name, $votes); $properties[$name] = $comment->$name; $numChanged++; } } } if($field->get('useStars')) { $stars = (int) $input->post("CommentStars$comment->id"); 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($field->useNotify && ctype_digit($notify)) { $notify = (int) $notify; if($notify === 0 && $comment->flags) { if($comment->flags & Comment::flagNotifyAll) $comment->flags = $comment->flags & ~Comment::flagNotifyAll; if($comment->flags & Comment::flagNotifyReply) $comment->flags = $comment->flags & ~Comment::flagNotifyReply; if($comment->flags & Comment::flagNotifyConfirmed) $comment->flags = $comment->flags & ~Comment::flagNotifyConfirmed; $properties['flags'] = $comment->flags; } else if($notify === Comment::flagNotifyAll && !($comment->flags & Comment::flagNotifyAll)) { $comment->flags = $comment->flags | Comment::flagNotifyAll; $properties['flags'] = $comment['flags']; } else if($notify === Comment::flagNotifyReply) { $comment->flags = $comment->flags | Comment::flagNotifyReply; $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 change if(!$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 parent if($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 * * @param Comment $comment * @return string * */ protected function renderComment(Comment $comment) { $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; $icons = array( 'edit' => 'edit', 'upvote' => 'arrow-up', 'downvote' => 'arrow-down', 'changed' => 'dot-circle-o', 'reply' => 'angle-double-down', 'replies' => 'angle-double-right', ); $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'), 'page' => $this->_('Page'), 'date' => $this->_('When'), 'status' => $this->_('Status'), 'cite' => $this->_('Cite'), 'website' => $this->_('Web'), 'email' => $this->_('Mail'), 'none' => $this->_('None'), 'parent' => $this->_('Parent'), 'where' => $this->_('Where'), 'replyTo' => $this->_('Reply to %s'), 'stars' => $this->_('Stars'), 'votes' => $this->_('Votes'), 'notify' => $this->_('Notify'), 'commentId' => $this->_('Comment ID'), ); $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(), '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'), 'edit' => $this->_('Edit value'), 'pageFilter' => $this->_('Show only comments from page'), ); foreach($values as $key => $value) { $values[$key] = $sanitizer->entities($value); } foreach($icons as $key => $value) { $icons[$key] = wireIconMarkup($value); } if($allowDepth) { $children = $comment->children(); $numChildren = count($children); } if($adminTheme && $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'); // if(strpos($classes['input'], 'uk-input') !== false) $classes['input'] .= " uk-form-blank"; } foreach($this->statusTranslations as $status => $label) { if($status == Comment::statusDelete && $numChildren) continue; $checked = $comment->status == $status ? "checked='checked' " : ''; $outs['status'] .= "  "; } if($page->editable()) { $text = "
" . "

$text $icons[edit] $labels[edit]

" . "
"; } else { $text = "

$text

"; } if($allowPageChange) $outs['page'] = "Page # " . " "; if($allowDepthChange) $outs['parent'] = "" . $this->_('Reply to #') . " " . ""; if($outs['parent'] || $outs['page']) $outs['where'] = "" . "$labels[where]" . "$outs[page]$outs[parent]" . ""; if($values['website']) $outs['website'] = "" . "$labels[website]" . "$values[website]" . ""; if($field->useStars) $outs['stars'] = "" . "$labels[stars]" . "" . "" . $comment->renderStars(array('input' => true)) . "" . ""; if($field->useVotes) $outs['votes'] = "" . "$labels[votes]" . "" . " " . " " . "" . ""; 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'] .= "  "; } $outs['notify'] = "$labels[notify]$outs[notify]"; } if($parent) { $a = $sanitizer->entities($parent->cite) . " $parent->id"; $outs['reply'] = // displayed after table "

" . "$icons[reply] " . sprintf($labels['replyTo'], $a) . "

"; } if($numChildren) $outs['children'] = // displayed after table "

" . "" . "$icons[replies] " . sprintf($this->_n('%d reply', '%d replies', $numChildren), $numChildren) . "" . "

"; /** @var FieldtypeComments $fieldtype */ // $fieldtype = $field->type; // $who = $fieldtype->getNotifyEmails($page, $field, $comment); // $text .= "
" . htmlentities(print_r($who, true)) . "
"; $numRows = 0; foreach($outs as $out) if(!empty($out)) $numRows++; $contentClass = 'CommentContent'; if($numRows >= 7) $contentClass .= ' CommentContentLarge'; $out = "
" . "" . "" . "" . "" . "" . "" . "" . "" . "" . $outs['notify'] . "" . "" . "" . "" . "" . "" . "" . "" . "" . "" . "" . "" . $outs['website'] . $outs['stars'] . $outs['votes'] . $outs['where'] . "
" . "" . "" . "$pageTitle " . " " . "$labels[view] / " . "$labels[edit]" . "" . "$icons[changed]" . "
$labels[status]$outs[status]
$labels[date]$values[date] $values[dateRelative]
$labels[cite]" . "$values[cite] " . //" " . // @todo //"$icons[edit] " . "$values[ip] " . "
$labels[email]" . "$values[email] " . //" " . // @todo //"$icons[edit]" . "
" . "
" . "$outs[reply]" . "
$text
" . "$outs[children]" . "
" . "
"; /* $out = "
" . "$out " . "
" . "$outs[parent]" . "
$text
" . "$outs[children]" . "
" . "
"; */ $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', "
\n
\n", $text); $text = str_replace('\r', "
\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 && $adminTheme instanceof AdminThemeFramework ? $adminTheme->getClass('select') : ''; $checkboxClass = $adminTheme && $adminTheme instanceof AdminThemeFramework ? $adminTheme->getClass('input-checkbox') : ''; $pagerLimitOut = " "; $pagerLimitOut = str_replace("