xref: /plugin/todo/action.php (revision 172792ecd44ed72cbe3d11a3ab88a216147ea689)
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(&$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',
37            'open' => '<todo>',
38            'close' => '</todo>',
39            'block' => false,
40        );
41    }
42
43    /**
44     * Handles ajax requests for to do plugin
45     *
46     * @brief This method is called by ajax if the user clicks on the to-do checkbox or the to-do text.
47     * It sets the to-do state to completed or reset it to open.
48     *
49     * POST Parameters:
50     *   index    int the position of the occurrence of the input element (starting with 0 for first element/to-do)
51     *   checked    int should the to-do set to completed (1) or to open (0)
52     *   path    string id/path/name of the page
53     *
54     * @date 20131008 Gerrit Uitslag <klapinklapin@gmail.com> \n
55     *                move ajax.php to action.php, added lock and conflict checks and improved saving
56     * @date 20130405 Leo Eibler <dokuwiki@sprossenwanne.at> \n
57     *                replace old sack() method with new jQuery method and use post instead of get \n
58     * @date 20130407 Leo Eibler <dokuwiki@sprossenwanne.at> \n
59     *                add user assignment for todos \n
60     * @date 20130408 Christian Marg <marg@rz.tu-clausthal.de> \n
61     *                change only the clicked to-do item instead of all items with the same text \n
62     *                origVal is not used anymore, we use the index (occurrence) of input element \n
63     * @date 20130408 Leo Eibler <dokuwiki@sprossenwanne.at> \n
64     *                migrate changes made by Christian Marg to current version of plugin \n
65     *
66     *
67     * @param Doku_Event $event
68     * @param mixed $param not defined
69     */
70    public function _ajax_call(&$event, $param) {
71        global $ID, $DATE;
72
73        if($event->data !== 'plugin_todo') {
74            return;
75        }
76        //no other ajax call handlers needed
77        $event->stopPropagation();
78        $event->preventDefault();
79
80        #Variables
81        // by einhirn <marg@rz.tu-clausthal.de> determine checkbox index by using class 'todocheckbox'
82
83        if(isset($_REQUEST['index'], $_REQUEST['checked'], $_REQUEST['path'])) {
84            // index = position of occurrence of <input> element (starting with 0 for first element)
85            $index = (int) $_REQUEST['index'];
86            // checked = flag if input is checked means to do is complete (1) or not (0)
87            $checked = (boolean) urldecode($_REQUEST['checked']);
88            // path = page ID (name)
89            $ID = cleanID(urldecode($_REQUEST['path']));
90        } else {
91            return;
92        }
93        // origVal = urlencoded original value (in the case this is called by dokuwiki searchpattern plugin rendered page)
94        $origVal = '';
95        if(isset($_REQUEST['origVal'])) $origVal = urldecode($_REQUEST['origVal']);
96
97        $date = 0;
98        if(isset($_REQUEST['date'])) $date = (int) $_REQUEST['date'];
99
100        $INFO = pageinfo();
101
102        #Determine Permissions
103        if(auth_quickaclcheck($ID) < AUTH_EDIT) {
104            echo "You do not have permission to edit this file.\nAccess was denied.";
105            return;
106        }
107        // Check, if page is locked
108        if(checklock($ID)) {
109            $this->printJson(array('message' => 'The page is currently locked.'));
110        }
111
112        //conflict check
113        if($date != 0 && $INFO['meta']['date']['modified'] > $date) {
114            $this->printJson(array('message' => 'A newer version of this page is available, refresh your page before trying again.'));
115            return;
116        }
117
118        #Retrieve Page Contents
119        $wikitext = rawWiki($ID);
120
121        #Determine position of tag
122        $contentChanged = false;
123        $todoTagStartPos = $todoTagEndPos = 0;
124        if($index >= 0) {
125            $index++;
126            // index is only set on the current page with the todos
127            // the occurances are counted, untill the index-th input is reached which is updated
128            $todoTagStartPos = strnpos($wikitext, '<todo', $index);
129            $todoTagEndPos = strpos($wikitext, '>', $todoTagStartPos) + 1;
130            if($todoTagEndPos > $todoTagStartPos) {
131                $contentChanged = true;
132            }
133        } else {
134            // this will happen if we are on a dokuwiki searchpattern plugin summary page
135            $checkedpattern = $checked ? '' : '*#[^>]';
136            $pattern = '/(<todo[^#>]' . $checkedpattern . '*>(' . preg_quote($origVal) . '<\/todo[\W]*?>))/';
137
138            $x = preg_match_all($pattern, $wikitext, $spMatches, PREG_OFFSET_CAPTURE);
139            if($x && isset($spMatches[0][0])) {
140                // yes, we found matches and index is in a valid range
141                $todoTagStartPos = $spMatches[1][0][1];
142                $todoTagEndPos = $spMatches[2][0][1];
143
144                $contentChanged = true;
145            }
146        }
147
148        // Modify content
149        if($contentChanged && $todoTagEndPos > $todoTagStartPos) {
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            saveWikiText($ID, $wikitext, 'Checkbox Change', $minoredit = true);
158            unlock($ID);
159
160            $return = array('date' => @filemtime(wikiFN($ID)));
161            $this->printJson($return);
162        }
163
164    }
165
166    /**
167     * Encode and print an arbitrary variable into JSON format
168     *
169     * @param mixed $return
170     */
171    private function printJson($return) {
172        $json = new JSON();
173        echo $json->encode($return);
174    }
175
176    /**
177     * @brief gets current to-do tag and returns a new one depending on checked
178     * @param $todoTag    string current to-do tag e.g. <todo @user>
179     * @param $checked    int check flag (todo completed=1, todo uncompleted=0)
180     * @return string new to-do completed or uncompleted tag e.g. <todo @user #>
181     */
182    private function _buildTodoTag($todoTag, $checked) {
183        $x = preg_match('%<todo([^>]*)>%i', $todoTag, $matches);
184        $newTag = '<todo';
185        if($x) {
186            if(($userPos = strpos($matches[1], '@')) !== false) {
187                $submatch = substr($todoTag, $userPos);
188                $x = preg_match('%@([-.\w]+)%i', $submatch, $matchinguser);
189                if($x) {
190                    $newTag .= ' @' . $matchinguser[1];
191                }
192            }
193        }
194        if($checked == 1) {
195            $newTag .= ' #';
196        }
197        $newTag .= '>';
198        return $newTag;
199    }
200
201    /**
202     * @brief Convert a string to a regex so it can be used in PHP "preg_match" function
203     * from dokuwiki searchpattern plugin
204     */
205    private function _todoStr2regex($str) {
206        $regex = ''; //init
207        for($i = 0; $i < strlen($str); $i++) { //for each char in the string
208            if(!ctype_alnum($str[$i])) { //if char is not alpha-numeric
209                $regex = $regex . '\\'; //escape it with a backslash
210            }
211            $regex = $regex . $str[$i]; //compose regex
212        }
213        return $regex; //return
214    }
215
216}
217
218if(!function_exists('strnpos')) {
219    /**
220     * Find position of $occurance-th $needle in haystack
221     */
222    function strnpos($haystack, $needle, $occurance, $pos = 0) {
223        for($i = 1; $i <= $occurance; $i++) {
224            $pos = strpos($haystack, $needle, $pos) + 1;
225        }
226        return $pos - 1;
227    }
228}