Completed
Push — master ( 99f21a...de69c8 )
by Patrick
02:48
created

SQLAuthenticator::activatePendingUser()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 7
nop 1
dl 0
loc 26
rs 8.439
c 0
b 0
f 0
1
<?php
2
namespace Auth;
3
4
if(!function_exists('password_hash') || !function_exists('password_verify'))
5
{
6
    define('PASSWORD_BCRYPT', 1);
7
    define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
8
    define('PASSWORD_BCRYPT_DEFAULT_COST', 10);
9
10
    function password_hash($password, $algo = PASSWORD_DEFAULT)
11
    {
12
        if(is_null($password) || is_int($password))
13
        {
14
            $password = (string)$password;
15
        }
16
        if(!is_string($password))
17
        {
18
            trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
19
            return false;
20
        }
21
        if(!is_int($algo))
22
        {
23
            trigger_error("password_hash() expects parameter 2 to be long, ".gettype($algo)." given", E_USER_WARNING);
24
            return false;
25
        }
26
        switch($algo)
27
        {
28
            case PASSWORD_BCRYPT:
29
                $cost = PASSWORD_BCRYPT_DEFAULT_COST;
30
                $rawSaltLen = 16;
31
                $requiredSaltLen = 22;
32
                $hashFormat = sprintf("$2y$%02d$", $cost);
33
                $resultLength = 60;
34
                break;
35
            default:
36
                trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
37
                return false;
38
        }
39
        $salt = openssl_random_pseudo_bytes($rawSaltLen);
40
        $base64Digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
41
        $bcrypt64Digits = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
42
        $base64String = base64_encode($salt);
43
        $salt = strtr(rtrim($base64String, '='), $base64Digits, $bcrypt64Digits);
44
        $salt = substr($salt, 0, $requiredSaltLen);
45
        $hash = $hashFormat.$salt;
46
        $ret = crypt($password, $hash);
47
        if(!is_string($ret) || strlen($ret) != $resultLength)
48
        {
49
            return false;
50
        }
51
        return $ret;
52
    }
53
54
    function password_verify($password, $hash)
55
    {
56
        $ret = crypt($password, $hash);
57
        if(!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13)
58
        {
59
            return false;
60
        }
61
        $status = 0;
62
        $count  = strlen($ret);
63
        for($i = 0; $i < $count; $i++)
64
        {
65
            $status |= (ord($ret[$i]) ^ ord($hash[$i]));
66
        }
67
        return $status === 0;
68
    }
69
}
70
71
class SQLAuthenticator extends Authenticator
72
{
73
    public $dataSet = null;
74
    public $pendingDataSet = null;
75
    private $dataTables = array();
76
    private $params;
77
78
    public function __construct($params)
79
    {
80
        parent::__construct($params);
81
        $this->params = $params;
82
        if($this->current)
83
        {
84
            $this->dataSet = $this->getCurrentDataSet();
85
        }
86
        if($this->pending)
87
        {
88
            $this->pendingDataSet = $this->getPendingDataSet();
89
        }
90
    }
91
92
    /**
93
     * @SuppressWarnings("StaticAccess")
94
     */
95
    private function getCurrentDataSet()
96
    {
97
        if(isset($this->params['current_data_set']))
98
        {
99
            return \DataSetFactory::getDataSetByName($this->params['current_data_set']);
100
        }
101
        return \DataSetFactory::getDataSetByName('authentication');
102
    }
103
104
    /**
105
     * @SuppressWarnings("StaticAccess")
106
     */
107
    private function getPendingDataSet()
108
    {
109
        if(isset($this->params['pending_data_set']))
110
        {
111
            return \DataSetFactory::getDataSetByName($this->params['pending_data_set']);
112
        }
113
        return \DataSetFactory::getDataSetByName('pending_authentication');
114
    }
115
116
    private function getDataTable($name, $pending = false)
117
    {
118
        if(isset($this->dataTables[$name]) && isset($this->dataTables[$name][$pending]))
119
        {
120
            return $this->dataTables[$name][$pending];
121
        }
122
        $dataSet = $this->dataSet;
123
        if($pending)
124
        {
125
            $dataSet = $this->pendingDataSet;
126
        }
127
        if($dataSet === null)
128
        {
129
            throw new \Exception('Unable to obtain dataset for SQL Authentication!');
130
        }
131
        $dataTable = $dataSet[$name];
132
        if(!isset($this->dataTables[$name]))
133
        {
134
            $this->dataTables[$name] = array();
135
        }
136
        $this->dataTables[$name][$pending] = $dataTable;
137
        return $dataTable;
138
    }
139
140
    /**
141
     * Get the data table for Pending Users
142
     *
143
     * @return boolean|\Data\DataTable The Pending User Data Table
144
     */
145
    private function getPendingUserDataTable()
146
    {
147
        if(isset($this->params['pending_user_table']))
148
        {
149
            return $this->getDataTable($this->params['pending_user_table'], true);
150
        }
151
        return $this->getDataTable('users', true);
152
    }
153
154
    public function login($username, $password)
155
    {
156
        if($this->current === false)
157
        {
158
            return false;
159
        }
160
        $userDataTable = $this->getDataTable('user');
161
        $filter = new \Data\Filter("uid eq '$username'");
162
        $users = $userDataTable->read($filter);
163
        if($users === false || !isset($users[0]))
164
        {
165
            return false;
166
        }
167
        if(password_verify($password, $users[0]['pass']))
168
        {
169
            return array('res'=>true, 'extended'=>$users[0]);
170
        }
171
        return false;
172
    }
173
174
    public function isLoggedIn($data)
175
    {
176
        if(isset($data['res']))
177
        {
178
            return $data['res'];
179
        }
180
        return false;
181
    }
182
183
    public function getUser($data)
184
    {
185
        if(isset($this->params['current_data_set']))
186
        {
187
            $data['current_data_set'] = $this->params['current_data_set'];
188
        }
189
        return new SQLUser($data, $this);
0 ignored issues
show
Documentation introduced by
$this is of type this<Auth\SQLAuthenticator>, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
190
    }
191
192
    private function getEntityByFilter($tableName, $filterStr, $className)
193
    {
194
        $dataTable = $this->getDataTable($tableName);
195
        $filter = new \Data\Filter($filterStr);
196
        $entities = $dataTable->read($filter);
197
        if(empty($entities))
198
        {
199
            return null;
200
        }
201
        return new $className($entities[0], $this);
202
    }
203
204
    public function getGroupByName($name)
205
    {
206
        return $this->getEntityByFilter('group', "gid eq '$name'", '\Auth\SQLGroup');
207
    }
208
209
    public function getUserByName($name)
210
    {
211
        return $this->getEntityByFilter('user', "uid eq '$name'", '\Auth\SQLUser');
212
    }
213
214
    private function getDataByFilter($dataTableName, $filter, $select, $top, $skip, $orderby)
215
    {
216
        $dataTable = $this->getDataTable($dataTableName);
217
        return $dataTable->read($filter, $select, $top, $skip, $orderby);
218
    }
219
220
    /**
221
     * @param string $dataTableName The Data Table to serach
222
     * @param string $className The class to obtain data in
223
     * @param boolean|array $select The fields to read
224
     * @param boolean|integer $top The number of entities to read
225
     * @param boolean|integer $skip The number of entities to skip
226
     * @param boolean|array $orderby The fields to sort by
227
     */
228
    private function convertDataToClass($dataTableName, $className, $filter, $select, $top, $skip, $orderby)
229
    {
230
        $data = $this->getDataByFilter($dataTableName, $filter, $select, $top, $skip, $orderby);
231
        if($data === false)
232
        {
233
            return false;
234
        }
235
        $count = count($data);
236
        for($i = 0; $i < $count; $i++)
237
        {
238
            $data[$i] = new $className($groups[$i], $this);
0 ignored issues
show
Bug introduced by
The variable $groups does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
239
        }
240
        return $data;
241
    }
242
243
    /**
244
     * @param boolean|array $select The fields to read
245
     * @param boolean|integer $top The number of entities to read
246
     * @param boolean|integer $skip The number of entities to skip
247
     * @param boolean|array $orderby The fields to sort by
248
     */
249
    public function getGroupsByFilter($filter, $select = false, $top = false, $skip = false, $orderby = false)
250
    {
251
        return $this->convertDataToClass('group', 'SQLGroup', $filter, $select, $top, $skip, $orderby);
252
    }
253
254
    /**
255
     * @param boolean|array $select The fields to read
256
     * @param boolean|integer $top The number of entities to read
257
     * @param boolean|integer $skip The number of entities to skip
258
     * @param boolean|array $orderby The fields to sort by
259
     */
260
    public function getUsersByFilter($filter, $select = false, $top = false, $skip = false, $orderby = false)
261
    {
262
        return $this->convertDataToClass('group', 'SQLUser', $filter, $select, $top, $skip, $orderby);
263
    }
264
265
    public function getPendingUserCount()
266
    {
267
        if($this->pending === false)
268
        {
269
            return 0;
270
        }
271
        $dataTable = $this->getPendingUserDataTable();
272
        if($dataTable === null)
273
        {
274
            return 0;
275
        }
276
        return $dataTable->count();
277
    }
278
279
    private function searchPendingUsers($filter, $select, $top, $skip, $orderby)
280
    {
281
        $userDataTable = $this->getPendingUserDataTable();
282
        $fieldData = $filter->to_mongo_filter();
283
        $firstFilter = new \Data\Filter('substringof(data,"'.implode($fieldData, ' ').'")');
284
        $users = $userDataTable->read($firstFilter, $select, $top, $skip, $orderby);
285
        if($users === false)
286
        {
287
            return false;
288
        }
289
        $ret = array();
290
        $count = count($users);
291
        for($i = 0; $i < $count; $i++)
292
        {
293
            $user = new SQLPendingUser($users[$i], $userDataTable);
0 ignored issues
show
Bug introduced by
It seems like $userDataTable defined by $this->getPendingUserDataTable() on line 281 can also be of type object<Data\DataTable>; however, Auth\SQLPendingUser::__construct() does only seem to accept boolean, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
294
            $err = false;
295
            foreach($fieldData as $field=>$data)
296
            {
297
                if(strcasecmp($user[$field], $data) !== 0)
298
                {
299
                    $err = true; break;
300
                }
301
            }
302
            if(!$err)
303
            {
304
                array_push($ret, $user);
305
            }
306
        }
307
        return $ret;
308
    }
309
310
    /**
311
     * @param \Data\Filter $filter The filter to read with
312
     * @param boolean|array $select The fields to read
313
     * @param boolean|integer $top The number of entities to read
314
     * @param boolean|integer $skip The number of entities to skip
315
     * @param boolean|array $orderby The fields to sort by
316
     */
317
    public function getPendingUsersByFilter($filter, $select = false, $top = false, $skip = false, $orderby = false)
318
    {
319
        if($this->pending === false)
320
        {
321
            return false;
322
        }
323
        if($filter !== false && !$filter->contains('hash'))
324
        {
325
            return $this->searchPendingUsers($filter, $select, $top, $skip, $orderby);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->searchPendingUser...$top, $skip, $orderby); of type false|array adds the type array to the return on line 325 which is incompatible with the return type of the parent method Auth\Authenticator::getPendingUsersByFilter of type boolean.
Loading history...
326
        }
327
        $userDataTable = $this->getPendingUserDataTable();
328
        $users = $userDataTable->read($filter, $select, $top, $skip, $orderby);
329
        if($users === false)
330
        {
331
            return false;
332
        }
333
        $count = count($users);
334
        for($i = 0; $i < $count; $i++)
335
        {
336
            $users[$i] = new SQLPendingUser($users[$i], $userDataTable);
0 ignored issues
show
Bug introduced by
It seems like $userDataTable defined by $this->getPendingUserDataTable() on line 327 can also be of type object<Data\DataTable>; however, Auth\SQLPendingUser::__construct() does only seem to accept boolean, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
337
        }
338
        return $users;
339
    }
340
341
    public function activatePendingUser($user)
342
    {
343
        if($this->pending === false)
344
        {
345
            return false;
346
        }
347
        $userDataTable = $this->getPendingUserDataTable();
348
        if(isset($user->password2))
349
        {
350
            unset($user->password2);
351
        }
352
        $json = json_encode($user);
353
        $hash = hash('sha512', $json);
354
        $array = array('hash'=>$hash, 'data'=>$json);
355
        $ret = $userDataTable->create($array);
356
        if($ret !== false)
357
        {
358
            $users = $this->getPendingUsersByFilter(new \Data\Filter("hash eq '$hash'"));
359
            if($users === false || !isset($users[0]))
360
            {
361
                throw new \Exception('Error retreiving user object after successful create!');
362
            }
363
            $users[0]->sendEmail();
364
        }
365
        return $ret;
366
    }
367
368
    public function getTempUserByHash($hash)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
369
    {
370
        $users = $this->getPendingUsersByFilter(new \Data\Filter("hash eq '$hash'"));
371
        if($users === false || !isset($users[0]))
372
        {
373
            return false;
374
        }
375
        return $users[0];
376
    }
377
}
378
/* vim: set tabstop=4 shiftwidth=4 expandtab: */
379