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
|
|||||
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
|
|||||
136 | } |
||||
137 | if (($response = file_get_contents(self::KEYSERVER_ADD, false, stream_context_create(array( |
||||
0 ignored issues
–
show
|
|||||
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
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. ![]() |
|||||
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 |
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.