1: <?php
2: /**
3: * The IMP_Crypt_Pgp:: class contains all functions related to handling
4: * PGP messages within IMP.
5: *
6: * Copyright 2002-2012 Horde LLC (http://www.horde.org/)
7: *
8: * See the enclosed file COPYING for license information (GPL). If you
9: * did not receive this file, see http://www.horde.org/licenses/gpl.
10: *
11: * @author Michael Slusarz <slusarz@horde.org>
12: * @category Horde
13: * @license http://www.horde.org/licenses/gpl GPL
14: * @package IMP
15: */
16: class IMP_Crypt_Pgp extends Horde_Crypt_Pgp
17: {
18: /* Name of PGP public key field in addressbook. */
19: const PUBKEY_FIELD = 'pgpPublicKey';
20:
21: /* Encryption type constants. */
22: const ENCRYPT = 'pgp_encrypt';
23: const SIGN = 'pgp_sign';
24: const SIGNENC = 'pgp_signenc';
25: const SYM_ENCRYPT = 'pgp_sym_enc';
26: const SYM_SIGNENC = 'pgp_syn_sign';
27:
28: /**
29: * Return the list of available encryption options for composing.
30: *
31: * @return array Keys are encryption type constants, values are gettext
32: * strings describing the encryption type.
33: */
34: public function encryptList()
35: {
36: $ret = array(
37: self::ENCRYPT => _("PGP Encrypt Message")
38: );
39:
40: if ($this->getPersonalPrivateKey()) {
41: $ret += array(
42: self::SIGN => _("PGP Sign Message"),
43: self::SIGNENC => _("PGP Sign/Encrypt Message")
44: );
45: }
46:
47: return $ret + array(
48: self::SYM_ENCRYPT => _("PGP Encrypt Message with passphrase"),
49: self::SYM_SIGNENC => _("PGP Sign/Encrypt Message with passphrase")
50: );
51: }
52:
53: /**
54: * Generate the personal Public/Private keypair and store in prefs.
55: *
56: * @param string $realname See Horde_Crypt_Pgp::
57: * @param string $email See Horde_Crypt_Pgp::
58: * @param string $passphrase See Horde_Crypt_Pgp::
59: * @param string $comment See Horde_Crypt_Pgp::
60: * @param string $keylength See Horde_Crypt_Pgp::
61: *
62: * @throws Horde_Crypt_Exception
63: */
64: public function generatePersonalKeys($name, $email, $passphrase,
65: $comment = '', $keylength = 1024)
66: {
67: $keys = $this->generateKey($name, $email, $passphrase, $comment, $keylength);
68:
69: /* Store the keys in the user's preferences. */
70: $this->addPersonalPublicKey($keys['public']);
71: $this->addPersonalPrivateKey($keys['private']);
72: }
73:
74: /**
75: * Add the personal public key to the prefs.
76: *
77: * @param mixed $public_key The public key to add (either string or
78: * array).
79: */
80: public function addPersonalPublicKey($public_key)
81: {
82: $GLOBALS['prefs']->setValue('pgp_public_key', (is_array($public_key)) ? implode('', $public_key) : $public_key);
83: }
84:
85: /**
86: * Add the personal private key to the prefs.
87: *
88: * @param mixed $private_key The private key to add (either string or
89: * array).
90: */
91: public function addPersonalPrivateKey($private_key)
92: {
93: $GLOBALS['prefs']->setValue('pgp_private_key', (is_array($private_key)) ? implode('', $private_key) : $private_key);
94: }
95:
96: /**
97: * Get the personal public key from the prefs.
98: *
99: * @return string The personal PGP public key.
100: */
101: public function getPersonalPublicKey()
102: {
103: return $GLOBALS['prefs']->getValue('pgp_public_key');
104: }
105:
106: /**
107: * Get the personal private key from the prefs.
108: *
109: * @return string The personal PGP private key.
110: */
111: public function getPersonalPrivateKey()
112: {
113: return $GLOBALS['prefs']->getValue('pgp_private_key');
114: }
115:
116: /**
117: * Deletes the specified personal keys from the prefs.
118: */
119: public function deletePersonalKeys()
120: {
121: $GLOBALS['prefs']->setValue('pgp_public_key', '');
122: $GLOBALS['prefs']->setValue('pgp_private_key', '');
123:
124: $this->unsetPassphrase('personal');
125: }
126:
127: /**
128: * Add a public key to an address book.
129: *
130: * @param string $public_key An PGP public key.
131: *
132: * @return array See Horde_Crypt_Pgp::pgpPacketInformation()
133: * @throws Horde_Crypt_Exception
134: * @throws Horde_Exception
135: */
136: public function addPublicKey($public_key)
137: {
138: /* Make sure the key is valid. */
139: $key_info = $this->pgpPacketInformation($public_key);
140: if (!isset($key_info['signature'])) {
141: throw new Horde_Crypt_Exception(_("Not a valid public key."));
142: }
143:
144: /* Remove the '_SIGNATURE' entry. */
145: unset($key_info['signature']['_SIGNATURE']);
146:
147: /* Store all signatures that appear in the key. */
148: foreach ($key_info['signature'] as $id => $sig) {
149: /* Check to make sure the key does not already exist in ANY
150: * address book and remove the id from the key_info for a correct
151: * output. */
152: try {
153: $result = $this->getPublicKey($sig['email'], array('nocache' => true, 'noserver' => true));
154: if (!empty($result)) {
155: unset($key_info['signature'][$id]);
156: continue;
157: }
158: } catch (Horde_Crypt_Exception $e) {}
159:
160: /* Add key to the user's address book. */
161: $GLOBALS['registry']->call('contacts/addField', array($sig['email'], $sig['name'], self::PUBKEY_FIELD, $public_key, $GLOBALS['prefs']->getValue('add_source')));
162: }
163:
164: return $key_info;
165: }
166:
167: /**
168: * Retrieves a public key by e-mail.
169: *
170: * First, the key will be attempted to be retrieved from a user's address
171: * book(s).
172: * Second, if unsuccessful, the key is attempted to be retrieved via a
173: * public PGP keyserver.
174: *
175: * @param string $address The e-mail address to search by.
176: * @param array $options Additional options:
177: * <pre>
178: * 'keyid' - (string) The key ID of the user's key.
179: * DEFAULT: key ID not used
180: * 'nocache' - (boolean) Don't retrieve from cache?
181: * DEFAULT: false
182: * 'noserver' - (boolean) Whether to check the public key servers for the
183: * key.
184: * DEFAULT: false
185: * </pre>
186: *
187: * @return string The PGP public key requested.
188: * @throws Horde_Crypt_Exception
189: */
190: public function getPublicKey($address, $options = array())
191: {
192: $keyid = empty($options['keyid'])
193: ? ''
194: : $options['keyid'];
195:
196: /* If there is a cache driver configured, try to get the public key
197: * from the cache. */
198: if (empty($options['nocache']) && ($cache = $GLOBALS['injector']->getInstance('Horde_Cache'))) {
199: $result = $cache->get("PGPpublicKey_" . $address . $keyid, 3600);
200: if ($result) {
201: Horde::logMessage('PGPpublicKey: ' . serialize($result), 'DEBUG');
202: return $result;
203: }
204: }
205:
206: try {
207: $key = Horde::callHook('pgp_key', array($address, $keyid), 'imp');
208: if ($key) {
209: return $key;
210: }
211: } catch (Horde_Exception_HookNotSet $e) {}
212:
213: /* Try retrieving by e-mail only first. */
214: $params = IMP::getAddressbookSearchParams();
215: $result = null;
216: try {
217: $result = $GLOBALS['registry']->call('contacts/getField', array($address, self::PUBKEY_FIELD, $params['sources'], true, true));
218: } catch (Horde_Exception $e) {}
219:
220: if (is_null($result)) {
221: /* TODO: Retrieve by ID. */
222:
223: /* See if the address points to the user's public key. */
224: $identity = $GLOBALS['injector']->getInstance('IMP_Identity');
225: $personal_pubkey = $this->getPersonalPublicKey();
226: if (!empty($personal_pubkey) && $identity->hasAddress($address)) {
227: $result = $personal_pubkey;
228: } elseif (empty($options['noserver'])) {
229: try {
230: $result = $this->getFromPublicKeyserver($keyid, $address);
231:
232: /* If there is a cache driver configured and a cache
233: * object exists, store the retrieved public key in the
234: * cache. */
235: if (is_object($cache)) {
236: $cache->set("PGPpublicKey_" . $address . $keyid, $result, 3600);
237: }
238: } catch (Horde_Crypt_Exception $e) {
239: /* Return now, if no public key found at all. */
240: Horde::logMessage('PGPpublicKey: ' . $e->getMessage(), 'DEBUG');
241: throw new Horde_Crypt_Exception(sprintf(_("Could not retrieve public key for %s."), $address));
242: }
243: } else {
244: $result = '';
245: }
246: }
247:
248: /* If more than one public key is returned, just return the first in
249: * the array. There is no way of knowing which is the "preferred" key,
250: * if the keys are different. */
251: if (is_array($result)) {
252: reset($result);
253: }
254:
255: return $result;
256: }
257:
258: /**
259: * Retrieves all public keys from a user's address book(s).
260: *
261: * @return array All PGP public keys available.
262: * @throws Horde_Crypt_Exception
263: */
264: public function listPublicKeys()
265: {
266: $params = IMP::getAddressbookSearchParams();
267: if (empty($params['sources'])) {
268: return array();
269: }
270:
271: return $GLOBALS['registry']->call('contacts/getAllAttributeValues', array(self::PUBKEY_FIELD, $params['sources']));
272: }
273:
274: /**
275: * Deletes a public key from a user's address book(s) by e-mail.
276: *
277: * @param string $email The e-mail address to delete.
278: *
279: * @throws Horde_Crypt_Exception
280: */
281: public function deletePublicKey($email)
282: {
283: $params = IMP::getAddressbookSearchParams();
284: return $GLOBALS['registry']->call('contacts/deleteField', array($email, self::PUBKEY_FIELD, $params['sources']));
285: }
286:
287: /**
288: * Get a public key via a public PGP keyserver.
289: *
290: * @param string $keyid The key ID of the requested key.
291: * @param string $address The email address of the requested key.
292: *
293: * @return string See Horde_Crypt_Pgp::getPublicKeyserver()
294: * @throws Horde_Crypt_Exception
295: */
296: public function getFromPublicKeyserver($keyid, $address = null)
297: {
298: return $this->_keyserverConnect($keyid, 'get', $address);
299: }
300:
301: /**
302: * Send a public key to a public PGP keyserver.
303: *
304: * @param string $pubkey The PGP public key.
305: *
306: * @return string See Horde_Crypt_Pgp::putPublicKeyserver()
307: * @throws Horde_Crypt_Exception
308: */
309: public function sendToPublicKeyserver($pubkey)
310: {
311: return $this->_keyserverConnect($pubkey, 'put');
312: }
313:
314: /**
315: * Connect to the keyservers
316: *
317: * @param string $data The data to send to the keyserver.
318: * @param string $method The method to use - either 'get' or 'put'.
319: * @param string $additional Any additional data.
320: *
321: * @return string See Horde_Crypt_Pgp::getPublicKeyserver() -or-
322: * Horde_Crypt_Pgp::putPublicKeyserver().
323: * @throws Horde_Crypt_Exception
324: */
325: protected function _keyserverConnect($data, $method, $additional = null)
326: {
327: global $conf;
328:
329: if (empty($conf['gnupg']['keyserver'])) {
330: throw new Horde_Crypt_Exception(_("Public PGP keyserver support has been disabled."));
331: }
332:
333: $timeout = empty($conf['gnupg']['timeout'])
334: ? Horde_Crypt_Pgp::KEYSERVER_TIMEOUT
335: : $conf['gnupg']['timeout'];
336:
337: if ($method == 'put') {
338: return $this->putPublicKeyserver($data, $conf['gnupg']['keyserver'][0], $timeout);
339: }
340:
341: foreach ($conf['gnupg']['keyserver'] as $server) {
342: try {
343: return $this->getPublicKeyserver($data, $server, $timeout, $additional);
344: } catch (Horde_Crypt_Exception $e) {}
345: }
346: throw new Horde_Crypt_Exception(_("Could not connect to public PGP keyserver"));
347: }
348:
349: /**
350: * Verifies a signed message with a given public key.
351: *
352: * @param string $text The text to verify.
353: * @param string $address E-mail address of public key.
354: * @param string $signature A PGP signature block.
355: * @param string $charset Charset to use.
356: *
357: * @return stdClass See Horde_Crypt_Pgp::decrypt().
358: * @throws Horde_Crypt_Exception
359: */
360: public function verifySignature($text, $address, $signature = '',
361: $charset = null)
362: {
363: if (!empty($signature)) {
364: $packet_info = $this->pgpPacketInformation($signature);
365: if (isset($packet_info['keyid'])) {
366: $keyid = $packet_info['keyid'];
367: }
368: }
369:
370: if (!isset($keyid)) {
371: $keyid = $this->getSignersKeyID($text);
372: }
373:
374: /* Get key ID of key. */
375: $public_key = $this->getPublicKey($address, array('keyid' => $keyid));
376:
377: if (empty($signature)) {
378: $options = array('type' => 'signature');
379: } else {
380: $options = array('type' => 'detached-signature', 'signature' => $signature);
381: }
382: $options['pubkey'] = $public_key;
383:
384: if (!empty($charset)) {
385: $options['charset'] = $charset;
386: }
387:
388: return $this->decrypt($text, $options);
389: }
390:
391: /**
392: * Decrypt a message with user's public/private keypair or a passphrase.
393: *
394: * @param string $text The text to decrypt.
395: * @param string $type Either 'literal', 'personal', or
396: * 'symmetric'.
397: * @param boolean $passphrase If $type is 'personal' or 'symmetrical',
398: * the passphrase to use.
399: *
400: * @return stdClass See Horde_Crypt_Pgp::decrypt().
401: * @throws Horde_Crypt_Exception
402: */
403: public function decryptMessage($text, $type, $passphrase = null)
404: {
405: switch ($type) {
406: case 'literal':
407: return $this->decrypt($text, array('type' => 'message', 'no_passphrase' => true));
408: break;
409:
410: case 'symmetric':
411: return $this->decrypt($text, array('type' => 'message', 'passphrase' => $passphrase));
412: break;
413:
414: case 'personal':
415: return $this->decrypt($text, array('type' => 'message', 'pubkey' => $this->getPersonalPublicKey(), 'privkey' => $this->getPersonalPrivateKey(), 'passphrase' => $passphrase));
416: }
417: }
418:
419: /**
420: * Gets a passphrase from the session cache.
421: *
422: * @param integer $type The type of passphrase. Either 'personal' or
423: * 'symmetric'.
424: * @param string $id If $type is 'symmetric', the ID of the stored
425: * passphrase.
426: *
427: * @return mixed The passphrase, if set, or null.
428: */
429: public function getPassphrase($type, $id = null)
430: {
431: if ($type == 'personal') {
432: $id = 'personal';
433: }
434:
435: if (!($cache = $GLOBALS['session']->get('imp', 'pgp')) ||
436: !isset($cache[$type][$id])) {
437: return null;
438: }
439:
440: $secret = $GLOBALS['injector']->getInstance('Horde_Secret');
441: return $secret->read($secret->getKey('imp'), $cache[$type][$id]);
442: }
443:
444: /**
445: * Store's the user's passphrase in the session cache.
446: *
447: * @param integer $type The type of passphrase. Either 'personal' or
448: * 'symmetric'.
449: * @param string $passphrase The user's passphrase.
450: * @param string $id If $type is 'symmetric', the ID of the
451: * stored passphrase.
452: *
453: * @return boolean Returns true if correct passphrase, false if incorrect.
454: */
455: public function storePassphrase($type, $passphrase, $id = null)
456: {
457: if ($type == 'personal') {
458: if ($this->verifyPassphrase($this->getPersonalPublicKey(), $this->getPersonalPrivateKey(), $passphrase) === false) {
459: return false;
460: }
461: $id = 'personal';
462: }
463:
464: $secret = $GLOBALS['injector']->getInstance('Horde_Secret');
465:
466: $cache = $GLOBALS['session']->get('imp', 'pgp', Horde_Session::TYPE_ARRAY);
467: $cache[$type][$id] = $secret->write($secret->getKey('imp'), $passphrase);
468: $GLOBALS['session']->set('imp', 'pgp', $cache);
469:
470: return true;
471: }
472:
473: /**
474: * Clear the passphrase from the session cache.
475: *
476: * @param integer $type The type of passphrase. Either 'personal' or
477: * 'symmetric'.
478: * @param string $id If $type is 'symmetric', the ID of the
479: * stored passphrase. Else, all passphrases
480: * are deleted.
481: */
482: public function unsetPassphrase($type, $id = null)
483: {
484: if ($cache = $GLOBALS['session']->get('imp', 'pgp')) {
485: if (($type == 'symmetric') && !is_null($id)) {
486: unset($cache['symmetric'][$id]);
487: } else {
488: unset($cache[$type]);
489: }
490: $GLOBALS['session']->set('imp', 'pgp', $cache);
491: }
492: }
493:
494: /**
495: * Generates a cache ID for symmetric message data.
496: *
497: * @param string $mailbox The mailbox of the message.
498: * @param integer $uid The UID of the message.
499: * @param string $id The MIME ID of the message.
500: *
501: * @return string A unique symmetric cache ID.
502: */
503: public function getSymmetricId($mailbox, $uid, $id)
504: {
505: return implode('|', array($mailbox, $uid, $id));
506: }
507:
508: /**
509: * Generates the javascript code for saving public keys.
510: *
511: * @param string $mailbox The mailbox of the message.
512: * @param integer $uid The UID of the message.
513: * @param string $id The MIME ID of the message.
514: *
515: * @return string The URL for saving public keys.
516: */
517: public function savePublicKeyUrl($mailbox, $uid, $id)
518: {
519: $params = array(
520: 'actionID' => 'save_attachment_public_key',
521: 'mailbox' => $mailbox,
522: 'uid' => $uid,
523: 'mime_id' => $id
524: );
525: return Horde::popupJs(Horde::url('pgp.php'), array('params' => $params, 'height' => 200, 'width' => 450));
526: }
527:
528: /**
529: * Provide the list of parameters needed for signing a message.
530: *
531: * @return array The list of parameters needed by encrypt().
532: */
533: protected function _signParameters()
534: {
535: return array(
536: 'pubkey' => $this->getPersonalPublicKey(),
537: 'privkey' => $this->getPersonalPrivateKey(),
538: 'passphrase' => $this->getPassphrase('personal')
539: );
540: }
541:
542: /**
543: * Provide the list of parameters needed for encrypting a message.
544: *
545: * @param array $addresses The e-mail address of the keys to use for
546: * encryption.
547: * @param string $symmetric If true, the symmetric password to use for
548: * encrypting. If null, uses the personal key.
549: *
550: * @return array The list of parameters needed by encrypt().
551: * @throws Horde_Crypt_Exception
552: */
553: protected function _encryptParameters($addresses, $symmetric)
554: {
555: if (!is_null($symmetric)) {
556: return array(
557: 'symmetric' => true,
558: 'passphrase' => $symmetric
559: );
560: }
561:
562: $addr_list = array();
563:
564: foreach ($addresses as $val) {
565: $addrOb = Horde_Mime_Address::bareAddress($val, $GLOBALS['session']->get('imp', 'maildomain'), true);
566: $key_addr = array_pop($addrOb);
567:
568: /* Get the public key for the address. */
569: $addr_list[$key_addr] = $this->getPublicKey($key_addr);
570: }
571:
572: return array('recips' => $addr_list);
573: }
574:
575: /**
576: * Sign a Horde_Mime_Part using PGP using IMP default parameters.
577: *
578: * @param Horde_Mime_Part $mime_part The object to sign.
579: *
580: * @return Horde_Mime_Part See Horde_Crypt_Pgp::signMIMEPart().
581: * @throws Horde_Crypt_Exception
582: */
583: public function impSignMimePart($mime_part)
584: {
585: return $this->signMimePart($mime_part, $this->_signParameters());
586: }
587:
588: /**
589: * Encrypt a Horde_Mime_Part using PGP using IMP default parameters.
590: *
591: * @param Horde_Mime_Part $mime_part The object to encrypt.
592: * @param array $addresses The e-mail address of the keys to
593: * use for encryption.
594: * @param string $symmetric If true, the symmetric password to
595: * use for encrypting. If null, uses
596: * the personal key.
597: *
598: * @return Horde_Mime_Part See Horde_Crypt_Pgp::encryptMimePart().
599: * @throws Horde_Crypt_Exception
600: */
601: public function impEncryptMimePart($mime_part, $addresses,
602: $symmetric = null)
603: {
604: return $this->encryptMimePart($mime_part, $this->_encryptParameters($addresses, $symmetric));
605: }
606:
607: /**
608: * Sign and Encrypt a Horde_Mime_Part using PGP using IMP default
609: * parameters.
610: *
611: * @param Horde_Mime_Part $mime_part The object to sign and encrypt.
612: * @param array $addresses The e-mail address of the keys to
613: * use for encryption.
614: * @param string $symmetric If true, the symmetric password to
615: * use for encrypting. If null, uses
616: * the personal key.
617: *
618: * @return Horde_Mime_Part See Horde_Crypt_Pgp::signAndencryptMimePart().
619: * @throws Horde_Crypt_Exception
620: */
621: public function impSignAndEncryptMimePart($mime_part, $addresses,
622: $symmetric = null)
623: {
624: return $this->signAndEncryptMimePart($mime_part, $this->_signParameters(), $this->_encryptParameters($addresses, $symmetric));
625: }
626:
627: /**
628: * Generate a Horde_Mime_Part object, in accordance with RFC 2015/3156,
629: * that contains the user's public key.
630: *
631: * @return Horde_Mime_Part See Horde_Crypt_Pgp::publicKeyMimePart().
632: */
633: public function publicKeyMimePart()
634: {
635: return parent::publicKeyMimePart($this->getPersonalPublicKey());
636: }
637:
638: /* UI related functions. */
639:
640: /**
641: * Print PGP Key information.
642: *
643: * @param string $key The PGP key.
644: */
645: public function printKeyInfo($key = '')
646: {
647: try {
648: $key_info = $this->pgpPrettyKey($key);
649: } catch (Horde_Crypt_Exception $e) {
650: Horde::logMessage($e, 'INFO');
651: $key_info = $e->getMessage();
652: }
653:
654: $this->textWindowOutput('PGP Key Information', $key_info);
655: }
656:
657: /**
658: * Output text in a window.
659: *
660: * @param string $name The window name.
661: * @param string $msg The text contents.
662: */
663: public function textWindowOutput($name, $msg)
664: {
665: $GLOBALS['browser']->downloadHeaders($name, 'text/plain; charset=' . 'UTF-8', true, strlen($msg));
666: echo $msg;
667: }
668:
669: /**
670: * Generate import key dialog.
671: *
672: * @param string $target Action ID for the UI screen.
673: * @param string $reload The reload cache value.
674: */
675: public function importKeyDialog($target, $reload)
676: {
677: $title = _("Import PGP Key");
678: require IMP_TEMPLATES . '/common-header.inc';
679:
680: /* Need to use regular status notification - AJAX notifications won't
681: * show in popup windows. */
682: if (IMP::getViewMode() == 'dimp') {
683: $GLOBALS['notification']->detach('status');
684: $GLOBALS['notification']->attach('status');
685: }
686: IMP::status();
687:
688: $t = $GLOBALS['injector']->createInstance('Horde_Template');
689: $t->setOption('gettext', true);
690: $t->set('selfurl', Horde::url('pgp.php'));
691: $t->set('broken_mp_form', $GLOBALS['browser']->hasQuirk('broken_multipart_form'));
692: $t->set('reload', htmlspecialchars($reload));
693: $t->set('target', $target);
694: $t->set('forminput', Horde_Util::formInput());
695: $t->set('import_public_key', $target == 'process_import_public_key');
696: $t->set('import_personal_public_key', $target == 'process_import_personal_public_key');
697: $t->set('import_personal_private_key', $target == 'process_import_personal_private_key');
698: echo $t->fetch(IMP_TEMPLATES . '/pgp/import_key.html');
699: }
700:
701: /**
702: * Attempt to import a key from form/uploaded data.
703: *
704: * @param string $key Key string.
705: *
706: * @return string The key contents.
707: * @throws Horde_Browser_Exception
708: */
709: public function getImportKey($key)
710: {
711: if (!empty($key)) {
712: return $key;
713: }
714:
715: $GLOBALS['browser']->wasFileUploaded('upload_key', _("key"));
716: return file_get_contents($_FILES['upload_key']['tmp_name']);
717: }
718:
719: /**
720: * Reload the window.
721: *
722: * @param string $reload The reload cache value.
723: */
724: public function reloadWindow($reload)
725: {
726: global $session;
727:
728: $href = $session->retrieve($reload);
729: $session->purge($reload);
730:
731: echo Horde::wrapInlineScript(array(
732: 'opener.focus();'.
733: 'opener.location.href="' . $href . '";',
734: 'window.close();'
735: ));
736: }
737:
738: }
739: