Passed
Push — master ( 06715c...2201e9 )
by Andreas
23:34
created

org_openpsa_user_accounthelper::send_mail()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2.024

Importance

Changes 0
Metric Value
cc 2
eloc 10
c 0
b 0
f 0
nc 2
nop 3
dl 0
loc 17
ccs 9
cts 11
cp 0.8182
crap 2.024
rs 9.9332
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
15
{
16 1
    use midcom_baseclasses_components_base;
0 ignored issues
show
introduced by
The trait midcom_baseclasses_components_base requires some properties which are not provided by org_openpsa_user_accounthelper: $i18n, $head
Loading history...
17
18
    /**
19
     * @var midcom_db_person
20
     */
21
    protected $person;
22
23
    /**
24
     * @var midcom_core_account
25
     */
26
    private $account;
27
28
    public $errstr;
29
30 15
    public function __construct(midcom_db_person $person = null)
31
    {
32 15
        $this->_component = 'org.openpsa.user';
33 15
        if (null !== $person) {
34 11
            $this->person = $person;
35
        }
36 15
    }
37
38 11
    protected function get_account() : midcom_core_account
39
    {
40 11
        if ($this->account === null) {
41 11
            $this->account = new midcom_core_account($this->person);
42
        }
43 11
        return $this->account;
44
    }
45
46
    /**
47
     * can be called by various handlers
48
     *
49
     * @param string $password password: leave blank for auto generated
50
     */
51 2
    public function create_account(string $person_guid, string $username, string $usermail, string $password = "", bool $send_welcome_mail = false) : bool
52
    {
53 2
        $this->errstr = ""; // start fresh
54
55
        // quick validation
56 2
        if (empty($person_guid)) {
57 1
            $this->errstr = "Unable to identify user: no guid given";
58 1
            return false;
59
        }
60
61 2
        if (empty($username)) {
62 1
            $this->errstr = "Unable to create account: no username given";
63 1
            return false;
64
        }
65
66 2
        if ($send_welcome_mail && empty($usermail)) {
67 1
            $this->errstr = "Unable to deliver welcome mail: no usermail address given";
68 1
            return false;
69
        }
70
71
        // Check if we get the person
72 2
        $this->person = new midcom_db_person($person_guid);
73 2
        $this->person->require_do('midgard:update');
74
75
        //need to generate password?
76 2
        if (empty($password)) {
77 1
            $generated_password = true;
78 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

78
            $password = $this->generate_safe_password(/** @scrutinizer ignore-type */ $this->_config->get("min_password_length"));
Loading history...
79
        } else {
80 2
            $generated_password = false;
81
        }
82
83 2
        $account = $this->get_account();
84
85
        //an account already existing?
86 2
        if ($account->get_password()) {
87 1
            $this->errstr = "Creating new account for existing account is not possible";
88 1
            return false;
89
        }
90
91
        //try creating
92 2
        if (!$this->set_account($username, $password)) {
93
            $this->errstr = "Could not set account, reason: " . midcom_connection::get_error_string();
94
            return false;
95
        }
96
97
        //send welcome mail?
98 2
        if ($send_welcome_mail) {
99 1
            if (!$this->send_mail($username, $usermail, $password)) {
100
                $this->delete_account();
101 1
                return false;
102
            }
103 1
        } elseif ($generated_password) {
104
            /*
105
             * no welcome mail was sent:
106
             * if the password was auto generated show it in an ui message
107
             */
108 1
            midcom::get()->uimessages->add(
109 1
                $this->_l10n->get('org.openpsa.user'),
110 1
                sprintf($this->_l10n->get("account_creation_success"), $username, $password), 'ok');
111
        }
112
113 2
        return true;
114
    }
115
116
    /**
117
     * Prepare the welcome mail for the user.
118
     *
119
     * The essential data (recipient, username, password) is already filled in
120
     * at this point. You can override this function in subclasses if you want
121
     * to customize the mail further
122
     */
123 2
    protected function prepare_mail(org_openpsa_mail $mail)
124
    {
125 2
        $mail->from = $this->_config->get('welcome_mail_from_address');
126 2
        $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...
127 2
        $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...
128 2
        $mail->parameters["SITE_URL"] = midcom::get()->config->get('midcom_site_url');
129 2
    }
130
131
    /**
132
     * Send mail
133
     */
134 2
    private function send_mail(string $username, string $usermail, string $password) : bool
135
    {
136 2
        $mail = new org_openpsa_mail();
137 2
        $mail->to = $usermail;
138
139
        // Make replacements to body
140 2
        $mail->parameters = [
141 2
            "USERNAME" => $username,
142 2
            "PASSWORD" => $password,
143
        ];
144
145 2
        $this->prepare_mail($mail);
146 2
        if (!$mail->send()) {
147
            $this->errstr = "Unable to deliver welcome mail: " . $mail->get_error_message();
148
            return false;
149
        }
150 2
        return true;
151
    }
152
153
    /**
154
     * Sets username and password for person
155
     *
156
     * @param string $new_password Contains the new password to set
157
     */
158 5
    public function set_account(string $username, $new_password) : bool
159
    {
160 5
        $account = $this->get_account();
161 5
        if (!empty($new_password)) {
162
            //check if the new encrypted password was already used
163 5
            if (   !$this->check_password_reuse($new_password, true)
164 5
                || !$this->check_password_strength($new_password, true)) {
165
                $this->errstr = "password strength too low";
166
                return false;
167
            }
168 5
            $this->save_old_password();
169 5
            $account->set_password($new_password);
170
        }
171
172 5
        $account->set_username($username);
173
174
        // probably username not unique
175 5
        if (!$account->save()) {
176
            $this->errstr = "Failed to save account, reason: " . midcom_connection::get_error_string();
177
            return false;
178
        }
179
180 5
        if (!empty($new_password)) {
181
            // add timestamp of password-change
182 5
            $this->person->set_parameter("org_openpsa_user_password", "last_change", time());
183
        }
184
        // sets privilege
185 5
        midcom::get()->auth->request_sudo($this->_component);
186 5
        $this->person->set_privilege('midgard:owner', "user:" . $this->person->guid);
187 5
        midcom::get()->auth->drop_sudo();
188
189 5
        return true;
190
    }
191
192
    /**
193
     * Returns an auto generated password which will pass the persons check_password_strength test
194
     */
195 7
    public function generate_safe_password(int $length = 0) : string
196
    {
197
        do {
198 7
            $password = midgard_admin_user_plugin::generate_password($length);
199 7
        } while (!$this->check_password_strength($password));
200 7
        return $password;
201
    }
202
203
    /**
204
     * Function to check if passed password was already used
205
     *
206
     * @return bool returns true if password wasn't used already
207
     */
208 6
    public function check_password_reuse(string $password, bool $show_ui_message = false) : bool
209
    {
210
        // check current password
211 6
        if (midcom_connection::verify_password($password, $this->get_account()->get_password())) {
212 1
            if ($show_ui_message) {
213
                midcom::get()->uimessages->add($this->_l10n->get('org.openpsa.user'), $this->_l10n->get('password is the same as the current one'), 'error');
214
            }
215 1
            return false;
216
        }
217
218
        // get last passwords
219 6
        $old_passwords = $this->get_old_passwords();
220
221
        // check last passwords
222 6
        foreach ($old_passwords as $old) {
223 1
            if (midcom_connection::verify_password($password, $old)) {
224 1
                if ($show_ui_message) {
225
                    midcom::get()->uimessages->add($this->_l10n->get('org.openpsa.user'), $this->_l10n->get('password was already used'), 'error');
226
                }
227 1
                return false;
228
            }
229
        }
230 6
        return true;
231
    }
232
233
    /**
234
     * Function to add current password to parameter old passwords - does not update()
235
     */
236 5
    private function save_old_password()
237
    {
238 5
        $max_old_passwords = $this->_config->get('max_old_passwords');
239 5
        if ($max_old_passwords < 1) {
240
            return;
241
        }
242 5
        $old_passwords_array = $this->get_old_passwords();
243 5
        array_unshift($old_passwords_array, $this->get_account()->get_password());
244
245 5
        if (count($old_passwords_array) > $max_old_passwords) {
246
            array_pop($old_passwords_array);
247
        }
248
249 5
        $new_passwords_string = serialize($old_passwords_array);
250
251 5
        $this->person->set_parameter("org_openpsa_user_password", "old_passwords", $new_passwords_string);
252 5
    }
253
254
    /**
255
     * Function get old passwords
256
     *
257
     * @return array - Array with old passwords - empty if there aren't any old passwords
258
     */
259 6
    private function get_old_passwords() : array
260
    {
261 6
        if ($old_passwords_string = $this->person->get_parameter("org_openpsa_user_password", "old_passwords")) {
262 1
            $old_passwords_array = unserialize($old_passwords_string);
263 1
            $count = count($old_passwords_array);
264 1
            $max = (int) $this->_config->get('max_old_passwords');
265 1
            if ($count > $max) {
266 1
                $old_passwords_array = array_slice($old_passwords_array, 0, $max);
267
            }
268
        } else {
269 6
            $old_passwords_array = [];
270
        }
271 6
        return $old_passwords_array;
272
    }
273
274
    /**
275
     * Function to check strength of passed password
276
     */
277 9
    public function check_password_strength(string $password, bool $show_ui_message = false) : bool
278
    {
279 9
        $password_length = mb_strlen($password);
280
281 9
        if ($password_length < $this->_config->get('min_password_length')) {
282 2
            if ($show_ui_message){
283
                midcom::get()->uimessages->add($this->_l10n->get('org.openpsa.user'), $this->_l10n->get('password too short'), 'error');
284
            }
285 2
            return false;
286
        }
287
288
        // score for length & repetition
289 8
        $score = $this->count_unique_characters($password) * 4;
290
291
        //check $password with rules
292 8
        $rules = $this->_config->get_array('password_score_rules');
293 8
        foreach ($rules as $rule) {
294 8
            if (preg_match($rule['match'], $password) > 0) {
295 8
                $score += $rule['score'];
296
            }
297
        }
298
299 8
        if ($score < $this->_config->get('min_password_score')) {
300 3
            if ($show_ui_message){
301
                midcom::get()->uimessages->add($this->_l10n->get('org.openpsa.user'), $this->_l10n->get('password weak'), 'error');
302
            }
303 3
            return false;
304
        }
305 8
        return true;
306
    }
307
308 8
    private function count_unique_characters(string $password) : int
309
    {
310
        // Split into individual (multibyte) characters, flip to filter out duplicates, and then count
311 8
        return count(array_flip(preg_split('//u', $password, -1, PREG_SPLIT_NO_EMPTY)));
312
    }
313
314
    /**
315
     * Function to check password age for this user (age is taken from config)
316
     *
317
     * @return boolean - true indicates password is ok - false password is to old
318
     */
319 1
    public function check_password_age() : bool
320
    {
321 1
        $max_age_days = $this->_config->get('password_max_age_days');
322 1
        if ($max_age_days == 0) {
323
            return true;
324
        }
325
326 1
        if ($last_change = $this->person->get_parameter("org_openpsa_user_password", "last_change")) {
327 1
            $max_timeframe = time() - ($max_age_days * 24 * 60 * 60);
328 1
            return $max_timeframe < $last_change;
329
        }
330 1
        return false;
331
    }
332
333
    /**
334
     * Function sends an email to the user with username and password
335
     *
336
     * @return boolean indicates success
337
     */
338 1
    public function welcome_email() : bool
339
    {
340 1
        $username = $this->get_account()->get_username();
341 1
        $email = $this->person->email;
342
        
343
        //Resets Password
344 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

344
        $password = $this->generate_safe_password(/** @scrutinizer ignore-type */ $this->_config->get("min_password_length"));
Loading history...
345 1
        $this->set_account($username, $password);
346 1
        return $this->send_mail($username, $email, $password);
347
    }
348
349
    /**
350
     * Function to disable account for time period given in config
351
     *
352
     * @return boolean - indicates success
353
     */
354 2
    public function disable_account() : bool
355
    {
356 2
        $account = $this->get_account();
357
358 2
        $timeframe_minutes = $this->_config->get('password_block_timeframe_min');
359
360 2
        if ($timeframe_minutes == 0) {
361
            return false;
362
        }
363 2
        $release_time = time() + ($timeframe_minutes * 60);
364
        $args = [
365 2
            'guid' => $this->person->guid,
366 2
            'parameter_name' => 'org_openpsa_user_blocked_account',
367 2
            'password' => 'account_password',
368
        ];
369
370 2
        $qb = midcom_services_at_entry_dba::new_query_builder();
371 2
        $qb->add_constraint('argumentsstore', '=', serialize($args));
372 2
        $qb->add_constraint('status', '=', midcom_services_at_entry_dba::SCHEDULED);
373 2
        if ($entry = $qb->get_result(0)) {
374
            //the account is already blocked, so we just extend the block's duration
375
            $entry->start = $release_time;
376
            return $entry->update();
377
        }
378
379 2
        if (!midcom_services_at_interface::register($release_time, 'org.openpsa.user', 'reopen_account', $args)) {
380
            throw new midcom_error("Failed to register interface for re_open the user account, last Midgard error was: " . midcom_connection::get_error_string());
381
        }
382 2
        $this->person->set_parameter("org_openpsa_user_blocked_account", "account_password", $account->get_password());
383 2
        $account->set_password('', false);
384 2
        return $account->save();
385
    }
386
387
    /**
388
     * Function to delete account
389
     *
390
     * @return boolean indicates success
391
     */
392 1
    public function delete_account() : bool
393
    {
394 1
        return $this->get_account()->delete();
395
    }
396
397
    /**
398
     * Permanently disable an user account
399
     *
400
     * @return boolean - indicates success
401
     */
402 2
    public function close_account() : bool
403
    {
404 2
        $account = $this->get_account();
405
406 2
        if (!$account->get_password()) {
407
            // the account is already blocked, so skip the rest
408 1
            return true;
409
        }
410
411 2
        $this->person->set_parameter("org_openpsa_user_blocked_account", "account_password", $account->get_password());
412 2
        $account->set_password('', false);
413 2
        return $account->save();
414
    }
415
416
    /**
417
     * Reopen a blocked account.
418
     */
419 1
    public function reopen_account()
420
    {
421 1
        $account = new midcom_core_account($this->person);
422 1
        if ($account->get_password()) {
423 1
            debug_add('Account for person #' . $this->person->id . ' does have a password already');
424
        } else {
425 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

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