Dotclear

source: inc/core/class.dc.auth.php @ 3730:5c45a5df9a59

Revision 3730:5c45a5df9a59, 19.8 KB checked in by franck <carnet.franck.paul@…>, 8 years ago (diff)

Code formatting (PSR-2)

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

Sites map