1<?php
2/**
3 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
4 * @author     Danny Lin <danny.0838@gmail.com>
5 */
6
7// must be run within Dokuwiki
8if(!defined('DOKU_INC')) die();
9if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
10require_once (DOKU_PLUGIN . 'action.php');
11require_once(DOKU_INC.'inc/fulltext.php');
12
13class action_plugin_editx extends DokuWiki_Action_Plugin {
14
15    /**
16     * register the eventhandlers
17     */
18    function register(Doku_Event_Handler $contr) {
19        $contr->register_hook('TPL_ACT_RENDER', 'AFTER', $this, '_append_to_edit', array());
20        $contr->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, '_handle_act', array());
21        $contr->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, '_handle_tpl_act', array());
22    }
23
24    /**
25     * main hooks
26     */
27    function _append_to_edit(&$event, $param) {
28        global $ID;
29        if ($event->data != 'edit') return;
30        if (!$this->_auth_check_all($ID)) return;
31        $link = sprintf('<a href="%s" class="action editx" rel="nofollow">%s</a>', wl($ID,'do=editx'), $this->getLang('pagemanagement'));
32        $intro = $this->locale_xhtml('intro');
33        $intro = str_replace( '@LINK@', $link, $intro );
34        print $intro;
35    }
36
37    function _handle_act(&$event, $param) {
38        if($event->data != 'editx') return;
39        $event->preventDefault();
40    }
41
42    function _handle_tpl_act(&$event, $param) {
43        if($event->data != 'editx') return;
44        $event->preventDefault();
45
46        switch ($_REQUEST['work']) {
47            case 'rename':
48                $opts['oldpage'] = cleanID($_REQUEST['oldpage']);
49                $opts['newpage'] = cleanID($_REQUEST['newpage']);
50                $opts['summary'] = $_REQUEST['summary'];
51                $opts['rp_nr'] = $_REQUEST['rp_nr'];
52                $opts['confirm'] = $_REQUEST['rp_confirm'];
53                $this->_rename_page($opts);
54                break;
55            case 'delete':
56                $opts['oldpage'] = cleanID($_REQUEST['oldpage']);
57                $opts['summary'] = $_REQUEST['summary'];
58                $opts['purge'] = $_REQUEST['dp_purge'];
59                $opts['confirm'] = $_REQUEST['dp_confirm'];
60                $this->_delete_page($opts);
61                break;
62            default:
63                $this->_print_form();
64                break;
65        }
66    }
67
68    /**
69     * helper functions
70     */
71    function _auth_check_list($list) {
72        global $conf;
73        global $USERINFO;
74
75        if(!$conf['useacl']) return true; //no ACL - then no checks
76
77        $allowed = explode(',',$list);
78        $allowed = array_map('trim', $allowed);
79        $allowed = array_unique($allowed);
80        $allowed = array_filter($allowed);
81
82        if(!count($allowed)) return true; //no restrictions
83
84        $user   = $_SERVER['REMOTE_USER'];
85        $groups = (array) $USERINFO['grps'];
86
87        if(in_array($user,$allowed)) return true; //user explicitly mentioned
88
89        //check group memberships
90        foreach($groups as $group){
91            if(in_array('@'.$group,$allowed)) return true;
92        }
93
94        //still here? no access!
95        return false;
96    }
97
98    function _auth_check_all($id) {
99        return $this->_auth_can_rename($id) ||
100            $this->_auth_can_delete($id);
101    }
102
103    function _auth_can_rename($id) {
104        static $cache = null;
105        if (!$cache[$id]) {
106            $cache[$id] = auth_quickaclcheck($id)>=AUTH_EDIT &&
107                $this->_auth_check_list($this->getConf('user_rename'));
108        }
109        return $cache[$id];
110    }
111
112    function _auth_can_rename_nr($id) {
113        static $cache = null;
114        if (!$cache[$id]) {
115            $cache[$id] = auth_quickaclcheck($id)>=AUTH_DELETE &&
116                $this->_auth_check_list($this->getConf('user_rename_nr'));
117        }
118        return $cache[$id];
119    }
120
121    function _auth_can_delete($id) {
122        static $cache = null;
123        if (!$cache[$id]) {
124            $cache[$id] = auth_quickaclcheck($id)>=AUTH_DELETE &&
125                $this->_auth_check_list($this->getConf('user_delete'));
126        }
127        return $cache[$id];
128    }
129
130    function _locate_filepairs(&$opts, $dir, $regex ){
131        global $conf;
132        $oldpath = $conf[$dir].'/'.str_replace(':','/',$opts['oldns']);
133        $newpath = $conf[$dir].'/'.str_replace(':','/',$opts['newns']);
134        if (!$opts['oldfiles']) $opts['oldfiles'] = array();
135        if (!$opts['newfiles']) $opts['newfiles'] = array();
136        $dh = @opendir($oldpath);
137        if($dh) {
138            while(($file = readdir($dh)) !== false){
139                if ($file{0}=='.') continue;
140                $oldfile = $oldpath.$file;
141                if (is_file($oldfile) && preg_match($regex,$file)){
142                    $opts['oldfiles'][] = $oldfile;
143                    if ($opts['move']) {
144                        $newfilebase = str_replace($opts['oldname'], $opts['newname'], $file);
145                        $newfile = $newpath.$newfilebase;
146                        if (@file_exists($newfile)) {
147                            $this->errors[] = sprintf( $this->getLang('rp_msg_file_conflict'), '<a href="'. wl($opts['newpage']) . '">'.$opts['newpage'].'</a>', $newfilebase );
148                            return false;
149                        }
150                        $opts['newfiles'][] = $newfile;
151                    }
152                }
153            }
154            closedir($dh);
155            return true;
156        }
157        return false;
158    }
159
160    function _apply_moves(&$opts) {
161        foreach ($opts['oldfiles'] as $i => $oldfile) {
162            $newfile = $opts['newfiles'][$i];
163            $newdir = dirname($newfile);
164            if (!io_mkdir_p($newdir)) continue;
165            io_rename($oldfile, $newfile);
166        }
167    }
168
169    function _apply_deletes(&$opts) {
170        foreach ($opts['oldfiles'] as $oldfile) {
171            unlink($oldfile);
172        }
173    }
174
175    function _FN($id) {
176        $id = str_replace(':','/',$id);
177        $id = utf8_encodeFN($id);
178        return $id;
179    }
180
181    function _custom_delete_page($id, $summary) {
182        global $ID, $INFO, $conf;
183        // mark as nonexist to prevent indexerWebBug
184        if ($id==$ID) $INFO['exists'] = 0;
185        // delete page, meta and attic
186        $file = wikiFN($id);
187        $old = @filemtime($file); // from page
188        if (file_exists($file)) unlink($file);
189        $opts['oldname'] = $this->_FN(noNS($id));
190        $opts['oldns'] = $this->_FN(getNS($id));
191        if ($opts['oldns']) $opts['oldns'] .= '/';
192        $this->_locate_filepairs( $opts, 'metadir', '/^'.$opts['oldname'].'\.(?!mlist)\w*?$/' );
193        $this->_locate_filepairs( $opts, 'olddir', '/^'.$opts['oldname'].'\.\d{10}\.txt(\.gz|\.bz2)?$/' );
194        $this->_apply_deletes($opts);
195        io_sweepNS($id, 'datadir');
196        io_sweepNS($id, 'metadir');
197        io_sweepNS($id, 'olddir');
198        // send notify mails
199        notify($id,'admin',$old,$summary);
200        notify($id,'subscribers',$old,$summary);
201        // update the purgefile (timestamp of the last time anything within the wiki was changed)
202        io_saveFile($conf['cachedir'].'/purgefile',time());
203        // if useheading is enabled, purge the cache of all linking pages
204        if(useHeading('content')){
205            $pages = ft_backlinks($id);
206            foreach ($pages as $page) {
207                $cache = new cache_renderer($page, wikiFN($page), 'xhtml');
208                $cache->removeCache();
209            }
210        }
211    }
212
213    /**
214     * main functions
215     */
216    function _rename_page(&$opts) {
217        // check confirmation
218        if (!$opts['confirm']) {
219            $this->errors[] = $this->getLang('rp_msg_unconfirmed');
220        }
221        // check old page
222        if (!$opts['oldpage']) {
223            $this->errors[] = $this->getLang('rp_msg_old_empty');
224        } else if (!page_exists($opts['oldpage'])) {
225            $this->errors[] = sprintf( $this->getLang('rp_msg_old_noexist'), $opts['oldpage'] );
226        } else if (!$this->_auth_can_rename($opts['oldpage'])) {
227            $this->errors[] = sprintf( $this->getLang('rp_msg_auth'), $opts['oldpage'] );
228        } else if (checklock($opts['oldpage'])) {
229            $this->errors[] = sprintf( $this->getLang('rp_msg_locked'), $opts['oldpage'] );
230        }
231        // check noredirect
232        if ($opts['rp_nr'] && !$this->_auth_can_rename_nr($opts['oldpage']))
233            $this->errors[] = $this->getLang('rp_msg_auth_nr');
234        // check new page
235        if (!$opts['newpage']) {
236            $this->errors[] = $this->getLang('rp_msg_new_empty');
237        } else if (page_exists($opts['newpage'])) {
238            $this->errors[] = sprintf( $this->getLang('rp_msg_new_exist'), '<a href="'. wl($opts['newpage']) . '">'.$opts['newpage'].'</a>' );
239        } else if (!$this->_auth_can_rename($opts['newpage'])) {
240            $this->errors[] = sprintf( $this->getLang('rp_msg_auth'), $opts['newpage'] );
241        } else if (checklock($opts['newpage'])) {
242            $this->errors[] = sprintf( $this->getLang('rp_msg_locked'), $opts['newpage'] );
243        }
244        // try to locate moves
245        if (!$this->errors) {
246            $opts['move'] = true;
247            $opts['oldname'] = $this->_FN(noNS($opts['oldpage']));
248            $opts['newname'] = $this->_FN(noNS($opts['newpage']));
249            $opts['oldns'] = $this->_FN(getNS($opts['oldpage']));
250            $opts['newns'] = $this->_FN(getNS($opts['newpage']));
251            if ($opts['oldns']) $opts['oldns'] .= '/';
252            if ($opts['newns']) $opts['newns'] .= '/';
253            $this->_locate_filepairs( $opts, 'metadir', '/^'.$opts['oldname'].'\.(?!mlist|meta|indexed)\w*?$/' );
254            $this->_locate_filepairs( $opts, 'olddir', '/^'.$opts['oldname'].'\.\d{10}\.txt(\.gz|\.bz2)?$/' );
255        }
256        // if no error do rename
257        if (!$this->errors) {
258            // move meta and attic
259            $this->_apply_moves($opts);
260            // save to newpage
261            $text = rawWiki($opts['oldpage']);
262            if ($opts['summary'])
263                $summary = sprintf( $this->getLang('rp_newsummaryx'), $opts['oldpage'], $opts['newpage'], $opts['summary'] );
264            else
265                $summary = sprintf( $this->getLang('rp_newsummary'), $opts['oldpage'], $opts['newpage'] );
266            saveWikiText($opts['newpage'],$text,$summary);
267            // purge or recreate old page
268            $summary = $opts['summary'] ?
269                sprintf( $this->getLang('rp_oldsummaryx'), $opts['oldpage'], $opts['newpage'], $opts['summary'] ) :
270                sprintf( $this->getLang('rp_oldsummary'), $opts['oldpage'], $opts['newpage'] );
271            if ($opts['rp_nr']) {
272                $this->_custom_delete_page( $opts['oldpage'], $summary );
273                // write change log afterwards, or it would be deleted
274                addLogEntry( null, $opts['oldpage'], DOKU_CHANGE_TYPE_DELETE, $summary ); // also writes to global changes
275                unlink(metaFN($opts['oldpage'],'.changes')); // purge page changes
276            }
277            else {
278                $text = $this->getConf('redirecttext');
279                if (!$text) $text = $this->getLang('redirecttext');
280                $text = str_replace( '@ID@', $opts['newpage'], $text );
281                @unlink(wikiFN($opts['oldpage']));  // remove old page file so no additional history
282                saveWikiText($opts['oldpage'],$text,$summary);
283            }
284        }
285        // show messages
286        if ($this->errors) {
287            foreach ($this->errors as $error) msg( $error, -1 );
288        }
289        else {
290            $msg = sprintf( $this->getLang('rp_msg_success'), $opts['oldpage'], '<a href="'. wl($opts['newpage']) . '">'.$opts['newpage'].'</a>' );
291            msg( $msg, 1 );
292        }
293        // display form and table
294        $data = array( rp_newpage => $opts['newpage'], rp_summary => $opts['summary'], rp_nr => $opts['rp_nr'] );
295        $this->_print_form($data);
296    }
297
298    function _delete_page(&$opts) {
299        // check confirm
300        if (!$opts['confirm']) {
301            $this->errors[] = $this->getLang('dp_msg_unconfirmed');
302        }
303        // check old page
304        if (!$opts['oldpage']) {
305            $this->errors[] = $this->getLang('dp_msg_old_empty');
306        } else if (!$this->_auth_can_delete($opts['oldpage'])) {
307            $this->errors[] = sprintf( $this->getLang('dp_msg_auth'), $opts['oldpage'] );
308        }
309        // if no error do delete
310        if (!$this->errors) {
311            $summary = $opts['summary'] ?
312                sprintf( $this->getLang('dp_oldsummaryx'), $opts['summary'] ) :
313                $this->getLang('dp_oldsummary');
314            $this->_custom_delete_page( $opts['oldpage'], $summary );
315            // write change log afterwards, or it would be deleted
316            addLogEntry( null, $opts['oldpage'], DOKU_CHANGE_TYPE_DELETE, $summary ); // also writes to global changes
317            if ($opts['purge']) unlink(metaFN($opts['oldpage'],'.changes')); // purge page changes
318        }
319        // show messages
320        if ($this->errors) {
321            foreach ($this->errors as $error) msg( $error, -1 );
322        }
323        else {
324            $msg = sprintf( $this->getLang('dp_msg_success'), $opts['oldpage'] );
325            msg( $msg, 1 );
326        }
327        // display form and table
328        $data = array( dp_purge => $opts['purge'], dp_summary => $opts['summary'] );
329        $this->_print_form($data);
330    }
331
332    function _print_form($data=null) {
333        global $ID, $lang;
334        $chk = ' checked="checked"';
335?>
336<h1><?php echo sprintf( $this->getLang('title'), $ID); ?></h1>
337<div id="config__manager">
338<?php
339    if ($this->_auth_can_rename($ID)) {
340?>
341    <form action="<?php echo wl($ID); ?>" method="post">
342    <fieldset>
343    <legend><?php echo $this->getLang('rp_title'); ?></legend>
344        <input type="hidden" name="do" value="editx" />
345        <input type="hidden" name="work" value="rename" />
346        <input type="hidden" name="oldpage" value="<?php echo $ID; ?>" />
347        <table class="inline">
348            <tr>
349                <td class="label"><?php echo $this->getLang('rp_newpage'); ?></td>
350                <td class="value"><input class="edit" type="input" name="newpage" value="<?php echo $data['rp_newpage']; ?>" /></td>
351            </tr>
352            <tr>
353                <td class="label"><?php echo $this->getLang('rp_summary'); ?></td>
354                <td class="value"><input class="edit" type="input" name="summary" value="<?php echo $data['rp_summary']; ?>" /></td>
355            </tr>
356<?php
357        if ($this->_auth_can_rename_nr($ID)) {
358?>
359            <tr>
360                <td class="label"><?php echo $this->getLang('rp_nr'); ?></td>
361                <td class="value"><input type="checkbox" name="rp_nr" value="1"<?php if ($data['rp_nr']) echo $chk; ?> /></td>
362            </tr>
363<?php
364    }
365?>
366            <tr>
367                <td class="label"><?php echo $this->getLang('rp_confirm'); ?></td>
368                <td class="value"><input type="checkbox" name="rp_confirm" value="1" /></td>
369            </tr>
370        </table>
371        <p>
372            <input type="submit" class="button" value="<?php echo $lang['btn_save']; ?>" />
373            <input type="reset" class="button" value="<?php echo $lang['btn_reset']; ?>" />
374        </p>
375    </fieldset>
376    </form>
377<?php
378    }
379    if ($this->_auth_can_delete($ID)) {
380?>
381    <form action="<?php echo wl($ID); ?>" method="post">
382    <fieldset>
383    <legend><?php echo $this->getLang('dp_title'); ?></legend>
384        <input type="hidden" name="do" value="editx" />
385        <input type="hidden" name="work" value="delete" />
386        <input type="hidden" name="oldpage" value="<?php echo $ID; ?>" />
387        <table class="inline">
388            <tr>
389                <td class="label"><?php echo $this->getLang('dp_summary'); ?></td>
390                <td class="value"><input class="edit" type="input" name="summary" value="<?php echo $data['dp_summary']; ?>" /></td>
391            </tr>
392            <tr>
393                <td class="label"><?php echo $this->getLang('dp_purge'); ?></td>
394                <td class="value"><input type="checkbox" name="dp_purge" value="1"<?php if ($data['dp_purge']) echo $chk; ?> /></td>
395            </tr>
396            <tr>
397                <td class="label"><?php echo $this->getLang('dp_confirm'); ?></td>
398                <td class="value"><input type="checkbox" name="dp_confirm" value="1" /></td>
399            </tr>
400        </table>
401        <p>
402            <input type="submit" class="button" value="<?php echo $lang['btn_save']; ?>" />
403            <input type="reset" class="button" value="<?php echo $lang['btn_reset']; ?>" />
404        </p>
405    </fieldset>
406    </form>
407<?php
408    }
409?>
410</div>
411<?php
412    }
413}
414// vim:ts=4:sw=4:et:enc=utf-8:
415