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 $date = filemtime($file); 143 $meta = p_get_metadata($id); 144 $result[$date . '_' . $id] = [ 145 'id' => $id, 146 'file' => $file, 147 'title' => $meta['title'], 148 'date' => $date, 149 'user' => $meta['creator'], 150 'desc' => $meta['description']['abstract'], 151 'num' => $number, 152 'comments' => $this->td($id, null, $class, $number), 153 'status' => $status, 154 'perm' => $perm, 155 'exists' => true, 156 'anchor' => 'discussion__section', 157 ]; 158 } 159 160 // finally sort by time of last comment 161 krsort($result); 162 163 if (is_numeric($num)) { 164 $result = array_slice($result, 0, $num); 165 } 166 167 return $result; 168 } 169 170 /** 171 * Returns an array of recently added comments to a given page or namespace 172 * Note: also used for content by Feed Plugin 173 * 174 * @param string $ns 175 * @param int|null $num number of comment per page 176 * @return array 177 */ 178 public function getComments($ns, $num = null) 179 { 180 global $conf, $INPUT; 181 182 $first = $INPUT->int('first'); 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 comment changelog line 227 * @param string $ns namespace (or id) to filter 228 * @param array $seen array to cache seen pages 229 * @return array|false with 230 * 'type' => string, 231 * 'extra' => string comment id, 232 * 'id' => string page id, 233 * 'perm' => int ACL permission 234 * 'file' => string file path of wiki page 235 * 'exists' => bool wiki page exists 236 * 'name' => string name of user 237 * 'desc' => string text of comment 238 * 'anchor' => string 239 * 240 * @see getRecentComments() 241 * @author Andreas Gohr <andi@splitbrain.org> 242 * @author Ben Coburn <btcoburn@silicodon.net> 243 * @author Esther Brunner <wikidesign@gmail.com> 244 * 245 */ 246 protected function handleRecentComment($line, $ns, &$seen) 247 { 248 if (empty($line)) return false; //skip empty lines 249 250 // split the line into parts 251 $recent = parseChangelogLine($line); 252 if ($recent === false) return false; 253 254 $cid = $recent['extra']; 255 $fullcid = $recent['id'] . '#' . $recent['extra']; 256 257 // skip seen ones 258 if (isset($seen[$fullcid])) return false; 259 260 // skip 'show comment' log entries 261 if ($recent['type'] === 'sc') return false; 262 263 // remember in seen to skip additional sights 264 $seen[$fullcid] = 1; 265 266 // check if it's a hidden page or comment 267 if (isHiddenPage($recent['id'])) return false; 268 if ($recent['type'] === 'hc') return false; 269 270 // filter namespace or id 271 if ($ns && strpos($recent['id'] . ':', $ns . ':') !== 0) return false; 272 273 // check ACL 274 $recent['perm'] = auth_quickaclcheck($recent['id']); 275 if ($recent['perm'] < AUTH_READ) return false; 276 277 // check existance 278 $recent['file'] = wikiFN($recent['id']); 279 $recent['exists'] = @file_exists($recent['file']); 280 if (!$recent['exists']) return false; 281 if ($recent['type'] === 'dc') return false; 282 283 // get discussion meta file name 284 $data = unserialize(io_readFile(metaFN($recent['id'], '.comments'), false)); 285 286 // check if discussion is turned off 287 if ($data['status'] === 0) return false; 288 289 $parent_id = $cid; 290 // Check for the comment and all parents if they exist and are visible. 291 do { 292 $tcid = $parent_id; 293 294 // check if the comment still exists 295 if (!isset($data['comments'][$tcid])) return false; 296 // check if the comment is visible 297 if ($data['comments'][$tcid]['show'] != 1) return false; 298 299 $parent_id = $data['comments'][$tcid]['parent']; 300 } while ($parent_id && $parent_id != $tcid); 301 302 // okay, then add some additional info 303 if (is_array($data['comments'][$cid]['user'])) { 304 $recent['name'] = $data['comments'][$cid]['user']['name']; 305 } else { 306 $recent['name'] = $data['comments'][$cid]['name']; 307 } 308 $recent['desc'] = strip_tags($data['comments'][$cid]['xhtml']); 309 $recent['anchor'] = 'comment_' . $cid; 310 311 return $recent; 312 } 313 314 /** 315 * Check if current user is member of the moderator groups 316 * 317 * @return bool is moderator? 318 */ 319 public function isDiscussionModerator() 320 { 321 global $USERINFO, $INPUT; 322 $groups = trim($this->getConf('moderatorgroups')); 323 324 if (auth_ismanager()) { 325 return true; 326 } 327 // Check if user is member of the moderator groups 328 if (!empty($groups) && auth_isMember($groups, $INPUT->server->str('REMOTE_USER'), (array)$USERINFO['grps'])) { 329 return true; 330 } 331 332 return false; 333 } 334} 335