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