Completed
Push — master ( 8edb92...ad3576 )
by Ralf
15:11
created

preferences_password::change()   F

Complexity

Conditions 25
Paths 1216

Size

Total Lines 169
Code Lines 107

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 25
eloc 107
c 1
b 0
f 0
nc 1216
nop 1
dl 0
loc 169
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * EGroupware preferences
4
 *
5
 * @package preferences
6
 * @link http://www.egroupware.org
7
 * @author Joseph Engo <[email protected]>
8
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
9
 */
10
11
use EGroupware\Api;
12
use EGroupware\Api\Framework;
13
use EGroupware\Api\Etemplate;
14
use PragmaRX\Google2FAQRCode\Google2FA;
15
use EGroupware\Api\Mail\Credentials;
16
use EGroupware\OpenID\Repositories\AccessTokenRepository;
0 ignored issues
show
Bug introduced by
The type EGroupware\OpenID\Reposi...s\AccessTokenRepository was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use EGroupware\OpenID\Repositories\ScopeRepository;
0 ignored issues
show
Bug introduced by
The type EGroupware\OpenID\Repositories\ScopeRepository was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use EGroupware\OpenID\Repositories\RefreshTokenRepository;
0 ignored issues
show
Bug introduced by
The type EGroupware\OpenID\Reposi...\RefreshTokenRepository was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
20
class preferences_password
21
{
22
	var $public_functions = array(
23
		'change' => True
24
	);
25
	const GAUTH_ANDROID = 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2';
26
	const GAUTH_IOS = 'https://appstore.com/googleauthenticator';
27
28
	/**
29
	 * Change password, two factor auth or revoke tokens
30
	 *
31
	 * @param type $content
32
	 */
33
	function change($content = null)
34
	{
35
		if ($GLOBALS['egw']->acl->check('nopasswordchange', 1))
36
		{
37
			Framework::window_close('Password change is disabled!');
38
		}
39
		$GLOBALS['egw_info']['flags']['app_header'] = lang('Change your password');
40
		$tmpl = new Etemplate('preferences.password');
41
42
		$readonlys = $sel_options = [];
43
		try {
44
			// PHP 7.1+: using SVG image backend (requiring XMLWriter) and not ImageMagic extension
45
			if (class_exists('BaconQrCode\Renderer\Image\SvgImageBackEnd'))
46
			{
47
				$image_backend = new \BaconQrCode\Renderer\Image\SvgImageBackEnd;
48
			}
49
			$google2fa = new Google2FA($image_backend);
50
			$prefs = new Api\Preferences($GLOBALS['egw_info']['user']['account_id']);
51
			$prefs->read_repository();
52
53
			if (!is_array($content))
0 ignored issues
show
introduced by
The condition is_array($content) is always false.
Loading history...
54
			{
55
				$content = [];
56
				$content['2fa'] = $this->generateQRCode($google2fa)+[
57
					'gauth_android' => self::GAUTH_ANDROID,
58
					'gauth_ios' => self::GAUTH_IOS,
59
				];
60
			}
61
			else
62
			{
63
				$secret_key = $content['2fa']['secret_key'];
64
				unset($content['2fa']['secret_key']);
65
66
				switch($content['tabs'])
67
				{
68
					case 'change_password':
69
						if ($content['button']['save'])
70
						{
71
							if (($errors = self::do_change($content['password'], $content['n_passwd'], $content['n_passwd_2'])))
72
							{
73
								Framework::message(implode("\n", $errors), 'error');
74
								$content = array();
75
							}
76
							else
77
							{
78
								Framework::refresh_opener(lang('Password changed'), 'preferences');
79
								Framework::window_close();
80
							}
81
						}
82
						break;
83
84
					case 'two_factor_auth':
85
						$auth = new Api\Auth();
86
						if (!$auth->authenticate($GLOBALS['egw_info']['user']['account_lid'], $content['password']))
87
						{
88
							$tmpl->set_validation_error('password', lang('Password is invalid'), '2fa');
89
							break;
90
						}
91
						switch(key($content['2fa']['action']))
92
						{
93
							case 'show':
94
								$content['2fa'] = $this->generateQRCode($google2fa, false);
95
								break;
96
							case 'reset':
97
								$content['2fa'] = $this->generateQRCode($google2fa, true);
98
								Framework::message(lang('New secret generated, you need to save it to disable the old one!'));
99
								break;
100
							case 'disable':
101
								if (Credentials::delete(0, $GLOBALS['egw_info']['user']['account_id'], Credentials::TWOFA))
102
								{
103
									Framework::refresh_opener(lang('Secret deleted, two factor authentication disabled.'), 'preferences');
104
									Framework::window_close();
105
								}
106
								else
107
								{
108
									Framework::message(lang('Failed to delete secret!'), 'error');
109
								}
110
								break;
111
							default:	// no action, save secret
112
								if (!$google2fa->verifyKey($secret_key, $content['2fa']['code']))
113
								{
114
									$tmpl->set_validation_error('code', lang('Code is invalid'), '2fa');
115
									break 2;
116
								}
117
								if (($content['2fa']['cred_id'] = Credentials::write(0,
118
									$GLOBALS['egw_info']['user']['account_lid'],
119
									$secret_key, Credentials::TWOFA,
120
									$GLOBALS['egw_info']['user']['account_id'],
121
									$content['2fa']['cred_id'])))
122
								{
123
									Framework::refresh_opener(lang('Two Factor Auth enabled.'), 'preferences');
124
									Framework::window_close();
125
								}
126
								else
127
								{
128
									Framework::message(lang('Failed to store secret!'), 'error');
129
								}
130
								break;
131
						}
132
						unset($content['2fa']['action']);
133
						break;
134
135
					case 'tokens':
136
						if (is_array($content) && $content['nm']['selected'])
137
						{
138
							try {
139
								switch($content['nm']['action'])
140
								{
141
									case 'delete':
142
										$token_repo = new AccessTokenRepository();
143
										$token_repo->revokeAccessToken(['access_token_id' => $content['nm']['selected']]);
144
										$refresh_token_repo = new RefreshTokenRepository();
145
										$refresh_token_repo->revokeRefreshToken(['access_token_id' => $content['nm']['selected']]);
146
										$msg = (count($content['nm']['selected']) > 1 ?
0 ignored issues
show
Unused Code introduced by
The assignment to $msg is dead and can be removed.
Loading history...
147
											count($content['nm']['selected']).' ' : '').
148
											lang('Access Token revoked.');
149
										break;
150
								}
151
							}
152
							catch(\Exception $e) {
153
								$msg = lang('Error').': '.$e->getMessage();
154
								break;
155
							}
156
						}
157
						break;
158
				}
159
			}
160
		}
161
		catch (Exception $e) {
162
			Framework::message($e->getMessage(), 'error');
163
		}
164
165
		// display tokens, if we have openid installed (currently no run-rights needed!)
166
		if ($GLOBALS['egw_info']['apps']['openid'] && class_exists(AccessTokenRepository::class))
167
		{
168
			$content['nm'] = [
169
				'get_rows' => 'preferences.'.__CLASS__.'.getTokens',
170
				'no_cat' => true,
171
				'no_filter' => true,
172
				'no_filter2' => true,
173
				'filter_no_lang' => true,
174
				'order' => 'access_token_updated',
175
				'sort' => 'DESC',
176
				'row_id' => 'access_token_id',
177
				'default_cols' => '!client_id',
178
				'actions' => self::tokenActions(),
0 ignored issues
show
Bug Best Practice introduced by
The method preferences_password::tokenActions() is not static, but was called statically. ( Ignorable by Annotation )

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

178
				'actions' => self::/** @scrutinizer ignore-call */ tokenActions(),
Loading history...
179
			];
180
			$sel_options += [
181
				'client_status' => ['Disabled', 'Active'],
182
				'access_token_revoked' => ['Active', 'Revoked'],
183
				'access_token_scopes' => (new ScopeRepository())->selOptions(),
184
			];
185
		}
186
		else
187
		{
188
			$readonlys['tabs']['tokens'] = true;
189
		}
190
191
		// disable 2FA tab, if admin disabled it
192
		if ($GLOBALS['egw_info']['server']['2fa_required'] === 'disabled')
193
		{
194
			$readonlys['tabs']['two_factor_auth'] = true;
195
		}
196
197
		$tmpl->exec('preferences.preferences_password.change', $content, $sel_options, $readonlys, [
198
			'2fa' => $content['2fa']+[
199
				'secret_key' => $secret_key,
200
			],
201
		], 2);
202
	}
203
204
	/**
205
	 * Query tokens for nextmatch widget
206
	 *
207
	 * @param array $query with keys 'start', 'search', 'order', 'sort', 'col_filter'
208
	 *	For other keys like 'filter', 'cat_id' you have to reimplement this method in a derived class.
209
	 * @param array &$rows returned rows/competitions
210
	 * @param array &$readonlys eg. to disable buttons based on acl, not use here, maybe in a derived class
211
	 * @return int number of rows found
212
	 */
213
	public function getTokens(array $query, array &$rows, array &$readonlys)
214
	{
215
		if (!class_exists(AccessTokenRepository::class)) return;
216
217
		$token_repo = new AccessTokenRepository();
218
		if (($ret = $token_repo->get_rows($query, $rows, $readonlys)))
219
		{
220
			foreach($rows as $key => &$row)
221
			{
222
				if (!is_int($key)) continue;
223
224
				// boolean does NOT work as key for select-box
225
				$row['access_token_revoked'] = (string)(int)$row['access_token_revoked'];
226
				$row['client_status'] = (string)(int)$row['client_status'];
227
228
				// dont send token itself to UI
229
				unset($row['access_token_identifier']);
230
231
				// format user-agent as "OS Version\nBrowser Version" prefering auth-code over access-token
232
				// as for implicit grant auth-code contains real user-agent, access-token container the server
233
				if (!empty($row['auth_code_user_agent']))
234
				{
235
					$row['user_agent'] = Api\Header\UserAgent::osBrowser($row['auth_code_user_agent']);
236
					$row['user_ip'] = $row['auth_code_ip'];
237
					$row['user_agent_tooltip'] = Api\Header\UserAgent::osBrowser($row['access_token_user_agent']);
238
					$row['user_ip_tooltip'] = $row['access_token_ip'];
239
				}
240
				else
241
				{
242
					$row['user_agent'] = Api\Header\UserAgent::osBrowser($row['access_token_user_agent']);
243
					$row['user_ip'] = $row['access_token_ip'];
244
				}
245
			}
246
		}
247
		return $ret;
248
	}
249
250
	/**
251
	 * Get actions for tokens
252
	 */
253
	protected function tokenActions()
254
	{
255
		return [
256
			'delete' => array(
257
				'caption' => 'Revoke',
258
				'allowOnMultiple' => true,
259
				'confirm' => 'Revoke this token',
260
			),
261
		];
262
	}
263
264
	/**
265
	 * Generate QRCode and optional new secret
266
	 *
267
	 * @param Google2FA $google2fa
268
	 * @param boolean|null $generate =null null: generate new qrCode/secret, if none exists
269
	 *  true: allways generate new qrCode (to reset existing one)
270
	 *  false: use existing secret, but generate qrCode
271
	 * @return array with keys "qrc" and "cred_id"
272
	 */
273
	protected function generateQRCode(Google2FA $google2fa, $generate=null)
274
	{
275
		$creds = Credentials::read(0, Credentials::TWOFA, $GLOBALS['egw_info']['user']['account_id']);
276
277
		if (!$generate && $creds && strlen($creds['2fa_password']) >= 16)
0 ignored issues
show
Bug Best Practice introduced by
The expression $generate of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
278
		{
279
			$secret_key = $creds['2fa_password'];
280
		}
281
		else
282
		{
283
			$secret_key = $google2fa->generateSecretKey();//16, $GLOBALS['egw_info']['user']['account_lid']);
284
		}
285
		$qrc = '';
286
		if (isset($generate) || empty($creds))
287
		{
288
			$image = $google2fa->getQRCodeInline(
289
				!empty($GLOBALS['egw_info']['server']['site_title']) ?
290
					$GLOBALS['egw_info']['server']['site_title'] : 'EGroupware',
291
				$GLOBALS['egw_info']['user']['account_email'],
292
				$secret_key
293
			);
294
			$qrc = 'data:image/'.(substr($image, 0, 5) === '<?xml' ? 'svg+xml' : 'png').
295
				';base64,'.base64_encode($image);
296
		}
297
		return [
298
			'qrc' => $qrc,
299
			'hide_qrc' => empty($qrc),
300
			'cred_id' => !empty($creds) ? $creds['2fa_cred_id'] : null,
301
			'secret_key' => $secret_key,
302
			'status' => !empty($creds) ? lang('Two Factor Auth is already setup.') : '',
303
		];
304
	}
305
306
	/**
307
	 * Do some basic checks and then change password
308
	 *
309
	 * @param string $old_passwd
310
	 * @param string $new_passwd
311
	 * @param string $new_passwd2
312
	 * @return array with already translated errors
313
	 */
314
	public static function do_change($old_passwd, $new_passwd, $new_passwd2)
315
	{
316
		if ($GLOBALS['egw_info']['flags']['currentapp'] != 'preferences')
317
		{
318
			Api\Translation::add_app('preferences');
319
		}
320
		$errors = array();
321
322
		if (isset($GLOBALS['egw_info']['user']['passwd']) &&
323
			$old_passwd !== $GLOBALS['egw_info']['user']['passwd'])
324
		{
325
			$errors[] = lang('The old password is not correct');
326
		}
327
		if ($new_passwd != $new_passwd2)
328
		{
329
			$errors[] = lang('The two passwords are not the same');
330
		}
331
332
		if ($old_passwd !== false && $old_passwd == $new_passwd)
333
		{
334
			$errors[] = lang('Old password and new password are the same. This is invalid. You must enter a new password');
335
		}
336
337
		if (!$new_passwd)
338
		{
339
			$errors[] = lang('You must enter a password');
340
		}
341
342
		// allow auth backends or configured password strenght to throw exceptions and display there message
343
		if (!$errors)
344
		{
345
			try {
346
				if (!$GLOBALS['egw']->auth->change_password($old_passwd, $new_passwd,
347
					$GLOBALS['egw']->session->account_id))
348
				{
349
					// if we have no specific error, add general message
350
					$errors[] = lang('Failed to change password.');
351
				}
352
			}
353
			catch (Exception $e) {
354
				$errors[] = $e->getMessage();
355
			}
356
		}
357
		return $errors;
358
	}
359
}
360