1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Esther Brunner <wikidesign@gmail.com> 5 */ 6 7use dokuwiki\Utf8\PhpString; 8 9/** 10 * Class admin_plugin_discussion 11 */ 12class admin_plugin_discussion extends DokuWiki_Admin_Plugin 13{ 14 15 /** 16 * @return int 17 */ 18 public function getMenuSort() 19 { 20 return 200; 21 } 22 23 /** 24 * @return bool 25 */ 26 public function forAdminOnly() 27 { 28 return false; 29 } 30 31 public function handle() 32 { 33 global $lang, $INPUT; 34 35 $cids = $INPUT->post->arr('cid'); 36 if (is_array($cids)) { 37 $cids = array_keys($cids); 38 } 39 /** @var action_plugin_discussion $action */ 40 $action = plugin_load('action', 'discussion'); 41 if (!$action) return; // couldn't load action plugin component 42 43 $act = $INPUT->post->str('comment'); 44 if ($act && !checkSecurityToken()) { 45 return; 46 } 47 switch ($act) { 48 case $lang['btn_delete']: 49 $action->save($cids, ''); 50 break; 51 52 case $this->getLang('btn_show'): 53 $action->save($cids, '', 'show'); 54 break; 55 56 case $this->getLang('btn_hide'): 57 $action->save($cids, '', 'hide'); 58 break; 59 60 case $this->getLang('btn_change'): 61 $this->changeStatus($INPUT->post->str('status')); 62 break; 63 } 64 } 65 66 public function html() 67 { 68 global $conf, $INPUT; 69 70 $first = $INPUT->int('first'); 71 72 $num = $conf['recent'] ?: 20; 73 74 ptln('<h1>' . $this->getLang('menu') . '</h1>'); 75 76 $threads = $this->getThreads(); 77 78 // slice the needed chunk of discussion pages 79 $isMore = count($threads) > ($first + $num); 80 $threads = array_slice($threads, $first, $num); 81 82 foreach ($threads as $thread) { 83 $comments = $this->getComments($thread); 84 $this->threadHead($thread); 85 if ($comments === false) { 86 ptln('</div>', 6); // class="level2" 87 continue; 88 } 89 90 ptln('<form method="post" action="' . wl($thread['id']) . '">', 8); 91 ptln('<div class="no">', 10); 92 ptln('<input type="hidden" name="do" value="admin" />', 10); 93 ptln('<input type="hidden" name="page" value="discussion" />', 10); 94 ptln('<input type="hidden" name="sectok" value="'.getSecurityToken().'" />', 10); 95 echo html_buildlist($comments, 'admin_discussion', [$this, 'commentItem'], [$this, 'liComment']); 96 $this->actionButtons(); 97 ptln('</div>', 10); // class="no" 98 ptln('</form>', 8); 99 ptln('</div>', 6); // class="level2" 100 } 101 $this->browseDiscussionLinks($isMore, $first, $num); 102 103 } 104 105 /** 106 * Returns an array of pages with discussion sections, sorted by recent comments 107 * 108 * @return array 109 */ 110 protected function getThreads() 111 { 112 global $conf; 113 114 // returns the list of pages in the given namespace and it's subspaces 115 $items = []; 116 search($items, $conf['datadir'], 'search_allpages', []); 117 118 // add pages with comments to result 119 $result = []; 120 foreach ($items as $item) { 121 $id = $item['id']; 122 123 // some checks 124 $file = metaFN($id, '.comments'); 125 if (!@file_exists($file)) continue; // skip if no comments file 126 127 $date = filemtime($file); 128 $result[] = [ 129 'id' => $id, 130 'file' => $file, 131 'date' => $date, 132 ]; 133 } 134 135 // finally sort by time of last comment 136 usort($result, ['admin_plugin_discussion', 'threadCmp']); 137 138 return $result; 139 } 140 141 /** 142 * Callback for comparison of thread data. 143 * 144 * Used for sorting threads in descending order by date of last comment. 145 * If this date happens to be equal for the compared threads, page id 146 * is used as second comparison attribute. 147 * 148 * @param array $a 149 * @param array $b 150 * @return int 151 */ 152 protected function threadCmp($a, $b) 153 { 154 if ($a['date'] == $b['date']) { 155 return strcmp($a['id'], $b['id']); 156 } 157 if ($a['date'] < $b['date']) { 158 return 1; 159 } else { 160 return -1; 161 } 162 } 163 164 /** 165 * Outputs header, page ID and status of a discussion thread 166 * 167 * @param array $thread 168 * @return bool 169 */ 170 protected function threadHead($thread) 171 { 172 $id = $thread['id']; 173 174 $labels = [ 175 0 => $this->getLang('off'), 176 1 => $this->getLang('open'), 177 2 => $this->getLang('closed') 178 ]; 179 $title = p_get_metadata($id, 'title'); 180 if (!$title) { 181 $title = $id; 182 } 183 echo '<h2 name="' . $id . '" id="' . $id . '">' . hsc($title) . '</h2>' 184 . '<form method="post" action="' . wl($id) . '">' 185 . '<div class="mediaright">' 186 . '<input type="hidden" name="do" value="admin" />' 187 . '<input type="hidden" name="page" value="discussion" />' 188 . '<input type="hidden" name="sectok" value="' . getSecurityToken() . '" />' 189 . $this->getLang('status') . ': ' 190 . '<select name="status" size="1">'; 191 foreach ($labels as $key => $label) { 192 $selected = ($key == $thread['status'] ? ' selected="selected"' : ''); 193 echo '<option value="' . $key . '"' . $selected . '>' . $label . '</option>'; 194 } 195 echo '</select> ' 196 . '<input type="submit" class="button" name="comment" value="' . $this->getLang('btn_change') . '" ' 197 . 'title="' . $this->getLang('btn_change') . '" />' 198 . '</div>' 199 . '</form>' 200 . '<div class="level2">' 201 . '<a href="' . wl($id) . '" class="wikilink1">' . $id . '</a> '; 202 return true; 203 } 204 205 /** 206 * Returns the full comments data for a given wiki page 207 * 208 * @param array $thread by reference with: 209 * 'id' => string page id, 210 * 'file' => string file location of .comments metadata file 211 * 'status' => int 212 * 'number' => int number of visible comments 213 * 214 * @return array|bool 215 */ 216 protected function getComments(&$thread) 217 { 218 $id = $thread['id']; 219 220 if (!$thread['file']) { 221 $thread['file'] = metaFN($id, '.comments'); 222 } 223 if (!@file_exists($thread['file'])) return false; // no discussion thread at all 224 225 $data = unserialize(io_readFile($thread['file'], false)); 226 227 $thread['status'] = $data['status']; 228 $thread['number'] = $data['number']; 229 if (empty($data['status'])) return false; // comments are turned off 230 if (empty($data['comments'])) return false; // no comments 231 232 $result = []; 233 foreach ($data['comments'] as $cid => $comment) { 234 $this->addComment($cid, $data, $result, $id); 235 } 236 237 if (empty($result)) { 238 return false; 239 } else { 240 return $result; 241 } 242 } 243 244 /** 245 * Recursive function to add the comment hierarchy to the result 246 * 247 * @param string $cid comment id of current comment 248 * @param array $data array with all comments by reference 249 * @param array $result array with all comments by reference enhanced with level 250 * @param string $id page id 251 * @param string $parent comment id of parent or empty 252 * @param int $level level of current comment, higher is deeper 253 */ 254 protected function addComment($cid, &$data, &$result, $id, $parent = '', $level = 1) 255 { 256 if (!isset($data['comments'][$cid]) || !is_array($data['comments'][$cid])) return; // corrupt datatype 257 258 $comment = $data['comments'][$cid]; 259 // handle only replies to given parent comment 260 if ($comment['parent'] != $parent) return; 261 262 // okay, add the comment to the result 263 $comment['id'] = $id; 264 $comment['level'] = $level; 265 $result[] = $comment; 266 267 // check answers to this comment 268 if (count($comment['replies'])) { 269 foreach ($comment['replies'] as $rid) { 270 $this->addComment($rid, $data, $result, $id, $cid, $level + 1); 271 } 272 } 273 } 274 275 /** 276 * Returns html of checkbox and info about a comment item 277 * 278 * @param array $comment array with comment data 279 * @return string html of checkbox and info 280 */ 281 public function commentItem($comment) 282 { 283 global $conf; 284 285 // prepare variables 286 if (is_array($comment['user'])) { // new format 287 $name = $comment['user']['name']; 288 $mail = $comment['user']['mail']; 289 } else { // old format 290 $name = $comment['name']; 291 $mail = $comment['mail']; 292 } 293 if (is_array($comment['date'])) { // new format 294 $created = $comment['date']['created']; 295 } else { // old format 296 $created = $comment['date']; 297 } 298 $abstract = preg_replace('/\s+?/', ' ', strip_tags($comment['xhtml'])); 299 if (PhpString::strlen($abstract) > 160) { 300 $abstract = PhpString::substr($abstract, 0, 160) . '...'; 301 } 302 303 return '<input type="checkbox" name="cid[' . $comment['cid'] . ']" value="1" /> ' 304 . $this->email($mail, $name, 'email') 305 . ', <a href="' . wl($comment['id']) . '#comment_' . $comment['cid'] . '" class="wikilink1">' 306 . strftime($conf['dformat'], $created) . ': ' 307 . '</a>' 308 . '<span class="abstract">' . $abstract . '</span>'; 309 } 310 311 /** 312 * Returns html of list item openings tag 313 * 314 * @param array $comment 315 * @return string 316 */ 317 public function liComment($comment) 318 { 319 $showclass = ($comment['show'] ? '' : ' hidden'); 320 return '<li class="level' . $comment['level'] . $showclass . '">'; 321 } 322 323 /** 324 * Show buttons to bulk remove, hide or show comments 325 */ 326 protected function actionButtons() 327 { 328 global $lang; 329 330 ptln('<div class="comment_buttons">', 12); 331 ptln('<input type="submit" name="comment" value="' . $this->getLang('btn_show') . '" class="button" title="' . $this->getLang('btn_show') . '" />', 14); 332 ptln('<input type="submit" name="comment" value="' . $this->getLang('btn_hide') . '" class="button" title="' . $this->getLang('btn_hide') . '" />', 14); 333 ptln('<input type="submit" name="comment" value="' . $lang['btn_delete'] . '" class="button" title="' . $lang['btn_delete'] . '" />', 14); 334 ptln('</div>', 12); // class="comment_buttons" 335 } 336 337 /** 338 * Displays links to older newer discussions 339 * 340 * @param bool $isMore whether there are more pages needed 341 * @param int $first first entry on this page 342 * @param int $num number of entries per page 343 */ 344 protected function browseDiscussionLinks($isMore, $first, $num) 345 { 346 global $ID; 347 348 if ($first == 0 && !$isMore) return; 349 350 $params = ['do' => 'admin', 'page' => 'discussion']; 351 $last = $first + $num; 352 ptln('<div class="level1">', 8); 353 $return = ''; 354 if ($first > 0) { 355 $first -= $num; 356 if ($first < 0) { 357 $first = 0; 358 } 359 $params['first'] = $first; 360 ptln('<p class="centeralign">', 8); 361 $return = '<a href="' . wl($ID, $params) . '" class="wikilink1"><< ' . $this->getLang('newer') . '</a>'; 362 if ($isMore) { 363 $return .= ' | '; 364 } else { 365 ptln($return, 10); 366 ptln('</p>', 8); 367 } 368 } elseif ($isMore) { 369 ptln('<p class="centeralign">', 8); 370 } 371 if ($isMore) { 372 $params['first'] = $last; 373 $return .= '<a href="' . wl($ID, $params) . '" class="wikilink1">' . $this->getLang('older') . ' >></a>'; 374 ptln($return, 10); 375 ptln('</p>', 8); 376 } 377 ptln('</div>', 6); // class="level1" 378 } 379 380 /** 381 * Changes the status of a comment section 382 * 383 * @param int $new 0=disabled, 1=enabled, 2=closed 384 */ 385 protected function changeStatus($new) 386 { 387 global $ID; 388 389 // get discussion meta file name 390 $file = metaFN($ID, '.comments'); 391 $data = unserialize(io_readFile($file, false)); 392 393 $old = $data['status']; 394 if ($old == $new) { 395 return; 396 } 397 398 // save the comment metadata file 399 $data['status'] = $new; 400 io_saveFile($file, serialize($data)); 401 402 // look for ~~DISCUSSION~~ command in page file and change it accordingly 403 $patterns = ['~~DISCUSSION:off\2~~', '~~DISCUSSION\2~~', '~~DISCUSSION:closed\2~~']; 404 $replace = $patterns[$new]; 405 $wiki = preg_replace('/~~DISCUSSION([\w:]*)(\|?.*?)~~/', $replace, rawWiki($ID)); 406 saveWikiText($ID, $wiki, $this->getLang('statuschanged'), true); 407 } 408} 409