Dotclear

source: inc/core/class.dc.auth.php @ 3731:3770620079d4

Revision 3731:3770620079d4, 19.5 KB checked in by franck <carnet.franck.paul@…>, 8 years ago (diff)

Simplify licence block at the beginning of each file

Line 
1<?php
2/**
3 * @brief Authentication and user credentials management
4 *
5 * dcAuth is a class used to handle everything related to user authentication
6 * and credentials. Object is provided by dcCore $auth property.
7 *
8 * @package Dotclear
9 * @subpackage Core
10 *
11 * @copyright Olivier Meunier & Association Dotclear
12 * @copyright GPL-2.0-only
13 */
14
15if (!defined('DC_RC_PATH')) {return;}
16
17class dcAuth
18{
19    /** @var dcCore dcCore instance */
20    protected $core;
21    /** @var connection Database connection object */
22    protected $con;
23
24    /** @var string User table name */
25    protected $user_table;
26    /** @var string Perm table name */
27    protected $perm_table;
28
29    /** @var string Current user ID */
30    protected $user_id;
31    /** @var array Array with user information */
32    protected $user_info = array();
33    /** @var array Array with user options */
34    protected $user_options = array();
35    /** @var boolean User must change his password after login */
36    protected $user_change_pwd;
37    /** @var boolean User is super admin */
38    protected $user_admin;
39    /** @var array Permissions for each blog */
40    protected $permissions = array();
41    /** @var boolean User can change its password */
42    protected $allow_pass_change = true;
43    /** @var array List of blogs on which the user has permissions */
44    protected $blogs = array();
45    /** @var integer Count of user blogs */
46    public $blog_count = null;
47
48    /** @var array Permission types */
49    protected $perm_types;
50
51    /** @var dcPrefs dcPrefs object */
52    public $user_prefs;
53
54    /**
55     * Class constructor. Takes dcCore object as single argument.
56     *
57     * @param dcCore    $core        dcCore object
58     */
59    public function __construct($core)
60    {
61        $this->core       = &$core;
62        $this->con        = &$core->con;
63        $this->blog_table = $core->prefix . 'blog';
64        $this->user_table = $core->prefix . 'user';
65        $this->perm_table = $core->prefix . 'permissions';
66
67        $this->perm_types = array(
68            'admin'        => __('administrator'),
69            'usage'        => __('manage their own entries and comments'),
70            'publish'      => __('publish entries and comments'),
71            'delete'       => __('delete entries and comments'),
72            'contentadmin' => __('manage all entries and comments'),
73            'categories'   => __('manage categories'),
74            'media'        => __('manage their own media items'),
75            'media_admin'  => __('manage all media items')
76        );
77    }
78
79    /// @name Credentials and user permissions
80    //@{
81    /**
82     * Checks if user exists and can log in. <var>$pwd</var> argument is optionnal
83     * while you may need to check user without password. This method will create
84     * credentials and populate all needed object properties.
85     *
86     * @param string    $user_id        User ID
87     * @param string    $pwd            User password
88     * @param string    $user_key        User key check
89     * @param boolean    $check_blog    checks if user is associated to a blog or not.
90     * @return boolean
91     */
92    public function checkUser($user_id, $pwd = null, $user_key = null, $check_blog = true)
93    {
94        # Check user and password
95        $strReq = 'SELECT user_id, user_super, user_pwd, user_change_pwd, ' .
96        'user_name, user_firstname, user_displayname, user_email, ' .
97        'user_url, user_default_blog, user_options, ' .
98        'user_lang, user_tz, user_post_status, user_creadt, user_upddt ' .
99        'FROM ' . $this->con->escapeSystem($this->user_table) . ' ' .
100        "WHERE user_id = '" . $this->con->escape($user_id) . "' ";
101
102        try {
103            $rs = $this->con->select($strReq);
104        } catch (Exception $e) {
105            $err = $e->getMessage();
106            return false;
107        }
108
109        if ($rs->isEmpty()) {
110            sleep(rand(2, 5));
111            return false;
112        }
113
114        $rs->extend('rsExtUser');
115
116        if ($pwd != '') {
117            $rehash = false;
118            if (password_verify($pwd, $rs->user_pwd)) {
119                // User password ok
120                if (password_needs_rehash($rs->user_pwd, PASSWORD_DEFAULT)) {
121                    $rs->user_pwd = $this->crypt($pwd);
122                    $rehash       = true;
123                }
124            } else {
125                // Check if pwd still stored in old fashion way
126                $ret = password_get_info($rs->user_pwd);
127                if (is_array($ret) && isset($ret['algo']) && $ret['algo'] == 0) {
128                    // hash not done with password_hash() function, check by old fashion way
129                    if (crypt::hmac(DC_MASTER_KEY, $pwd, DC_CRYPT_ALGO) == $rs->user_pwd) {
130                        // Password Ok, need to store it in new fashion way
131                        $rs->user_pwd = $this->crypt($pwd);
132                        $rehash       = true;
133                    } else {
134                        // Password KO
135                        sleep(rand(2, 5));
136                        return false;
137                    }
138                } else {
139                    // Password KO
140                    sleep(rand(2, 5));
141                    return false;
142                }
143            }
144            if ($rehash) {
145                // Store new hash in DB
146                $cur           = $this->con->openCursor($this->user_table);
147                $cur->user_pwd = (string) $rs->user_pwd;
148                $cur->update("WHERE user_id = '" . $rs->user_id . "'");
149            }
150        } elseif ($user_key != '') {
151            if (http::browserUID(DC_MASTER_KEY . $rs->user_id . $this->cryptLegacy($rs->user_id)) != $user_key) {
152                return false;
153            }
154        }
155
156        $this->user_id         = $rs->user_id;
157        $this->user_change_pwd = (boolean) $rs->user_change_pwd;
158        $this->user_admin      = (boolean) $rs->user_super;
159
160        $this->user_info['user_pwd']          = $rs->user_pwd;
161        $this->user_info['user_name']         = $rs->user_name;
162        $this->user_info['user_firstname']    = $rs->user_firstname;
163        $this->user_info['user_displayname']  = $rs->user_displayname;
164        $this->user_info['user_email']        = $rs->user_email;
165        $this->user_info['user_url']          = $rs->user_url;
166        $this->user_info['user_default_blog'] = $rs->user_default_blog;
167        $this->user_info['user_lang']         = $rs->user_lang;
168        $this->user_info['user_tz']           = $rs->user_tz;
169        $this->user_info['user_post_status']  = $rs->user_post_status;
170        $this->user_info['user_creadt']       = $rs->user_creadt;
171        $this->user_info['user_upddt']        = $rs->user_upddt;
172
173        $this->user_info['user_cn'] = dcUtils::getUserCN($rs->user_id, $rs->user_name,
174            $rs->user_firstname, $rs->user_displayname);
175
176        $this->user_options = array_merge($this->core->userDefaults(), $rs->options());
177
178        $this->user_prefs = new dcPrefs($this->core, $this->user_id);
179
180        # Get permissions on blogs
181        if ($check_blog && ($this->findUserBlog() === false)) {
182            return false;
183        }
184        return true;
185    }
186
187    /**
188     * This method crypt given string (password, session_id, …).
189     *
190     * @param string $pwd string to be crypted
191     * @return string crypted value
192     */
193    public function crypt($pwd)
194    {
195        return password_hash($pwd, PASSWORD_DEFAULT);
196    }
197
198    /**
199     * This method crypt given string (password, session_id, …).
200     *
201     * @param string $pwd string to be crypted
202     * @return string crypted value
203     */
204    public function cryptLegacy($pwd)
205    {
206        return crypt::hmac(DC_MASTER_KEY, $pwd, DC_CRYPT_ALGO);
207    }
208
209    /**
210     * This method only check current user password.
211     *
212     * @param string    $pwd            User password
213     * @return boolean
214     */
215    public function checkPassword($pwd)
216    {
217        if (!empty($this->user_info['user_pwd'])) {
218            return password_verify($pwd, $this->user_info['user_pwd']);
219        }
220
221        return false;
222    }
223
224    /**
225     * This method checks if user session cookie exists
226     *
227     * @return boolean
228     */
229    public function sessionExists()
230    {
231        return isset($_COOKIE[DC_SESSION_NAME]);
232    }
233
234    /**
235     * This method checks user session validity.
236     *
237     * @return boolean
238     */
239    public function checkSession($uid = null)
240    {
241        $this->core->session->start();
242
243        # If session does not exist, logout.
244        if (!isset($_SESSION['sess_user_id'])) {
245            $this->core->session->destroy();
246            return false;
247        }
248
249        # Check here for user and IP address
250        $this->checkUser($_SESSION['sess_user_id']);
251        $uid = $uid ?: http::browserUID(DC_MASTER_KEY);
252
253        $user_can_log = $this->userID() !== null && $uid == $_SESSION['sess_browser_uid'];
254
255        if (!$user_can_log) {
256            $this->core->session->destroy();
257            return false;
258        }
259
260        return true;
261    }
262
263    /**
264     * Checks if user must change his password in order to login.
265     *
266     * @return boolean
267     */
268    public function mustChangePassword()
269    {
270        return $this->user_change_pwd;
271    }
272
273    /**
274     * Checks if user is super admin
275     *
276     * @return boolean
277     */
278    public function isSuperAdmin()
279    {
280        return $this->user_admin;
281    }
282
283    /**
284     * Checks if user has permissions given in <var>$permissions</var> for blog
285     * <var>$blog_id</var>. <var>$permissions</var> is a coma separated list of
286     * permissions.
287     *
288     * @param string    $permissions    Permissions list
289     * @param string    $blog_id        Blog ID
290     * @return boolean
291     */
292    public function check($permissions, $blog_id)
293    {
294        if ($this->user_admin) {
295            return true;
296        }
297
298        $p = array_map('trim', explode(',', $permissions));
299        $b = $this->getPermissions($blog_id);
300
301        if ($b != false) {
302            if (isset($b['admin'])) {
303                return true;
304            }
305
306            foreach ($p as $v) {
307                if (isset($b[$v])) {
308                    return true;
309                }
310            }
311        }
312
313        return false;
314    }
315
316    /**
317     * Returns true if user is allowed to change its password.
318     *
319     * @return    boolean
320     */
321    public function allowPassChange()
322    {
323        return $this->allow_pass_change;
324    }
325    //@}
326
327    /// @name User code handlers
328    //@{
329    public function getUserCode()
330    {
331        $code =
332        pack('a32', $this->userID()) .
333        pack('H*', $this->crypt($this->getInfo('user_pwd')));
334        return bin2hex($code);
335    }
336
337    public function checkUserCode($code)
338    {
339        $code = @pack('H*', $code);
340
341        $user_id = trim(@pack('a32', substr($code, 0, 32)));
342        $pwd     = @unpack('H*hex', substr($code, 32));
343
344        if ($user_id === false || $pwd === false) {
345            return false;
346        }
347
348        $pwd = $pwd['hex'];
349
350        $strReq = 'SELECT user_id, user_pwd ' .
351        'FROM ' . $this->user_table . ' ' .
352        "WHERE user_id = '" . $this->con->escape($user_id) . "' ";
353
354        $rs = $this->con->select($strReq);
355
356        if ($rs->isEmpty()) {
357            return false;
358        }
359
360        if ($this->crypt($rs->user_pwd) != $pwd) {
361            return false;
362        }
363
364        return $rs->user_id;
365    }
366    //@}
367
368    /// @name Sudo
369    //@{
370    /**
371     * Calls $f function with super admin rights.
372     * Returns the function result.
373     *
374     * @param callback    $f            Callback function
375     * @return mixed
376     */
377    public function sudo($f)
378    {
379        if (!is_callable($f)) {
380            throw new Exception($f . ' function doest not exist');
381        }
382
383        $args = func_get_args();
384        array_shift($args);
385
386        if ($this->user_admin) {
387            $res = call_user_func_array($f, $args);
388        } else {
389            $this->user_admin = true;
390            try {
391                $res              = call_user_func_array($f, $args);
392                $this->user_admin = false;
393            } catch (Exception $e) {
394                $this->user_admin = false;
395                throw $e;
396            }
397        }
398
399        return $res;
400    }
401    //@}
402
403    /// @name User information and options
404    //@{
405    /**
406     * Returns user permissions for a blog as an array which looks like:
407     *
408     *  - [blog_id]
409     *    - [permission] => true
410     *    - ...
411     *
412     * @param string    $blog_id        Blog ID
413     * @return array
414     */
415    public function getPermissions($blog_id)
416    {
417        if (isset($this->blogs[$blog_id])) {
418            return $this->blogs[$blog_id];
419        }
420
421        if ($this->user_admin) {
422            $strReq = 'SELECT blog_id ' .
423            'from ' . $this->blog_table . ' ' .
424            "WHERE blog_id = '" . $this->con->escape($blog_id) . "' ";
425            $rs = $this->con->select($strReq);
426
427            $this->blogs[$blog_id] = $rs->isEmpty() ? false : array('admin' => true);
428
429            return $this->blogs[$blog_id];
430        }
431
432        $strReq = 'SELECT permissions ' .
433        'FROM ' . $this->perm_table . ' ' .
434        "WHERE user_id = '" . $this->con->escape($this->user_id) . "' " .
435        "AND blog_id = '" . $this->con->escape($blog_id) . "' " .
436            "AND (permissions LIKE '%|usage|%' OR permissions LIKE '%|admin|%' OR permissions LIKE '%|contentadmin|%') ";
437        $rs = $this->con->select($strReq);
438
439        $this->blogs[$blog_id] = $rs->isEmpty() ? false : $this->parsePermissions($rs->permissions);
440
441        return $this->blogs[$blog_id];
442    }
443
444    public function getBlogCount()
445    {
446        if ($this->blog_count === null) {
447            $this->blog_count = $this->core->getBlogs(array(), true)->f(0);
448        }
449
450        return $this->blog_count;
451    }
452
453    public function findUserBlog($blog_id = null)
454    {
455        if ($blog_id && $this->getPermissions($blog_id) !== false) {
456            return $blog_id;
457        } else {
458            if ($this->user_admin) {
459                $strReq = 'SELECT blog_id ' .
460                'FROM ' . $this->blog_table . ' ' .
461                'ORDER BY blog_id ASC ' .
462                $this->con->limit(1);
463            } else {
464                $strReq = 'SELECT P.blog_id ' .
465                'FROM ' . $this->perm_table . ' P, ' . $this->blog_table . ' B ' .
466                "WHERE user_id = '" . $this->con->escape($this->user_id) . "' " .
467                "AND P.blog_id = B.blog_id " .
468                "AND (permissions LIKE '%|usage|%' OR permissions LIKE '%|admin|%' OR permissions LIKE '%|contentadmin|%') " .
469                "AND blog_status >= 0 " .
470                'ORDER BY P.blog_id ASC ' .
471                $this->con->limit(1);
472            }
473
474            $rs = $this->con->select($strReq);
475            if (!$rs->isEmpty()) {
476                return $rs->blog_id;
477            }
478        }
479
480        return false;
481    }
482
483    /**
484     * Returns current user ID
485     *
486     * @return string
487     */
488    public function userID()
489    {
490        return $this->user_id;
491    }
492
493    /**
494     * Returns information about a user .
495     *
496     * @param string    $n            Information name
497     * @return string
498     */
499    public function getInfo($n)
500    {
501        if (isset($this->user_info[$n])) {
502            return $this->user_info[$n];
503        }
504
505        return;
506    }
507
508    /**
509     * Returns a specific user option
510     *
511     * @param string    $n            Option name
512     * @return string
513     */
514    public function getOption($n)
515    {
516        if (isset($this->user_options[$n])) {
517            return $this->user_options[$n];
518        }
519        return;
520    }
521
522    /**
523     * Returns all user options in an associative array.
524     *
525     * @return array
526     */
527    public function getOptions()
528    {
529        return $this->user_options;
530    }
531    //@}
532
533    /// @name Permissions
534    //@{
535    /**
536     * Returns an array with permissions parsed from the string <var>$level</var>
537     *
538     * @param string    $level        Permissions string
539     * @return array
540     */
541    public function parsePermissions($level)
542    {
543        $level = preg_replace('/^\|/', '', $level);
544        $level = preg_replace('/\|$/', '', $level);
545
546        $res = array();
547        foreach (explode('|', $level) as $v) {
548            $res[$v] = true;
549        }
550        return $res;
551    }
552
553    /**
554     * Returns <var>perm_types</var> property content.
555     *
556     * @return array
557     */
558    public function getPermissionsTypes()
559    {
560        return $this->perm_types;
561    }
562
563    /**
564     * Adds a new permission type.
565     *
566     * @param string    $name        Permission name
567     * @param string    $title        Permission title
568     */
569    public function setPermissionType($name, $title)
570    {
571        $this->perm_types[$name] = $title;
572    }
573    //@}
574
575    /// @name Password recovery
576    //@{
577    /**
578     * Add a recover key to a specific user identified by its email and
579     * password.
580     *
581     * @param string    $user_id        User ID
582     * @param string    $user_email    User Email
583     * @return string
584     */
585    public function setRecoverKey($user_id, $user_email)
586    {
587        $strReq = 'SELECT user_id ' .
588        'FROM ' . $this->user_table . ' ' .
589        "WHERE user_id = '" . $this->con->escape($user_id) . "' " .
590        "AND user_email = '" . $this->con->escape($user_email) . "' ";
591
592        $rs = $this->con->select($strReq);
593
594        if ($rs->isEmpty()) {
595            throw new Exception(__('That user does not exist in the database.'));
596        }
597
598        $key = md5(uniqid('', true));
599
600        $cur                   = $this->con->openCursor($this->user_table);
601        $cur->user_recover_key = $key;
602
603        $cur->update("WHERE user_id = '" . $this->con->escape($user_id) . "'");
604
605        return $key;
606    }
607
608    /**
609     * Creates a new user password using recovery key. Returns an array:
610     *
611     * - user_email
612     * - user_id
613     * - new_pass
614     *
615     * @param string    $recover_key    Recovery key
616     * @return array
617     */
618    public function recoverUserPassword($recover_key)
619    {
620        $strReq = 'SELECT user_id, user_email ' .
621        'FROM ' . $this->user_table . ' ' .
622        "WHERE user_recover_key = '" . $this->con->escape($recover_key) . "' ";
623
624        $rs = $this->con->select($strReq);
625
626        if ($rs->isEmpty()) {
627            throw new Exception(__('That key does not exist in the database.'));
628        }
629
630        $new_pass = crypt::createPassword();
631
632        $cur                   = $this->con->openCursor($this->user_table);
633        $cur->user_pwd         = $this->crypt($new_pass);
634        $cur->user_recover_key = null;
635        $cur->user_change_pwd  = 1; // User will have to change this temporary password at next login
636
637        $cur->update("WHERE user_recover_key = '" . $this->con->escape($recover_key) . "'");
638
639        return array('user_email' => $rs->user_email, 'user_id' => $rs->user_id, 'new_pass' => $new_pass);
640    }
641    //@}
642
643    /** @name User management callbacks
644    This 3 functions only matter if you extend this class and use
645    DC_AUTH_CLASS constant.
646    These are called after core user management functions.
647    Could be useful if you need to add/update/remove stuff in your
648    LDAP directory    or other third party authentication database.
649     */
650    //@{
651
652    /**
653     * Called after core->addUser
654     * @see dcCore::addUser
655     * @param cursor    $cur            User cursor
656     */
657    public function afterAddUser($cur)
658    {}
659
660    /**
661     * Called after core->updUser
662     * @see dcCore::updUser
663     * @param string    $id            User ID
664     * @param cursor    $cur            User cursor
665     */
666    public function afterUpdUser($id, $cur)
667    {}
668
669    /**
670     * Called after core->delUser
671     * @see dcCore::delUser
672     * @param string    $id            User ID
673     */
674    public function afterDelUser($id)
675    {}
676    //@}
677}
Note: See TracBrowser for help on using the repository browser.

Sites map