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