1<?php 2 3use dokuwiki\Parsing\Handler\Nest; 4use dokuwiki\Utf8\PhpString; 5 6/** 7 * DokuWiki Plugin do (Syntax Component) 8 * 9 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 10 * @author Andreas Gohr <gohr@cosmocode.de> 11 * @author Adrian Lang <lang@cosmocode.de> 12 * @author Dominik Eckelmann <eckelmann@cosmocode.de> 13 */ 14 15class syntax_plugin_do_do extends DokuWiki_Syntax_Plugin 16{ 17 /** @var helper_plugin_do */ 18 protected $hlp = null; // helper plugin 19 protected $position = 0; 20 21 private $run = array(); // page run cache 22 private $saved = array(); // save state cache 23 private $ids = array(); 24 25 public function __construct() 26 { 27 $this->hlp = plugin_load('helper', 'do'); 28 } 29 30 /** @inheritDoc */ 31 public function getType() 32 { 33 return 'formatting'; 34 } 35 36 /** @inheritDoc */ 37 public function getPType() 38 { 39 return 'normal'; 40 } 41 42 /** @inheritDoc */ 43 public function getSort() 44 { 45 return 155; 46 } 47 48 /** @inheritDoc */ 49 public function getAllowedTypes() 50 { 51 return array('formatting'); 52 } 53 54 /** @inheritDoc */ 55 public function connectTo($mode) 56 { 57 $this->Lexer->addEntryPattern('<do.*?>(?=.*?</do>)', $mode, 'plugin_do_do'); 58 } 59 60 /** @inheritDoc */ 61 public function postConnect() 62 { 63 $this->Lexer->addExitPattern('</do>', 'plugin_do_do'); 64 } 65 66 /** @inheritDoc */ 67 public function handle($match, $state, $pos, Doku_Handler $handler) 68 { 69 global $auth; 70 71 $data = array( 72 'task' => array(), 73 'state' => $state 74 ); 75 switch ($state) { 76 case DOKU_LEXER_ENTER: 77 $content = trim(substr($match, 3, -1)); 78 79 // get the assignment date 80 if (preg_match('/\b(\d\d\d\d-\d\d-\d\d)\b/', $content, $grep)) { 81 $data['task']['date'] = $grep[1]; 82 $content = trim(str_replace($data['task']['date'], '', $content)); 83 } 84 85 // get the assigned users 86 if ($content !== '') { 87 $data['task']['users'] = explode(',', $content); 88 $data['task']['users'] = array_map('trim', $data['task']['users']); 89 if ($auth) { 90 $data['task']['users'] = array_map(array($auth, 'cleanUser'), $data['task']['users']); 91 } 92 $data['task']['users'] = array_unique($data['task']['users']); 93 $data['task']['users'] = array_filter($data['task']['users']); 94 } 95 96 $ReWriter = new Nest($handler->getCallWriter(), 'plugin_do_do'); 97 $handler->setCallWriter($ReWriter); 98 $handler->addPluginCall('do_do', $data, $state, $pos, $match); 99 break; 100 101 case DOKU_LEXER_UNMATCHED: 102 $handler->addCall('cdata', array($match), $pos); 103 break; 104 105 case DOKU_LEXER_EXIT: 106 global $ID; 107 $data['task']['text'] = $this->_textContent( 108 p_render( 109 'xhtml', 110 array_slice($handler->getCallWriter()->calls, 1), 111 $ignoreme 112 ) 113 ); 114 $data['task']['md5'] = md5( 115 PhpString::strtolower(preg_replace('/\s/', '', $data['task']['text'])) . $ID 116 ); 117 118 // Add missing data from ENTER and EXIT to the other 119 $handler->getCallWriter()->calls[0][1][1]['task'] += $data['task']; 120 $data['task'] += $handler->getCallWriter()->calls[0][1][1]['task']; 121 122 $handler->addPluginCall('do_do', $data, $state, $pos, $match); 123 $handler->getCallWriter()->process(); 124 $ReWriter = $handler->getCallWriter(); 125 $handler->setCallWriter($ReWriter->getCallWriter()); 126 } 127 return false; 128 } 129 130 /** 131 * Return the plain-text content of an html blob, similar to 132 * node.textContent, but trimmed 133 */ 134 protected function _textContent($text) 135 { 136 return trim(html_entity_decode(strip_tags($text), ENT_QUOTES, 'UTF-8')); 137 } 138 139 /** 140 * Return the task as it was before this rendering run 141 * 142 * @param string $page - the current page 143 * @param string $md5 - the task identifier 144 * 145 * @return array - the task data (empty for new tasks) 146 */ 147 protected function _oldTask($page, $md5) 148 { 149 static $oldTasks = null; // old task cache 150 static $curPage = null; // what page are we working on? 151 152 // reinit the cache whenever the page changes 153 if ($curPage != $page) { 154 $curPage = $page; 155 $oldTasks = array(); 156 $statuses = $this->hlp->loadTasks(array('id' => $page)); 157 foreach ($statuses as $state) { 158 $oldTasks[$state['md5']] = $state; 159 } 160 } 161 if (empty($oldTasks[$md5])) { 162 return array(); 163 } 164 return (array)$oldTasks[$md5]; 165 } 166 167 /** 168 * Decide if a task needs to be saved. 169 * 170 * Returns true on the first call, false on subsequent calls 171 * for a given task 172 */ 173 protected function _needsSave($page, $md5) 174 { 175 if (isset($this->saved[$page][$md5])) { 176 return false; 177 } 178 $this->saved[$page][$md5] = 1; 179 return true; 180 } 181 182 /** @inheritDoc */ 183 public function render($mode, Doku_Renderer $R, $data) 184 { 185 global $ID; 186 187 // we don't care for QC FIXME we probably should ignore even more renderers 188 if ($mode == 'qc') { 189 return false; 190 } 191 192 // augment current task with original creator info and old assignees 193 $oldtask = $this->_oldTask($ID, $data['task']['md5']); 194 if ($oldtask) { 195 $data['task']['creator'] = $oldtask['creator']; 196 $data['task']['msg'] = $oldtask['msg']; 197 $data['task']['status'] = $oldtask['status']; 198 } 199 200 // save data to sqlite during meta data run 201 if ($mode === 'metadata') { 202 $this->_save($data); 203 return true; 204 } 205 206 // show simple task with status icon for export renderers 207 if ($mode != 'xhtml') { 208 $R->info['cache'] = false; 209 switch ($data['state']) { 210 case DOKU_LEXER_ENTER: 211 $pre = ($oldtask && $oldtask['status']) ? '' : 'un'; 212 $R->externalmedia(DOKU_URL . "lib/plugins/do/pix/${pre}done.png"); 213 break; 214 215 case DOKU_LEXER_EXIT: 216 if ($data['task']['msg']) { 217 $R->cdata(' (' . $data['task']['msg'] . ')'); 218 } 219 } 220 return true; 221 } 222 223 // handle XHTML output with status management 224 switch ($data['state']) { 225 case DOKU_LEXER_ENTER: 226 $param = array( 227 'do' => 'plugin_do', 228 'do_page' => $ID, 229 'do_md5' => $data['task']['md5'] 230 ); 231 $id = ''; 232 if (!in_array($data['task']['md5'], $this->ids)) { 233 $id = 'id="plgdo__' . $data['task']['md5'] . '" '; 234 $this->ids[] = $data['task']['md5']; 235 } 236 $pre = ($oldtask && $oldtask['status']) ? '' : 'un'; 237 $R->doc .= '<span ' . $id . 'class="plugin_do_item plugin_do_' . $data['task']['md5'] . '">' 238 . ' <a class="plugin_do_status" href="' . wl($ID, $param) . '">' 239 . ' <img src="' . DOKU_BASE . 'lib/plugins/do/pix/' . $pre . 'done.png" />' 240 . ' </a>' 241 . ' <span class="plugin_do_task">'; 242 243 break; 244 245 case DOKU_LEXER_EXIT: 246 $R->doc .= '</span>' 247 . '<span class="plugin_do_commit">' 248 . (empty($data['task']['msg']) ? '' : '(' . $this->getLang('js')['note_done'] . hsc($data['task']['msg']) . ')') 249 . '</span>'; 250 251 if (isset($data['task']['users']) || isset($data['task']['date'])) { 252 $R->doc .= ' <span class="plugin_do_meta">('; 253 if (isset($data['task']['users'])) { 254 $R->doc .= $this->getLang('user'); 255 256 $users = $data['task']['users']; 257 $userCount = count($users); 258 for ($i = 0; $i < $userCount; $i++) { 259 $R->doc .= ' <span class="plugin_do_meta_user">' . $this->hlp->getPrettyUser($users[$i]) . '</span>'; 260 if ($i < $userCount - 1) { 261 $R->doc .= ', '; 262 } 263 } 264 if (isset($data['task']['date'])) { 265 $R->doc .= '. '; 266 } 267 } 268 if (isset($data['task']['date'])) { 269 $R->doc .= $this->getLang('date') . ' <span class="plugin_do_meta_date">' . hsc($data['task']['date']) . '</span>'; 270 } 271 $R->doc .= ')</span>'; 272 } 273 $R->doc .= '</span>'; 274 break; 275 } 276 277 return true; 278 } 279 280 /** 281 * Save data in the metadata renderer 282 * 283 * @param array $data 284 */ 285 protected function _save($data) 286 { 287 global $ID; 288 global $auth; 289 290 // on the first run for this page, clean up 291 if (!isset($this->run[$ID])) { 292 $this->hlp->cleanPageTasks($ID); 293 $this->run[$ID] = true; 294 } 295 296 // we save at the end of our instructions 297 if ($data['state'] !== DOKU_LEXER_EXIT) { 298 return; 299 } 300 301 // did we save already? 302 if (!$this->_needsSave($ID, $data['task']['md5'])) { 303 return; 304 } 305 306 // make sure data is complete 307 if (!isset($data['task']['creator'])) { 308 $data['task']['creator'] = $_SERVER['REMOTE_USER']; 309 } 310 $data['task']['page'] = $ID; 311 $data['task']['pos'] = ++$this->position; 312 313 // save it 314 $this->hlp->saveTask($data['task']); 315 316 // now decide if we should mail anyone 317 if (!$auth) { 318 return; 319 } 320 if (!isset($data['task']['users'])) { 321 return; 322 } 323 if (!$this->getConf('notify_assignee')) { 324 return; 325 } 326 327 // don't mail current or original editor or old assignees 328 $oldtask = $this->_oldTask($ID, $data['task']['md5']); 329 $receivers = array_diff( 330 $data['task']['users'], 331 (array)$oldtask['users'], 332 array($_SERVER['REMOTE_USER'], $data['task']['creator']) 333 ); 334 335 // now mail any new assignees if task is still open 336 if (!$data['task']['status']) { 337 $this->hlp->sendMail($receivers, 'open', $data['task'], $data['task']['creator']); 338 } 339 } 340} 341 342