['onPluginsInitialized', 0]]; } /** * Add the comment form information to the page header dynamically * * Used by Form plugin >= 2.0 */ public function onFormPageHeaderProcessed(Event $event) { $header = $event['header']; if ($this->enable) { if (!isset($header->form)) { $header->form = $this->grav['config']->get('plugins.comments.form'); } } $event->header = $header; } public function onTwigSiteVariables() { $this->grav['twig']->enable_comments_plugin = $this->enable; $this->grav['twig']->commenting_enabled = $this->commenting_enabled; $this->grav['twig']->pingbacks_enabled = $this->pingbacks_enabled; $this->grav['twig']->comments = $this->fetchComments(); //$this->grav['twig']->pingbacks = $this->fetchPingbacks(); $this->grav['twig']->akismet_enabled = $this->akismet_enabled; //$this->grav['twig']->recent_comments = $this->getRecentComments(); //cannot be used for functions with arguments $function = new Twig_SimpleFunction('recent_comments', [$this, 'getRecentComments']); $this->grav['twig']->twig()->addFunction($function); if ($this->config->get('plugins.comments.built_in_css')) { $this->grav['assets']->addCss('plugin://comments/assets/comments.css'); } $this->grav['assets']->add('jquery', 101)->addJs('plugin://comments/assets/comments.js'); } /** * Determine if the plugin should be enabled based on the enable_on_routes and disable_on_routes config options */ private function calculateEnable() { $uri = $this->grav['uri']; $disable_on_routes = (array)$this->config->get('plugins.comments.disable_on_routes'); $enable_on_routes = (array)$this->config->get('plugins.comments.enable_on_routes'); $callback = $this->config->get('plugins.comments.ajax_callback'); $path = $uri->path(); if ($callback === $path) { $this->enable = true; return; } if (!in_array($path, $disable_on_routes)) { if (in_array($path, $enable_on_routes)) { $this->enable = true; } else { foreach ($enable_on_routes as $route) { if (Utils::startsWith($path, $route)) { $this->enable = true; break; } } } } $this->commenting_enabled = $this->grav['config']->get('plugins.comments.commenting'); $this->pingbacks_enabled = $this->grav['config']->get('plugins.comments.pingbacks'); $this->akismet_enabled = $this->grav['config']->get('plugins.comments.akismet'); } /** * Frontend side initialization */ private function initializeFrontend() { $this->enable(['onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], ]); if ($this->enable) { $this->enable(['onPageInitialized' => ['onPageInitialized', 0], 'onFormProcessed' => ['onFormProcessed', 0], 'onFormPageHeaderProcessed' => ['onFormPageHeaderProcessed', 0], 'onTwigSiteVariables' => ['onTwigSiteVariables', 0]]); } $cache = $this->grav['cache']; $uri = $this->grav['uri']; //init cache id $this->comments_cache_id = md5('comments-data' . $cache->getKey() . '-' . $uri->url()); $this->pingbacks_cache_id = md5('pingbacks-data' . $cache->getKey() . '-' . $uri->url()); } /** * Admin side initialization */ private function initializeAdmin() { /** @var Uri $uri */ $uri = $this->grav['uri']; $this->enable(['onTwigTemplatePaths' => ['onTwigAdminTemplatePaths', 0], 'onAdminMenu' => ['onAdminMenu', 0], 'onAdminTaskExecute' => ['onAdminTaskExecute', 0], 'onAdminAfterSave' => ['onAdminAfterSave', 0], 'onAdminAfterDelete' => ['onAdminAfterDelete', 0], 'onDataTypeExcludeFromDataManagerPluginHook' => ['onDataTypeExcludeFromDataManagerPluginHook', 0], ]); if (strpos($uri->path(), $this->config->get('plugins.admin.route') . '/' . $this->route) === false) { return; } $page = $this->grav['uri']->param('page'); $comments = $this->getLastComments($page); if ($page > 0) { echo json_encode($comments); exit(); } $this->grav['twig']->comments = $comments; $this->grav['twig']->pages = $this->fetchPages(); } /** */ public function onPluginsInitialized() { $this->calculateEnable(); if ($this->isAdmin()) { $this->initializeAdmin(); } else { $this->initializeFrontend(); } } /** * Handle ajax call. */ public function onPageInitialized() { $is_ajax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; //$callback = $this->config->get('plugins.comments.ajax_callback'); // Process comment if required if ($is_ajax) { // || $callback === $this->grav['uri']->path() $action = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_STRING); switch ($action) { case 'addComment': case '': case null: // try to add the comment $result = $this->addComment(true); echo json_encode(['status' => $result[0], 'message' => $result[1], 'data' => $result[2], ]); break; case 'delete': // try to delete the comment $result = $this->deleteComment(true); echo json_encode(['status' => $result[0], 'message' => $result[1], 'data' => $result[2], ]); break; default: //request unknown, present error page //Set a 400 (bad request) response code. http_response_code(400); echo 'request malformed - action unknown'; break; } exit(); //prevents the page frontend from beeing displayed. } } /** * Validate ajax input before deleting comment * * @return boolean[]|string[]|array[][] */ private function deleteComment() { $language = $this->grav['language']; if (!$this->grav['user']->authorize('admin.super')) { http_response_code(403); return [false, 'access forbidden', [0, 0]]; } $id = filter_input(INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT); $nonce = filter_input(INPUT_POST, 'nonce', FILTER_SANITIZE_STRING); // ensure both values are sent if (is_null($id) || is_null($nonce)) { // Set a 400 (bad request) response code and exit. http_response_code(400); return [false, 'request malformed - missing parameter(s)', [0, 0]]; } if (!Utils::verifyNonce($nonce, 'comments')) { http_response_code(403); return [false, 'Invalid security nonce', [0, $nonce]]; } $lang = $this->grav['language']->getLanguage(); $path = $this->grav['page']->path(); $route = $this->grav['page']->route(); $data = $this->removeComment($route, $path, $id, $lang); if ($data[0]) { return [true, $language->translate('PLUGIN_COMMENTS.DELETE_SUCCESS'), $data[1]]; } else { http_response_code(403); //forbidden return [false, $language->translate('PLUGIN_COMMENTS.DELETE_FAIL'), $data[1]]; } } /** * Validate ajax input before adding comment * * @return boolean[]|string[]|array[][] */ private function addComment($raiseErrors=true) { if (!$this->active) { return [false, 'Comment plugin is currently disabled.', [0, 0]]; } if (!$this->commenting_enabled) { return [false, 'Commenting is currently disabled.', [0, 0]]; } $language = $this->grav['language']; if (!$_SERVER["REQUEST_METHOD"] == "POST") { // Not a POST request, set a 403 (forbidden) response code. if( $raiseErrors ) { http_response_code(403); } return [false, 'There was a problem with your submission, please try again.', [0, 0]]; } // get and filter the data if (!isset($_POST['data']) || !is_array($_POST['data'])) { // Set a 400 (bad request) response code and exit. if( $raiseErrors ) { http_response_code(400); } return [false, 'missing data', [0, 0]]; } $post = isset($_POST['data']) ? $_POST['data'] : []; $parent = filter_input(INPUT_POST, 'parent', FILTER_SANITIZE_NUMBER_INT); $name = isset($post['name']) ? filter_var($post['name'], FILTER_SANITIZE_STRING) : null; $email = isset($post['email']) ? filter_var($post['email'], FILTER_SANITIZE_EMAIL) : null; $text = isset($post['text']) ? filter_var($post['text'], FILTER_SANITIZE_STRING) : null; $date = isset($post['date']) ? filter_var($post['date'], FILTER_SANITIZE_STRING) : null; $site = isset($post['site']) ? filter_var($post['site'], FILTER_SANITIZE_URL) : null; $lang = isset($post['lang']) ? filter_var($post['lang'], FILTER_SANITIZE_STRING) : null; $path = isset($post['path']) ? filter_var($post['path'], FILTER_SANITIZE_STRING) : null; $formname = filter_input(INPUT_POST, 'form-name', FILTER_SANITIZE_STRING); $formnonce = filter_input(INPUT_POST, 'form-nonce', FILTER_SANITIZE_STRING); if (!Utils::verifyNonce($formnonce, 'comments')) { if( $raiseErrors ) { http_response_code(403); } return [false, 'Invalid security nonce', [0, $formnonce]]; } // ensure both values are sent if (is_null($text)) { // Set a 400 (bad request) response code and exit. http_response_code(400); return [false, 'missing text', [0, 0]]; //return [false, $language->translate('PLUGIN_COMMENTS.FAIL'), $data]; } // sanity checks for parents if ($parent < 0) { $parent = 0; } elseif ($parent > 999) { //TODO: Change to 'exists in list of comment ids $parent = 0; } $ip = $_SERVER['REMOTE_ADDR']; $lang = $this->grav['language']->getLanguage(); $path = $this->grav['page']->path(); $route = $this->grav['page']->route(); $user = $this->grav['user']->authenticated ? $this->grav['user']->username : ''; $isAdmin = $this->grav['user']->authorize('admin.login'); $comment = $this->saveComment($ip, $route, $path, $parent, $lang, $text, $name, $email, $site, $user, $isAdmin); //$comments = $this->fetchComments(); $data = array('parent' => $comment['parent'], 'id' => $comment['id'], 'text' => $comment['text'], 'name' => $comment['author'], 'date' => $comment['date'], 'authenticated' => !empty($comment['user']), 'isAdmin' => !empty($comment['isAdmin']), 'ADD_REPLY' => $language->translate('PLUGIN_COMMENTS.ADD_REPLY'), 'REPLY' => $language->translate('PLUGIN_COMMENTS.REPLY'), 'WRITTEN_ON' => $language->translate('PLUGIN_COMMENTS.WRITTEN_ON'), 'BY' => $language->translate('PLUGIN_COMMENTS.BY'),); return [true, $language->translate('PLUGIN_COMMENTS.SUCCESS'), $data]; } /** * Handle form processing instructions. * * @param Event $event */ private function removeComment($route, $path, $id, $lang) { $entry_removed = false; $message = ''; $date = time(); //date('D, d M Y H:i:s', time()); /******************************/ /** store comments with page **/ /******************************/ $localfilename = $path . '/comments.yaml'; $localfile = CompiledYamlFile::instance($localfilename); if (file_exists($localfilename)) { $data = $localfile->content(); if (isset($data['comments']) && is_array($data['comments'])) { foreach ($data['comments'] as $key => $comment) { if (!empty($comment['parent']) && $comment['parent'] == $id) { //hit an existing comment that is a reply to comment selected for deletion. //deletion of "parent" comment not allowed to preserve integrity of nested comments. //TODO: Alternatively allow it to mark parent comments as deleted // and make sure (via Comment class / setCommentLevels) that children are // filtered out from fetch regardless of their own deletion state. $data['comments'][$key] = array_merge(array('deleted' => ''), $comment); //set date after merge //reason: could be possible that "deleted" already exists (e.g. false or '') in $comment which would overwrite the first (newly added) occurence $data['comments'][$key]['deleted'] = $date; //no need to look further as ids are supposed to be unique. $localfile->save($data); $entry_removed = false; $reply_id = empty($comment['id']) ? '' : $comment['id']; $message = "Found active reply ($reply_id) for selected comment ($id)."; return [$entry_removed, $message]; break; } } foreach ($data['comments'] as $key => $comment) { if (!empty($comment['id']) && $comment['id'] == $id) { //add deleted as first item in array (better readability in file) $data['comments'][$key] = array_merge(array('deleted' => ''), $comment); //set date after merge //reason: could be possible that "deleted" already exists (e.g. false or '') in $comment which would overwrite the first (newly added) occurence $data['comments'][$key]['deleted'] = $date; //no need to look further as ids are supposed to be unique. $localfile->save($data); $entry_removed = true; $message = "Deleted comment ($id) via path ($path)"; break; } } } } else { //nothing } /**********************************/ /** store comments in index file **/ /**********************************/ $indexfilename = DATA_DIR . 'comments/index.yaml'; $indexfile = CompiledYamlFile::instance($indexfilename); if (file_exists($indexfilename)) { $dataIndex = $indexfile->content(); if (isset($dataIndex['comments']) && is_array($dataIndex['comments'])) { foreach ($dataIndex['comments'] as $key => $comment) { if (!empty($comment['page']) && !empty($comment['id']) && $comment['page'] == $route && $comment['id'] == $id) { //add deleted as first item in array (better readability in file) $dataIndex['comments'][$key] = array_merge(array('deleted' => ''), $comment); //set date after merge //reason: could be possible that "deleted" already exists (e.g. false or '') in $comment which would overwrite the first (newly added) occurence $dataIndex['comments'][$key]['deleted'] = $date; //no need to look further as ids are supposed to be unique. $indexfile->save($dataIndex); break; } } } } else { //nothing } //clear cache $this->grav['cache']->delete($this->comments_cache_id); $this->grav['cache']->delete($this->pingbacks_cache_id); return [$entry_removed, $message]; } private function validateComment($name, $email, $text, $site) { if ($this->akismet_enabled) { $key = $this->grav['config']->get('plugins.comments.akismet_key_api'); $url_override = $this->grav['config']->get('plugins.comments.akismet_site'); $url = !empty($url_override) ? $url_override : $_SERVER['HTTP_HOST']; $akismet = new \Akismet($url, $key); $akismet->setCommentAuthor($name); $akismet->setCommentAuthorEmail($email); $akismet->setCommentAuthorURL($site); $akismet->setCommentContent($text); //$akismet->setPermalink($comment->post->permalink); try { $approved = !$akismet->isCommentSpam() ? 'true' : 'false'; } catch(Exception $e) { //EventLog::log($e->getMessage(), 'notice', 'comment', 'HabariAkismet'); // TODO: admin needs to approve comment $approved = 'pending'; } return $approved; } return 'true'; } /** * Handle form processing instructions. * * @param Event $event */ private function saveComment($ip, $route, $path, $parent, $lang, $text, $name, $email, $site = "", $user = "", $isAdmin = false) { $date = date('D, d M Y H:i:s', time()); $approved = $this->validateComment($name, $email, $text, $site); /******************************/ /** store comments with page **/ /******************************/ $localfilename = $path . '/comments.yaml'; $localfile = CompiledYamlFile::instance($localfilename); if (file_exists($localfilename)) { $data = $localfile->content(); $data['autoincrement']++; } else { $data = array('autoincrement' => 1, 'comments' => array()); } $localid = $data['autoincrement']; $newComment = ['id' => $data['autoincrement'], 'ip' => $ip, 'parent' => $parent, 'lang' => $lang, 'text' => $text, 'date' => $date, 'author' => $name, 'email' => $email, 'site' => $site, 'user' => $user, 'approved' => $approved, 'isAdmin' => !empty($isAdmin), ]; $data['comments'][] = $newComment; $localfile->save($data); /**********************************/ /** store comments in index file **/ /**********************************/ $indexfilename = DATA_DIR . 'comments/index.yaml'; $indexfile = CompiledYamlFile::instance($indexfilename); if (file_exists($indexfilename)) { $data = $indexfile->content(); } else { $data = array('comments' => array()); } $data['comments'][] = ['page' => $route, 'id' => $localid, 'parent' => $parent, 'lang' => $lang, 'text' => $text, 'date' => $date, 'author' => $name, 'email' => $email, 'site' => $site, 'approved' => $approved, ]; $indexfile->save($data); //clear cache, don't let incoming spam thrash the cache. if ($approved == 'true') { $this->grav['cache']->delete($this->comments_cache_id); $this->grav['cache']->delete($this->pingbacks_cache_id); } return $newComment; } /** * Handle form processing instructions. * * @param Event $event */ public function onFormProcessed(Event $event) { $form = $event['form']; $action = $event['action']; $params = $event['params']; switch ($action) { case 'addComment': addComment(false); break; } } /** * Used to add a recent comments widget. Call {{ recent_comments(123,12) }} specifying an integer representing the result length. * * Returns three different arrays with stats and comments. * * @param integer $limit max amount of comments in result set * @param integer $limit_pages max amount of pages in result set * * @return array|array|array global stats, page stats, list of recent comments, options */ public function getRecentComments($limit, $limit_pages) { $routes = $this->grav['pages']->routes(); //routes[route] => path $paths = array_flip($routes); $cache = $this->grav['cache']; $options = array('comments_limit' => $limit, 'pages_limit' => $limit_pages,); //use cached stats if possible $recent_comments_cache_id = md5('comments-stats' . $cache->getKey()); if ($recent_comments = $cache->fetch($recent_comments_cache_id)) { //use cache only if limits are big enough if ($recent_comments['options']['comments_limit'] >= $options['comments_limit'] && $recent_comments['options']['pages_limit'] >= $options['pages_limit']) { return $recent_comments; } } $path = PAGES_DIR; $dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); $itrFilter = new \RecursiveIteratorIterator($dirItr, \RecursiveIteratorIterator::SELF_FIRST); $filesItr = new \RegexIterator($itrFilter, '/^.+comments\.yaml$/i'); $files = array(); $global_stats = array('active_entries' => 0, 'deleted_entries' => 0, 'active_comments' => 0, 'deleted_comments' => 0, 'active_replies' => 0, 'deleted_replies' => 0, 'pages_with_active_entries' => 0,); $page_stats = array(); $comments = array(); foreach ($filesItr as $filepath => $file) { if ($file->isDir()) { // this should never trigger as we are looking vor yamls only } else { $route = ''; $fileFolder = substr($filepath, 0, strlen($filepath) - strlen($file->getFilename()) - 1); if (!empty($paths[str_replace('/', '\\', $fileFolder) ])) $route = $paths[str_replace('/', '\\', $fileFolder) ]; if (!empty($paths[str_replace('\\', '/', $fileFolder) ])) $route = $paths[str_replace('\\', '/', $fileFolder) ]; $page_stats[$filepath] = array('active_entries' => 0, 'deleted_entries' => 0, 'active_comments' => 0, 'deleted_comments' => 0, 'active_replies' => 0, 'deleted_replies' => 0, 'latest_active_entry' => 0, 'route' => $route,); $localfile = CompiledYamlFile::instance($filepath); $localcomments = $localfile->content(); if (!empty($localcomments['comments']) && is_array($localcomments['comments'])) { foreach ($localcomments['comments'] as $comment) { if (!empty($comment['deleted'])) { empty($comment['parent']) ? $page_stats[$filepath]['deleted_comments']++ : $page_stats[$filepath]['deleted_replies']++; empty($comment['parent']) ? $global_stats['deleted_comments']++ : $global_stats['deleted_replies']++; $page_stats[$filepath]['deleted_entries']++; $global_stats['deleted_entries']++; } else { empty($comment['parent']) ? $page_stats[$filepath]['active_comments']++ : $page_stats[$filepath]['active_replies']++; empty($comment['parent']) ? $global_stats['active_comments']++ : $global_stats['active_replies']++; $page_stats[$filepath]['active_entries']++; $global_stats['active_entries']++; //use unix timestamp for comparing and sorting if (is_int($comment['date'])) { $time = $comment['date']; } else { $time = \DateTime::createFromFormat('D, d M Y H:i:s', $comment['date'])->getTimestamp(); } if (empty($page_stats[$filepath]['latest_active_entry']) || $page_stats[$filepath]['latest_active_entry'] < $time) { $page_stats[$filepath]['latest_active_entry'] = $time; } $comments[] = array_merge(array('path' => $filepath, 'route' => $route, 'time' => $time,), $comment); } } } if (!empty($page_stats[$filepath]['latest_active_entry'])) { $global_stats['pages_with_active_entries']++; } } } //most recent comments first usort($comments, function ($a, $b) { if ($a['time'] === $b['time']) return 0; if ($a['time'] < $b['time']) return 1; return -1; }); //most recent pages first usort($page_stats, function ($a, $b) { if ($a['latest_active_entry'] === $b['latest_active_entry']) return 0; if ($a['latest_active_entry'] < $b['latest_active_entry']) return 1; return -1; }); //reduce comments in output to limit if (!empty($limit) && $limit > 0 && $limit < count($comments)) { $comments = array_slice($comments, 0, $limit); } //reduce pages in output to limit if (!empty($limit_pages) && $limit_pages > 0 && $limit_pages < count($page_stats)) { $page_stats = array_slice($page_stats, 0, $limit_pages); } //save to cache if enabled $cache->save($recent_comments_cache_id, ['global_stats' => $global_stats, 'pages' => $page_stats, 'comments' => $comments, 'options' => $options]); return ['global_stats' => $global_stats, 'pages' => $page_stats, 'comments' => $comments, 'options' => $options]; } private function getFilesOrderedByModifiedDate($path = '') { $files = []; if (!$path) { $path = DATA_DIR . 'comments'; } if (!file_exists($path)) { Folder::mkdir($path); } $dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); $filterItr = new RecursiveFolderFilterIterator($dirItr); $itr = new \RecursiveIteratorIterator($filterItr, \RecursiveIteratorIterator::SELF_FIRST); $itrItr = new \RecursiveIteratorIterator($dirItr, \RecursiveIteratorIterator::SELF_FIRST); $filesItr = new \RegexIterator($itrItr, '/^.+\.yaml$/i'); // Collect files if modified in the last 7 days foreach ($filesItr as $filepath => $file) { $modifiedDate = $file->getMTime(); $sevenDaysAgo = time() - (7 * 24 * 60 * 60); if ($modifiedDate < $sevenDaysAgo) { continue; } $files[] = (object)array("modifiedDate" => $modifiedDate, "fileName" => $file->getFilename(), "filePath" => $filepath, "data" => Yaml::parse(file_get_contents($filepath))); } // Traverse folders and recurse foreach ($itr as $file) { if ($file->isDir()) { $this->getFilesOrderedByModifiedDate($file->getPath() . '/' . $file->getFilename()); } } // Order files by last modified date usort($files, function ($a, $b) { return !($a->modifiedDate > $b->modifiedDate); }); return $files; } private function getLastComments($page = 0) { $number = 30; $files = []; $files = $this->getFilesOrderedByModifiedDate(); $comments = []; foreach ($files as $file) { $data = Yaml::parse(file_get_contents($file->filePath)); for ($i = 0;$i < count($data['comments']);$i++) { $commentTimestamp = \DateTime::createFromFormat('D, d M Y H:i:s', $data['comments'][$i]['date'])->getTimestamp(); $data['comments'][$i]['pageTitle'] = $data['title']; $data['comments'][$i]['filePath'] = $file->filePath; $data['comments'][$i]['timestamp'] = $commentTimestamp; } if (count($data['comments'])) { $comments = array_merge($comments, $data['comments']); } } // Order comments by date usort($comments, function ($a, $b) { return !($a['timestamp'] > $b['timestamp']); }); $totalAvailable = count($comments); $comments = array_slice($comments, $page * $number, $number); $totalRetrieved = count($comments); return (object)array("comments" => $comments, "page" => $page, "totalAvailable" => $totalAvailable, "totalRetrieved" => $totalRetrieved); } /** * Return the comments associated to the current route */ public function fetchComments() { $cache = $this->grav['cache']; //search in cache if ($comments = $cache->fetch($this->comments_cache_id)) { return $comments; } $lang = $this->grav['language']->getLanguage(); $filename = $lang ? '/' . $lang : ''; $filename.= $this->grav['uri']->path() . '.yaml'; $comments = $this->getDataFromFilename($filename) ['comments']; $comments = $this->setCommentLevels($comments); //save to cache if enabled $cache->save($this->comments_cache_id, $comments); return $comments; } /** * Return the latest commented pages */ private function setCommentLevels($comments) { if (!is_array($comments)) { return $comments; } $levelsflat = array(); foreach ($comments as $key => $comment) { if (!empty($comment['deleted'])) { //if field "deleted" exists and is filled with a true value then ignore the comment completely. //TODO: This only works on this position as long as it is forbidden to delete comments that have active replies (children). // Otherwise implement that children get the deleted flag recursively or are ignored via Comment class. } else { $levelsflat[$comment['id']]['parent'] = $comment['parent']; $levelsflat[$comment['id']]['class'] = new Comment($comment['id'], $comments[$key]); } } //get starting points (entries without valid parent = root element) $leveltree = array(); foreach ($levelsflat as $id => $parent) { $parent = $parent['parent']; if (!isset($levelsflat[$parent])) { $leveltree[$id] = $levelsflat[$id]['class']; } else { $currentParent = $levelsflat[$parent]['class']; $currentChild = $levelsflat[$id]['class']; $levelsflat[$id]['class']->setParent($currentParent); $levelsflat[$parent]['class']->addSubComment($currentChild); } } //youngest comments first (DESC date), only root comments. Keep replies in ASC date order. //as long as comments are not editable, it is sufficient to reverse order from comment file $leveltree = array_reverse($leveltree, true); //reset comment values to nested order $comments = array(); foreach ($leveltree as $id => $comment) { $comments = array_merge($comments, $comment->getContent()); } return $comments; } /** * Return the pingbacks associated to the current route */ private function fetchPingbacks() { $cache = $this->grav['cache']; //search in cache if ($pingbacks = $cache->fetch($this->pingbacks_cache_id)) { return $pingbacks; } $lang = $this->grav['language']->getLanguage(); $filename = $lang ? '/' . $lang : ''; $filename.= $this->grav['uri']->path() . '.yaml'; $pingbacks = $this->getDataFromFilenameOld($filename) ['pingbacks']; //save to cache if enabled $cache->save($this->pingbacks_cache_id, $pingbacks); return $pingbacks; } /** * Return the latest commented pages */ private function fetchPages() { $files = []; $files = $this->getFilesOrderedByModifiedDate(); $pages = []; foreach ($files as $file) { $pages[] = ['title' => $file->data['title'], 'commentsCount' => count($file->data['comments']), 'lastCommentDate' => date('D, d M Y H:i:s', $file->modifiedDate) ]; } return $pages; } /** * Given a data file route, return the YAML content already parsed */ private function getDataFromFilename($fileRoute) { //Single item details //$fileInstance = CompiledYamlFile::instance(DATA_DIR . 'comments/' . $fileRoute); //Use comment file in page folder $fileInstance = CompiledYamlFile::instance($this->grav['page']->path() . '/comments.yaml'); if (!$fileInstance->content()) { //Item not found return; } return $fileInstance->content(); } private function getDataFromFilenameOld($fileRoute) { //Single item details $fileInstance = File::instance(DATA_DIR . 'comments/' . $fileRoute); if (!$fileInstance->content()) { //Item not found return; } return Yaml::parse($fileInstance->content()); } /** * Add templates directory to twig lookup paths. */ public function onTwigTemplatePaths() { $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; } /** * Add plugin templates path */ public function onTwigAdminTemplatePaths() { $this->grav['twig']->twig_paths[] = __DIR__ . '/admin/templates'; $this->grav['twig']->akismet_enabled = $this->akismet_enabled; if ($this->akismet_enabled) { $key = $this->grav['config']->get('plugins.comments.akismet_key_api'); $url_override = $this->grav['config']->get('plugins.comments.akismet_site'); $url = !empty($url_override) ? $url_override : $_SERVER['HTTP_HOST']; $akismet = new \Akismet($url, $key); if ($akismet->isKeyValid()) { $this->grav['twig']->warning_message = ""; } else { $this->grav['twig']->warning_message = sprintf("Akismet API key \"%s\" is invalid for the url \"%s\". Provide a correct url override or make sure you're registered. Please check to make sure the key is entered correctly.", $key, $url); } } } /** * Handle the Reindex task from the admin * * @param Event $e */ public function onAdminTaskExecute(Event $e) { if ($e['method'] == 'taskReindexComments') { $controller = $e['controller']; header('Content-type: application/json'); if (!$controller->authorizeTask('reindexComments', ['admin.configuration', 'admin.super'])) { $json_response = ['status' => 'error', 'message' => ' Index not created', 'details' => 'Insufficient permissions to reindex the comments index file.']; echo json_encode($json_response); exit; } /*TODO // disable warnings error_reporting(1); // capture content ob_start(); $this->gtnt->createIndex(); ob_get_clean(); list($status, $msg) = $this->getIndexCount(); $json_response = [ 'status' => $status ? 'success' : 'error', 'message' => ' ' . $msg ]; echo json_encode($json_response); */ exit; } } /** * Perform an 'add' or 'update' for comment data as needed * * @param $event * @return bool */ public function onAdminAfterSave($event) { $obj = $event['object']; if ($obj instanceof Page) { //nothing to do //save means, the page changed, but still exists } return true; } /** * Perform an 'add' or 'update' for comment data as needed * * @param $event * @return bool */ public function onAdminAfterDelete($event) { $obj = $event['object']; if ($obj instanceof Page) { //TODO $this->deleteComment($obj); //clear cache $this->grav['cache']->delete(md5('comments-stats' . $this->grav['cache']->getKey())); } return true; } /** * Add navigation item to the admin plugin */ public function onAdminMenu() { $this->grav['twig']->plugins_hooked_nav['PLUGIN_COMMENTS.COMMENTS'] = ['route' => $this->route, 'icon' => 'fa-file-text']; $options = ['authorize' => 'taskReindexComments', 'hint' => 'reindexes the comments index', 'class' => 'comments-reindex', 'icon' => 'fa-file-text']; $this->grav['twig']->plugins_quick_tray['PLUGIN_COMMENTS.COMMENTS'] = $options; } /** * Exclude comments from the Data Manager plugin */ public function onDataTypeExcludeFromDataManagerPluginHook() { $this->grav['admin']->dataTypesExcludedFromDataManagerPlugin[] = 'comments'; } }