Udm::call()   F
last analyzed

Complexity

Conditions 25
Paths 864

Size

Total Lines 94
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 25
eloc 58
c 3
b 0
f 0
nc 864
nop 7
dl 0
loc 94
rs 0.1888

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 support for Univention UDM REST Api
4
 *
5
 * @link https://www.egroupware.org
6
 * @author Ralf Becker <[email protected]>
7
 *
8
 * @link https://www.univention.com/blog-en/2019/07/udm-rest-api-beta-version-released/
9
 *
10
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
11
 * @package api
12
 * @subpackage accounts
13
 */
14
15
namespace EGroupware\Api\Accounts\Univention;
16
17
use EGroupware\Api;
18
19
/**
20
 * Univention UDM REST Api
21
 *
22
 * @todo Use just UDM instead of still calling ldap/parent
23
 */
24
class Udm
25
{
26
	/**
27
	 * Config to use
28
	 *
29
	 * @var array $config
30
	 */
31
	protected $config;
32
33
	/**
34
	 * Hostname of master, derived from ldap_host
35
	 *
36
	 * @var string
37
	 */
38
	protected $host;
39
40
	/**
41
	 * Username, derived from ldap_root_dn
42
	 *
43
	 * @var string
44
	 */
45
	protected $user;
46
47
	/**
48
	 * Udm url prefix, prepend to relative path like 'users/user'
49
	 */
50
	const PREFIX = '/univention/udm/';
51
52
	/**
53
	 * Log webservice-calls to error_log
54
	 */
55
	const DEBUG = false;
56
57
	/**
58
	 * Constructor
59
	 *
60
	 * @param array $config =null config to use, default $GLOBALS['egw_info']['server']
61
	 * @throws Api\Exception\WrongParameter for missing LDAP config
62
	 */
63
	public function __construct(array $config=null)
64
	{
65
		$this->config = isset($config) ? $config : $GLOBALS['egw_info']['server'];
66
67
		$this->host = parse_url($this->config['ldap_host'], PHP_URL_HOST);
68
		if (empty($this->host))
69
		{
70
			throw new Api\Exception\WrongParameter ("Univention needs 'ldap_host' configured!");
71
		}
72
		$matches = null;
73
		if (!preg_match('/^(cn|uid)=([^,]+),/i', $this->config['ldap_root_dn'], $matches))
74
		{
75
			throw new Api\Exception\WrongParameter ("Univention needs 'ldap_rood_dn' configured!");
76
		}
77
		$this->user = $matches[2];
78
	}
79
80
	/**
81
	 * Call UDM REST Api
82
	 *
83
	 * @param string $_path path to call, if relative PREFIX is prepended eg. 'users/user'
84
	 * @param string $_method ='GET'
85
	 * @param array $_payload =[] payload to send
86
	 * @param array& $headers =[] on return response headers
87
	 * @param string $if_match =null etag for If-Match header
88
	 * @param boolean $return_dn =false return DN of Location header
89
	 * @param int $retry =1 >0 retry on connection-error only
90
	 * @return array|string decoded JSON or DN for $return_DN === true
91
	 * @throws UdmCantConnect for connection errors or JSON decoding errors
92
	 * @throws UdmError for returned JSON error object
93
	 * @throws UdmMissingLocation for missing Location header with DN ($return_dn === true)
94
	 */
95
	protected function call($_path, $_method='GET', array $_payload=[], &$headers=[], $if_match=null, $return_dn=false, $retry=1)
96
	{
97
		$curl = curl_init();
98
99
		// fix error like: Request argument "policies" is not a "dict" (PHP encodes empty arrays as array, not object)
100
		if (array_key_exists('policies', $_payload) && empty($_payload['policies']))
101
		{
102
			$_payload['policies'] = new \stdClass();	// force "policies": {}
103
		}
104
		if (is_array($_payload['properties']) && array_key_exists('umcProperty', $_payload['properties']) && empty($_payload['properties']['umcProperty']))
105
		{
106
			$_payload['properties']['umcProperty'] = new \stdClass();	// force "umcProperty": {}
107
		}
108
109
		$curlOpts = [
110
			CURLOPT_URL => 'https://'.$this->host.($_path[0] !== '/' ? self::PREFIX : '').$_path,
111
			CURLOPT_USERPWD => $this->user.':'.$this->config['ldap_root_pw'],
112
			//CURLOPT_SSL_VERIFYHOST => 2,	// 0: to disable certificate check
113
			CURLOPT_HTTPHEADER => [
114
				'Accept: application/json',
115
			],
116
			CURLOPT_CUSTOMREQUEST => $_method,
117
			CURLOPT_RETURNTRANSFER => 1,
118
			//CURLOPT_FOLLOWLOCATION => 1,
119
			CURLOPT_TIMEOUT => 30,	// setting a timeout of 30 seconds, as recommended by Univention
120
			CURLOPT_VERBOSE => 1,
121
			CURLOPT_HEADER => 1,
122
		];
123
		if (isset($if_match))
124
		{
125
			$curlOpts[CURLOPT_HTTPHEADER][] = 'If-Match: '.$if_match;
126
		}
127
		switch($_method)
128
		{
129
			case 'PUT':
130
			case 'POST':
131
				$curlOpts[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json';
132
				$curlOpts[CURLOPT_POSTFIELDS] = json_encode($_payload, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
133
				break;
134
135
			case 'GET':
136
			default:
137
				if ($_payload)
138
				{
139
					$curlOpts[CURLOPT_URL] .= '?'. http_build_query($_payload);
140
				}
141
				break;
142
		}
143
		curl_setopt_array($curl, $curlOpts);
144
		$response = curl_exec($curl);
145
146
		$header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
147
		$headers = self::getHeaders(substr($response, 0, $header_size));
148
		$body = substr($response, $header_size);
149
150
		$path = urldecode($_path);	// for nicer error-messages
151
		if ($response === false || $body !== '' && !($json = json_decode($body, true)) && json_last_error())
152
		{
153
			$info = curl_getinfo($curl);
154
			curl_close($curl);
155
			if ($retry > 0)
156
			{
157
				error_log(__METHOD__."($path, $_method, ...) failed, retrying in 100ms, returned $body, headers=".json_encode($headers, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).", curl_getinfo()=".json_encode($info, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE));
158
				usleep(100000);
159
				return $this->call($_path, $_method, $_payload, $headers, $if_match, $return_dn, --$retry);
160
			}
161
			error_log(__METHOD__."($path, $_method, ...) returned $body, headers=".json_encode($headers, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).", curl_getinfo()=".json_encode($info, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE));
162
			error_log(__METHOD__."($path, $_method, ".json_encode($_payload, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).")");
163
			throw new UdmCantConnect("Error contacting Univention UDM REST Api ($path)".($response !== false ? ': '.json_last_error() : ''));
164
		}
165
		curl_close($curl);
166
		// error in json or non 20x http status
167
		if (!empty($json['error']) || !preg_match('|^HTTP/[0-9.]+ 20|', $headers[0]))
168
		{
169
			error_log(__METHOD__."($path, $_method, ...) returned $response, headers=".json_encode($headers, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE));
170
			error_log(__METHOD__."($path, $_method, ".json_encode($_payload, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).")");
171
			throw new UdmError("UDM REST Api ($path): ".(empty($json['error']['message']) ? $headers[0] : $json['error']['message']), $json['error']['code']);
172
		}
173
		if (self::DEBUG)
174
		{
175
			error_log(__METHOD__."($path, $_method, ...) returned $response, headers=".json_encode($headers, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE));
176
			error_log(__METHOD__."($path, $_method, ".json_encode($_payload, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).")");
177
		}
178
179
		if ($return_dn)
180
		{
181
			$matches = null;
182
			if (!isset($headers['location']) || !preg_match('|/([^/]+)$|', $headers['location'], $matches))
183
			{
184
				throw new UdmMissingLocation("UDM REST Api ($path) did not return Location header!");
185
			}
186
			return urldecode($matches[1]);
187
		}
188
		return $json;
189
	}
190
191
	/**
192
	 * Convert header string in array of headers
193
	 *
194
	 * A "HTTP/1.1 100 Continue" is NOT returned!
195
	 *
196
	 * @param string $head
197
	 * @return array with name => value pairs, 0: http-status, value can be an array for multiple headers with same name
198
	 */
199
	protected static function getHeaders($head)
200
	{
201
		$headers = [];
202
		foreach(explode("\r\n", $head) as $header)
203
		{
204
			if (empty($header)) continue;
205
206
			$parts = explode(':', $header, 2);
207
			if (count($parts) < 2)
208
			{
209
				$headers[0] = $header;	// http-status
210
			}
211
			else
212
			{
213
				$name = strtolower($parts[0]);
214
				if (!isset($headers[$name]))
215
				{
216
					$headers[$name] = trim($parts[1]);
217
				}
218
				else
219
				{
220
					if (!is_array($headers[$name]))
221
					{
222
						$headers[$name] = [$headers[$name]];
223
					}
224
					$headers[$name][] = trim($parts[1]);
225
				}
226
			}
227
		}
228
		if (self::DEBUG) error_log(__METHOD__."(\$head) returning ".json_encode($headers));
229
		return $headers;
230
	}
231
232
	/**
233
	 * Create a user
234
	 *
235
	 * @param array $data
236
	 * @throws Exception on error-message
237
	 * @return string with DN of new user
238
	 */
239
	public function createUser(array $data)
240
	{
241
		// set default values
242
		$payload = $this->user2udm($data, $this->call('users/user/add'));
243
244
		$payload['superordinate'] = null;
245
		$payload['position'] = $this->config['ldap_context'];
246
247
		$headers = [];
248
		return $this->call('users/user/', 'POST', $payload, $headers, null, true);
249
	}
250
251
	/**
252
	 * Update a user
253
	 *
254
	 * @param string $dn dn of user to update
255
	 * @param array $data
256
	 * @return string with dn
257
	 * @throws Exception on error-message
258
	 */
259
	public function updateUser($dn, array $data)
260
	{
261
		// set existing values
262
		$get_headers = [];
263
		$payload = $this->user2udm($data, $this->call('users/user/'.urlencode($dn), 'GET', [], $get_headers));
264
265
		$headers = [];
266
		return $this->call('users/user/'.urlencode($dn), 'PUT', $payload, $headers, $get_headers['etag'], true);
267
	}
268
269
	/**
270
	 * Copy EGroupware user-values to UDM ones
271
	 *
272
	 * @param array $data
273
	 * @param array $payload
274
	 * @return array with updated payload
275
	 */
276
	protected function user2udm(array $data, array $payload)
277
	{
278
		// gives error: The property passwordexpiry has an invalid value: Value may not change.
279
		unset($payload['properties']['passwordexpiry']);
280
281
		foreach([
282
			'account_lid' => 'username',
283
			'account_passwd' => 'password',
284
			'account_lastname' => 'lastname',
285
			'account_firstname' => 'firstname',
286
			'account_id' => ['uidNumber', 'sambaRID'],
287
			'account_email' => 'mailPrimaryAddress',
288
			'mustchangepassword' => 'pwdChangeNextLogin',
289
		] as $egw => $names)
290
		{
291
			if (!empty($data[$egw]))
292
			{
293
				foreach((array)$names as $name)
294
				{
295
					if (!array_key_exists($name, $payload['properties']))
296
					{
297
						throw new \Exception ("No '$name' in properties: ".json_encode($payload['properties']));
298
					}
299
					$payload['properties'][$name] = $data[$egw];
300
				}
301
			}
302
		}
303
304
		if (!empty($data['account_email']))
305
		{
306
			// we need to set mailHomeServer, so mailbox gets created for Dovecot
307
			// get_default() does not work for Adminstrator, try acc_id=1 instead
308
			// if everything fails try ldap host / master ...
309
			try {
310
				if (!($account = Api\Mail\Account::get_default(false, false, false)))
311
				{
312
					$account = Api\Mail\Account::read(1);
313
				}
314
				$hostname = $account->acc_imap_host;
315
			}
316
			catch(\Exception $e) {
317
				unset($e);
318
			}
319
			if (empty($hostname)) $hostname = $this->host;
320
			$payload['properties']['mailHomeServer'] = $hostname;
321
		}
322
323
		return $payload;
324
	}
325
326
	/**
327
	 * Create a group
328
	 *
329
	 * @param array $data
330
	 * @throws Exception on error-message
331
	 * @return string with DN of new user
332
	 */
333
	public function createGroup(array $data)
334
	{
335
		// set default values
336
		$payload = $this->group2udm($data, $this->call('groups/group/add'));
337
338
		$payload['superordinate'] = null;
339
		$payload['position'] = empty($this->config['ldap_group_context']) ? $this->config['ldap_context'] : $this->config['ldap_group_context'];
340
341
		$headers = [];
342
		return $this->call('groups/group/', 'POST', $payload, $headers, null, true);
343
	}
344
345
	/**
346
	 * Update a group
347
	 *
348
	 * @param string $dn dn of group to update
349
	 * @param array $data
350
	 * @throws Exception on error-message
351
	 * @return string with DN of new user
352
	 */
353
	public function updateGroup($dn, array $data)
354
	{
355
		// set existing values
356
		$get_headers = [];
357
		$payload = $this->group2udm($data, $this->call('groups/group/'.urlencode($dn), 'GET', [], $get_headers));
358
359
		$headers = [];
360
		return $this->call('groups/group/'.urlencode($dn), 'PUT', $payload, $headers, $get_headers['etag'], true);
361
	}
362
363
	/**
364
	 * Copy EGroupware group values to UDM ones
365
	 *
366
	 * @param array $data
367
	 * @param array $payload
368
	 * @return array with updated payload
369
	 */
370
	protected function group2udm(array $data, array $payload)
371
	{
372
		foreach([
373
			'account_lid' => 'name',
374
			'account_id' => 'gidNumber',
375
		] as $egw => $names)
376
		{
377
			if (!empty($data[$egw]))
378
			{
379
				foreach((array)$names as $name)
380
				{
381
					if (!array_key_exists($name, $payload['properties']))
382
					{
383
						throw new \Exception ("No '$name' in properties: ".json_encode($payload['properties']));
384
					}
385
					// our account_id is negative for groups!
386
					$payload['properties'][$name] = $egw === 'account_id' ? abs($data[$egw]) : $data[$egw];
387
				}
388
			}
389
		}
390
391
		return $payload;
392
	}
393
}