1<?php
2/**
3 * DokuWiki Plugin authnc (Auth Component)
4 *
5 * The commented functions are kept fore reference or later implementation.
6 *
7 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
8 * @author  Henrik Jürges <ratzeputz@rtzptz.xyz>
9 */
10
11// must be run within Dokuwiki
12if (!defined('DOKU_INC')) {
13    die();
14}
15
16class auth_plugin_authnc extends DokuWiki_Auth_Plugin
17{
18
19    protected $con = NULL;
20
21    protected $curl = NULL;
22
23    /**
24     * Constructor.
25     */
26    public function __construct()
27    {
28        parent::__construct(); // for compatibility
29        global $config_cascade;
30        global $config;
31
32        $this->curl = curl_init();
33        $options = array(
34            CURLOPT_HTTPGET => 1, // default, but make clear
35            CURLOPT_RETURNTRANSFER => TRUE,
36            CURLOPT_HTTPHEADER => array("OCS-APIRequest:true"),
37        );
38        curl_setopt_array($this->curl, $options);
39
40        $this->cando['addUser']     = false; // can Users be created?
41        $this->cando['delUser']     = false; // can Users be deleted?
42        $this->cando['modLogin']    = false; // can login names be changed?
43        $this->cando['modPass']     = false; // can passwords be changed?
44        $this->cando['modName']     = false; // can real names be changed?
45        $this->cando['modMail']     = false; // can emails be changed?
46        $this->cando['modGroups']   = false; // can groups be changed?
47        $this->cando['getUsers']    = true; // can a (filtered) list of users be retrieved?
48        $this->cando['getUserCount']= true; // can the number of users be retrieved?
49        $this->cando['getGroups']   = true; // can a list of available groups be retrieved?
50        $this->cando['external']    = true; // does the module do external auth checking?
51        $this->cando['logout']      = true; // can the user logout again? (eg. not possible with HTTP auth)
52
53        if (!function_exists('curl_init') || ! $this->server_online()) {
54            $this->success = false;
55        }
56        $this->success = true;
57    }
58
59
60    /**
61     * Log off the current user [ OPTIONAL ]
62     */
63    public function logOff()
64    {
65        // return nothing to log out
66        curl_close($this->curl);
67    }
68
69    /**
70     * Do all authentication [ OPTIONAL ]
71     *
72     * @param   string $user   Username
73     * @param   string $pass   Cleartext Password
74     * @param   bool   $sticky Cookie should not expire
75     *
76     * @return  bool             true on successful auth
77     */
78    public function trustExternal($user, $pass, $sticky = false)
79    {
80        global $USERINFO;
81        global $conf;
82        $sticky ? $sticky = true : $sticky = false; //sanity check
83
84        // check only if a user tries to log in, otherwise the function is called with every pageload
85        if (!empty($user)) {
86            // try the login
87            $server = $this->con . 'users/' . $user;
88            $xml = $this->nc_request($server, $user, $pass);
89            $logged_in = false;
90            if ($xml && $xml->meta->status == "ok") {
91                // hurray, we're succeded
92
93                $logged_in = true;
94            } else {
95                $msg = $xml ? " with error " . $xml->meta->message : " connection error";
96                msg("Failed to log in " . $msg);
97            }
98
99            if ($logged_in) {
100                $groups = array();
101                foreach ($xml->data->groups->element as $grp) {
102                    $groups[] = (string)$grp;
103                }
104                // set the globals if authed
105                $USERINFO['name'] = (string)$xml->data->displayname;
106                $USERINFO['mail'] = (string)$xml->data->email;
107                $USERINFO['grps'] = $groups;
108                $_SERVER['REMOTE_USER'] = $user;
109                $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
110                $_SESSION[DOKU_COOKIE]['auth']['pass'] = $pass;
111                $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
112            }
113        }
114
115        // check if already logged in
116        if (!empty($_SESSION[DOKU_COOKIE]['auth']['info'])) {
117            $USERINFO['name'] = $_SESSION[DOKU_COOKIE]['auth']['info']['name'];
118            $USERINFO['mail'] = $_SESSION[DOKU_COOKIE]['auth']['info']['mail'];
119            $USERINFO['grps'] = $_SESSION[DOKU_COOKIE]['auth']['info']['grps'];
120            $_SERVER['REMOTE_USER'] = $_SESSION[DOKU_COOKIE]['auth']['user'];
121            $logged_in = true;
122        }
123        return $logged_in;
124    }
125
126    /**
127     * Check user+password
128     *
129     * May be ommited if trustExternal is used.
130     *
131     * @param   string $user the user name
132     * @param   string $pass the clear text password
133     *
134     * @return  bool
135     */
136    public function checkPass($user, $pass)
137    {
138        return false;
139    }
140
141    /**
142     * Return user info
143     *
144     * Returns info about the given user needs to contain
145     * at least these fields:
146     *
147     * name string  full name of the user
148     * mail string  email addres of the user
149     * grps array   list of groups the user is in
150     *
151     * @param   string $user          the user name
152     * @param   bool   $requireGroups whether or not the returned data must include groups
153     *
154     * @return  array  containing user data or false
155     */
156    public function getUserData($user, $requireGroups=true)
157    {
158        global $USERINFO;
159        $self['user'] = $_SESSION[DOKU_COOKIE]['auth']['user'];
160        $self['name'] = $USERINFO['name'];
161        $self['mail'] = $USERINFO['mail'];
162        if ($requireGroups) {
163            $self['grps'] = $USERINFO['grps'];
164        }
165        return $self;
166    }
167
168    /**
169     * Create a new User [implement only where required/possible]
170     *
171     * Returns false if the user already exists, null when an error
172     * occurred and true if everything went well.
173     *
174     * The new user HAS TO be added to the default group by this
175     * function!
176     *
177     * Set addUser capability when implemented
178     *
179     * @param  string     $user
180     * @param  string     $pass
181     * @param  string     $name
182     * @param  string     $mail
183     * @param  null|array $grps
184     *
185     * @return bool|null
186     */
187    //public function createUser($user, $pass, $name, $mail, $grps = null)
188    //{
189    // FIXME implement
190    //    return null;
191    //}
192
193    /**
194     * Modify user data [implement only where required/possible]
195     *
196     * Set the mod* capabilities according to the implemented features
197     *
198     * @param   string $user    nick of the user to be changed
199     * @param   array  $changes array of field/value pairs to be changed (password will be clear text)
200     *
201     * @return  bool
202     */
203    //public function modifyUser($user, $changes)
204    //{
205    // FIXME implement
206    //    return false;
207    //}
208
209    /**
210     * Delete one or more users [implement only where required/possible]
211     *
212     * Set delUser capability when implemented
213     *
214     * @param   array  $users
215     *
216     * @return  int    number of users deleted
217     */
218    //public function deleteUsers($users)
219    //{
220    // FIXME implement
221    //    return false;
222    //}
223
224    /**
225     * Bulk retrieval of user data [implement only where required/possible]
226     *
227     * Set getUsers capability when implemented
228     *
229     * @param   int   $start  index of first user to be returned
230     * @param   int   $limit  max number of users to be returned, 0 for unlimited
231     * @param   array $filter array of field/pattern pairs, null for no filter
232     *
233     * @return  array list of userinfo (refer getUserData for internal userinfo details)
234     */
235    public function retrieveUsers($start = 0, $limit = 0, $filter = null)
236    {
237        global $USERINFO;
238        $server = $this->con . 'users';
239        $xml = $this->nc_request($server, $_SESSION[DOKU_COOKIE]['auth']['user'], $_SESSION[DOKU_COOKIE]['auth']['pass']);
240        if (! $xml || ! $xml->data->users) {
241            msg("Retrieving user list failed");
242            return array();
243        }
244
245        $users = array();
246        $self['user'] = $_SESSION[DOKU_COOKIE]['auth']['user'];
247        $self['name'] = $USERINFO['name'];
248        $self['mail'] = $USERINFO['mail'];
249        $self['grps'] = $USERINFO['grps'];
250        $users[] = $self;
251        foreach($xml->data->users->element as $user) {
252            // Request the user information for every user, this may take a while
253            if ($user == $_SESSION[DOKU_COOKIE]['auth']['user']) {
254                continue; // Skip the session user
255            }
256
257            $server = $this->con . 'users/' . (string)$user;
258            $xml = $this->nc_request($server, $_SESSION[DOKU_COOKIE]['auth']['user'], $_SESSION[DOKU_COOKIE]['auth']['pass']);
259            if ($xml && $xml->meta->status == "ok" && $xml->data->enabled == '1') {
260                $usr['user'] = (string)$user;
261                $usr['name'] = (string)$xml->data->displayname;
262                $usr['mail'] = (string)$xml->data->email;
263                $groups = array();
264                foreach ($xml->data->groups->element as $grp) {
265                    $groups[] = (string)$grp;
266                }
267                 $usr['grps'] = $groups;
268                 $users[] = $usr;
269            }
270        }
271        return $users;
272    }
273
274    /**
275     * Return a count of the number of user which meet $filter criteria
276     * [should be implemented whenever retrieveUsers is implemented]
277     *
278     * Set getUserCount capability when implemented
279     *
280     * @param  array $filter array of field/pattern pairs, empty array for no filter
281     *
282     * @return int
283     */
284    public function getUserCount($filter = array())
285    {
286        $server = $this->con . 'users';
287        $xml = $this->nc_request($server, $_SESSION[DOKU_COOKIE]['auth']['user'], $_SESSION[DOKU_COOKIE]['auth']['pass']);
288        if (! $xml || ! $xml->data->users) {
289            msg("Retrieving user count failed");
290            return 0;
291        }
292        return count($xml->data->users->element);
293    }
294
295    /**
296     * Define a group [implement only where required/possible]
297     *
298     * Set addGroup capability when implemented
299     *
300     * @param   string $group
301     *
302     * @return  bool
303     */
304    //public function addGroup($group)
305    //{
306        // FIXME implement
307    //    return false;
308    //}
309
310    /**
311     * Retrieve groups [implement only where required/possible]
312     *
313     * Set getGroups capability when implemented
314     *
315     * @param   int $start
316     * @param   int $limit
317     *
318     * @return  array
319     */
320    public function retrieveGroups($start = 0, $limit = 0)
321    {
322        $server = $this->con . 'groups';
323        $xml = $this->nc_request($server, $_SESSION[DOKU_COOKIE]['auth']['user'], $_SESSION[DOKU_COOKIE]['auth']['pass']);
324        if (! $xml || ! $xml->data->groups) {
325            msg("Retrieving groups failed");
326            return array();
327        }
328        $groups = array();
329        foreach ($xml->data->groups->element as $grp) {
330            msg((string) $grp);
331            $groups[(string)$grp] = (string)$grp;
332        }
333        return $groups;
334    }
335
336    /**
337     * Return case sensitivity of the backend
338     *
339     * When your backend is caseinsensitive (eg. you can login with USER and
340     * user) then you need to overwrite this method and return false
341     *
342     * @return bool
343     */
344    public function isCaseSensitive()
345    {
346        return true;
347    }
348
349    /**
350     * Sanitize a given username
351     *
352     * This function is applied to any user name that is given to
353     * the backend and should also be applied to any user name within
354     * the backend before returning it somewhere.
355     *
356     * This should be used to enforce username restrictions.
357     *
358     * @param string $user username
359     * @return string the cleaned username
360     */
361    public function cleanUser($user)
362    {
363        return $user;
364    }
365
366    /**
367     * Sanitize a given groupname
368     *
369     * This function is applied to any groupname that is given to
370     * the backend and should also be applied to any groupname within
371     * the backend before returning it somewhere.
372     *
373     * This should be used to enforce groupname restrictions.
374     *
375     * Groupnames are to be passed without a leading '@' here.
376     *
377     * @param  string $group groupname
378     *
379     * @return string the cleaned groupname
380     */
381    public function cleanGroup($group)
382    {
383        return $group;
384    }
385
386    /**
387     * Check Session Cache validity [implement only where required/possible]
388     *
389     * DokuWiki caches user info in the user's session for the timespan defined
390     * in $conf['auth_security_timeout'].
391     *
392     * This makes sure slow authentication backends do not slow down DokuWiki.
393     * This also means that changes to the user database will not be reflected
394     * on currently logged in users.
395     *
396     * To accommodate for this, the user manager plugin will touch a reference
397     * file whenever a change is submitted. This function compares the filetime
398     * of this reference file with the time stored in the session.
399     *
400     * This reference file mechanism does not reflect changes done directly in
401     * the backend's database through other means than the user manager plugin.
402     *
403     * Fast backends might want to return always false, to force rechecks on
404     * each page load. Others might want to use their own checking here. If
405     * unsure, do not override.
406     *
407     * @param  string $user - The username
408     *
409     * @return bool
410     */
411    //public function useSessionCache($user)
412    //{
413      // FIXME implement
414    //}
415
416    protected function server_online() {
417        if ($this->con) return true; // some link is already set
418        // check if the server is reachable by opening a socket
419        $host = explode(':', $this->getConf('server'));
420        $fp = fSockOpen('ssl:' . $host[1], $this->getConf('port'), $errno, $errstr, 5);
421        if (!$fp) return false; // server is not reachable
422        $this->con = $this->getConf('server') . ':' . $this->getConf('port') . '/' . $this->getConf('ocs-path');
423        return true; // no more error checking, assume reachable
424    }
425
426    /**
427     * Send a request to the nextcloud instance.
428     *
429     * Returns the parsed xml file or NULL if
430     * the request or parsing failed.
431     *
432     * At some point curl generates an invalid syntax 998 error
433     * see https://www.freedesktop.org/wiki/Specifications/open-collaboration-services/
434     * and https://help.nextcloud.com/t/api-error-creating-user-failure-998-invalid-query/56530
435     *
436     * @param string $url  request url, shall return xml
437     * @param string $user the user name
438     * @param string $pass the users password
439     *
440     * @return object the parsed xml or NULL
441     */
442    protected function nc_request($url, $user, $pass) {
443        curl_setopt($this->curl, CURLOPT_USERPWD, $user . ':' . $pass);
444        curl_setopt($this->curl, CURLOPT_URL, $url);
445        if ($result = curl_exec($this->curl)) {
446            return simplexml_load_string($result);
447        } else {
448            msg('Request failed with error: ' . curl_error($ch) . '. Return code: ' . $result);
449            return NULL;
450        }
451    }
452}
453