1<?php
2
3/**
4 * ToDo Action Plugin: Inserts button for ToDo plugin into toolbar
5 *
6 * Original Example: http://www.dokuwiki.org/devel:action_plugins
7 * @author     Babbage <babbage@digitalbrink.com>
8 * @date 20130405 Leo Eibler <dokuwiki@sprossenwanne.at> \n
9 *                replace old sack() method with new jQuery method and use post instead of get \n
10 * @date 20130408 Leo Eibler <dokuwiki@sprossenwanne.at> \n
11 *                remove getInfo() call because it's done by plugin.info.txt (since dokuwiki 2009-12-25 Lemming)
12 */
13
14if(!defined('DOKU_INC')) die();
15/**
16 * Class action_plugin_todo registers actions
17 */
18class action_plugin_todo extends DokuWiki_Action_Plugin {
19
20    /**
21     * Register the eventhandlers
22     */
23    public function register(Doku_Event_Handler $controller) {
24        $controller->register_hook('TOOLBAR_DEFINE', 'AFTER', $this, 'insert_button', array());
25        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, '_ajax_call', array());
26    }
27
28    /**
29     * Inserts the toolbar button
30     */
31    public function insert_button(&$event, $param) {
32        $event->data[] = array(
33            'type' => 'format',
34            'title' => $this->getLang('qb_todobutton'),
35            'icon' => '../../plugins/todo/todo.png',
36// key 't' is already used for going to top of page, bug #76
37//	    'key' => 't',
38            'open' => '<todo>',
39            'close' => '</todo>',
40            'block' => false,
41        );
42    }
43
44    /**
45     * Handles ajax requests for to do plugin
46     *
47     * @brief This method is called by ajax if the user clicks on the to-do checkbox or the to-do text.
48     * It sets the to-do state to completed or reset it to open.
49     *
50     * POST Parameters:
51     *   index    int the position of the occurrence of the input element (starting with 0 for first element/to-do)
52     *   checked    int should the to-do set to completed (1) or to open (0)
53     *   path    string id/path/name of the page
54     *
55     * @date 20140317 Leo Eibler <dokuwiki@sprossenwanne.at> \n
56     *                use todo content as change description \n
57     * @date 20131008 Gerrit Uitslag <klapinklapin@gmail.com> \n
58     *                move ajax.php to action.php, added lock and conflict checks and improved saving
59     * @date 20130405 Leo Eibler <dokuwiki@sprossenwanne.at> \n
60     *                replace old sack() method with new jQuery method and use post instead of get \n
61     * @date 20130407 Leo Eibler <dokuwiki@sprossenwanne.at> \n
62     *                add user assignment for todos \n
63     * @date 20130408 Christian Marg <marg@rz.tu-clausthal.de> \n
64     *                change only the clicked to-do item instead of all items with the same text \n
65     *                origVal is not used anymore, we use the index (occurrence) of input element \n
66     * @date 20130408 Leo Eibler <dokuwiki@sprossenwanne.at> \n
67     *                migrate changes made by Christian Marg to current version of plugin \n
68     *
69     *
70     * @param Doku_Event $event
71     * @param mixed $param not defined
72     */
73    public function _ajax_call(&$event, $param) {
74        global $ID, $conf, $lang;
75
76        if($event->data !== 'plugin_todo') {
77            return;
78        }
79        //no other ajax call handlers needed
80        $event->stopPropagation();
81        $event->preventDefault();
82
83        #Variables
84        // by einhirn <marg@rz.tu-clausthal.de> determine checkbox index by using class 'todocheckbox'
85        if(isset($_REQUEST['mode'], $_REQUEST['pageid'])) {
86            $mode = $_REQUEST['mode'];
87            // path = page ID
88            $ID = cleanID(urldecode($_REQUEST['pageid']));
89        } else {
90            return;
91        }
92
93        if($mode == 'checkbox') {
94            if(isset($_REQUEST['index'], $_REQUEST['checked'], $_REQUEST['pageid'])) {
95                // index = position of occurrence of <input> element (starting with 0 for first element)
96                $index = (int) $_REQUEST['index'];
97                // checked = flag if input is checked means to do is complete (1) or not (0)
98                $checked = (boolean) urldecode($_REQUEST['checked']);
99            } else {
100                return;
101            }
102        }
103
104        $date = 0;
105        if(isset($_REQUEST['date'])) $date = (int) $_REQUEST['date'];
106
107        $INFO = pageinfo();
108
109        #Determine Permissions
110        if(auth_quickaclcheck($ID) < AUTH_EDIT) {
111            echo "You do not have permission to edit this file.\nAccess was denied.";
112            return;
113        }
114        // Check, if page is locked
115        if(checklock($ID)) {
116            $locktime = filemtime(wikiLockFN($ID));
117            $expire = dformat($locktime + $conf['locktime']);
118            $min = round(($conf['locktime'] - (time() - $locktime)) / 60);
119
120            $msg = $this->getLang('lockedpage').'
121'.$lang['lockedby'] . ': ' . editorinfo($INFO['locked']) . '
122' . $lang['lockexpire'] . ': ' . $expire . ' (' . $min . ' min)';
123            $this->printJson(array('message' => $msg));
124            return;
125        }
126
127        //conflict check
128        if($date != 0 && $INFO['meta']['date']['modified'] > $date) {
129            $this->printJson(array('message' => $this->getLang('refreshpage')));
130            return;
131        }
132
133        #Retrieve Page Contents
134        $wikitext = rawWiki($ID);
135
136        switch($mode) {
137            case 'checkbox':
138                #Determine position of tag
139                if($index >= 0) {
140                    $index++;
141                    // index is only set on the current page with the todos
142                    // the occurances are counted, untill the index-th input is reached which is updated
143                    $todoTagStartPos = $this->_strnpos($wikitext, '<todo', $index);
144                    $todoTagEndPos = strpos($wikitext, '>', $todoTagStartPos) + 1;
145
146                            if($todoTagStartPos!==false && $todoTagEndPos > $todoTagStartPos) {
147                                // @date 20140714 le add todo text to minorchange
148                                $todoTextEndPos = strpos( $wikitext, '</todo', $todoTagEndPos );
149                                $todoText = substr( $wikitext, $todoTagEndPos, $todoTextEndPos-$todoTagEndPos );
150                                // update text
151                                $oldTag = substr($wikitext, $todoTagStartPos, ($todoTagEndPos - $todoTagStartPos));
152                                $newTag = $this->_buildTodoTag($oldTag, $checked);
153                                $wikitext = substr_replace($wikitext, $newTag, $todoTagStartPos, ($todoTagEndPos - $todoTagStartPos));
154
155                                // save Update (Minor)
156                                lock($ID);
157                                // @date 20140714 le add todo text to minorchange, use different message for checked or unchecked
158                                saveWikiText($ID, $wikitext, $this->getLang($checked?'checkboxchange_on':'checkboxchange_off').': '.$todoText, $minoredit = true);
159                                unlock($ID);
160
161                        $return = array(
162                            'date' => @filemtime(wikiFN($ID)),
163                            'succeed' => true
164                        );
165                        $this->printJson($return);
166
167                    }
168                }
169                break;
170            case 'uncheckall':
171                $newWikitext = preg_replace('/(<todo.*?)(\s+#[^>\s]*)(.*?>|\s.*?<\/todo>)/', '$1$3', $wikitext);
172
173                lock($ID);
174                saveWikiText($ID, $newWikitext, 'Unchecked all ToDos', $minoredit = true);
175                unlock($ID);
176
177                $return = array(
178                    'date' => @filemtime(wikiFN($ID)),
179                    'succeed' => true
180                );
181                $this->printJson($return);
182                break;
183        }
184    }
185
186    /**
187     * Encode and print an arbitrary variable into JSON format
188     *
189     * @param mixed $return
190     */
191    private function printJson($return) {
192        $json = new JSON();
193        echo $json->encode($return);
194    }
195
196    /**
197     * @brief gets current to-do tag and returns a new one depending on checked
198     * @param $todoTag    string current to-do tag e.g. <todo @user>
199     * @param $checked    int check flag (todo completed=1, todo uncompleted=0)
200     * @return string new to-do completed or uncompleted tag e.g. <todo @user #>
201     */
202    private function _buildTodoTag($todoTag, $checked) {
203        $user = '';
204        if($checked == 1) {
205            if(!empty($_SERVER['REMOTE_USER'])) { $user = $_SERVER['REMOTE_USER']; }
206            $newTag = preg_replace('/>/', ' #'.$user.':'.date('Y-m-d').'>', $todoTag);
207        } else {
208            $newTag = preg_replace('/[\s]*[#].*>/', '>', $todoTag);
209        }
210        return $newTag;
211    }
212
213    /**
214     * Find position of $occurance-th $needle in haystack
215     */
216    private function _strnpos($haystack, $needle, $occurance, $pos = 0) {
217        for($i = 1; $i <= $occurance; $i++) {
218            $pos = strpos($haystack, $needle, $pos);
219
220            if ($pos===false) {return false; }
221
222            $pos++;
223        }
224        return $pos - 1;
225    }
226}
227