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