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 number of comment per page 173 * @return array 174 */ 175 public function getComments($ns, $num = null) 176 { 177 global $conf, $INPUT; 178 179 $first = $INPUT->int('first'); 180 181 if (!$num || !is_numeric($num)) { 182 $num = $conf['recent']; 183 } 184 185 $result = []; 186 $count = 0; 187 188 if (!@file_exists($conf['metadir'] . '/_comments.changes')) { 189 return $result; 190 } 191 192 // read all recent changes. (kept short) 193 $lines = file($conf['metadir'] . '/_comments.changes'); 194 195 $seen = []; //caches seen pages in order to skip them 196 // handle lines 197 $line_num = count($lines); 198 for ($i = ($line_num - 1); $i >= 0; $i--) { 199 $rec = $this->handleRecentComment($lines[$i], $ns, $seen); 200 if ($rec !== false) { 201 if (--$first >= 0) continue; // skip first entries 202 203 $result[$rec['date']] = $rec; 204 $count++; 205 // break when we have enough entries 206 if ($count >= $num) break; 207 } 208 } 209 210 // finally sort by time of last comment 211 krsort($result); 212 213 return $result; 214 } 215 216 /* ---------- Changelog function adapted for the Discussion Plugin ---------- */ 217 218 /** 219 * Internal function used by $this->getComments() 220 * 221 * don't call directly 222 * 223 * @param string $line comment changelog line 224 * @param string $ns namespace (or id) to filter 225 * @param array $seen array to cache seen pages 226 * @return array|false with 227 * 'type' => string, 228 * 'extra' => string comment id, 229 * 'id' => string page id, 230 * 'perm' => int ACL permission 231 * 'file' => string file path of wiki page 232 * 'exists' => bool wiki page exists 233 * 'name' => string name of user 234 * 'desc' => string text of comment 235 * 'anchor' => string 236 * 237 * @see getRecentComments() 238 * @author Andreas Gohr <andi@splitbrain.org> 239 * @author Ben Coburn <btcoburn@silicodon.net> 240 * @author Esther Brunner <wikidesign@gmail.com> 241 * 242 */ 243 protected function handleRecentComment($line, $ns, &$seen) 244 { 245 if (empty($line)) return false; //skip empty lines 246 247 // split the line into parts 248 $recent = parseChangelogLine($line); 249 if ($recent === false) return false; 250 251 $cid = $recent['extra']; 252 $fullcid = $recent['id'] . '#' . $recent['extra']; 253 254 // skip seen ones 255 if (isset($seen[$fullcid])) return false; 256 257 // skip 'show comment' log entries 258 if ($recent['type'] === 'sc') return false; 259 260 // remember in seen to skip additional sights 261 $seen[$fullcid] = 1; 262 263 // check if it's a hidden page or comment 264 if (isHiddenPage($recent['id'])) return false; 265 if ($recent['type'] === 'hc') return false; 266 267 // filter namespace or id 268 if ($ns && strpos($recent['id'] . ':', $ns . ':') !== 0) return false; 269 270 // check ACL 271 $recent['perm'] = auth_quickaclcheck($recent['id']); 272 if ($recent['perm'] < AUTH_READ) return false; 273 274 // check existance 275 $recent['file'] = wikiFN($recent['id']); 276 $recent['exists'] = @file_exists($recent['file']); 277 if (!$recent['exists']) return false; 278 if ($recent['type'] === 'dc') return false; 279 280 // get discussion meta file name 281 $data = unserialize(io_readFile(metaFN($recent['id'], '.comments'), false)); 282 283 // check if discussion is turned off 284 if ($data['status'] === 0) return false; 285 286 $parent_id = $cid; 287 // Check for the comment and all parents if they exist and are visible. 288 do { 289 $tcid = $parent_id; 290 291 // check if the comment still exists 292 if (!isset($data['comments'][$tcid])) return false; 293 // check if the comment is visible 294 if ($data['comments'][$tcid]['show'] != 1) return false; 295 296 $parent_id = $data['comments'][$tcid]['parent']; 297 } while ($parent_id && $parent_id != $tcid); 298 299 // okay, then add some additional info 300 if (is_array($data['comments'][$cid]['user'])) { 301 $recent['name'] = $data['comments'][$cid]['user']['name']; 302 } else { 303 $recent['name'] = $data['comments'][$cid]['name']; 304 } 305 $recent['desc'] = strip_tags($data['comments'][$cid]['xhtml']); 306 $recent['anchor'] = 'comment_' . $cid; 307 308 return $recent; 309 } 310 311 /** 312 * Check if current user is member of the moderator groups 313 * 314 * @return bool is moderator? 315 */ 316 public function isDiscussionModerator() 317 { 318 global $USERINFO, $INPUT; 319 $groups = trim($this->getConf('moderatorgroups')); 320 321 if (auth_ismanager()) { 322 return true; 323 } 324 // Check if user is member of the moderator groups 325 if (!empty($groups) && auth_isMember($groups, $INPUT->server->str('REMOTE_USER'), (array)$USERINFO['grps'])) { 326 return true; 327 } 328 329 return false; 330 } 331} 332