xref: /dokuwiki/lib/plugins/acl/admin.php (revision a989fef837bbeb01153f67fab49f0de2cd9e8b32)
1<?php
2/**
3 * ACL administration functions
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Andreas Gohr <andi@splitbrain.org>
7 * @author     Anika Henke <anika@selfthinker.org> (concepts)
8 * @author     Frank Schubert <frank@schokilade.de> (old version)
9 */
10// must be run within Dokuwiki
11if(!defined('DOKU_INC')) die();
12
13if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
14require_once(DOKU_PLUGIN.'admin.php');
15
16/**
17 * All DokuWiki plugins to extend the admin function
18 * need to inherit from this class
19 */
20class admin_plugin_acl extends DokuWiki_Admin_Plugin {
21    var $acl = null;
22    var $ns  = null;
23    var $who = '';
24    var $usersgroups = array();
25    var $specials = array();
26
27    /**
28     * return some info
29     */
30    function getInfo(){
31        return array(
32            'author' => 'Andreas Gohr',
33            'email'  => 'andi@splitbrain.org',
34            'date'   => '2008-12-16',
35            'name'   => 'ACL',
36            'desc'   => 'Manage Page Access Control Lists',
37            'url'    => 'http://dokuwiki.org/plugin:acl',
38        );
39    }
40
41    /**
42     * return prompt for admin menu
43     */
44    function getMenuText($language) {
45        return $this->getLang('admin_acl');
46    }
47
48    /**
49     * return sort order for position in admin menu
50     */
51    function getMenuSort() {
52        return 1;
53    }
54
55    /**
56     * handle user request
57     *
58     * Initializes internal vars and handles modifications
59     *
60     * @author Andreas Gohr <andi@splitbrain.org>
61     */
62    function handle() {
63        global $AUTH_ACL;
64        global $ID;
65
66        // fresh 1:1 copy without replacements
67        $AUTH_ACL = file(DOKU_CONF.'acl.auth.php');
68
69        // namespace given?
70        if($_REQUEST['ns'] == '*'){
71            $this->ns = '*';
72        }else{
73            $this->ns = cleanID($_REQUEST['ns']);
74        }
75
76        // user or group choosen?
77        $who = trim($_REQUEST['acl_w']);
78        if($_REQUEST['acl_t'] == '__g__' && $who){
79            $this->who = '@'.ltrim($who,'@');
80        }elseif($_REQUEST['acl_t'] == '__u__' && $who){
81            $this->who = ltrim($who,'@');
82        }elseif($_REQUEST['acl_t'] &&
83                $_REQUEST['acl_t'] != '__u__' &&
84                $_REQUEST['acl_t'] != '__g__'){
85            $this->who = $_REQUEST['acl_t'];
86        }elseif($who){
87            $this->who = $who;
88        }
89
90        // handle modifications
91        if(isset($_REQUEST['cmd'])){
92            // scope for modifications
93            if($this->ns){
94                if($this->ns == '*'){
95                    $scope = '*';
96                }else{
97                    $scope = $this->ns.':*';
98                }
99            }else{
100                $scope = $ID;
101            }
102
103            if(isset($_REQUEST['cmd']['save']) && $scope && $this->who && isset($_REQUEST['acl'])){
104                // handle additions or single modifications
105                $this->_acl_del($scope, $this->who);
106                $this->_acl_add($scope, $this->who, (int) $_REQUEST['acl']);
107            }elseif(isset($_REQUEST['cmd']['del']) && $scope && $this->who){
108                // handle single deletions
109                $this->_acl_del($scope, $this->who);
110            }elseif(isset($_REQUEST['cmd']['update'])){
111                // handle update of the whole file
112                foreach((array) $_REQUEST['del'] as $where => $names){
113                    // remove all rules marked for deletion
114                    foreach($names as $who)
115                        unset($_REQUEST['acl'][$where][$who]);
116                }
117                // prepare lines
118                $lines = array();
119                // keep header
120                foreach($AUTH_ACL as $line){
121                    if($line{0} == '#'){
122                        $lines[] = $line;
123                    }else{
124                        break;
125                    }
126                }
127                // re-add all rules
128                foreach((array) $_REQUEST['acl'] as $where => $opt){
129                    foreach($opt as $who => $perm){
130                        $who = auth_nameencode($who,true);
131                        $lines[] = "$where\t$who\t$perm\n";
132                    }
133                }
134                // save it
135                io_saveFile(DOKU_CONF.'acl.auth.php', join('',$lines));
136            }
137
138            // reload ACL config
139            $AUTH_ACL = file(DOKU_CONF.'acl.auth.php');
140        }
141
142        // initialize ACL array
143        $this->_init_acl_config();
144    }
145
146    /**
147     * ACL Output function
148     *
149     * print a table with all significant permissions for the
150     * current id
151     *
152     * @author  Frank Schubert <frank@schokilade.de>
153     * @author  Andreas Gohr <andi@splitbrain.org>
154     */
155    function html() {
156        global $ID;
157
158        echo '<div id="acl_manager">'.NL;
159        echo '<h1>'.$this->getLang('admin_acl').'</h1>'.NL;
160        echo '<div class="level1">'.NL;
161
162        echo '<div id="acl__tree">'.NL;
163        $this->_html_explorer($_REQUEST['ns']);
164        echo '</div>'.NL;
165
166        echo '<div id="acl__detail">'.NL;
167        $this->_html_detail();
168        echo '</div>'.NL;
169        echo '</div>'.NL;
170
171        echo '<div class="clearer"></div>';
172        echo '<h2>'.$this->getLang('current').'</h2>'.NL;
173        echo '<div class="level2">'.NL;
174        $this->_html_table();
175        echo '</div>'.NL;
176
177        echo '<div class="footnotes"><div class="fn">'.NL;
178        echo '<sup><a id="fn__1" class="fn_bot" name="fn__1" href="#fnt__1">1)</a></sup>'.NL;
179        echo $this->getLang('p_include');
180        echo '</div></div>';
181
182        echo '</div>'.NL;
183    }
184
185    /**
186     * returns array with set options for building links
187     *
188     * @author Andreas Gohr <andi@splitbrain.org>
189     */
190    function _get_opts($addopts=null){
191        global $ID;
192        $opts = array(
193                    'do'=>'admin',
194                    'page'=>'acl',
195                );
196        if($this->ns) $opts['ns'] = $this->ns;
197        if($this->who) $opts['acl_w'] = $this->who;
198
199        if(is_null($addopts)) return $opts;
200        return array_merge($opts, $addopts);
201    }
202
203    /**
204     * Display a tree menu to select a page or namespace
205     *
206     * @author Andreas Gohr <andi@splitbrain.org>
207     */
208    function _html_explorer(){
209        require_once(DOKU_INC.'inc/search.php');
210        global $conf;
211        global $ID;
212        global $lang;
213
214        $dir = $conf['datadir'];
215        $ns  = $this->ns;
216        if(empty($ns)){
217            $ns = dirname(str_replace(':','/',$ID));
218            if($ns == '.') $ns ='';
219        }elseif($ns == '*'){
220            $ns ='';
221        }
222        $ns  = utf8_encodeFN(str_replace(':','/',$ns));
223
224
225        $data = array();
226        search($data,$conf['datadir'],'search_index',array('ns' => $ns));
227
228
229        // wrap a list with the root level around the other namespaces
230        $item = array( 'level' => 0, 'id' => '*', 'type' => 'd',
231                   'open' =>'true', 'label' => '['.$lang['mediaroot'].']');
232
233        echo '<ul class="acltree">';
234        echo $this->_html_li_acl($item);
235        echo '<div class="li">';
236        echo $this->_html_list_acl($item);
237        echo '</div>';
238        echo html_buildlist($data,'acl',
239                            array($this,'_html_list_acl'),
240                            array($this,'_html_li_acl'));
241        echo '</li>';
242        echo '</ul>';
243
244    }
245
246    /**
247     * Display the current ACL for selected where/who combination with
248     * selectors and modification form
249     *
250     * @author Andreas Gohr <andi@splitbrain.org>
251     */
252    function _html_detail(){
253        global $conf;
254        global $ID;
255
256        echo '<form action="'.wl().'" method="post" accept-charset="utf-8"><div class="no">'.NL;
257
258        echo '<div id="acl__user">';
259        echo $this->getLang('acl_perms').' ';
260        $inl =  $this->_html_select();
261        echo '<input type="text" name="acl_w" class="edit" value="'.(($inl)?'':hsc(ltrim($this->who,'@'))).'" />'.NL;
262        echo '<input type="submit" value="'.$this->getLang('btn_select').'" class="button" />'.NL;
263        echo '</div>'.NL;
264
265        echo '<div id="acl__info">';
266        $this->_html_info();
267        echo '</div>';
268
269        echo '<input type="hidden" name="ns" value="'.hsc($this->ns).'" />'.NL;
270        echo '<input type="hidden" name="id" value="'.hsc($ID).'" />'.NL;
271        echo '<input type="hidden" name="do" value="admin" />'.NL;
272        echo '<input type="hidden" name="page" value="acl" />'.NL;
273        echo '</div></form>'.NL;
274    }
275
276    /**
277     * Print infos and editor
278     */
279    function _html_info(){
280        global $ID;
281
282        if($this->who){
283            $current = $this->_get_exact_perm();
284
285            // explain current permissions
286            $this->_html_explain($current);
287            // load editor
288            $this->_html_acleditor($current);
289        }else{
290            echo '<p>';
291            if($this->ns){
292                printf($this->getLang('p_choose_ns'),hsc($this->ns));
293            }else{
294                printf($this->getLang('p_choose_id'),hsc($ID));
295            }
296            echo '</p>';
297
298            echo $this->locale_xhtml('help');
299        }
300    }
301
302    /**
303     * Display the ACL editor
304     *
305     * @author Andreas Gohr <andi@splitbrain.org>
306     */
307    function _html_acleditor($current){
308        global $lang;
309
310        echo '<fieldset>';
311        if(is_null($current)){
312            echo '<legend>'.$this->getLang('acl_new').'</legend>';
313        }else{
314            echo '<legend>'.$this->getLang('acl_mod').'</legend>';
315        }
316
317
318        echo $this->_html_checkboxes($current,empty($this->ns),'acl');
319
320        if(is_null($current)){
321            echo '<input type="submit" name="cmd[save]" class="button" value="'.$lang['btn_save'].'" />'.NL;
322        }else{
323            echo '<input type="submit" name="cmd[save]" class="button" value="'.$lang['btn_update'].'" />'.NL;
324            echo '<input type="submit" name="cmd[del]" class="button" value="'.$lang['btn_delete'].'" />'.NL;
325        }
326
327        echo '</fieldset>';
328    }
329
330    /**
331     * Explain the currently set permissions in plain english/$lang
332     *
333     * @author Andreas Gohr <andi@splitbrain.org>
334     */
335    function _html_explain($current){
336        global $ID;
337        global $auth;
338
339        $who = $this->who;
340        $ns  = $this->ns;
341
342        // prepare where to check
343        if($ns){
344            if($ns == '*'){
345                $check='*';
346            }else{
347                $check=$ns.':*';
348            }
349        }else{
350            $check = $ID;
351        }
352
353        // prepare who to check
354        if($who{0} == '@'){
355            $user   = '';
356            $groups = array(ltrim($who,'@'));
357        }else{
358            $user = auth_nameencode($who);
359            $info = $auth->getUserData($user);
360            if($info === false){
361                $groups = array();
362            }else{
363                $groups = $info['grps'];
364            }
365        }
366
367        // check the permissions
368        $perm = auth_aclcheck($check,$user,$groups);
369
370        // build array of named permissions
371        $names = array();
372        if($perm){
373            if($ns){
374                if($perm >= AUTH_DELETE) $names[] = $this->getLang('acl_perm16');
375                if($perm >= AUTH_UPLOAD) $names[] = $this->getLang('acl_perm8');
376                if($perm >= AUTH_CREATE) $names[] = $this->getLang('acl_perm4');
377            }
378            if($perm >= AUTH_EDIT) $names[] = $this->getLang('acl_perm2');
379            if($perm >= AUTH_READ) $names[] = $this->getLang('acl_perm1');
380            $names = array_reverse($names);
381        }else{
382            $names[] = $this->getLang('acl_perm0');
383        }
384
385        // print permission explanation
386        echo '<p>';
387        if($user){
388            if($ns){
389                printf($this->getLang('p_user_ns'),hsc($who),hsc($ns),join(', ',$names));
390            }else{
391                printf($this->getLang('p_user_id'),hsc($who),hsc($ID),join(', ',$names));
392            }
393        }else{
394            if($ns){
395                printf($this->getLang('p_group_ns'),hsc(ltrim($who,'@')),hsc($ns),join(', ',$names));
396            }else{
397                printf($this->getLang('p_group_id'),hsc(ltrim($who,'@')),hsc($ID),join(', ',$names));
398            }
399        }
400        echo '</p>';
401
402        // add note if admin
403        if($perm == AUTH_ADMIN){
404            echo '<p>'.$this->getLang('p_isadmin').'</p>';
405        }elseif(is_null($current)){
406            echo '<p>'.$this->getLang('p_inherited').'</p>';
407        }
408    }
409
410
411    /**
412     * Item formatter for the tree view
413     *
414     * User function for html_buildlist()
415     *
416     * @author Andreas Gohr <andi@splitbrain.org>
417     */
418    function _html_list_acl($item){
419        global $ID;
420        $ret = '';
421        // what to display
422        if($item['label']){
423            $base = $item['label'];
424        }else{
425            $base = ':'.$item['id'];
426            $base = substr($base,strrpos($base,':')+1);
427        }
428
429        // highlight?
430        if( ($item['type']=='d' && $item['id'] == $this->ns) ||
431            ($item['type']!='d' && $item['id'] == $ID)) $cl = ' cur';
432
433        // namespace or page?
434        if($item['type']=='d'){
435            if($item['open']){
436                $img   = DOKU_BASE.'lib/images/minus.gif';
437                $alt   = '&minus;';
438            }else{
439                $img   = DOKU_BASE.'lib/images/plus.gif';
440                $alt   = '+';
441            }
442            $ret .= '<img src="'.$img.'" alt="'.$alt.'" />';
443            $ret .= '<a href="'.wl('',$this->_get_opts(array('ns'=>$item['id']))).'" class="idx_dir'.$cl.'">';
444            $ret .= $base;
445            $ret .= '</a>';
446        }else{
447            $ret .= '<a href="'.wl('',$this->_get_opts(array('id'=>$item['id'],'ns'=>''))).'" class="wikilink1'.$cl.'">';
448            $ret .= noNS($item['id']);
449            $ret .= '</a>';
450        }
451        return $ret;
452    }
453
454
455    function _html_li_acl($item){
456            return '<li class="level'.$item['level'].'">';
457    }
458
459
460    /**
461     * Get current ACL settings as multidim array
462     *
463     * @author Andreas Gohr <andi@splitbrain.org>
464     */
465    function _init_acl_config(){
466        global $AUTH_ACL;
467        global $conf;
468        $acl_config=array();
469        $usersgroups = array();
470
471        // get special users and groups
472        $this->specials[] = '@ALL';
473        $this->specials[] = '@'.$conf['defaultgroup'];
474        if($conf['manager'] != '!!not set!!'){
475            $this->specials = array_merge($this->specials,
476                                          array_map('trim',
477                                                    explode(',',$conf['manager'])));
478        }
479        $this->specials = array_filter($this->specials);
480        $this->specials = array_unique($this->specials);
481        sort($this->specials);
482
483        foreach($AUTH_ACL as $line){
484            $line = trim(preg_replace('/#.*$/','',$line)); //ignore comments
485            if(!$line) continue;
486
487            $acl = preg_split('/\s+/',$line);
488            //0 is pagename, 1 is user, 2 is acl
489
490            $acl[1] = rawurldecode($acl[1]);
491            $acl_config[$acl[0]][$acl[1]] = $acl[2];
492
493            // store non-special users and groups for later selection dialog
494            $ug = $acl[1];
495            if(in_array($ug,$this->specials)) continue;
496            $usersgroups[] = $ug;
497        }
498
499        $usersgroups = array_unique($usersgroups);
500        sort($usersgroups);
501        ksort($acl_config);
502
503        $this->acl = $acl_config;
504        $this->usersgroups = $usersgroups;
505    }
506
507    /**
508     * Display all currently set permissions in a table
509     *
510     * @author Andreas Gohr <andi@splitbrain.org>
511     */
512    function _html_table(){
513        global $lang;
514        global $ID;
515
516        echo '<form action="'.wl().'" method="post" accept-charset="utf-8"><div class="no">'.NL;
517        if($this->ns){
518            echo '<input type="hidden" name="ns" value="'.hsc($this->ns).'" />'.NL;
519        }else{
520            echo '<input type="hidden" name="id" value="'.hsc($ID).'" />'.NL;
521        }
522        echo '<input type="hidden" name="acl_w" value="'.hsc($this->who).'" />'.NL;
523        echo '<input type="hidden" name="do" value="admin" />'.NL;
524        echo '<input type="hidden" name="page" value="acl" />'.NL;
525        echo '<table class="inline">';
526        echo '<tr>';
527        echo '<th>'.$this->getLang('where').'</th>';
528        echo '<th>'.$this->getLang('who').'</th>';
529        echo '<th>'.$this->getLang('perm').'<sup><a id="fnt__1" class="fn_top" name="fnt__1" href="#fn__1">1)</a></sup></th>';
530        echo '<th>'.$lang['btn_delete'].'</th>';
531        echo '</tr>';
532        foreach($this->acl as $where => $set){
533            foreach($set as $who => $perm){
534                echo '<tr>';
535                echo '<td>';
536                if(substr($where,-1) == '*'){
537                    echo '<span class="aclns">'.hsc($where).'</span>';
538                    $ispage = false;
539                }else{
540                    echo '<span class="aclpage">'.hsc($where).'</span>';
541                    $ispage = true;
542                }
543                echo '</td>';
544
545                echo '<td>';
546                if($who{0} == '@'){
547                    echo '<span class="aclgroup">'.hsc($who).'</span>';
548                }else{
549                    echo '<span class="acluser">'.hsc($who).'</span>';
550                }
551                echo '</td>';
552
553                echo '<td>';
554                echo $this->_html_checkboxes($perm,$ispage,'acl['.$where.']['.$who.']');
555                echo '</td>';
556
557                echo '<td align="center">';
558                echo '<input type="checkbox" name="del['.hsc($where).'][]" value="'.hsc($who).'" />';
559                echo '</td>';
560                echo '</tr>';
561            }
562        }
563
564        echo '<tr>';
565        echo '<th align="right" colspan="4">';
566        echo '<input type="submit" value="'.$lang['btn_update'].'" name="cmd[update]" class="button" />';
567        echo '</th>';
568        echo '</tr>';
569        echo '</table>';
570        echo '</div></form>'.NL;
571    }
572
573
574    /**
575     * Returns the permission which were set for exactly the given user/group
576     * and page/namespace. Returns null if no exact match is available
577     *
578     * @author Andreas Gohr <andi@splitbrain.org>
579     */
580    function _get_exact_perm(){
581        global $ID;
582        if($this->ns){
583            if($this->ns == '*'){
584                $check = '*';
585            }else{
586                $check = $this->ns.':*';
587            }
588        }else{
589            $check = $ID;
590        }
591
592        if(isset($this->acl[$check][$this->who])){
593            return $this->acl[$check][$this->who];
594        }else{
595            return null;
596        }
597    }
598
599    /**
600     * adds new acl-entry to conf/acl.auth.php
601     *
602     * @author  Frank Schubert <frank@schokilade.de>
603     */
604    function _acl_add($acl_scope, $acl_user, $acl_level){
605        $acl_config = file_get_contents(DOKU_CONF.'acl.auth.php');
606        $acl_user = auth_nameencode($acl_user,true);
607
608        // max level for pagenames is edit
609        if(strpos($acl_scope,'*') === false) {
610            if($acl_level > AUTH_EDIT) $acl_level = AUTH_EDIT;
611        }
612
613
614        $new_acl = "$acl_scope\t$acl_user\t$acl_level\n";
615
616        $new_config = $acl_config.$new_acl;
617
618        return io_saveFile(DOKU_CONF.'acl.auth.php', $new_config);
619    }
620
621    /**
622     * remove acl-entry from conf/acl.auth.php
623     *
624     * @author  Frank Schubert <frank@schokilade.de>
625     */
626    function _acl_del($acl_scope, $acl_user){
627        $acl_config = file(DOKU_CONF.'acl.auth.php');
628        $acl_user = auth_nameencode($acl_user,true);
629
630        $acl_pattern = '^'.preg_quote($acl_scope,'/').'\s+'.$acl_user.'\s+[0-8].*$';
631
632        // save all non!-matching
633        $new_config = preg_grep("/$acl_pattern/", $acl_config, PREG_GREP_INVERT);
634
635        return io_saveFile(DOKU_CONF.'acl.auth.php', join('',$new_config));
636    }
637
638    /**
639     * print the permission radio boxes
640     *
641     * @author  Frank Schubert <frank@schokilade.de>
642     * @author  Andreas Gohr <andi@splitbrain.org>
643     */
644    function _html_checkboxes($setperm,$ispage,$name){
645        global $lang;
646
647        static $label = 0; //number labels
648        $ret = '';
649
650        if($ispage && $setperm > AUTH_EDIT) $perm = AUTH_EDIT;
651
652        foreach(array(AUTH_NONE,AUTH_READ,AUTH_EDIT,AUTH_CREATE,AUTH_UPLOAD,AUTH_DELETE) as $perm){
653            $label += 1;
654
655            //general checkbox attributes
656            $atts = array( 'type'  => 'radio',
657                           'id'    => 'pbox'.$label,
658                           'name'  => $name,
659                           'value' => $perm );
660            //dynamic attributes
661            if(!is_null($setperm) && $setperm == $perm) $atts['checked']  = 'checked';
662            if($ispage && $perm > AUTH_EDIT){
663                $atts['disabled'] = 'disabled';
664                $class = ' class="disabled"';
665            }else{
666                $class = '';
667            }
668
669            //build code
670            $ret .= '<label for="pbox'.$label.'" title="'.$this->getLang('acl_perm'.$perm).'"'.$class.'>';
671            $ret .= '<input '.html_attbuild($atts).' />&nbsp;';
672            $ret .= $this->getLang('acl_perm'.$perm);
673            $ret .= '</label>'.NL;
674        }
675        return $ret;
676    }
677
678    /**
679     * Print a user/group selector (reusing already used users and groups)
680     *
681     * @author  Andreas Gohr <andi@splitbrain.org>
682     */
683    function _html_select(){
684        global $conf;
685        $inlist = false;
686
687        if($this->who &&
688           !in_array($this->who,$this->usersgroups) &&
689           !in_array($this->who,$this->specials)){
690
691            if($this->who{0} == '@'){
692                $gsel = ' selected="selected"';
693            }else{
694                $usel   = ' selected="selected"';
695            }
696        }else{
697            $usel = '';
698            $gsel = '';
699            $inlist = true;
700        }
701
702
703        echo '<select name="acl_t" class="edit">'.NL;
704        echo '  <option value="__g__" class="aclgroup"'.$gsel.'>'.$this->getLang('acl_group').':</option>'.NL;
705        echo '  <option value="__u__"  class="acluser"'.$usel.'>'.$this->getLang('acl_user').':</option>'.NL;
706        echo '  <optgroup label="&nbsp;">'.NL;
707        foreach($this->specials as $ug){
708            if($ug == $this->who){
709                $sel    = ' selected="selected"';
710                $inlist = true;
711            }else{
712                $sel = '';
713            }
714
715            if($ug{0} == '@'){
716                    echo '  <option value="'.hsc($ug).'" class="aclgroup"'.$sel.'>'.hsc($ug).'</option>'.NL;
717            }else{
718                    echo '  <option value="'.hsc($ug).'" class="acluser"'.$sel.'>'.hsc($ug).'</option>'.NL;
719            }
720        }
721        echo '  </optgroup>'.NL;
722        echo '  <optgroup label="&nbsp;">'.NL;
723        foreach($this->usersgroups as $ug){
724            if($ug == $this->who){
725                $sel    = ' selected="selected"';
726                $inlist = true;
727            }else{
728                $sel = '';
729            }
730
731            if($ug{0} == '@'){
732                    echo '  <option value="'.hsc($ug).'" class="aclgroup"'.$sel.'>'.hsc($ug).'</option>'.NL;
733            }else{
734                    echo '  <option value="'.hsc($ug).'" class="acluser"'.$sel.'>'.hsc($ug).'</option>'.NL;
735            }
736        }
737        echo '  </optgroup>'.NL;
738        echo '</select>'.NL;
739        return $inlist;
740    }
741}
742