Issues (4868)

addressbook/inc/class.addressbook_bo.inc.php (4 issues)

Severity
1
<?php
2
/**
3
 * EGroupware addressbook: Contacts
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Cornelius Weiss <[email protected]>
7
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8
 * @author Joerg Lehrke <[email protected]>
9
 * @package addressbook
10
 * @copyright (c) 2005-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
11
 * @copyright (c) 2005/6 by Cornelius Weiss <[email protected]>
12
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
13
 * @version $Id$
14
 */
15
16
use EGroupware\Api;
17
use EGroupware\Api\Acl;
18
19
/**
20
 * Business object for addressbook
21
 *
22
 * Currently this only contains PGP stuff, which needs to be called via Ajax
23
 */
24
class addressbook_bo extends Api\Contacts
25
{
26
	static public $pgp_key_regexp = '/-----BEGIN PGP PUBLIC KEY BLOCK-----.*-----END PGP PUBLIC KEY BLOCK-----\r?\n?/s';
27
28
	/**
29
	 * Search addressbook for PGP public keys of given recipients
30
	 *
31
	 * EMail addresses are lowercased to make search case-insensitive
32
	 *
33
	 * @param string|int|array $recipients (array of) email addresses or numeric account-ids
34
	 * @return array email|account_id => key pairs
35
	 */
36
	public function get_pgp_keys($recipients)
37
	{
38
		return $this->get_keys($recipients, true);
39
	}
40
41
	/**
42
	 * Keyserver URL and CA to verify ssl connection
43
	 */
44
	const KEYSERVER = 'https://hkps.pool.sks-keyservers.net/pks/lookup?op=get&exact=on&search=';
45
	const KEYSERVER_CA = '/addressbook/doc/sks-keyservers.netCA.pem';
46
47
	/**
48
	 * Search keyserver for PGP public keys
49
	 *
50
	 * @param int|string|array $recipients (array of) email addresses or numeric account-ids
51
	 * @param array $result =array()
52
	 */
53
	public static function get_pgp_keyserver($recipients, array $result=array())
54
	{
55
		foreach($recipients as $recipient)
56
		{
57
			$id = $recipient;
58
			if (is_numeric($recipient))
59
			{
60
				$recipient = $GLOBALS['egw']->accounts->id2name($recipient, 'account_email');
61
			}
62
			$matches = null;
63
			if (($response = file_get_contents(self::KEYSERVER.urlencode($recipient), false, stream_context_create(array(
64
					'ssl' => array(
65
						'verify_peer' => true,
66
						'cafile' => EGW_SERVER_ROOT.self::KEYSERVER_CA,
67
					)
68
				)))) && preg_match(self::$pgp_key_regexp, $response, $matches))
69
			{
70
				$result[$id] = $matches[0];
71
			}
72
		}
73
		return $result;
74
	}
75
76
	/**
77
	 * Search addressbook for PGP public keys of given recipients
78
	 *
79
	 * EMail addresses are lowercased to make search case-insensitive
80
	 *
81
	 * @param string|int|array $recipients (array of) email addresses or numeric account-ids
82
	 * @return array email|account_id => key pairs
83
	 */
84
	public function ajax_get_pgp_keys($recipients)
85
	{
86
		if (!$recipients) return array();
87
88
		if (!is_array($recipients)) $recipients = array($recipients);
89
90
		$result = $this->get_pgp_keys($recipients);
91
92
		if (($missing = array_diff($recipients, array_keys($result))))
93
		{
94
			$result = self::get_pgp_keyserver($missing, $result);
95
		}
96
		//error_log(__METHOD__."(".array2string($recipients).") returning ".array2string($result));
97
		Api\Json\Response::get()->data($result);
98
	}
99
100
	/**
101
	 * Set PGP keys for given email or account_id, if user has necessary rights
102
	 *
103
	 * @param array $keys email|account_id => public key pairs to store
104
	 * @param boolean $allow_user_updates =null for admins, set config to allow regular users to store their pgp key
105
	 * @return int number of pgp keys stored
106
	 */
107
	public function ajax_set_pgp_keys($keys, $allow_user_updates=null)
108
	{
109
		$message = $this->set_keys($keys, true, $allow_user_updates);
110
		// add all keys to public keyserver too
111
		$message .= "\n".lang('%1 key(s) added to public keyserver "%2".',
112
			self::set_pgp_keyserver($keys), PARSE_URL(self::KEYSERVER_ADD, PHP_URL_HOST));
0 ignored issues
show
The call to lang() has too many arguments starting with self::set_pgp_keyserver($keys). ( Ignorable by Annotation )

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

112
		$message .= "\n"./** @scrutinizer ignore-call */ lang('%1 key(s) added to public keyserver "%2".',

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
113
114
		Api\Json\Response::get()->data($message);
115
	}
116
117
	/**
118
	 * Keyserver add URL
119
	 */
120
	const KEYSERVER_ADD = 'https://hkps.pool.sks-keyservers.net/pks/add';
121
122
	/**
123
	 * Upload PGP keys to public keyserver
124
	 *
125
	 * @param array $keys email|account_id => public key pairs to store
126
	 * @return int number of pgp keys stored
127
	 */
128
	public static function set_pgp_keyserver($keys)
129
	{
130
		$added = 0;
131
		foreach($keys as $email => $cert)
132
		{
133
			if (is_numeric($email))
134
			{
135
				$email = $GLOBALS['egw']->accounts->id2name($email, 'account_email');
0 ignored issues
show
The assignment to $email is dead and can be removed.
Loading history...
136
			}
137
			if (($response = file_get_contents(self::KEYSERVER_ADD, false, stream_context_create(array(
0 ignored issues
show
The assignment to $response is dead and can be removed.
Loading history...
138
					'ssl' => array(
139
						'verify_peer' => true,
140
						'cafile' => EGW_SERVER_ROOT.self::KEYSERVER_CA,
141
					),
142
					'http' => array(
143
						'header'  => "Content-type: text/plain",
144
						'method'  => 'POST',
145
						'content' => http_build_query(array(
146
							'keytext' => $cert,
147
						)),
148
					),
149
				)))))
150
			{
151
				$added++;
152
			}
153
		}
154
		return $added;
155
	}
156
157
	/**
158
	 * Where to store public key depending on type and storage backend
159
	 *
160
	 * @param boolean $pgp true: PGP, false: S/Mime
161
	 * @param array $contact =null contact array to pass to get_backend()
162
	 * @return boolean true: store as file, false: store with contact
163
	 */
164
	public function pubkey_use_file($pgp, array $contact=null)
165
	{
166
		return $pgp || empty($contact) || get_class($this->get_backend($contact)) == 'EGroupware\\Api\\Contacts\\Sql';
167
	}
168
169
	/**
170
	 * Set keys for given email or account_id and key type based on regexp (SMIME or PGP), if user has necessary rights
171
	 *
172
	 * @param array $keys email|account_id => public key pairs to store
173
	 * @param boolean $pgp true: PGP, false: S/Mime
174
	 * @param boolean $allow_user_updates = null for admins, set config to allow regular users to store their key
175
	 *
176
	 * @return string message of the update operation result
177
	 */
178
	public function set_keys ($keys, $pgp, $allow_user_updates = null)
179
	{
180
		if (isset($allow_user_updates) && isset($GLOBALS['egw_info']['user']['apps']['admin']))
181
		{
182
			$update = false;
183
			if ($allow_user_updates && !in_array('pubkey', $this->own_account_acl))
184
			{
185
				$this->own_account_acl[] = 'pubkey';
186
				$update = true;
187
			}
188
			elseif (!$allow_user_updates && ($key = array_search('pubkey', $this->own_account_acl)) !== false)
189
			{
190
				unset($this->own_account_acl[$key]);
191
				$update = true;
192
			}
193
			if ($update)
194
			{
195
				Config::save_value('own_account_acl', $this->own_account_acl, 'phpgwapi');
196
			}
197
		}
198
199
		$key_regexp = $pgp ? self::$pgp_key_regexp : Api\Mail\Smime::$certificate_regexp;
200
		$file = $pgp ? Api\Contacts::FILES_PGP_PUBKEY : Api\Contacts::FILES_SMIME_PUBKEY;
201
202
		$criteria = array();
203
		foreach($keys as $recipient => $key)
204
		{
205
			if (!preg_match($key_regexp, $key))
206
			{
207
				return lang('File is not a %1 public key!', $pgp ? lang('PGP') : lang('S/MIME'));
0 ignored issues
show
The call to lang() has too many arguments starting with $pgp ? lang('PGP') : lang('S/MIME'). ( Ignorable by Annotation )

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

207
				return /** @scrutinizer ignore-call */ lang('File is not a %1 public key!', $pgp ? lang('PGP') : lang('S/MIME'));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
208
			}
209
210
			if (is_numeric($recipient))
211
			{
212
				$criteria['egw_addressbook.account_id'][] = (int)$recipient;
213
			}
214
			else
215
			{
216
				$criteria['contact_email'][] = $recipient;
217
			}
218
		}
219
		if (!$criteria) return 0;
220
221
		$updated = 0;
222
		$filters = array(null);
223
		// if accounts-backend is NOT SQL, we need to search the accounts separate
224
		if ($this->so_accounts)
225
		{
226
			$filters[] = array('owner' => '0');
227
		}
228
		foreach($filters as $filter)
229
		{
230
			foreach((array)$this->search($criteria, false, '', '', '', false, 'OR', false, $filter) as $contact)
231
			{
232
				if ($contact['account_id'] && isset($keys[$contact['account_id']]))
233
				{
234
					$key = $keys[$contact['account_id']];
235
				}
236
				elseif (isset($keys[$contact['email']]))
237
				{
238
					$key = $keys[$contact['email']];
239
				}
240
241
				// key is stored in file for sql backend or allways for pgp key
242
				$path = null;
243
				if ($contact['id'] && $this->pubkey_use_file($pgp, $contact))
244
				{
245
					$path =  Api\Link::vfs_path('addressbook', $contact['id'], $file);
246
					$contact['files'] |= $pgp ? self::FILES_BIT_PGP_PUBKEY : self::FILES_BIT_SMIME_PUBKEY;
247
					// remove evtl. existing old pubkey
248
					if (preg_match($key_regexp, $contact['pubkey']))
249
					{
250
						$contact['pubkey'] = preg_replace($key_regexp, '', $contact['pubkey']);
251
					}
252
					$updated++;
253
				}
254
				elseif (empty($contact['pubkey']) || !preg_match($key_regexp, $contact['pubkey']))
255
				{
256
					$contact['pubkey'] .= $key;
257
				}
258
				else
259
				{
260
					$contact['pubkey'] = preg_replace($key_regexp, $key, $contact['pubkey']);
261
				}
262
				$contact['photo_unchanged'] = true;	// otherwise photo will be lost, because $contact['jpegphoto'] is not set
263
				if ($this->check_perms(Acl::EDIT, $contact) && $this->save($contact))
264
				{
265
					if ($path)
266
					{
267
						// check_perms && save check ACL, in case of access only via own-account we have to use root to allow the update
268
						$backup = Api\Vfs::$is_root; Api\Vfs::$is_root = true;
269
						if (file_put_contents($path, $key)) ++$updated;
270
						Api\Vfs::$is_root = $backup;
271
					}
272
					else
273
					{
274
						++$updated;
275
					}
276
				}
277
			}
278
		}
279
		if ($criteria == array('egw.addressbook.account_id' => array((int)$GLOBALS['egw_info']['user']['account_id'])))
280
		{
281
			$message = !$updated ? lang('Permissiong denied! Ask your administrator to allow regular uses to update their public keys.') :
282
				lang('Your new public key has been stored in accounts addressbook.');
283
		}
284
		else
285
		{
286
			$message = !$updated ? false: lang('%1 public keys added.', $updated);
287
		}
288
		return $message;
289
	}
290
291
	/**
292
	 * Search addressbook for keys of given recipients
293
	 *
294
	 * EMail addresses are lowercased to make search case-insensitive
295
	 *
296
	 * @param string|int|array $recipients (array of) email addresses or numeric account-ids or "contact:$id" for contacts by id
297
	 * @param boolean $pgp true: PGP, false: S/Mime public keys
298
	 * @return array email|account_id => key pairs
299
	 */
300
	protected function get_keys ($recipients, $pgp)
301
	{
302
		if (!$recipients) return array();
303
304
		if (!is_array($recipients)) $recipients = array($recipients);
305
306
		$criteria = $result = array();
307
		foreach($recipients as &$recipient)
308
		{
309
			if (is_numeric($recipient))
310
			{
311
				$criteria['egw_addressbook.account_id'][] = (int)$recipient;
312
			}
313
			else
314
			{
315
				$criteria['contact_email_home'][] = $criteria['contact_email'][] = $recipient = strtolower($recipient);
316
			}
317
		}
318
		$filters = array(null);
319
		// if accounts-backend is NOT SQL, we need to search the accounts separate
320
		if ($this->so_accounts)
321
		{
322
			$filters[] = array('owner' => '0');
323
		}
324
		foreach ($filters as $filter)
325
		{
326
			foreach((array)$this->search($criteria, array('account_id', 'contact_email', 'contact_email_home', 'contact_pubkey', 'contact_id'),
327
				'', '', '', false, 'OR', false, $filter) as $contact)
328
			{
329
				// first check for file and second for pubkey field (LDAP, AD or old SQL)
330
				if (($content = $this->get_key($contact, $pgp)))
331
				{
332
					$contact['email'] = strtolower($contact['email']);
333
					if (empty($criteria['account_id']) || in_array($contact['email'], $recipients))
334
					{
335
						if (in_array($contact['email_home'], $recipients))
336
						{
337
							$result[$contact['email_home']] = $content;
338
						}
339
						else
340
						{
341
							$result[$contact['email']] = $content;
342
						}
343
					}
344
					else
345
					{
346
						$result[$contact['account_id']] = $content;
347
					}
348
				}
349
			}
350
		}
351
		return $result;
352
	}
353
354
	/**
355
	 * Extract PGP or S/Mime pubkey from contact array
356
	 *
357
	 * @param array $contact
358
	 * @param boolean $pgp
359
	 * @return string pubkey or NULL
360
	 */
361
	function get_key(array $contact, $pgp)
362
	{
363
		if ($pgp)
364
		{
365
			$key_regexp = self::$pgp_key_regexp;
366
			$file = Api\Contacts::FILES_PGP_PUBKEY;
367
		}
368
		else
369
		{
370
			$key_regexp = Api\Mail\Smime::$certificate_regexp;
371
			$file = Api\Contacts::FILES_SMIME_PUBKEY;
372
		}
373
		$matches = null;
374
		if (file_exists($path = Api\Link::vfs_path('addressbook', $contact['id'], $file)) &&
375
			($content = file_get_contents($path)) &&
376
			preg_match($key_regexp, $content, $matches) ||
377
			preg_match($key_regexp, $contact['pubkey'], $matches))
378
		{
379
			return $matches[0];
380
		}
381
		return null;
382
	}
383
384
	/**
385
	 * Search addressbook for SMIME Certificate keys of given recipients
386
	 *
387
	 * EMail addresses are lowercased to make search case-insensitive
388
	 *
389
	 * @param string|int|array $recipients (array of) email addresses or numeric account-ids
390
	 * @return array email|account_id => key pairs
391
	 */
392
	public function get_smime_keys($recipients)
393
	{
394
		return $this->get_keys($recipients, false);
395
	}
396
397
	/**
398
	 * Set SMIME keys for given email or account_id, if user has necessary rights
399
	 *
400
	 * @param array $keys email|account_id => public key pairs to store
401
	 * @param boolean $allow_user_updates =null for admins, set config to allow regular users to store their smime key
402
	 *
403
	 * @return string message of the update operation result
404
	 */
405
	public function set_smime_keys($keys, $allow_user_updates=null)
406
	{
407
		return $this->set_keys($keys, false, $allow_user_updates);
408
	}
409
410
	/**
411
	 * Saves contact
412
	 *
413
	 * Reimplemented to strip pubkeys pasted into pubkey field or imported and store them as files in Vfs.
414
	 * We allways store PGP pubkeys to Vfs, but S/Mime ones only for SQL backend, not for LDAP or AD.
415
	 *
416
	 * @param array &$contact contact array from etemplate::exec
417
	 * @param boolean $ignore_acl =false should the acl be checked or not
418
	 * @param boolean $touch_modified =true should modified/r be updated
419
	 * @return int|string|boolean id on success, false on failure, the error-message is in $this->error
420
	 */
421
	function save(&$contact, $ignore_acl=false, $touch_modified=true)
422
	{
423
		if (($id = parent::save($contact, $ignore_acl, $touch_modified)) && !empty($contact['pubkey']))
424
		{
425
			$files = 0;
426
			foreach(array(
427
				array(addressbook_bo::$pgp_key_regexp, Api\Contacts::FILES_PGP_PUBKEY, Api\Contacts::FILES_BIT_PGP_PUBKEY),
428
				array(Api\Mail\Smime::$certificate_regexp, Api\Contacts::FILES_SMIME_PUBKEY, Api\Contacts::FILES_BIT_SMIME_PUBKEY),
429
			) as $data)
430
			{
431
				list($regexp, $file, $bit) = $data;
432
				$matches = null;
433
				if (!empty($contact['pubkey']) && preg_match($regexp, $contact['pubkey'], $matches) &&
434
					// check if we store that pubkey as file (PGP allways, but S/Mime only for SQL backend, not for LDAP or AD!)
435
					$this->pubkey_use_file($bit === Api\Contacts::FILES_BIT_PGP_PUBKEY, $contact))
436
				{
437
					// check_perms && save check ACL, in case of access only via own-account we have to use root to allow the update
438
					$backup = Api\Vfs::$is_root; Api\Vfs::$is_root = true;
439
					if (file_put_contents(Api\Link::vfs_path('addressbook', $id, $file), $matches[0]))
440
					{
441
						$files |= $bit;
442
						$contact['pubkey'] = str_replace($matches[0], '', $contact['pubkey']);
443
					}
444
					Api\Vfs::$is_root = $backup;
445
				}
446
			}
447
			// if we stripped a pubkey / stored it as file --> remove it from DB
448
			if ($files)
449
			{
450
				if (!trim($contact['pubkey'])) $contact['pubkey'] = null;
451
				$contact['files'] |= $files;
452
				parent::save($contact, $ignore_acl, $touch_modified);
453
			}
454
		}
455
		return $id;
456
	}
457
}
458