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 if ($comments) { 85 $num = $comments['number']; 86 if (!$comments['status'] || ($comments['status'] == 2 && !$num)) { 87 return ''; 88 } 89 } else { 90 $num = 0; 91 } 92 } 93 94 if ($num == 0) { 95 $comment = '0 ' . $this->getLang('nocomments'); 96 } elseif ($num == 1) { 97 $comment = '1 ' . $this->getLang('comment'); 98 } else { 99 $comment = $num . ' ' . $this->getLang('comments'); 100 } 101 102 return '<a href="' . wl($id) . $section . '" class="wikilink1" title="' . $id . $section . '">' 103 . $comment 104 . '</a>'; 105 } 106 107 /** 108 * Returns an array of pages with discussion sections, sorted by recent comments 109 * Note: also used for content by Feed Plugin 110 * 111 * @param string $ns 112 * @param null|int $num 113 * @param string|bool $skipEmpty 114 * @return array 115 */ 116 public function getThreads($ns, $num = null, $skipEmpty = false) 117 { 118 global $conf; 119 120 // returns the list of pages in the given namespace and it's subspaces 121 $dir = utf8_encodeFN(str_replace(':', '/', $ns)); 122 $opts = [ 123 'depth' => 0, // 0=all 124 'skipacl' => true // is checked later 125 ]; 126 $items = []; 127 search($items, $conf['datadir'], 'search_allpages', $opts, $dir); 128 129 // add pages with comments to result 130 $result = []; 131 foreach ($items as $item) { 132 $id = $item['id']; 133 134 // some checks 135 $perm = auth_quickaclcheck($id); 136 if ($perm < AUTH_READ) continue; // skip if no permission 137 $file = metaFN($id, '.comments'); 138 if (!@file_exists($file)) continue; // skip if no comments file 139 $data = unserialize(io_readFile($file, false)); 140 $status = $data['status']; 141 $number = $data['number']; 142 143 if (!$status || ($status == 2 && !$number)) continue; // skip if comments are off or closed without comments 144 if ($skipEmpty && $number == 0) continue; // skip if discussion is empty and flag is set 145 146 //new comments are added to the end of array 147 $date = false; 148 if(isset($data['comments'])) { 149 $latestcomment = end($data['comments']); 150 $date = $latestcomment['date']['created'] ?? false; 151 } 152 //e.g. if no comments 153 if(!$date) { 154 $date = filemtime($file); 155 } 156 157 $meta = p_get_metadata($id); 158 $result[$date . '_' . $id] = [ 159 'id' => $id, 160 'file' => $file, 161 'title' => $meta['title'] ?? '', 162 'date' => $date, 163 'user' => $meta['creator'], 164 'desc' => $meta['description']['abstract'], 165 'num' => $number, 166 'comments' => $this->td($id, null, $class, $number), 167 'status' => $status, 168 'perm' => $perm, 169 'exists' => true, 170 'anchor' => 'discussion__section', 171 ]; 172 } 173 174 // finally sort by time of last comment 175 krsort($result); 176 177 if (is_numeric($num)) { 178 $result = array_slice($result, 0, $num); 179 } 180 181 return $result; 182 } 183 184 /** 185 * Returns an array of recently added comments to a given page or namespace 186 * Note: also used for content by Feed Plugin 187 * 188 * @param string $ns 189 * @param int|null $num number of comment per page 190 * @return array 191 */ 192 public function getComments($ns, $num = null) 193 { 194 global $conf, $INPUT; 195 196 $first = $INPUT->int('first'); 197 198 if (!$num || !is_numeric($num)) { 199 $num = $conf['recent']; 200 } 201 202 $result = []; 203 $count = 0; 204 205 if (!@file_exists($conf['metadir'] . '/_comments.changes')) { 206 return $result; 207 } 208 209 // read all recent changes. (kept short) 210 $lines = file($conf['metadir'] . '/_comments.changes'); 211 212 $seen = []; //caches seen pages in order to skip them 213 // handle lines 214 $line_num = count($lines); 215 for ($i = ($line_num - 1); $i >= 0; $i--) { 216 $rec = $this->handleRecentComment($lines[$i], $ns, $seen); 217 if ($rec !== false) { 218 if (--$first >= 0) continue; // skip first entries 219 220 $result[$rec['date']] = $rec; 221 $count++; 222 // break when we have enough entries 223 if ($count >= $num) break; 224 } 225 } 226 227 // finally sort by time of last comment 228 krsort($result); 229 230 return $result; 231 } 232 233 /* ---------- Changelog function adapted for the Discussion Plugin ---------- */ 234 235 /** 236 * Internal function used by $this->getComments() 237 * 238 * don't call directly 239 * 240 * @param string $line comment changelog line 241 * @param string $ns namespace (or id) to filter 242 * @param array $seen array to cache seen pages 243 * @return array|false with 244 * 'type' => string, 245 * 'extra' => string comment id, 246 * 'id' => string page id, 247 * 'perm' => int ACL permission 248 * 'file' => string file path of wiki page 249 * 'exists' => bool wiki page exists 250 * 'name' => string name of user 251 * 'desc' => string text of comment 252 * 'anchor' => string 253 * 254 * @see getRecentComments() 255 * @author Andreas Gohr <andi@splitbrain.org> 256 * @author Ben Coburn <btcoburn@silicodon.net> 257 * @author Esther Brunner <wikidesign@gmail.com> 258 * 259 */ 260 protected function handleRecentComment($line, $ns, &$seen) 261 { 262 if (empty($line)) return false; //skip empty lines 263 264 // split the line into parts 265 $recent = parseChangelogLine($line); 266 if ($recent === false) return false; 267 268 $cid = $recent['extra']; 269 $fullcid = $recent['id'] . '#' . $recent['extra']; 270 271 // skip seen ones 272 if (isset($seen[$fullcid])) return false; 273 274 // skip 'show comment' log entries 275 if ($recent['type'] === 'sc') return false; 276 277 // remember in seen to skip additional sights 278 $seen[$fullcid] = 1; 279 280 // check if it's a hidden page or comment 281 if (isHiddenPage($recent['id'])) return false; 282 if ($recent['type'] === 'hc') return false; 283 284 // filter namespace or id 285 if ($ns && strpos($recent['id'] . ':', $ns . ':') !== 0) return false; 286 287 // check ACL 288 $recent['perm'] = auth_quickaclcheck($recent['id']); 289 if ($recent['perm'] < AUTH_READ) return false; 290 291 // check existance 292 $recent['file'] = wikiFN($recent['id']); 293 $recent['exists'] = @file_exists($recent['file']); 294 if (!$recent['exists']) return false; 295 if ($recent['type'] === 'dc') return false; 296 297 // get discussion meta file name 298 $data = unserialize(io_readFile(metaFN($recent['id'], '.comments'), false)); 299 300 // check if discussion is turned off 301 if ($data['status'] === 0) return false; 302 303 $parent_id = $cid; 304 // Check for the comment and all parents if they exist and are visible. 305 do { 306 $tcid = $parent_id; 307 308 // check if the comment still exists 309 if (!isset($data['comments'][$tcid])) return false; 310 // check if the comment is visible 311 if ($data['comments'][$tcid]['show'] != 1) return false; 312 313 $parent_id = $data['comments'][$tcid]['parent']; 314 } while ($parent_id && $parent_id != $tcid); 315 316 // okay, then add some additional info 317 if (is_array($data['comments'][$cid]['user'])) { 318 $recent['name'] = $data['comments'][$cid]['user']['name']; 319 } else { 320 $recent['name'] = $data['comments'][$cid]['name']; 321 } 322 $recent['desc'] = strip_tags($data['comments'][$cid]['xhtml']); 323 $recent['anchor'] = 'comment_' . $cid; 324 325 return $recent; 326 } 327 328 /** 329 * Check if current user is member of the moderator groups 330 * 331 * @return bool is moderator? 332 */ 333 public function isDiscussionModerator() 334 { 335 global $USERINFO, $INPUT; 336 $groups = trim($this->getConf('moderatorgroups')); 337 338 if (auth_ismanager()) { 339 return true; 340 } 341 // Check if user is member of the moderator groups 342 if (!empty($groups) && auth_isMember($groups, $INPUT->server->str('REMOTE_USER'), (array)$USERINFO['grps'])) { 343 return true; 344 } 345 346 return false; 347 } 348} 349