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