1<?php
2/**
3 * DokuWiki Plugin groupmgr (Syntax Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Alex Forencich <alex@alexforencich.com>
7 *
8 * Syntax:
9 * ~~GROUPMGR|[groups to manage]|[allowed users and groups]~~
10 *
11 * Examples:
12 *   ~~GROUPMGR|posters|@moderators~~
13 *   Members of group 'posters' can be managed by group 'moderators'
14 *
15 *   ~~GROUPMGR|groupa, groupb|joe, @admin~~
16 *   Members of groups 'groupa' and 'groupb' can be managed by user 'joe'
17 *     members of the 'admin' group
18 *
19 * Note: superuser groups can only be managed by super users,
20 *       forbidden groups can be configured,
21 *       and users cannot remove themselves from the group that lets them access
22 *       the group manager (including admins)
23 *
24 * Note: if require_conf_namespace config option is set, then plugin looks in
25 *       conf_namespace:$ID for configuration.  Plugin will also check config
26 *       namespace if a placeholder tag is used (~~GROUPMGR~~).  This is the
27 *       default configuration for security reasons.
28 *
29 */
30
31// must be run within Dokuwiki
32if (!defined('DOKU_INC')) die();
33
34if (!defined('DOKU_LF')) define('DOKU_LF', "\n");
35if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t");
36if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
37
38require_once DOKU_PLUGIN.'syntax.php';
39
40function remove_item_by_value($val, $arr, $preserve = true) {
41    if (empty($arr) || !is_array($arr)) { return false; }
42    foreach(array_keys($arr,$val) as $key){ unset($arr[$key]); }
43    return ($preserve) ? $arr : array_values($arr);
44}
45
46class syntax_plugin_groupmgr extends DokuWiki_Syntax_Plugin {
47    /**
48     * Plugin information
49     */
50    function getInfo(){
51        return array(
52            'author' => 'Alex Forencich',
53            'email'  => 'alex@alexforencich.com',
54            'date'   => '2010-11-28',
55            'name'   => 'Group Manager Syntax plugin',
56            'desc'   => 'Embeddable group manager',
57            'url'    => 'http://www.alexforencich.com/'
58        );
59    }
60
61    /**
62     * Plugin type
63     */
64    function getType() {
65        return 'substition';
66    }
67
68    /**
69     * PType
70     */
71    function getPType() {
72        return 'normal';
73    }
74
75    /**
76     * Sort order
77     */
78    function getSort() {
79        return 160;
80    }
81
82    /**
83     * Register syntax handler
84     */
85    function connectTo($mode) {
86        $this->Lexer->addSpecialPattern('~~GROUPMGR\|[^~]*?~~',$mode,'plugin_groupmgr');
87        $this->Lexer->addSpecialPattern('~~GROUPMGR~~',$mode,'plugin_groupmgr');
88    }
89
90    /**
91     * Handle match
92     */
93    function handle($match, $state, $pos, &$handler){
94        $data = array(null, $state, $pos);
95
96        if (strlen($match) == 12)
97            return $data;
98
99        // Strip away tag
100        $match = substr($match, 11, -2);
101
102        // split arguments
103        $ar = explode("|", $match);
104
105        $match = array();
106
107        // reorganize into array
108        foreach ($ar as $it) {
109            $ar2 = explode(",", $it);
110            foreach ($ar2 as &$it2)
111                $it2 = trim($it2);
112            $match[] = $ar2;
113        }
114
115        // pass to render method
116        $data[0] = $match;
117
118        return $data;
119    }
120
121    /**
122     * Render it
123     */
124    function render($mode, &$renderer, $data) {
125        global $auth;
126        global $lang;
127        global $INFO;
128        global $conf;
129        global $ID;
130
131        // we are parsing a submitted comment...
132        if (isset($_REQUEST['comment']))
133            return false;
134
135        // disable caching
136        $renderer->info['cache'] = false;
137
138        $this->setupLocale();
139
140        if (!method_exists($auth,"retrieveUsers")) return false;
141
142        if ($mode == 'xhtml') {
143            // need config namespace?
144            if ($this->getConf('require_conf_namespace')) {
145                // set it to null, it will be reloaded anyway
146                $data[0] = null;
147            }
148
149            $conf_namespace = $this->getConf('conf_namespace');
150
151            // empty tag?
152            if (is_null($data[0]) || count($data[0]) == 0) {
153                // load from conf namespace
154                // build page name
155                $conf_page = "";
156                if (substr($ID, 0, strlen($conf_namespace)) != $conf_namespace) {
157                    $conf_page .= $conf_namespace;
158                    if (substr($conf_page, -1) != ':') $conf_page .= ":";
159                }
160                $conf_page .= $ID;
161
162                // get file name
163                $fn = wikiFN($conf_page);
164
165                if (!file_exists($fn))
166                    return false;
167
168                // read file
169                $page = file_get_contents($fn);
170
171                // find config tag
172                $i = preg_match('/~~GROUPMGR\|[^~]*?~~/', $page, &$match);
173
174                if ($i == 0)
175                    return false;
176
177                // parse config
178                $match = substr($match[0], 11, -2);
179
180                $ar = explode("|", $match);
181                $match = array();
182
183                // reorganize into array
184                foreach ($ar as $it) {
185                    $ar2 = explode(",", $it);
186                    foreach ($ar2 as &$it2)
187                        $it2 = trim($it2);
188                    $match[] = $ar2;
189                }
190
191                // pass to render method
192                $data[0] = $match;
193            }
194
195            // don't render if an argument hasn't been specified
196            if (!isset($data[0][0]) || !isset($data[0][1]))
197                return false;
198
199            $grplst = $data[0][0];
200            $authlst = $data[0][1];
201
202            // parse forbidden groups
203            $forbiddengrplst = array();
204            $str = $this->getConf('forbidden_groups');
205            if (isset($str)) {
206                $arr = explode(",", $str);
207                foreach ($arr as $val) {
208                    $val = trim($val);
209                    $forbiddengrplst[] = $val;
210                }
211            }
212
213            // parse admin groups
214            $admingrplst = array();
215            if (isset($conf['superuser'])) {
216                $arr = explode(",", $conf['superuser']);
217                foreach ($arr as $val) {
218                    $val = trim($val);
219                    if ($val[0] == "@") {
220                        $val = substr($val, 1);
221                        $admingrplst[] = $val;
222                    }
223                }
224            }
225
226            // forbid admin groups if user is not a superuser
227            if (!$INFO['isadmin']) {
228                foreach ($admingrplst as $val) {
229                    $forbiddengrplst[] = $val;
230                }
231            }
232
233            // remove forbidden groups from group list
234            foreach ($forbiddengrplst as $val) {
235                $grplst = remove_item_by_value($val, $grplst, false);
236            }
237
238            // build array of user's credentials
239            $check = array($_SERVER['REMOTE_USER']);
240            if (is_array($INFO['userinfo'])) {
241                foreach ($INFO['userinfo']['grps'] as $val) {
242                    $check[] = "@" . $val;
243                }
244            }
245
246            // does user have permission?
247            // Also, save authenticated group for later
248            $authbygrp = "";
249            $ok = 0;
250            foreach ($authlst as $val) {
251                if (in_array($val, $check)) {
252                    $ok = 1;
253                    if ($val[0] == "@") {
254                        $authbygrp = substr($val, 1);
255                    }
256                }
257            }
258
259            // continue if user has explicit permission or is an admin
260            if ($INFO['isadmin'] || $ok) {
261                // authorized
262                $status = 0;
263
264                // nab user info
265                $users = $auth->retrieveUsers(0, 0, array());
266
267                // open form
268                $renderer->doc .= "<form method=\"post\" action=\"" . htmlspecialchars($_SERVER['REQUEST_URI'])
269                    . "\" name=\"groupmgr\" enctype=\"application/x-www-form-urlencoded\">";
270
271                // open table and print header
272                $renderer->doc .= "<table class=\"inline\">\n";
273                $renderer->doc .= "  <tbody>\n";
274                $renderer->doc .= "    <tr>\n";
275                $renderer->doc .= "      <th>" . $lang['user'] . "</th>\n";
276                $renderer->doc .= "      <th>" . $lang['fullname'] . "</th>\n";
277                $renderer->doc .= "      <th>" . $lang['email'] . "</th>\n";
278                // loop through available groups
279                foreach ($grplst as $g) {
280                    $renderer->doc .= "      <th>" . htmlspecialchars($g) . "</th>\n";
281                }
282                $renderer->doc .= "    </tr>\n";
283
284                // loop through users
285                foreach ($users as $name => $u) {
286                    // print user info
287                    $renderer->doc .= "    <tr>\n";
288                    $renderer->doc .= "      <td>" . htmlspecialchars($name);
289                    // need tag so user isn't pulled out of a group if it was added
290                    // between initial page load and update
291                    // use MD5 hash to take care of formatting issues
292                    $hn = md5($name);
293                    $renderer->doc .= "<input type=\"hidden\" name=\"id_" . $hn . "\" value=\"1\" />";
294                    $renderer->doc .= "</td>\n";
295                    $renderer->doc .= "      <td>" . htmlspecialchars($u['name']) . "</td>\n";
296                    $renderer->doc .= "      <td>";
297                    $renderer->emaillink($u['mail']);
298                    $renderer->doc .= "</td>\n";
299                    // loop through groups
300                    foreach ($grplst as $g) {
301                        $renderer->doc .= "      <td>";
302
303                        $chk = "chk_" . $hn . "_" . md5($g);
304
305                        // does this box need to be disabled?
306                        // prevents user from taking himself out of an important group
307                        $disabled = 0;
308                        // if this box applies to a current group membership of the current user, continue check
309                        if (in_array($g, $u['grps']) && $_SERVER['REMOTE_USER'] == $name) {
310                            // if user is an admin and group is an admin group, disable
311                            if ($INFO['isadmin'] && in_array($g, $admingrplst)) {
312                                $disabled = 1;
313                                // if user was authenticated by this group, disable
314                            } else if (strlen($authbygrp) > 0 && $g == $authbygrp) {
315                                $disabled = 1;
316                            }
317                        }
318
319                        // update user group membership
320                        // only update if something changed
321                        // keep track of status
322                        $update = array();
323                        if (!$disabled && $_POST["id_" . $hn]) {
324                            if ($_POST[$chk]) {
325                                if (!in_array($g, $u['grps'])) {
326                                    $u['grps'][] = $g;
327                                    $update['grps'] = $u['grps'];
328                                }
329                            } else {
330                                if (in_array($g, $u['grps'])) {
331                                    $u['grps'] = remove_item_by_value($g, $u['grps'], false);
332                                    $update['grps'] = $u['grps'];
333                                }
334                            }
335                            if (count($update) > 0) {
336                                if ($auth->modifyUser($name, $update)) {
337                                    if ($status == 0) $status = 1;
338                                } else {
339                                    $status = 2;
340                                }
341                            }
342                        }
343
344                        // display check box
345                        $renderer->doc .= "<input type=\"checkbox\" name=\"" . $chk . "\"";
346                        if (in_array($g, $u['grps'])) {
347                            $renderer->doc .= " checked=\"true\"";
348                        }
349                        if ($disabled) {
350                            $renderer->doc .= " disabled=\"true\"";
351                        }
352
353                        $renderer->doc .= " />";
354
355                        $renderer->doc .= "</td>\n";
356                    }
357                    $renderer->doc .= "    </tr>\n";
358                }
359
360                $renderer->doc .= "  </tbody>\n";
361                $renderer->doc .= "</table>\n";
362
363                // update button
364                $renderer->doc .= "<div><input class=\"button\" type=\"submit\" value=\"" . $lang['btn_update'] . "\" /></div>";
365
366                $renderer->doc .= "</form>";
367
368                // display relevant status message
369                if ($status == 1) {
370                    msg($this->lang['updatesuccess'], 1);
371                } else if ($status == 2) {
372                    msg($this->lang['updatefailed'], -1);
373                }
374
375            } else {
376                // not authorized
377                $renderer->doc .= "<p>" . $this->lang['notauthorized'] . "</p>\n";
378            }
379
380            return true;
381        }
382        return false;
383    }
384}
385
386// vim:ts=4:sw=4:et:enc=utf-8:
387