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