Ajde_User::loadByCredentials()   B
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 4
nop 2
dl 0
loc 15
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
abstract class Ajde_User extends Ajde_Model
4
{
5
    protected $_autoloadParents = false;
6
    protected $_displayField = 'fullname';
7
8
    public $usernameField = 'username';
9
    public $passwordField = 'password';
10
11
    const USERGROUP_USERS = 1;
12
    const USERGROUP_ADMINS = 2;
13
    const USERGROUP_CLIENTS = 3;
14
    const USERGROUP_EMPLOYEES = 4;
15
16
    public $defaultUserGroup = self::USERGROUP_USERS;
17
18
    protected $cookieLifetime = 30; // in days
19
20
    private static $_user;
21
22
    /**
23
     * @return UserModel
24
     */
25
    public static function getLoggedIn()
26
    {
27
        if (!isset(self::$_user)) {
28
            $session = new Ajde_Session('user');
29
            if ($session->has('model')) {
30
                $user = $session->getModel('model');
31
                self::$_user = $user;
32
            } else {
33
                self::$_user = false;
34
            }
35
        }
36
37
        return self::$_user;
38
    }
39
40
    public static function isAdmin()
41
    {
42
        return ($user = self::getLoggedIn()) && (string) self::getLoggedIn()->getUsergroup() == self::USERGROUP_ADMINS;
43
    }
44
45
    public static function isDebugger()
46
    {
47
        return ($user = self::getLoggedIn()) && $user->getDebug();
0 ignored issues
show
Documentation Bug introduced by
The method getDebug does not exist on object<UserModel>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
48
    }
49
50
    public static function isTester()
51
    {
52
        return ($user = self::getLoggedIn()) && $user->getTester();
0 ignored issues
show
Documentation Bug introduced by
The method getTester does not exist on object<UserModel>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
53
    }
54
55
    public function hasPassword()
56
    {
57
        return !$this->verifyHash('');
58
    }
59
60
    public function loadByCredentials($username, $password)
61
    {
62
        if (empty($username) || empty($password)) {
63
            return false;
64
        }
65
66
        $sql = 'SELECT * FROM '.$this->_table.' WHERE '.$this->usernameField.' = ? LIMIT 1';
67
        $values = [$username];
68
        $user = $this->_load($sql, $values);
69
        if ($user === false) {
70
            return false;
71
        }
72
73
        return $this->verifyHash($password) ? $user : false;
74
    }
75
76
    /**
77
     * @return UsergroupModel:
0 ignored issues
show
Documentation introduced by
The doc-type UsergroupModel: could not be parsed: Unknown type name "UsergroupModel:" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
78
     */
79
    public function getUsergroup()
80
    {
81
        $this->loadParent('usergroup');
82
83
        return $this->get('usergroup');
84
    }
85
86
    public function createHash($password)
87
    {
88
        // @see http://net.tutsplus.com/tutorials/php/understanding-hash-functions-and-keeping-passwords-safe/
89
        if (CRYPT_BLOWFISH !== 1) {
90
            Ajde_Dump::warn('BLOWFISH algorithm not available for hashing, using MD5 instead');
91
            // Use MD5
92
            $algo = '$1';
93
            $cost = '';
94
            $unique_salt = $this->generateSecret(12);
95
        } else {
96
            // Use BLOWFISH
97
            $algo = '$2a';
98
            $cost = '$10';
99
            $unique_salt = $this->generateSecret(22);
100
        }
101
        $hash = crypt($password, $algo.$cost.'$'.$unique_salt);
102
        if (empty($hash)) {
103
            // TODO:
104
            throw new Ajde_Exception('crypt() algorithm failed');
105
        }
106
107
        return $hash;
108
    }
109
110
    public function verifyHash($password)
111
    {
112
        $hash = $this->get($this->passwordField);
113
        if (empty($hash)) {
114
            return false;
115
        }
116
        if (CRYPT_BLOWFISH !== 1) {
117
            // Use MD5
118
            $full_salt = substr($hash, 0, 15);
119
        } else {
120
            // Use BLOWFISH
121
            $full_salt = substr($hash, 0, 29);
122
        }
123
        $new_hash = crypt($password, $full_salt);
124
125
        return $hash == $new_hash;
126
    }
127
128
    public function login()
129
    {
130
        if (empty($this->_data)) {
131
            // TODO:
132
            throw new Ajde_Exception('Invalid user object');
133
        }
134
        $session = new Ajde_Session('user');
135
        $session->setModel('model', $this);
136
        self::$_user = $this;
137
    }
138
139
    public function logout()
140
    {
141
        // First destroy current session
142
        // TODO: overhead to call session_regenerate_id? is it not required??
143
        //session_regenerate_id();
144
        $session = new Ajde_Session('user');
145
        $session->destroy();
146
        $cookie = new Ajde_Cookie(config('app.id').'_user');
147
        $cookie->destroy();
148
        self::$_user = null;
149
    }
150
151
    public function refresh()
152
    {
153
        $this->loadByPK($this->getPK());
154
    }
155
156
    public function generateSecret($length = 255)
157
    {
158
        return substr(sha1(mt_rand()), 0, $length);
159
    }
160
161
    public function add($username, $password)
162
    {
163
        $hash = $this->createHash($password);
164
        $this->populate([
165
            $this->usernameField => $username,
166
            $this->passwordField => $hash,
167
            'usergroup'          => $this->defaultUserGroup,
168
            'secret'             => $this->generateSecret(),
169
        ]);
170
171
        return $this->insert();
172
    }
173
174
    public function storeCookie($includeDomain = true)
175
    {
176
        $hash = $this->getCookieHash($includeDomain);
177
        $cookieValue = $this->getPK().':'.$hash;
178
        $cookie = new Ajde_Cookie(config('app.id').'_user', true);
179
        $cookie->setLifetime($this->cookieLifetime);
180
        $cookie->set('auth', $cookieValue);
181
182
        return true;
183
    }
184
185
    public function getCookieHash($includeDomain = true)
186
    {
187
        if (empty($this->_data)) {
188
            // TODO:
189
            throw new Ajde_Exception('Invalid user object');
190
        }
191
        if (!in_array('sha256', hash_algos())) {
192
            // TODO:
193
            throw new Ajde_Exception('SHA-256 algorithm not available for hashing');
194
        }
195
        $userSecret = $this->get('secret');
196
        $appSecret = config('security.secret');
197
        if ($includeDomain) {
198
            $hash = hash('sha256', $userSecret.$appSecret.$_SERVER['REMOTE_ADDR'].$_SERVER['HTTP_USER_AGENT']);
199
        } else {
200
            $hash = hash('sha256', $userSecret.$appSecret);
201
        }
202
        if (empty($hash)) {
203
            // TODO:
204
            throw new Ajde_Exception('SHA-256 algorithm failed');
205
        }
206
207
        return $hash;
208
    }
209
210
    public function verifyCookie($includeDomain = true)
211
    {
212
        $cookie = new Ajde_Cookie(config('app.id').'_user', true);
213
        if (!$cookie->has('auth')) {
214
            return false;
215
        }
216
        $auth = $cookie->get('auth');
217
        list($uid, $hash) = explode(':', $auth);
218
        if (!$this->loadByPK($uid)) {
219
            return false;
220
        }
221
        if ($this->getCookieHash($includeDomain) === $hash) {
222
            $this->login();
223
            Ajde_Session_Flash::alert(sprintf(trans('Welcome back %s'), $this->getFullname()));
0 ignored issues
show
Documentation Bug introduced by
The method getFullname does not exist on object<Ajde_User>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
224
            Ajde_Cache::getInstance()->disable();
225
        } else {
226
            return false;
227
        }
228
    }
229
230
    public function canChangeEmailTo($newEmail)
231
    {
232
        if ($this->isFieldEncrypted('email')) {
233
            $newEmail = $this->doEncrypt($newEmail);
234
        }
235
        $values = [$newEmail, $this->getPK()];
236
        $sql = 'SELECT * FROM '.$this->_table.' WHERE email = ? AND id != ? LIMIT 1';
237
238
        return !$this->_load($sql, $values, false);
239
    }
240
241
    public function canChangeUsernameTo($newUsername)
242
    {
243
        if ($this->isFieldEncrypted($this->usernameField)) {
244
            $newUsername = $this->doEncrypt($newUsername);
245
        }
246
        $values = [$newUsername, $this->getPK()];
247
        $sql = 'SELECT * FROM '.$this->_table.' WHERE '.$this->usernameField.' = ? AND id != ? LIMIT 1';
248
249
        return !$this->_load($sql, $values, false);
250
    }
251
252
    public function resetUser()
253
    {
254
        if (!$this->hasNotEmpty('email')) {
255
            return false;
256
        }
257
        $resetHash = $this->getResetHash();
258
        $this->set('reset_hash', $resetHash);
259
        $this->save();
260
        $this->sendResetMail($resetHash);
261
262
        return $resetHash;
263
    }
264
265
    public function sendResetMail($hash)
266
    {
267
        // @todo exception
268
        throw new Ajde_Exception('Please implement sendResetMail in UserModel');
269
    }
270
271
    public function getResetHash()
272
    {
273
        if (empty($this->_data)) {
274
            // TODO:
275
            throw new Ajde_Exception('Invalid user object');
276
        }
277
        if (!in_array('sha256', hash_algos())) {
278
            // TODO:
279
            throw new Ajde_Exception('SHA-256 algorithm not available for hashing');
280
        }
281
        $userSecret = $this->get('secret');
282
        $appSecret = config('security.secret');
283
        $hash = strtotime('+1 month').':'.hash('sha256', $userSecret.$appSecret.microtime().rand());
284
285
        if (empty($hash)) {
286
            // TODO:
287
            throw new Ajde_Exception('SHA-256 algorithm failed');
288
        }
289
290
        return $hash;
291
    }
292
}
293