preferences_password::change()   F
last analyzed

Complexity

Conditions 40
Paths 6784

Size

Total Lines 186
Code Lines 103

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 40
eloc 103
c 1
b 1
f 0
nc 6784
nop 1
dl 0
loc 186
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: Security and passwords
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
17
/**
18
 * Security and passwords
19
 *
20
 * Other apps can add tabs to this popup by implementing the "preferences_security" hook
21
 * like eg. the OpenID App does to allow users to revoke tokens.
22
 */
23
class preferences_password
24
{
25
	var $public_functions = array(
26
		'change' => True
27
	);
28
	const GAUTH_ANDROID = 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2';
29
	const GAUTH_IOS = 'https://appstore.com/googleauthenticator';
30
31
	/**
32
	 * Change password, two factor auth or revoke tokens
33
	 *
34
	 * @param type $content
35
	 */
36
	function change($content = null)
37
	{
38
		$GLOBALS['egw_info']['flags']['app_header'] = lang('Security & Password');
39
		$tmpl = new Etemplate('preferences.password');
40
41
		$readonlys = $sel_options = [];
42
		try {
43
			// PHP 7.1+: using SVG image backend (requiring XMLWriter) and not ImageMagic extension
44
			if (class_exists('BaconQrCode\Renderer\Image\SvgImageBackEnd'))
45
			{
46
				$image_backend = new \BaconQrCode\Renderer\Image\SvgImageBackEnd;
0 ignored issues
show
Bug introduced by
The type BaconQrCode\Renderer\Image\SvgImageBackEnd 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...
47
			}
48
			$google2fa = new Google2FA($image_backend);
49
			$prefs = new Api\Preferences($GLOBALS['egw_info']['user']['account_id']);
50
			$prefs->read_repository();
51
52
			if (!is_array($content))
0 ignored issues
show
introduced by
The condition is_array($content) is always false.
Loading history...
53
			{
54
				$content = [];
55
				$content['2fa'] = $this->generateQRCode($google2fa)+[
56
					'gauth_android' => self::GAUTH_ANDROID,
57
					'gauth_ios' => self::GAUTH_IOS,
58
				];
59
			}
60
			else
61
			{
62
				$secret_key = $content['2fa']['secret_key'];
63
				unset($content['2fa']['secret_key']);
64
65
				// check user password for everything but password change, where it will be checked anyway
66
				$auth = new Api\Auth();
67
				if ($content['tabs'] !== 'change_password' &&
68
					!$auth->authenticate($GLOBALS['egw_info']['user']['account_lid'], $content['password']))
69
				{
70
					$tmpl->set_validation_error('password', lang('Password is invalid'));
71
				}
72
				else
73
				{
74
					switch($content['tabs'])
75
					{
76
						case 'change_password':
77
							if (!$GLOBALS['egw']->acl->check('nopasswordchange', 1) && $content['button']['save'])
78
							{
79
								if (($errors = self::do_change($content['password'], $content['n_passwd'], $content['n_passwd_2'])))
80
								{
81
									Framework::message(implode("\n", $errors), 'error');
82
								}
83
								else
84
								{
85
									Framework::refresh_opener(lang('Password changed'), 'preferences');
86
									Framework::window_close();
87
								}
88
							}
89
							break;
90
91
						case 'two_factor_auth':
92
							switch(key($content['2fa']['action']))
93
							{
94
								case 'show':
95
									$content['2fa'] = $this->generateQRCode($google2fa, false);
96
									break;
97
								case 'reset':
98
									$content['2fa'] = $this->generateQRCode($google2fa, true);
99
									Framework::message(lang('New secret generated, you need to save it to disable the old one!'));
100
									break;
101
								case 'disable':
102
									if (Credentials::delete(0, $GLOBALS['egw_info']['user']['account_id'], Credentials::TWOFA))
103
									{
104
										Framework::refresh_opener(lang('Secret deleted, two factor authentication disabled.'), 'preferences');
105
										Framework::window_close();
106
									}
107
									else
108
									{
109
										Framework::message(lang('Failed to delete secret!'), 'error');
110
									}
111
									break;
112
								default:	// no action, save secret
113
									if (!$google2fa->verifyKey($secret_key, $content['2fa']['code']))
114
									{
115
										$tmpl->set_validation_error('code', lang('Code is invalid'), '2fa');
116
										break 2;
117
									}
118
									if (($content['2fa']['cred_id'] = Credentials::write(0,
119
										$GLOBALS['egw_info']['user']['account_lid'],
120
										$secret_key, Credentials::TWOFA,
121
										$GLOBALS['egw_info']['user']['account_id'],
122
										$content['2fa']['cred_id'])))
123
									{
124
										Framework::refresh_opener(lang('Two Factor Auth enabled.'), 'preferences');
125
										Framework::window_close();
126
									}
127
									else
128
									{
129
										Framework::message(lang('Failed to store secret!'), 'error');
130
									}
131
									break;
132
							}
133
							unset($content['2fa']['action']);
134
							break;
135
136
						default:
137
							// for other tabs call their save_callback (user password is already checked!)
138
							if (!empty($content['save_callbacks'][$content['tabs']]) &&
139
								($msg = call_user_func_array($content['save_callbacks'][$content['tabs']], [&$content])))
140
							{
141
								Framework::message($msg, 'success');
142
							}
143
							break;
144
					}
145
				}
146
			}
147
		}
148
		catch (Exception $e) {
149
			Framework::message($e->getMessage(), 'error');
150
		}
151
152
		// disable 2FA tab, if admin disabled it
153
		if ($GLOBALS['egw_info']['server']['2fa_required'] === 'disabled')
154
		{
155
			$readonlys['tabs']['two_factor_auth'] = true;
156
		}
157
158
		// disable password change, if user has not right to change it
159
		if ($GLOBALS['egw']->acl->check('nopasswordchange', 1))
160
		{
161
			$readonlys['tabs']['change_password'] = true;
162
		}
163
164
		$preserve = [
165
			'2fa' => $content['2fa']+[
166
				'secret_key' => $secret_key,
167
			]
168
		];
169
170
		$tmpl->setElementAttribute('tabs', 'add_tabs', true);
171
		$tabs =& $tmpl->getElementAttribute('tabs', 'tabs');
172
		if (($first_call = !isset($tabs)))
173
		{
174
			$tabs = array();
175
		}
176
		// register hooks, if openid is available, but new hook not yet registered (should be removed after 19.1)
177
		if (!empty($GLOBALS['egw_info']['apps']['openid']) && !Api\Hooks::implemented('preferences_security'))
178
		{
179
			Api\Hooks::read(true);
180
		}
181
		$hook_data = Api\Hooks::process(array('location' => 'preferences_security')+$content, ['openid'], true);
182
		foreach($hook_data as $extra_tabs)
183
		{
184
			if (!$extra_tabs) continue;
185
186
			foreach(isset($extra_tabs[0]) ? $extra_tabs : [$extra_tabs] as $extra_tab)
187
			{
188
				if (!empty($extra_tab['data']) && is_array($extra_tab['data']))
189
				{
190
					$content = array_merge($content, $extra_tab['data']);
191
				}
192
				if (!empty($extra_tab['preserve']) && is_array($extra_tab['preserve']))
193
				{
194
					$preserve = array_merge($preserve, $extra_tab['preserve']);
195
				}
196
				if (!empty($extra_tab['sel_options']) && is_array($extra_tab['sel_options']))
197
				{
198
					$sel_options = array_merge($sel_options, $extra_tab['sel_options']);
199
				}
200
				if (!empty($extra_tab['readonlys']) && is_array($extra_tab['readonlys']))
201
				{
202
					$readonlys = array_merge($readonlys, $extra_tab['readonlys']);
203
				}
204
				if (!empty($extra_tab['save_callback']))
205
				{
206
					$preserve['save_callbacks'][$extra_tab['name']] = $extra_tab['save_callback'];
207
				}
208
				// we must NOT add tabs more then once!
209
				if ($first_call && !empty($extra_tab['label']) && !empty($extra_tab['name']))
210
				{
211
					$tabs[] = array(
212
						'label' =>	$extra_tab['label'],
213
						'template' =>	$extra_tab['name'],
214
						'prepend' => $extra_tab['prepend'],
215
					);
216
				}
217
				//error_log(__METHOD__."() changed tabs=".array2string($tabs));
218
			}
219
		}
220
221
		$tmpl->exec('preferences.preferences_password.change', $content, $sel_options, $readonlys, $preserve, 2);
222
	}
223
224
	/**
225
	 * Generate QRCode and optional new secret
226
	 *
227
	 * @param Google2FA $google2fa
228
	 * @param boolean|null $generate =null null: generate new qrCode/secret, if none exists
229
	 *  true: allways generate new qrCode (to reset existing one)
230
	 *  false: use existing secret, but generate qrCode
231
	 * @return array with keys "qrc" and "cred_id"
232
	 */
233
	protected function generateQRCode(Google2FA $google2fa, $generate=null)
234
	{
235
		$creds = Credentials::read(0, Credentials::TWOFA, $GLOBALS['egw_info']['user']['account_id']);
236
237
		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...
238
		{
239
			$secret_key = $creds['2fa_password'];
240
		}
241
		else
242
		{
243
			$secret_key = $google2fa->generateSecretKey();//16, $GLOBALS['egw_info']['user']['account_lid']);
244
		}
245
		if (isset($generate) || empty($creds))
246
		{
247
			$image = $google2fa->getQRCodeInline(
248
				!empty($GLOBALS['egw_info']['server']['site_title']) ?
249
					$GLOBALS['egw_info']['server']['site_title'] : 'EGroupware',
250
				$GLOBALS['egw_info']['user']['account_email'],
251
				$secret_key
252
			);
253
			// bacon/bacon-qr-code >= 2 does not generate a data-url itself, but 1.x does :(
254
			if (substr($image, 0, 11) !== 'data:image/')
255
			{
256
				$image = 'data:image/'.(substr($image, 0, 5) === '<?xml' ? 'svg+xml' : 'png').
257
					';base64,'.base64_encode($image);
258
			}
259
		}
260
		return [
261
			'qrc' => $image,
262
			'hide_qrc' => empty($image),
263
			'cred_id' => !empty($creds) ? $creds['2fa_cred_id'] : null,
264
			'secret_key' => $secret_key,
265
			'status' => !empty($creds) ? lang('Two Factor Auth is already setup.') : '',
266
		];
267
	}
268
269
	/**
270
	 * Do some basic checks and then change password
271
	 *
272
	 * @param string $old_passwd
273
	 * @param string $new_passwd
274
	 * @param string $new_passwd2
275
	 * @return array with already translated errors
276
	 */
277
	public static function do_change($old_passwd, $new_passwd, $new_passwd2)
278
	{
279
		if ($GLOBALS['egw_info']['flags']['currentapp'] != 'preferences')
280
		{
281
			Api\Translation::add_app('preferences');
282
		}
283
		$errors = array();
284
285
		if (isset($GLOBALS['egw_info']['user']['passwd']) &&
286
			$old_passwd !== $GLOBALS['egw_info']['user']['passwd'])
287
		{
288
			$errors[] = lang('The old password is not correct');
289
		}
290
		if ($new_passwd != $new_passwd2)
291
		{
292
			$errors[] = lang('The two passwords are not the same');
293
		}
294
295
		if ($old_passwd !== false && $old_passwd == $new_passwd)
296
		{
297
			$errors[] = lang('Old password and new password are the same. This is invalid. You must enter a new password');
298
		}
299
300
		if (!$new_passwd)
301
		{
302
			$errors[] = lang('You must enter a password');
303
		}
304
305
		// allow auth backends or configured password strenght to throw exceptions and display there message
306
		if (!$errors)
307
		{
308
			try {
309
				if (!$GLOBALS['egw']->auth->change_password($old_passwd, $new_passwd,
310
					$GLOBALS['egw']->session->account_id))
311
				{
312
					// if we have no specific error, add general message
313
					$errors[] = lang('Failed to change password.');
314
				}
315
			}
316
			catch (Exception $e) {
317
				$errors[] = $e->getMessage();
318
			}
319
		}
320
		return $errors;
321
	}
322
}
323