Completed
Push — master ( 14b95c...8cfd20 )
by Andreas
19:02
created

org_openpsa_user_accounthelper::is_blocked()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author CONTENT CONTROL http://www.contentcontrol-berlin.de/
4
 * @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/
5
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
6
 * @package org.openpsa.user
7
 */
8
9
/**
10
 * Helper class for creating a new account for an existing person
11
 *
12
 * @package org.openpsa.user
13
 */
14
class org_openpsa_user_accounthelper extends midcom_baseclasses_components_purecode
15
{
16
    /**
17
     * The person we're working on
18
     *
19
     * @var midcom_db_person
20
     */
21
    protected $person;
22
23
    /**
24
     * The account we're working on
25
     *
26
     * @var midcom_core_account
27
     */
28
    private $account;
29
30
    public $errstr;
31
32 14
    public function __construct(midcom_db_person $person = null)
33
    {
34 14
        if (null !== $person) {
35 10
            $this->person = $person;
36
        }
37 14
        parent::__construct();
38 14
    }
39
40 10
    protected function get_account() : midcom_core_account
41
    {
42 10
        if ($this->account === null) {
43 10
            $this->account = new midcom_core_account($this->person);
44
        }
45 10
        return $this->account;
46
    }
47
48
    /**
49
     * can be called by various handlers
50
     *
51
     * @param string $person_guid
52
     * @param string $username
53
     * @param string $usermail
54
     * @param string $password password: leave blank for auto generated
55
     * @param boolean $send_welcome_mail
56
     */
57 2
    public function create_account($person_guid, $username, $usermail, $password = "", $send_welcome_mail = false) : bool
58
    {
59 2
        $this->errstr = ""; // start fresh
60
61
        // quick validation
62 2
        if (empty($person_guid)) {
63 1
            $this->errstr = "Unable to identify user: no guid given";
64 1
            return false;
65
        }
66
67 2
        if (empty($username)) {
68 1
            $this->errstr = "Unable to create account: no username given";
69 1
            return false;
70
        }
71
72 2
        if ($send_welcome_mail && empty($usermail)) {
73 1
            $this->errstr = "Unable to deliver welcome mail: no usermail address given";
74 1
            return false;
75
        }
76
77
        // Check if we get the person
78 2
        $this->person = new midcom_db_person($person_guid);
79 2
        $this->person->require_do('midgard:update');
80
81
        //need to generate password?
82 2
        if (empty($password)) {
83 1
            $generated_password = true;
84 1
            $password = $this->generate_safe_password($this->_config->get("min_password_length"));
0 ignored issues
show
Bug introduced by
It seems like $this->_config->get('min_password_length') can also be of type false; however, parameter $length of org_openpsa_user_account...enerate_safe_password() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

84
            $password = $this->generate_safe_password(/** @scrutinizer ignore-type */ $this->_config->get("min_password_length"));
Loading history...
85
        } else {
86 2
            $generated_password = false;
87
        }
88
89 2
        $account = $this->get_account();
90
91
        //an account already existing?
92 2
        if ($account->get_password()) {
93 1
            $this->errstr = "Creating new account for existing account is not possible";
94 1
            return false;
95
        }
96
97
        //try creating
98 2
        if (!$this->set_account($username, $password)) {
99
            $this->errstr = "Could not set account, reason: " . midcom_connection::get_error_string();
100
            return false;
101
        }
102
103
        //send welcome mail?
104 2
        if ($send_welcome_mail) {
105 1
            $mail = new org_openpsa_mail();
106 1
            $mail->to = $usermail;
107
108
            // Make replacements to body
109 1
            $mail->parameters = [
110 1
                "USERNAME" => $username,
111 1
                "PASSWORD" => $password,
112
            ];
113
114 1
            $this->prepare_mail($mail);
115
116 1
            if (!$mail->send()) {
117
                $this->errstr = "Unable to deliver welcome mail: " . $mail->get_error_message();
0 ignored issues
show
Bug introduced by
Are you sure $mail->get_error_message() of type false|mixed|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

117
                $this->errstr = "Unable to deliver welcome mail: " . /** @scrutinizer ignore-type */ $mail->get_error_message();
Loading history...
118
                $this->delete_account();
119 1
                return false;
120
            }
121 1
        } elseif ($generated_password) {
122
            /*
123
             * no welcome mail was sent:
124
             * if the password was auto generated show it in an ui message
125
             */
126 1
            midcom::get()->uimessages->add(
127 1
                $this->_l10n->get('org.openpsa.user'),
128 1
                sprintf($this->_l10n->get("account_creation_success"), $username, $password), 'ok');
129
        }
130
131 2
        return true;
132
    }
133
134
    /**
135
     * Prepare the welcome mail for the user.
136
     *
137
     * The essential data (recipient, username, password) is already filled in
138
     * at this point. You can override this function in subclasses if you want
139
     * to customize the mail further
140
     *
141
     * @param org_openpsa_mail $mail
142
     */
143 1
    protected function prepare_mail(org_openpsa_mail $mail)
144
    {
145 1
        $mail->from = $this->_config->get('welcome_mail_from_address');
146 1
        $mail->subject = $this->_config->get('welcome_mail_title');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_config->get('welcome_mail_title') can also be of type false. However, the property $subject is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
147 1
        $mail->body = $this->_config->get('welcome_mail_body');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_config->get('welcome_mail_body') can also be of type false. However, the property $body is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
148 1
        $mail->parameters["SITE_URL"] = midcom::get()->config->get('midcom_site_url');
149 1
    }
150
151
    /**
152
     * Sets username and password for person
153
     *
154
     * @param string $username Contains username
155
     * @param string $new_password Contains the new password to set
156
     */
157 3
    public function set_account($username, $new_password) : bool
158
    {
159 3
        $account = $this->get_account();
160 3
        if (!empty($new_password)) {
161
            //check if the new encrypted password was already used
162 3
            if (   !$this->check_password_reuse($new_password, true)
163 3
                || !$this->check_password_strength($new_password, true)) {
164
                $this->errstr = "password strength too low";
165
                return false;
166
            }
167 3
            $this->save_old_password();
168 3
            $account->set_password($new_password);
169
        }
170
171 3
        $account->set_username($username);
172
173
        // probably username not unique
174 3
        if (!$account->save()) {
175
            $this->errstr = "Failed to save account, reason: " . midcom_connection::get_error_string();
176
            return false;
177
        }
178
179 3
        if (!empty($new_password)) {
180
            // add timestamp of password-change
181 3
            $this->person->set_parameter("org_openpsa_user_password", "last_change", time());
182
        }
183
        // sets privilege
184 3
        midcom::get()->auth->request_sudo($this->_component);
185 3
        $this->person->set_privilege('midgard:owner', "user:" . $this->person->guid);
186 3
        midcom::get()->auth->drop_sudo();
187
188 3
        return true;
189
    }
190
191
    /**
192
     * Returns an auto generated password of variable length
193
     *
194
     * @param int $length The number of chars the password will contain
195
     */
196 8
    public static function generate_password($length = 0) : string
197
    {
198
        // Safety
199 8
        if ($length == 0) {
200 7
            $length = 8;
201
        }
202
        // Valid characters for default password (PONDER: make configurable ?)
203 8
        $passwdchars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,-*!:+=()/&%$<>?#@';
204 8
        $first_last_chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
205
206
        //make sure password doesn't begin or end in punctuation character
207 8
        $password = midcom_helper_misc::random_string(1, $first_last_chars);
208 8
        $password .= midcom_helper_misc::random_string($length - 2, $passwdchars);
209 8
        $password .= midcom_helper_misc::random_string(1, $first_last_chars);
210 8
        return $password;
211
    }
212
213
    /**
214
     * Returns an auto generated password which will pass the persons check_password_strength test
215
     *
216
     * @param int $length The number of chars the password will contain
217
     */
218 6
    public function generate_safe_password($length = 0) : string
219
    {
220
        do {
221 6
            $password = self::generate_password($length);
222 6
        } while (!$this->check_password_strength($password));
223 6
        return $password;
224
    }
225
226
    /**
227
     * Function to check if passed password was already used
228
     *
229
     * @param string $password Password to check
230
     * @return bool returns true if password wasn't used already
231
     */
232 5
    public function check_password_reuse($password, $show_ui_message = false) : bool
233
    {
234
        // check current password
235 5
        if (midcom_connection::verify_password($password, $this->get_account()->get_password())) {
236 1
            if ($show_ui_message) {
237
                midcom::get()->uimessages->add($this->_l10n->get('org.openpsa.user'), $this->_l10n->get('password is the same as the current one'), 'error');
238
            }
239 1
            return false;
240
        }
241
242
        // get last passwords
243 5
        $old_passwords = $this->get_old_passwords();
244
245
        // check last passwords
246 5
        foreach ($old_passwords as $old) {
247 1
            if (midcom_connection::verify_password($password, $old)) {
248 1
                if ($show_ui_message) {
249
                    midcom::get()->uimessages->add($this->_l10n->get('org.openpsa.user'), $this->_l10n->get('password was already used'), 'error');
250
                }
251 1
                return false;
252
            }
253
        }
254 5
        return true;
255
    }
256
257
    /**
258
     * Function to add current password to parameter old passwords - does not update()
259
     */
260 3
    private function save_old_password()
261
    {
262 3
        $max_old_passwords = $this->_config->get('max_old_passwords');
263 3
        if ($max_old_passwords < 1) {
264
            return;
265
        }
266 3
        $old_passwords_array = $this->get_old_passwords();
267 3
        array_unshift($old_passwords_array, $this->get_account()->get_password());
268
269 3
        if (count($old_passwords_array) > $max_old_passwords) {
270
            array_pop($old_passwords_array);
271
        }
272
273 3
        $new_passwords_string = serialize($old_passwords_array);
274
275 3
        $this->person->set_parameter("org_openpsa_user_password", "old_passwords", $new_passwords_string);
276 3
    }
277
278
    /**
279
     * Function get old passwords
280
     *
281
     * @return array - Array with old passwords - empty if there aren't any old passwords
282
     */
283 5
    private function get_old_passwords() : array
284
    {
285 5
        if ($old_passwords_string = $this->person->get_parameter("org_openpsa_user_password", "old_passwords")) {
286 1
            $old_passwords_array = unserialize($old_passwords_string);
287 1
            $count = count($old_passwords_array);
288 1
            $max = (int) $this->_config->get('max_old_passwords');
289 1
            if ($count > $max) {
290 1
                $old_passwords_array = array_slice($old_passwords_array, 0, $max);
291
            }
292
        } else {
293 4
            $old_passwords_array = [];
294
        }
295 5
        return $old_passwords_array;
296
    }
297
298
    /**
299
     * Function to check strength of passed password
300
     *
301
     * @param string $password Contains password to check
302
     */
303 8
    public function check_password_strength($password, $show_ui_message = false) : bool
304
    {
305 8
        $password_length = mb_strlen($password);
306
307 8
        if ($password_length < $this->_config->get('min_password_length')) {
308 2
            if ($show_ui_message){
309
                midcom::get()->uimessages->add($this->_l10n->get('org.openpsa.user'), $this->_l10n->get('password too short'), 'error');
310
            }
311 2
            return false;
312
        }
313
314
        // score for length & repetition
315 7
        $score = $this->count_unique_characters($password) * 4;
316
317
        //check $password with rules
318 7
        $rules = $this->_config->get('password_score_rules');
319 7
        foreach ($rules as $rule) {
320 7
            if (preg_match($rule['match'], $password) > 0) {
321 7
                $score += $rule['score'];
322
            }
323
        }
324
325 7
        if ($score < $this->_config->get('min_password_score')) {
326 3
            if ($show_ui_message){
327
                midcom::get()->uimessages->add($this->_l10n->get('org.openpsa.user'), $this->_l10n->get('password weak'), 'error');
328
            }
329 3
            return false;
330
        }
331 7
        return true;
332
    }
333
334 7
    private function count_unique_characters(string $password) : int
335
    {
336
        // Split into individual (multibyte) characters, flip to filter out duplicates, and then count
337 7
        return count(array_flip(preg_split('//u', $password, null, PREG_SPLIT_NO_EMPTY)));
0 ignored issues
show
Bug introduced by
It seems like preg_split('//u', $passw...l, PREG_SPLIT_NO_EMPTY) can also be of type false; however, parameter $array of array_flip() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

337
        return count(array_flip(/** @scrutinizer ignore-type */ preg_split('//u', $password, null, PREG_SPLIT_NO_EMPTY)));
Loading history...
338
    }
339
340
    /**
341
     * Function to check password age for this user (age is taken from config)
342
     *
343
     * @return boolean - true indicates password is ok - false password is to old
344
     */
345 1
    public function check_password_age() : bool
346
    {
347 1
        $max_age_days = $this->_config->get('password_max_age_days');
348 1
        if ($max_age_days == 0) {
349
            return true;
350
        }
351
352 1
        if ($last_change = $this->person->get_parameter("org_openpsa_user_password", "last_change")) {
353 1
            $max_timeframe = time() - ($max_age_days * 24 * 60 * 60);
354 1
            return $max_timeframe < $last_change;
355
        }
356 1
        return false;
357
    }
358
359
    /**
360
     * Function to disable account for time period given in config
361
     *
362
     * @return boolean - indicates success
363
     */
364 2
    public function disable_account() : bool
365
    {
366 2
        $account = $this->get_account();
367
368 2
        $timeframe_minutes = $this->_config->get('password_block_timeframe_min');
369
370 2
        if ($timeframe_minutes == 0) {
371
            return false;
372
        }
373 2
        $release_time = time() + ($timeframe_minutes * 60);
374
        $args = [
375 2
            'guid' => $this->person->guid,
376 2
            'parameter_name' => 'org_openpsa_user_blocked_account',
377 2
            'password' => 'account_password',
378
        ];
379
380 2
        $qb = midcom_services_at_entry_dba::new_query_builder();
381 2
        $qb->add_constraint('argumentsstore', '=', serialize($args));
382 2
        $qb->add_constraint('status', '=', midcom_services_at_entry_dba::SCHEDULED);
383 2
        if ($entry = $qb->get_result(0)) {
384
            //the account is already blocked, so we just extend the block's duration
385
            $entry->start = $release_time;
386
            return $entry->update();
387
        }
388
389 2
        if (!midcom_services_at_interface::register($release_time, 'org.openpsa.user', 'reopen_account', $args)) {
390
            throw new midcom_error("Failed to register interface for re_open the user account, last Midgard error was: " . midcom_connection::get_error_string());
391
        }
392 2
        $this->person->set_parameter("org_openpsa_user_blocked_account", "account_password", $account->get_password());
393 2
        $account->set_password('', false);
394 2
        return $account->save();
395
    }
396
397
    /**
398
     * Function to delete account
399
     *
400
     * @return boolean indicates success
401
     */
402 1
    public function delete_account() : bool
403
    {
404 1
        return $this->get_account()->delete();
405
    }
406
407
    /**
408
     * Permanently disable an user account
409
     *
410
     * @return boolean - indicates success
411
     */
412 2
    public function close_account() : bool
413
    {
414 2
        $account = $this->get_account();
415
416 2
        if (!$account->get_password()) {
417
            // the account is already blocked, so skip the rest
418 1
            return true;
419
        }
420
421 2
        $this->person->set_parameter("org_openpsa_user_blocked_account", "account_password", $account->get_password());
422 2
        $account->set_password('', false);
423 2
        return $account->save();
424
    }
425
426
    /**
427
     * Reopen a blocked account.
428
     */
429 1
    public function reopen_account()
430
    {
431 1
        $account = new midcom_core_account($this->person);
432 1
        if ($account->get_password()) {
433 1
            debug_add('Account for person #' . $this->person->id . ' does have a password already');
434
        } else {
435 1
            $account->set_password($this->person->get_parameter('org_openpsa_user_blocked_account', 'account_password'), false);
0 ignored issues
show
Bug introduced by
It seems like $this->person->get_param...t', 'account_password') can also be of type null; however, parameter $password of midcom_core_account::set_password() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

435
            $account->set_password(/** @scrutinizer ignore-type */ $this->person->get_parameter('org_openpsa_user_blocked_account', 'account_password'), false);
Loading history...
436 1
            if (!$account->save()) {
437
                throw new midcom_error('Failed to save account: ' . midcom_connection::get_error_string());
438
            }
439
        }
440
441 1
        $this->person->delete_parameter('org_openpsa_user_blocked_account', 'account_password');
442 1
    }
443
444
    /**
445
     * Determine if an account is blocked
446
     */
447 4
    public function is_blocked() : bool
448
    {
449 4
        return !empty($this->person->get_parameter("org_openpsa_user_blocked_account", "account_password"));
450
    }
451
452 1
    public static function get_person_by_formdata($data)
453
    {
454 1
        if (   empty($data['username'])
455 1
            || empty($data['password'])) {
456 1
            return false;
457
        }
458
459 1
        midcom::get()->auth->request_sudo('org.openpsa.user');
460 1
        $qb = midcom_db_person::new_query_builder();
461 1
        midcom_core_account::add_username_constraint($qb, '=', $data['username']);
462 1
        $results = $qb->execute();
463 1
        midcom::get()->auth->drop_sudo();
464
465 1
        if (count($results) != 1) {
466 1
            return false;
467
        }
468 1
        return $results[0];
469
    }
470
471
    /**
472
     * Record failed login attempts and disable account is necessary
473
     *
474
     * @param string $component the component we take the config values from
475
     * @return boolean True if further login attempts are allowed, false otherwise
476
     */
477 1
    public function check_login_attempts($component = null) : bool
478
    {
479 1
        $stat = true;
480 1
        $component = $component ?: "org.openpsa.user";
481
482
        //max-attempts allowed & timeframe
483 1
        $max_attempts = midcom_baseclasses_components_configuration::get($component, 'config')->get('max_password_attempts');
484 1
        $timeframe = midcom_baseclasses_components_configuration::get($component, 'config')->get('password_block_timeframe_min');
485
486 1
        if (   $max_attempts == 0
487 1
            || $timeframe == 0) {
488
            return $stat;
489
        }
490
491 1
        midcom::get()->auth->request_sudo('org.openpsa.user');
492
493 1
        if ($attempts = $this->person->get_parameter("org_openpsa_user_password", "attempts")) {
494 1
            $attempts = unserialize($attempts);
495 1
            if (is_array($attempts)) {
496 1
                $attempts = array_slice($attempts, 0, ($max_attempts - 1));
497
            }
498
        }
499 1
        if (!is_array($attempts)) {
500 1
            $attempts = [];
501
        }
502 1
        array_unshift($attempts, time());
503
504
        /*
505
         * If the maximum number of attempts is reached and the oldest attempt
506
         * on the stack is within our defined timeframe, we block the account
507
         */
508 1
        if (   count($attempts) >= $max_attempts
509 1
            && $attempts[$max_attempts - 1] >= (time() - ($timeframe * 60))) {
510 1
            $this->disable_account();
511 1
            $stat = false;
512
        }
513
514 1
        $attempts = serialize($attempts);
515 1
        $this->person->set_parameter("org_openpsa_user_password", "attempts", $attempts);
516 1
        midcom::get()->auth->drop_sudo();
517 1
        return $stat;
518
    }
519
}
520