1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Esther Brunner <wikidesign@gmail.com> 5 */ 6 7/** 8 * Class helper_plugin_discussion 9 */ 10class helper_plugin_discussion extends DokuWiki_Plugin 11{ 12 13 /** 14 * @return array 15 */ 16 public function getMethods() 17 { 18 $result = []; 19 $result[] = [ 20 'name' => 'th', 21 'desc' => 'returns the header of the comments column for pagelist', 22 'return' => ['header' => 'string'], 23 ]; 24 $result[] = [ 25 'name' => 'td', 26 'desc' => 'returns the link to the discussion section with number of comments', 27 'params' => [ 28 'id' => 'string', 29 'number of comments (optional)' => 'integer'], 30 'return' => ['link' => 'string'], 31 ]; 32 $result[] = [ 33 'name' => 'getThreads', 34 'desc' => 'returns pages with discussion sections, sorted by recent comments', 35 'params' => [ 36 'namespace' => 'string', 37 'number (optional)' => 'integer'], 38 'return' => ['pages' => 'array'], 39 ]; 40 $result[] = [ 41 'name' => 'getComments', 42 'desc' => 'returns recently added or edited comments individually', 43 'params' => [ 44 'namespace' => 'string', 45 'number (optional)' => 'integer'], 46 'return' => ['pages' => 'array'], 47 ]; 48 $result[] = [ 49 'name' => 'isDiscussionModerator', 50 'desc' => 'check if current user is member of moderator groups', 51 'params' => [], 52 'return' => ['isModerator' => 'boolean'] 53 ]; 54 return $result; 55 } 56 57 /** 58 * Returns the column header for the Pagelist Plugin 59 * 60 * @return string 61 */ 62 public function th() 63 { 64 return $this->getLang('discussion'); 65 } 66 67 /** 68 * Returns the link to the discussion section of a page 69 * 70 * @param string $id page id 71 * @param string $col column name, used if more columns needed per plugin 72 * @param string $class class name per cell set by reference 73 * @param null|int $num number of visible comments -- internally used, not by pagelist plugin 74 * @return string 75 */ 76 public function td($id, $col = null, &$class = null, $num = null) 77 { 78 $section = '#discussion__section'; 79 80 if (!isset($num)) { 81 $cfile = metaFN($id, '.comments'); 82 $comments = unserialize(io_readFile($cfile, false)); 83 84 $num = $comments['number']; 85 if (!$comments['status'] || ($comments['status'] == 2 && !$num)) { 86 return ''; 87 } 88 } 89 90 if ($num == 0) { 91 $comment = '0 ' . $this->getLang('nocomments'); 92 } elseif ($num == 1) { 93 $comment = '1 ' . $this->getLang('comment'); 94 } else { 95 $comment = $num . ' ' . $this->getLang('comments'); 96 } 97 98 return '<a href="' . wl($id) . $section . '" class="wikilink1" title="' . $id . $section . '">' 99 . $comment 100 . '</a>'; 101 } 102 103 /** 104 * Returns an array of pages with discussion sections, sorted by recent comments 105 * Note: also used for content by Feed Plugin 106 * 107 * @param string $ns 108 * @param null|int $num 109 * @param string|bool $skipEmpty 110 * @return array 111 */ 112 public function getThreads($ns, $num = null, $skipEmpty = false) 113 { 114 global $conf; 115 116 $dir = $conf['datadir'] . utf8_encodeFN(($ns ? '/' . str_replace(':', '/', $ns) : '')); 117 118 // returns the list of pages in the given namespace and it's subspaces 119 $items = []; 120 search($items, $dir, 'search_allpages', []); 121 122 // add pages with comments to result 123 $result = []; 124 foreach ($items as $item) { 125 $id = ($ns ? $ns . ':' : '') . $item['id']; 126 127 // some checks 128 $perm = auth_quickaclcheck($id); 129 if ($perm < AUTH_READ) continue; // skip if no permission 130 $file = metaFN($id, '.comments'); 131 if (!@file_exists($file)) continue; // skip if no comments file 132 $data = unserialize(io_readFile($file, false)); 133 $status = $data['status']; 134 $number = $data['number']; 135 136 if (!$status || ($status == 2 && !$number)) continue; // skip if comments are off or closed without comments 137 if ($skipEmpty == 'y' && $number == 0) continue; // skip if discussion is empty and flag is set 138 139 $date = filemtime($file); 140 $meta = p_get_metadata($id); 141 $result[$date . '_' . $id] = [ 142 'id' => $id, 143 'file' => $file, 144 'title' => $meta['title'], 145 'date' => $date, 146 'user' => $meta['creator'], 147 'desc' => $meta['description']['abstract'], 148 'num' => $number, 149 'comments' => $this->td($id, null, $class, $number), 150 'status' => $status, 151 'perm' => $perm, 152 'exists' => true, 153 'anchor' => 'discussion__section', 154 ]; 155 } 156 157 // finally sort by time of last comment 158 krsort($result); 159 160 if (is_numeric($num)) { 161 $result = array_slice($result, 0, $num); 162 } 163 164 return $result; 165 } 166 167 /** 168 * Returns an array of recently added comments to a given page or namespace 169 * Note: also used for content by Feed Plugin 170 * 171 * @param string $ns 172 * @param int|null $num 173 * @return array 174 */ 175 public function getComments($ns, $num = null) 176 { 177 global $conf; 178 179 $first = $_REQUEST['first']; 180 if (!is_numeric($first)) { 181 $first = 0; 182 } 183 184 if ((!$num) || (!is_numeric($num))) { 185 $num = $conf['recent']; 186 } 187 188 $result = []; 189 $count = 0; 190 191 if (!@file_exists($conf['metadir'] . '/_comments.changes')) { 192 return $result; 193 } 194 195 // read all recent changes. (kept short) 196 $lines = file($conf['metadir'] . '/_comments.changes'); 197 198 $seen = []; //caches seen pages in order to skip them 199 // handle lines 200 $line_num = count($lines); 201 for ($i = ($line_num - 1); $i >= 0; $i--) { 202 $rec = $this->handleRecentComment($lines[$i], $ns, $seen); 203 if ($rec !== false) { 204 if (--$first >= 0) continue; // skip first entries 205 206 $result[$rec['date']] = $rec; 207 $count++; 208 // break when we have enough entries 209 if ($count >= $num) break; 210 } 211 } 212 213 // finally sort by time of last comment 214 krsort($result); 215 216 return $result; 217 } 218 219 /* ---------- Changelog function adapted for the Discussion Plugin ---------- */ 220 221 /** 222 * Internal function used by $this->getComments() 223 * 224 * don't call directly 225 * 226 * @param string $line 227 * @param string $ns 228 * @param array $seen 229 * @return array|false 230 * @see getRecentComments() 231 * @author Andreas Gohr <andi@splitbrain.org> 232 * @author Ben Coburn <btcoburn@silicodon.net> 233 * @author Esther Brunner <wikidesign@gmail.com> 234 * 235 */ 236 protected function handleRecentComment($line, $ns, &$seen) 237 { 238 if (empty($line)) return false; //skip empty lines 239 240 // split the line into parts 241 $recent = parseChangelogLine($line); 242 if ($recent === false) return false; 243 244 $cid = $recent['extra']; 245 $fullcid = $recent['id'] . '#' . $recent['extra']; 246 247 // skip seen ones 248 if (isset($seen[$fullcid])) return false; 249 250 // skip 'show comment' log entries 251 if ($recent['type'] === 'sc') return false; 252 253 // remember in seen to skip additional sights 254 $seen[$fullcid] = 1; 255 256 // check if it's a hidden page or comment 257 if (isHiddenPage($recent['id'])) return false; 258 if ($recent['type'] === 'hc') return false; 259 260 // filter namespace or id 261 if ($ns && strpos($recent['id'] . ':', $ns . ':') !== 0) return false; 262 263 // check ACL 264 $recent['perm'] = auth_quickaclcheck($recent['id']); 265 if ($recent['perm'] < AUTH_READ) return false; 266 267 // check existance 268 $recent['file'] = wikiFN($recent['id']); 269 $recent['exists'] = @file_exists($recent['file']); 270 if (!$recent['exists']) return false; 271 if ($recent['type'] === 'dc') return false; 272 273 // get discussion meta file name 274 $data = unserialize(io_readFile(metaFN($recent['id'], '.comments'), false)); 275 276 // check if discussion is turned off 277 if ($data['status'] === 0) return false; 278 279 $parent_id = $cid; 280 // Check for the comment and all parents if they exist and are visible. 281 do { 282 $tcid = $parent_id; 283 284 // check if the comment still exists 285 if (!isset($data['comments'][$tcid])) return false; 286 // check if the comment is visible 287 if ($data['comments'][$tcid]['show'] != 1) return false; 288 289 $parent_id = $data['comments'][$tcid]['parent']; 290 } while ($parent_id && $parent_id != $tcid); 291 292 // okay, then add some additional info 293 if (is_array($data['comments'][$cid]['user'])) { 294 $recent['name'] = $data['comments'][$cid]['user']['name']; 295 } else { 296 $recent['name'] = $data['comments'][$cid]['name']; 297 } 298 $recent['desc'] = strip_tags($data['comments'][$cid]['xhtml']); 299 $recent['anchor'] = 'comment_' . $cid; 300 301 return $recent; 302 } 303 304 /** 305 * @return bool 306 */ 307 global $USERINFO; 308 public function isDiscussionModerator() 309 { 310 $groups = trim($this->getConf('moderatorgroups')); 311 312 if (auth_ismanager()) { 313 return true; 314 } 315 // Check if user is member of the moderator groups 316 if (!empty($groups) && auth_isMember($groups, $_SERVER['REMOTE_USER'], (array)$USERINFO['grps'])) { 317 return true; 318 } 319 320 return false; 321 } 322} 323